/** * 工作台外壳 —— 自原型 `(proto)/page.tsx` 平移(README《前端 UI · 原型 1:1 平移》)。 * * 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报)→ 轨底身份; * 任务四区 = 左栏列表区 / 中上标题区 / 中下工作区 / 右栏对话区。 * * **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。 */ import { useRef, 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 { 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); /* * 工单填报值 —— **壳层持有**,工作区与对话区共用同一份。 * * 验收工单是「AI 已填好、可编辑」,而**裁决在对话区的验收单上**(PRD 口径): * 编辑与裁决是同一件事的两半,却分在两个区里做。缓冲只活在工作区里, * 人在报告上改的值就到不了裁决那一刻 —— 验收回传的 `values` 恒空,修订永远落不下来。 * * 换任务即清空:填报值是**过程状态**,不跨单。 */ const [orderValues, setOrderValues] = useState>({}); /* * 对话区点「打开工单」→ 视图区跳转定位(PRD:locate)。 * `seq` 每点递增:同一张卡点两次也要再跳一次 —— 人就是想回到它。 */ const [locate, setLocate] = useState<{ seq: number; orderId?: string; tab?: 'comms' } | null>(null); const locateSeq = useRef(0); 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: queryKeys.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 (
{NAV_MODULES.find((m) => m.key === module)?.label ?? ''}
{me}
{module === 'tasks' ? (
{collapsed && ( )} { // 换任务即清空填报缓冲与定位:它们是**过程状态**,不跨单 setOrderValues({}); setLocate(null); setTaskId(id); }} onCollapse={() => setCollapsed(true)} /> {/* 手上没任务时,三栏也得**站得住**:中栏与对话区各给自己的空态。 少一栏会让整个工位看着像坏了 —— 空态是常态,不是缺页。 */} {detail.data !== undefined ? ( replyOrder.mutate(reply)} values={orderValues} onField={(k, v) => setOrderValues((old) => ({ ...old, [k]: v }))} locate={locate} /> ) : ( /* * **手上没任务时,中栏也得是一张卡**。 * * 其余四个模块(审批 / 消息 / 纪要 / 汇报)的空态都挂 `jc-panel-card`, * 只有任务这一栏是裸的 `execution-panel` —— 而那个类在工作区里被显式 * 去边框去底色(视图区不再有外层卡)。于是同一套工作台上,四个模块的空态 * 是白卡片,任务这一栏却是画布上飘着的一段文字,看着像没渲染出来。 */
{tasks.isLoading ? (
载入中…
) : ( )}
)}
{detail.data !== undefined ? ( { locateSeq.current += 1; setLocate({ seq: locateSeq.current, orderId }); }} onOpenComms={() => { locateSeq.current += 1; setLocate({ seq: locateSeq.current, tab: 'comms' }); }} orderValues={orderValues} onField={(k, v) => setOrderValues((old) => ({ ...old, [k]: v }))} /> ) : (
任务助理 Agent 开一单即可对话