TTeamDesk 学习手册

src/features/teams/members.tsx

此代码来自构建时的仓库快照。修改源码后重新构建即可更新。

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>
  );
}

搜索教程

输入关键词,搜索全部章节。

阅读设置

外观
正文字号

从一张工单出发,读懂数据如何流转。

设置与进度仅保存在当前浏览器。