/** * 工作台外壳 —— 自原型 `(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, WorkOrderReply } from '@contracts'; import { listApprovalsForMeRoute, listMessagesRoute, listTasksRoute, logoutRoute, replyWorkOrderRoute, 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 { EmptyState } from '@web-shared/desk/module-kit.tsx'; import { SURFACE_META, canSeeSurface } from '@web-shared/gov/surfaces.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 logout = useMutation({ mutationFn: () => call<{ ok: boolean }>(logoutRoute), onSuccess: () => { queryClient.clear(); // 整页导航:SSE 连接、组件状态、路由位置一并归零 —— 登出不留残影 window.location.assign('/'); }, }); const me = session.data?.staff.name ?? '—'; /** * 工单回传 —— 视图区与对话区同一条口。 * 回传后按键失效、画面由服务端重投影:前端不自行推演卡与工单的状态(§9.2)。 */ const replyOrder = useMutation({ mutationFn: (reply: WorkOrderReply) => call(replyWorkOrderRoute, { body: { reply } }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['desk'] }); }, }); return (
{module === 'tasks' ? (
{collapsed && ( )} setCollapsed(true)} /> {/* 手上没任务时,三栏也得**站得住**:中栏与对话区各给自己的空态。 少一栏会让整个工位看着像坏了 —— 空态是常态,不是缺页。 */} {detail.data !== undefined ? ( replyOrder.mutate(reply)} /> ) : (
{tasks.isLoading ? (
载入中…
) : ( )}
)}
{detail.data !== undefined ? ( undefined} /> ) : (
任务助理 Agent 开一单即可对话