Archived
活动模块 L3 实例详情页:过程时间线为主角的主从布局;终止/编辑/删除收在最深层;L2 查看详情接入
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
* feature `gov-task` 唯一出口(编码规范 §9.1)。
|
||||
* 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看
|
||||
*/
|
||||
export { TasksHome, ActvPage, WfPage, TaskPage, TaskLogPage, WorkflowBoard } from './internal/panel.tsx';
|
||||
export { TasksHome, ActiPage, ActvPage, WfPage, TaskPage, TaskLogPage, WorkflowBoard } from './internal/panel.tsx';
|
||||
|
||||
@@ -294,7 +294,7 @@ function inTimeRange(at: string, from: string, to: string): boolean {
|
||||
* 沙箱区:加载服务器返回的活动视图 HTML —— iframe + sandbox 隔离;
|
||||
* v1 静态占位页,src 与内容高度通讯(postMessage)待接口一起定。
|
||||
*/
|
||||
export function ActvPage({ id }: { id: string }) {
|
||||
export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const a = DEMO_ACTIVITIES.find((x) => x.id === id);
|
||||
const [status, setStatus] = useState<'active' | 'disabled' | undefined>(undefined);
|
||||
const [blocked, setBlocked] = useState(false);
|
||||
@@ -418,7 +418,7 @@ export function ActvPage({ id }: { id: string }) {
|
||||
</Note>
|
||||
)}
|
||||
|
||||
<Sec ico="repeat" title="历史活动" extra={`共 ${instances.length} 个 · 点行进 L3(即将开放)`}>
|
||||
<Sec ico="repeat" title="历史活动" extra={`共 ${instances.length} 个 · 查看详情进 L3`}>
|
||||
{/* 统计带 + 检索条同一行(2026-08-26 用户定:检索放右侧)——
|
||||
左:总数 + 五态灰字合计(状态色只进 6px 小圆点);右:条件下拉 + 随条件换控件 */}
|
||||
<div className="g-ihead">
|
||||
@@ -568,7 +568,8 @@ export function ActvPage({ id }: { id: string }) {
|
||||
终止活动
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="g-rowbtn" title="L3 实例详情即将开放">
|
||||
{/* L3 实例详情(2026-08-26 接):路由 id 带活动与实例两段,详情页按活动找回上下文 */}
|
||||
<button type="button" className="g-rowbtn" onClick={() => nav.push({ k: 'acti', id: `${a.id}:${r.id}` })}>
|
||||
查看详情
|
||||
</button>
|
||||
</span>
|
||||
@@ -635,6 +636,255 @@ export function ActvPage({ id }: { id: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ==================== 实例详情(L3,2026-08-26) ==================== */
|
||||
|
||||
/** 动作过程的一步(占位形状,按将来接口铺):步骤 / 执行方 / 时间 / 用时 / 结果 / 一句说明 */
|
||||
type DemoStep = {
|
||||
name: string;
|
||||
by: 'agent' | 'human';
|
||||
/** `MM-DD HH:mm`,未到达的步骤为空串 */
|
||||
at: string;
|
||||
/** 完成步的用时,如 `6m`;未完成为空串 */
|
||||
dur: string;
|
||||
note: string;
|
||||
res: 'done' | 'fail' | 'current' | 'pending';
|
||||
};
|
||||
|
||||
/**
|
||||
* 活动一次运行的节点流(占位)—— agent 跑规划与执行,验收这一步永远留给人
|
||||
* (2026-08-25 用户定的人机分工:AI 不能承担责任,负责人验收规划与统计产出)。
|
||||
*/
|
||||
const STEP_FLOW: ReadonlyArray<{ name: string; by: 'agent' | 'human'; dur: string; note: string }> = [
|
||||
{ name: '输入接收', by: 'agent', dur: '1m', note: '询价单已进入,要素识别完成,实例正式起步。' },
|
||||
{ name: '要素结构化', by: 'agent', dur: '6m', note: '抽取客户 / 物料 / 交期等字段,落入结构化模型。' },
|
||||
{ name: '定价测算', by: 'agent', dur: '12m', note: '按成本模型逐项给出建议价与毛利测算。' },
|
||||
{ name: '负责人验收', by: 'human', dur: '5m', note: '确认测算结果 —— 规划类产出必须由人验收担责。' },
|
||||
{ name: '输出归档', by: 'agent', dur: '1m', note: '生成报价文件并归档,产出回写台账。' },
|
||||
];
|
||||
|
||||
/** 各步相对启动时间的偏移(分钟,占位)—— 真数据随接口时间戳一起换 */
|
||||
const STEP_OFFSET = [0, 3, 9, 21, 26] as const;
|
||||
|
||||
/** 占位时间 `MM-DD HH:mm`(2026 年)的加分钟格式化 —— 与 instanceTime 配套 */
|
||||
function fmtMinute(t: number): string {
|
||||
const d = new Date(t);
|
||||
const p = (n: number): string => String(n).padStart(2, '0');
|
||||
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** 步工厂:以启动时间为零点铺某一步(占位)—— note 可覆盖默认说明(如「被手动终止」) */
|
||||
function mkStep(t0: number, i: number, res: DemoStep['res'], note?: string): DemoStep {
|
||||
return {
|
||||
...STEP_FLOW[i]!,
|
||||
...(note === undefined ? {} : { note }),
|
||||
res,
|
||||
at: res === 'pending' ? '' : fmtMinute(t0 + STEP_OFFSET[i]! * 60000),
|
||||
dur: res === 'done' ? STEP_FLOW[i]!.dur : '',
|
||||
};
|
||||
}
|
||||
|
||||
/** 按实例状态铺五步的结果(占位演示):进行中走到测算,驳回倒在验收,失败倒在结构化,待执行一步未走 */
|
||||
function demoSteps(state: InstanceState, startedAt: string): readonly DemoStep[] {
|
||||
const t0 = instanceTime(startedAt);
|
||||
const mk = (i: number, res: DemoStep['res']): DemoStep => mkStep(t0, i, res);
|
||||
switch (state) {
|
||||
case '已完成':
|
||||
return [mk(0, 'done'), mk(1, 'done'), mk(2, 'done'), mk(3, 'done'), mk(4, 'done')];
|
||||
case '进行中':
|
||||
return [mk(0, 'done'), mk(1, 'done'), mk(2, 'current'), mk(3, 'pending'), mk(4, 'pending')];
|
||||
case '待执行':
|
||||
return [mk(0, 'pending'), mk(1, 'pending'), mk(2, 'pending'), mk(3, 'pending'), mk(4, 'pending')];
|
||||
case '驳回':
|
||||
return [mk(0, 'done'), mk(1, 'done'), mk(2, 'done'), mk(3, 'fail'), mk(4, 'pending')];
|
||||
case '失败':
|
||||
return [mk(0, 'done'), mk(1, 'fail'), mk(2, 'pending'), mk(3, 'pending'), mk(4, 'pending')];
|
||||
}
|
||||
}
|
||||
|
||||
/** 手动终止的过程面(占位):终止发生在测算步 —— 走完的留痕,当前步记「已终止」,后面不再走 */
|
||||
function terminatedSteps(startedAt: string): readonly DemoStep[] {
|
||||
const t0 = instanceTime(startedAt);
|
||||
return [
|
||||
mkStep(t0, 0, 'done'),
|
||||
mkStep(t0, 1, 'done'),
|
||||
mkStep(t0, 2, 'fail', '定价测算进行中,被负责人手动终止 —— 已算出的中间结果留痕保留。'),
|
||||
mkStep(t0, 3, 'pending'),
|
||||
mkStep(t0, 4, 'pending'),
|
||||
];
|
||||
}
|
||||
|
||||
/** 步结果的色调类 —— 复用全局 t-*(色即状态,色盲可读:结果词在点旁边) */
|
||||
const STEP_RES_TONE: Readonly<Record<DemoStep['res'], string>> = {
|
||||
done: 't-ok',
|
||||
fail: 't-danger',
|
||||
current: 't-brand',
|
||||
pending: 't-mute',
|
||||
};
|
||||
|
||||
/**
|
||||
* L3 · 实例详情 —— 一次运行的完整视图(2026-08-26 规划,用户确认后落地):
|
||||
* 主从 2:1(DESIGN-SPEC §9)—— 左「动作过程记录」是主角(追溯与验收的落点),
|
||||
* 右栏输入/输出是辅助;编辑与删除收在这一层(功能 5/6),不污染清单。
|
||||
* 数据为静态占位,结构按将来接口的形状铺。
|
||||
*/
|
||||
export function ActiPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const [aid, iid] = id.split(':');
|
||||
const a = DEMO_ACTIVITIES.find((x) => x.id === aid);
|
||||
const inst = a !== undefined ? demoInstances(a.owner).find((x) => x.id === iid) : undefined;
|
||||
/* 状态就地翻转(本期前端演示):终止暂落「失败」(归态接口语义未定),对接后走 mutation 由服务端重投影;
|
||||
term 记下终止时刻 —— 终止的过程面与「自己跑挂」不同(当前步记「已终止」,不是「附件缺失」那种失败原因) */
|
||||
const [term, setTerm] = useState<string | undefined>(undefined);
|
||||
if (a === undefined || inst === undefined) {
|
||||
return <Note tone="warn">实例不存在 —— 它可能已被删除,从面包屑返回活动详情看看。</Note>;
|
||||
}
|
||||
const state: InstanceState = term !== undefined ? '失败' : inst.state;
|
||||
const steps = term !== undefined ? terminatedSteps(inst.startedAt) : demoSteps(state, inst.startedAt);
|
||||
const doneN = steps.filter((s) => s.res === 'done').length;
|
||||
/* 结束时间/耗时:占位按「第五步走完」推(27m),真数据随接口一起换 */
|
||||
const endedAt = fmtMinute(instanceTime(inst.startedAt) + 27 * 60000);
|
||||
const reason =
|
||||
term !== undefined
|
||||
? `已于 ${term} 由 ${inst.owner} 手动终止 —— 终止后归入哪个状态接口语义未定,本期前端落「失败」占位。`
|
||||
: state === '驳回'
|
||||
? `验收驳回:测算毛利低于阈值,退回重新测算 —— 负责人 ${inst.owner}。`
|
||||
: state === '失败'
|
||||
? '要素结构化失败:询价单附件缺失,已通知发起人补齐后重试。'
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={'g-actid' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
||||
<PageHead
|
||||
ico={a.ico}
|
||||
tone={INSTANCE_ST[state]}
|
||||
kicker={`${a.name} · 一次运行`}
|
||||
title={inst.id}
|
||||
chips={
|
||||
<>
|
||||
{/* key 带状态:终止后状态丸翻转时弹出一次高亮 —— 变化要看得见 */}
|
||||
<Chip key={state} tone={INSTANCE_ST[state]} ico={state === '进行中' ? 'play' : state === '已完成' ? 'circle-check' : state === '待执行' ? 'hourglass' : state === '驳回' ? 'circle-x' : 'power'}>
|
||||
{state}
|
||||
</Chip>
|
||||
<Chip ico="user">{inst.owner}</Chip>
|
||||
<Chip ico="calendar-days">启动 {inst.startedAt}</Chip>
|
||||
{state === '已完成' && <Chip ico="flag">结束 {endedAt} · 耗时 27m</Chip>}
|
||||
{state === '进行中' && <Chip ico="timer">已跑 12m</Chip>}
|
||||
<Chip ico="cpu">{a.llm}</Chip>
|
||||
</>
|
||||
}
|
||||
desc="一次运行的完整视图:输入 / 过程 / 输出 / 状态。编辑与删除收在这一层,清单页不受打扰。"
|
||||
acts={[
|
||||
/* 头部动作(2026-08-26 规划):进行中可终止(二次确认);编辑本期只留入口(编什么待定);删除二次确认(用户标注存疑,先按规划做) */
|
||||
...(state === '进行中'
|
||||
? [
|
||||
{
|
||||
label: '终止活动',
|
||||
ico: 'power',
|
||||
danger: true,
|
||||
ask: {
|
||||
title: `确认终止实例 ${inst.id}?`,
|
||||
note: '进行中的步骤就地停下,不再产生后续动作;已经走完的步骤留痕保留。',
|
||||
impact: { who: '该实例当前与后续的全部动作', undo: '不可恢复 —— 只能重新启动一次新运行', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
||||
okLabel: '确认终止',
|
||||
onOk: () => setTerm(fmtMinute(Date.now())),
|
||||
},
|
||||
} satisfies ActSpec,
|
||||
]
|
||||
: []),
|
||||
{ label: '编辑实例', ico: 'pencil-line', disabled: true },
|
||||
{
|
||||
label: '删除实例',
|
||||
ico: 'trash-2',
|
||||
danger: true,
|
||||
ask: {
|
||||
title: `确认删除实例 ${inst.id}?`,
|
||||
note: '删除后,这次运行的输入 / 过程 / 输出记录一并移除,清单里也不再出现。',
|
||||
impact: { who: '该实例及其全部运行记录', undo: '不可撤销 —— 删除即清空', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
||||
okLabel: '确认删除',
|
||||
onOk: () => nav.push({ k: 'actd', id: a.id }),
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{reason !== undefined && <Note tone="warn">{reason}</Note>}
|
||||
{state === '待执行' && <Note>待执行:已排入队列,等待调度 —— 到点或由负责人手动起步。</Note>}
|
||||
|
||||
<div className="g-actid-grid">
|
||||
{/* 主角:动作过程记录 —— 时间线竖排,当前步呼吸高亮,未到达灰显(2026-08-26 用户定:过程是实例页的重点) */}
|
||||
<Sec ico="list-checks" title="动作过程记录" extra={`${doneN} / ${steps.length} 步`}>
|
||||
<ol className="g-tl">
|
||||
{steps.map((s, i) => (
|
||||
<li key={s.name} className={`g-tl-row ${s.res} ${STEP_RES_TONE[s.res]}`}>
|
||||
<span className="g-tl-rail" aria-hidden>
|
||||
<i className="g-tl-dot" />
|
||||
{i < steps.length - 1 && <i className="g-tl-line" />}
|
||||
</span>
|
||||
<div className="g-tl-c">
|
||||
<div className="g-tl-h">
|
||||
<b>
|
||||
{i + 1} · {s.name}
|
||||
</b>
|
||||
<span className="g-tl-by">
|
||||
<Ico name={s.by === 'agent' ? 'cpu' : 'user'} s={11} />
|
||||
{s.by === 'agent' ? a.llm : `负责人 · ${inst.owner}`}
|
||||
</span>
|
||||
{/* 时间 meta 只摆事实(什么时候 / 用了多久),结果交给右侧结果词 —— 不重复说两遍 */}
|
||||
<span className="g-tl-meta">
|
||||
{s.res === 'pending' ? '' : s.res === 'current' ? `${s.at} 起` : `${s.at} · 用时 ${s.dur}`}
|
||||
</span>
|
||||
<span className="g-tl-res">
|
||||
{s.res === 'done' ? '完成' : s.res === 'fail' ? (term !== undefined ? '已终止' : state === '驳回' ? '驳回' : '失败') : s.res === 'current' ? '进行中' : '未到达'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="g-tl-note">{s.note}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Sec>
|
||||
|
||||
{/* 辅栏:输入 / 输出 —— 完整输入与最终产出(2026-08-26 规划);文件下载随接口开放(票据机制已备) */}
|
||||
<div className="g-actid-rail">
|
||||
<Sec ico="inbox" title="输入">
|
||||
<div className="g-kv">
|
||||
<div className="g-kv-row">
|
||||
<em>内容</em>
|
||||
<b>{inst.input}</b>
|
||||
</div>
|
||||
<div className="g-kv-row">
|
||||
<em>来源</em>
|
||||
<b>邮件 · quotes@bpos.demo</b>
|
||||
</div>
|
||||
<div className="g-kv-row">
|
||||
<em>提交</em>
|
||||
<b className="g-mono">{inst.startedAt}</b>
|
||||
</div>
|
||||
<div className="g-kv-note">文件类输入的原文下载随接口开放(本期占位)。</div>
|
||||
</div>
|
||||
</Sec>
|
||||
<Sec ico="file-text" title="输出">
|
||||
{inst.output === '—' ? (
|
||||
<div className="g-file-empty">暂无产出 —— 走到「输出归档」后在这里出现。</div>
|
||||
) : (
|
||||
<div className="g-file">
|
||||
<span className="g-file-i">
|
||||
<Ico name="file-text" s={15} />
|
||||
</span>
|
||||
<span className="g-file-n">
|
||||
<b>{inst.output}</b>
|
||||
<em>报价文件 · 输出归档步生成</em>
|
||||
</span>
|
||||
<button type="button" className="g-rowbtn" disabled title="随接口开放下载(票据机制已备)">
|
||||
<Ico name="download" s={12} /> 下载
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Sec>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ② 员工管理 ---------------- */
|
||||
|
||||
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
|
||||
|
||||
Reference in New Issue
Block a user