工作台空占位补齐 —— 有内容渲染内容,没内容才落空态

任务面原来缺两处占位:中栏是一行裸文字,对话区在没选中任务时**整栏不渲染**。
补齐后三栏恒在:中栏落设计空态「手上没有任务」,对话区给全壳(题头 +
空态 + 禁用输入条),空着也看得出这是干什么的地方。

但真起一单之后暴露了更重的问题:占位在**说谎**。待处理角标写着 1,正文却
说「手上没有待办」—— 因为 ChatStream(643 行)与 WorkOrderReport(449 行)
早就写好、也从 @features/desk-task 导出了,却没有一处在用。占位不是缺内容,
是内容没接上。三处接回真渲染:

  待处理  → WorkOrderReport(工单正文、编号、派发时间、数据与分析)
  工作日志 → .jc-log 行,按归侧四类各一色(机器/人类/裁决/等世界)
  工作成果 → .jc-file 行,点开进 FileViewer;打开时右槽换「返回」
  对话区  → ChatStream,三个回传(说一句 / 答卡 / 答工单)在这里收口

顺带治了窄对话区的抗压:工作区优先保「五按钮 + 翻页」,对话区在 1280 下被
压到 212px,卡头标题 `flex:1` 没设 `min-width:0`,一挤就逐字折断成竖排单字。
降级顺序改成「状态芯片换行下沉、标题始终整词可读」—— 截断能让布局不崩,
但读的人不知道这是什么卡。不改任何一栏尺寸。

