forked from ChaiHao/BPOS-MVP
工作台空占位补齐 —— 有内容渲染内容,没内容才落空态
任务面原来缺两处占位:中栏是一行裸文字,对话区在没选中任务时**整栏不渲染**。 补齐后三栏恒在:中栏落设计空态「手上没有任务」,对话区给全壳(题头 + 空态 + 禁用输入条),空着也看得出这是干什么的地方。 但真起一单之后暴露了更重的问题:占位在**说谎**。待处理角标写着 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:
@@ -8,12 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import type { Session, TaskCard, TaskDetail } from '@contracts';
|
import type { Session, TaskCard, TaskDetail, WorkOrderReply } from '@contracts';
|
||||||
import {
|
import {
|
||||||
listApprovalsForMeRoute,
|
listApprovalsForMeRoute,
|
||||||
listMessagesRoute,
|
listMessagesRoute,
|
||||||
listTasksRoute,
|
listTasksRoute,
|
||||||
logoutRoute,
|
logoutRoute,
|
||||||
|
replyWorkOrderRoute,
|
||||||
sessionRoute,
|
sessionRoute,
|
||||||
taskDetailRoute,
|
taskDetailRoute,
|
||||||
} from '@contracts';
|
} from '@contracts';
|
||||||
@@ -25,6 +26,7 @@ import { ReportDesk } from '@features/desk-report';
|
|||||||
import { Ico } from '@web-shared/Ico.tsx';
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
import { call } from '@web-shared/typed-fetch.ts';
|
import { call } from '@web-shared/typed-fetch.ts';
|
||||||
import { useChatWidth } from '@web-shared/useChatWidth.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 { SURFACE_META, canSeeSurface } from '@web-shared/gov/surfaces.ts';
|
||||||
import { queryKeys } from './query.ts';
|
import { queryKeys } from './query.ts';
|
||||||
|
|
||||||
@@ -91,6 +93,17 @@ export function AppShell() {
|
|||||||
|
|
||||||
const me = session.data?.staff.name ?? '—';
|
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 (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<div className="app-body">
|
<div className="app-body">
|
||||||
@@ -186,29 +199,72 @@ export function AppShell() {
|
|||||||
onPick={setTaskId}
|
onPick={setTaskId}
|
||||||
onCollapse={() => setCollapsed(true)}
|
onCollapse={() => setCollapsed(true)}
|
||||||
/>
|
/>
|
||||||
|
{/*
|
||||||
|
手上没任务时,三栏也得**站得住**:中栏与对话区各给自己的空态。
|
||||||
|
少一栏会让整个工位看着像坏了 —— 空态是常态,不是缺页。
|
||||||
|
*/}
|
||||||
{detail.data !== undefined ? (
|
{detail.data !== undefined ? (
|
||||||
<ViewRegion key={current} detail={detail.data} />
|
<ViewRegion
|
||||||
|
key={current}
|
||||||
|
detail={detail.data}
|
||||||
|
onReplyOrder={(reply) => replyOrder.mutate(reply)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<main className="execution-panel">
|
<main className="execution-panel">
|
||||||
<div className="jc-list-empty">
|
{tasks.isLoading ? (
|
||||||
<Ico name="list-checks" s={18} />
|
<div className="jc-list-empty">
|
||||||
{tasks.isLoading ? '载入中…' : '还没有落到你工位的任务'}
|
<Ico name="list-checks" s={18} />
|
||||||
</div>
|
载入中…
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
ico="list-checks"
|
||||||
|
tone="brand"
|
||||||
|
title="手上没有任务"
|
||||||
|
desc="流程走到你负责的活动时,任务会连同办这件事要用的全部上下文一起送到这里 —— 待处理的事、往来记录、产出的文件都在一屏之内。"
|
||||||
|
hint="有新任务时左侧列表会亮角标"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
{detail.data !== undefined && (
|
<i
|
||||||
<i
|
className="jc-vsplit"
|
||||||
className="jc-vsplit"
|
title="拖拽调整对话区宽度 · 双击复位"
|
||||||
title="拖拽调整对话区宽度 · 双击复位"
|
onPointerDown={split.onSplitDown}
|
||||||
onPointerDown={split.onSplitDown}
|
onDoubleClick={split.onSplitReset}
|
||||||
onDoubleClick={split.onSplitReset}
|
/>
|
||||||
/>
|
<section className="session-chat">
|
||||||
)}
|
{detail.data !== undefined ? (
|
||||||
{detail.data !== undefined && (
|
<TaskChat key={current} detail={detail.data} onLocate={() => undefined} />
|
||||||
<section className="session-chat">
|
) : (
|
||||||
<TaskChat key={current} />
|
<div className="jc-tch">
|
||||||
</section>
|
<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>
|
</div>
|
||||||
) : module === 'approvals' ? (
|
) : module === 'approvals' ? (
|
||||||
<ApprovalDesk />
|
<ApprovalDesk />
|
||||||
|
|||||||
@@ -2922,3 +2922,31 @@
|
|||||||
.proto-root .jc-tnav-btn.back svg {
|
.proto-root .jc-tnav-btn.back svg {
|
||||||
transform: translateY(-2px);
|
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; }
|
||||||
|
|||||||
@@ -1,45 +1,95 @@
|
|||||||
/**
|
/**
|
||||||
* 对话区 —— 「对话流是**一切回环输入的唯一入口**」(OS 4.1)。
|
* 对话区 —— 「对话流是**一切回环输入的唯一入口**」(OS 4.1)。
|
||||||
*
|
*
|
||||||
* 壳(`jc-tch` 题头 / 滚动区 / composer)照原型留着;气泡与卡族备在 `chat-bubbles.tsx`。
|
* 壳(`jc-tch` 题头 / 滚动区 / composer)由 `ChatStream` 自己带;这里只做取数与回传:
|
||||||
* 空态文案从用人的角度写 —— 空对话是这一单刚开始时的**常态**,不是缺陷。
|
* 说一句、答卡、答工单,回传后按键失效,画面由服务端重投影(前端零过程状态)。
|
||||||
|
*
|
||||||
|
* **有内容就渲染内容,没内容才落空态** —— 空对话是这一单刚开始时的常态,不是缺陷;
|
||||||
|
* 但一旦真有话,占位就变成了谎:角标说有、正文说没有,人会以为坏了。
|
||||||
*/
|
*/
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { ChatCardReply, TaskDetail, WorkOrderReply } from '@contracts';
|
||||||
|
import { replyChatCardRoute, replyWorkOrderRoute, sayRoute } from '@contracts';
|
||||||
|
import { call } from '@web-shared/typed-fetch.ts';
|
||||||
import { Ico } from '@web-shared/Ico.tsx';
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
|
import { EmptyState } from '@web-shared/desk/module-kit.tsx';
|
||||||
|
import { ChatStream } from './chat-bubbles.tsx';
|
||||||
|
|
||||||
export function TaskChat() {
|
export function TaskChat({ detail, onLocate }: { detail: TaskDetail; onLocate: (orderId: string) => void }) {
|
||||||
return (
|
const queryClient = useQueryClient();
|
||||||
<div className="jc-tch">
|
const taskId = detail.card.id;
|
||||||
<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">
|
/** 回传后一律重取任务投影 —— 卡与工单的状态由服务端说,前端不自行推演 */
|
||||||
<div className="jc-empty t-brand">
|
const refresh = () => {
|
||||||
<span className="jc-empty-art">
|
void queryClient.invalidateQueries({ queryKey: ['desk', 'task', taskId] });
|
||||||
<i className="r1" />
|
void queryClient.invalidateQueries({ queryKey: ['desk', 'tasks'] });
|
||||||
<i className="r2" />
|
};
|
||||||
<span className="jc-empty-ico">
|
|
||||||
<Ico name="messages-square" s={26} />
|
const say = useMutation({
|
||||||
</span>
|
mutationFn: (text: string) => call(sayRoute, { params: { taskId }, body: { text } }),
|
||||||
|
onSuccess: refresh,
|
||||||
|
});
|
||||||
|
const replyCard = useMutation({
|
||||||
|
mutationFn: (reply: ChatCardReply) => call(replyChatCardRoute, { body: { reply } }),
|
||||||
|
onSuccess: refresh,
|
||||||
|
});
|
||||||
|
const replyOrder = useMutation({
|
||||||
|
mutationFn: (reply: WorkOrderReply) => call(replyWorkOrderRoute, { body: { reply } }),
|
||||||
|
onSuccess: refresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (detail.chat.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="jc-tch">
|
||||||
|
<header className="jc-tch-head">
|
||||||
|
<span className="jc-tch-mark">
|
||||||
|
<Ico name="bot" s={15} />
|
||||||
</span>
|
</span>
|
||||||
<b>还没有消息</b>
|
<b className="jc-tch-name">任务助理</b>
|
||||||
<p>AI 有结论、要派工、或者要你拿个主意,都会在这里说。你也可以随时交代两句。</p>
|
<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}
|
||||||
|
placeholder="有什么要补充或交代的?"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key !== 'Enter' || e.shiftKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const text = e.currentTarget.value.trim();
|
||||||
|
if (text !== '') {
|
||||||
|
say.mutate(text);
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button className="copilot-send" disabled={say.isPending}>
|
||||||
|
<Ico name="send" s={14} /> 发送
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<div className="copilot-composer session-composer">
|
return (
|
||||||
<div className="composer-row">
|
<ChatStream
|
||||||
<textarea rows={2} disabled placeholder="有什么要补充或交代的?" />
|
chat={detail.chat}
|
||||||
<button className="copilot-send" disabled>
|
orders={detail.workOrders}
|
||||||
<Ico name="send" s={14} /> 发送
|
thinking={say.isPending}
|
||||||
</button>
|
onSay={(text) => say.mutate(text)}
|
||||||
</div>
|
onReplyCard={(reply) => replyCard.mutate(reply)}
|
||||||
</div>
|
onReplyOrder={(reply) => replyOrder.mutate(reply)}
|
||||||
</div>
|
onLocate={onLocate}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,24 +11,31 @@
|
|||||||
* 不写「这块还没做」—— 后者是开发视角,用户不关心,也看不懂。
|
* 不写「这块还没做」—— 后者是开发视角,用户不关心,也看不懂。
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { TaskDetail } from '@contracts';
|
import type { FileDoc, TaskDetail, WorkOrderReply } from '@contracts';
|
||||||
import { TERMINAL_STATE_LABEL } from '@contracts';
|
import { TERMINAL_STATE_LABEL } from '@contracts';
|
||||||
import { Ico } from '@web-shared/Ico.tsx';
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
||||||
import { EmptyState } from '@web-shared/desk/module-kit.tsx';
|
import { EmptyState } from '@web-shared/desk/module-kit.tsx';
|
||||||
import { ViewBlocks } from '@ui';
|
import { ViewBlocks } from '@ui';
|
||||||
|
import { WorkOrderReport } from './work-order-report.tsx';
|
||||||
|
import { FileViewer } from './FileViewer.tsx';
|
||||||
|
|
||||||
|
|
||||||
type Tab = 'open' | 'comms' | 'done' | 'wf' | 'log';
|
type Tab = 'open' | 'comms' | 'done' | 'wf' | 'log';
|
||||||
|
|
||||||
export type ViewRegionProps = {
|
export type ViewRegionProps = {
|
||||||
readonly detail: TaskDetail;
|
readonly detail: TaskDetail;
|
||||||
|
/** 工单回传 —— 由壳层接路由,视图区只管把人填的东西交出去 */
|
||||||
|
onReplyOrder: (reply: WorkOrderReply) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export function ViewRegion({ detail }: ViewRegionProps) {
|
export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||||
const [view, setView] = useState<Tab>('open');
|
const [view, setView] = useState<Tab>('open');
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
/** 表单值是**过程状态**,只活在这一次填写里:回传成功后画面由服务端重投影 */
|
||||||
|
const [values, setValues] = useState<Record<string, string>>({});
|
||||||
|
const [openFile, setOpenFile] = useState<FileDoc | undefined>(undefined);
|
||||||
|
|
||||||
const orders = detail.workOrders;
|
const orders = detail.workOrders;
|
||||||
const openList = orders.filter((o) => o.done === undefined);
|
const openList = orders.filter((o) => o.done === undefined);
|
||||||
@@ -140,7 +147,15 @@ export function ViewRegion({ detail }: ViewRegionProps) {
|
|||||||
翻页**恒显示**:只有一件也出 1/1,两侧箭头灰显。
|
翻页**恒显示**:只有一件也出 1/1,两侧箭头灰显。
|
||||||
按件数隐藏会让点位条忽有忽无,右侧跟着跳 —— 页签条要定得住。
|
按件数隐藏会让点位条忽有忽无,右侧跟着跳 —— 页签条要定得住。
|
||||||
*/}
|
*/}
|
||||||
{view === 'open' && (
|
{/*
|
||||||
|
右槽两态:成果进二级界面时是「返回」,否则待处理位上是翻页。
|
||||||
|
顶部五个按钮**始终不变** —— 二级界面长在工作区内部,不是另一页。
|
||||||
|
*/}
|
||||||
|
{openFile !== undefined ? (
|
||||||
|
<button type="button" className="jc-tnav-btn jc-tnav-back" onClick={() => setOpenFile(undefined)}>
|
||||||
|
<Ico name="arrow-left" s={14} /> 返回
|
||||||
|
</button>
|
||||||
|
) : view === 'open' ? (
|
||||||
<span className="jc-tnav-pg" aria-label="翻页">
|
<span className="jc-tnav-pg" aria-label="翻页">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -164,7 +179,7 @@ export function ViewRegion({ detail }: ViewRegionProps) {
|
|||||||
<Ico name="chevron-right" s={15} />
|
<Ico name="chevron-right" s={15} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* key:换件 / 换页签即重挂载滚动区,回到正文顶部 */}
|
{/* key:换件 / 换页签即重挂载滚动区,回到正文顶部 */}
|
||||||
@@ -174,7 +189,9 @@ export function ViewRegion({ detail }: ViewRegionProps) {
|
|||||||
vpClassName="wf-body"
|
vpClassName="wf-body"
|
||||||
inset={-6}
|
inset={-6}
|
||||||
>
|
>
|
||||||
{view === 'comms' ? (
|
{openFile !== undefined ? (
|
||||||
|
<FileViewer doc={openFile} />
|
||||||
|
) : view === 'comms' ? (
|
||||||
/* 占位:与外界的往来(邮件线程等)还没接进来 —— 明说没接,不装成「暂无数据」 */
|
/* 占位:与外界的往来(邮件线程等)还没接进来 —— 明说没接,不装成「暂无数据」 */
|
||||||
<EmptyState
|
<EmptyState
|
||||||
ico="mail"
|
ico="mail"
|
||||||
@@ -185,12 +202,27 @@ export function ViewRegion({ detail }: ViewRegionProps) {
|
|||||||
) : view === 'wf' ? (
|
) : view === 'wf' ? (
|
||||||
<WfDetail detail={detail} openN={openList.length} doneN={orders.length - openList.length} />
|
<WfDetail detail={detail} openN={openList.length} doneN={orders.length - openList.length} />
|
||||||
) : view === 'log' ? (
|
) : view === 'log' ? (
|
||||||
<EmptyState
|
detail.log.length > 0 ? (
|
||||||
ico="archive"
|
<div className="jc-log">
|
||||||
tone="brand"
|
{detail.log.map((line) => (
|
||||||
title="还没有进展"
|
<div key={line.id} className={'jc-log-row t-' + line.side}>
|
||||||
desc="AI 每推进一步都记在这里,并标明是谁的产出:AI 完成、我填的、我确认的、等外部。"
|
<span className="jc-log-at jc-mono">{line.at.slice(5, 16).replace('T', ' ')}</span>
|
||||||
/>
|
<span className="jc-log-side">{line.sideLabel}</span>
|
||||||
|
<span className="jc-log-t">
|
||||||
|
<b>{line.title}</b>
|
||||||
|
{line.summary !== undefined && <em>{line.summary}</em>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
ico="archive"
|
||||||
|
tone="brand"
|
||||||
|
title="还没有进展"
|
||||||
|
desc="AI 每推进一步都记在这里,并标明是谁的产出:AI 完成、我填的、我确认的、等外部。"
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : view === 'done' ? (
|
) : view === 'done' ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
ico="archive"
|
ico="archive"
|
||||||
@@ -198,6 +230,21 @@ export function ViewRegion({ detail }: ViewRegionProps) {
|
|||||||
title="还没有成果"
|
title="还没有成果"
|
||||||
desc="这一单产出的报告与文件会归档在这里,一行一份、点开整篇看。"
|
desc="这一单产出的报告与文件会归档在这里,一行一份、点开整篇看。"
|
||||||
/>
|
/>
|
||||||
|
) : cur !== undefined ? (
|
||||||
|
/*
|
||||||
|
有工单就把工单摆出来 —— 角标说有 1 件、正文说「手上没有待办」,那是占位在说谎。
|
||||||
|
办这张单要用的上下文(活动推的那一屏、依据、表单)由 WorkOrderReport 一并铺开。
|
||||||
|
*/
|
||||||
|
<WorkOrderReport
|
||||||
|
order={cur}
|
||||||
|
detail={detail}
|
||||||
|
typeLine={cur.mode === 'acceptance' ? '验收工单 · 终局唯放行' : '执行工单 · 作业指导书 + 表单'}
|
||||||
|
blocks={cur.mode === 'execution' ? (cur.blocks ?? detail.view) : detail.view}
|
||||||
|
{...(detail.viewHtml === undefined ? {} : { html: detail.viewHtml, srcTitle: detail.card.activity })}
|
||||||
|
values={values}
|
||||||
|
onField={(k, v) => setValues((old) => ({ ...old, [k]: v }))}
|
||||||
|
onReply={onReplyOrder}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
ico="list-checks"
|
ico="list-checks"
|
||||||
|
|||||||
Reference in New Issue
Block a user