人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 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:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 推送 → 失效映射 —— **这段错了不报错,只是画面比事实旧**。
|
||||
*
|
||||
* 它整段失效过一次:键前缀写成 `['tasks']`,而工作台的键是 `['desk','tasks']` ——
|
||||
* 推送照发、一条也匹配不到,任务办完了列表不挪窝。这类错编译器与类型都拦不住,
|
||||
* 只能靠断言「推送到了,哪几把查询会重取」。
|
||||
*
|
||||
* 测法:把每一屏真实在用的键都塞进 QueryClient,发一次 touch,看谁被标脏。
|
||||
* 新增一屏就往 SCREENS 里加一行 —— 这张表本身就是「哪一屏靠什么刷新」的账。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { invalidateFor } from '../subscribe.ts';
|
||||
import type { Touch } from '../subscribe.ts';
|
||||
|
||||
/** 各屏真实在用的键(与代码里的 queryKey 逐条对齐) */
|
||||
const SCREENS: ReadonlyArray<readonly [string, readonly unknown[]]> = [
|
||||
['工作台 · 任务列表', ['desk', 'tasks']],
|
||||
['工作台 · 任务详情', ['desk', 'task', 'T1']],
|
||||
['工作台 · 消息', ['desk', 'messages']],
|
||||
['工作台 · 审批', ['desk', 'approvals']],
|
||||
['运营台 · 任务', ['gov', 'tasks']],
|
||||
['运营台 · 任务日志', ['gov', 'tasklog', 'T1']],
|
||||
['运营台 · 工作流', ['gov', 'workflow', 'W1']],
|
||||
['运营台 · 审批', ['gov', 'approvals']],
|
||||
['运营台 · 驾驶舱', ['gov', 'metrics']],
|
||||
['设计台 · 活动库', ['gov', 'activities']],
|
||||
['设计台 · 工具箱', ['gov', 'tools']],
|
||||
['设计台 · 大模型', ['gov', 'llm']],
|
||||
['设计台 · 流程包', ['gov', 'process']],
|
||||
['设计台 · 数据域', ['gov', 'ledger', 'quote_intake', 'record']],
|
||||
['测试台 · 当前在跑', ['gov', 'testbed', 'current']],
|
||||
['测试台 · 单活动观测', ['gov', 'testbed', 'activity', 'T1']],
|
||||
['测试台 · 流程包观测', ['gov', 'testbed', 'workflow', 'W1']],
|
||||
['测试台 · trace', ['gov', 'testbed', 'trace', 'W1']],
|
||||
['员工', ['gov', 'staff']],
|
||||
];
|
||||
|
||||
/** 发一次推送,回报哪几屏被标脏 */
|
||||
function dirtiedBy(touch: Touch): string[] {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
for (const [, key] of SCREENS) qc.setQueryData(key, { seeded: true });
|
||||
invalidateFor(qc, touch);
|
||||
return SCREENS.filter(([, key]) => qc.getQueryState(key)?.isInvalidated === true).map(([name]) => name);
|
||||
}
|
||||
|
||||
describe('task touch —— 一单动了', () => {
|
||||
const dirty = dirtiedBy({ kind: 'task', id: 'T1', reason: 'advanced' });
|
||||
|
||||
it('工作台该动的都动:列表 / 这一单 / 审批', () => {
|
||||
expect(dirty).toEqual(expect.arrayContaining(['工作台 · 任务列表', '工作台 · 任务详情', '工作台 · 审批']));
|
||||
});
|
||||
|
||||
it('运营台的任务面与日志跟着动', () => {
|
||||
expect(dirty).toEqual(expect.arrayContaining(['运营台 · 任务', '运营台 · 任务日志']));
|
||||
});
|
||||
|
||||
/*
|
||||
* 测试台的键自成一支(`gov.testbed.*`),不在上面几条的前缀里。
|
||||
* 漏了它 = 测试台只剩 2 秒轮询,而轮询到终态就停:一单跑完那一屏就此定格。
|
||||
*/
|
||||
it('**测试台跟着动** —— 它不在任何一条任务键的前缀里', () => {
|
||||
expect(dirty).toEqual(expect.arrayContaining(['测试台 · 当前在跑', '测试台 · 单活动观测']));
|
||||
});
|
||||
|
||||
it('不误伤配置面 —— 一单推进不该让活动库/工具箱整屏重取', () => {
|
||||
expect(dirty).not.toContain('设计台 · 活动库');
|
||||
expect(dirty).not.toContain('设计台 · 工具箱');
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflow touch —— 一条流动了', () => {
|
||||
const dirty = dirtiedBy({ kind: 'workflow', id: 'W1', reason: 'task-advanced' });
|
||||
|
||||
it('流自己、名下任务与审批、测试台一起动', () => {
|
||||
expect(dirty).toEqual(
|
||||
expect.arrayContaining([
|
||||
'运营台 · 工作流',
|
||||
'工作台 · 任务列表',
|
||||
'工作台 · 审批',
|
||||
'运营台 · 审批',
|
||||
'测试台 · 当前在跑',
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('system touch —— 全局变了(配置改了 / 试跑单被销毁 / 台账落了新事实)', () => {
|
||||
const dirty = dirtiedBy({ kind: 'system', id: 'gov', reason: 'POST /api/gov/activities' });
|
||||
|
||||
it('治理两台全屏失效:配置面、数据域、驾驶舱', () => {
|
||||
expect(dirty).toEqual(
|
||||
expect.arrayContaining([
|
||||
'设计台 · 活动库',
|
||||
'设计台 · 工具箱',
|
||||
'设计台 · 大模型',
|
||||
'设计台 · 流程包',
|
||||
'设计台 · 数据域',
|
||||
'运营台 · 驾驶舱',
|
||||
'测试台 · 当前在跑',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* 销毁试跑单拔掉的是**真任务** —— 它这一刻还挂在负责人的工作台列表上。
|
||||
* 不连 desk 一起失效,那儿会留着一张点进去就 404 的鬼卡片。
|
||||
*/
|
||||
it('**工作台也要失效** —— 被销毁的试跑单还挂在人家列表上', () => {
|
||||
expect(dirty).toEqual(expect.arrayContaining(['工作台 · 任务列表', '工作台 · 任务详情']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('staff touch —— 某个人的东西变了', () => {
|
||||
const dirty = dirtiedBy({ kind: 'staff', id: 'S1', reason: 'elicitation' });
|
||||
|
||||
it('只动消息气泡与员工面,不惊动任务面', () => {
|
||||
expect(dirty).toEqual(expect.arrayContaining(['工作台 · 消息', '员工']));
|
||||
expect(dirty).not.toContain('工作台 · 任务列表');
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { Session, TaskCard, TaskDetail, WorkOrderReply } from '@contracts';
|
||||
import {
|
||||
@@ -45,6 +45,22 @@ export function AppShell() {
|
||||
const [module, setModule] = useState<ModuleKey>('tasks');
|
||||
const [taskId, setTaskId] = useState('');
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
/*
|
||||
* 工单填报值 —— **壳层持有**,工作区与对话区共用同一份。
|
||||
*
|
||||
* 验收工单是「AI 已填好、可编辑」,而**裁决在对话区的验收单上**(PRD 口径):
|
||||
* 编辑与裁决是同一件事的两半,却分在两个区里做。缓冲只活在工作区里,
|
||||
* 人在报告上改的值就到不了裁决那一刻 —— 验收回传的 `values` 恒空,修订永远落不下来。
|
||||
*
|
||||
* 换任务即清空:填报值是**过程状态**,不跨单。
|
||||
*/
|
||||
const [orderValues, setOrderValues] = useState<Record<string, string>>({});
|
||||
/*
|
||||
* 对话区点「打开工单」→ 视图区跳转定位(PRD:locate)。
|
||||
* `seq` 每点递增:同一张卡点两次也要再跳一次 —— 人就是想回到它。
|
||||
*/
|
||||
const [locate, setLocate] = useState<{ seq: number; orderId?: string; tab?: 'comms' } | null>(null);
|
||||
const locateSeq = useRef(0);
|
||||
const split = useChatWidth();
|
||||
|
||||
|
||||
@@ -196,7 +212,12 @@ export function AppShell() {
|
||||
tasks={list}
|
||||
selectedId={current}
|
||||
collapsed={collapsed}
|
||||
onPick={setTaskId}
|
||||
onPick={(id) => {
|
||||
// 换任务即清空填报缓冲与定位:它们是**过程状态**,不跨单
|
||||
setOrderValues({});
|
||||
setLocate(null);
|
||||
setTaskId(id);
|
||||
}}
|
||||
onCollapse={() => setCollapsed(true)}
|
||||
/>
|
||||
{/*
|
||||
@@ -208,9 +229,20 @@ export function AppShell() {
|
||||
key={current}
|
||||
detail={detail.data}
|
||||
onReplyOrder={(reply) => replyOrder.mutate(reply)}
|
||||
values={orderValues}
|
||||
onField={(k, v) => setOrderValues((old) => ({ ...old, [k]: v }))}
|
||||
locate={locate}
|
||||
/>
|
||||
) : (
|
||||
<main className="execution-panel">
|
||||
/*
|
||||
* **手上没任务时,中栏也得是一张卡**。
|
||||
*
|
||||
* 其余四个模块(审批 / 消息 / 纪要 / 汇报)的空态都挂 `jc-panel-card`,
|
||||
* 只有任务这一栏是裸的 `execution-panel` —— 而那个类在工作区里被显式
|
||||
* 去边框去底色(视图区不再有外层卡)。于是同一套工作台上,四个模块的空态
|
||||
* 是白卡片,任务这一栏却是画布上飘着的一段文字,看着像没渲染出来。
|
||||
*/
|
||||
<main className="execution-panel jc-panel-card">
|
||||
{tasks.isLoading ? (
|
||||
<div className="jc-list-empty">
|
||||
<Ico name="list-checks" s={18} />
|
||||
@@ -235,7 +267,20 @@ export function AppShell() {
|
||||
/>
|
||||
<section className="session-chat">
|
||||
{detail.data !== undefined ? (
|
||||
<TaskChat key={current} detail={detail.data} onLocate={() => undefined} />
|
||||
<TaskChat
|
||||
key={current}
|
||||
detail={detail.data}
|
||||
onLocate={(orderId) => {
|
||||
locateSeq.current += 1;
|
||||
setLocate({ seq: locateSeq.current, orderId });
|
||||
}}
|
||||
onOpenComms={() => {
|
||||
locateSeq.current += 1;
|
||||
setLocate({ seq: locateSeq.current, tab: 'comms' });
|
||||
}}
|
||||
orderValues={orderValues}
|
||||
onField={(k, v) => setOrderValues((old) => ({ ...old, [k]: v }))}
|
||||
/>
|
||||
) : (
|
||||
<div className="jc-tch">
|
||||
<header className="jc-tch-head">
|
||||
|
||||
+292
-10
@@ -116,7 +116,7 @@
|
||||
/* ===== 三栏衔接:列表区 / 视图区 / 对话区统一为三张等距同规格卡片 =====
|
||||
覆盖 vendor 的「左中融合 + 右侧浮卡」旧结构:等间距 gutter、同圆角、同描边、同投影 */
|
||||
/* 对话区宽度 = var(--jc-chat-w)(用户拖拽分隔柄写入 workspace inline style),未拖时走各断点默认 */
|
||||
.proto-root .workspace { position: relative; gap: 12px; padding: 12px 14px; --jc-ws-pr: 14px; --jcai-ai-chat-width: var(--jc-chat-w, 440px); grid-template-columns: 310px minmax(var(--jc-work-min), 1fr) minmax(0, var(--jcai-ai-chat-width)); }
|
||||
.proto-root .workspace { position: relative; gap: 12px; padding: 12px 14px; --jc-ws-pr: 14px; --jcai-ai-chat-width: var(--jc-chat-w, 440px); grid-template-columns: minmax(var(--jc-side-min), 310px) minmax(var(--jc-work-min), 1fr) minmax(var(--jc-chat-min), var(--jcai-ai-chat-width)); }
|
||||
/* 收起态 grid:无媒体查询、全断点生效,覆盖 vendor 各断点的三栏定义(修列错位 bug)。
|
||||
清单收起→[视图区 1fr | 会话区];会话收起→[清单 | 视图区 1fr];都收起→[视图区 1fr]。 */
|
||||
.proto-root .workspace.jc-side-collapsed:not(.jc-chat-collapsed) { grid-template-columns: minmax(0, 1fr) var(--jcai-ai-chat-width, 440px); }
|
||||
@@ -146,10 +146,25 @@
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
/*
|
||||
空态要在这张卡里**上下居中**,不是贴着顶。
|
||||
|
||||
`.jc-empty` 早就写了 `margin: auto`,可它一直只把自己横向居中 ——
|
||||
`margin-top/bottom: auto` 只有在 flex/grid 容器里才吃得开,在块级容器里算 0。
|
||||
而这张卡(`.jc-panel-card`)恰恰没设 display:flex(设了 flex 的是它的反面
|
||||
`:not(.jc-panel-card)`),于是五个模块的空态全贴在卡的顶边上。
|
||||
|
||||
只挑「直接儿子是空态」的那些卡改 —— `DocPanel`(纪要正文)用的是同一个类名,
|
||||
它里头是一整块要滚的文档,居中了就成了上下留白、内容夹在中间。
|
||||
`:has()` 在这儿正好:按内容挑,不必给六处组件各加一个修饰类。 */
|
||||
.proto-root .workspace .execution-panel.jc-panel-card:has(> .jc-empty) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.proto-root .session-chat { border-radius: 12px; }
|
||||
/* 窄屏(≤1380 视口):收窄三栏配比,保证对话区不被裁切(拖拽值 --jc-chat-w 仍优先) */
|
||||
@media (max-width: 1380px) {
|
||||
.proto-root .workspace { --jc-ws-pr: 12px; --jcai-ai-chat-width: var(--jc-chat-w, clamp(430px, 34vw, 560px)); grid-template-columns: 256px minmax(var(--jc-work-min), 1fr) minmax(0, var(--jcai-ai-chat-width)); gap: 10px; padding: 10px 12px; }
|
||||
.proto-root .workspace { --jc-ws-pr: 12px; --jcai-ai-chat-width: var(--jc-chat-w, clamp(430px, 34vw, 560px)); grid-template-columns: minmax(var(--jc-side-min), 256px) minmax(var(--jc-work-min), 1fr) minmax(var(--jc-chat-min), var(--jcai-ai-chat-width)); gap: 10px; padding: 10px 12px; }
|
||||
}
|
||||
|
||||
/* 对话区/视图区可调比例:workspace 直属分隔柄,锚在「视图区↔对话区」的 12px gutter 上
|
||||
@@ -1638,6 +1653,12 @@
|
||||
.proto-root .jc-tbl th { background: var(--soft); text-align: left; padding: 8px 10px; font-weight: 600; color: var(--ink-2); white-space: nowrap; }
|
||||
.proto-root .jc-tbl td { padding: 7px 10px; border-top: 1px solid var(--line-soft); color: var(--ink); }
|
||||
.proto-root .jc-tbl input { width: 100%; border: 1px solid var(--brand); border-radius: 5px; padding: 3px 6px; font: inherit; font-size: 12px; }
|
||||
/* 空格子喊「待填写」—— 红边红占位字,填上即恢复常态(一片文字里的两个空格,不喊没人看得见) */
|
||||
.proto-root .jc-tbl input.jc-tbl-todo { border-color: var(--danger); background: var(--danger-soft); }
|
||||
.proto-root .jc-tbl input.jc-tbl-todo::placeholder { color: var(--danger); font-weight: 700; opacity: 1; }
|
||||
.proto-root .jc-tbl input.jc-tbl-todo:focus { outline: 2px solid var(--danger); outline-offset: -1px; }
|
||||
/* 只读态的空格子:如实说没填,不留一片让人以为「本来就没这项」的空白 */
|
||||
.proto-root .jc-tbl .jc-tbl-empty { font-style: normal; font-size: 11px; color: var(--ink-4); }
|
||||
.proto-root .jc-tbl-bar { display: flex; gap: 7px; align-items: center; justify-content: flex-end; }
|
||||
|
||||
.proto-root .jc-bars { display: flex; flex-direction: column; gap: 7px; }
|
||||
@@ -2115,6 +2136,20 @@
|
||||
.proto-root .jc-dv-h { display: flex; align-items: flex-start; gap: 10px; }
|
||||
.proto-root .jc-dv-ico { flex: none; width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; color: var(--ink-4); background: var(--soft); }
|
||||
.proto-root .jc-dv.s-ok .jc-dv-ico { color: var(--ok); background: var(--ok-soft); }
|
||||
/*
|
||||
* **执行与验收两种颜色**(2026-08-02 用户定):一摞成果里「我办过的那张单」与
|
||||
* 「我签过字的那一关」是两类东西 —— 同色排下来,人得逐行读标题才分得出。
|
||||
* 执行走品牌蓝(在办的调子),验收走绿(过了关的调子);退回压过两者走警示色。
|
||||
*/
|
||||
.proto-root .jc-dv.m-exe .jc-dv-ico { color: var(--brand); background: var(--brand-wash); }
|
||||
.proto-root .jc-dv.m-exe .jc-dv-st { color: var(--brand-deep); background: var(--brand-wash); }
|
||||
.proto-root .jc-dv.m-exe { border-left: 3px solid var(--brand-soft); }
|
||||
.proto-root .jc-dv.m-acc .jc-dv-ico { color: var(--ok); background: var(--ok-soft); }
|
||||
.proto-root .jc-dv.m-acc .jc-dv-st { color: var(--ok); background: var(--ok-soft); }
|
||||
.proto-root .jc-dv.m-acc { border-left: 3px solid var(--ok); }
|
||||
.proto-root .jc-dv.s-bad.m-exe, .proto-root .jc-dv.s-bad.m-acc { border-left-color: var(--danger); }
|
||||
.proto-root .jc-dv.s-bad.m-exe .jc-dv-ico, .proto-root .jc-dv.s-bad.m-acc .jc-dv-ico { color: var(--danger); background: var(--danger-soft); }
|
||||
.proto-root .jc-dv.s-bad.m-exe .jc-dv-st, .proto-root .jc-dv.s-bad.m-acc .jc-dv-st { color: var(--danger); background: var(--danger-soft); }
|
||||
.proto-root .jc-dv.s-live .jc-dv-ico { color: #fff; background: var(--ai-grad); }
|
||||
.proto-root .jc-dv-t { flex: 1; min-width: 0; }
|
||||
.proto-root .jc-dv-t em { display: block; font-style: normal; font-size: 10px; font-weight: 700; letter-spacing: .04em; color: var(--ink-4); font-family: var(--mono); }
|
||||
@@ -2267,6 +2302,15 @@
|
||||
/* 卡体与动作栏 */
|
||||
.proto-root .jc-fc-b { padding: 11px 13px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.proto-root .jc-fc-ft { display: flex; align-items: center; gap: 8px; padding: 9px 13px; border-top: 1px solid var(--line-soft); background: #fbfcfe; }
|
||||
/*
|
||||
* **交不了的时候,说清差在哪** —— 只摆一个灰按钮等于让人猜自己哪儿做错了。
|
||||
* 底色用警示色而不是错误红:这不是出错,是活还没干完。
|
||||
*/
|
||||
.proto-root .jc-fc-gate { padding: 9px 13px; border-top: 1px solid #f2dfc2; background: #fffaf2; }
|
||||
.proto-root .jc-fc-gate p { display: flex; align-items: center; gap: 6px; margin: 0; color: #9a5b02; font-size: 12.5px; }
|
||||
.proto-root .jc-fc-gate p svg { flex: 0 0 auto; }
|
||||
.proto-root .jc-fc-gate ul { margin: 6px 0 0; padding: 0 0 0 19px; list-style: disc; color: var(--ink-2); font-size: 12px; line-height: 1.75; }
|
||||
.proto-root .jc-fc-gate li.more { list-style: none; margin-left: -19px; color: var(--ink-3); }
|
||||
.proto-root .jc-fc-sp { flex: 1; }
|
||||
.proto-root .jc-fc-open { width: 100%; justify-content: center; }
|
||||
/* 说明 / 建议 */
|
||||
@@ -2286,12 +2330,48 @@
|
||||
.proto-root .jc-fc-doc:hover { border-color: var(--fc); background: #fff; box-shadow: var(--shadow-1); }
|
||||
.proto-root .jc-fc-doc-ico { flex: none; width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; color: #fff; background: var(--fc); }
|
||||
.proto-root .jc-fc-doc-bd { flex: 1; min-width: 0; }
|
||||
.proto-root .jc-fc-doc-bd em { display: block; font-style: normal; font-size: 9.5px; font-weight: 800; letter-spacing: .05em; color: var(--ink-4); }
|
||||
.proto-root .jc-fc-doc-bd b { display: block; margin-top: 1px; font-size: 12.5px; font-weight: 700; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/*
|
||||
* **四字粗体在上、小字说明在下**(2026-08-02 用户:执行卡与验收卡「格式不统一」)。
|
||||
* 从前是反的(小字在上),而两张卡里只有一张这么排 —— 人扫过去落点对不齐。
|
||||
*/
|
||||
.proto-root .jc-fc-doc-bd b { display: block; font-size: 12.5px; font-weight: 800; color: var(--ink); }
|
||||
.proto-root .jc-fc-doc-bd em { display: block; margin-top: 2px; font-style: normal; font-size: 10.5px; font-weight: 500; line-height: 1.5; color: var(--ink-3); }
|
||||
.proto-root .jc-fc-doc > svg { flex: none; color: var(--ink-4); transition: transform var(--t-fast); }
|
||||
.proto-root .jc-fc-doc:hover > svg { transform: translateX(2px); color: var(--fc); }
|
||||
/* 判据表(卡内紧凑版) */
|
||||
.proto-root .jc-fc .jc-rp-crit.sm li { padding: 6px 11px; font-size: 11.5px; }
|
||||
/*
|
||||
* **作业指导书与验收判据同一套版面**(2026-08-02 用户定)。
|
||||
* 从前指导书是一串松散的圆点(`.jc-steps`),判据是一摞带章的行 ——
|
||||
* 同一张卡上两套版面语言,人得在两种读法之间来回切。
|
||||
* 差别只留一处:指导书**没有达标/未达标** —— 它是要办的事,不是已办成的事;
|
||||
* 不给步骤画对勾:系统并不知道人做没做,画上去就是撒谎。
|
||||
*/
|
||||
.proto-root .jc-guide li { align-items: flex-start; }
|
||||
.proto-root .jc-guide .jc-lv { flex: none; margin-top: 1px; }
|
||||
.proto-root .jc-guide .jc-lv-step { background: var(--brand-wash); color: var(--brand); border: 1px solid var(--brand-soft); }
|
||||
.proto-root .jc-guide .jc-lv-done { display: inline-grid; place-items: center; background: #fff4e5; color: #de7802; border: 1px solid #f2dfc2; padding: 1px 4px; }
|
||||
.proto-root .jc-guide .t { line-height: 1.65; }
|
||||
/* 末行是这几步的收口 —— 底色分开,读到那儿就知道「做到这样算数」 */
|
||||
.proto-root .jc-guide li.done { background: #fffaf2; }
|
||||
.proto-root .jc-guide li.done .t b { display: block; margin-bottom: 1px; color: #9a5b02; font-weight: 800; }
|
||||
|
||||
/*
|
||||
* **办结之后那一屏:落地结果**(2026-08-02 用户定)。
|
||||
* 判据说的是「签之前要核什么」;签完之后人问的是「世界上发生了什么」。
|
||||
*/
|
||||
.proto-root .jc-landing { display: flex; flex-direction: column; gap: 8px; }
|
||||
.proto-root .jc-landing-h { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 12.5px; color: var(--ok); }
|
||||
.proto-root .jc-landing-h b { font-weight: 800; }
|
||||
.proto-root .jc-landing-h span { color: var(--ink-2); font-weight: 700; }
|
||||
.proto-root .jc-landing .jc-rp-crit .r { color: var(--ink-3); font-weight: 700; }
|
||||
.proto-root .jc-landing-p { display: flex; align-items: flex-start; gap: 6px; margin: 0; font-size: 11.5px; color: var(--ink-2); line-height: 1.6; }
|
||||
.proto-root .jc-landing-p svg { flex: 0 0 auto; margin-top: 2px; color: var(--brand); }
|
||||
.proto-root .jc-landing-p b { margin-right: 6px; font-weight: 800; color: var(--ink); }
|
||||
.proto-root .jc-landing-p.warn { color: #9a5b02; }
|
||||
.proto-root .jc-landing-p.warn svg { color: #de7802; }
|
||||
/* 带过去了哪些字段 —— 摆名字不摆值:值在成果件里,这一行答的是「带了哪些东西」 */
|
||||
.proto-root .jc-landing-carry { display: block; margin-top: 3px; font-style: normal; font-size: 11px; color: var(--ink-3); line-height: 1.6; }
|
||||
|
||||
/* 决策选项(卡内) */
|
||||
.proto-root .jc-tch .jc-branches { margin: 0; display: flex; flex-direction: column; gap: 7px; }
|
||||
@@ -2685,7 +2765,15 @@
|
||||
.proto-root .jc-mdonut .jc-donut-lg span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ══ 空占位(待处理 / 已完成):层叠光环 + 图标 + 主副文 + 提示囊 ══ */
|
||||
.proto-root .jc-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 58px 24px 64px; text-align: center; animation: rise var(--t-med) both; }
|
||||
/*
|
||||
* 空态 —— **在它那一栏里居中**,不是吊在顶上。
|
||||
*
|
||||
* 从前是固定的上下内边距(58/64),于是在一栏一人高的工作区里,那幅图与那两行字
|
||||
* 挤在顶部三分之一处,底下留着一大片白。空态是常态不是缺页:它该像一张摆正的卡片,
|
||||
* 而不是页面还没加载完的残影。
|
||||
* `margin: auto` 让它在 flex 容器里上下左右都居中;容器不给高度时退回原来的内边距。
|
||||
*/
|
||||
.proto-root .jc-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; margin: auto; gap: 8px; padding: 40px 24px; text-align: center; animation: rise var(--t-med) both; }
|
||||
.proto-root .jc-empty-art { position: relative; width: 108px; height: 108px; margin-bottom: 6px; }
|
||||
.proto-root .jc-empty-art i { position: absolute; border-radius: 999px; }
|
||||
.proto-root .jc-empty-art .r1 { inset: 0; opacity: .55; }
|
||||
@@ -2864,12 +2952,29 @@
|
||||
*/
|
||||
.proto-root .workspace {
|
||||
--jc-work-min: 690px;
|
||||
/*
|
||||
* 对话区也要有地板(2026-07-31 补)。
|
||||
*
|
||||
* 上面那段只给了中栏地板,对话区写的是 `minmax(0, …)` —— 意思是「让到多少算多少」。
|
||||
* 三种 MacBook Air 都还剩 384px 以上,于是当时认为够。**1280 宽不在那张表里**:
|
||||
* 1280 − 内边距 28 − 间距 24 = 1228,减去 310(清单)+ 690(中栏)只剩 228 ——
|
||||
* 比它自己写的下限 340px 还窄。实测后果:决策卡的两个选项被压成一列竖排的单字,
|
||||
* 「我按缺口起草一封询问函」一个字一行,人根本没法读,更谈不上点。
|
||||
*
|
||||
* 三个都到地板要 180 + 690 + 340 = 1210 ≤ 1228,装得下 ——
|
||||
* 所以让**清单**先窄(它是一列任务卡,窄一点照样读),而不是压对话区。
|
||||
*/
|
||||
--jc-chat-min: 340px;
|
||||
--jc-side-min: 180px;
|
||||
}
|
||||
|
||||
/* 收起清单 / 收起对话时中栏本就宽,地板不必再管 —— 免得收起态反被地板顶出横向溢出 */
|
||||
.proto-root .workspace.jc-side-collapsed,
|
||||
.proto-root .workspace.jc-chat-collapsed {
|
||||
--jc-work-min: 0px;
|
||||
/* 收起态那一栏根本不在网格里,地板留着只会把剩下的栏顶出横向溢出 */
|
||||
--jc-chat-min: 0px;
|
||||
--jc-side-min: 0px;
|
||||
}
|
||||
|
||||
/* 列表区空态 —— 紧凑版:软色圆 + 主副文,与主面板的整屏光环同一语气 */
|
||||
@@ -2924,16 +3029,112 @@
|
||||
}
|
||||
|
||||
/* ── 工作日志(补充,非原型)—— 一行一步,标明谁的产出 ────────────────── */
|
||||
/* ── 沟通记录:一单往来的**原件** —— 一封一张纸,收/发两向分色 ── */
|
||||
/* 系统开场白上的那一下 —— 灰调不抢戏:它是个去处,不是要人拍板的动作 */
|
||||
.proto-root .jc-sys-act { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; padding: 4px 11px; border: 1px solid var(--line); border-radius: 999px; background: #fff; font: inherit; font-size: 11.5px; font-weight: 600; color: var(--ink-2); cursor: pointer; transition: border-color var(--t-fast), color var(--t-fast); }
|
||||
.proto-root .jc-sys-act:hover { border-color: var(--brand); color: var(--brand); }
|
||||
.proto-root .jc-sys-act:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
|
||||
/* ── 数据录入卡(第五类人类工具,2026-07-31)──────────────────────────
|
||||
壳、头、表单、按钮**一律用卡族公用的那套**(`jc-fc` / `jc-fc-h` / `jc-cfill`)。
|
||||
上一版自造了一整套 `jc-entry-*` 骨架:圆角、描边、头部配色全跟兄弟卡对不上,
|
||||
连类型章(图标 + 类型名)都没有 —— 五张卡摆在一条对话流里,唯独它像外来的。
|
||||
卡族要长得像一家人,身份写在头上,不靠各造一套框来区分。
|
||||
|
||||
只留三样这张卡独有的语义:落表提示、必填星、「待填写」小签。 */
|
||||
.proto-root .jc-fc.t-entry { --fc: var(--ok); --fc-soft: var(--ok-soft); }
|
||||
/* 落到哪张表、哪一行 —— 按下确认写的是什么,得先说清楚 */
|
||||
/*
|
||||
* **卡面摆的是表的人读名,不是 `customer.profile`**(2026-08-02 用户定)——
|
||||
* 界面不摆系统内的英文名。所以这一格不再用等宽字体:它现在是一句人话。
|
||||
*/
|
||||
.proto-root .jc-entry-tbl { display: block; margin-top: 1px; font-size: 11.5px; font-weight: 700; color: var(--ink-2); }
|
||||
.proto-root .jc-entry-key { display: block; margin-top: 2px; font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
/* 自增的那几列不摆进表单,但要说一声 —— 一声不吭地拿掉,人会以为这张档少了一格 */
|
||||
.proto-root .jc-entry-auto { display: flex; align-items: center; gap: 5px; margin: 6px 0 0; font-size: 11px; color: var(--ink-3); }
|
||||
.proto-root .jc-entry-auto svg { flex: 0 0 auto; color: var(--brand); }
|
||||
/* 两段以上才出的小标题 —— 一段的卡上摆它,是把上面那句又说了一遍 */
|
||||
.proto-root .jc-entry-sec { margin: 10px 0 0; font-size: 11px; font-weight: 800; letter-spacing: .04em; color: var(--ink-3); }
|
||||
.proto-root .jc-fc-b > .jc-cfill { margin-top: 0; }
|
||||
.proto-root .jc-cfill-f .req { font-style: normal; color: var(--danger); margin-left: 2px; }
|
||||
/* 待填写用**警示色的小签**,不染整格:染整格会把已填好的那几格也搅乱 */
|
||||
.proto-root .jc-entry-todo { font-style: normal; margin-left: 6px; padding: 0 6px; border-radius: 999px; background: var(--danger); color: #fff; font-size: 10px; font-weight: 700; }
|
||||
.proto-root .jc-cfill-f.todo input { border-color: var(--danger-soft, #f6c6c6); }
|
||||
.proto-root .jc-entry-warn { margin-right: auto; font-size: 11px; color: var(--danger); }
|
||||
.proto-root .jc-comms { display: flex; flex-direction: column; gap: 10px; padding: 2px 0; }
|
||||
.proto-root .jc-comm { border: 1px solid var(--line-soft); border-radius: 10px; background: #fff; overflow: hidden; }
|
||||
.proto-root .jc-comm.t-in { border-left: 3px solid var(--brand); }
|
||||
.proto-root .jc-comm.t-out { border-left: 3px solid #8b93a7; }
|
||||
.proto-root .jc-comm-h { display: flex; align-items: center; gap: 10px; padding: 10px 14px 8px; }
|
||||
/* ── 沟通记录的未读(2026-07-31 用户定)──────────────────────────────
|
||||
一封客户来件落进来之后,「看没看过」从前没人记 —— 新到的信和三天前的信
|
||||
长得一模一样,人只能靠时间戳自己比。未读要**一眼扫得出**:左缘一道红边 +
|
||||
标题旁一枚「未读」小签,不靠底色染整块(整块变色会把正文的可读性也搭进去)。
|
||||
计数只跟着按钮走:打开页签不自动清 —— 那是替人宣布他看过了。 */
|
||||
.proto-root .jc-comm.unread { border-color: var(--danger-soft, #f6c6c6); box-shadow: inset 3px 0 0 var(--danger); }
|
||||
.proto-root .jc-comm-new { flex: none; padding: 1px 7px; border-radius: 999px; background: var(--danger); color: #fff; font-size: 10px; font-weight: 800; letter-spacing: .04em; }
|
||||
/* 时间戳把按钮顶到最右 —— 标记是这一行的收尾动作,不跟标题抢位置 */
|
||||
.proto-root .jc-comm-h .jc-comm-at { margin-left: auto; }
|
||||
.proto-root .jc-comm-mark { flex: none; display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border: 1px solid var(--line-soft); border-radius: 999px; background: #fff; font: inherit; font-size: 11px; font-weight: 600; color: var(--ink-3); cursor: pointer; transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast); }
|
||||
.proto-root .jc-comm-mark:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
|
||||
.proto-root .jc-comm-mark:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
|
||||
.proto-root .jc-comm-mark:disabled { opacity: .55; cursor: default; }
|
||||
/* 已读那一枚淡下去 —— 它是「想改回未读」的入口,不该跟未读的按钮抢注意力 */
|
||||
.proto-root .jc-comm-mark.done { color: var(--ink-4); border-style: dashed; }
|
||||
.proto-root .jc-comm-dir { display: inline-flex; align-items: center; gap: 4px; flex: none; padding: 2px 7px; border-radius: 5px; background: #eef2fb; color: var(--brand); font-size: 11px; font-weight: 600; }
|
||||
.proto-root .jc-comm-dir.t-out { background: #f1f2f5; color: #5d6577; }
|
||||
.proto-root .jc-comm-sub { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 600; color: var(--ink); }
|
||||
.proto-root .jc-comm-at { flex: none; font-size: 11px; color: var(--ink-4); }
|
||||
.proto-root .jc-comm-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; padding: 0 14px 10px; font-size: 11.5px; color: var(--ink-3); }
|
||||
.proto-root .jc-comm-meta b { font-weight: 600; color: var(--ink-2); }
|
||||
.proto-root .jc-comm-meta em { font-style: normal; color: var(--ink-4); }
|
||||
/* ── 邮件正文:让它读起来像**一封信**,不像第二张卡 ────────────────────
|
||||
从前是「整块灰底 + 一条硬分割线」:白色信头下面接一块灰,两者拼成两个盒子,
|
||||
人第一眼看到的是两张摞着的卡,而不是一封有抬头有正文的信。
|
||||
改成**引文那一路**:同底色、左缘一道浅竖线、往里缩一格 —— 竖线说的是
|
||||
「下面这段是对方写的原话」,这比一块灰底说得清楚。
|
||||
|
||||
三处取自 ui-ux-pro-max 的排版规则:
|
||||
· 行长限 65–75 字符(`max-width: 68ch`)—— 正文原来横铺整栏,一行三四十个汉字,
|
||||
眼睛回扫时容易串行;
|
||||
· 行高落到 1.75(原来 1.85 偏散,那是给大段散文的);
|
||||
· 正文是这一屏的主内容,字号提到 13px —— 它不该比元信息还小。
|
||||
|
||||
`overflow-wrap` 从 `anywhere` 收成 `break-word`:`anywhere` 会把词从中间劈开,
|
||||
而料号(LM358DR)被劈成两行是**读错的开始**,只有超长且无处可断时才允许断。
|
||||
|
||||
字体写 `inherit` 不写 `var(--sans)`:**`--sans` 这个变量全仓库没有定义过**
|
||||
(只有 `--mono` 有)。从前这里是 `font: 400 12.5px/1.85 var(--sans)` 简写 ——
|
||||
带未定义 var 的简写会**整条作废**,字号行高一起丢,实测算出来是 16px/normal。
|
||||
`<pre>` 的 UA 默认字体是等宽,所以这一句必须显式写,不能省。 */
|
||||
.proto-root .jc-comm-body { margin: 0 14px 12px; padding: 2px 0 2px 12px; border-left: 2px solid var(--line); max-width: 68ch; font-family: inherit; font-size: 13px; font-weight: 400; line-height: 1.75; color: var(--ink-2); white-space: pre-wrap; overflow-wrap: break-word; }
|
||||
/* 来件与发件各给一点色相 —— 与信头左缘那道边同源,一眼分得出方向 */
|
||||
.proto-root .jc-comm.t-in .jc-comm-body { border-left-color: var(--brand-wash, #dbe6fb); }
|
||||
.proto-root .jc-comm.t-out .jc-comm-body { border-left-color: #e3e5ea; }
|
||||
.proto-root .jc-comm-att { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 14px; border-top: 1px solid var(--line-soft); }
|
||||
.proto-root .jc-att { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 8px; border: 1px solid var(--line-soft); border-radius: 6px; background: #fff; font-size: 11.5px; color: var(--ink-2); }
|
||||
.proto-root .jc-att svg { flex: none; color: #4f8a52; }
|
||||
/* 读不出内容的件:警示色 + 那句说明照抄 —— 摆一个「已上传 ✓」是撒谎 */
|
||||
.proto-root .jc-att.blind { border-color: #e4c9a5; background: #fdf8f0; }
|
||||
.proto-root .jc-att.blind svg { color: #b5803c; }
|
||||
.proto-root .jc-att b { flex: none; font-weight: 600; }
|
||||
.proto-root .jc-att i { flex: none; font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
.proto-root .jc-att em { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
|
||||
.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); }
|
||||
/* 归侧四类各一色:机器 / 人 / 人裁 / 系统等待 —— 一眼看出这一步是谁推的 */
|
||||
/*
|
||||
* **日志只有三色**(2026-08-02 用户定):负责人绿 · 智能体蓝 · 系统灰。
|
||||
* 从前按归侧(machine / human / human-verdict / world-wait / trigger)上五色 ——
|
||||
* 那是度量的分法,对读日志的人不构成区别:他只问「这一步是我办的、AI 办的,
|
||||
* 还是流程自己走到的」。三档一档一意,一眼扫得动。
|
||||
*/
|
||||
.proto-root .jc-log-row.t-agent .jc-log-side { background: var(--brand-wash); color: var(--brand-deep); }
|
||||
.proto-root .jc-log-row.t-owner .jc-log-side { background: #e6f7f4; color: #0a6a64; }
|
||||
.proto-root .jc-log-row.t-system .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); }
|
||||
@@ -2992,3 +3193,84 @@
|
||||
.proto-root .jc-port.on .jc-port-dot { border-color: var(--brand); background: var(--brand); box-shadow: inset 0 0 0 2.5px #fff; }
|
||||
.proto-root .jc-port b { font-weight: 700; }
|
||||
.proto-root .jc-port em { margin-left: auto; font-style: normal; font-size: 10px; color: var(--ink-4); }
|
||||
|
||||
/* ═════════ AI 正在工作 —— 两处状态动画(2026-07-30) ═════════
|
||||
语义:「在持续办事」,不是「在打字」。三点打字气泡(jc-typing)只在
|
||||
AI 正回你的话时出现;这里的均衡条与涟漪表达的是**环在自己转**。
|
||||
动效纪律:纯 CSS、幅度克制、只动 transform/opacity(不回流);
|
||||
prefers-reduced-motion 下全部静止但状态仍可读。 */
|
||||
|
||||
/* ── ① 对话区 · AI 办事条(消息流末尾的常驻在场) ── */
|
||||
.proto-root .jc-aiwork { display: inline-flex; align-items: center; gap: 9px; padding: 9px 13px; background: linear-gradient(135deg, rgba(51, 112, 255, 0.06), rgba(51, 112, 255, 0.02)); border: 1px solid rgba(51, 112, 255, 0.18); border-radius: 4px 14px 14px 14px; }
|
||||
.proto-root .jc-aiwork-eq { display: inline-flex; align-items: flex-end; gap: 2.5px; height: 14px; }
|
||||
.proto-root .jc-aiwork-eq i { width: 3px; height: 5px; border-radius: 2px; background: var(--brand); transform-origin: bottom; animation: jcEq 1.15s ease-in-out infinite; }
|
||||
.proto-root .jc-aiwork-eq i:nth-child(2) { animation-delay: 0.14s; }
|
||||
.proto-root .jc-aiwork-eq i:nth-child(3) { animation-delay: 0.28s; }
|
||||
.proto-root .jc-aiwork-eq i:nth-child(4) { animation-delay: 0.42s; }
|
||||
.proto-root .jc-aiwork-txt { font-size: 12px; font-weight: 600; color: var(--brand); letter-spacing: 0.01em; }
|
||||
.proto-root .jc-aiwork-sub { font-size: 11px; color: var(--ink-3); }
|
||||
@keyframes jcEq { 0%, 100% { transform: scaleY(0.45); opacity: 0.55; } 40% { transform: scaleY(1.9); opacity: 1; } }
|
||||
/* 头像呼吸环:AI 在场的第二信号 —— 很轻,只有留意的人看得见 */
|
||||
.proto-root .jc-ava-live { animation: jcBreathe 2.4s ease-in-out infinite; }
|
||||
@keyframes jcBreathe { 0%, 100% { box-shadow: 0 0 0 0 rgba(51, 112, 255, 0.28); } 50% { box-shadow: 0 0 0 6px rgba(51, 112, 255, 0); } }
|
||||
|
||||
/* ── ② 空工作区 · AI 推进中场景(涟漪 + 工作线) ── */
|
||||
.proto-root .jc-working { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 52px 24px 58px; text-align: center; animation: rise var(--t-med) both; }
|
||||
.proto-root .jc-working-art { position: relative; width: 116px; height: 116px; margin-bottom: 4px; display: grid; place-items: center; }
|
||||
/* 涟漪两环:从中心缓慢扩散消隐 —— 「环在转」的空间隐喻 */
|
||||
.proto-root .jc-working-art .rp { position: absolute; inset: 22px; border-radius: 999px; border: 1.5px solid rgba(51, 112, 255, 0.35); animation: jcRipple 2.6s cubic-bezier(0.2, 0.6, 0.36, 1) infinite; }
|
||||
.proto-root .jc-working-art .rp2 { animation-delay: 1.3s; }
|
||||
.proto-root .jc-working-ico { position: relative; z-index: 1; width: 52px; height: 52px; border-radius: 16px; display: grid; place-items: center; color: #fff; background: linear-gradient(140deg, var(--brand), #244ecf); box-shadow: 0 6px 18px rgba(51, 112, 255, 0.32); animation: jcBreathe 2.4s ease-in-out infinite; }
|
||||
@keyframes jcRipple { 0% { transform: scale(0.72); opacity: 0; } 18% { opacity: 0.9; } 100% { transform: scale(1.55); opacity: 0; } }
|
||||
/* 工作线:三行微光骨架 —— AI 正在把报告一行行写出来的意象 */
|
||||
.proto-root .jc-working-lines { display: flex; flex-direction: column; gap: 7px; width: 190px; margin: 8px 0 2px; }
|
||||
.proto-root .jc-working-lines i { height: 7px; border-radius: 4px; background: linear-gradient(90deg, var(--soft) 25%, rgba(51, 112, 255, 0.16) 50%, var(--soft) 75%); background-size: 220% 100%; animation: jcShimmer 1.7s ease-in-out infinite; }
|
||||
.proto-root .jc-working-lines i:nth-child(2) { width: 78%; animation-delay: 0.18s; }
|
||||
.proto-root .jc-working-lines i:nth-child(3) { width: 56%; animation-delay: 0.36s; }
|
||||
@keyframes jcShimmer { 0% { background-position: 120% 0; } 100% { background-position: -100% 0; } }
|
||||
.proto-root .jc-working > b { font-size: 15px; font-weight: 800; color: var(--ink); }
|
||||
.proto-root .jc-working > p { margin: 0; max-width: 30em; font-size: 12px; color: var(--ink-3); line-height: 1.8; }
|
||||
.proto-root .jc-working-hint { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; padding: 5px 13px; background: rgba(51, 112, 255, 0.07); border: 1px solid rgba(51, 112, 255, 0.2); border-radius: 999px; font-size: 11px; font-weight: 600; color: var(--brand); }
|
||||
|
||||
/* 动效偏好:全部静止 —— 状态仍在(条不动、环常亮、线为素底),信息一分不少 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.proto-root .jc-aiwork-eq i,
|
||||
.proto-root .jc-ava-live,
|
||||
.proto-root .jc-working-art .rp,
|
||||
.proto-root .jc-working-ico,
|
||||
.proto-root .jc-working-lines i { animation: none; }
|
||||
.proto-root .jc-working-art .rp { opacity: 0.35; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* ═══ 算出来的去向 ═══════════════════════════════════════════════════════
|
||||
* 与「选口」那一组同一副面孔,但**不是按钮**:去向是分类表算出来的事实,
|
||||
* 不该长得像一道可点的选择题。判据原文摆在下面 —— 人要能回核「凭什么走这条」。
|
||||
*/
|
||||
.proto-root .jc-ports.routed .jc-port { cursor: default; }
|
||||
.proto-root .jc-ports.routed .jc-port:hover { transform: none; }
|
||||
.proto-root .jc-ports-why { margin: 6px 0 0; font-size: 11px; color: var(--ink-4); }
|
||||
.proto-root .jc-ports-why code { font-size: 10.5px; color: var(--ink-3); background: var(--soft); border-radius: 5px; padding: 1px 6px; }
|
||||
|
||||
/* ── 验收判据的第三态 —— 放在末尾,压得过后面那条 `li:hover` ───────────────
|
||||
*
|
||||
* 裁过、裁成不满足:那不是「活没干完」,是 AI 办过、办不到,把判断交给人
|
||||
* (提交门本来就允许这样提交:裁过即可)。从前它与「压根没裁」同挤成一个红叉,
|
||||
* 人看到一条永远不绿的项,既不知道 AI 有没有办过、也不知道为什么,只能凭感觉签。
|
||||
* 用琥珀不用红:**红是缺口,琥珀是「等你拿个主意」**。
|
||||
*
|
||||
* 位置有讲究:`.jc-rp-crit li:hover` 与它同特异性(0,3,1),写在前面就会被悬停盖掉 ——
|
||||
* 鼠标一扫过,那一条就变回普通底色,人以为自己看错了。
|
||||
*/
|
||||
/*
|
||||
* 标法用**左侧色条**而不是整行底色:底色那一层在这条链上压不过既有的
|
||||
* `li` / `li:hover`(同特异性、位置更后),追下去只会变成一串 !important;
|
||||
* 而色条是 inset 阴影,谁也不跟它抢 —— 本仓标「命中行」本来就用这一招
|
||||
* (`jc-dmn-r.hit::before` 那条左边的竖线)。视觉语言一致,还更醒目。
|
||||
*/
|
||||
.proto-root .jc-rp-crit li.judged { box-shadow: inset 3px 0 0 #de7802; }
|
||||
.proto-root .jc-rp-crit li.judged .r { color: #de7802; }
|
||||
/* 理由那一句:裁成不满足时它才是签字的人真正要读的东西 */
|
||||
.proto-root .jc-rp-crit .jc-crit-why {
|
||||
display: block; margin-top: 3px; font-style: normal; font-size: 11px;
|
||||
line-height: 1.6; color: var(--ink-3);
|
||||
}
|
||||
|
||||
@@ -226,6 +226,56 @@
|
||||
.g-card-h svg { color: var(--ink-4); }
|
||||
.g-card-h .x { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--ink-4); }
|
||||
.g-card-b { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
/* `.g-card-b` 全仓库无一处使用——`Card` 组件的 `children` 直接落进 `.g-card`,从没吐过这层内容区。
|
||||
自带边框/内边距的件(`.g-rows` 等)塞进去还成立,`.g-entries` 网格自身 `padding: 0`,
|
||||
数据域一塞就和外框贴死成两条 1px 边框叠一条线(设计中心 · 数据域实测四面 0-1px)。
|
||||
`Card` 有 26 处调用横跨十个治理台面板,动 `Card` 本身或 `.g-card` 得逐面板复核会不会
|
||||
变成双重内边距,面太大;这里只在「网格直吐进卡体」这一种组合上后代选择器精确落刀,
|
||||
不碰共享组件与其余调用点。取值同 `.g-card-b`(与 `.g-card-h` 的 16px 横向对齐)。 */
|
||||
.g-card > .g-entries { padding: 14px 16px; }
|
||||
/* `.g-newflow`(新建流程 / 新建审批活动两处表单容器)此前**全仓库零条规则**——
|
||||
`Card` 的 children 直落 `.g-card`(见上一条注释),这层什么都不兜:输入框贴着卡的
|
||||
圆角边框「漏」出去、字段之间没有竖向行距、操作条贴死卡的右边缘(治理台实测截图为证)。
|
||||
两处调用点(gov-activity 审批活动表单 · gov-process 新建流程表单)结构相同,直接给
|
||||
`.g-newflow` 补一份通用表单容器,两边一起受益;取值抄 `.g-card-b`——那条规则本来就是
|
||||
给「直落 .g-card 的内容」定的,只是从没接线,不是发明新数字。 */
|
||||
.g-newflow { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
/* ── 数据域:按**性质**分区(2026-07-31)────────────────────────────────
|
||||
从前这一屏只按域分组,19 张表看着一模一样 —— 人分不出哪几张是要治理的主数据、
|
||||
哪几张只是一单一份的凭证。而「该不该管它、谁有权改它」全看性质:
|
||||
主数据要治理(谁能改、依据、去重、时效),单据只需查得到、派生根本不该手写。
|
||||
所以分区要**看得见**:一条带色的区头 + 一句说清这一类是什么。 */
|
||||
.g-kind { display: flex; flex-direction: column; gap: 12px; }
|
||||
.g-kind + .g-kind { margin-top: 22px; }
|
||||
.g-kind-h { display: grid; grid-template-columns: auto auto auto 1fr; align-items: center; gap: 8px;
|
||||
padding: 10px 14px; border-left: 3px solid var(--kind-c, var(--brand)); border-radius: 0 8px 8px 0;
|
||||
background: var(--kind-bg, var(--brand-wash)); }
|
||||
.g-kind-h svg { color: var(--kind-c, var(--brand)); }
|
||||
.g-kind-h b { font-size: 13.5px; font-weight: 800; color: var(--ink); letter-spacing: .02em; }
|
||||
.g-kind-h .n { padding: 1px 8px; border-radius: 999px; background: #fff;
|
||||
font: 700 10.5px/1.7 var(--sh-mono, ui-monospace); color: var(--kind-c, var(--brand)); }
|
||||
/* 说辞占满剩下那一格,窄屏时整条换行到下一行 —— 它不是装饰,是这一类的定义 */
|
||||
.g-kind-h p { grid-column: 4; margin: 0; font-size: 11.5px; line-height: 1.7; color: var(--ink-3); }
|
||||
@media (max-width: 1100px) {
|
||||
.g-kind-h { grid-template-columns: auto auto auto; }
|
||||
.g-kind-h p { grid-column: 1 / -1; margin-top: 4px; }
|
||||
}
|
||||
/* 四档各一个色:主数据(要治理)最重,派生用警示色 —— 它标的是「这本不该手写」 */
|
||||
.g-kind.t-m { --kind-c: #1a6dd6; --kind-bg: #eff5ff; }
|
||||
.g-kind.t-r { --kind-c: #5b6b7f; --kind-bg: #f4f6f9; }
|
||||
.g-kind.t-d { --kind-c: #b8791a; --kind-bg: #fdf6ea; }
|
||||
.g-kind.t-u { --kind-c: #9aa4b2; --kind-bg: #f6f7f9; }
|
||||
|
||||
/*
|
||||
卡内网格改 `auto-fill` —— **每张卡一样大,不随本域有几个对象变形**(2026-07-31)。
|
||||
`auto-fit` 会把空轨道**折叠掉**,剩下的轨道再把整行分完:于是同一屏上
|
||||
只有 1 个对象的域(customer / quote_archive)那张卡被拉成**整行宽**、
|
||||
2 个对象的域(supplier / quote_pricing / quote_negotiate)每张占**半行**、
|
||||
≥3 个的才是正常的三分之一 —— 一页里三种尺寸,看着像三种东西。
|
||||
`auto-fill` 保留空轨道:轨道宽只由容器宽定,与本域对象数无关,
|
||||
1 个对象就摆 1 张、右边留白,尺寸和别的域一模一样。
|
||||
留白是对的:卡的大小该表示「它是一张对象卡」,不该表示「这个域有几个对象」。 */
|
||||
.g-card > .g-entries { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||
.g-code { display: block; padding: 11px 13px; border-radius: 10px; background: #f5f7fa; border: 1px solid var(--line-soft); font-family: var(--mono); font-size: 11.5px; line-height: 1.75; color: var(--ink-2); overflow-wrap: anywhere; }
|
||||
|
||||
/* ============ 矩阵表 ============ */
|
||||
@@ -426,6 +476,12 @@
|
||||
/* 审批依据:一行两格 + 删 */
|
||||
.g-basis { display: flex; flex-direction: column; gap: 6px; }
|
||||
.g-basis-r { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 6px; }
|
||||
/* 「加一条」按钮是 `.g-basis`(纵向 flex)的直接子项,继承默认 `align-items: stretch`
|
||||
被拉成贯穿整行的横杠、看着像一行内容而不是按钮(实测截图为证)。`.g-basis-r` 那几行
|
||||
要满宽(两个输入 + 一枚删除钮),不能把 `.g-basis` 整体收窄去将就按钮,所以只钉住
|
||||
这一枚按钮自己的 cross-axis,不碰 `.g-basis` 本身、也不影响别处的 `.g-ver-act`
|
||||
(全仓库 `.g-basis` 只此一处调用,这条选择器不会外溢)。 */
|
||||
.g-basis > .g-ver-act { align-self: flex-start; }
|
||||
|
||||
/* 幽灵卡:同尺寸的虚线位 —— 一眼看出「这儿还能再添一条」 */
|
||||
.g-fcard.ghost { flex-direction: row; justify-content: center; align-items: center; gap: 7px; min-height: 122px; border-style: dashed; border-color: #c0cbd8; background: #fcfdfe; box-shadow: none; color: var(--ink-4); font: 600 12.5px/1 var(--sans); cursor: pointer; }
|
||||
@@ -936,6 +992,67 @@
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04), 0 10px 26px -16px rgba(8, 15, 28, .55);
|
||||
}
|
||||
|
||||
/* ── 单在另一台上:页签是人点的,不抢 —— 只在这一台顶上挂一句「它在那边」 ── */
|
||||
.tb-else { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--sh-line); border-radius: 8px; background: rgba(0, 0, 0, .22); }
|
||||
.tb-else b { font: 600 11px/1 var(--sh-mono); letter-spacing: .08em; color: var(--sh-steel); }
|
||||
.tb-else span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--sh-muted); }
|
||||
.tb-else .tb-ghost { margin-left: auto; flex: none; }
|
||||
|
||||
/* 起单入参纸上的必填记号 —— 一枚星号,跟在字段名后半格(「必填」二字挤得字段名折行) */
|
||||
.tb-pp-r > em .req { margin-left: 2px; font-style: normal; font-weight: 700; color: #c0553c; }
|
||||
|
||||
/* ── 契约面读数:起单前就该看见这个活动手上有什么 ──
|
||||
跑不动的原因八成在这几行里:工具没装、模型没绑、台账没物化、预算给太紧。 */
|
||||
.tb-face { display: flex; flex-direction: column; gap: 2px; padding: 8px 0 2px; }
|
||||
.tb-face-r { display: grid; grid-template-columns: 46px minmax(0, 1fr); align-items: baseline; gap: 10px; padding: 6px 0; border-bottom: 1px solid #16233c; }
|
||||
.tb-face-r > em { font: 600 10px/1.5 var(--sh-mono); font-style: normal; letter-spacing: .1em; color: var(--sh-muted); }
|
||||
.tb-face-r .v { display: flex; flex-wrap: wrap; gap: 5px; min-width: 0; font-size: 12px; color: var(--sh-ink); }
|
||||
.tb-face-r .v.mono { font: 500 11px/1.5 var(--sh-mono); color: var(--sh-steel); }
|
||||
.tb-face-r .v.none { font-size: 11.5px; color: var(--sh-muted); }
|
||||
.tb-face-r .chip { font: 500 10.5px/1.5 var(--sh-mono); font-style: normal; padding: 1px 7px; border: 1px solid var(--sh-line); border-radius: 999px; color: var(--sh-steel); overflow-wrap: anywhere; }
|
||||
|
||||
/* ── 预算读数三格:Harness 在环外掐的就是这三个数 ── */
|
||||
.tb-gauges { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; padding: 12px 0 4px; }
|
||||
.tb-g { display: flex; flex-direction: column; gap: 5px; padding: 9px 12px; border: 1px solid #1b2a44; border-radius: 9px; background: rgba(0, 0, 0, .22); }
|
||||
.tb-g > em { font: 600 9.5px/1 var(--sh-mono); font-style: normal; letter-spacing: .16em; text-transform: uppercase; color: var(--sh-muted); }
|
||||
.tb-g > b { font: 700 15px/1 var(--sh-mono); color: var(--sh-live); }
|
||||
.tb-g > b > i { font: 500 11px/1 var(--sh-mono); font-style: normal; color: var(--sh-muted); }
|
||||
.tb-g .bar { display: block; height: 3px; border-radius: 2px; background: #16233a; overflow: hidden; }
|
||||
.tb-g .bar > i { display: block; height: 100%; background: var(--sh-live); transition: width var(--t-fast); }
|
||||
.tb-g.warm > b, .tb-g.warm .bar > i { color: var(--sh-brass); background-color: var(--sh-brass); }
|
||||
.tb-g.warm > b { background: none; }
|
||||
.tb-g.hot > b, .tb-g.hot .bar > i { color: var(--sh-alarm); background-color: var(--sh-alarm); }
|
||||
.tb-g.hot > b { background: none; }
|
||||
.tb-reason { margin: 6px 0 0; font-size: 12px; color: var(--sh-brass); }
|
||||
|
||||
/* ── 逐拍轨迹:活动内部的流水账 —— 「第三拍为什么被拒」这句话只在这儿 ── */
|
||||
.tb-trail { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; }
|
||||
.tb-trail li { display: grid; grid-template-columns: 62px minmax(0, 1fr); gap: 10px; padding: 5px 0; border-bottom: 1px solid #141f34; }
|
||||
.tb-trail li .t { font: 500 10px/1.6 var(--sh-mono); color: var(--sh-muted); }
|
||||
.tb-trail li .b { min-width: 0; font-size: 12px; line-height: 1.6; color: var(--sh-steel); overflow-wrap: anywhere; }
|
||||
.tb-trail li .b .d { display: block; font: 400 10.5px/1.6 var(--sh-mono); font-style: normal; color: var(--sh-muted); overflow-wrap: anywhere; }
|
||||
/* 三种要紧的行各给一色:干成了(磷光)/ 被拒(珊瑚)/ 引擎迁移(黄铜) */
|
||||
.tb-trail li.k-action .b { color: var(--sh-live); }
|
||||
.tb-trail li.k-result .b { color: var(--sh-ink); }
|
||||
.tb-trail li.k-engine-transition .b { color: var(--sh-brass); }
|
||||
|
||||
/* ── 台账草稿:成果就是这些行 ── */
|
||||
.tb-drafts { display: flex; flex-direction: column; gap: 10px; padding: 8px 0 2px; }
|
||||
.tb-draft-g .addr { display: block; font: 600 10px/1.8 var(--sh-mono); letter-spacing: .08em; color: var(--sh-muted); }
|
||||
/* 主键那一列给够宽:它是「这一行是谁」(询价单号 · PN),88px 会把它折成三行 */
|
||||
.tb-draft-g .row { display: grid; grid-template-columns: minmax(0, clamp(110px, 24%, 220px)) minmax(0, 1fr); gap: 10px; padding: 4px 0; border-bottom: 1px solid #141f34; }
|
||||
.tb-draft-g .row .k { font: 600 11px/1.6 var(--sh-mono); color: var(--sh-live); overflow-wrap: anywhere; }
|
||||
.tb-draft-g .row .p { font: 400 10.5px/1.6 var(--sh-mono); color: var(--sh-steel); overflow-wrap: anywhere; }
|
||||
.tb-none { margin: 8px 0 0; font-size: 11.5px; line-height: 1.75; color: var(--sh-muted); }
|
||||
|
||||
/* ── 卡:问了谁什么 ── */
|
||||
.tb-cards { display: flex; flex-direction: column; padding: 6px 0 2px; }
|
||||
.tb-cards .row { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: baseline; gap: 9px; padding: 6px 0; border-bottom: 1px solid #141f34; }
|
||||
.tb-cards .row .m { font: 600 9.5px/1.6 var(--sh-mono); letter-spacing: .06em; color: var(--sh-brass); }
|
||||
.tb-cards .row .t { min-width: 0; font-size: 12px; color: var(--sh-ink); overflow-wrap: anywhere; }
|
||||
.tb-cards .row .s { font: 500 10px/1.6 var(--sh-mono); color: var(--sh-brass); }
|
||||
.tb-cards .row.done .s { color: var(--sh-muted); }
|
||||
|
||||
/* ── 仪器顶条:一盏灯 + 状态词 —— 装配态与观测态共用同一根 ── */
|
||||
.tb-strip { display: flex; align-items: center; gap: 11px; min-height: 30px; padding: 2px 2px 10px; border-bottom: 1px solid #1b2a44; margin-bottom: 2px; }
|
||||
.tb-lamp { flex: none; width: 9px; height: 9px; border-radius: 50%; background: currentColor; box-shadow: 0 0 9px currentColor; }
|
||||
@@ -1026,7 +1143,10 @@
|
||||
|
||||
/* ── ② 节点指派:一条竖轨挂着所有节点,右端是插座 —— 插了人点琥珀 ── */
|
||||
.tb-asn { display: flex; flex-direction: column; padding: 4px 0 10px; }
|
||||
.tb-asn-r { display: grid; grid-template-columns: 20px 34px minmax(0, 1fr) 40px minmax(150px, 190px); align-items: center; gap: 9px; padding: 6px 0; }
|
||||
.tb-asn-r { display: grid; grid-template-columns: 20px 34px minmax(0, 1fr) 40px minmax(184px, 238px); align-items: center; gap: 9px; padding: 6px 0; }
|
||||
/* 单活动那一台只有一格插座、没有「活动/审批」那一列 —— 少一格就得少一列,
|
||||
不然 select 掉进 40px 那一格里,名字挤成一坨(踩过:「下拉框尺寸不够」) */
|
||||
.tb-asn-r.solo { grid-template-columns: 20px 52px minmax(0, 1fr) minmax(184px, 238px); }
|
||||
.tb-asn-r .rail { position: relative; align-self: stretch; }
|
||||
.tb-asn-r .rail::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; border-left: 1px solid var(--sh-line); }
|
||||
.tb-asn-r:first-child .rail::before { top: 50%; }
|
||||
@@ -1057,6 +1177,63 @@
|
||||
.tb-go:disabled { opacity: .45; cursor: default; }
|
||||
.tb-go:focus-visible { outline: 2px solid var(--sh-live); outline-offset: 2px; }
|
||||
|
||||
/* ── 附件格:选的是**真文件** —— 一件一枚签条,读出来什么就写什么 ── */
|
||||
.tb-files { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px 0; }
|
||||
.tb-file { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 3px 6px 3px 8px; border: 1px solid #ddd7c8; border-radius: 4px; background: #f4f1e8; font: 500 11px/1.5 var(--sans); color: #4a4536; }
|
||||
.tb-file svg { flex: none; color: #6a8f5e; }
|
||||
.tb-file.blind svg { color: #c0885c; }
|
||||
.tb-file b { flex: none; font-weight: 600; }
|
||||
.tb-file i { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 400 10px/1.5 var(--sh-mono); font-style: normal; color: #8b8674; }
|
||||
.tb-file button { display: inline-flex; flex: none; padding: 2px; border: 0; background: transparent; color: #8b8674; cursor: pointer; }
|
||||
.tb-file button:hover { color: #b0413e; }
|
||||
.tb-file-add { display: inline-flex; align-items: center; gap: 5px; padding: 4px 9px; border: 1px dashed #c9c2ae; border-radius: 4px; color: #8b8674; font: 500 11px/1.5 var(--sans); cursor: pointer; }
|
||||
.tb-file-add:hover { border-color: #8b8674; color: #4a4536; }
|
||||
.tb-file-add:focus-within { outline: 2px solid #8b8674; outline-offset: 1px; }
|
||||
.tb-file-add input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
|
||||
/* ── 试跑存档:目录一行一次试跑 —— 读数 mono、题目无衬线,右端一枚删 ── */
|
||||
.tb-logs { display: flex; flex-direction: column; gap: 6px; }
|
||||
/* 第二列是**试跑短号**(2026-07-31 加):同一条流反复试跑,主题一模一样,
|
||||
没有号就只能靠报时分秒指认「哪一次」—— 号在最前面才好一眼扫到并对上话 */
|
||||
.tb-log-r { display: grid; grid-template-columns: 44px 74px minmax(0, 1fr) 82px 168px 138px 92px 24px; align-items: center; gap: 10px; width: 100%; padding: 9px 12px; border: 1px solid var(--sh-line); border-radius: 8px; background: var(--sh-panel); text-align: left; cursor: pointer; transition: border-color var(--t-fast), background var(--t-fast); }
|
||||
.tb-log-r:hover { border-color: var(--sh-lineb); background: var(--sh-panel-2); }
|
||||
.tb-log-r:focus-visible { outline: 2px solid var(--sh-live); outline-offset: 1px; }
|
||||
.tb-log-r .kd { justify-self: start; padding: 2px 6px; border-radius: 4px; border: 1px solid var(--sh-line); font: 600 9px/1.5 var(--sh-mono); letter-spacing: .1em; color: var(--sh-muted); }
|
||||
/* 试跑短号 —— 等宽、可点、点了拷完整 id;列表与详情共用一枚 */
|
||||
.tb-runid { justify-self: start; padding: 2px 6px; border-radius: 4px; border: 1px dashed var(--sh-line); background: transparent; font: 600 10px/1.5 var(--sh-mono); letter-spacing: .04em; color: var(--sh-muted); cursor: pointer; white-space: nowrap; transition: color var(--t-fast), border-color var(--t-fast); }
|
||||
.tb-runid:hover { color: var(--sh-live); border-color: var(--sh-live); border-style: solid; }
|
||||
.tb-runid:focus-visible { outline: 2px solid var(--sh-live); outline-offset: 1px; }
|
||||
/* 拷上了就实心亮一下 —— 界面不撒谎:拷不上不会走到这个态 */
|
||||
.tb-runid.ok { color: var(--sh-live); border-color: var(--sh-live); border-style: solid; }
|
||||
/* 详情页那一枚跟在标题后面,别跟标题挤在一起 */
|
||||
.tb-strip .tb-runid { margin-left: 8px; }
|
||||
.tb-log-r .kd.package { border-color: rgba(98, 169, 224, .34); color: var(--sh-live); }
|
||||
.tb-log-r .kd.activity { border-color: rgba(242, 180, 92, .4); color: var(--sh-brass); }
|
||||
.tb-log-r .sub { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.tb-log-r .sub b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; font-weight: 600; color: var(--sh-ink); }
|
||||
.tb-log-r .sub em { font: 400 9.5px/1 var(--sh-mono); font-style: normal; letter-spacing: .06em; color: var(--sh-muted); }
|
||||
.tb-log-r .st { font: 600 10px/1 var(--sh-mono); letter-spacing: .06em; color: var(--sh-muted); }
|
||||
.tb-log-r .st.ok { color: var(--sh-live); }
|
||||
.tb-log-r .st.bad { color: var(--sh-alarm); }
|
||||
.tb-log-r .st.cut { color: var(--sh-brass); }
|
||||
.tb-log-r .rd { font: 400 10px/1 var(--sh-mono); color: var(--sh-muted); }
|
||||
.tb-log-r .at { font: 400 10px/1 var(--sh-mono); color: var(--sh-steel); }
|
||||
.tb-log-r .del { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 5px; color: var(--sh-muted); cursor: pointer; transition: color var(--t-fast), background var(--t-fast); }
|
||||
.tb-log-r .del:hover { background: rgba(224, 96, 92, .14); color: var(--sh-alarm); }
|
||||
.tb-log-r .del:focus-visible { outline: 2px solid var(--sh-alarm); outline-offset: 1px; }
|
||||
|
||||
/* 存档正文:逐拍轨迹 / 草稿 / 人卡 —— 三张表同一副骨架,一行一条 */
|
||||
.tb-trail { display: flex; flex-direction: column; max-height: 420px; overflow: auto; }
|
||||
.tb-trail-r { display: grid; grid-template-columns: 62px 116px minmax(0, 1fr) auto; align-items: baseline; gap: 10px; padding: 6px 14px; border-bottom: 1px solid var(--sh-line); }
|
||||
.tb-trail-r:last-child { border-bottom: 0; }
|
||||
.tb-trail-r .at { font: 400 10px/1.6 var(--sh-mono); color: var(--sh-muted); }
|
||||
.tb-trail-r .at.cut { color: var(--sh-brass); }
|
||||
.tb-trail-r .kd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 600 9.5px/1.6 var(--sh-mono); letter-spacing: .08em; color: var(--sh-steel); }
|
||||
.tb-trail-r .tt { min-width: 0; font-size: 11.5px; line-height: 1.6; color: var(--sh-ink); word-break: break-word; }
|
||||
.tb-trail-r .tt.mono { font: 400 10.5px/1.7 var(--sh-mono); color: var(--sh-steel); }
|
||||
/* 草稿与人卡那两张只有三格:头一格空着比错位好 */
|
||||
.tb-trail-r > :first-child:not(.at) { grid-column: 1 / span 2; }
|
||||
|
||||
/* 空台面:没有生效流 —— 一个虚线的空槽,指路去流程包 */
|
||||
.tb-slot { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; padding: 44px 16px 46px; border: 1.5px dashed var(--sh-line); border-radius: 10px; text-align: center; }
|
||||
.tb-slot svg { color: var(--sh-muted); opacity: .7; }
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import { queryKeys } from './query.ts';
|
||||
|
||||
type Touch = { kind: 'task' | 'workflow' | 'staff' | 'system'; id: string; reason: string };
|
||||
export type Touch = { kind: 'task' | 'workflow' | 'staff' | 'system'; id: string; reason: string };
|
||||
|
||||
export function connectStream(queryClient: QueryClient): () => void {
|
||||
const source = new EventSource('/api/stream');
|
||||
|
||||
source.addEventListener('touch', (event) => {
|
||||
const touch = JSON.parse((event as MessageEvent<string>).data) as Touch;
|
||||
switch (touch.kind) {
|
||||
/**
|
||||
* 一次推送该失效哪些键 —— **单独导出是为了让它可测**。
|
||||
*
|
||||
* 这段映射曾经整段失效(键前缀写成 `['tasks']`,而工作台的键是 `['desk','tasks']`)——
|
||||
* 推送照发、画面纹丝不动,而且不报错。这类错只能靠断言「推送到了哪把查询会重取」来守,
|
||||
* 所以把它从 EventSource 里剥出来:连接是 I/O,映射是逻辑。
|
||||
*/
|
||||
export function invalidateFor(queryClient: QueryClient, touch: Touch): void {
|
||||
switch (touch.kind) {
|
||||
/*
|
||||
* **键名一律取自 `queryKeys`**,不手写字面量:TanStack 的失效按**前缀**匹配,
|
||||
* 而工作台的键都以 `desk` 打头(`['desk','tasks']`)。从前这里写的是 `['tasks']` ——
|
||||
@@ -31,6 +34,13 @@ export function connectStream(queryClient: QueryClient): () => void {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.approvals });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'tasks'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'tasklog', touch.id] });
|
||||
/*
|
||||
* **测试台也要跟着动。** 它的键自成一支(`gov.testbed.*`),不在上面几条的前缀里 ——
|
||||
* 从前只靠自己 2 秒一轮的轮询,而轮询到终态就停:一单跑完之后,
|
||||
* 那一屏就此定格,连「当前在跑的是哪一单」都不再问(实测:跑完了旋钮还锁着)。
|
||||
* 推送到了就失效,轮询退回成兜底。
|
||||
*/
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'testbed'] });
|
||||
break;
|
||||
case 'workflow':
|
||||
// 流动了,自己名下的任务与审批都可能变 —— 工作台一并失效
|
||||
@@ -39,15 +49,30 @@ export function connectStream(queryClient: QueryClient): () => void {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'workflow', touch.id] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'tasks'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'approvals'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'testbed'] });
|
||||
break;
|
||||
case 'staff':
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.messages });
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'staff'] });
|
||||
break;
|
||||
case 'system':
|
||||
/*
|
||||
* 全局变更(配置改了 / 试跑单被销毁 / 台账落了新事实)——**两面一起失效**。
|
||||
*
|
||||
* 只失效 `gov` 是不够的:销毁试跑单拔掉的是**真任务**,它这一刻还挂在
|
||||
* 负责人的工作台列表上;不连 `desk` 一起失效,那儿会留着一张点进去就 404 的卡片。
|
||||
*/
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['desk'] });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function connectStream(queryClient: QueryClient): () => void {
|
||||
const source = new EventSource('/api/stream');
|
||||
|
||||
source.addEventListener('touch', (event) => {
|
||||
invalidateFor(queryClient, JSON.parse((event as MessageEvent<string>).data) as Touch);
|
||||
});
|
||||
|
||||
// 重连即补课:断线期间的推送收不到,回来先整体失效一次
|
||||
|
||||
Reference in New Issue
Block a user