src/features/teams/members.tsx
此代码来自构建时的仓库快照。修改源码后重新构建即可更新。
'use client';
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
Alert,
App,
Button,
Card,
Form,
Input,
Popconfirm,
Select,
Table,
Tag,
} from 'antd';
import { useTRPC } from '@/trpc/client';
import { useTeam } from './team-workspace';
import { memberRoles, roleLabels, type MemberRole } from '@/shared/team';
const roles = memberRoles.map((value) => ({ value, label: roleLabels[value] }));
export function Members() {
const team = useTeam();
const trpc = useTRPC();
const cache = useQueryClient();
const { message } = App.useApp();
const [form] = Form.useForm();
const [roleTarget, setRoleTarget] = useState<{
userId: string;
role: MemberRole;
}>();
const members = useQuery(trpc.team.members.queryOptions({ teamId: team.id }));
const refresh = async () => {
await Promise.all([
cache.invalidateQueries({ queryKey: trpc.ticket.events.queryKey() }),
cache.invalidateQueries({ queryKey: trpc.ticket.detail.queryKey() }),
cache.invalidateQueries({ queryKey: trpc.ticket.list.queryKey() }),
cache.invalidateQueries({
queryKey: trpc.team.members.queryKey({ teamId: team.id }),
}),
cache.invalidateQueries({ queryKey: trpc.team.list.queryKey() }),
]);
};
const add = useMutation(
trpc.team.addMember.mutationOptions({
async onSuccess() {
form.resetFields();
await refresh();
void message.success('成员已添加');
},
onError(error) {
void message.error(error.message);
},
}),
);
const change = useMutation(
trpc.team.changeRole.mutationOptions({
async onSuccess() {
setRoleTarget(undefined);
await refresh();
void message.success('角色已更新');
},
onError(error) {
void message.error(error.message);
},
}),
);
const remove = useMutation(
trpc.team.removeMember.mutationOptions({
async onSuccess() {
await refresh();
void message.success('成员已移除');
},
onError(error) {
void message.error(error.message);
},
}),
);
const busy = add.isPending || change.isPending || remove.isPending;
return (
<main className="mx-auto max-w-5xl px-5 py-10">
<h1 className="mb-2 text-3xl font-semibold">{team.name} · 团队成员</h1>
<p className="mb-6 text-slate-500">
管理员管理成员;客服处理团队工单;客户只能查看自己提交的工单。
</p>
{team.role === 'ADMIN' && (
<Card title="添加已注册成员" className="mb-6">
<Form
form={form}
layout="inline"
initialValues={{ role: 'CUSTOMER' }}
onFinish={(values: { email: string; role: MemberRole }) =>
add.mutate({ ...values, teamId: team.id })
}
disabled={busy}
>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, type: 'email', message: '请输入注册邮箱' },
]}
>
<Input placeholder="对方已注册的邮箱" />
</Form.Item>
<Form.Item name="role" label="角色">
<Select className="min-w-28" options={roles} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={add.isPending}>
添加成员
</Button>
</Form>
<p className="mt-4 text-sm text-slate-500">
当前直接添加已注册账号,不发送邮件邀请。
</p>
</Card>
)}
{members.isError && (
<Alert
className="mb-4"
type="error"
title={members.error.message}
action={<Button onClick={() => void members.refetch()}>重试</Button>}
/>
)}
<Card>
<Table
rowKey="userId"
loading={members.isPending}
dataSource={members.data ?? []}
pagination={false}
scroll={{ x: 600 }}
columns={[
{
title: '姓名',
dataIndex: 'name',
render: (name, item) => (
<>
{name}
{item.userId === team.userId && (
<Tag className="ml-2">我</Tag>
)}
</>
),
},
{ title: '邮箱', dataIndex: 'email' },
{
title: '角色',
render: (_, item) =>
team.role === 'ADMIN' ? (
<Select
aria-label={`${item.email} 的角色`}
disabled={busy}
value={item.role}
options={roles}
onChange={(role) =>
setRoleTarget({ userId: item.userId, role })
}
/>
) : (
roleLabels[item.role]
),
},
...(team.role === 'ADMIN'
? [
{
title: '操作',
render: (_: unknown, item: { userId: string }) => (
<Popconfirm
title="确定移除这位成员?"
description="移除后无法继续访问本团队工单。"
onConfirm={() =>
remove.mutate({
teamId: team.id,
userId: item.userId,
})
}
>
<Button danger disabled={busy}>
移除
</Button>
</Popconfirm>
),
},
]
: []),
]}
/>
</Card>
{roleTarget && (
<Card className="mt-4" title="确认角色变更">
<p className="mb-3">
将成员角色修改为「{roleLabels[roleTarget.role]}
」?权限会在后续请求中生效。
</p>
<Button
type="primary"
loading={change.isPending}
onClick={() => change.mutate({ teamId: team.id, ...roleTarget })}
>
确认修改
</Button>
<Button
className="ml-2"
disabled={change.isPending}
onClick={() => setRoleTarget(undefined)}
>
取消
</Button>
</Card>
)}
</main>
);
}