feat(webui): 阶段管道与中断队列统一为等大表框,字体加大加粗

问题:阶段管道是「小圆点 + 一条连接线 + 9px 小字」,中断队列是五行
「名字 | 进度条 | 元数据」的扁条 —— 两块都远小于旁边的 KPI 框,中断队列四级
全为 0 时四行几乎全是空白,既占高度又难看。

改法:两块统一成同一套视觉语言 —— **等大表框**(与 KPI 同一种骨架)。

阶段管道(.rt-pipe-row / .rt-pipe-cell)
- 5 个等大框,框内 = 图标 + 阶段名 + 本阶段本轮发生的事件 chip。
- 阶段名 9px/500 → 13.5px/700;图标 12px → 15px。
- 当前阶段整框点亮(accent 描边 + 淡底 + 内阴影),不再靠一个小圆点表意。
- 删掉圆点、连接线、滑块把手那套已死的 CSS(.rt-pipe-track/.rt-pipe-knob 等)。

中断队列(.rt-queues / .rt-qcell)
- 五行扁条 → 5 个等大框(L4/L3/L2/L1 + 排队),一行排开。
- 框头级别名 16px/800、深度数字 26px/800(原来深度只是行末一个小数字)。
- 级别色同时用在框头、点亮格槽、有积压时的整框描边 —— 一处配色贯穿。
- 排队队列无级别,用虚线框与四级中断区分(另一**类别**,不是另一优先级)。
- 保留可见格槽:0 时也有形状,不会变回一片空白。

列宽自适应:两块共用 repeat(auto-fit, minmax(118px, 1fr)),
118px 而不是 150px 是为了让 640–740px 容器(窄屏侧栏收起后的宽度)也能
5 个框排一行,不出现「4 个 + 1 个」的孤行。chip 补 min-width:0 以免撑破窄框。

顺带清掉一条无用的旧 .rt-chip 规则(与新规则重名且只被阶段事件用到)。

实测(现网 CDP,1400/1100/900/700/480 五档):
- 1400/1100/700px:5 框一行(200px / 140px / 120px);900/480px:换行且框仍等大
- 五档均无横向溢出
- 字号:阶段名 13.5px/700,级别 16px/800,深度 26px/800
- 注入一轮轨迹:输入=输入|行动=思考|工具=qq_get_message x3 qq*|输出=生成|结束=完成
- 注入 L3=3/排队=2:L3 描边 rgba(255,166,87,.55)、框头与点亮槽同为琥珀色;
  排队绿框;空的 L4 保持默认描边(首次读到的默认色是 0.25s 过渡中途,非 bug)
- chip 未溢出所在框;总览/侧栏/顶栏渲染文本无 emoji
This commit is contained in:
JianFeeeee
2026-09-14 18:38:08 +08:00
parent 37924b295b
commit f89e57a732
2 changed files with 173 additions and 170 deletions

View File

