/** * 工作台外壳 —— 自原型 `(proto)/page.tsx` 平移(README《前端 UI · 原型 1:1 平移》)。 * * 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报)→ 轨底身份; * 任务四区 = 左栏列表区 / 中上标题区 / 中下工作区 / 右栏对话区。 * * **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。 */ import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import type { Session, TaskCard, TaskDetail } from '@contracts'; import { listApprovalsForMeRoute, listMessagesRoute, listTasksRoute, replyWorkOrderRoute, sayRoute, sessionRoute, taskDetailRoute } from '@contracts'; import { TaskChat, TaskList, ViewRegion } from '@features/desk-task'; import { ApprovalDesk } from '@features/desk-approval'; import { MessageDesk } from '@features/desk-message'; import { MinuteDesk } from '@features/desk-minute'; import { ReportDesk } from '@features/desk-report'; import { Ico } from '@web-shared/Ico.tsx'; import { call } from '@web-shared/typed-fetch.ts'; import { useChatWidth } from '@web-shared/useChatWidth.ts'; import { queryKeys } from './query.ts'; type ModuleKey = 'tasks' | 'approvals' | 'messages' | 'minutes' | 'report'; const NAV_MODULES: ReadonlyArray<{ key: ModuleKey; label: string; icon: string }> = [ { key: 'tasks', label: '任务', icon: 'list-checks' }, { key: 'approvals', label: '审批', icon: 'file-check' }, { key: 'messages', label: '消息', icon: 'messages-square' }, { key: 'minutes', label: '纪要', icon: 'notebook-pen' }, { key: 'report', label: '汇报', icon: 'bar-chart' }, ]; export function AppShell() { const queryClient = useQueryClient(); const [module, setModule] = useState('tasks'); const [taskId, setTaskId] = useState(''); const [collapsed, setCollapsed] = useState(false); const split = useChatWidth(); const session = useQuery({ queryKey: queryKeys.session, queryFn: () => call(sessionRoute), }); const tasks = useQuery({ queryKey: queryKeys.tasks, queryFn: () => call<{ tasks: TaskCard[] }>(listTasksRoute), }); // 角标常驻:责任面要一眼看见「什么在等我」,不能等切过去才知道 const messages = useQuery({ queryKey: queryKeys.messages, queryFn: () => call<{ messages: Array<{ id: string; read: boolean }> }>(listMessagesRoute), }); const approvals = useQuery({ queryKey: ['approvals'], queryFn: () => call<{ approvals: Array<{ state: string }> }>(listApprovalsForMeRoute), }); const list = tasks.data?.tasks ?? []; const active = list.filter((t) => t.terminal === undefined); const current = taskId !== '' ? taskId : (active[0]?.id ?? list[0]?.id ?? ''); const detail = useQuery({ queryKey: queryKeys.task(current), queryFn: () => call(taskDetailRoute, { params: { taskId: current } }), enabled: current !== '', }); const reply = useMutation({ mutationFn: (payload: Record) => call<{ ok: boolean }>(replyWorkOrderRoute, { body: { reply: payload } }), // 回传后按目标失效缓存 —— 组件不自行轮询 onSuccess: () => { void queryClient.invalidateQueries({ queryKey: queryKeys.task(current) }); void queryClient.invalidateQueries({ queryKey: queryKeys.tasks }); }, }); const say = useMutation({ mutationFn: (text: string) => call(sayRoute, { params: { taskId: current }, body: { text } }), onSuccess: () => void queryClient.invalidateQueries({ queryKey: queryKeys.task(current) }), }); const me = session.data?.staff.name ?? '—'; // composer 锁:有卡在等作答时锁住自由输入 const locked = (detail.data?.workOrders ?? []).some((o) => o.done === undefined); return (
{module === 'tasks' ? (
{collapsed && ( )} setCollapsed(true)} /> {detail.data !== undefined ? ( reply.mutate(payload)} /> ) : (
{tasks.isLoading ? '载入中…' : '还没有落到你工位的任务'}
)} {detail.data !== undefined && ( )} {detail.data !== undefined && ( say.mutate(text)} onDecide={() => undefined} /> )}
) : module === 'approvals' ? ( ) : module === 'messages' ? ( ) : module === 'minutes' ? ( ) : ( )}
); }