dsh-workplan 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.en.md +19 -1
- package/README.md +19 -1
- package/client.js +345 -15
- package/lib/host.js +1 -1
- package/package.json +3 -2
- package/src/client.ts +429 -22
- package/src/host.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
+
## 1.5.1 — 2026-10-28
|
|
2
|
+
|
|
3
|
+
**「先看这三句」与「分步进度」位置对换的补丁版**。用户以截图圈出展开态计划卡里的这两个板块,
|
|
4
|
+
要求把它们的布局位置互换。本次只调整渲染顺序,**数据、接口、样式与既有测试断言零改动**。
|
|
5
|
+
|
|
6
|
+
### 改动
|
|
7
|
+
|
|
8
|
+
- **展开态面板顺序对换**(`src/client.ts`):原来的「先看这三句(`Guide`)→ 降级横幅 →
|
|
9
|
+
分步进度(`PlanOverview`)」改为「分步进度(`PlanOverview`)→ 降级横幅 → 先看这三句(`Guide`)」。
|
|
10
|
+
两个面板组件本身及其内部逻辑不变;`PlanOverview` 仍然只在计划卡展开时(`open === true`)挂载,
|
|
11
|
+
折叠态依旧不挂载。
|
|
12
|
+
- **版本**:`1.5.0 → 1.5.1`(patch,仅布局顺序对换,不新增能力、不改对外载荷);`package.json`、
|
|
13
|
+
`package-lock.json` 两处、`src/host.ts` 的 `meta.version` 同步;`lib/host.js`、`dist/host.js`、
|
|
14
|
+
`client.js` 由 `npm run build` 重新生成。
|
|
15
|
+
|
|
16
|
+
### 验证
|
|
17
|
+
|
|
18
|
+
- `npm run build` → exit 0;`npm run check` → exit 0。
|
|
19
|
+
- `npm test` → **99/99 通过**(0 fail)。
|
|
20
|
+
- 7 处版本一致性检查 → `[PASS] 7 处版本均为 1.5.1`(`package.json`、`package-lock.json` 两处、
|
|
21
|
+
`src/host.ts`、`lib/host.js`、`dist/host.js`、`CHANGELOG.md` 顶部)。
|
|
22
|
+
- 本次**未打 tag、未发布、未重启服务**;需要在运行实例里看到换序,须由主 Agent 重启 DSH
|
|
23
|
+
(或重新加载 web profile)后再刷新页面。
|
|
24
|
+
|
|
25
|
+
### 回滚
|
|
26
|
+
|
|
27
|
+
- 升级前四个文件的副本:`.backups/pre-1.5.1-20261028-221343/`
|
|
28
|
+
(`package.json`、`package-lock.json`、`CHANGELOG.md`、`src/host.ts`)。
|
|
29
|
+
- 回滚后必须重新 `npm run build`;运行中的进程不会自动重载 `lib/host.js`。
|
|
30
|
+
|
|
31
|
+
## 1.5.0 — 2026-10-28
|
|
32
|
+
|
|
33
|
+
**「进度必须一眼看得见」的图表化版本**。1.4.3 把数字收敛成单一来源,但计划卡折叠后仍然只有一行
|
|
34
|
+
12px 灰字(`本计划 0/8 做好了` + 54×5px 迷你条 + `0 卡住了`),0% 时迷你条宽度为 0、轨道色与
|
|
35
|
+
折叠按钮底色同 token,等于**没有图**;百分比只存在于 `title` 悬停提示里;组头在 0/0 时印
|
|
36
|
+
`未完成 0 / 已完成 0(完成率暂无数据)`。用户以截图指出三处画圈区域「不直观、不一目了然、不醒目」,
|
|
37
|
+
要求「必须通过图表等形式突出显示」。
|
|
38
|
+
|
|
39
|
+
### 改动(只动展示层,`plan.ts` / `runtime.ts` / `host.ts` 逻辑零改动)
|
|
40
|
+
|
|
41
|
+
- **折叠卡摘要改为图表**(`src/client.ts`,默认视图即可读,无需展开、无需悬停):
|
|
42
|
+
44px SVG 环 + 24px 百分比数字 + `本计划 X/Y 做好了` + 一行「现在在做什么」(卡住优先→正在做→
|
|
43
|
+
全部做完/还没开始,带任务名)+ 8px 四段构成条(按件数分宽,`done/active/blocked/pending`)+
|
|
44
|
+
11px 状态图例(`做好了 0 · 正在做 1 · 卡住了 0 · 还没开始 7`)+ 6px 阶段步进条
|
|
45
|
+
(每阶段一段,当前阶段描边,`data-stage-id` / `data-tone` / `data-current`)。
|
|
46
|
+
- **展开后新增「分步进度」**:逐阶段行(名称、`done/total`、4px 细条、状态)+加权总体进度,
|
|
47
|
+
替换旧的一根计划级进度条;折叠态不挂载,保持卡头紧凑。
|
|
48
|
+
- **0% 不再等于「没有图」**:四层信号同时存在——环的零轨加粗到 `rgba(100,116,139,.80)`(≈3.14:1,
|
|
49
|
+
`data-zero="true"`)、24px「0%」、整条灰色「还没开始」构成条、虚线阶段槽 + 当前阶段描边;
|
|
50
|
+
0% 不画 SVG 弧,避免圆头端点在 12 点方向留下与「已完成」混淆的小点。
|
|
51
|
+
- **组头收口**:0% 组环零轨同样加粗(`data-zero="true"`);0/0(没有工作项)时**删除**
|
|
52
|
+
`未完成 0 / 已完成 0(完成率暂无数据)` 整句,只保留环上的 `—` 与组内空态;100% 组
|
|
53
|
+
(`未完成 0 / 已完成 3(完成率 100%)`)整句保留,因为那正是用户要的结论。
|
|
54
|
+
- **零新增依赖**:进度全部由既有 `session.stats` / `session.stages` / `session.statuses` 派生,
|
|
55
|
+
纯函数组件,无 `useEffect`、无定时器、不引用 chart.js;`package.json` 无 `dependencies`。
|
|
56
|
+
注意 `node_modules` 里仍能看到 `chart.js@4.6.1`、`@kurkle/color@0.3.4` 标为 extraneous:
|
|
57
|
+
它们是 1.4.3 之后那次「移除 chart.js 依赖」改动未执行 `npm prune` 的残留,不出现在
|
|
58
|
+
`package.json`、不参与 `npm run build`,运行时图表库来自 `vendor/chart.umd.js` 路由。
|
|
59
|
+
- **版本**:`1.4.3 → 1.5.0`(minor,新增用户可见能力);`package.json` / `package-lock.json` 两处 /
|
|
60
|
+
`src/host.ts` 的 `meta.version` 同步,`lib/host.js`、`dist/host.js` 由 `npm run build` 生成。
|
|
61
|
+
|
|
62
|
+
### 验证
|
|
63
|
+
|
|
64
|
+
- `npm run check` → exit 0(strict + noImplicitAny)。
|
|
65
|
+
- `npm test` → **99/99 通过**(1.4.3 基线 90 项 + 新增 `tests/progress-visual.test.ts` 8 项 +
|
|
66
|
+
`tests/version-sync.test.ts` 1 项)。
|
|
67
|
+
新增用例锁定:0/8 的四个可见信号、3/8→38% 且构成条件数之和等于 total、卡住项
|
|
68
|
+
`data-alarm="true"` 且阶段 `data-tone="blocked"`、三个阶段恰好一个 `data-current="true"`、
|
|
69
|
+
图例恰好四项且顺序固定、折叠态不挂载分步面板、脏数据不产生 NaN/Infinity/undefined 且不超过 100%、
|
|
70
|
+
卡头不引入 canvas 或图表库。`version-sync` 锁定 `package.json` = `package-lock.json` 两处 =
|
|
71
|
+
`src/host.ts` 的 `meta.version`,升级时不需要改测试。
|
|
72
|
+
- 同步修改的既有断言:`tests/group-summary.test.ts`(两处 `完成率暂无数据` 改为「不再出现」)、
|
|
73
|
+
`tests/session-collapse.test.ts`(`0 卡住了` → 图例 `卡住了 0` + 可见 `100%`)。
|
|
74
|
+
|
|
75
|
+
### 回滚
|
|
76
|
+
|
|
77
|
+
- 改造前整树快照:`.backups/v1.4.3-pre-progress-viz-20261028-133958.tar.gz`
|
|
78
|
+
(SHA-256 `b8ccc0efa7c307160739ffecdb4af44daab44b3a0f0c779c74d9d0783f6993c7`)。
|
|
79
|
+
- 代码完成、版本号未升时的中间快照:`.backups/v1.4.3-progress-viz-code-20261028-142153.tar.gz`
|
|
80
|
+
(SHA-256 `c79513f5c0279e3bb291d536fba7a631fa8018e27bc7d4fca500fbabd9e602da`)。
|
|
81
|
+
- 恢复后必须 `npm run build` 并重启 DSH web profile(宿主启动时加载 `lib/host.js`,刷新浏览器不够)。
|
|
82
|
+
|
|
1
83
|
## 1.4.3 — 2026-10-28
|
|
2
84
|
|
|
3
85
|
**「同一个数字只能有一个来源」的根治版**。1.4.2 及更早版本里,同一个计划的数字存在两套真相源:
|
package/README.en.md
CHANGED
|
@@ -102,6 +102,23 @@ grep -RInE "writeFileSync|child_process|eval\(" src
|
|
|
102
102
|
npm audit --omit=dev
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
## What 1.5.0 changes (progress you can see)
|
|
106
|
+
|
|
107
|
+
1.5.0 is presentation-only: the collapsed plan card swaps its single grey line ("本计划 0/8 做好了" plus a 54×5px bar) for an actual chart.
|
|
108
|
+
|
|
109
|
+
| Where | Before | After |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| collapsed plan card | 12px grey text + mini bar + "0 卡住了"; the percentage existed only in the hover `title` | 44px ring + 24px percentage + `本计划 X/Y 做好了` + one "what is happening now" line (blocked first, with the task name) + 8px four-segment composition bar + status legend + stage stepper |
|
|
112
|
+
| expanded panel | one plan-level progress bar | "Step progress": one row per stage (name, `done/total`, thin bar, state) plus a weighted overall figure |
|
|
113
|
+
| a 0% plan | the bar fill was 0px wide and its track used the same token as the card background — effectively no chart | four signals: a thickened ring track, a 24px "0%", a full grey "not started" bar, and dashed stage slots with the current stage outlined |
|
|
114
|
+
| a group with 0 work items | `未完成 0 / 已完成 0(完成率暂无数据)` | that sentence is gone; only the `—` in the ring remains (the in-group empty state still renders when expanded) |
|
|
115
|
+
|
|
116
|
+
None of the counts disappeared — the legend always carries all four buckets with their numbers.
|
|
117
|
+
|
|
118
|
+
**How to see it**: the front end ships in `client.js`, which DSH Web loads at start-up, so restart DSH Web and then reload <http://127.0.0.1:3080>. For an offline comparison open [docs/preview/progress-visual-1.5.0.html](docs/preview/progress-visual-1.5.0.html) (`npm run preview` regenerates it with the same CSS for both versions).
|
|
119
|
+
|
|
120
|
+
No new dependencies, `plan.ts` / `runtime.ts` untouched; rollback is documented in [CHANGELOG.md](CHANGELOG.md).
|
|
121
|
+
|
|
105
122
|
## What 1.4.3 fixes
|
|
106
123
|
|
|
107
124
|
1.4.3 is a counting-consistency release. The plugin used to keep two sources of truth — live subtask state and a cached stats snapshot — so the panel could disagree with reality.
|
|
@@ -136,8 +153,9 @@ dsh plugin --profile web remove dsh-workplan
|
|
|
136
153
|
```bash
|
|
137
154
|
npm ci
|
|
138
155
|
npm run check # tsc --noEmit
|
|
139
|
-
npm test #
|
|
156
|
+
npm test # 99 tests, including count invariants, mutation tests and the progress chart
|
|
140
157
|
npm run build # src/*.ts → lib/*.js, client.js
|
|
158
|
+
npm run preview # regenerate the 1.4.3 / 1.5.0 card comparison under docs/preview
|
|
141
159
|
```
|
|
142
160
|
|
|
143
161
|
`lib/` and `client.js` are committed build output; CI rebuilds them and fails if they drift.
|
package/README.md
CHANGED
|
@@ -109,6 +109,23 @@ grep -RInE "writeFileSync|child_process|eval\(" src # 应无命中
|
|
|
109
109
|
npm audit --omit=dev
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
## 1.5.0 改了什么(进度一眼可见)
|
|
113
|
+
|
|
114
|
+
1.5.0 只改展示层:把计划卡折叠态那行「本计划 0/8 做好了 + 一根 54×5px 的小条」换成图表组合。
|
|
115
|
+
|
|
116
|
+
| 位置 | 改前 | 改后 |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| 折叠中的计划卡 | 12px 灰字 + 迷你条 + 「0 卡住了」;百分比只在鼠标悬停的 `title` 里 | 44px 环形图 + 24px 百分比 + `本计划 X/Y 做好了` + 一行「现在在做什么」(卡住优先,带任务名)+ 8px 四段构成条 + 状态图例 + 阶段步进条 |
|
|
119
|
+
| 展开后的面板 | 一根计划级进度条 | 「分步进度」:每个阶段一行(名称、`完成/总数`、细条、状态),外加加权总体进度 |
|
|
120
|
+
| 0% 的计划 | 迷你条填充宽度为 0,轨道又与卡片底色用同一个 token,等于没有图 | 四层信号:加粗的环轨、24px「0%」、整条灰色的「还没开始」构成条、虚线阶段槽 + 当前阶段描边 |
|
|
121
|
+
| 0 个工作项的组头 | `未完成 0 / 已完成 0(完成率暂无数据)` | 整句删除,只留环上的 `—`;组内空态仍在(展开可见) |
|
|
122
|
+
|
|
123
|
+
「卡住了」这类数字一个都没少,只是换了呈现:图例始终是 `做好了 / 正在做 / 卡住了 / 还没开始` 四项带数字。
|
|
124
|
+
|
|
125
|
+
**怎么看到效果**:插件的前端在 `client.js` 里,DSH Web 进程启动时加载它,所以先重启 DSH Web,再刷新 <http://127.0.0.1:3080>。不想改环境也能离线看对比:打开 [docs/preview/progress-visual-1.5.0.html](docs/preview/progress-visual-1.5.0.html)(`npm run preview` 可重新生成,它用同一份 CSS 渲染 1.4.3 与 1.5.0 的卡头)。
|
|
126
|
+
|
|
127
|
+
没有新增依赖,`plan.ts` / `runtime.ts` 无改动;回滚方式见 [CHANGELOG.md](CHANGELOG.md)。
|
|
128
|
+
|
|
112
129
|
## 1.4.3 修了什么(为什么值得升级)
|
|
113
130
|
|
|
114
131
|
1.4.3 是一次**计数一致性**修复:插件曾同时维护「实时任务状态」和「缓存的统计快照」两份真相,导致面板数字和实际状态对不上(截图里「0 个正在做」但下面有任务正在做,就是它)。
|
|
@@ -147,8 +164,9 @@ dsh plugin --profile web remove dsh-workplan
|
|
|
147
164
|
```bash
|
|
148
165
|
npm ci
|
|
149
166
|
npm run check # tsc --noEmit
|
|
150
|
-
npm test #
|
|
167
|
+
npm test # 99 个测试,含计数不变量、变异测试与进度图表用例
|
|
151
168
|
npm run build # 编译 src/*.ts → lib/*.js、client.js
|
|
169
|
+
npm run preview # 重新生成 docs/preview 里的 1.4.3 / 1.5.0 卡头对比页
|
|
152
170
|
```
|
|
153
171
|
|
|
154
172
|
`lib/` 与 `client.js` 是**提交进仓库的构建产物**,CI 会重建并校验二者无差异。
|
package/client.js
CHANGED
|
@@ -172,6 +172,8 @@ window.__ModuleLoader__.load({
|
|
|
172
172
|
"jumpToTask": "跳转到任务明细",
|
|
173
173
|
"scopeAll": "当前各会话最新计划汇总",
|
|
174
174
|
"scopeThis": "本计划",
|
|
175
|
+
"vizStageTitle": "分步进度",
|
|
176
|
+
"vizWeighted": "总体进度(加权)",
|
|
175
177
|
"allPlansHint": "上面是当前各会话最新计划的合计;历史计划不在此汇总,下面每张卡片只统计本计划。",
|
|
176
178
|
"openHint": "打开左侧「任务进度」看完整安排。"
|
|
177
179
|
},
|
|
@@ -332,6 +334,8 @@ window.__ModuleLoader__.load({
|
|
|
332
334
|
"jumpToTask": "Jump to task details",
|
|
333
335
|
"scopeAll": "Latest plan per live session",
|
|
334
336
|
"scopeThis": "This plan",
|
|
337
|
+
"vizStageTitle": "Step progress",
|
|
338
|
+
"vizWeighted": "Overall progress (weighted)",
|
|
335
339
|
"allPlansHint": "These totals cover the latest plan in each live session; older plans are not included. Each card below counts only its own plan.",
|
|
336
340
|
"openHint": "Open “Task progress” on the left for the full plan."
|
|
337
341
|
}
|
|
@@ -364,6 +368,9 @@ window.__ModuleLoader__.load({
|
|
|
364
368
|
底槽用中性灰而不是品牌色,避免「未完成部分」被误读成状态色。 */
|
|
365
369
|
--dsh-wp-done-ink: var(--dsh-wp-done-text);
|
|
366
370
|
--dsh-wp-ring-track: rgba(148,163,184,.30);
|
|
371
|
+
/* 1.5.0 — 进度区图表化:构成条/阶段槽的中性轨道,以及 0% 零轨(≥3:1 图形对比)。 */
|
|
372
|
+
--dsh-wp-track: rgba(148,163,184,.22);
|
|
373
|
+
--dsh-wp-track-strong: rgba(100,116,139,.80);
|
|
367
374
|
min-width: 0;
|
|
368
375
|
width: 100%;
|
|
369
376
|
height: 100%;
|
|
@@ -405,6 +412,10 @@ window.__ModuleLoader__.load({
|
|
|
405
412
|
.dsh-wp-group-ring { position: relative; width: 44px; height: 44px; flex: none; margin-left: auto; display: inline-grid; place-items: center; }
|
|
406
413
|
.dsh-wp-group-ring svg { position: absolute; inset: 0; display: block; }
|
|
407
414
|
.dsh-wp-group-ring-track { stroke: var(--dsh-wp-ring-track, rgba(148,163,184,.30)); }
|
|
415
|
+
/* 1.5.0 — 0% 不等于「没有图」:零轨加粗到 ≥3:1(rgba(100,116,139,.80)),
|
|
416
|
+
环仍然看得见;「暂无数据」保持细轨 + 「—」,两种状态在视觉上不是一回事。 */
|
|
417
|
+
.dsh-wp-group-ring[data-zero="true"] .dsh-wp-group-ring-track { stroke: var(--dsh-wp-track-strong); }
|
|
418
|
+
.dsh-wp-group-ring[data-zero="true"] .dsh-wp-group-ring-label { font-size: 12px; font-weight: 800; }
|
|
408
419
|
.dsh-wp-group-ring-arc { stroke: var(--dsh-wp-done-ink, #0284c7); transition: stroke-dasharray .28s ease; }
|
|
409
420
|
.dsh-wp-group-ring-label { position: relative; font-size: 11px; font-weight: 700; line-height: 1; letter-spacing: -.2px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, #0f172a); }
|
|
410
421
|
@media (prefers-reduced-motion: reduce) { .dsh-wp-group-ring-arc { transition: none; } }
|
|
@@ -629,6 +640,84 @@ window.__ModuleLoader__.load({
|
|
|
629
640
|
.dsh-wp-mini-fill--blocked { background: var(--dsw-alias-state-error-primary); }
|
|
630
641
|
.dsh-wp-mini-fill--warn { background: var(--dsw-alias-state-warn-primary); }
|
|
631
642
|
.dsh-wp-mini-fill--idle { background: var(--dsw-alias-state-idle-primary); }
|
|
643
|
+
/* ── 1.5.0 计划卡进度图表(用户诉求:画圈处必须一眼可读)─────────────────
|
|
644
|
+
四层信号同时存在,任何一层失效都还能读出进度,且都不依赖颜色:
|
|
645
|
+
① 44px 环:图形比例;0% 时轨道加粗仍可见,不画弧(避免圆头端点留下小点)。
|
|
646
|
+
② 24px 百分比:主文本色,浅色底 17:1;「—」表示分母为 0,绝不写 NaN。
|
|
647
|
+
③ 整行四段构成条:按件数分宽;0% 时是一条完整的灰色「还没开始」,
|
|
648
|
+
这也是「整条灰=都还没开始」的图形解释。
|
|
649
|
+
④ 11px 状态图例 + 一行「现在在做什么」+ 6px 阶段步进条。
|
|
650
|
+
数据只来自既有 session.stats / stages / statuses:无新接口、无依赖、无定时器。 */
|
|
651
|
+
.dsh-wp-session-collapse .dsh-wp-collapse-meta { flex: 1 1 100%; display: block; margin-top: 6px; }
|
|
652
|
+
.dsh-wp-planviz {
|
|
653
|
+
display: grid;
|
|
654
|
+
grid-template-columns: 44px minmax(0, 1fr);
|
|
655
|
+
grid-template-areas: "ring head" "bar bar" "legend legend" "step step";
|
|
656
|
+
column-gap: 10px;
|
|
657
|
+
row-gap: 6px;
|
|
658
|
+
width: 100%;
|
|
659
|
+
min-width: 0;
|
|
660
|
+
}
|
|
661
|
+
.dsh-wp-planviz-ring { grid-area: ring; position: relative; width: 44px; height: 44px; display: inline-grid; place-items: center; }
|
|
662
|
+
.dsh-wp-planviz-ring svg { position: absolute; inset: 0; display: block; }
|
|
663
|
+
.dsh-wp-planring-track { stroke: var(--dsh-wp-track); }
|
|
664
|
+
.dsh-wp-planring-arc { stroke: var(--dsh-wp-done-ink); transition: stroke-dasharray .28s ease; }
|
|
665
|
+
.dsh-wp-planviz-ring[data-zero="true"] .dsh-wp-planring-track { stroke: var(--dsh-wp-track-strong); }
|
|
666
|
+
.dsh-wp-planviz-head { grid-area: head; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
|
|
667
|
+
.dsh-wp-planviz-pct { font-size: 24px; font-weight: 700; line-height: 1.15; letter-spacing: -.4px; color: var(--dsw-alias-label-primary, #0f172a); font-variant-numeric: tabular-nums; }
|
|
668
|
+
.dsh-wp-planviz-scope { min-width: 0; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
669
|
+
.dsh-wp-planviz-alarm { font-size: 11px; font-weight: 650; padding: 1px 7px; border-radius: 999px; color: var(--dsw-alias-state-error-primary, #ef4444); color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 80%, var(--dsw-alias-label-primary, #0f172a) 20%); background: rgba(239,68,68,.12); }
|
|
670
|
+
.dsh-wp-planviz-now { flex: 1 1 100%; display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); }
|
|
671
|
+
.dsh-wp-planviz-now-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
672
|
+
.dsh-wp-planviz[data-state="active"] .dsh-wp-planviz-now-dot { background: var(--dsh-wp-active); }
|
|
673
|
+
.dsh-wp-planviz[data-state="done"] .dsh-wp-planviz-now-dot { background: var(--dsh-wp-done); }
|
|
674
|
+
.dsh-wp-planviz[data-state="blocked"] .dsh-wp-planviz-now-dot { background: var(--dsw-alias-state-error-primary); }
|
|
675
|
+
.dsh-wp-planviz-now-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
676
|
+
.dsh-wp-planviz-bar { grid-area: bar; display: flex; align-items: stretch; gap: 2px; height: 8px; border-radius: 999px; background: var(--dsh-wp-track); overflow: hidden; }
|
|
677
|
+
.dsh-wp-planviz-seg { flex: 1 1 0; min-width: 4px; height: 100%; border-radius: 999px; }
|
|
678
|
+
.dsh-wp-planviz-seg[data-kind="done"] { background: var(--dsh-wp-done); }
|
|
679
|
+
.dsh-wp-planviz-seg[data-kind="active"] { background: var(--dsh-wp-active); }
|
|
680
|
+
.dsh-wp-planviz-seg[data-kind="blocked"] { background: var(--dsw-alias-state-error-primary); }
|
|
681
|
+
.dsh-wp-planviz-seg[data-kind="pending"] { background: var(--dsw-alias-state-idle-primary); }
|
|
682
|
+
.dsh-wp-planviz-legend { grid-area: legend; display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
683
|
+
.dsh-wp-planviz-legend-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
684
|
+
.dsh-wp-planviz-legend-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
685
|
+
.dsh-wp-planviz-legend-item[data-kind="done"] .dsh-wp-planviz-legend-dot { background: var(--dsh-wp-done); }
|
|
686
|
+
.dsh-wp-planviz-legend-item[data-kind="active"] .dsh-wp-planviz-legend-dot { background: var(--dsh-wp-active); }
|
|
687
|
+
.dsh-wp-planviz-legend-item[data-kind="blocked"] .dsh-wp-planviz-legend-dot { background: var(--dsw-alias-state-error-primary); }
|
|
688
|
+
.dsh-wp-stepper { grid-area: step; display: flex; gap: 3px; min-width: 0; }
|
|
689
|
+
.dsh-wp-stepper-seg { position: relative; flex: 1 1 0; min-width: 0; height: 6px; border-radius: 999px; background: var(--dsh-wp-track); overflow: hidden; }
|
|
690
|
+
.dsh-wp-stepper-seg[data-tone="pending"] { background: transparent; border: 1px dashed var(--dsh-wp-track-strong); }
|
|
691
|
+
.dsh-wp-stepper-seg[data-tone="blocked"] { box-shadow: inset 0 0 0 1px var(--dsw-alias-state-error-primary); }
|
|
692
|
+
.dsh-wp-stepper-seg[data-current="true"] { outline: 1.5px solid var(--dsh-wp-active-text); outline-offset: 1px; }
|
|
693
|
+
.dsh-wp-stepper-fill { display: block; height: 100%; border-radius: 999px; background: var(--dsh-wp-active); }
|
|
694
|
+
.dsh-wp-stepper-seg[data-tone="done"] .dsh-wp-stepper-fill { background: var(--dsh-wp-done); }
|
|
695
|
+
.dsh-wp-stepper-seg[data-tone="blocked"] .dsh-wp-stepper-fill { background: var(--dsw-alias-state-error-primary); }
|
|
696
|
+
@media (prefers-reduced-motion: reduce) { .dsh-wp-planring-arc { transition: none; } }
|
|
697
|
+
/* 短屏(纵向更紧):先撤图例与阶段条,保住环 + 百分比 + 构成条 + 当前步骤。 */
|
|
698
|
+
@media (max-height: 720px) and (max-width: 600px) {
|
|
699
|
+
.dsh-wp-planviz-legend, .dsh-wp-stepper { display: none; }
|
|
700
|
+
}
|
|
701
|
+
/* 展开态「分步进度」:替换旧的计划级进度条,保留加权进度并给出逐阶段分解。 */
|
|
702
|
+
.dsh-wp-planoverview { margin: 12px 0 4px; padding: 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-2); display: grid; gap: 8px; }
|
|
703
|
+
.dsh-wp-planoverview-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); }
|
|
704
|
+
.dsh-wp-planoverview-title { font-size: 12px; font-weight: 650; letter-spacing: .2px; }
|
|
705
|
+
.dsh-wp-planoverview-stats { display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
706
|
+
.dsh-wp-planrows { display: grid; }
|
|
707
|
+
.dsh-wp-planrow { display: grid; grid-template-columns: 7px minmax(0, 1fr) 48px auto auto; align-items: center; gap: 8px; min-height: 28px; padding: 4px 0; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
708
|
+
.dsh-wp-planrow:first-child { border-top: 0; }
|
|
709
|
+
.dsh-wp-planrow[data-current="true"] { background: rgba(249,115,22,.06); border-radius: 8px; }
|
|
710
|
+
.dsh-wp-planrow-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
711
|
+
.dsh-wp-planrow[data-tone="done"] .dsh-wp-planrow-dot { background: var(--dsh-wp-done); }
|
|
712
|
+
.dsh-wp-planrow[data-tone="active"] .dsh-wp-planrow-dot { background: var(--dsh-wp-active); }
|
|
713
|
+
.dsh-wp-planrow[data-tone="blocked"] .dsh-wp-planrow-dot { background: var(--dsw-alias-state-error-primary); }
|
|
714
|
+
.dsh-wp-planrow-name { min-width: 0; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #0f172a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
715
|
+
.dsh-wp-planrow-bar { display: block; height: 4px; border-radius: 999px; background: var(--dsh-wp-track); overflow: hidden; }
|
|
716
|
+
.dsh-wp-planrow-fill { display: block; height: 100%; border-radius: 999px; background: var(--dsh-wp-active); }
|
|
717
|
+
.dsh-wp-planrow[data-tone="done"] .dsh-wp-planrow-fill { background: var(--dsh-wp-done); }
|
|
718
|
+
.dsh-wp-planrow[data-tone="blocked"] .dsh-wp-planrow-fill { background: var(--dsw-alias-state-error-primary); }
|
|
719
|
+
.dsh-wp-planrow-count { font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
720
|
+
.dsh-wp-planrow-state { font-size: 11px; color: var(--dsw-alias-label-secondary, #475569); white-space: nowrap; }
|
|
632
721
|
.dsh-wp-sessions { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); gap: 16px; align-items: start; }
|
|
633
722
|
.dsh-wp-session {
|
|
634
723
|
min-width: 0;
|
|
@@ -1188,18 +1277,253 @@ window.__ModuleLoader__.load({
|
|
|
1188
1277
|
const primary = contradictions.primary || {};
|
|
1189
1278
|
return h('details', { className: 'dsh-wp-details' }, h('summary', null, t('analysis')), h('div', { className: 'dsh-wp-muted', style: { marginTop: '6px' } }, t('whyHint')), h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('taskUnderstanding')), h('div', { className: 'dsh-wp-grid' }, h(Field, { label: t('deliverable') }, task.deliverable || '—'), h(Field, { label: t('successCriteria') }, h(List, { items: task.successCriteria, empty: '—' })), h(Field, { label: t('constraints') }, h(List, { items: task.constraints, empty: '—' })), h(Field, { label: t('knownFacts') }, h(List, { items: task.knownFacts, empty: '—' })), h(Field, { label: t('unknowns') }, h(List, { items: task.unknowns, empty: '—' })), h(Field, { label: t('assumptions') }, h(List, { items: task.assumptions, empty: '—' })))), primary.statement ? h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('contradictions')), h(Field, { label: t('primaryContradiction') }, primary.statement), h('div', { className: 'dsh-wp-grid', style: { marginTop: '8px' } }, h(Field, { label: t('evidence') }, h(List, { items: primary.evidence, empty: '—' })), h(Field, { label: t('whyPrimary') }, primary.whyPrimary || '—'), h(Field, { label: t('secondary') }, h(List, { items: contradictions.secondary, empty: '—' })), h(Field, { label: t('transformation') }, h(List, { items: contradictions.transformationConditions, empty: '—' })))) : null, session.risks?.length ? h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('risks')), session.risks.map((risk, index) => h('div', { className: 'dsh-wp-field', key: index, style: { marginTop: '6px' } }, h('div', { className: 'dsh-wp-field-value' }, risk.risk), h('div', { className: 'dsh-wp-field-label' }, `${t('likelihood')}: ${levelText(t, risk.likelihood)} · ${t('impact')}: ${risk.impact}`), h('div', { className: 'dsh-wp-muted' }, `${t('mitigation')}: ${risk.mitigation} · ${t('trigger')}: ${risk.trigger}`)))) : null, session.verification?.length ? h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('verification')), session.verification.map((item, index) => h('div', { className: 'dsh-wp-field', key: index, style: { marginTop: '6px' } }, h('div', { className: 'dsh-wp-field-value' }, item.item), h('div', { className: 'dsh-wp-muted' }, `${item.method} → ${item.evidence}`)))) : null, session.practiceLoop ? h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('practiceLoop')), h('div', { className: 'dsh-wp-grid' }, h(Field, { label: t('plan') }, session.practiceLoop.plan), h(Field, { label: t('do') }, session.practiceLoop.do), h(Field, { label: t('check') }, session.practiceLoop.check), h(Field, { label: t('act') }, session.practiceLoop.act))) : null, h('div', { className: 'dsh-wp-section dsh-wp-muted' }, t('advancedHint')), h('div', { className: 'dsh-wp-section dsh-wp-muted dsh-wp-code' }, `${t('planId')}: ${session.planId || '—'}`));
|
|
1190
1279
|
}
|
|
1280
|
+
// ── 1.5.0 进度图表化:把「做好了 / 卡住了」从一行 12px 文字升级为图表 ──
|
|
1281
|
+
// 数据只来自 session.stats / session.stages / session.statuses;全部是纯函数,
|
|
1282
|
+
// 无副作用、无新依赖;0/0 与脏数据收敛为「—」,绝不输出 NaN / Infinity。
|
|
1283
|
+
const PLAN_RING_SIZE = 44;
|
|
1284
|
+
const PLAN_RING_RADIUS = 18;
|
|
1285
|
+
const PLAN_RING_STROKE = 4;
|
|
1286
|
+
const PLAN_RING_CIRCUMFERENCE = 2 * Math.PI * PLAN_RING_RADIUS;
|
|
1287
|
+
function planCount(value) {
|
|
1288
|
+
const number = Number(value);
|
|
1289
|
+
if (!Number.isFinite(number))
|
|
1290
|
+
return 0;
|
|
1291
|
+
return Math.max(0, Math.round(number));
|
|
1292
|
+
}
|
|
1293
|
+
// 与 groupTotals 同一套口径:先夹到 [0,total],再让「正在做/还没开始」
|
|
1294
|
+
// 吃掉剩余量,四段之和恒等于总数(构成条不会溢出或出现负宽度)。
|
|
1295
|
+
function planCounts(stats) {
|
|
1296
|
+
const source = stats || {};
|
|
1297
|
+
const total = planCount(source.total);
|
|
1298
|
+
const done = Math.min(planCount(source.done), total);
|
|
1299
|
+
const blocked = Math.min(planCount(source.blocked), total - done);
|
|
1300
|
+
const active = Math.min(planCount(source.inProgress), total - done - blocked);
|
|
1301
|
+
const pending = Math.max(0, total - done - blocked - active);
|
|
1302
|
+
const percent = total > 0 ? Math.round((done / total) * 100) : null;
|
|
1303
|
+
return { total, done, active, blocked, pending, percent };
|
|
1304
|
+
}
|
|
1305
|
+
function planStatusMap(statuses) {
|
|
1306
|
+
const byId = new Map();
|
|
1307
|
+
for (const item of Array.isArray(statuses) ? statuses : []) {
|
|
1308
|
+
if (item && item.subtaskId != null)
|
|
1309
|
+
byId.set(String(item.subtaskId), item);
|
|
1310
|
+
}
|
|
1311
|
+
return byId;
|
|
1312
|
+
}
|
|
1313
|
+
function planTaskEntries(stages, statuses) {
|
|
1314
|
+
const byId = planStatusMap(statuses);
|
|
1315
|
+
const entries = [];
|
|
1316
|
+
for (const stage of Array.isArray(stages) ? stages : []) {
|
|
1317
|
+
const subtasks = Array.isArray(stage?.subtasks) ? stage.subtasks : [];
|
|
1318
|
+
for (const subtask of subtasks) {
|
|
1319
|
+
if (subtask?.id == null)
|
|
1320
|
+
continue;
|
|
1321
|
+
const item = byId.get(String(subtask.id));
|
|
1322
|
+
entries.push({ title: String(subtask?.title ?? ''), status: String(item?.status ?? 'pending') });
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return entries;
|
|
1326
|
+
}
|
|
1327
|
+
// 当前阶段=第一个有「正在做」的阶段;否则第一个还没全部做完的阶段。
|
|
1328
|
+
function planCurrentStageId(rows) {
|
|
1329
|
+
const running = rows.find((row) => row.active > 0);
|
|
1330
|
+
if (running)
|
|
1331
|
+
return running.id;
|
|
1332
|
+
const open = rows.find((row) => row.tone !== 'done');
|
|
1333
|
+
return open ? open.id : null;
|
|
1334
|
+
}
|
|
1335
|
+
function planStageRows(stages, statuses) {
|
|
1336
|
+
const byId = planStatusMap(statuses);
|
|
1337
|
+
const rows = [];
|
|
1338
|
+
const list = Array.isArray(stages) ? stages : [];
|
|
1339
|
+
list.forEach((stage, index) => {
|
|
1340
|
+
const subtasks = Array.isArray(stage?.subtasks) ? stage.subtasks : [];
|
|
1341
|
+
let done = 0;
|
|
1342
|
+
let active = 0;
|
|
1343
|
+
let blocked = 0;
|
|
1344
|
+
for (const subtask of subtasks) {
|
|
1345
|
+
const status = subtask?.id == null ? undefined : byId.get(String(subtask.id))?.status;
|
|
1346
|
+
if (status === 'done')
|
|
1347
|
+
done += 1;
|
|
1348
|
+
else if (status === 'in_progress')
|
|
1349
|
+
active += 1;
|
|
1350
|
+
else if (status === 'blocked')
|
|
1351
|
+
blocked += 1;
|
|
1352
|
+
}
|
|
1353
|
+
const total = subtasks.length;
|
|
1354
|
+
const tone = blocked > 0
|
|
1355
|
+
? 'blocked'
|
|
1356
|
+
: total > 0 && done === total
|
|
1357
|
+
? 'done'
|
|
1358
|
+
: active > 0 || done > 0 ? 'active' : 'pending';
|
|
1359
|
+
rows.push({
|
|
1360
|
+
id: String(stage?.id ?? `stage-${index}`),
|
|
1361
|
+
name: String(stage?.name ?? ''),
|
|
1362
|
+
index,
|
|
1363
|
+
total,
|
|
1364
|
+
done,
|
|
1365
|
+
active,
|
|
1366
|
+
blocked,
|
|
1367
|
+
tone,
|
|
1368
|
+
current: false,
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
const currentId = planCurrentStageId(rows);
|
|
1372
|
+
return rows.map((row) => ({ ...row, current: row.id === currentId }));
|
|
1373
|
+
}
|
|
1374
|
+
// 「现在在做什么」:卡住优先(需要人介入),其次是正在做,再次是全部做完/还没开始。
|
|
1375
|
+
function planNow(t, counts, entries) {
|
|
1376
|
+
const withTitle = (label, title) => (title ? `${label} · ${title}` : label);
|
|
1377
|
+
if (counts.blocked > 0) {
|
|
1378
|
+
const blocked = entries.find((entry) => entry.status === 'blocked');
|
|
1379
|
+
return { state: 'blocked', text: withTitle(t('stats.blocked'), blocked?.title) };
|
|
1380
|
+
}
|
|
1381
|
+
if (counts.active > 0) {
|
|
1382
|
+
const active = entries.find((entry) => entry.status === 'in_progress');
|
|
1383
|
+
return { state: 'active', text: withTitle(t('stats.inProgress'), active?.title) };
|
|
1384
|
+
}
|
|
1385
|
+
if (counts.total > 0 && counts.done === counts.total)
|
|
1386
|
+
return { state: 'done', text: t('helpDone') };
|
|
1387
|
+
return { state: 'pending', text: t('stats.pending') };
|
|
1388
|
+
}
|
|
1389
|
+
function PlanRing({ percent }) {
|
|
1390
|
+
const value = percent === null ? null : Math.max(0, Math.min(100, Math.round(percent)));
|
|
1391
|
+
// 0% 时不画弧:圆头端点会在 12 点方向留下一个与「已完成」混淆的小点。
|
|
1392
|
+
const arc = value === null ? 0 : (value / 100) * PLAN_RING_CIRCUMFERENCE;
|
|
1393
|
+
return h('span', {
|
|
1394
|
+
className: 'dsh-wp-planviz-ring',
|
|
1395
|
+
'aria-hidden': 'true',
|
|
1396
|
+
'data-zero': value === 0 ? 'true' : 'false',
|
|
1397
|
+
}, h('svg', { viewBox: `0 0 ${PLAN_RING_SIZE} ${PLAN_RING_SIZE}`, width: PLAN_RING_SIZE, height: PLAN_RING_SIZE, focusable: 'false' }, h('circle', {
|
|
1398
|
+
className: 'dsh-wp-planring-track',
|
|
1399
|
+
cx: PLAN_RING_SIZE / 2,
|
|
1400
|
+
cy: PLAN_RING_SIZE / 2,
|
|
1401
|
+
r: PLAN_RING_RADIUS,
|
|
1402
|
+
fill: 'none',
|
|
1403
|
+
strokeWidth: PLAN_RING_STROKE,
|
|
1404
|
+
}), value !== null && value > 0
|
|
1405
|
+
? h('circle', {
|
|
1406
|
+
className: 'dsh-wp-planring-arc',
|
|
1407
|
+
cx: PLAN_RING_SIZE / 2,
|
|
1408
|
+
cy: PLAN_RING_SIZE / 2,
|
|
1409
|
+
r: PLAN_RING_RADIUS,
|
|
1410
|
+
fill: 'none',
|
|
1411
|
+
strokeWidth: PLAN_RING_STROKE,
|
|
1412
|
+
strokeLinecap: 'round',
|
|
1413
|
+
strokeDasharray: `${arc} ${PLAN_RING_CIRCUMFERENCE - arc}`,
|
|
1414
|
+
transform: `rotate(-90 ${PLAN_RING_SIZE / 2} ${PLAN_RING_SIZE / 2})`,
|
|
1415
|
+
})
|
|
1416
|
+
: null));
|
|
1417
|
+
}
|
|
1418
|
+
function PlanLegend({ t, counts }) {
|
|
1419
|
+
const items = [
|
|
1420
|
+
{ kind: 'done', label: t('stats.done'), count: counts.done },
|
|
1421
|
+
{ kind: 'active', label: t('stats.inProgress'), count: counts.active },
|
|
1422
|
+
{ kind: 'blocked', label: t('stats.blocked'), count: counts.blocked },
|
|
1423
|
+
{ kind: 'pending', label: t('stats.pending'), count: counts.pending },
|
|
1424
|
+
];
|
|
1425
|
+
return h('div', { className: 'dsh-wp-planviz-legend' }, items.map((item) => h('span', {
|
|
1426
|
+
key: item.kind,
|
|
1427
|
+
className: 'dsh-wp-planviz-legend-item',
|
|
1428
|
+
'data-kind': item.kind,
|
|
1429
|
+
'data-count': String(item.count),
|
|
1430
|
+
}, h('span', { className: 'dsh-wp-planviz-legend-dot', 'aria-hidden': 'true' }), `${item.label} ${item.count}`)));
|
|
1431
|
+
}
|
|
1432
|
+
function PlanSegments({ counts }) {
|
|
1433
|
+
const segments = [
|
|
1434
|
+
{ kind: 'done', count: counts.done },
|
|
1435
|
+
{ kind: 'active', count: counts.active },
|
|
1436
|
+
{ kind: 'blocked', count: counts.blocked },
|
|
1437
|
+
{ kind: 'pending', count: counts.pending },
|
|
1438
|
+
];
|
|
1439
|
+
return h('div', {
|
|
1440
|
+
className: 'dsh-wp-planviz-bar',
|
|
1441
|
+
'aria-hidden': 'true',
|
|
1442
|
+
'data-total': String(counts.total),
|
|
1443
|
+
}, counts.total === 0
|
|
1444
|
+
? null
|
|
1445
|
+
: segments.map((segment) => (segment.count > 0
|
|
1446
|
+
? h('span', {
|
|
1447
|
+
key: segment.kind,
|
|
1448
|
+
className: 'dsh-wp-planviz-seg',
|
|
1449
|
+
'data-kind': segment.kind,
|
|
1450
|
+
'data-count': String(segment.count),
|
|
1451
|
+
style: { flexGrow: segment.count },
|
|
1452
|
+
})
|
|
1453
|
+
: null)));
|
|
1454
|
+
}
|
|
1455
|
+
function PlanStepper({ t, rows }) {
|
|
1456
|
+
if (rows.length < 1 || rows.length > 12)
|
|
1457
|
+
return null;
|
|
1458
|
+
return h('div', { className: 'dsh-wp-stepper', 'aria-hidden': 'true', 'data-stages': String(rows.length) }, rows.map((row) => {
|
|
1459
|
+
const reach = row.total > 0 ? ((row.done + row.active + row.blocked) / row.total) * 100 : 0;
|
|
1460
|
+
const width = row.tone === 'pending' ? 0 : reach;
|
|
1461
|
+
return h('span', {
|
|
1462
|
+
key: row.id,
|
|
1463
|
+
className: 'dsh-wp-stepper-seg',
|
|
1464
|
+
'data-stage-id': row.id,
|
|
1465
|
+
'data-tone': row.tone,
|
|
1466
|
+
'data-current': row.current ? 'true' : 'false',
|
|
1467
|
+
title: `${stageLabel(t, row.index)} ${row.done}/${row.total} ${t('stats.done')}`,
|
|
1468
|
+
}, h('span', {
|
|
1469
|
+
className: 'dsh-wp-stepper-fill',
|
|
1470
|
+
'data-width': String(Math.round(width)),
|
|
1471
|
+
style: { width: `${width}%` },
|
|
1472
|
+
}));
|
|
1473
|
+
}));
|
|
1474
|
+
}
|
|
1475
|
+
function PlanViz({ t, counts, now, rows }) {
|
|
1476
|
+
const state = counts.percent === null
|
|
1477
|
+
? 'idle'
|
|
1478
|
+
: counts.blocked > 0
|
|
1479
|
+
? 'blocked'
|
|
1480
|
+
: counts.percent >= 100 ? 'done' : 'active';
|
|
1481
|
+
return h('div', {
|
|
1482
|
+
className: 'dsh-wp-planviz',
|
|
1483
|
+
'data-percent': counts.percent === null ? 'none' : String(counts.percent),
|
|
1484
|
+
'data-alarm': counts.blocked > 0 ? 'true' : 'false',
|
|
1485
|
+
'data-state': state,
|
|
1486
|
+
}, h(PlanRing, { percent: counts.percent }), h('div', { className: 'dsh-wp-planviz-head' }, h('span', { className: 'dsh-wp-planviz-pct' }, counts.percent === null ? '—' : `${counts.percent}%`), h('span', { className: 'dsh-wp-planviz-scope' }, `${t('scopeThis')} ${counts.done}/${counts.total} ${t('stats.done')}`), counts.blocked > 0
|
|
1487
|
+
? h('span', { className: 'dsh-wp-planviz-alarm' }, `${counts.blocked} ${t('stats.blocked')}`)
|
|
1488
|
+
: null, h('span', { className: 'dsh-wp-planviz-now', 'data-state': now.state }, h('span', { className: 'dsh-wp-planviz-now-dot', 'aria-hidden': 'true' }), h('span', { className: 'dsh-wp-planviz-now-text' }, now.text))), h(PlanSegments, { counts }), h(PlanLegend, { t, counts }), h(PlanStepper, { t, rows }));
|
|
1489
|
+
}
|
|
1490
|
+
// 展开后的「分步进度」:逐阶段行 + 加权总体进度,替代旧的一根计划级进度条。
|
|
1491
|
+
function PlanOverview({ t, stats, counts, rows }) {
|
|
1492
|
+
const stateOf = (tone) => (tone === 'done'
|
|
1493
|
+
? t('stats.done')
|
|
1494
|
+
: tone === 'active' ? t('stats.inProgress') : tone === 'blocked' ? t('stats.blocked') : t('stats.pending'));
|
|
1495
|
+
return h('div', {
|
|
1496
|
+
className: 'dsh-wp-planoverview',
|
|
1497
|
+
'data-plan-total': String(counts.total),
|
|
1498
|
+
'data-plan-done': String(counts.done),
|
|
1499
|
+
}, h('div', { className: 'dsh-wp-planoverview-head' }, h('span', { className: 'dsh-wp-planoverview-title' }, t('vizStageTitle')), h('span', null, `${counts.done}/${counts.total} ${t('stats.done')}`)), h('div', { className: 'dsh-wp-planoverview-stats' }, h('span', null, `${t('vizWeighted')} ${clampPct(stats?.progress ?? 0)}%`), h('span', null, `${counts.active} ${t('stats.inProgress')} · ${counts.pending} ${t('stats.pending')}`), counts.blocked > 0 ? h('span', null, `${counts.blocked} ${t('stats.blocked')}`) : null), rows.length
|
|
1500
|
+
? h('div', { className: 'dsh-wp-planrows' }, rows.map((row) => {
|
|
1501
|
+
const width = row.total > 0 ? (row.done / row.total) * 100 : 0;
|
|
1502
|
+
return h('div', {
|
|
1503
|
+
key: row.id,
|
|
1504
|
+
className: 'dsh-wp-planrow',
|
|
1505
|
+
'data-tone': row.tone,
|
|
1506
|
+
'data-current': row.current ? 'true' : 'false',
|
|
1507
|
+
}, h('span', { className: 'dsh-wp-planrow-dot', 'aria-hidden': 'true' }), h('span', { className: 'dsh-wp-planrow-name' }, `${stageLabel(t, row.index)}:${row.name}`), h('span', { className: 'dsh-wp-planrow-bar' }, h('span', {
|
|
1508
|
+
className: 'dsh-wp-planrow-fill',
|
|
1509
|
+
'data-width': String(Math.round(width)),
|
|
1510
|
+
style: { width: `${width}%` },
|
|
1511
|
+
})), h('span', { className: 'dsh-wp-planrow-count' }, `${row.done}/${row.total}`), h('span', { className: 'dsh-wp-planrow-state' }, stateOf(row.tone)));
|
|
1512
|
+
}))
|
|
1513
|
+
: null);
|
|
1514
|
+
}
|
|
1191
1515
|
function SessionCard({ session, t, open, onToggle }) {
|
|
1192
1516
|
const domId = `dsh-wp-session-${session.sessionId}`;
|
|
1193
1517
|
const phase = session.phase || 'pending';
|
|
1194
1518
|
const stats = session.stats || {};
|
|
1195
1519
|
const tone = phaseTone(phase);
|
|
1196
1520
|
const departments = Array.isArray(session.departments) ? session.departments : [];
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1521
|
+
// 1.5.0:折叠卡摘要=图表(环 + 百分比 + 四段构成条 + 图例 + 阶段步进条);
|
|
1522
|
+
// 数字仍只来自 session.stats,与旧头部的口径完全一致,不引入第二套统计。
|
|
1523
|
+
const counts = planCounts(stats);
|
|
1524
|
+
const statuses = Array.isArray(session.statuses) ? session.statuses : [];
|
|
1525
|
+
const stageRows = planStageRows(session.stages, statuses);
|
|
1526
|
+
const now = planNow(t, counts, planTaskEntries(session.stages, statuses));
|
|
1203
1527
|
return h('article', { id: domId, 'data-session-id': session.sessionId, className: `dsh-wp-session dsh-wp-tone-${tone}` }, h(Collapse, {
|
|
1204
1528
|
id: `dsh-wp-collapse-${session.sessionId}`,
|
|
1205
1529
|
open: open === true,
|
|
@@ -1207,15 +1531,11 @@ window.__ModuleLoader__.load({
|
|
|
1207
1531
|
className: 'dsh-wp-session-collapse',
|
|
1208
1532
|
panelClassName: 'dsh-wp-session-body',
|
|
1209
1533
|
title: session.title || t('panel'),
|
|
1210
|
-
meta:
|
|
1211
|
-
h('span', { key: 'scope' }, `${t('scopeThis')} ${done}/${total} ${t('stats.done')}`),
|
|
1212
|
-
h('span', { key: 'bar', className: 'dsh-wp-mini', title: `${percent}%` }, h('span', { className: `dsh-wp-mini-fill dsh-wp-mini-fill--${tone}`, style: { width: `${percent}%` } })),
|
|
1213
|
-
h('span', { key: 'blocked' }, `${blocked} ${t('stats.blocked')}`),
|
|
1214
|
-
],
|
|
1534
|
+
meta: h(PlanViz, { t, counts, now, rows: stageRows }),
|
|
1215
1535
|
badge: h(Pill, { tone }, t(`phase.${phase}`)),
|
|
1216
1536
|
}, session.updatedAt
|
|
1217
1537
|
? h('div', { className: 'dsh-wp-meta dsh-wp-session-updated' }, `${t('updatedAt')} ${formatTime(session.updatedAt)}`)
|
|
1218
|
-
: null, h(
|
|
1538
|
+
: null, open === true ? h(PlanOverview, { t, stats, counts, rows: stageRows }) : null, session.degraded ? h('div', { className: 'dsh-wp-banner dsh-wp-banner--degraded' }, h('strong', null, t('degraded')), h('div', null, plainError(session.error || '')), h('div', { className: 'dsh-wp-muted' }, t('degradedHint'))) : null, h(Guide, { session, t }), departments.length ? h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('who')), h('div', { className: 'dsh-wp-departments' }, departments.map((dept) => h(Pill, { key: dept.id, tone: dept.blocked > 0 ? 'blocked' : dept.done === dept.total ? 'done' : 'active' }, `${deptName(t, dept.id)} ${dept.done}/${dept.total} ${t('stats.done')}`)))) : null, h('div', { className: 'dsh-wp-section' }, h('div', { className: 'dsh-wp-section-title' }, t('steps')), (session.stages || []).map((stage, index) => h(StageCard, {
|
|
1219
1539
|
key: stage.id,
|
|
1220
1540
|
stage,
|
|
1221
1541
|
statuses: session.statuses || [],
|
|
@@ -1431,7 +1751,12 @@ window.__ModuleLoader__.load({
|
|
|
1431
1751
|
? Math.min(100, Math.max(0, percent))
|
|
1432
1752
|
: null;
|
|
1433
1753
|
const arc = value === null ? 0 : (value / 100) * circumference;
|
|
1434
|
-
return h('span', {
|
|
1754
|
+
return h('span', {
|
|
1755
|
+
className: 'dsh-wp-group-ring',
|
|
1756
|
+
'aria-hidden': 'true',
|
|
1757
|
+
// 0% 与「暂无数据」必须在视觉上可区分:0% 加粗零轨(≥3:1),无数则细轨 + 破折号。
|
|
1758
|
+
'data-zero': value === 0 ? 'true' : 'false',
|
|
1759
|
+
}, h('svg', { viewBox: `0 0 ${size} ${size}`, width: size, height: size, focusable: 'false' }, h('circle', {
|
|
1435
1760
|
className: 'dsh-wp-group-ring-track',
|
|
1436
1761
|
cx: size / 2,
|
|
1437
1762
|
cy: size / 2,
|
|
@@ -1479,8 +1804,11 @@ window.__ModuleLoader__.load({
|
|
|
1479
1804
|
const totals = useMemo(() => groupTotals(list), [list]);
|
|
1480
1805
|
const groupTitle = t(isDone ? 'groupDone' : 'groupLive');
|
|
1481
1806
|
const hintText = t(isDone ? 'groupDoneHint' : 'groupLiveHint');
|
|
1807
|
+
// 用户 Q3:0/0 时不再显示「未完成 0 / 已完成 0(完成率暂无数据)」——
|
|
1808
|
+
// 环上已经是「—」,组内空态也已说明没有工作项;再重复一遍 0/0 只是噪声。
|
|
1809
|
+
// 全部做完(100%)仍保留整句,因为那时「未完成 0」正是用户要找的结论。
|
|
1482
1810
|
const summaryText = totals.percent === null
|
|
1483
|
-
?
|
|
1811
|
+
? null
|
|
1484
1812
|
: fill(t('groupSummary'), { open: totals.open, done: totals.done, pct: totals.percent });
|
|
1485
1813
|
// 读屏用完整句:计数、完成率与三态明细一起念,避免只听到一个孤零零的数字。
|
|
1486
1814
|
const ariaText = totals.total > 0
|
|
@@ -1506,7 +1834,9 @@ window.__ModuleLoader__.load({
|
|
|
1506
1834
|
collapsible ? h('span', { key: 'chevron', className: 'dsh-wp-group-chevron', 'aria-hidden': 'true' }) : null,
|
|
1507
1835
|
// 摘要独占一整行(CSS 里 flex-basis:100%):窄面板只截断摘要本身,
|
|
1508
1836
|
// 组名/徽章/环不会被挤走;完整句放在 title,悬停可读全。
|
|
1509
|
-
|
|
1837
|
+
summaryText === null
|
|
1838
|
+
? null
|
|
1839
|
+
: h('span', { key: 'summary', className: 'dsh-wp-group-sub', title: hintText }, summaryText),
|
|
1510
1840
|
];
|
|
1511
1841
|
const head = collapsible
|
|
1512
1842
|
? h('button', {
|
package/lib/host.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workplan",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Automatic task decomposition, real department subagents, and a responsive workplan dashboard for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"build": "rm -rf dist && tsc -p tsconfig.json && cp dist/plan.js dist/runtime.js dist/host.js dist/watchdog.js lib/ && cp dist/client.js client.js",
|
|
67
67
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
68
68
|
"test": "npm run build && rm -rf dist-tests && tsc -p tsconfig.test.json && node --test dist-tests/tests/*.test.js",
|
|
69
|
-
"verify": "npm run check && npm test"
|
|
69
|
+
"verify": "npm run check && npm test",
|
|
70
|
+
"preview": "node scripts/render-preview.ts --emit"
|
|
70
71
|
},
|
|
71
72
|
"devDependencies": {
|
|
72
73
|
"@types/node": "^22.20.5",
|
package/src/client.ts
CHANGED
|
@@ -174,6 +174,8 @@ window.__ModuleLoader__.load({
|
|
|
174
174
|
"jumpToTask": "跳转到任务明细",
|
|
175
175
|
"scopeAll": "当前各会话最新计划汇总",
|
|
176
176
|
"scopeThis": "本计划",
|
|
177
|
+
"vizStageTitle": "分步进度",
|
|
178
|
+
"vizWeighted": "总体进度(加权)",
|
|
177
179
|
"allPlansHint": "上面是当前各会话最新计划的合计;历史计划不在此汇总,下面每张卡片只统计本计划。",
|
|
178
180
|
"openHint": "打开左侧「任务进度」看完整安排。"
|
|
179
181
|
},
|
|
@@ -334,6 +336,8 @@ window.__ModuleLoader__.load({
|
|
|
334
336
|
"jumpToTask": "Jump to task details",
|
|
335
337
|
"scopeAll": "Latest plan per live session",
|
|
336
338
|
"scopeThis": "This plan",
|
|
339
|
+
"vizStageTitle": "Step progress",
|
|
340
|
+
"vizWeighted": "Overall progress (weighted)",
|
|
337
341
|
"allPlansHint": "These totals cover the latest plan in each live session; older plans are not included. Each card below counts only its own plan.",
|
|
338
342
|
"openHint": "Open “Task progress” on the left for the full plan."
|
|
339
343
|
}
|
|
@@ -367,6 +371,9 @@ window.__ModuleLoader__.load({
|
|
|
367
371
|
底槽用中性灰而不是品牌色,避免「未完成部分」被误读成状态色。 */
|
|
368
372
|
--dsh-wp-done-ink: var(--dsh-wp-done-text);
|
|
369
373
|
--dsh-wp-ring-track: rgba(148,163,184,.30);
|
|
374
|
+
/* 1.5.0 — 进度区图表化:构成条/阶段槽的中性轨道,以及 0% 零轨(≥3:1 图形对比)。 */
|
|
375
|
+
--dsh-wp-track: rgba(148,163,184,.22);
|
|
376
|
+
--dsh-wp-track-strong: rgba(100,116,139,.80);
|
|
370
377
|
min-width: 0;
|
|
371
378
|
width: 100%;
|
|
372
379
|
height: 100%;
|
|
@@ -408,6 +415,10 @@ window.__ModuleLoader__.load({
|
|
|
408
415
|
.dsh-wp-group-ring { position: relative; width: 44px; height: 44px; flex: none; margin-left: auto; display: inline-grid; place-items: center; }
|
|
409
416
|
.dsh-wp-group-ring svg { position: absolute; inset: 0; display: block; }
|
|
410
417
|
.dsh-wp-group-ring-track { stroke: var(--dsh-wp-ring-track, rgba(148,163,184,.30)); }
|
|
418
|
+
/* 1.5.0 — 0% 不等于「没有图」:零轨加粗到 ≥3:1(rgba(100,116,139,.80)),
|
|
419
|
+
环仍然看得见;「暂无数据」保持细轨 + 「—」,两种状态在视觉上不是一回事。 */
|
|
420
|
+
.dsh-wp-group-ring[data-zero="true"] .dsh-wp-group-ring-track { stroke: var(--dsh-wp-track-strong); }
|
|
421
|
+
.dsh-wp-group-ring[data-zero="true"] .dsh-wp-group-ring-label { font-size: 12px; font-weight: 800; }
|
|
411
422
|
.dsh-wp-group-ring-arc { stroke: var(--dsh-wp-done-ink, #0284c7); transition: stroke-dasharray .28s ease; }
|
|
412
423
|
.dsh-wp-group-ring-label { position: relative; font-size: 11px; font-weight: 700; line-height: 1; letter-spacing: -.2px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, #0f172a); }
|
|
413
424
|
@media (prefers-reduced-motion: reduce) { .dsh-wp-group-ring-arc { transition: none; } }
|
|
@@ -632,6 +643,84 @@ window.__ModuleLoader__.load({
|
|
|
632
643
|
.dsh-wp-mini-fill--blocked { background: var(--dsw-alias-state-error-primary); }
|
|
633
644
|
.dsh-wp-mini-fill--warn { background: var(--dsw-alias-state-warn-primary); }
|
|
634
645
|
.dsh-wp-mini-fill--idle { background: var(--dsw-alias-state-idle-primary); }
|
|
646
|
+
/* ── 1.5.0 计划卡进度图表(用户诉求:画圈处必须一眼可读)─────────────────
|
|
647
|
+
四层信号同时存在,任何一层失效都还能读出进度,且都不依赖颜色:
|
|
648
|
+
① 44px 环:图形比例;0% 时轨道加粗仍可见,不画弧(避免圆头端点留下小点)。
|
|
649
|
+
② 24px 百分比:主文本色,浅色底 17:1;「—」表示分母为 0,绝不写 NaN。
|
|
650
|
+
③ 整行四段构成条:按件数分宽;0% 时是一条完整的灰色「还没开始」,
|
|
651
|
+
这也是「整条灰=都还没开始」的图形解释。
|
|
652
|
+
④ 11px 状态图例 + 一行「现在在做什么」+ 6px 阶段步进条。
|
|
653
|
+
数据只来自既有 session.stats / stages / statuses:无新接口、无依赖、无定时器。 */
|
|
654
|
+
.dsh-wp-session-collapse .dsh-wp-collapse-meta { flex: 1 1 100%; display: block; margin-top: 6px; }
|
|
655
|
+
.dsh-wp-planviz {
|
|
656
|
+
display: grid;
|
|
657
|
+
grid-template-columns: 44px minmax(0, 1fr);
|
|
658
|
+
grid-template-areas: "ring head" "bar bar" "legend legend" "step step";
|
|
659
|
+
column-gap: 10px;
|
|
660
|
+
row-gap: 6px;
|
|
661
|
+
width: 100%;
|
|
662
|
+
min-width: 0;
|
|
663
|
+
}
|
|
664
|
+
.dsh-wp-planviz-ring { grid-area: ring; position: relative; width: 44px; height: 44px; display: inline-grid; place-items: center; }
|
|
665
|
+
.dsh-wp-planviz-ring svg { position: absolute; inset: 0; display: block; }
|
|
666
|
+
.dsh-wp-planring-track { stroke: var(--dsh-wp-track); }
|
|
667
|
+
.dsh-wp-planring-arc { stroke: var(--dsh-wp-done-ink); transition: stroke-dasharray .28s ease; }
|
|
668
|
+
.dsh-wp-planviz-ring[data-zero="true"] .dsh-wp-planring-track { stroke: var(--dsh-wp-track-strong); }
|
|
669
|
+
.dsh-wp-planviz-head { grid-area: head; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
|
|
670
|
+
.dsh-wp-planviz-pct { font-size: 24px; font-weight: 700; line-height: 1.15; letter-spacing: -.4px; color: var(--dsw-alias-label-primary, #0f172a); font-variant-numeric: tabular-nums; }
|
|
671
|
+
.dsh-wp-planviz-scope { min-width: 0; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
672
|
+
.dsh-wp-planviz-alarm { font-size: 11px; font-weight: 650; padding: 1px 7px; border-radius: 999px; color: var(--dsw-alias-state-error-primary, #ef4444); color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 80%, var(--dsw-alias-label-primary, #0f172a) 20%); background: rgba(239,68,68,.12); }
|
|
673
|
+
.dsh-wp-planviz-now { flex: 1 1 100%; display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); }
|
|
674
|
+
.dsh-wp-planviz-now-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
675
|
+
.dsh-wp-planviz[data-state="active"] .dsh-wp-planviz-now-dot { background: var(--dsh-wp-active); }
|
|
676
|
+
.dsh-wp-planviz[data-state="done"] .dsh-wp-planviz-now-dot { background: var(--dsh-wp-done); }
|
|
677
|
+
.dsh-wp-planviz[data-state="blocked"] .dsh-wp-planviz-now-dot { background: var(--dsw-alias-state-error-primary); }
|
|
678
|
+
.dsh-wp-planviz-now-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
679
|
+
.dsh-wp-planviz-bar { grid-area: bar; display: flex; align-items: stretch; gap: 2px; height: 8px; border-radius: 999px; background: var(--dsh-wp-track); overflow: hidden; }
|
|
680
|
+
.dsh-wp-planviz-seg { flex: 1 1 0; min-width: 4px; height: 100%; border-radius: 999px; }
|
|
681
|
+
.dsh-wp-planviz-seg[data-kind="done"] { background: var(--dsh-wp-done); }
|
|
682
|
+
.dsh-wp-planviz-seg[data-kind="active"] { background: var(--dsh-wp-active); }
|
|
683
|
+
.dsh-wp-planviz-seg[data-kind="blocked"] { background: var(--dsw-alias-state-error-primary); }
|
|
684
|
+
.dsh-wp-planviz-seg[data-kind="pending"] { background: var(--dsw-alias-state-idle-primary); }
|
|
685
|
+
.dsh-wp-planviz-legend { grid-area: legend; display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 11px; line-height: 1.5; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
686
|
+
.dsh-wp-planviz-legend-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
687
|
+
.dsh-wp-planviz-legend-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
688
|
+
.dsh-wp-planviz-legend-item[data-kind="done"] .dsh-wp-planviz-legend-dot { background: var(--dsh-wp-done); }
|
|
689
|
+
.dsh-wp-planviz-legend-item[data-kind="active"] .dsh-wp-planviz-legend-dot { background: var(--dsh-wp-active); }
|
|
690
|
+
.dsh-wp-planviz-legend-item[data-kind="blocked"] .dsh-wp-planviz-legend-dot { background: var(--dsw-alias-state-error-primary); }
|
|
691
|
+
.dsh-wp-stepper { grid-area: step; display: flex; gap: 3px; min-width: 0; }
|
|
692
|
+
.dsh-wp-stepper-seg { position: relative; flex: 1 1 0; min-width: 0; height: 6px; border-radius: 999px; background: var(--dsh-wp-track); overflow: hidden; }
|
|
693
|
+
.dsh-wp-stepper-seg[data-tone="pending"] { background: transparent; border: 1px dashed var(--dsh-wp-track-strong); }
|
|
694
|
+
.dsh-wp-stepper-seg[data-tone="blocked"] { box-shadow: inset 0 0 0 1px var(--dsw-alias-state-error-primary); }
|
|
695
|
+
.dsh-wp-stepper-seg[data-current="true"] { outline: 1.5px solid var(--dsh-wp-active-text); outline-offset: 1px; }
|
|
696
|
+
.dsh-wp-stepper-fill { display: block; height: 100%; border-radius: 999px; background: var(--dsh-wp-active); }
|
|
697
|
+
.dsh-wp-stepper-seg[data-tone="done"] .dsh-wp-stepper-fill { background: var(--dsh-wp-done); }
|
|
698
|
+
.dsh-wp-stepper-seg[data-tone="blocked"] .dsh-wp-stepper-fill { background: var(--dsw-alias-state-error-primary); }
|
|
699
|
+
@media (prefers-reduced-motion: reduce) { .dsh-wp-planring-arc { transition: none; } }
|
|
700
|
+
/* 短屏(纵向更紧):先撤图例与阶段条,保住环 + 百分比 + 构成条 + 当前步骤。 */
|
|
701
|
+
@media (max-height: 720px) and (max-width: 600px) {
|
|
702
|
+
.dsh-wp-planviz-legend, .dsh-wp-stepper { display: none; }
|
|
703
|
+
}
|
|
704
|
+
/* 展开态「分步进度」:替换旧的计划级进度条,保留加权进度并给出逐阶段分解。 */
|
|
705
|
+
.dsh-wp-planoverview { margin: 12px 0 4px; padding: 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-2); display: grid; gap: 8px; }
|
|
706
|
+
.dsh-wp-planoverview-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); }
|
|
707
|
+
.dsh-wp-planoverview-title { font-size: 12px; font-weight: 650; letter-spacing: .2px; }
|
|
708
|
+
.dsh-wp-planoverview-stats { display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
709
|
+
.dsh-wp-planrows { display: grid; }
|
|
710
|
+
.dsh-wp-planrow { display: grid; grid-template-columns: 7px minmax(0, 1fr) 48px auto auto; align-items: center; gap: 8px; min-height: 28px; padding: 4px 0; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
711
|
+
.dsh-wp-planrow:first-child { border-top: 0; }
|
|
712
|
+
.dsh-wp-planrow[data-current="true"] { background: rgba(249,115,22,.06); border-radius: 8px; }
|
|
713
|
+
.dsh-wp-planrow-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }
|
|
714
|
+
.dsh-wp-planrow[data-tone="done"] .dsh-wp-planrow-dot { background: var(--dsh-wp-done); }
|
|
715
|
+
.dsh-wp-planrow[data-tone="active"] .dsh-wp-planrow-dot { background: var(--dsh-wp-active); }
|
|
716
|
+
.dsh-wp-planrow[data-tone="blocked"] .dsh-wp-planrow-dot { background: var(--dsw-alias-state-error-primary); }
|
|
717
|
+
.dsh-wp-planrow-name { min-width: 0; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #0f172a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
718
|
+
.dsh-wp-planrow-bar { display: block; height: 4px; border-radius: 999px; background: var(--dsh-wp-track); overflow: hidden; }
|
|
719
|
+
.dsh-wp-planrow-fill { display: block; height: 100%; border-radius: 999px; background: var(--dsh-wp-active); }
|
|
720
|
+
.dsh-wp-planrow[data-tone="done"] .dsh-wp-planrow-fill { background: var(--dsh-wp-done); }
|
|
721
|
+
.dsh-wp-planrow[data-tone="blocked"] .dsh-wp-planrow-fill { background: var(--dsw-alias-state-error-primary); }
|
|
722
|
+
.dsh-wp-planrow-count { font-size: 12px; color: var(--dsw-alias-label-secondary, #475569); font-variant-numeric: tabular-nums; }
|
|
723
|
+
.dsh-wp-planrow-state { font-size: 11px; color: var(--dsw-alias-label-secondary, #475569); white-space: nowrap; }
|
|
635
724
|
.dsh-wp-sessions { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); gap: 16px; align-items: start; }
|
|
636
725
|
.dsh-wp-session {
|
|
637
726
|
min-width: 0;
|
|
@@ -1333,18 +1422,336 @@ window.__ModuleLoader__.load({
|
|
|
1333
1422
|
)
|
|
1334
1423
|
}
|
|
1335
1424
|
|
|
1425
|
+
// ── 1.5.0 进度图表化:把「做好了 / 卡住了」从一行 12px 文字升级为图表 ──
|
|
1426
|
+
// 数据只来自 session.stats / session.stages / session.statuses;全部是纯函数,
|
|
1427
|
+
// 无副作用、无新依赖;0/0 与脏数据收敛为「—」,绝不输出 NaN / Infinity。
|
|
1428
|
+
const PLAN_RING_SIZE = 44
|
|
1429
|
+
const PLAN_RING_RADIUS = 18
|
|
1430
|
+
const PLAN_RING_STROKE = 4
|
|
1431
|
+
const PLAN_RING_CIRCUMFERENCE = 2 * Math.PI * PLAN_RING_RADIUS
|
|
1432
|
+
|
|
1433
|
+
type PlanTone = 'done' | 'active' | 'blocked' | 'pending' | 'idle'
|
|
1434
|
+
type PlanNowState = 'active' | 'blocked' | 'done' | 'pending'
|
|
1435
|
+
|
|
1436
|
+
type PlanCounts = {
|
|
1437
|
+
total: number
|
|
1438
|
+
done: number
|
|
1439
|
+
active: number
|
|
1440
|
+
blocked: number
|
|
1441
|
+
pending: number
|
|
1442
|
+
percent: number | null
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
type PlanStageRow = {
|
|
1446
|
+
id: string
|
|
1447
|
+
name: string
|
|
1448
|
+
index: number
|
|
1449
|
+
total: number
|
|
1450
|
+
done: number
|
|
1451
|
+
active: number
|
|
1452
|
+
blocked: number
|
|
1453
|
+
tone: PlanTone
|
|
1454
|
+
current: boolean
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
type PlanTaskEntry = { title: string; status: string }
|
|
1458
|
+
type PlanNow = { text: string; state: PlanNowState }
|
|
1459
|
+
|
|
1460
|
+
function planCount(value: any): number {
|
|
1461
|
+
const number = Number(value)
|
|
1462
|
+
if (!Number.isFinite(number)) return 0
|
|
1463
|
+
return Math.max(0, Math.round(number))
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// 与 groupTotals 同一套口径:先夹到 [0,total],再让「正在做/还没开始」
|
|
1467
|
+
// 吃掉剩余量,四段之和恒等于总数(构成条不会溢出或出现负宽度)。
|
|
1468
|
+
function planCounts(stats: any): PlanCounts {
|
|
1469
|
+
const source = stats || {}
|
|
1470
|
+
const total = planCount(source.total)
|
|
1471
|
+
const done = Math.min(planCount(source.done), total)
|
|
1472
|
+
const blocked = Math.min(planCount(source.blocked), total - done)
|
|
1473
|
+
const active = Math.min(planCount(source.inProgress), total - done - blocked)
|
|
1474
|
+
const pending = Math.max(0, total - done - blocked - active)
|
|
1475
|
+
const percent = total > 0 ? Math.round((done / total) * 100) : null
|
|
1476
|
+
return { total, done, active, blocked, pending, percent }
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function planStatusMap(statuses: any): Map<string, any> {
|
|
1480
|
+
const byId = new Map<string, any>()
|
|
1481
|
+
for (const item of Array.isArray(statuses) ? statuses : []) {
|
|
1482
|
+
if (item && item.subtaskId != null) byId.set(String(item.subtaskId), item)
|
|
1483
|
+
}
|
|
1484
|
+
return byId
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function planTaskEntries(stages: any, statuses: any): PlanTaskEntry[] {
|
|
1488
|
+
const byId = planStatusMap(statuses)
|
|
1489
|
+
const entries: PlanTaskEntry[] = []
|
|
1490
|
+
for (const stage of Array.isArray(stages) ? stages : []) {
|
|
1491
|
+
const subtasks = Array.isArray(stage?.subtasks) ? stage.subtasks : []
|
|
1492
|
+
for (const subtask of subtasks) {
|
|
1493
|
+
if (subtask?.id == null) continue
|
|
1494
|
+
const item = byId.get(String(subtask.id))
|
|
1495
|
+
entries.push({ title: String(subtask?.title ?? ''), status: String(item?.status ?? 'pending') })
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return entries
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// 当前阶段=第一个有「正在做」的阶段;否则第一个还没全部做完的阶段。
|
|
1502
|
+
function planCurrentStageId(rows: PlanStageRow[]): string | null {
|
|
1503
|
+
const running = rows.find((row) => row.active > 0)
|
|
1504
|
+
if (running) return running.id
|
|
1505
|
+
const open = rows.find((row) => row.tone !== 'done')
|
|
1506
|
+
return open ? open.id : null
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function planStageRows(stages: any, statuses: any): PlanStageRow[] {
|
|
1510
|
+
const byId = planStatusMap(statuses)
|
|
1511
|
+
const rows: PlanStageRow[] = []
|
|
1512
|
+
const list = Array.isArray(stages) ? stages : []
|
|
1513
|
+
list.forEach((stage: any, index: number) => {
|
|
1514
|
+
const subtasks = Array.isArray(stage?.subtasks) ? stage.subtasks : []
|
|
1515
|
+
let done = 0
|
|
1516
|
+
let active = 0
|
|
1517
|
+
let blocked = 0
|
|
1518
|
+
for (const subtask of subtasks) {
|
|
1519
|
+
const status = subtask?.id == null ? undefined : byId.get(String(subtask.id))?.status
|
|
1520
|
+
if (status === 'done') done += 1
|
|
1521
|
+
else if (status === 'in_progress') active += 1
|
|
1522
|
+
else if (status === 'blocked') blocked += 1
|
|
1523
|
+
}
|
|
1524
|
+
const total = subtasks.length
|
|
1525
|
+
const tone: PlanTone = blocked > 0
|
|
1526
|
+
? 'blocked'
|
|
1527
|
+
: total > 0 && done === total
|
|
1528
|
+
? 'done'
|
|
1529
|
+
: active > 0 || done > 0 ? 'active' : 'pending'
|
|
1530
|
+
rows.push({
|
|
1531
|
+
id: String(stage?.id ?? `stage-${index}`),
|
|
1532
|
+
name: String(stage?.name ?? ''),
|
|
1533
|
+
index,
|
|
1534
|
+
total,
|
|
1535
|
+
done,
|
|
1536
|
+
active,
|
|
1537
|
+
blocked,
|
|
1538
|
+
tone,
|
|
1539
|
+
current: false,
|
|
1540
|
+
})
|
|
1541
|
+
})
|
|
1542
|
+
const currentId = planCurrentStageId(rows)
|
|
1543
|
+
return rows.map((row) => ({ ...row, current: row.id === currentId }))
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// 「现在在做什么」:卡住优先(需要人介入),其次是正在做,再次是全部做完/还没开始。
|
|
1547
|
+
function planNow(t: (key: string) => string, counts: PlanCounts, entries: PlanTaskEntry[]): PlanNow {
|
|
1548
|
+
const withTitle = (label: string, title?: string): string => (title ? `${label} · ${title}` : label)
|
|
1549
|
+
if (counts.blocked > 0) {
|
|
1550
|
+
const blocked = entries.find((entry) => entry.status === 'blocked')
|
|
1551
|
+
return { state: 'blocked', text: withTitle(t('stats.blocked'), blocked?.title) }
|
|
1552
|
+
}
|
|
1553
|
+
if (counts.active > 0) {
|
|
1554
|
+
const active = entries.find((entry) => entry.status === 'in_progress')
|
|
1555
|
+
return { state: 'active', text: withTitle(t('stats.inProgress'), active?.title) }
|
|
1556
|
+
}
|
|
1557
|
+
if (counts.total > 0 && counts.done === counts.total) return { state: 'done', text: t('helpDone') }
|
|
1558
|
+
return { state: 'pending', text: t('stats.pending') }
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function PlanRing({ percent }: { percent: number | null }) {
|
|
1562
|
+
const value = percent === null ? null : Math.max(0, Math.min(100, Math.round(percent)))
|
|
1563
|
+
// 0% 时不画弧:圆头端点会在 12 点方向留下一个与「已完成」混淆的小点。
|
|
1564
|
+
const arc = value === null ? 0 : (value / 100) * PLAN_RING_CIRCUMFERENCE
|
|
1565
|
+
return h('span', {
|
|
1566
|
+
className: 'dsh-wp-planviz-ring',
|
|
1567
|
+
'aria-hidden': 'true',
|
|
1568
|
+
'data-zero': value === 0 ? 'true' : 'false',
|
|
1569
|
+
},
|
|
1570
|
+
h('svg', { viewBox: `0 0 ${PLAN_RING_SIZE} ${PLAN_RING_SIZE}`, width: PLAN_RING_SIZE, height: PLAN_RING_SIZE, focusable: 'false' },
|
|
1571
|
+
h('circle', {
|
|
1572
|
+
className: 'dsh-wp-planring-track',
|
|
1573
|
+
cx: PLAN_RING_SIZE / 2,
|
|
1574
|
+
cy: PLAN_RING_SIZE / 2,
|
|
1575
|
+
r: PLAN_RING_RADIUS,
|
|
1576
|
+
fill: 'none',
|
|
1577
|
+
strokeWidth: PLAN_RING_STROKE,
|
|
1578
|
+
}),
|
|
1579
|
+
value !== null && value > 0
|
|
1580
|
+
? h('circle', {
|
|
1581
|
+
className: 'dsh-wp-planring-arc',
|
|
1582
|
+
cx: PLAN_RING_SIZE / 2,
|
|
1583
|
+
cy: PLAN_RING_SIZE / 2,
|
|
1584
|
+
r: PLAN_RING_RADIUS,
|
|
1585
|
+
fill: 'none',
|
|
1586
|
+
strokeWidth: PLAN_RING_STROKE,
|
|
1587
|
+
strokeLinecap: 'round',
|
|
1588
|
+
strokeDasharray: `${arc} ${PLAN_RING_CIRCUMFERENCE - arc}`,
|
|
1589
|
+
transform: `rotate(-90 ${PLAN_RING_SIZE / 2} ${PLAN_RING_SIZE / 2})`,
|
|
1590
|
+
})
|
|
1591
|
+
: null,
|
|
1592
|
+
),
|
|
1593
|
+
)
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function PlanLegend({ t, counts }: { t: (key: string) => string; counts: PlanCounts }) {
|
|
1597
|
+
const items = [
|
|
1598
|
+
{ kind: 'done', label: t('stats.done'), count: counts.done },
|
|
1599
|
+
{ kind: 'active', label: t('stats.inProgress'), count: counts.active },
|
|
1600
|
+
{ kind: 'blocked', label: t('stats.blocked'), count: counts.blocked },
|
|
1601
|
+
{ kind: 'pending', label: t('stats.pending'), count: counts.pending },
|
|
1602
|
+
]
|
|
1603
|
+
return h('div', { className: 'dsh-wp-planviz-legend' },
|
|
1604
|
+
items.map((item) => h('span', {
|
|
1605
|
+
key: item.kind,
|
|
1606
|
+
className: 'dsh-wp-planviz-legend-item',
|
|
1607
|
+
'data-kind': item.kind,
|
|
1608
|
+
'data-count': String(item.count),
|
|
1609
|
+
},
|
|
1610
|
+
h('span', { className: 'dsh-wp-planviz-legend-dot', 'aria-hidden': 'true' }),
|
|
1611
|
+
`${item.label} ${item.count}`,
|
|
1612
|
+
)),
|
|
1613
|
+
)
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function PlanSegments({ counts }: { counts: PlanCounts }) {
|
|
1617
|
+
const segments = [
|
|
1618
|
+
{ kind: 'done', count: counts.done },
|
|
1619
|
+
{ kind: 'active', count: counts.active },
|
|
1620
|
+
{ kind: 'blocked', count: counts.blocked },
|
|
1621
|
+
{ kind: 'pending', count: counts.pending },
|
|
1622
|
+
]
|
|
1623
|
+
return h('div', {
|
|
1624
|
+
className: 'dsh-wp-planviz-bar',
|
|
1625
|
+
'aria-hidden': 'true',
|
|
1626
|
+
'data-total': String(counts.total),
|
|
1627
|
+
},
|
|
1628
|
+
counts.total === 0
|
|
1629
|
+
? null
|
|
1630
|
+
: segments.map((segment) => (segment.count > 0
|
|
1631
|
+
? h('span', {
|
|
1632
|
+
key: segment.kind,
|
|
1633
|
+
className: 'dsh-wp-planviz-seg',
|
|
1634
|
+
'data-kind': segment.kind,
|
|
1635
|
+
'data-count': String(segment.count),
|
|
1636
|
+
style: { flexGrow: segment.count },
|
|
1637
|
+
})
|
|
1638
|
+
: null)),
|
|
1639
|
+
)
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function PlanStepper({ t, rows }: { t: (key: string) => string; rows: PlanStageRow[] }) {
|
|
1643
|
+
if (rows.length < 1 || rows.length > 12) return null
|
|
1644
|
+
return h('div', { className: 'dsh-wp-stepper', 'aria-hidden': 'true', 'data-stages': String(rows.length) },
|
|
1645
|
+
rows.map((row) => {
|
|
1646
|
+
const reach = row.total > 0 ? ((row.done + row.active + row.blocked) / row.total) * 100 : 0
|
|
1647
|
+
const width = row.tone === 'pending' ? 0 : reach
|
|
1648
|
+
return h('span', {
|
|
1649
|
+
key: row.id,
|
|
1650
|
+
className: 'dsh-wp-stepper-seg',
|
|
1651
|
+
'data-stage-id': row.id,
|
|
1652
|
+
'data-tone': row.tone,
|
|
1653
|
+
'data-current': row.current ? 'true' : 'false',
|
|
1654
|
+
title: `${stageLabel(t, row.index)} ${row.done}/${row.total} ${t('stats.done')}`,
|
|
1655
|
+
},
|
|
1656
|
+
h('span', {
|
|
1657
|
+
className: 'dsh-wp-stepper-fill',
|
|
1658
|
+
'data-width': String(Math.round(width)),
|
|
1659
|
+
style: { width: `${width}%` },
|
|
1660
|
+
}),
|
|
1661
|
+
)
|
|
1662
|
+
}),
|
|
1663
|
+
)
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function PlanViz({ t, counts, now, rows }: { t: (key: string) => string; counts: PlanCounts; now: PlanNow; rows: PlanStageRow[] }) {
|
|
1667
|
+
const state = counts.percent === null
|
|
1668
|
+
? 'idle'
|
|
1669
|
+
: counts.blocked > 0
|
|
1670
|
+
? 'blocked'
|
|
1671
|
+
: counts.percent >= 100 ? 'done' : 'active'
|
|
1672
|
+
return h('div', {
|
|
1673
|
+
className: 'dsh-wp-planviz',
|
|
1674
|
+
'data-percent': counts.percent === null ? 'none' : String(counts.percent),
|
|
1675
|
+
'data-alarm': counts.blocked > 0 ? 'true' : 'false',
|
|
1676
|
+
'data-state': state,
|
|
1677
|
+
},
|
|
1678
|
+
h(PlanRing, { percent: counts.percent }),
|
|
1679
|
+
h('div', { className: 'dsh-wp-planviz-head' },
|
|
1680
|
+
h('span', { className: 'dsh-wp-planviz-pct' }, counts.percent === null ? '—' : `${counts.percent}%`),
|
|
1681
|
+
h('span', { className: 'dsh-wp-planviz-scope' }, `${t('scopeThis')} ${counts.done}/${counts.total} ${t('stats.done')}`),
|
|
1682
|
+
counts.blocked > 0
|
|
1683
|
+
? h('span', { className: 'dsh-wp-planviz-alarm' }, `${counts.blocked} ${t('stats.blocked')}`)
|
|
1684
|
+
: null,
|
|
1685
|
+
h('span', { className: 'dsh-wp-planviz-now', 'data-state': now.state },
|
|
1686
|
+
h('span', { className: 'dsh-wp-planviz-now-dot', 'aria-hidden': 'true' }),
|
|
1687
|
+
h('span', { className: 'dsh-wp-planviz-now-text' }, now.text),
|
|
1688
|
+
),
|
|
1689
|
+
),
|
|
1690
|
+
h(PlanSegments, { counts }),
|
|
1691
|
+
h(PlanLegend, { t, counts }),
|
|
1692
|
+
h(PlanStepper, { t, rows }),
|
|
1693
|
+
)
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// 展开后的「分步进度」:逐阶段行 + 加权总体进度,替代旧的一根计划级进度条。
|
|
1697
|
+
function PlanOverview({ t, stats, counts, rows }: { t: (key: string) => string; stats: any; counts: PlanCounts; rows: PlanStageRow[] }) {
|
|
1698
|
+
const stateOf = (tone: PlanTone): string => (tone === 'done'
|
|
1699
|
+
? t('stats.done')
|
|
1700
|
+
: tone === 'active' ? t('stats.inProgress') : tone === 'blocked' ? t('stats.blocked') : t('stats.pending'))
|
|
1701
|
+
return h('div', {
|
|
1702
|
+
className: 'dsh-wp-planoverview',
|
|
1703
|
+
'data-plan-total': String(counts.total),
|
|
1704
|
+
'data-plan-done': String(counts.done),
|
|
1705
|
+
},
|
|
1706
|
+
h('div', { className: 'dsh-wp-planoverview-head' },
|
|
1707
|
+
h('span', { className: 'dsh-wp-planoverview-title' }, t('vizStageTitle')),
|
|
1708
|
+
h('span', null, `${counts.done}/${counts.total} ${t('stats.done')}`),
|
|
1709
|
+
),
|
|
1710
|
+
h('div', { className: 'dsh-wp-planoverview-stats' },
|
|
1711
|
+
h('span', null, `${t('vizWeighted')} ${clampPct(stats?.progress ?? 0)}%`),
|
|
1712
|
+
h('span', null, `${counts.active} ${t('stats.inProgress')} · ${counts.pending} ${t('stats.pending')}`),
|
|
1713
|
+
counts.blocked > 0 ? h('span', null, `${counts.blocked} ${t('stats.blocked')}`) : null,
|
|
1714
|
+
),
|
|
1715
|
+
rows.length
|
|
1716
|
+
? h('div', { className: 'dsh-wp-planrows' },
|
|
1717
|
+
rows.map((row) => {
|
|
1718
|
+
const width = row.total > 0 ? (row.done / row.total) * 100 : 0
|
|
1719
|
+
return h('div', {
|
|
1720
|
+
key: row.id,
|
|
1721
|
+
className: 'dsh-wp-planrow',
|
|
1722
|
+
'data-tone': row.tone,
|
|
1723
|
+
'data-current': row.current ? 'true' : 'false',
|
|
1724
|
+
},
|
|
1725
|
+
h('span', { className: 'dsh-wp-planrow-dot', 'aria-hidden': 'true' }),
|
|
1726
|
+
h('span', { className: 'dsh-wp-planrow-name' }, `${stageLabel(t, row.index)}:${row.name}`),
|
|
1727
|
+
h('span', { className: 'dsh-wp-planrow-bar' },
|
|
1728
|
+
h('span', {
|
|
1729
|
+
className: 'dsh-wp-planrow-fill',
|
|
1730
|
+
'data-width': String(Math.round(width)),
|
|
1731
|
+
style: { width: `${width}%` },
|
|
1732
|
+
}),
|
|
1733
|
+
),
|
|
1734
|
+
h('span', { className: 'dsh-wp-planrow-count' }, `${row.done}/${row.total}`),
|
|
1735
|
+
h('span', { className: 'dsh-wp-planrow-state' }, stateOf(row.tone)),
|
|
1736
|
+
)
|
|
1737
|
+
}),
|
|
1738
|
+
)
|
|
1739
|
+
: null,
|
|
1740
|
+
)
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1336
1743
|
function SessionCard({ session, t, open, onToggle }: any) {
|
|
1337
1744
|
const domId = `dsh-wp-session-${session.sessionId}`
|
|
1338
1745
|
const phase = session.phase || 'pending'
|
|
1339
1746
|
const stats = session.stats || {}
|
|
1340
1747
|
const tone = phaseTone(phase)
|
|
1341
1748
|
const departments = Array.isArray(session.departments) ? session.departments : []
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
const
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1749
|
+
// 1.5.0:折叠卡摘要=图表(环 + 百分比 + 四段构成条 + 图例 + 阶段步进条);
|
|
1750
|
+
// 数字仍只来自 session.stats,与旧头部的口径完全一致,不引入第二套统计。
|
|
1751
|
+
const counts = planCounts(stats)
|
|
1752
|
+
const statuses = Array.isArray(session.statuses) ? session.statuses : []
|
|
1753
|
+
const stageRows = planStageRows(session.stages, statuses)
|
|
1754
|
+
const now = planNow(t, counts, planTaskEntries(session.stages, statuses))
|
|
1348
1755
|
return h('article', { id: domId, 'data-session-id': session.sessionId, className: `dsh-wp-session dsh-wp-tone-${tone}` },
|
|
1349
1756
|
h(Collapse, {
|
|
1350
1757
|
id: `dsh-wp-collapse-${session.sessionId}`,
|
|
@@ -1353,29 +1760,19 @@ window.__ModuleLoader__.load({
|
|
|
1353
1760
|
className: 'dsh-wp-session-collapse',
|
|
1354
1761
|
panelClassName: 'dsh-wp-session-body',
|
|
1355
1762
|
title: session.title || t('panel'),
|
|
1356
|
-
meta:
|
|
1357
|
-
h('span', { key: 'scope' }, `${t('scopeThis')} ${done}/${total} ${t('stats.done')}`),
|
|
1358
|
-
h('span', { key: 'bar', className: 'dsh-wp-mini', title: `${percent}%` },
|
|
1359
|
-
h('span', { className: `dsh-wp-mini-fill dsh-wp-mini-fill--${tone}`, style: { width: `${percent}%` } }),
|
|
1360
|
-
),
|
|
1361
|
-
h('span', { key: 'blocked' }, `${blocked} ${t('stats.blocked')}`),
|
|
1362
|
-
],
|
|
1763
|
+
meta: h(PlanViz, { t, counts, now, rows: stageRows }),
|
|
1363
1764
|
badge: h(Pill, { tone }, t(`phase.${phase}`)),
|
|
1364
1765
|
},
|
|
1365
1766
|
session.updatedAt
|
|
1366
1767
|
? h('div', { className: 'dsh-wp-meta dsh-wp-session-updated' }, `${t('updatedAt')} ${formatTime(session.updatedAt)}`)
|
|
1367
1768
|
: null,
|
|
1368
|
-
h(
|
|
1769
|
+
open === true ? h(PlanOverview, { t, stats, counts, rows: stageRows }) : null,
|
|
1369
1770
|
session.degraded ? h('div', { className: 'dsh-wp-banner dsh-wp-banner--degraded' },
|
|
1370
1771
|
h('strong', null, t('degraded')),
|
|
1371
1772
|
h('div', null, plainError(session.error || '')),
|
|
1372
1773
|
h('div', { className: 'dsh-wp-muted' }, t('degradedHint')),
|
|
1373
1774
|
) : null,
|
|
1374
|
-
h(
|
|
1375
|
-
h('div', { className: 'dsh-wp-progress-meta' },
|
|
1376
|
-
h('span', null, `${t('stats.progress')} ${clampPct(stats.progress ?? 0)}%`),
|
|
1377
|
-
h('span', null, `${stats.inProgress ?? 0} ${t('stats.inProgress')} · ${stats.pending ?? 0} ${t('stats.pending')}`),
|
|
1378
|
-
),
|
|
1775
|
+
h(Guide, { session, t }),
|
|
1379
1776
|
departments.length ? h('div', { className: 'dsh-wp-section' },
|
|
1380
1777
|
h('div', { className: 'dsh-wp-section-title' }, t('who')),
|
|
1381
1778
|
h('div', { className: 'dsh-wp-departments' },
|
|
@@ -1615,7 +2012,12 @@ window.__ModuleLoader__.load({
|
|
|
1615
2012
|
? Math.min(100, Math.max(0, percent))
|
|
1616
2013
|
: null
|
|
1617
2014
|
const arc = value === null ? 0 : (value / 100) * circumference
|
|
1618
|
-
return h('span', {
|
|
2015
|
+
return h('span', {
|
|
2016
|
+
className: 'dsh-wp-group-ring',
|
|
2017
|
+
'aria-hidden': 'true',
|
|
2018
|
+
// 0% 与「暂无数据」必须在视觉上可区分:0% 加粗零轨(≥3:1),无数则细轨 + 破折号。
|
|
2019
|
+
'data-zero': value === 0 ? 'true' : 'false',
|
|
2020
|
+
},
|
|
1619
2021
|
h('svg', { viewBox: `0 0 ${size} ${size}`, width: size, height: size, focusable: 'false' },
|
|
1620
2022
|
h('circle', {
|
|
1621
2023
|
className: 'dsh-wp-group-ring-track',
|
|
@@ -1668,8 +2070,11 @@ window.__ModuleLoader__.load({
|
|
|
1668
2070
|
const totals = useMemo(() => groupTotals(list), [list])
|
|
1669
2071
|
const groupTitle = t(isDone ? 'groupDone' : 'groupLive')
|
|
1670
2072
|
const hintText = t(isDone ? 'groupDoneHint' : 'groupLiveHint')
|
|
2073
|
+
// 用户 Q3:0/0 时不再显示「未完成 0 / 已完成 0(完成率暂无数据)」——
|
|
2074
|
+
// 环上已经是「—」,组内空态也已说明没有工作项;再重复一遍 0/0 只是噪声。
|
|
2075
|
+
// 全部做完(100%)仍保留整句,因为那时「未完成 0」正是用户要找的结论。
|
|
1671
2076
|
const summaryText = totals.percent === null
|
|
1672
|
-
?
|
|
2077
|
+
? null
|
|
1673
2078
|
: fill(t('groupSummary'), { open: totals.open, done: totals.done, pct: totals.percent })
|
|
1674
2079
|
// 读屏用完整句:计数、完成率与三态明细一起念,避免只听到一个孤零零的数字。
|
|
1675
2080
|
const ariaText = totals.total > 0
|
|
@@ -1695,7 +2100,9 @@ window.__ModuleLoader__.load({
|
|
|
1695
2100
|
collapsible ? h('span', { key: 'chevron', className: 'dsh-wp-group-chevron', 'aria-hidden': 'true' }) : null,
|
|
1696
2101
|
// 摘要独占一整行(CSS 里 flex-basis:100%):窄面板只截断摘要本身,
|
|
1697
2102
|
// 组名/徽章/环不会被挤走;完整句放在 title,悬停可读全。
|
|
1698
|
-
|
|
2103
|
+
summaryText === null
|
|
2104
|
+
? null
|
|
2105
|
+
: h('span', { key: 'summary', className: 'dsh-wp-group-sub', title: hintText }, summaryText),
|
|
1699
2106
|
]
|
|
1700
2107
|
const head = collapsible
|
|
1701
2108
|
? h('button', {
|
package/src/host.ts
CHANGED
|
@@ -325,7 +325,7 @@ export function apply(ctx: any, config: WorkplanConfig = {}) {
|
|
|
325
325
|
|
|
326
326
|
export const meta = {
|
|
327
327
|
name,
|
|
328
|
-
version: '1.
|
|
328
|
+
version: '1.5.1',
|
|
329
329
|
promptOrder: PROMPT_ORDER,
|
|
330
330
|
routes: ['/dsh-workplan/plans', '/dsh-workplan/vendor/chart.umd.js'],
|
|
331
331
|
}
|