@ -2347,34 +2347,92 @@
background: linear-gradient(90deg, var(--sakura-400), var(--frost-300));
transition: width 0.35s var(--ease-out);
}
.rt-levels {
/* ===== 队列:五个等大表框 =====
此前是五行「名字 | 进度条 | 元数据」的扁条:四级中断全为 0 时,
四行几乎全是空白,既占高度又难看。改成五个等大的框(与 KPI、阶段管道
同一套视觉语言),深度用大字报出来,下面是可见的格槽。 */
.rt-queues {
display: grid;
/* 118px 而不是 150px容器 640740px窄屏侧栏收起后就是这个宽度
5 个框仍能排成一行不会出现「4 个 + 1 个」的孤行。 */
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
gap: 10px;
margin-bottom: 8px;
}
.rt-qcell {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 6px;
gap: 7px;
padding: 11px 12px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
transition:
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-level {
display: grid;
grid-template-columns: 74px 1fr 96px;
.rt-qhead {
display: flex;
align-items: baseline;
gap: 7px;
min-width: 0;
}
.rt-qhead > b {
font-size: 16px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
}
.rt-qhead > span {
font-size: 10.5px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rt-qnum {
font-size: 26px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.rt-qmeta {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-size: 10.5px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.rt-level .rt-lv-name {
color: var(--text-secondary);
white-space: nowrap;
/* 级别配色:框头与格槽点亮共用同一套色 */
.rt-qcell.rt-lv-4 .rt-qhead > b { color: #ff5c7a; }
.rt-qcell.rt-lv-3 .rt-qhead > b { color: #ffa657; }
.rt-qcell.rt-lv-2 .rt-qhead > b { color: var(--sakura-400); }
.rt-qcell.rt-lv-1 .rt-qhead > b { color: var(--frost-300); }
.rt-qcell.rt-lv-q .rt-qhead > b { color: #a3be8c; }
/* 有积压时整个框描边点亮:一眼看出哪条队列在堵 */
.rt-qcell.rt-lv-4.rt-active { border-color: rgba(255, 92, 122, 0.55); box-shadow: 0 0 0 1px rgba(255, 92, 122, 0.18); }
.rt-qcell.rt-lv-3.rt-active { border-color: rgba(255, 166, 87, 0.55); box-shadow: 0 0 0 1px rgba(255, 166, 87, 0.18); }
.rt-qcell.rt-lv-2.rt-active { border-color: rgba(var(--accent-rgb), 0.55); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.18); }
.rt-qcell.rt-lv-1.rt-active { border-color: rgba(136, 192, 208, 0.55); box-shadow: 0 0 0 1px rgba(136, 192, 208, 0.18); }
.rt-qcell.rt-lv-q.rt-active { border-color: rgba(163, 190, 140, 0.55); box-shadow: 0 0 0 1px rgba(163, 190, 140, 0.18); }
/* 排队队列无级别:用虚线框与四级中断区分开 */
.rt-qcell.rt-qcell-queued {
border-style: dashed;
}
/* 队列格槽:固定可见的「车位」,占用多少一眼可数。
此前是宽度百分比进度条 —— 队列为 0 时宽度为 0整行只剩文字
看上去就是「这块空着」。 */
.rt-level .rt-slots {
.rt-slots {
display: flex;
align-items: stretch;
gap: 3px;
height: 14px;
height: 16px;
min-width: 0;
}
.rt-level .rt-slots > i {
.rt-slots > i {
flex: 1 1 0;
min-width: 3px;
border-radius: 3px;
@ -2384,22 +2442,17 @@
background 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-level .rt-slots.rt-lv-4 > i.on { background: #ff5c7a; box-shadow: 0 0 6px rgba(255, 92, 122, 0.45); }
.rt-level .rt-slots.rt-lv-3 > i.on { background: #ffa657; box-shadow: 0 0 6px rgba(255, 166, 87, 0.4); }
.rt-level .rt-slots.rt-lv-2 > i.on { background: var(--sakura-400); box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.4); }
.rt-level .rt-slots.rt-lv-1 > i.on { background: var(--frost-300); box-shadow: 0 0 6px rgba(136, 192, 208, 0.4); }
.rt-level .rt-slots.rt-lv-q > i.on { background: #a3be8c; box-shadow: 0 0 6px rgba(163, 190, 140, 0.4); }
.rt-slots.rt-lv-4 > i.on { background: #ff5c7a; box-shadow: 0 0 6px rgba(255, 92, 122, 0.45); }
.rt-slots.rt-lv-3 > i.on { background: #ffa657; box-shadow: 0 0 6px rgba(255, 166, 87, 0.4); }
.rt-slots.rt-lv-2 > i.on { background: var(--sakura-400); box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.4); }
.rt-slots.rt-lv-1 > i.on { background: var(--frost-300); box-shadow: 0 0 6px rgba(136, 192, 208, 0.4); }
.rt-slots.rt-lv-q > i.on { background: #a3be8c; box-shadow: 0 0 6px rgba(163, 190, 140, 0.4); }
.rt-slots-more {
flex: 0 0 auto;
align-self: center;
margin-left: 4px;
font-size: 10px;
font-weight: 400;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.rt-level .rt-lv-meta {
text-align: right;
font-weight: 600;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
@ -2430,13 +2483,7 @@
.rt-topo-svg .tp-edge { fill: none; }
.rt-topo-svg .tp-edge-in { stroke-opacity: 0.45; stroke-width: 1.4; }
.rt-topo-svg .tp-edge-out { stroke-opacity: 0.35; stroke-width: 1.2; }
/* 第五条:排队队列无级别)。用虚线分隔 + 中性色,与“有级别”的四条区分开——
它不是优先级,而是另一**类别**(排队 vs 中断),同一套配色会误导。 */
.rt-level.rt-level-queued {
border-top: 1px dashed rgba(255, 255, 255, 0.14);
margin-top: 5px;
padding-top: 7px;
}
/* 排队队列无级别:虚线框已在 .rt-qcell.rt-qcell-queued 处理 */
/* 中断栈:栈顶在上,做出"层叠现场"的观感 */
.rt-stack {
display: flex;
@ -2555,14 +2602,6 @@
color: var(--text-muted);
margin-left: auto;
}
.rt-chip {
font-size: 9px;
padding: 1px 6px;
border-radius: 999px;
background: rgba(163, 190, 140, 0.16);
color: var(--text-secondary, var(--text-muted));
white-space: nowrap;
}
.rt-badge-warn {
font-size: 9px;
padding: 1px 6px;
@ -2653,102 +2692,78 @@
box-shadow: var(--shadow-glow);
}
.rt-section-title {
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.4px;
margin: 12px 0 6px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.3px;
margin: 14px 0 8px;
text-transform: uppercase;
}
/* ===== 阶段管道滑块(总览)=====
七个阶段各一格,滑块停在当前阶段;空闲时整条降透明度,不做假动画
滑块用绝对定位 + left 过渡实现"滑过去"(内联 left 由 rtPipelineHtml 给)。 */
.rt-pipe {
position: relative;
padding: 4px 0 2px;
/* ===== 阶段管道:五个等大表框 =====
与中断队列、KPI 同一套视觉语言(等大框 + 大号加粗标题)
此前是「小圆点 + 一条连接线 + 9px 小字」,在总览里几乎读不出
「现在走到哪一步」,尺寸也远小于旁边的 KPI 框。
现在事件直接落在它所属的框里,不需要靠颜色或图例去猜。 */
.rt-pipe-row {
display: grid;
/* 与 .rt-queues 同一套列宽规则:两块的框宽、换行点保持一致 */
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
gap: 10px;
margin: 2px 0 8px;
transition: opacity 0.3s var(--ease-out);
}
.rt-pipe-idle {
opacity: 0.5;
opacity: 0.55;
}
.rt-pipe-track {
position: absolute;
left: 22px;
right: 22px;
top: 9px;
height: 2px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
}
.rt-pipe-nodes {
position: relative;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.rt-pipe-node {
.rt-pipe-cell {
display: flex;
flex-direction: column;
gap: 8px;
padding: 11px 12px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
transition:
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out),
background 0.25s var(--ease-out);
}
.rt-pipe-cell.active {
border-color: var(--accent);
background: rgba(var(--accent-rgb), 0.08);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.22);
}
.rt-pipe-head {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.rt-pipe-node > i {
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--bg-input);
border: 2px solid var(--border-color);
position: relative;
z-index: 1;
transition:
background 0.25s var(--ease-out),
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-pipe-node > b {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
max-width: 100%;
font-size: 9px;
font-weight: 500;
color: var(--text-muted);
.rt-pipe-head > b {
font-size: 13.5px;
font-weight: 700;
letter-spacing: 0.01em;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 图标自带 stroke标签文字不能用下划线<u> 只为分组) */
.rt-pipe-node > b > u {
text-decoration: none;
.rt-pipe-head .rt-ico {
width: 15px;
height: 15px;
color: var(--text-secondary);
}
.rt-pipe-node.done > i {
background: var(--frost-300);
border-color: var(--frost-300);
.rt-pipe-cell.active .rt-pipe-head > b,
.rt-pipe-cell.active .rt-pipe-head .rt-ico {
color: var(--accent);
}
.rt-pipe-node.active > i {
background: var(--sakura-400);
border-color: var(--sakura-400);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.18);
}
.rt-pipe-node.active > b {
color: var(--text-primary);
font-weight: 600;
}
.rt-pipe-knob {
position: absolute;
top: 4px;
width: 11px;
height: 11px;
margin-left: -5.5px;
border-radius: 50%;
background: #fff;
box-shadow: 0 0 10px rgba(var(--accent-rgb), 0.85);
transition: left 0.45s var(--ease-out);
z-index: 2;
}
.rt-pipe-idle .rt-pipe-knob {
box-shadow: none;
opacity: 0.5;
.rt-pipe-events {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 23px;
}
/* 拓扑光点:沿 animateMotion 给的路径跑;不吃鼠标事件,跑完由 JS 摘掉。 */
.rt-spark {
@ -2867,25 +2882,8 @@
color: var(--text-muted);
}
/* 阶段管道下方的「每阶段事件」分列
与上面的节点同为 5 等分栅格,所以每一列正好落在它对应阶段的下面;
列内 chip 用不同配色区分普通工具与 output_* 输出通道。 */
.rt-pipe-cols {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
margin: 4px 0 10px;
padding-top: 6px;
border-top: 1px dashed rgba(255, 255, 255, 0.09);
}
.rt-pipe-col {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 4px;
min-width: 0;
}
/* 行内小图标:一律 SVG不使用 emoji/符号字符。 */
/* 行内小图标:一律 SVG不使用 emoji/符号字符
事件 chip 用它区分普通工具与 output_* 输出通道。 */
.rt-ico {
width: 12px;
height: 12px;
@ -2910,12 +2908,14 @@
display: inline-flex;
align-items: center;
gap: 3px;
/* min-width:0 + overflow:hidden框窄时 chip 跟着缩,而不是把框撞破 */
min-width: 0;
max-width: 170px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 10px;
padding: 2px 7px;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);

View File

@ -836,30 +836,20 @@
var d = depth || 0;
var out = '<span class="rt-slots ' + (extraCls || "") + '">';
for (var i = 0; i < n; i++) out += '<i class="' + (i < d ? "on" : "") + '"></i>';
// 溢出计数必须留在 .rt-slots 内:.rt-level 是 3 列栅格
// 溢出计数必须留在 .rt-slots 内:格槽是 flex 行,多一个兄弟节点会被挤出去
// 多一个兄弟节点会被挤到下一行,整块队列就串了。
if (d > n) out += '<b class="rt-slots-more">+' + (d - n) + "</b>";
return out + "</span>";
}
// rtPipelineHtml 画阶段管道:**五个等大的表框**,每框里是本阶段本轮发生的事。
//
// 为什么不是「小圆点 + 连接线 + 9px 小字」:那种画法在总览里几乎读不出
// 「现在走到哪一步」,尺寸也远小于旁边的 KPI 框。现在与中断队列、KPI 用同
// 一套视觉语言(等大框 + 大号加粗标题),并且事件直接落在它所属的框里,
// 所以「这一步发生了什么」不需要靠颜色或图例去猜。
function rtPipelineHtml(phase, trail) {
var g = rtPhaseGroup(phase);
var nodes = RT_PIPE_GROUPS.map(function (s, i) {
var cls = "rt-pipe-node" + (i === g ? " active" : "");
return (
'<span class="' + cls + '"><i></i><b>' +
RT_ICO[s.ico] +
"<u>" + __(s.zh, s.en) + "</u>" +
(s.loop
? ' <em class="rt-loop" title="' +
__("工具调用会回到行动后,可多次", "tool calls loop back; may repeat") +
'">' + RT_ICO.loop + "</em>"
: "") +
"</b></span>"
);
}).join("");
// 事件按**发生它的阶段**分列,列的横向位置与上面的阶段节点对齐
// (同为 5 等分栅格),所以「这一步发生了什么」不需要靠颜色去猜。
var cols = RT_PIPE_GROUPS.map(function (s, i) {
var cells = RT_PIPE_GROUPS.map(function (s, i) {
var items = (trail || []).filter(function (t) {
return (t.g | 0) === i;
});
@ -879,17 +869,26 @@
})
.join("")
: '<span class="rt-chip rt-chip-none">' + __("无", "none") + "</span>";
return '<div class="rt-pipe-col">' + body + "</div>";
return (
'<div class="rt-pipe-cell' + (i === g ? " active" : "") + '">' +
'<div class="rt-pipe-head">' + RT_ICO[s.ico] +
"<b>" + __(s.zh, s.en) + "</b>" +
(s.loop
? '<em class="rt-loop" title="' +
__("工具调用会回到行动后,可多次", "tool calls loop back; may repeat") +
'">' + RT_ICO.loop + "</em>"
: "") +
"</div>" +
'<div class="rt-pipe-events">' + body + "</div>" +
"</div>"
);
}).join("");
return (
'<div class="rt-section-title">' +
__("阶段管道", "Stage pipeline") +
(g < 0 ? " " + __("(空闲)", "(idle)") : "") +
"</div>" +
'<div class="rt-pipe' + (g < 0 ? " rt-pipe-idle" : "") + '">' +
'<div class="rt-pipe-nodes">' + nodes + "</div>" +
"</div>" +
'<div class="rt-pipe-cols">' + cols + "</div>"
'<div class="rt-pipe-row' + (g < 0 ? " rt-pipe-idle" : "") + '">' + cells + "</div>"
);
}
@ -1225,27 +1224,31 @@
});
// 槽位数按全场最大深度缩放(且至少 5 格0 时也有可见形状,不空着。
var qSlots = Math.max(5, Math.min(16, maxQ));
h += '<div class="rt-levels">';
// 五个**等大表框**(四级中断 + 一条排队),与阶段管道同一套视觉语言。
// 此前是五行扁条,四级中断全为 0 时四行几乎全是空白,又占高度又难看。
h += '<div class="rt-queues">';
RT_LEVELS.forEach(function (L) {
var depth = q[L.lv] || 0;
var reg = byLv[L.lv] || 0;
var pre = preLv[L.lv] || 0;
h +=
'<div class="rt-level" title="' + escHtml(L.desc) + '"><span class="rt-lv-name">' + L.name + "</span>" +
'<div class="rt-qcell ' + L.cls + (depth ? " rt-active" : "") + '" title="' + escHtml(L.desc) + '">' +
'<div class="rt-qhead"><b>' + L.name + "</b><span>" + escHtml(L.desc) + "</span></div>" +
'<div class="rt-qnum">' + depth + "</div>" +
rtSlots(depth, qSlots, L.cls) +
'<span class="rt-lv-meta">' + depth + " · " +
'<div class="rt-qmeta">' +
rtMini(reg, maxReg, __("登记", "registered")) +
rtMini(pre, maxPre, __("抢占", "preempted")) +
"</span></div>";
"</div></div>";
});
// 第五条:排队队列(无级别,纯 FIFO
// 注意:它必须在 .rt-levels **之内**:早前它被写在容器闭合之后,下面又多一个
// </div>,多出来的闭合标签会把祖先节点提前关掉,整块布局被撞歪。
// 它不是优先级而是另一**类别**(排队 vs 中断),所以用虚线框区分。
h +=
'<div class="rt-level rt-level-queued" title="' + __("排队(无级别,纯 FIFO", "queued (no priority, FIFO)") + '"><span class="rt-lv-name">' +
__("排队", "queued") + "</span>" +
'<div class="rt-qcell rt-qcell-queued rt-lv-q' + (ready ? " rt-active" : "") + '" title="' + __("排队(无级别,纯 FIFO", "queued (no priority, FIFO)") + '">' +
'<div class="rt-qhead"><b>' + __("排队", "queued") + "</b><span>FIFO</span></div>" +
'<div class="rt-qnum">' + ready + "</div>" +
rtSlots(ready, qSlots, "rt-lv-q") +
'<span class="rt-lv-meta">' + ready + " · FIFO</span></div>";
'<div class="rt-qmeta">' + __("无级别", "no priority") + "</div></div>";
h += "</div>";
if (sc.immediate) {
h += '<div class="rt-frame">' + RT_ICO.bolt + " " + __("立即运行", "immediate") + "" +