src/features/teams/team-workspace.tsx
此代码来自构建时的仓库快照。修改源码后重新构建即可更新。
'use client';
import { createContext, useContext, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useQueryState, parseAsString } from 'nuqs';
import { DraftProvider } from '@/features/tickets/draft-provider';
import { draftStorageKey } from '@/stores/drafts';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
Alert,
App,
Button,
Form,
Input,
Modal,
Select,
Space,
Spin,
Tag,
} from 'antd';
import { authClient } from '@/lib/auth-client';
import { AuthForm } from '@/features/auth/auth-form';
import { useTRPC } from '@/trpc/client';
import { roleLabels, type MemberRole } from '@/shared/team';
type CurrentTeam = {
id: string;
name: string;
role: MemberRole;
userId: string;
};
const TeamContext = createContext<CurrentTeam | null>(null);
export function useTeam() {
const value = useContext(TeamContext);
if (!value) throw new Error('必须在团队工作区中使用');
return value;
}
export function TeamWorkspace({ children }: { children: React.ReactNode }) {
const session = authClient.useSession();
const trpc = useTRPC();
const queryClient = useQueryClient();
const { message } = App.useApp();
const router = useRouter();
const [selected] = useQueryState('team', parseAsString);
const setSelected = (id: string) => router.push(`/?team=${id}`);
const [open, setOpen] = useState(false);
const [leaving, setLeaving] = useState(false);
const [form] = Form.useForm<{ name: string }>();
const teams = useQuery(
trpc.team.list.queryOptions(undefined, {
enabled: !!session.data,
staleTime: 0,
}),
);
const current = selected
? teams.data?.find((team) => team.id === selected)
: teams.data?.[0];
const create = useMutation(
trpc.team.create.mutationOptions({
async onSuccess(team) {
setSelected(team.id);
await queryClient.invalidateQueries({
queryKey: trpc.team.list.queryKey(),
});
setOpen(false);
form.resetFields();
void message.success('团队已创建,你是首位管理员');
},
onError(error) {
void message.error(error.message);
},
}),
);
async function signOut() {
setLeaving(true);
try {
await queryClient.cancelQueries();
const result = await authClient.signOut();
if (result.error) throw new Error('退出失败');
queryClient.clear();
// 草稿只在当前浏览器会话保留;主动退出时一并清除该账号的本地副本。
if (session.data)
sessionStorage.removeItem(draftStorageKey(session.data.user.id));
// 退出后完整刷新,清除已渲染的私有内容及上一账号的缓存。
// eslint-disable-next-line @next/next/no-location-assign-relative-destination
window.location.assign('/');
} catch {
setLeaving(false);
void message.error('退出失败,请重试');
}
}
if (session.isPending || leaving)
return (
<div className="p-20 text-center">
<Spin description="正在验证会话…" fullscreen />
</div>
);
if (!session.data)
return (
<>
<AuthForm />
{session.error && (
<Alert type="error" title="会话检查失败,请确认服务配置后刷新" />
)}
</>
);
return (
<>
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3">
<Space wrap>
<span>{session.data.user.name}</span>
<Select
aria-label="当前团队"
className="min-w-44"
placeholder="选择团队"
value={current?.id}
options={teams.data?.map((team) => ({
value: team.id,
label: team.name,
}))}
onChange={(id) => setSelected(id)}
/>
<Button onClick={() => setOpen(true)}>创建团队</Button>
{current && <Tag color="blue">{roleLabels[current.role]}</Tag>}
</Space>
<Space>
<Link href={current ? `/?team=${current.id}` : '/'}>工单</Link>
{current && <Link href={`/team?team=${current.id}`}>团队成员</Link>}
{current && (
<Link href={`/tasks?team=${current.id}`}>任务与提醒</Link>
)}
<Button onClick={() => void signOut()}>退出登录</Button>
</Space>
</div>
{teams.isError ? (
<div className="mx-auto max-w-3xl p-8">
<Alert
type="error"
title="团队加载失败"
description={teams.error.message}
action={<Button onClick={() => void teams.refetch()}>重试</Button>}
/>
</div>
) : teams.isPending ? (
<div className="p-20 text-center">
<Spin />
</div>
) : !current ? (
<div className="mx-auto max-w-xl p-10">
<h1 className="mb-5 text-2xl font-semibold">欢迎加入 TeamDesk</h1>
<p className="mb-5 text-slate-500">
请选择有权限的团队,或创建自己的团队。加入已有团队时,请管理员使用你的注册邮箱添加成员。
</p>
<Button type="primary" onClick={() => setOpen(true)}>
创建第一个团队
</Button>
</div>
) : (
<TeamContext.Provider
key={`${session.data.user.id}:${current.id}:${current.role}`}
value={{ ...current, userId: session.data.user.id }}
>
<DraftProvider userId={session.data.user.id}>
{children}
</DraftProvider>
</TeamContext.Provider>
)}
<Modal
title="创建团队"
open={open}
footer={null}
onCancel={() => {
if (!create.isPending) setOpen(false);
}}
>
<Form
form={form}
layout="vertical"
onFinish={(values) => create.mutate(values)}
>
<Form.Item
label="团队名称"
name="name"
rules={[
{ required: true, whitespace: true, message: '请输入团队名称' },
{ max: 80 },
]}
>
<Input maxLength={80} />
</Form.Item>
<Button htmlType="submit" type="primary" loading={create.isPending}>
创建团队
</Button>
</Form>
</Modal>
</>
);
}