From 99f021f964c065fb785d5d204f4771b048259e29 Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 15:39:19 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=A8=A1=E5=9D=97=20L3?= =?UTF-8?q?=20=E5=AE=9E=E4=BE=8B=E8=AF=A6=E6=83=85=E9=A1=B5:=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E6=97=B6=E9=97=B4=E7=BA=BF=E4=B8=BA=E4=B8=BB=E8=A7=92?= =?UTF-8?q?=E7=9A=84=E4=B8=BB=E4=BB=8E=E5=B8=83=E5=B1=80;=E7=BB=88?= =?UTF-8?q?=E6=AD=A2/=E7=BC=96=E8=BE=91/=E5=88=A0=E9=99=A4=E6=94=B6?= =?UTF-8?q?=E5=9C=A8=E6=9C=80=E6=B7=B1=E5=B1=82;L2=20=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/gov-shell.tsx | 6 +- apps/web/src/app/styles/console.css | 73 +++++ apps/web/src/features/gov-task/index.ts | 2 +- .../src/features/gov-task/internal/panel.tsx | 256 +++++++++++++++++- apps/web/src/shared/gov/gov-route.ts | 5 + docs/活动模块重构需求-2026-08-25.md | 1 + 6 files changed, 337 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/gov-shell.tsx b/apps/web/src/app/gov-shell.tsx index 1c3faad..c2e46ef 100644 --- a/apps/web/src/app/gov-shell.tsx +++ b/apps/web/src/app/gov-shell.tsx @@ -25,7 +25,7 @@ import { useGovObjects, useGovTasks } from '@web-shared/gov/gov-api.ts'; import { domainShort, sortDomains } from '@web-shared/gov/domains.ts'; import { queryKeys } from './query.ts'; import { BlueprintHome } from '@features/gov-blueprint'; -import { TasksHome, ActvPage, TaskLogPage, TaskPage, WfPage } from '@features/gov-task'; +import { TasksHome, ActiPage, ActvPage, TaskLogPage, TaskPage, WfPage } from '@features/gov-task'; import { RunsHome } from '@features/gov-runs'; import { EmpPage, PeopleHome, RolePage } from '@features/gov-staff'; import { PkgPage, WorkflowsHome } from '@features/gov-process'; @@ -330,7 +330,9 @@ function Detail({ route, nav }: { route: Route; nav: Nav }) { case 'act': return ; case 'actd': - return ; + return ; + case 'acti': + return ; case 'tool': return ; case 'obj': diff --git a/apps/web/src/app/styles/console.css b/apps/web/src/app/styles/console.css index 61af5bd..1a7a550 100644 --- a/apps/web/src/app/styles/console.css +++ b/apps/web/src/app/styles/console.css @@ -635,6 +635,79 @@ @media (prefers-reduced-motion: reduce) { .g-actvd > * { animation: none; } } + +/* ══ 实例详情 L3(2026-08-26,`.g-actid` 作用域)══ + 主从 2:1(DESIGN-SPEC §9):左「动作过程记录」是主角,右栏输入/输出是辅助; + 业务色相从 L1/L2 接过来(--ah 只染页头图标块与文件图标块,一处记忆点) */ +.g-actid { display: flex; flex-direction: column; gap: 16px; --ah: #3370ff; --ah-soft: #eaf1ff; } +.g-actid.h-teal { --ah: #0aa294; --ah-soft: #e0f5f3; } +.g-actid.h-amber { --ah: #d9730d; --ah-soft: #fdf0e0; } +.g-actid.h-violet { --ah: #7f3bf5; --ah-soft: #f1e9fe; } +/* 入场与 L2 同一节奏:自下而上淡入,60ms 逐段 stagger(深入一层,从下进场) */ +.g-actid > * { animation: actvd-in .4s ease-out both; } +.g-actid > *:nth-child(2) { animation-delay: 60ms; } +.g-actid > *:nth-child(3) { animation-delay: 120ms; } +.g-actid > *:nth-child(4) { animation-delay: 180ms; } +.g-actid .g-dh-i { color: var(--ah); background: var(--ah-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ah) 18%, transparent); } +/* 状态丸翻转(终止)时弹出一次高亮 —— 变化要看得见 */ +.g-actid .g-dh .chips > * { animation: actvd-pop .28s ease-out; } + +/* 主从栅格:宽屏 2:1,≤1024 收单列(§14 降级路径) */ +.g-actid-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 16px; align-items: start; } +.g-actid-rail { display: flex; flex-direction: column; gap: 16px; min-width: 0; } +@media (max-width: 1024px) { + .g-actid-grid { grid-template-columns: 1fr; } +} + +/* —— 动作过程记录:竖时间线 —— 轨(点+线)在左,内容在右; + 状态色只进圆点与结果词(--tone 来自行上的 t-* 类),行面不上色 */ +.g-actid .g-tl { list-style: none; margin: 0; padding: 0; background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; overflow: hidden; } +.g-actid .g-tl-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 12px; padding: 12px 14px; transition: background var(--t-fast); } +.g-actid .g-tl-row + .g-tl-row { border-top: 1px solid #eef1f5; } +.g-actid .g-tl-row:hover { background: #f7f9fc; } +.g-actid .g-tl-rail { display: flex; flex-direction: column; align-items: center; padding-top: 4px; } +.g-actid .g-tl-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--tone, #c7cdd6); flex: none; } +/* 连接线向下探出 25px(下行上 padding 12 + 边框 1 + 本行下 padding 12),把时间线串成一条不间断的竖轨 */ +.g-actid .g-tl-line { width: 1px; flex: 1; margin-top: 3px; margin-bottom: -25px; background: #dde2ea; } +/* 未到达:空心点 + 灰字 —— 「还没走到」与「走完了」要一眼分开 */ +.g-actid .g-tl-row.pending .g-tl-dot { background: #fff; box-shadow: inset 0 0 0 2px #d8dee6; } +/* 当前步:呼吸环(全页唯一一处持续动效,§13 克制;reduced-motion 关掉) */ +.g-actid .g-tl-row.current .g-tl-dot { animation: actid-pulse 1.6s ease-out infinite; } +@keyframes actid-pulse { + 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone) 35%, transparent); } + 100% { box-shadow: 0 0 0 7px transparent; } +} +.g-actid .g-tl-c { min-width: 0; } +.g-actid .g-tl-h { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; } +.g-actid .g-tl-h b { font-size: 13px; font-weight: 600; color: var(--ink); } +.g-actid .g-tl-by { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--ink-3); } +.g-actid .g-tl-meta { margin-left: auto; font: 500 11px/1.6 var(--mono); color: var(--ink-4); } +.g-actid .g-tl-res { font-size: 11px; font-weight: 600; color: var(--tone-ink); } +.g-actid .g-tl-note { margin: 3px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-3); } +.g-actid .g-tl-row.pending .g-tl-h b { font-weight: 500; color: var(--ink-4); } +.g-actid .g-tl-row.pending .g-tl-res { font-weight: 400; color: var(--ink-4); } +.g-actid .g-tl-row.pending .g-tl-note { color: var(--ink-4); } + +/* —— 辅栏:输入(键值行)/ 输出(文件行)—— 白卡 1px 描边,与 L2 列表同表面 */ +.g-actid .g-kv { background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; padding: 4px 14px; } +.g-actid .g-kv-row { display: flex; gap: 12px; padding: 8px 0; } +.g-actid .g-kv-row + .g-kv-row { border-top: 1px solid #eef1f5; } +.g-actid .g-kv-row em { flex: none; width: 44px; padding-top: 1px; font-style: normal; font-size: 12px; color: var(--ink-4); } +.g-actid .g-kv-row b { font-weight: 500; font-size: 12.5px; line-height: 1.6; color: var(--ink); word-break: break-all; } +.g-actid .g-kv-note { padding: 8px 0; border-top: 1px solid #eef1f5; font-size: 11.5px; color: var(--ink-4); } +.g-actid .g-file { display: flex; align-items: center; gap: 10px; background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; padding: 10px 14px; } +.g-actid .g-file-i { flex: none; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 6px; background: var(--ah-soft); color: var(--ah); } +.g-actid .g-file-n { min-width: 0; } +.g-actid .g-file-n b { display: block; font: 500 12px/1.4 var(--mono); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.g-actid .g-file-n em { font-style: normal; font-size: 11px; color: var(--ink-4); } +.g-actid .g-file .g-rowbtn { margin-left: auto; flex: none; } +.g-actid .g-file-empty { background: #fff; border: 1px dashed #d8dee6; border-radius: 8px; padding: 18px 14px; text-align: center; font-size: 12px; color: var(--ink-4); } + +@media (prefers-reduced-motion: reduce) { + .g-actid > * { animation: none; } + .g-actid .g-tl-row.current .g-tl-dot { animation: none; box-shadow: 0 0 0 3px var(--tone-soft); } + .g-actid .g-dh .chips > * { animation: none; } +} .g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; } .g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); } .g-fcard .mbtn.bad:hover { border-color: #e6c4c4; background: #fdf2f2; color: var(--danger); } diff --git a/apps/web/src/features/gov-task/index.ts b/apps/web/src/features/gov-task/index.ts index 7466b65..c5df31c 100644 --- a/apps/web/src/features/gov-task/index.ts +++ b/apps/web/src/features/gov-task/index.ts @@ -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'; diff --git a/apps/web/src/features/gov-task/internal/panel.tsx b/apps/web/src/features/gov-task/internal/panel.tsx index f996317..1d65eda 100644 --- a/apps/web/src/features/gov-task/internal/panel.tsx +++ b/apps/web/src/features/gov-task/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 }) { )} - + {/* 统计带 + 检索条同一行(2026-08-26 用户定:检索放右侧)—— 左:总数 + 五态灰字合计(状态色只进 6px 小圆点);右:条件下拉 + 随条件换控件 */}
@@ -568,7 +568,8 @@ export function ActvPage({ id }: { id: string }) { 终止活动 )} - @@ -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> = { + 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(undefined); + if (a === undefined || inst === undefined) { + return 实例不存在 —— 它可能已被删除,从面包屑返回活动详情看看。; + } + 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 ( +
+ + {/* key 带状态:终止后状态丸翻转时弹出一次高亮 —— 变化要看得见 */} + + {state} + + {inst.owner} + 启动 {inst.startedAt} + {state === '已完成' && 结束 {endedAt} · 耗时 27m} + {state === '进行中' && 已跑 12m} + {a.llm} + + } + 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 && {reason}} + {state === '待执行' && 待执行:已排入队列,等待调度 —— 到点或由负责人手动起步。} + +
+ {/* 主角:动作过程记录 —— 时间线竖排,当前步呼吸高亮,未到达灰显(2026-08-26 用户定:过程是实例页的重点) */} + +
    + {steps.map((s, i) => ( +
  1. + + + {i < steps.length - 1 && } + +
    +
    + + {i + 1} · {s.name} + + + + {s.by === 'agent' ? a.llm : `负责人 · ${inst.owner}`} + + {/* 时间 meta 只摆事实(什么时候 / 用了多久),结果交给右侧结果词 —— 不重复说两遍 */} + + {s.res === 'pending' ? '' : s.res === 'current' ? `${s.at} 起` : `${s.at} · 用时 ${s.dur}`} + + + {s.res === 'done' ? '完成' : s.res === 'fail' ? (term !== undefined ? '已终止' : state === '驳回' ? '驳回' : '失败') : s.res === 'current' ? '进行中' : '未到达'} + +
    +

    {s.note}

    +
    +
  2. + ))} +
+
+ + {/* 辅栏:输入 / 输出 —— 完整输入与最终产出(2026-08-26 规划);文件下载随接口开放(票据机制已备) */} +
+ +
+
+ 内容 + {inst.input} +
+
+ 来源 + 邮件 · quotes@bpos.demo +
+
+ 提交 + {inst.startedAt} +
+
文件类输入的原文下载随接口开放(本期占位)。
+
+
+ + {inst.output === '—' ? ( +
暂无产出 —— 走到「输出归档」后在这里出现。
+ ) : ( +
+ + + + + {inst.output} + 报价文件 · 输出归档步生成 + + +
+ )} +
+
+
+
+ ); +} + /* ---------------- ② 员工管理 ---------------- */ /** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */ diff --git a/apps/web/src/shared/gov/gov-route.ts b/apps/web/src/shared/gov/gov-route.ts index a2780e6..393969d 100644 --- a/apps/web/src/shared/gov/gov-route.ts +++ b/apps/web/src/shared/gov/gov-route.ts @@ -23,6 +23,9 @@ export type Route = /* 活动定义详情(运营中心 · 活动模块 L2,2026-08-25 重构):id = 活动 id; 与设计中心的 `act`(活动库)是两回事 —— 那是技术安装面,这是业务运营面 */ | { k: 'actd'; id: string } + /* 实例详情(活动模块 L3,2026-08-26):id = `<活动 id>:<实例号>` —— + 实例没有独立接口,详情页按活动 id 找回它的定义与清单,再取这一条实例 */ + | { k: 'acti'; id: string } | { k: 'llm'; id: string } | { k: 'chan'; id: string }; @@ -243,6 +246,8 @@ export function routeLabel(r: Route): string { return '记录详情'; case 'actd': return '活动详情'; + case 'acti': + return '实例详情'; case 'llm': return 'LLM 实例'; case 'chan': diff --git a/docs/活动模块重构需求-2026-08-25.md b/docs/活动模块重构需求-2026-08-25.md index 3ca81fc..c8159aa 100644 --- a/docs/活动模块重构需求-2026-08-25.md +++ b/docs/活动模块重构需求-2026-08-25.md @@ -89,6 +89,7 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活 - 2026-08-25 · **文案口径(用户定)**:创建实例 → **启动活动**;实例清单 → **历史活动**;停止 → **终止活动**。 - 2026-08-26 · **L2 历史活动改列表(用户定)**:卡片式清单撤掉,改表格列(实例 / 负责人 / 输入 / 输出 / 启动时间 / 状态 / 操作,操作栏带标题),隔行浅斑马 + 悬停亮行,行内操作(终止活动 / 查看详情)常驻;分页单页 8 条不变。 - 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。 +- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;头部动作三个:终止活动(进行中才有,二次确认)、编辑实例(disabled 占位)、删除实例(二次确认 —— 删除存疑仍悬着,先按规划做进 UI)。终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。 ## 三、功能表(整理稿) -- 2.54.0 From 0c029b694da2166f2453f4d2d32625694fe70e03 Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 19:42:31 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E6=92=A4?= =?UTF-8?q?=E9=A1=B6=E6=A0=8F:=E8=BA=AB=E4=BB=BD=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=94=B6=E8=BF=9B=E5=B7=A6=E8=BD=A8=E5=BA=95=E9=83=A8(?= =?UTF-8?q?=E6=82=AC=E5=81=9C=E4=BB=8E=E8=BD=A8=E5=8F=B3=E4=BE=A7=E5=BC=B9?= =?UTF-8?q?=E5=87=BA);=E5=A7=93=E5=90=8D=E5=B1=95=E7=A4=BA=E6=94=B9?= =?UTF-8?q?=E5=A7=93=E5=9C=A8=E5=89=8D(token=20claims=20=E4=B8=8E=E5=90=8D?= =?UTF-8?q?=E5=86=8C=E5=90=8C=E6=AD=A5=E4=B8=A4=E5=A4=84=E6=8B=BC=E8=A3=85?= =?UTF-8?q?=E5=90=8C=E5=B0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/modules/org/__tests__/claims.test.ts | 5 ++ .../__tests__/keycloak-admin-roster.test.ts | 4 +- .../internal/adapter/keycloak-admin-roster.ts | 3 +- .../adapter/keycloak-token-verifier.ts | 4 ++ .../src/modules/org/internal/domain/claims.ts | 8 ++- apps/api/src/modules/org/port.ts | 4 ++ apps/web/src/app/app-shell.tsx | 53 ++++++++----------- apps/web/src/app/styles/app.css | 18 ++++--- 8 files changed, 59 insertions(+), 40 deletions(-) diff --git a/apps/api/src/modules/org/__tests__/claims.test.ts b/apps/api/src/modules/org/__tests__/claims.test.ts index 559c3c9..50b79c3 100644 --- a/apps/api/src/modules/org/__tests__/claims.test.ts +++ b/apps/api/src/modules/org/__tests__/claims.test.ts @@ -27,6 +27,11 @@ describe('identityOf —— claims 归一成一个人', () => { expect('email' in id).toBe(false); }); + it('姓/名散件在 → 姓在前拼「姓 名」,不采用全名字段的名+姓序(2026-08-26 用户定)', () => { + const id = identityOf({ sub: 's', preferred_username: 'shan', name: '亲 删', given_name: '亲', family_name: '删' }); + expect(id.name).toBe('删 亲'); + }); + it('realm_access 有但 roles 缺 → 空角色', () => { expect(identityOf({ sub: 's', preferred_username: 'bob', realm_access: {} }).roles).toEqual([]); }); diff --git a/apps/api/src/modules/org/__tests__/keycloak-admin-roster.test.ts b/apps/api/src/modules/org/__tests__/keycloak-admin-roster.test.ts index 8689740..218787b 100644 --- a/apps/api/src/modules/org/__tests__/keycloak-admin-roster.test.ts +++ b/apps/api/src/modules/org/__tests__/keycloak-admin-roster.test.ts @@ -42,13 +42,13 @@ function fakeKeycloak(): { fetch: typeof fetch; calls: string[] } { } describe('Keycloak Admin API 名册', () => { - it('分页拉全(100 + 5)、姓名拼 firstName lastName、缺名回退用户名、角色滤噪声、enabled 照抄', async () => { + it('分页拉全(100 + 5)、姓名姓在前拼 lastName firstName、缺名回退用户名、角色滤噪声、enabled 照抄', async () => { const kc = fakeKeycloak(); const roster = createKeycloakRoster({ url: 'https://kc.test/', realm: 'corp', clientId: 'provisioning', secret: 's', fetch: kc.fetch }); const users = await roster.listUsers(); expect(users).toHaveLength(105); expect(users[0]).toEqual({ sub: 'sub-0', account: 'u0', name: 'u0', email: 'u0@x.test', roles: ['user'], enabled: true }); - expect(users[1]).toEqual({ sub: 'sub-1', account: 'u1', name: '名 1', email: 'u1@x.test', roles: ['admin', 'user'], enabled: true }); + expect(users[1]).toEqual({ sub: 'sub-1', account: 'u1', name: '1 名', email: 'u1@x.test', roles: ['admin', 'user'], enabled: true }); expect(users[3]?.enabled).toBe(false); expect(kc.calls.filter((c) => c.includes('/users?')).length).toBe(2); }); diff --git a/apps/api/src/modules/org/internal/adapter/keycloak-admin-roster.ts b/apps/api/src/modules/org/internal/adapter/keycloak-admin-roster.ts index bffb86b..841e858 100644 --- a/apps/api/src/modules/org/internal/adapter/keycloak-admin-roster.ts +++ b/apps/api/src/modules/org/internal/adapter/keycloak-admin-roster.ts @@ -76,7 +76,8 @@ export function createKeycloakRoster(options: KeycloakRosterOptions): RosterSour const out: RosterUser[] = []; for (const u of users) { const roles = await adminGet(`/users/${encodeURIComponent(u.id)}/role-mappings/realm/composite`); - const name = [u.firstName, u.lastName] + /* 姓在前(2026-08-26 用户定)—— 与 token 那条路(claims.ts `identityOf`)同一把尺 */ + const name = [u.lastName, u.firstName] .filter((s): s is string => s !== undefined && s.trim() !== '') .join(' ') .trim(); diff --git a/apps/api/src/modules/org/internal/adapter/keycloak-token-verifier.ts b/apps/api/src/modules/org/internal/adapter/keycloak-token-verifier.ts index 06c6795..70d2363 100644 --- a/apps/api/src/modules/org/internal/adapter/keycloak-token-verifier.ts +++ b/apps/api/src/modules/org/internal/adapter/keycloak-token-verifier.ts @@ -24,6 +24,8 @@ const claimsSchema = z.object({ sub: z.string().min(1), preferred_username: z.string().min(1), name: z.string().optional(), + given_name: z.string().optional(), + family_name: z.string().optional(), email: z.string().optional(), realm_access: z.object({ roles: z.array(z.string()).optional() }).optional(), }); @@ -39,6 +41,8 @@ function toVerifiedClaims(claims: z.infer): VerifiedClaims sub: claims.sub, preferred_username: claims.preferred_username, ...(claims.name === undefined ? {} : { name: claims.name }), + ...(claims.given_name === undefined ? {} : { given_name: claims.given_name }), + ...(claims.family_name === undefined ? {} : { family_name: claims.family_name }), ...(claims.email === undefined ? {} : { email: claims.email }), ...(claims.realm_access === undefined ? {} diff --git a/apps/api/src/modules/org/internal/domain/claims.ts b/apps/api/src/modules/org/internal/domain/claims.ts index 0015c65..40ecd23 100644 --- a/apps/api/src/modules/org/internal/domain/claims.ts +++ b/apps/api/src/modules/org/internal/domain/claims.ts @@ -21,7 +21,13 @@ export function normalizeRoles(roles: readonly string[]): string[] { export function identityOf(claims: VerifiedClaims): Identity { const account = claims.preferred_username; - const name = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim(); + /* 姓名展示姓在前(2026-08-26 用户定):有姓/名散件就按「姓 名」拼; + 没有散件才用 Keycloak 的全名 `name`(它按「名 姓」拼,中文场景顺序是反的) */ + const cjk = [claims.family_name, claims.given_name] + .filter((s): s is string => s !== undefined && s.trim() !== '') + .join(' '); + const fallback = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim(); + const name = cjk !== '' ? cjk : fallback; const email = claims.email === undefined || claims.email.trim() === '' ? undefined : claims.email.trim(); return { sub: claims.sub, diff --git a/apps/api/src/modules/org/port.ts b/apps/api/src/modules/org/port.ts index b438108..3207f5e 100644 --- a/apps/api/src/modules/org/port.ts +++ b/apps/api/src/modules/org/port.ts @@ -14,6 +14,10 @@ export type VerifiedClaims = { readonly sub: string; readonly preferred_username: string; readonly name?: string; + /* 名/姓分开收(2026-08-26 用户定:中文姓名姓在前)—— Keycloak 的 `name` 是全名(名+姓), + 展示要「姓 名」得靠这两个散件拼;没带就回退 `name` 原样 */ + readonly given_name?: string; + readonly family_name?: string; readonly email?: string; readonly realm_access?: { readonly roles?: readonly string[] }; }; diff --git a/apps/web/src/app/app-shell.tsx b/apps/web/src/app/app-shell.tsx index 58c81b3..f751eef 100644 --- a/apps/web/src/app/app-shell.tsx +++ b/apps/web/src/app/app-shell.tsx @@ -1,7 +1,7 @@ /** * 工作台外壳 —— 自原型 `(proto)/page.tsx` 平移(README《前端 UI · 原型 1:1 平移》)。 * - * 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报);身份在顶栏右侧菜单; + * 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报)→ 轨底身份菜单; * 任务四区 = 左栏列表区 / 中上标题区 / 中下工作区 / 右栏对话区。 * * **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。 @@ -167,38 +167,31 @@ export function AppShell() { ); })} - {/* 轨底不再报身份:头像与姓名挪到顶栏右侧的身份菜单里(2026-08-25 用户定) */} + {/* 身份菜单收在轨底(2026-08-26 用户定:顶栏撤掉,内容挪到左轨底部)—— + 轨宽 78px,头像上姓名下竖排;悬停/聚焦从轨右侧弹出,登出仍是全站唯一入口 */} +
+ {me.slice(0, 1)} + {me} +
+ {/* 个人中心独立成页(/me 台面),整页导航过去(2026-08-25 用户定) */} + + + 个人中心 + + {/* 设计中心直链数据域页,整页导航过去(2026-08-25 用户定) */} + + + 设计中心 + + +
+
-
- {/* 顶栏不挂模块名,只留右侧身份菜单(2026-08-25 用户定) */} -
- {/* 身份菜单:悬停头像/姓名出下拉 —— 个人中心(建设中提示)+ 退出登录;登出仍是全站唯一入口 */} -
- {me.slice(0, 1)} - {me} - -
- {/* 个人中心独立成页(/me 台面),整页导航过去(2026-08-25 用户定) */} - - - 个人中心 - - {/* 设计中心直链数据域页,整页导航过去(2026-08-25 用户定) */} - - - 设计中心 - - -
-
-
-
-
{module === 'tasks' ? (
diff --git a/apps/web/src/app/styles/app.css b/apps/web/src/app/styles/app.css index df384bc..a167b91 100644 --- a/apps/web/src/app/styles/app.css +++ b/apps/web/src/app/styles/app.css @@ -476,16 +476,14 @@ .proto-root .jc-sup:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(51,112,255,.12); } .proto-root .jc-sup:disabled { background: #f3f4f6; color: #5c626b; } -/* ===== 左侧一体化导航轨:品牌 → 五大模块(身份已挪去顶栏右侧,见下)===== */ +/* ===== 左侧一体化导航轨:品牌 → 五大模块 → 轨底身份区(2026-08-26 用户定:顶栏撤了,身份收回轨底)===== */ .proto-root .app-nav { padding: 14px 10px 12px; } .proto-root .jc-rail-brand { text-align: center; font-size: 10px; font-weight: 800; letter-spacing: .14em; color: var(--ink-3); margin: -4px 0 6px; } .proto-root .app-nav button { min-height: 58px; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); } .proto-root .app-nav button:active { transform: scale(.97); } -/* 轨底身份区已撤:头像与姓名挪去顶栏右侧身份菜单(2026-08-25 用户定); - .jc-nav-ava 保留 —— 顶栏身份菜单复用这枚渐变头像 */ .proto-root .jc-nav-ava { width: 32px; height: 32px; border-radius: 999px; display: grid; place-items: center; flex: 0 0 auto; line-height: 1; color: #fff; font-size: 13px; font-weight: 800; background: linear-gradient(140deg, #2bc7d4, #0e9fd8); box-shadow: 0 2px 6px rgba(14, 159, 216, .35); } -/* ===== 顶栏:44px 通栏,只放右侧身份菜单 —— 与左轨互不重叠,登出只有这一处入口 ===== */ +/* ===== 主区:顶栏已撤(2026-08-26 用户定),内容区直接铺满 ===== */ .proto-root .app-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; } .proto-root .app-main-body { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; } .proto-root .app-main-body > * { flex: 1 1 auto; min-width: 0; min-height: 0; } @@ -500,8 +498,8 @@ /* 但右上角装饰光晕定位 right:-60px,裁切一放开它就探出视口右缘 60px,整页出现横向滚动 (2026-08-25 用户报:页面能左右滑)—— 把它收回盒内,光晕照留 */ .proto-root .jc-topbar.jc-topbar-app::after { right: 0; } -/* 顶栏右侧身份菜单:头像 + 姓名,悬停出下拉(个人中心 / 退出登录)—— 纯 CSS 悬停无 JS; - 登出入口从独立按钮收进下拉,仍是全站唯一入口(2026-08-25 用户定) */ +/* 身份菜单:头像 + 姓名,悬停出下拉(个人中心 / 设计中心 / 退出登录)—— 纯 CSS 悬停无 JS; + 登出入口收进下拉,是全站唯一入口(2026-08-25 用户定);2026-08-26 从顶栏挪进左轨底部(见下方 rail 变体) */ .proto-root .jc-usermenu { position: relative; display: flex; align-items: center; gap: 8px; padding: 4px 8px 4px 4px; border-radius: 10px; cursor: default; color: var(--ink-4); transition: background var(--t-fast); } .proto-root .jc-usermenu:hover { background: var(--soft); } .proto-root .jc-usermenu .jc-nav-ava { width: 28px; height: 28px; font-size: 12px; } @@ -514,6 +512,14 @@ .proto-root .jc-usermenu-pop button.danger:hover { background: var(--danger-soft); color: var(--danger); } .proto-root .jc-usermenu-pop button:disabled { opacity: .6; cursor: default; } +/* 轨底变体(2026-08-26 用户定):轨宽 78px,头像上姓名下竖排,与模块钮之间一道细分隔; + 下拉改从轨右侧弹出、底缘与触发区对齐,悬停桥跟着转到右侧 */ +.proto-root .jc-usermenu-rail { margin-top: auto; align-self: stretch; flex-direction: column; justify-content: center; gap: 4px; padding: 10px 2px 4px; border-top: 1px solid var(--line-soft); border-radius: 0; } +.proto-root .jc-usermenu-rail:hover { background: none; } +.proto-root .jc-usermenu-rail .jc-topbar-name { max-width: 66px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; text-align: center; } +.proto-root .jc-usermenu-rail::after { top: 0; bottom: 0; left: 100%; right: auto; width: 10px; height: auto; } +.proto-root .jc-usermenu-rail .jc-usermenu-pop { top: auto; right: auto; bottom: 0; left: calc(100% + 10px); transform: translateX(-4px); } + /* 「建设中」提示弹窗:未开放模块(审批/消息/纪要/汇报)与个人中心的占位口径 —— 不跳页只弹提示 */ .proto-root .jc-wip-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; background: rgba(31, 35, 41, .45); } .proto-root .jc-wip-modal { width: 360px; max-width: 92vw; padding: 24px 20px 18px; background: #fff; border-radius: 16px; box-shadow: 0 24px 60px rgba(31, 35, 41, .22); text-align: center; animation: rise var(--t-med) both; } -- 2.54.0 From 023d3157a012b7f49927a9dfa2ec736ac1a19bfe Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 19:59:46 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E3=80=81=E4=B8=8D=E5=8F=AF=E7=BC=96=E8=BE=91?= =?UTF-8?q?(2026-08-26=20=E7=94=A8=E6=88=B7=E5=AE=9A):L3=20=E6=92=A4?= =?UTF-8?q?=E6=8E=89=E7=BC=96=E8=BE=91/=E5=88=A0=E9=99=A4=E5=85=A5?= =?UTF-8?q?=E5=8F=A3,=E5=8F=AA=E7=95=99=E8=BF=9B=E8=A1=8C=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E7=BB=88=E6=AD=A2=E6=B4=BB=E5=8A=A8;=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E7=A8=BF=E5=90=8C=E6=AD=A5=E5=85=B3=E9=97=AD=E4=B8=A4?= =?UTF-8?q?=E6=9D=A1=E5=BE=85=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/gov-shell.tsx | 2 +- .../src/features/gov-task/internal/panel.tsx | 24 +++++-------------- docs/活动模块重构需求-2026-08-25.md | 7 +++--- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/gov-shell.tsx b/apps/web/src/app/gov-shell.tsx index c2e46ef..4fa6df7 100644 --- a/apps/web/src/app/gov-shell.tsx +++ b/apps/web/src/app/gov-shell.tsx @@ -332,7 +332,7 @@ function Detail({ route, nav }: { route: Route; nav: Nav }) { case 'actd': return ; case 'acti': - return ; + return ; case 'tool': return ; case 'obj': diff --git a/apps/web/src/features/gov-task/internal/panel.tsx b/apps/web/src/features/gov-task/internal/panel.tsx index 1d65eda..8b7807e 100644 --- a/apps/web/src/features/gov-task/internal/panel.tsx +++ b/apps/web/src/features/gov-task/internal/panel.tsx @@ -724,10 +724,11 @@ const STEP_RES_TONE: Readonly> = { /** * L3 · 实例详情 —— 一次运行的完整视图(2026-08-26 规划,用户确认后落地): * 主从 2:1(DESIGN-SPEC §9)—— 左「动作过程记录」是主角(追溯与验收的落点), - * 右栏输入/输出是辅助;编辑与删除收在这一层(功能 5/6),不污染清单。 + * 右栏输入/输出是辅助。实例不可删除、不可编辑(2026-08-26 用户定)—— + * 编辑/删除入口撤掉,唯一的动作是进行中的「终止活动」。 * 数据为静态占位,结构按将来接口的形状铺。 */ -export function ActiPage({ id, nav }: { id: string; nav: Nav }) { +export function ActiPage({ id }: { id: string }) { 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; @@ -735,7 +736,7 @@ export function ActiPage({ id, nav }: { id: string; nav: Nav }) { term 记下终止时刻 —— 终止的过程面与「自己跑挂」不同(当前步记「已终止」,不是「附件缺失」那种失败原因) */ const [term, setTerm] = useState(undefined); if (a === undefined || inst === undefined) { - return 实例不存在 —— 它可能已被删除,从面包屑返回活动详情看看。; + return 实例不存在 —— 它不在该活动的清单里,从面包屑返回活动详情看看。; } const state: InstanceState = term !== undefined ? '失败' : inst.state; const steps = term !== undefined ? terminatedSteps(inst.startedAt) : demoSteps(state, inst.startedAt); @@ -771,9 +772,9 @@ export function ActiPage({ id, nav }: { id: string; nav: Nav }) { {a.llm} } - desc="一次运行的完整视图:输入 / 过程 / 输出 / 状态。编辑与删除收在这一层,清单页不受打扰。" + desc="一次运行的完整视图:输入 / 过程 / 输出 / 状态 —— 实例不可编辑、不可删除,进行中的运行可终止。" acts={[ - /* 头部动作(2026-08-26 规划):进行中可终止(二次确认);编辑本期只留入口(编什么待定);删除二次确认(用户标注存疑,先按规划做) */ + /* 唯一动作(2026-08-26 用户定:实例不许删、不许编):进行中可终止,二次确认 */ ...(state === '进行中' ? [ { @@ -790,19 +791,6 @@ export function ActiPage({ id, nav }: { id: string; nav: Nav }) { } 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 && {reason}} diff --git a/docs/活动模块重构需求-2026-08-25.md b/docs/活动模块重构需求-2026-08-25.md index c8159aa..666c8f3 100644 --- a/docs/活动模块重构需求-2026-08-25.md +++ b/docs/活动模块重构需求-2026-08-25.md @@ -89,7 +89,8 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活 - 2026-08-25 · **文案口径(用户定)**:创建实例 → **启动活动**;实例清单 → **历史活动**;停止 → **终止活动**。 - 2026-08-26 · **L2 历史活动改列表(用户定)**:卡片式清单撤掉,改表格列(实例 / 负责人 / 输入 / 输出 / 启动时间 / 状态 / 操作,操作栏带标题),隔行浅斑马 + 悬停亮行,行内操作(终止活动 / 查看详情)常驻;分页单页 8 条不变。 - 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。 -- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;头部动作三个:终止活动(进行中才有,二次确认)、编辑实例(disabled 占位)、删除实例(二次确认 —— 删除存疑仍悬着,先按规划做进 UI)。终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。 +- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。 +- 2026-08-26 · **实例不可删除、不可编辑(用户定,推翻 L3 原规划)**:L3 的编辑/删除入口撤掉,唯一动作是进行中的「终止活动」(二次确认);六功能里实例的「编辑/删除」两项就此划掉,待确认问题里的两条(编辑指什么 / 删除是否放开)同时关闭。 ## 三、功能表(整理稿) @@ -100,6 +101,6 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活 - ~~活动模块里的「设计」与设计中心「活动库」是什么关系?~~ **已答(2026-08-25)**:不再有「设计中心 vs 运营中心」的分工 —— 设计并入运营,活动模块自带设计能力;设计中心各区后续按同思路并入运营中心重做。 - ~~「设计」在活动模块里做到什么程度?~~ **已答(2026-08-25)**:六功能已界定 —— 活动(定义)的创建与展示 + 实例的创建/展示/编辑/删除;定义只许创建不许编删,创建要过审核。 - 活动创建的**审核**:谁来审、在模块内审还是别处、审什么(输入输出契约的合理性?)?(待用户展开) -- 实例的**编辑**具体指编辑什么:实例的输入数据?进行中的干预(暂停/叫停)?还是产出修正?(待用户展开) -- 实例删除是否真放开 —— 用户已标注存疑,先规划,后续按需求调。(悬着,不阻塞) +- 实例的**编辑**~~具体指编辑什么~~:**已答(2026-08-26):实例不可编辑**,入口不做。 +- 实例删除~~是否真放开~~:**已答(2026-08-26):实例不可删除**,入口不做;六功能只剩实例的创建/展示。 - 「运营」侧还要不要监控/验收类功能(此前提过负责人验收规划与统计产出)—— 六功能里未直接出现,是否归在「实例的展示/编辑」里,还是另有入口?(待用户展开) -- 2.54.0 From 237286e908679481eabd8a9b9fbc25462aaef33d Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 20:15:13 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4=E6=94=B9=E6=B0=94=E6=B3=A1?= =?UTF-8?q?=E5=BC=B9=E5=B1=82(2026-08-26=20=E7=94=A8=E6=88=B7=E5=AE=9A):?= =?UTF-8?q?=E9=94=9A=E5=AE=9A=E9=A1=B5=E5=A4=B4=E5=8F=B3=E4=B8=8B+?= =?UTF-8?q?=E5=B0=8F=E4=B8=89=E8=A7=92=E6=8C=87=E5=9B=9E=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E9=92=AE,=E4=B8=89=E5=BF=85=E7=AD=94=E6=94=B6=E7=AB=96?= =?UTF-8?q?=E6=8E=92;L2=20=E7=A6=81=E7=94=A8/=E6=BF=80=E6=B4=BB=E4=B8=8E?= =?UTF-8?q?=20L3=20=E7=BB=88=E6=AD=A2=E5=90=8C=E7=94=9F=E6=95=88;=E9=A1=B5?= =?UTF-8?q?=E5=A4=B4=E5=AE=B9=E5=99=A8=E8=A1=A5=E6=AD=A3=20z-index=20?= =?UTF-8?q?=E7=A0=B4=E5=8A=A8=E7=94=BB=E5=B1=82=E5=8F=A0=E5=9B=B0=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/styles/console.css | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/web/src/app/styles/console.css b/apps/web/src/app/styles/console.css index 1a7a550..3448ef1 100644 --- a/apps/web/src/app/styles/console.css +++ b/apps/web/src/app/styles/console.css @@ -708,6 +708,32 @@ .g-actid .g-tl-row.current .g-tl-dot { animation: none; box-shadow: 0 0 0 3px var(--tone-soft); } .g-actid .g-dh .chips > * { animation: none; } } + +/* ══ 二次确认改气泡弹层(2026-08-26 用户定:不要瘫在 box 下方的确认带)══ + 锚在页头右下、小三角指回触发钮;三必答从三栏收成竖排三行; + 阴影只给弹层(§10),危险确认沿用非实心 danger 钮(§11),进场 180ms 从触发点展开(§13) */ +/* 注意:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文, + 气泡的 z-index 只在页头内部作数 —— 所以页头容器自己要有正 z-index,气泡才盖得住后面的区块 */ +.g-actvd .g-dhwrap, .g-actid .g-dhwrap { position: relative; z-index: 30; } +.g-actvd .g-ask, .g-actid .g-ask { position: absolute; top: calc(100% + 10px); right: 0; z-index: 40; + width: 372px; padding: 13px 15px 12px; gap: 10px; + border: 1px solid var(--tone-line, var(--cline)); border-radius: 12px; + box-shadow: 0 12px 32px rgba(22, 26, 34, .14), 0 2px 8px rgba(22, 26, 34, .08); + animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: top right; } +@keyframes actvd-ask { from { opacity: 0; transform: translateY(-4px) scale(.97); } } +/* 小三角:指回右上方的触发钮(左/上两条边旋转 45° 拼出) */ +.g-actvd .g-ask::before, .g-actid .g-ask::before { content: ""; position: absolute; top: -6px; right: 60px; + width: 10px; height: 10px; background: #fff; transform: rotate(45deg); + border-left: 1px solid var(--tone-line, var(--cline)); border-top: 1px solid var(--tone-line, var(--cline)); } +/* 三必答竖排:标签定宽左、内容右,气泡窄也读得顺 */ +.g-actvd .g-impact, .g-actid .g-impact { grid-template-columns: 1fr; border-radius: 8px; } +.g-actvd .g-impact > div, .g-actid .g-impact > div { flex-direction: row; align-items: baseline; gap: 10px; padding: 7px 10px; } +.g-actvd .g-impact em, .g-actid .g-impact em { flex: none; width: 52px; } +.g-actvd .g-impact b, .g-actid .g-impact b { font-weight: 500; } +.g-actvd .g-ask-a .g-btn, .g-actid .g-ask-a .g-btn { height: 28px; } +@media (prefers-reduced-motion: reduce) { + .g-actvd .g-ask, .g-actid .g-ask { animation: none; } +} .g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; } .g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); } .g-fcard .mbtn.bad:hover { border-color: #e6c4c4; background: #fdf2f2; color: var(--danger); } -- 2.54.0 From 8139948c28ba7be2226a5e5ee03f9c7923232758 Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 20:41:47 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4=E7=98=A6=E8=BA=AB=E6=88=90?= =?UTF-8?q?=E8=BD=BB=E6=B0=94=E6=B3=A1(2026-08-26=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=AE=9A):=E5=8F=AA=E9=97=AE=E4=B8=80=E5=8F=A5+=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4/=E5=8F=96=E6=B6=88,=E4=B8=8D=E6=91=86=E5=BD=B1?= =?UTF-8?q?=E5=93=8D=E4=B8=89=E5=BF=85=E7=AD=94;=E6=9C=AC=E5=9C=B0=20PopAs?= =?UTF-8?q?k=20=E7=BB=84=E4=BB=B6=E6=9B=BF=E4=BB=A3=E5=85=B1=E4=BA=AB=20As?= =?UTF-8?q?kBand,L2=20=E7=A6=81=E7=94=A8/=E6=BF=80=E6=B4=BB=E4=B8=8E=20L3?= =?UTF-8?q?=20=E7=BB=88=E6=AD=A2=E5=90=8C=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/styles/console.css | 29 ++- .../src/features/gov-task/internal/panel.tsx | 173 +++++++++++------- docs/活动模块重构需求-2026-08-25.md | 1 + 3 files changed, 120 insertions(+), 83 deletions(-) diff --git a/apps/web/src/app/styles/console.css b/apps/web/src/app/styles/console.css index 3448ef1..7e98475 100644 --- a/apps/web/src/app/styles/console.css +++ b/apps/web/src/app/styles/console.css @@ -709,30 +709,27 @@ .g-actid .g-dh .chips > * { animation: none; } } -/* ══ 二次确认改气泡弹层(2026-08-26 用户定:不要瘫在 box 下方的确认带)══ - 锚在页头右下、小三角指回触发钮;三必答从三栏收成竖排三行; - 阴影只给弹层(§10),危险确认沿用非实心 danger 钮(§11),进场 180ms 从触发点展开(§13) */ -/* 注意:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文, - 气泡的 z-index 只在页头内部作数 —— 所以页头容器自己要有正 z-index,气泡才盖得住后面的区块 */ -.g-actvd .g-dhwrap, .g-actid .g-dhwrap { position: relative; z-index: 30; } -.g-actvd .g-ask, .g-actid .g-ask { position: absolute; top: calc(100% + 10px); right: 0; z-index: 40; - width: 372px; padding: 13px 15px 12px; gap: 10px; +/* ══ 二次确认·轻气泡(2026-08-26 用户定:只问一句 + 确认/取消,不摆影响三必答)══ + 锚在页头右下、小三角指回触发钮;点气泡外的透明罩即取消; + 阴影只给弹层(§10),确认钮 danger 非实心(§11),进场 180ms 从触发点展开(§13) */ +/* 正 z-index 必须有:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文, + 没有它,气泡的 z-index 只在页头内部作数,盖不住后面的区块 */ +.g-actvd .g-pahead, .g-actid .g-pahead { position: relative; z-index: 30; } +.g-actvd .g-popask-veil, .g-actid .g-popask-veil { position: fixed; inset: 0; z-index: 39; } +.g-actvd .g-popask, .g-actid .g-popask { position: absolute; top: calc(100% + 10px); right: 0; z-index: 40; + width: 264px; padding: 13px 14px 12px; background: #fff; border: 1px solid var(--tone-line, var(--cline)); border-radius: 12px; box-shadow: 0 12px 32px rgba(22, 26, 34, .14), 0 2px 8px rgba(22, 26, 34, .08); animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: top right; } @keyframes actvd-ask { from { opacity: 0; transform: translateY(-4px) scale(.97); } } /* 小三角:指回右上方的触发钮(左/上两条边旋转 45° 拼出) */ -.g-actvd .g-ask::before, .g-actid .g-ask::before { content: ""; position: absolute; top: -6px; right: 60px; +.g-actvd .g-popask::before, .g-actid .g-popask::before { content: ""; position: absolute; top: -6px; right: 60px; width: 10px; height: 10px; background: #fff; transform: rotate(45deg); border-left: 1px solid var(--tone-line, var(--cline)); border-top: 1px solid var(--tone-line, var(--cline)); } -/* 三必答竖排:标签定宽左、内容右,气泡窄也读得顺 */ -.g-actvd .g-impact, .g-actid .g-impact { grid-template-columns: 1fr; border-radius: 8px; } -.g-actvd .g-impact > div, .g-actid .g-impact > div { flex-direction: row; align-items: baseline; gap: 10px; padding: 7px 10px; } -.g-actvd .g-impact em, .g-actid .g-impact em { flex: none; width: 52px; } -.g-actvd .g-impact b, .g-actid .g-impact b { font-weight: 500; } -.g-actvd .g-ask-a .g-btn, .g-actid .g-ask-a .g-btn { height: 28px; } +.g-actvd .g-popask p, .g-actid .g-popask p { margin: 0; font-size: 13px; font-weight: 600; line-height: 1.55; color: var(--ink); } +.g-actvd .g-popask-a, .g-actid .g-popask-a { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } @media (prefers-reduced-motion: reduce) { - .g-actvd .g-ask, .g-actid .g-ask { animation: none; } + .g-actvd .g-popask, .g-actid .g-popask { animation: none; } } .g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; } .g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); } diff --git a/apps/web/src/features/gov-task/internal/panel.tsx b/apps/web/src/features/gov-task/internal/panel.tsx index 8b7807e..83981ec 100644 --- a/apps/web/src/features/gov-task/internal/panel.tsx +++ b/apps/web/src/features/gov-task/internal/panel.tsx @@ -283,6 +283,36 @@ function inTimeRange(at: string, from: string, to: string): boolean { return true; } +/** 轻量二次确认的规格(2026-08-26 用户定:只问一句 + 确认/取消 —— 影响三必答(AskBand)是治理台重度动作的规格,禁用活动/终止实例是轻动作,不摆) */ +type PopAskSpec = { title: string; okLabel: string; danger?: boolean; onOk: () => void }; + +/** 气泡式二次确认 —— 锚在页头右下的小泡泡:一句话问清,确认/取消两钮;点气泡外的透明罩即取消 */ +function PopAsk({ spec, onCancel }: { spec: PopAskSpec; onCancel: () => void }) { + return ( + <> +
+
+

{spec.title}

+
+ + +
+
+ + ); +} + /** * L2 · 活动详情 —— 两个问题一屏答:「这个活动是什么」(定义,只读) * 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。 @@ -309,6 +339,8 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) { const [fState, setFState] = useState(''); const [fFrom, setFFrom] = useState(''); const [fTo, setFTo] = useState(''); + /* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡,不走 AskBand 三必答 */ + const [ask, setAsk] = useState(null); if (a === undefined) return 活动不存在 —— 它可能已被调整,返回活动清单看看。; const st = status ?? a.status; const instances = rows ?? demoInstances(a.owner); @@ -354,64 +386,66 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) { return (
- - {st === 'active' ? '激活中' : '已禁用'} - {a.version !== undefined && v{a.version}} - {/* 绑定的 LLM 上头版(2026-08-25 用户定):每个活动一个专属 agent */} - {a.llm} - {a.owner} - 创建于 {a.createdAt} - - } - desc="活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。" - acts={[ - st === 'active' - ? { - label: '禁用活动', - ico: 'power', - danger: true, - ask: { - title: '确认禁用这个活动?', - note: '禁用后不能再为它创建实例;已在跑的实例不受影响。', - impact: { who: '该活动及其后续实例创建', undo: '可恢复 —— 随时重新激活', when: '确认后立即生效(本期前端演示,接口对接后落库)' }, - okLabel: '确认禁用', - onOk: () => { - /* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */ - if (a.instances > 0) { - setBlocked(true); - } else { - setStatus('disabled'); - setBlocked(false); - } - }, +
+ + {st === 'active' ? '激活中' : '已禁用'} + {a.version !== undefined && v{a.version}} + {/* 绑定的 LLM 上头版(2026-08-25 用户定):每个活动一个专属 agent */} + {a.llm} + {a.owner} + 创建于 {a.createdAt} + + } + desc="活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。" + acts={[ + st === 'active' + ? { + label: '禁用活动', + ico: 'power', + danger: true, + onClick: () => + setAsk({ + title: '确认禁用这个活动?', + okLabel: '确认禁用', + danger: true, + onOk: () => { + /* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */ + if (a.instances > 0) { + setBlocked(true); + } else { + setStatus('disabled'); + setBlocked(false); + } + }, + }), + } + : { + label: '激活活动', + ico: 'zap', + primary: true, + onClick: () => + setAsk({ + title: '确认激活这个活动?', + okLabel: '确认激活', + onOk: () => { + setStatus('active'); + setBlocked(false); + }, + }), }, - } - : { - label: '激活活动', - ico: 'zap', - primary: true, - ask: { - title: '确认激活这个活动?', - note: '激活后可以为它创建实例。', - impact: { who: '该活动及其后续实例创建', undo: '可逆 —— 随时可以再次禁用', when: '确认后立即生效(本期前端演示,接口对接后落库)' }, - okLabel: '确认激活', - onOk: () => { - setStatus('active'); - setBlocked(false); - }, - }, - }, - { label: '启动活动', ico: 'plus', disabled: true }, - /* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts - { label: '版本迭代', ico: 'git-branch', disabled: true }, - */ - ]} - /> + { label: '启动活动', ico: 'plus', disabled: true }, + /* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts + { label: '版本迭代', ico: 'git-branch', disabled: true }, + */ + ]} + /> + {ask !== null && setAsk(null)} />} +
{blocked && ( 无法禁用:该活动还有 {a.instances} 个实例 —— 这条提示将来由接口返回(有实例时接口拒绝禁用),本期为前端占位演示。 @@ -735,6 +769,8 @@ export function ActiPage({ id }: { id: string }) { /* 状态就地翻转(本期前端演示):终止暂落「失败」(归态接口语义未定),对接后走 mutation 由服务端重投影; term 记下终止时刻 —— 终止的过程面与「自己跑挂」不同(当前步记「已终止」,不是「附件缺失」那种失败原因) */ const [term, setTerm] = useState(undefined); + /* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡 */ + const [ask, setAsk] = useState(null); if (a === undefined || inst === undefined) { return 实例不存在 —— 它不在该活动的清单里,从面包屑返回活动详情看看。; } @@ -754,7 +790,8 @@ export function ActiPage({ id }: { id: string }) { return (
- + setTerm(fmtMinute(Date.now())), - }, + onClick: () => + setAsk({ + title: `确认终止实例 ${inst.id}?`, + okLabel: '确认终止', + danger: true, + onOk: () => setTerm(fmtMinute(Date.now())), + }), } satisfies ActSpec, ] : []), ]} - /> + /> + {ask !== null && setAsk(null)} />} +
{reason !== undefined && {reason}} {state === '待执行' && 待执行:已排入队列,等待调度 —— 到点或由负责人手动起步。} diff --git a/docs/活动模块重构需求-2026-08-25.md b/docs/活动模块重构需求-2026-08-25.md index 666c8f3..13d117e 100644 --- a/docs/活动模块重构需求-2026-08-25.md +++ b/docs/活动模块重构需求-2026-08-25.md @@ -91,6 +91,7 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活 - 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。 - 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。 - 2026-08-26 · **实例不可删除、不可编辑(用户定,推翻 L3 原规划)**:L3 的编辑/删除入口撤掉,唯一动作是进行中的「终止活动」(二次确认);六功能里实例的「编辑/删除」两项就此划掉,待确认问题里的两条(编辑指什么 / 删除是否放开)同时关闭。 +- 2026-08-26 · **二次确认的规格(用户定)**:活动模块的确认走**轻气泡**(锚在触发钮下方的小泡泡,只问一句 + 确认/取消,点罩即取消)—— 不摆影响三必答;三必答(AskBand)是治理台重度动作的规格,别处的确认带暂不动。 ## 三、功能表(整理稿) -- 2.54.0 From fce60b1570b19c367a368c77a2ea14f20212ab6a Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 20:49:18 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=B0=94=E6=B3=A1?= =?UTF-8?q?=E6=8C=AA=E5=88=B0=E8=A7=A6=E5=8F=91=E9=92=AE=E5=B7=A6=E4=BE=A7?= =?UTF-8?q?(2026-08-26=20=E7=94=A8=E6=88=B7=E5=AE=9A:=E5=9C=A8=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=97=81=E8=BE=B9):=E5=9E=82=E7=9B=B4=E5=AF=B9?= =?UTF-8?q?=E5=88=B0=E9=92=AE=E4=B8=AD=E7=BA=BF,=E5=B0=8F=E4=B8=89?= =?UTF-8?q?=E8=A7=92=E4=BB=8E=E5=8F=B3=E7=BC=98=E6=8C=87=E5=9B=9E;L2/L3=20?= =?UTF-8?q?=E5=90=84=E8=87=AA=E7=9A=84=20right=20=E8=AE=A9=E4=BD=8D?= =?UTF-8?q?=E5=80=BC=E9=9A=8F=E9=92=AE=E6=95=B0=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/styles/console.css | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/styles/console.css b/apps/web/src/app/styles/console.css index 7e98475..af59d72 100644 --- a/apps/web/src/app/styles/console.css +++ b/apps/web/src/app/styles/console.css @@ -710,22 +710,26 @@ } /* ══ 二次确认·轻气泡(2026-08-26 用户定:只问一句 + 确认/取消,不摆影响三必答)══ - 锚在页头右下、小三角指回触发钮;点气泡外的透明罩即取消; + 气泡挨在触发钮**左边**(垂直对到钮中线),小三角从右缘指回钮;点透明罩即取消; 阴影只给弹层(§10),确认钮 danger 非实心(§11),进场 180ms 从触发点展开(§13) */ /* 正 z-index 必须有:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文, 没有它,气泡的 z-index 只在页头内部作数,盖不住后面的区块 */ .g-actvd .g-pahead, .g-actid .g-pahead { position: relative; z-index: 30; } .g-actvd .g-popask-veil, .g-actid .g-popask-veil { position: fixed; inset: 0; z-index: 39; } -.g-actvd .g-popask, .g-actid .g-popask { position: absolute; top: calc(100% + 10px); right: 0; z-index: 40; +/* right 的取值 = 页头右 padding + 触发钮右边那几枚钮的宽度和 + 10px 间隙 —— 钮文案改了这里要跟着量 */ +.g-actvd .g-popask, .g-actid .g-popask { position: absolute; top: 16px; z-index: 40; width: 264px; padding: 13px 14px 12px; background: #fff; border: 1px solid var(--tone-line, var(--cline)); border-radius: 12px; box-shadow: 0 12px 32px rgba(22, 26, 34, .14), 0 2px 8px rgba(22, 26, 34, .08); - animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: top right; } -@keyframes actvd-ask { from { opacity: 0; transform: translateY(-4px) scale(.97); } } -/* 小三角:指回右上方的触发钮(左/上两条边旋转 45° 拼出) */ -.g-actvd .g-popask::before, .g-actid .g-popask::before { content: ""; position: absolute; top: -6px; right: 60px; + animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: right center; } +/* L2:触发钮右边还有「启动活动」占位钮 → 让出两枚钮的宽度;L3:触发钮自己就是最右枚 */ +.g-actvd .g-popask { right: 232px; } +.g-actid .g-popask { right: 130px; } +@keyframes actvd-ask { from { opacity: 0; transform: translateX(4px) scale(.97); } } +/* 小三角:右缘中点指向触发钮(上/右两条边旋转 45° 拼出) */ +.g-actvd .g-popask::before, .g-actid .g-popask::before { content: ""; position: absolute; top: 50%; margin-top: -5px; right: -6px; width: 10px; height: 10px; background: #fff; transform: rotate(45deg); - border-left: 1px solid var(--tone-line, var(--cline)); border-top: 1px solid var(--tone-line, var(--cline)); } + border-top: 1px solid var(--tone-line, var(--cline)); border-right: 1px solid var(--tone-line, var(--cline)); } .g-actvd .g-popask p, .g-actid .g-popask p { margin: 0; font-size: 13px; font-weight: 600; line-height: 1.55; color: var(--ink); } .g-actvd .g-popask-a, .g-actid .g-popask-a { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } @media (prefers-reduced-motion: reduce) { -- 2.54.0 From a8797b8cc802d9b855c36646a98ecbf9625e29e9 Mon Sep 17 00:00:00 2001 From: shanqin <45168502@qq.com> Date: Wed, 26 Aug 2026 21:20:25 +0800 Subject: [PATCH 7/8] =?UTF-8?q?L2=20=E6=B2=99=E7=AE=B1=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E9=A1=B5=E6=B8=85=E7=A9=BA=E6=BC=94=E7=A4=BA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?(2026-08-26=20=E7=94=A8=E6=88=B7=E5=AE=9A):=E5=8F=AA=E7=95=99?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6=E5=BD=93=E7=BB=93=E6=9E=84=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?,=E7=A9=BA=E5=80=BC=E5=8F=A3=E5=BE=84=20-/=E6=9A=82=E6=97=A0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/sandbox-demo/activity-view.html | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/web/public/sandbox-demo/activity-view.html b/apps/web/public/sandbox-demo/activity-view.html index 7850cc9..41a036d 100644 --- a/apps/web/public/sandbox-demo/activity-view.html +++ b/apps/web/public/sandbox-demo/activity-view.html @@ -8,6 +8,7 @@ 活动视图占位页(2026-08-25,L2 沙箱 v1): 模拟「服务器返回的 HTML」—— 由 iframe + sandbox="allow-scripts" 隔离渲染, 与宿主页面零样式/脚本互通;正式版由接口按活动定义返回。 + 2026-08-26 用户定:演示数据全部清空,只留骨架当结构占位(空值口径:无数据显 `-` / 暂无数据)。 --> -
-
- -
-

询价接收与结构化

-

quote-intake · 活动视图(服务器返回 HTML · 沙箱隔离渲染)

-
-
- -
-
今日接收-
-
待人工补全-
-
结构化成功-
-
平均耗时-
-
- -
-

最近的询价原文

- - - - - - - -
来源物料数接收时间结构化结果
暂无数据
-
- -
-

人工补全

-
-
物料编码
-
-
数量
-
-
交期要求
-
-
- - -
-
-
-
-- 2.54.0