五闸:tsc ✓ eslint ✓ depcruise 373 模块 ✓ api:check 公开契约无变化 ✓ 481 测试 ✓
走查后清了运行数据(仓库自带 reset-runs,配置原样留着),换业务化主题重起一单。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 10:25:19 +08:00
co-authored by Claude Opus 5
parent 5619d86ce4
commit c59e73754b
4 changed files with 243 additions and 62 deletions
+75 -19
View File
@@ -8,12 +8,13 @@
*/
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type { Session, TaskCard, TaskDetail } from '@contracts';
import type { Session, TaskCard, TaskDetail, WorkOrderReply } from '@contracts';
import {
listApprovalsForMeRoute,
listMessagesRoute,
listTasksRoute,
logoutRoute,
replyWorkOrderRoute,
sessionRoute,
taskDetailRoute,
} from '@contracts';
@@ -25,6 +26,7 @@ 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';
@@ -91,6 +93,17 @@ export function AppShell() {
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 (
<div className="app-shell">
<div className="app-body">
@@ -186,29 +199,72 @@ export function AppShell() {
onPick={setTaskId}
onCollapse={() => setCollapsed(true)}
/>
{/*
手上没任务时,三栏也得**站得住**:中栏与对话区各给自己的空态。
少一栏会让整个工位看着像坏了 —— 空态是常态,不是缺页。
*/}
{detail.data !== undefined ? (
<ViewRegion key={current} detail={detail.data} />
<ViewRegion
key={current}
detail={detail.data}
onReplyOrder={(reply) => replyOrder.mutate(reply)}
/>
) : (
<main className="execution-panel">
<div className="jc-list-empty">
<Ico name="list-checks" s={18} />
{tasks.isLoading ? '载入中…' : '还没有落到你工位的任务'}
</div>
{tasks.isLoading ? (
<div className="jc-list-empty">
<Ico name="list-checks" s={18} />
载入中…
</div>
) : (
<EmptyState
ico="list-checks"
tone="brand"
title="手上没有任务"
desc="流程走到你负责的活动时,任务会连同办这件事要用的全部上下文一起送到这里 —— 待处理的事、往来记录、产出的文件都在一屏之内。"
hint="有新任务时左侧列表会亮角标"
/>
)}
</main>
)}
{detail.data !== undefined && (
<i
className="jc-vsplit"
title="拖拽调整对话区宽度 · 双击复位"
onPointerDown={split.onSplitDown}
onDoubleClick={split.onSplitReset}
/>
)}
{detail.data !== undefined && (
<section className="session-chat">
<TaskChat key={current} />
</section>
)}
<i
className="jc-vsplit"
title="拖拽调整对话区宽度 · 双击复位"
onPointerDown={split.onSplitDown}
onDoubleClick={split.onSplitReset}
/>
<section className="session-chat">
{detail.data !== undefined ? (
<TaskChat key={current} detail={detail.data} onLocate={() => undefined} />
) : (
<div className="jc-tch">
<header className="jc-tch-head">
<span className="jc-tch-mark">
<Ico name="bot" s={15} />
</span>
<b className="jc-tch-name">任务助理</b>
<em className="jc-tch-tag">Agent</em>
<span className="jc-tch-desc">开一单即可对话</span>
</header>
<div className="jc-tch-scroll">
<EmptyState
ico="messages-square"
tone="brand"
title="还没有可对话的任务"
desc="每一单任务都有自己的对话:AI 有结论、要派工、或者要你拿个主意,都在那一单里说 —— 所以得先有一单在手。"
/>
</div>
<div className="copilot-composer session-composer">
<div className="composer-row">
<textarea rows={2} disabled placeholder="选中一单任务后就能在这里交代" />
<button className="copilot-send" disabled>
<Ico name="send" s={14} /> 发送
</button>
</div>
</div>
</div>
)}
</section>
</div>
) : module === 'approvals' ? (
<ApprovalDesk />
+28
View File
@@ -2922,3 +2922,31 @@
.proto-root .jc-tnav-btn.back svg {
transform: translateY(-2px);
}
/* ── 工作日志(补充,非原型)—— 一行一步,标明谁的产出 ────────────────── */
.proto-root .jc-log { display: flex; flex-direction: column; }
.proto-root .jc-log-row { display: flex; gap: 10px; align-items: baseline; padding: 8px 2px; }
.proto-root .jc-log-row + .jc-log-row { border-top: 1px solid var(--line-soft); }
.proto-root .jc-log-at { flex: none; font-size: 10.5px; color: var(--ink-4); }
.proto-root .jc-log-side { flex: none; font-size: 10.5px; font-weight: 700; padding: 1px 7px; border-radius: 999px; background: var(--soft); color: var(--ink-2); }
/* 归侧四类各一色:机器 / 人 / 人裁 / 世界等待 —— 一眼看出这一步是谁推的 */
.proto-root .jc-log-row.t-machine .jc-log-side { background: var(--brand-wash); color: var(--brand-deep); }
.proto-root .jc-log-row.t-human .jc-log-side { background: #e6f7f4; color: #0a6a64; }
.proto-root .jc-log-row.t-human-verdict .jc-log-side { background: #fff4e5; color: #de7802; }
.proto-root .jc-log-row.t-world-wait .jc-log-side { background: var(--soft); color: var(--ink-3); }
.proto-root .jc-log-t { flex: 1; min-width: 0; }
.proto-root .jc-log-t b { display: block; font-size: 12.5px; font-weight: 600; }
.proto-root .jc-log-t em { display: block; margin-top: 2px; font-style: normal; font-size: 11px; line-height: 1.6; color: var(--ink-3); }
/* 成果二级界面的返回键 —— 顶部页签不变,右槽换成它 */
.proto-root .jc-tnav-back { width: auto; padding: 0 9px; gap: 5px; font-size: 11.5px; font-weight: 600; }
/* ── 窄对话区的抗压(补充,非原型)────────────────────────────────────
对话区在小屏上会被压到 ~210px(工作区优先保「五按钮 + 翻页」,任务列表可收起让位)。
卡片原样搬过来是不抗压的:卡头标题 `flex: 1` 没设 `min-width: 0`,一挤就**逐字折断**
成竖排单字;按钮同理。这里只教卡片怎么变窄,不改任何一栏的尺寸。
降级的顺序是:先让状态芯片换行下沉(标题拿回整行),标题始终整词可读 —— 而不是把
「工单 · 执行」截成一个「工」字。截断能让布局不崩,但读的人不知道这是什么卡。 */
.proto-root .jc-fc-h { flex-wrap: wrap; row-gap: 3px; }
.proto-root .jc-fc-h b { min-width: 0; flex: 1 1 auto; }
.proto-root .jc-fc-open { white-space: nowrap; }