Archived
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
@@ -11,14 +11,19 @@
|
||||
* 不写「这块还没做」—— 后者是开发视角,用户不关心,也看不懂。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { FileDoc, TaskDetail, WorkOrderReply } from '@contracts';
|
||||
import { TERMINAL_STATE_LABEL } from '@contracts';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { markCommReadRoute, saveTableRoute } from '@contracts';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import type { FileDoc, TaskDetail, WorkOrder, WorkOrderReply } from '@contracts';
|
||||
import { TERMINAL_STATE_LABEL, isRejection } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
||||
import { EmptyState } from '@web-shared/desk/module-kit.tsx';
|
||||
import { ViewBlocks } from '@ui';
|
||||
import { WorkOrderReport } from './work-order-report.tsx';
|
||||
import { ViewBlocks, ViewContainer } from '@ui';
|
||||
import type { ViewEdits, ViewSave } from '@ui';
|
||||
import { GuideCard, WorkOrderReport } from './work-order-report.tsx';
|
||||
import { FileViewer } from './FileViewer.tsx';
|
||||
import { atFull, atMinute } from '@web-shared/at.ts';
|
||||
|
||||
|
||||
type Tab = 'open' | 'comms' | 'done' | 'wf' | 'log';
|
||||
@@ -27,24 +32,136 @@ export type ViewRegionProps = {
|
||||
readonly detail: TaskDetail;
|
||||
/** 工单回传 —— 由壳层接路由,视图区只管把人填的东西交出去 */
|
||||
onReplyOrder: (reply: WorkOrderReply) => void;
|
||||
/**
|
||||
* 填报值由**壳层持有** —— 不是这一份的局部状态。
|
||||
*
|
||||
* PRD 的口径:验收工单「AI 已填好、可编辑」,而**裁决在对话区的验收单上**。
|
||||
* 编辑与裁决是同一件事的两半,却分在两个区里做 —— 缓冲只活在工作区里,
|
||||
* 人在报告上改的值就到不了裁决那一刻:验收回传的 `values` 恒空,修订永远落不下来。
|
||||
*/
|
||||
readonly values: Readonly<Record<string, string>>;
|
||||
onField: (key: string, value: string) => void;
|
||||
/**
|
||||
* 对话区点了「打开工单」→ 视图区跳转定位(PRD:locate)。
|
||||
* `seq` 每点一次递增 —— 同一张卡点两次也要再跳一次(人就是想回到它)。
|
||||
* 待办的跳「待处理」并翻到那一页;办结的跳「工作成果」并打开那份定稿报告。
|
||||
*/
|
||||
/*
|
||||
* `orderId` 跳工单;`tab` 直接跳页签 —— 起单那句系统开场白的「查看邮件」用后者
|
||||
* (它指的不是某张工单,是那封来件本身)。
|
||||
*/
|
||||
readonly locate?: { seq: number; orderId?: string; tab?: Tab } | null;
|
||||
};
|
||||
|
||||
|
||||
export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
/**
|
||||
* 件上改出来的行 → 回传载荷 —— **按台账地址对上工单声明的那张表**。
|
||||
*
|
||||
* 件里那几张可改的表各挂着 `data-edit="域.对象"`;工单的 `blocks[i]` 声明了
|
||||
* `ledger`(写哪张表)与 `fields`(列在哪个位置)。这里按地址找到那一块,
|
||||
* 再**照它的 `fields` 顺序**摆格子 —— 于是回传仍是老样子 `[列名, ...行]`,
|
||||
* 服务端那道写回(`applyBoundTableEdits`)一个字都不用改。
|
||||
*
|
||||
* 按地址找而不是按位置找:件与卡是两份东西,位置对齐迟早会错开一位,
|
||||
* 而错开一位的后果是把年用量写进目标价那一列 —— 那种错没人看得出来。
|
||||
*/
|
||||
function collectEdits(edits: ViewEdits, order: WorkOrder, onField: (k: string, v: string) => void): void {
|
||||
const blocks = order.blocks ?? [];
|
||||
for (const [address, rows] of Object.entries(edits)) {
|
||||
const at = blocks.findIndex((b) => b.t === 'table' && b.ledger === address);
|
||||
const block = blocks[at];
|
||||
if (at < 0 || block === undefined || block.t !== 'table') continue;
|
||||
const fields = block.fields ?? [];
|
||||
if (fields.length === 0) continue;
|
||||
const table = [block.cols, ...rows.map((row) => fields.map((f) => row[f] ?? ''))];
|
||||
onField(`table:${at}`, JSON.stringify(table));
|
||||
}
|
||||
}
|
||||
|
||||
export function ViewRegion({ detail, onReplyOrder, values, onField, locate }: ViewRegionProps) {
|
||||
const [view, setView] = useState<Tab>('open');
|
||||
const [page, setPage] = useState(0);
|
||||
/** 表单值是**过程状态**,只活在这一次填写里:回传成功后画面由服务端重投影 */
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
/** 工作成果:列表里点开的那份定稿报告 */
|
||||
const [doneSel, setDoneSel] = useState<string | null>(null);
|
||||
const [openFile, setOpenFile] = useState<FileDoc | undefined>(undefined);
|
||||
|
||||
const orders = detail.workOrders;
|
||||
const openList = orders.filter((o) => o.done === undefined);
|
||||
/* 未读条数 —— 页签那枚红气泡读的就是它;标记按钮按一下这个数才动 */
|
||||
const unreadN = detail.comms.filter((m) => m.unread).length;
|
||||
|
||||
/*
|
||||
* 标记已读 / 未读 —— 乐观更新交给失效:标完服务端广播 task 触点,
|
||||
* 这一屏跟着重取。不在前端手写缓存(订阅层统一做,组件不自建过程状态)。
|
||||
*/
|
||||
const queryClient = useQueryClient();
|
||||
/**
|
||||
* 件上按下「保存」→ **落草稿层 + 让服务端按新数据重渲那一帧**。
|
||||
*
|
||||
* 不落库,人改的东西只活在浏览器里,刷新一次就没了;不重渲,由数据算出来的东西
|
||||
* (类徽 A/B/C、分布条、结论横幅那条去向)会停在上一次渲染那一刻 ——
|
||||
* 实测:人把三条料全改成 A 类,底下的去向还写着「去选型」。
|
||||
* 落完重取任务投影,整份件跟着一起变。**工单不办结,放行门的位置没动。**
|
||||
*/
|
||||
const saveTable = useMutation({
|
||||
mutationFn: (save: ViewSave) =>
|
||||
call(saveTableRoute, {
|
||||
params: { taskId: detail.card.id },
|
||||
body: {
|
||||
tables: save.tables as Record<string, Array<Record<string, string>>>,
|
||||
drops: save.drops as Record<string, Array<Record<string, string>>>,
|
||||
},
|
||||
}),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['desk', 'task', detail.card.id] }),
|
||||
});
|
||||
|
||||
const markRead = useMutation({
|
||||
mutationFn: (input: { commId: string; read: boolean }) =>
|
||||
call(markCommReadRoute, { params: { commId: input.commId }, body: { read: input.read } }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['desk'] }),
|
||||
});
|
||||
const doneList = orders.filter((o) => o.done !== undefined);
|
||||
/*
|
||||
* **字签下了,但这一单还没收尾** —— 签完到收尾之间那一小段(合并 / 发口 / 落终态)。
|
||||
* 认的是「有一张办结的验收单 + 还没有终态」:两者同时成立只有这一种局面。
|
||||
*/
|
||||
const signed =
|
||||
detail.card.terminal === undefined &&
|
||||
orders.some((o) => o.mode === 'acceptance' && o.done !== undefined && !isRejection(o.done.result));
|
||||
const pos = Math.min(page, Math.max(0, openList.length - 1));
|
||||
const cur = openList[pos];
|
||||
const doneItem = doneSel === null ? undefined : doneList.find((o) => o.id === doneSel);
|
||||
|
||||
/*
|
||||
* 对话区点击 → 跳到工单所在页签与页位(渲染期调整 state,seen 保证幂等 —— PRD 同款)。
|
||||
* 办结的跳成果详情;待办的跳待处理并翻到那一页。
|
||||
*/
|
||||
const [seenLoc, setSeenLoc] = useState(0);
|
||||
if (locate != null && locate.seq !== seenLoc) {
|
||||
setSeenLoc(locate.seq);
|
||||
if (locate.tab !== undefined) {
|
||||
setOpenFile(undefined);
|
||||
setView(locate.tab);
|
||||
setPage(0);
|
||||
setDoneSel(null);
|
||||
}
|
||||
const hit = orders.find((o) => o.id === locate.orderId);
|
||||
if (hit !== undefined) {
|
||||
setOpenFile(undefined);
|
||||
if (hit.done !== undefined) {
|
||||
setView('done');
|
||||
setDoneSel(hit.id);
|
||||
} else {
|
||||
setView('open');
|
||||
setPage(Math.max(0, openList.findIndex((o) => o.id === locate.orderId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const go = (to: Tab) => {
|
||||
setView(to);
|
||||
setPage(0);
|
||||
setDoneSel(null);
|
||||
};
|
||||
|
||||
const terminalLabel = detail.card.terminal === undefined ? undefined : TERMINAL_STATE_LABEL[detail.card.terminal];
|
||||
@@ -114,6 +231,8 @@ export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
onClick={() => go('comms')}
|
||||
>
|
||||
<Ico name="mail" s={14} /> 沟通记录
|
||||
{/* 未读气泡:红的,数字是**未读条数**,不是总条数 —— 它要回答「有没有我还没看的」 */}
|
||||
{unreadN > 0 && <em className="jc-unread">{unreadN}</em>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -155,6 +274,10 @@ export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
<button type="button" className="jc-tnav-btn jc-tnav-back" onClick={() => setOpenFile(undefined)}>
|
||||
<Ico name="arrow-left" s={14} /> 返回
|
||||
</button>
|
||||
) : view === 'done' && doneItem !== undefined ? (
|
||||
<button type="button" className="jc-tnav-btn back" onClick={() => setDoneSel(null)}>
|
||||
<Ico name="chevron-left" s={14} /> 返回列表
|
||||
</button>
|
||||
) : view === 'open' ? (
|
||||
<span className="jc-tnav-pg" aria-label="翻页">
|
||||
<button
|
||||
@@ -184,7 +307,7 @@ export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
|
||||
{/* key:换件 / 换页签即重挂载滚动区,回到正文顶部 */}
|
||||
<ScrollArea
|
||||
key={`${view}:${cur?.id ?? '-'}`}
|
||||
key={`${view}:${view === 'done' ? (doneSel ?? 'list') : (cur?.id ?? '-')}`}
|
||||
className="jc-sa--fill"
|
||||
vpClassName="wf-body"
|
||||
inset={-6}
|
||||
@@ -193,26 +316,88 @@ export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
<FileViewer doc={openFile} />
|
||||
) : view === 'comms' ? (
|
||||
/*
|
||||
* 「还没接」和「暂无数据」是两回事,占位不能混:
|
||||
* 邮件与通道消息的数据在 `channel` / `event` 里确实有,但按单归档的投影还没做 ——
|
||||
* 所以在这儿等下去也不会自己冒出内容来。说「都会归到这里」就是把没接说成了空,
|
||||
* 人会一直等,或者以为坏了。如实说没接通,再说接通之后是什么样。
|
||||
* 沟通记录 = 一单往来的**原件**。进来的那一封是开案的那份料 ——
|
||||
* 人一点开这单就该看见「这单从哪来的」,不必等模型跑几拍再说。
|
||||
*
|
||||
* 附件那一行照抄基座读出来的说明:读出多少字 / 为什么读不出。
|
||||
* **摆一个「已上传 ✓」是撒谎** —— 活动读不出内容的件,人得当场知道。
|
||||
*/
|
||||
<EmptyState
|
||||
ico="mail"
|
||||
tone="muted"
|
||||
title="往来记录还没接通"
|
||||
desc="跟客户、供应商往来的邮件和消息,目前还没有按单归档到这一屏。接通后连原件一起存:谈过什么、什么时候谈的,回头都查得到。"
|
||||
/>
|
||||
detail.comms.length === 0 ? (
|
||||
<EmptyState
|
||||
ico="mail"
|
||||
tone="muted"
|
||||
title="这一单还没有往来记录"
|
||||
desc="跟客户、供应商往来的邮件和消息会按单归到这里,连原件一起存:谈过什么、什么时候谈的,回头都查得到。"
|
||||
/>
|
||||
) : (
|
||||
<div className="jc-comms">
|
||||
{detail.comms.map((m) => (
|
||||
<article key={m.id} className={'jc-comm t-' + m.direction + (m.unread ? ' unread' : '')}>
|
||||
<header className="jc-comm-h">
|
||||
<span className={'jc-comm-dir t-' + m.direction}>
|
||||
<Ico name={m.direction === 'in' ? 'corner-down-left' : 'corner-up-right'} s={12} />
|
||||
{m.direction === 'in' ? '收到' : '发出'}
|
||||
</span>
|
||||
<b className="jc-comm-sub">{m.subject}</b>
|
||||
{/* 未读点:一眼扫得出哪几封还没看 —— 与右边那个按钮说的是同一件事 */}
|
||||
{m.unread && <span className="jc-comm-new">未读</span>}
|
||||
<span className="jc-comm-at jc-mono">{atMinute(m.at)}</span>
|
||||
{/*
|
||||
标记按钮 —— **气泡计数只跟着这一下走**。
|
||||
不做「打开页签即已读」:那是替人宣布他看过了,而他可能只是路过。
|
||||
反向也留着:看了一半想留个记号,标回未读。
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
className={'jc-comm-mark' + (m.unread ? '' : ' done')}
|
||||
disabled={markRead.isPending}
|
||||
aria-label={m.unread ? `把「${m.subject}」标为已读` : `把「${m.subject}」标回未读`}
|
||||
onClick={() => markRead.mutate({ commId: m.id, read: m.unread })}
|
||||
>
|
||||
<Ico name={m.unread ? 'check' : 'rotate-ccw'} s={11} />
|
||||
{m.unread ? '标为已读' : '标回未读'}
|
||||
</button>
|
||||
</header>
|
||||
<div className="jc-comm-meta">
|
||||
<span className="jc-mono">{m.channel}</span>
|
||||
<span>{m.direction === 'in' ? '来自' : '发往'}</span>
|
||||
<b>{m.party === '' ? '未署名' : m.party}</b>
|
||||
{m.note !== '' && <em>{m.note}</em>}
|
||||
</div>
|
||||
{m.body !== '' && <pre className="jc-comm-body">{m.body}</pre>}
|
||||
{m.attachments.length > 0 && (
|
||||
<div className="jc-comm-att">
|
||||
{m.attachments.map((a) => (
|
||||
<span key={a.name} className={'jc-att' + (a.readable === true ? '' : ' blind')}>
|
||||
<Ico name={a.readable === true ? 'file-text' : 'alert-triangle'} s={12} />
|
||||
<b>{a.name}</b>
|
||||
{a.size !== undefined && <i className="jc-mono">{(a.size / 1024).toFixed(0)} KB</i>}
|
||||
{a.note !== undefined && <em>{a.note}</em>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : view === 'wf' ? (
|
||||
<WfDetail detail={detail} openN={openList.length} doneN={orders.length - openList.length} />
|
||||
) : view === 'log' ? (
|
||||
detail.log.length > 0 ? (
|
||||
<div className="jc-log">
|
||||
{detail.log.map((line) => (
|
||||
<div key={line.id} className={'jc-log-row t-' + line.side}>
|
||||
<span className="jc-log-at jc-mono">{line.at.slice(5, 16).replace('T', ' ')}</span>
|
||||
<span className="jc-log-side">{line.sideLabel}</span>
|
||||
/*
|
||||
* **三色一档一意**(2026-08-02 用户定):负责人绿 · 智能体蓝 · 系统灰。
|
||||
* 从前按归侧上色,而归侧有五种 —— 那是**度量**的分法
|
||||
* (自主率要把「等外面」与「机器干的」分开);人扫这一屏只问
|
||||
* 「这一步是我办的、AI 办的,还是流程自己走到的」。
|
||||
*/
|
||||
<div key={line.id} className={'jc-log-row t-' + line.who}>
|
||||
<span className="jc-log-at jc-mono">{atMinute(line.at)}</span>
|
||||
{/* 章摆的是**动作**(四字,与对话区同词),不是归侧 ——
|
||||
归侧仍在 `t-${side}` 的配色里,度量也照旧靠它 */}
|
||||
<span className="jc-log-side">{line.action}</span>
|
||||
<span className="jc-log-t">
|
||||
<b>{line.title}</b>
|
||||
{line.summary !== undefined && <em>{line.summary}</em>}
|
||||
@@ -229,35 +414,185 @@ export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
/>
|
||||
)
|
||||
) : view === 'done' ? (
|
||||
<EmptyState
|
||||
ico="archive"
|
||||
tone="brand"
|
||||
title="还没有成果"
|
||||
desc="这一单产出的报告与文件会归档在这里,一行一份、点开整篇看。"
|
||||
/>
|
||||
doneItem !== undefined ? (
|
||||
/*
|
||||
* 定稿归档 = **同一份件,不加编辑态**(§9.3 第 2 条:在办与归档同一张模版)。
|
||||
* 与待处理那一屏一样,这里也只放件本身 —— 判据留在对话区那张办结卡上,
|
||||
* 归档之后「凭什么算过」比当时更常被问起,所以它跟着卡走,不跟着件走。
|
||||
*/
|
||||
doneItem.mode === 'acceptance' && detail.viewHtml !== undefined ? (
|
||||
<ViewContainer html={detail.viewHtml} title={`${detail.card.activity} · 定稿报告`} />
|
||||
) : (
|
||||
<article className="jc-mst-doc">
|
||||
<WorkOrderReport
|
||||
order={doneItem}
|
||||
detail={detail}
|
||||
typeLine={doneItem.mode === 'acceptance' ? '验收工单 · 定稿归档' : '执行工单 · 定稿归档'}
|
||||
blocks={doneItem.blocks !== undefined && doneItem.blocks.length > 0 ? doneItem.blocks : detail.view}
|
||||
values={{}}
|
||||
onField={() => undefined}
|
||||
onReply={() => undefined}
|
||||
/>
|
||||
</article>
|
||||
)
|
||||
) : doneList.length > 0 ? (
|
||||
/* 一行一份成果报告,点进去看整篇(PRD:jc-dvs 列表) */
|
||||
<div className="jc-dvs">
|
||||
{doneList.map((o) => {
|
||||
const res = o.done?.result;
|
||||
const bad = res !== undefined && isRejection(res);
|
||||
return (
|
||||
<button
|
||||
key={o.id}
|
||||
type="button"
|
||||
/*
|
||||
* **执行与验收两种颜色**(2026-08-02 用户定):一摞成果里
|
||||
* 「我办过的那张单」与「我签过字的那一关」是两类东西 ——
|
||||
* 同色排下来,人得逐行读标题才分得出。退回的仍压过颜色走警示色。
|
||||
*/
|
||||
className={
|
||||
'jc-dv jc-dv-row ' + (bad ? 's-bad' : 's-ok') + (o.mode === 'acceptance' ? ' m-acc' : ' m-exe')
|
||||
}
|
||||
onClick={() => setDoneSel(o.id)}
|
||||
>
|
||||
<span className="jc-dv-h">
|
||||
<span className="jc-dv-ico">
|
||||
<Ico name={o.mode === 'acceptance' ? 'shield-check' : 'file-text'} s={16} />
|
||||
</span>
|
||||
<span className="jc-dv-t">
|
||||
<em>
|
||||
派发 {atMinute(o.at)}
|
||||
{o.done !== undefined ? ` · 办结 ${atMinute(o.done.at)}` : ''}
|
||||
</em>
|
||||
<b>{o.title}</b>
|
||||
</span>
|
||||
<span className="jc-dv-st">
|
||||
{bad ? '已退回' : o.mode === 'acceptance' ? '验收通过' : '已办结'}
|
||||
</span>
|
||||
<Ico name="chevron-right" s={15} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
ico="archive"
|
||||
tone="brand"
|
||||
title="还没有成果报告"
|
||||
desc="办结一件工单,它就会定稿成一份可回看的报告,归档在这里 —— 一行一份、点开整篇看。"
|
||||
hint="先去「待处理」把手上的事办完"
|
||||
/>
|
||||
)
|
||||
) : 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}
|
||||
/>
|
||||
* **工作区只放那份件本身**(2026-08-01 用户定)。
|
||||
*
|
||||
* 从前这里是 `WorkOrderReport`:它自带题头、编号带、依据带、判据表、
|
||||
* 章节头,再把活动推的那一屏**包在自己里面** —— 于是人看到的是
|
||||
* 「一份报告里嵌着另一份报告」,两套版面语言叠在一起。用户原话:
|
||||
* 「你用你自己的报告格式嵌套了一层我要的报告格式」。
|
||||
*
|
||||
* 现在分家:件归工作区(整篇铺开、可就地改),
|
||||
* **判据与作业指导书跟着「要你办的这件事」走,落在对话区的卡上**;
|
||||
* 提交那一下也在那儿 —— 编辑与提交分在两个区,缓冲由壳层持有(values)。
|
||||
*
|
||||
* 活动没推件的(还没到推件那一拍、或包里就没有模版)才退回工单报告:
|
||||
* 那时工作区总不能是白的。
|
||||
*/
|
||||
detail.viewHtml !== undefined ? (
|
||||
<ViewContainer
|
||||
html={detail.viewHtml}
|
||||
title={`${detail.card.activity} · 汇报报告`}
|
||||
editable
|
||||
onEdits={(save) => {
|
||||
/*
|
||||
* **落库按地址走** —— 件自己说了它是哪张表(`data-edit="域.对象"`),
|
||||
* 原样送上去即可。从前借道当下那张卡的 `blocks[i]` 收列名,于是
|
||||
* 卡上没有那一块时(熔断卡就没有)**保存一格都送不出去**,
|
||||
* 而屏上照旧有闸、有编辑、有保存 —— 界面给了个落不了地的口子。
|
||||
*
|
||||
* 缓冲那一份仍按卡的形状收(`table:N`):卡上那张表由提交那一下带走,
|
||||
* 与件上这一张是两条路,卡没有对应的块时它自然收不到东西,不影响落库。
|
||||
*/
|
||||
saveTable.mutate(save);
|
||||
collectEdits(save.tables, cur, onField);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<article className="jc-mst-doc">
|
||||
<WorkOrderReport
|
||||
order={cur}
|
||||
detail={detail}
|
||||
typeLine={cur.mode === 'acceptance' ? '验收工单 · 终局唯放行' : '执行工单 · 作业指导书 + 表单'}
|
||||
blocks={cur.blocks !== undefined && cur.blocks.length > 0 ? cur.blocks : detail.view}
|
||||
values={values}
|
||||
onField={onField}
|
||||
onReply={onReplyOrder}
|
||||
/>
|
||||
{cur.mode === 'execution' && <GuideCard order={cur} />}
|
||||
</article>
|
||||
)
|
||||
) : (
|
||||
<EmptyState
|
||||
ico="list-checks"
|
||||
tone="brand"
|
||||
title="手上没有待办"
|
||||
desc="需要你执行或验收的事会送到这里,带着办这件事要用的全部上下文 —— 不用你去别处找。"
|
||||
hint="AI 正在往下推进,轮到你时会第一时间出现"
|
||||
/>
|
||||
/*
|
||||
* 空待处理**四态分明** —— 状态如实,不许一句「没有待办」把四种局面糊成一种:
|
||||
* 推进中 = AI 在自己转(动画场景);等外部 = 环停着听回音;
|
||||
* 都办完了 = 你的活清零、单还在走;已收尾 = 一切定稿。
|
||||
*/
|
||||
detail.card.terminal !== undefined ? (
|
||||
<EmptyState
|
||||
ico="circle-check"
|
||||
tone="ok"
|
||||
title="这单已收尾"
|
||||
desc="所有工单都定稿归档了。要回看哪一份,去「工作成果」一行一份点开整篇。"
|
||||
/>
|
||||
) : signed ? (
|
||||
/*
|
||||
* **签完到收尾之间那一小段**(2026-08-02 用户:「点完验收按钮后,
|
||||
* 状态变化时交互上会闪一下中间态,视觉效果不好」)。
|
||||
*
|
||||
* 那一闪不是渲染毛病,是**真的中间态**:字签下去了,合并、发口、收尾还在跑。
|
||||
* 从前这一段落在通用四态里 —— 待办清零 → 「都办完了」闪一下 → 再跳「已收尾」。
|
||||
* 「都办完了」这句话在那一刻是对的,可它说的是**另一件事**(你手上没活了),
|
||||
* 于是读起来像跳了一帧。
|
||||
*
|
||||
* 现在这一段有自己的话:**字已签下,正在落地**。它不是为了盖住中间态 ——
|
||||
* 中间态本来就该说出来,只是要说对。
|
||||
*/
|
||||
<EmptyState
|
||||
ico="hourglass"
|
||||
tone="brand"
|
||||
title="字已签下,正在落地"
|
||||
desc="成果正在并进正式表、按去向发往下游。落完这一屏会换成收尾结果 —— 不用刷新。"
|
||||
hint="通常几秒钟"
|
||||
/>
|
||||
) : detail.card.runtime === 'running' ? (
|
||||
<WorkingScene doneN={doneList.length} />
|
||||
) : detail.card.runtime === 'awaiting-event' ? (
|
||||
<EmptyState
|
||||
ico="hourglass"
|
||||
tone="brand"
|
||||
title="AI 在等一个外部回音"
|
||||
desc="等的可能是一封回信、一批数据、或一个时点。回音一到它就接着推进;需要你出手时,工单会带着完整上下文出现在这里。"
|
||||
hint="不用盯着 —— 轮到你时左侧列表会亮角标"
|
||||
/>
|
||||
) : doneList.length > 0 ? (
|
||||
<EmptyState
|
||||
ico="circle-check"
|
||||
tone="ok"
|
||||
title="都办完了"
|
||||
desc="手上的工单清零。AI 接着往下推进,需要你执行或验收的,会第一时间送到这里。"
|
||||
hint="工作成果里可以回看每一份定稿报告"
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
ico="list-checks"
|
||||
tone="brand"
|
||||
title="手上没有待办"
|
||||
desc="需要你执行或验收的事会送到这里,带着办这件事要用的全部上下文 —— 不用你去别处找。"
|
||||
hint="轮到你时会第一时间出现"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ScrollArea>
|
||||
</section>
|
||||
@@ -265,6 +600,41 @@ export function ViewRegion({ detail, onReplyOrder }: ViewRegionProps) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ═════════════════ AI 推进中 · 空工作区的工作场景 ═════════════════ */
|
||||
|
||||
/**
|
||||
* AI 正在办这单、还没轮到人 —— 空待处理不是「没有东西」,是**有人在干活**。
|
||||
* 涟漪说「环在转」,微光工作线说「报告正在一行行写出来」;
|
||||
* 都是纯 CSS、只动 transform/opacity,prefers-reduced-motion 下静止但状态仍可读。
|
||||
*/
|
||||
function WorkingScene({ doneN }: { doneN: number }) {
|
||||
return (
|
||||
<div className="jc-working" aria-live="polite">
|
||||
<span className="jc-working-art" aria-hidden>
|
||||
<i className="rp" />
|
||||
<i className="rp rp2" />
|
||||
<span className="jc-working-ico">
|
||||
<Ico name="bot" s={26} />
|
||||
</span>
|
||||
</span>
|
||||
<b>AI 正在推进这单</b>
|
||||
<p>
|
||||
看材料、写台账、调工具都由它来;需要你执行或验收的,工单会带着完整上下文出现在这里。
|
||||
{doneN > 0 ? `此前你办结的 ${doneN} 件都在「工作成果」里。` : ''}
|
||||
</p>
|
||||
<span className="jc-working-lines" aria-hidden>
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span className="jc-working-hint">
|
||||
<i className="jc-pulse-dot" />
|
||||
实时盯着 · 有进展这里立即更新
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ═════════════════ 任务详情:KPI 带 + 流程链 + 上下文档案 ═════════════════ */
|
||||
|
||||
function WSec({ ico, label, aside }: { ico: string; label: string; aside?: React.ReactNode }) {
|
||||
@@ -386,7 +756,7 @@ function WfDetail({ detail, openN, doneN }: { detail: TaskDetail; openN: number;
|
||||
<em>编号</em>
|
||||
<p className="jc-mono">
|
||||
{detail.card.id}
|
||||
<span className="jc-mono">起于 {detail.card.startedAt.slice(0, 16).replace('T', ' ')}</span>
|
||||
<span className="jc-mono">起于 {atFull(detail.card.startedAt)}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user