dsh-workplan 1.5.1 → 1.6.0
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 +267 -2
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.js +256 -54
- package/lib/host.js +1 -1
- package/lib/plan.js +112 -8
- package/lib/runtime.js +207 -23
- package/package.json +1 -1
- package/src/client.ts +253 -57
- package/src/host.ts +1 -1
- package/src/plan.ts +110 -8
- package/src/runtime.ts +201 -21
package/src/client.ts
CHANGED
|
@@ -20,21 +20,26 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
"panel": "任务进度",
|
|
21
21
|
"groupLive": "进行中",
|
|
22
22
|
"groupDone": "已完成",
|
|
23
|
+
"groupEnded": "已结束",
|
|
23
24
|
"groupLiveHint": "还没结束,正在或即将推进",
|
|
24
25
|
"groupDoneHint": "已全部完成并收尾",
|
|
26
|
+
"groupEndedHint": "这一轮已经停了,没有东西在跑;说「续跑」可继续推进",
|
|
25
27
|
"groupCollapse": "收起",
|
|
26
28
|
"groupExpand": "展开",
|
|
27
29
|
"groupNeeds": "需处理",
|
|
28
30
|
"groupLiveEmpty": "没有进行中的任务",
|
|
29
31
|
"groupDoneEmpty": "还没有完成的任务",
|
|
32
|
+
"groupEndedEmpty": "没有已结束但未完成的任务",
|
|
30
33
|
"groupSummary": "未完成 {open} / 已完成 {done}(完成率 {pct}%)",
|
|
31
34
|
"groupSummaryNoRate": "未完成 {open} / 已完成 {done}(完成率暂无数据)",
|
|
32
35
|
"groupAria": "{title}组,{plans} 个计划,共 {total} 件具体工作,已完成 {done} 件,完成率 {pct}%。其中正在做 {active} 件、卡住 {blocked} 件、还没开始 {pending} 件。",
|
|
33
36
|
"groupAriaEmpty": "{title}组,{plans} 个计划,暂无具体工作项。",
|
|
34
37
|
"groupPlansLabel": "个计划",
|
|
35
|
-
"chartGroups": "
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
+
"chartGroups": "计划数量与完成情况",
|
|
39
|
+
"chartOpen": "未结束",
|
|
40
|
+
"chartDone": "已完成",
|
|
41
|
+
"chartLive": "未完成计划的完成度(按工作项)",
|
|
42
|
+
"chartMore": "另有 {n} 个未完成计划未画入图表",
|
|
38
43
|
|
|
39
44
|
"title": "任务进度:一眼看懂",
|
|
40
45
|
"subtitle": "用大白话说明:现在要做什么、谁在做、做到哪一步、接下来做什么。",
|
|
@@ -45,6 +50,9 @@ window.__ModuleLoader__.load({
|
|
|
45
50
|
"helpNobody": "还没开始",
|
|
46
51
|
"helpDone": "都做完了",
|
|
47
52
|
"helpNextDone": "没有下一步,这件事已完成",
|
|
53
|
+
"helpWaitActive": "等这一步做完,再继续下一项",
|
|
54
|
+
"helpNoPlan": "还没有安排任何工作",
|
|
55
|
+
"nowRemaining": "还剩 {n} 项",
|
|
48
56
|
"helpHint": "负责团队 = 谁来做;具体工作 = 要完成的一件事;卡住了 = 需要人处理。",
|
|
49
57
|
"handleBlocked": "先处理",
|
|
50
58
|
"resumeReady": "对主 Agent 说「续跑这个计划」,会从受阻处继续(已完成的不重跑)",
|
|
@@ -66,7 +74,8 @@ window.__ModuleLoader__.load({
|
|
|
66
74
|
"blocked": "卡住了",
|
|
67
75
|
"paused": "预算用完,可续跑",
|
|
68
76
|
"degraded": "安排失败",
|
|
69
|
-
"cancelled": "已取消"
|
|
77
|
+
"cancelled": "已取消",
|
|
78
|
+
"ended": "已结束,可续跑"
|
|
70
79
|
},
|
|
71
80
|
"status": {
|
|
72
81
|
"pending": "还没开始",
|
|
@@ -182,6 +191,7 @@ window.__ModuleLoader__.load({
|
|
|
182
191
|
"en": {
|
|
183
192
|
"groupLive": "In progress",
|
|
184
193
|
"groupDone": "Completed",
|
|
194
|
+
"groupEnded": "Ended",
|
|
185
195
|
"groupLiveHint": "Not finished yet — running or about to run",
|
|
186
196
|
"groupDoneHint": "All work finished and wrapped up",
|
|
187
197
|
"groupCollapse": "Collapse",
|
|
@@ -189,14 +199,17 @@ window.__ModuleLoader__.load({
|
|
|
189
199
|
"groupNeeds": "needs attention",
|
|
190
200
|
"groupLiveEmpty": "No work in progress",
|
|
191
201
|
"groupDoneEmpty": "Nothing completed yet",
|
|
202
|
+
"groupEndedEmpty": "No ended-but-unfinished plans",
|
|
192
203
|
"groupSummary": "Open {open} / done {done} ({pct}% complete)",
|
|
193
204
|
"groupSummaryNoRate": "Open {open} / done {done} (completion rate unavailable)",
|
|
194
205
|
"groupAria": "{title} group: {plans} plan(s), {total} work item(s), {done} done, {pct}% complete. Right now {active} in progress, {blocked} blocked, {pending} not started.",
|
|
195
206
|
"groupAriaEmpty": "{title} group: {plans} plan(s), no work items yet.",
|
|
196
207
|
"groupPlansLabel": "plan(s)",
|
|
197
|
-
"chartGroups": "Plan count
|
|
198
|
-
"
|
|
199
|
-
"
|
|
208
|
+
"chartGroups": "Plan count and completion",
|
|
209
|
+
"chartOpen": "Open",
|
|
210
|
+
"chartDone": "Completed",
|
|
211
|
+
"chartLive": "Open plans by work item",
|
|
212
|
+
"chartMore": "{n} more open plans not drawn",
|
|
200
213
|
"panel": "Task progress",
|
|
201
214
|
"title": "Task progress at a glance",
|
|
202
215
|
"subtitle": "Plain language: what this is, who is doing it, how far along it is, and what happens next.",
|
|
@@ -207,6 +220,9 @@ window.__ModuleLoader__.load({
|
|
|
207
220
|
"helpNobody": "Not started yet",
|
|
208
221
|
"helpDone": "Everything is done",
|
|
209
222
|
"helpNextDone": "Nothing next; this task is complete",
|
|
223
|
+
"helpWaitActive": "Wait for this step to finish, then continue",
|
|
224
|
+
"helpNoPlan": "No work has been scheduled yet",
|
|
225
|
+
"nowRemaining": "{n} item(s) left",
|
|
210
226
|
"helpHint": "Team = who does it; work item = one thing to finish; stuck = a person needs to help.",
|
|
211
227
|
"handleBlocked": "Fix first",
|
|
212
228
|
"resumeReady": "Tell the main agent to resume this plan; it continues from the blocked step without re-running finished work",
|
|
@@ -228,7 +244,8 @@ window.__ModuleLoader__.load({
|
|
|
228
244
|
"blocked": "Stuck",
|
|
229
245
|
"paused": "Paused (budget spent)",
|
|
230
246
|
"degraded": "Planning failed",
|
|
231
|
-
"cancelled": "Cancelled"
|
|
247
|
+
"cancelled": "Cancelled",
|
|
248
|
+
"ended": "Ended (resumable)"
|
|
232
249
|
},
|
|
233
250
|
"status": {
|
|
234
251
|
"pending": "Not started",
|
|
@@ -399,6 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
399
416
|
.dsh-wp-group::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 4px; }
|
|
400
417
|
.dsh-wp-group--live::before { background: linear-gradient(180deg, #fb923c, #f97316); }
|
|
401
418
|
.dsh-wp-group--done::before { background: linear-gradient(180deg, #38bdf8, #0284c7); }
|
|
419
|
+
.dsh-wp-group--ended::before { background: linear-gradient(180deg, #94a3b8, #64748b); }
|
|
402
420
|
/* T6 — 组头多出一行完成度摘要,底部内边距 10px → 6px,两组仍在同一屏内读完。 */
|
|
403
421
|
.dsh-wp-group-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 0 4px 6px; }
|
|
404
422
|
.dsh-wp-group-head--btn { width: 100%; background: none; border: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; border-radius: 8px; }
|
|
@@ -425,6 +443,8 @@ window.__ModuleLoader__.load({
|
|
|
425
443
|
.dsh-wp-group-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
|
|
426
444
|
.dsh-wp-group--live .dsh-wp-group-dot { background: #f97316; box-shadow: 0 0 0 4px rgba(249,115,22,.16); animation: dsh-wp-pulse 1.8s ease-in-out infinite; }
|
|
427
445
|
.dsh-wp-group--done .dsh-wp-group-dot { background: #0ea5e9; box-shadow: 0 0 0 4px rgba(14,165,233,.14); }
|
|
446
|
+
/* [1.6.0] 已结束:灰点、不脉冲——"没有东西在跑"这件事不允许用动画表达。 */
|
|
447
|
+
.dsh-wp-group--ended .dsh-wp-group-dot { background: #94a3b8; box-shadow: 0 0 0 4px rgba(148,163,184,.16); }
|
|
428
448
|
.dsh-wp-group-title { font-size: 15px; font-weight: 700; letter-spacing: .2px; color: var(--dsw-alias-label-primary, #0f172a); }
|
|
429
449
|
/* 设计稿 §6.1:橙 #f97316 / 白 ≈2.8:1、蓝 #0ea5e9 / 白 ≈2.77:1,均不足 4.5:1,
|
|
430
450
|
15px 粗体也不算法定「大字号」,所以组头一律用主文本色;品牌色只做「面」——
|
|
@@ -433,6 +453,7 @@ window.__ModuleLoader__.load({
|
|
|
433
453
|
.dsh-wp-group-count { min-width: 22px; padding: 0 7px; height: 20px; border-radius: 999px; font-size: 12px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; color: var(--dsw-alias-label-primary, #0f172a); font-variant-numeric: tabular-nums; }
|
|
434
454
|
.dsh-wp-group--live .dsh-wp-group-count { background: rgba(249,115,22,.14); box-shadow: inset 0 0 0 1px rgba(249,115,22,.34); }
|
|
435
455
|
.dsh-wp-group--done .dsh-wp-group-count { background: rgba(14,165,233,.14); box-shadow: inset 0 0 0 1px rgba(14,165,233,.34); }
|
|
456
|
+
.dsh-wp-group--ended .dsh-wp-group-count { background: rgba(148,163,184,.16); box-shadow: inset 0 0 0 1px rgba(148,163,184,.36); }
|
|
436
457
|
/* WCAG AA:错误色叠 12% 红底后,浅色 #ec1313 只有 3.70:1;与主文本色混合 20%
|
|
437
458
|
让浅色变深、深色变浅,两套主题都 ≥4.5:1(实测 5.16:1 / 5.67:1)。 */
|
|
438
459
|
.dsh-wp-group-alert { font-size: 12px; font-weight: 650; 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); padding: 1px 8px; border-radius: 999px; }
|
|
@@ -745,6 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
745
766
|
.dsh-wp-group::before { content: ''; position: absolute; left: 0; top: 14px; bottom: 14px; width: 3px; border-radius: 0 3px 3px 0; opacity: .9; }
|
|
746
767
|
.dsh-wp-group--live::before { background: linear-gradient(180deg, #fbbf24, #ea580c); }
|
|
747
768
|
.dsh-wp-group--done::before { background: linear-gradient(180deg, #7dd3fc, #0369a1); }
|
|
769
|
+
.dsh-wp-group--ended::before { background: linear-gradient(180deg, #cbd5e1, #94a3b8); }
|
|
748
770
|
.dsh-wp-group--done { opacity: .97; }
|
|
749
771
|
.dsh-wp-group-head--btn { transition: background-color .16s ease; }
|
|
750
772
|
.dsh-wp-group-head--btn:hover { background: rgba(148,163,184,.10); }
|
|
@@ -976,12 +998,33 @@ window.__ModuleLoader__.load({
|
|
|
976
998
|
|
|
977
999
|
function phaseTone(phase: any) {
|
|
978
1000
|
if (phase === 'blocked') return 'blocked'
|
|
1001
|
+
/*
|
|
1002
|
+
* [1.6.0] `ended` = 这一轮停了,但计划没做完。它既不是成功(done),也不能
|
|
1003
|
+
* 冒充正在干活(active);用中性 idle 色,卡在哪由二级徽标 suspendedPhase 说明。
|
|
1004
|
+
*/
|
|
1005
|
+
if (phase === 'ended') return 'idle'
|
|
979
1006
|
if (phase === 'degraded' || phase === 'cancelled' || phase === 'paused') return 'warn'
|
|
980
1007
|
if (phase === 'integrating' || phase === 'done') return 'done'
|
|
981
1008
|
if (phase === 'executing' || phase === 'planning') return 'active'
|
|
982
1009
|
return 'idle'
|
|
983
1010
|
}
|
|
984
1011
|
|
|
1012
|
+
/*
|
|
1013
|
+
* [1.6.0] 「已结束」徽标要把「停在哪儿」一起说出来。
|
|
1014
|
+
*
|
|
1015
|
+
* 只说「已结束,可续跑」,用户不知道是预算用完、卡住了、还是压根没开始;
|
|
1016
|
+
* 只说原阶段(「正在做」),又会像事故截图那样暗示还在跑。两段式表达:
|
|
1017
|
+
* 终态 + 原阶段,一次说清两个事实。
|
|
1018
|
+
*/
|
|
1019
|
+
function phaseLabel(phase: any, session: any, t: any) {
|
|
1020
|
+
const base = t(`phase.${phase}`)
|
|
1021
|
+
if (phase !== 'ended') return base
|
|
1022
|
+
const suspended = String(session?.suspendedPhase ?? '')
|
|
1023
|
+
if (!suspended) return base
|
|
1024
|
+
const detail = t(`phase.${suspended}`)
|
|
1025
|
+
return detail && detail !== base ? `${base} · ${detail}` : base
|
|
1026
|
+
}
|
|
1027
|
+
|
|
985
1028
|
function allEntries(session: any) {
|
|
986
1029
|
const byId = new Map((Array.isArray(session?.statuses) ? session.statuses : []).map((item: any) => [item.subtaskId, item]))
|
|
987
1030
|
const entries: any[] = []
|
|
@@ -1028,14 +1071,21 @@ window.__ModuleLoader__.load({
|
|
|
1028
1071
|
}
|
|
1029
1072
|
|
|
1030
1073
|
function workerText(session: any, t: any) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1074
|
+
/*
|
|
1075
|
+
* [1.5.2] 与 `planNow` 同序:卡住优先。
|
|
1076
|
+
*
|
|
1077
|
+
* 旧顺序是「正在做」优先,而卡片右上角的「现在」是「卡住了」优先,
|
|
1078
|
+
* 于是同一屏一边说「现在卡住了 T1」、一边说「正在做 T2」(T5 C12)。
|
|
1079
|
+
* 卡住需要人介入,两个位置都应先报它。
|
|
1080
|
+
*/
|
|
1035
1081
|
const blocked = findEntry(session, 'blocked')
|
|
1036
1082
|
if (blocked) {
|
|
1037
1083
|
return `${deptName(t, blocked.subtask.department)} · ${blocked.subtask.title}(${t('status.blocked')})`
|
|
1038
1084
|
}
|
|
1085
|
+
const active = findEntry(session, 'in_progress')
|
|
1086
|
+
if (active) {
|
|
1087
|
+
return `${deptName(t, active.subtask.department)} · ${active.subtask.title}`
|
|
1088
|
+
}
|
|
1039
1089
|
const stats = session.stats || {}
|
|
1040
1090
|
if ((stats.total ?? 0) > 0 && (stats.done ?? 0) >= (stats.total ?? 0)) return t('helpDone')
|
|
1041
1091
|
return t('helpNobody')
|
|
@@ -1061,11 +1111,22 @@ window.__ModuleLoader__.load({
|
|
|
1061
1111
|
function nextText(session: any, t: any) {
|
|
1062
1112
|
const blocked = findEntry(session, 'blocked')
|
|
1063
1113
|
if (blocked) return `${t('handleBlocked')}:${deptName(t, blocked.subtask.department)} · ${blocked.subtask.title}${resumeHint(session, t)}`
|
|
1114
|
+
/*
|
|
1115
|
+
* [1.5.2] 「没有下一步」只允许出现在真正全部完成之后。
|
|
1116
|
+
*
|
|
1117
|
+
* 旧实现找不到 pending 就直接 return「没有下一步,这件事已完成」,于是
|
|
1118
|
+
* · 单任务计划刚进入 in_progress(环 0%)→ 同一张卡宣布「已完成」(T5 C2);
|
|
1119
|
+
* · 降级计划 0/0 → 一边「安排失败」一边「已完成」(T5 C11)。
|
|
1120
|
+
* 现在先按计数判终态,再区分「等当前这一步」「根本没有计划」。
|
|
1121
|
+
*/
|
|
1122
|
+
const counts = planCounts(session.stats || {})
|
|
1123
|
+
if (counts.total > 0 && counts.done === counts.total) return t('helpDone')
|
|
1124
|
+
if (counts.total === 0) return t('helpNoPlan')
|
|
1064
1125
|
const next = findEntry(session, 'pending') || allEntries(session).find((entry: any) => !entry.item)
|
|
1065
1126
|
if (next) return `${deptName(t, next.subtask.department)} · ${next.subtask.title}`
|
|
1066
|
-
const
|
|
1067
|
-
if (
|
|
1068
|
-
return t('
|
|
1127
|
+
const active = findEntry(session, 'in_progress')
|
|
1128
|
+
if (active) return `${t('helpWaitActive')}:${deptName(t, active.subtask.department)} · ${active.subtask.title}`
|
|
1129
|
+
return t('helpWaitActive')
|
|
1069
1130
|
}
|
|
1070
1131
|
|
|
1071
1132
|
function progressText(item: any, t: any) {
|
|
@@ -1339,15 +1400,19 @@ window.__ModuleLoader__.load({
|
|
|
1339
1400
|
const done = items.filter(({ item }: any) => item?.status === 'done').length
|
|
1340
1401
|
const blocked = items.filter(({ item }: any) => item?.status === 'blocked').length
|
|
1341
1402
|
const inProgress = items.some(({ item }: any) => item?.status === 'in_progress')
|
|
1342
|
-
|
|
1403
|
+
// [1.5.2] 与 `planStageRows` 同一条规则:有 done 就是 active,不再只在
|
|
1404
|
+
// 有 in_progress 时才算「正在做」。旧规则让同一阶段在概览行显示「正在做」、
|
|
1405
|
+
// 在阶段卡显示「还没开始」(T5 C9)。
|
|
1406
|
+
const tone = blocked > 0 ? 'blocked' : items.length > 0 && done === items.length ? 'done' : inProgress || done > 0 ? 'active' : 'idle'
|
|
1343
1407
|
// Same tone/counts as before; only the presentation moved into the
|
|
1344
1408
|
// collapsed summary row (done/total + state pill), so the numbers keep one
|
|
1345
1409
|
// source: this step's own items from `statuses`.
|
|
1346
1410
|
const stateText = blocked > 0
|
|
1347
1411
|
? `${blocked} ${t('stats.blocked')}`
|
|
1348
|
-
: done === items.length
|
|
1412
|
+
: items.length > 0 && done === items.length
|
|
1349
1413
|
? t('helpDone')
|
|
1350
|
-
: inProgress ? t('phase.executing')
|
|
1414
|
+
: inProgress ? t('phase.executing')
|
|
1415
|
+
: done > 0 ? fill(t('nowRemaining'), { n: items.length - done }) : t('phase.pending')
|
|
1351
1416
|
return h('section', { className: `dsh-wp-stage dsh-wp-tone-${tone}` },
|
|
1352
1417
|
h(Collapse, {
|
|
1353
1418
|
className: 'dsh-wp-stage-collapse',
|
|
@@ -1463,6 +1528,18 @@ window.__ModuleLoader__.load({
|
|
|
1463
1528
|
return Math.max(0, Math.round(number))
|
|
1464
1529
|
}
|
|
1465
1530
|
|
|
1531
|
+
/*
|
|
1532
|
+
* [1.5.2] 计划级百分比的唯一算式。
|
|
1533
|
+
*
|
|
1534
|
+
* 1.5.1 及以前,同一页面上至少有三处各自 `Math.round(done / total * 100)`:
|
|
1535
|
+
* 卡片环(planCounts)、组头环(groupTotals)、按任务进度条(TaskBars / 总览柱状图)。
|
|
1536
|
+
* 三份实现口径相同只是巧合,任何一处单独改动都会让「同一张卡两个百分比」重现。
|
|
1537
|
+
* 现在只有这一个函数,其它地方一律调用它。
|
|
1538
|
+
*/
|
|
1539
|
+
function planPercent(done: number, total: number): number | null {
|
|
1540
|
+
return total > 0 ? Math.round((done / total) * 100) : null
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1466
1543
|
// 与 groupTotals 同一套口径:先夹到 [0,total],再让「正在做/还没开始」
|
|
1467
1544
|
// 吃掉剩余量,四段之和恒等于总数(构成条不会溢出或出现负宽度)。
|
|
1468
1545
|
function planCounts(stats: any): PlanCounts {
|
|
@@ -1472,8 +1549,44 @@ window.__ModuleLoader__.load({
|
|
|
1472
1549
|
const blocked = Math.min(planCount(source.blocked), total - done)
|
|
1473
1550
|
const active = Math.min(planCount(source.inProgress), total - done - blocked)
|
|
1474
1551
|
const pending = Math.max(0, total - done - blocked - active)
|
|
1475
|
-
|
|
1476
|
-
|
|
1552
|
+
return { total, done, active, blocked, pending, percent: planPercent(done, total) }
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/*
|
|
1556
|
+
* [1.5.2] 完成语义的第二道保险:状态文案必须服从计数事实。
|
|
1557
|
+
*
|
|
1558
|
+
* 后端已保证「工作项全部 done ⇒ phase=done」(`reconcilePhase`)。这里再兜一层,
|
|
1559
|
+
* 让任何历史载荷、旧缓存或第三方构造的 payload 都不可能渲染出「100% + 正在
|
|
1560
|
+
* 收尾」:计数齐全且全部完成时一律按「已完成」呈现;反过来,phase 说 done
|
|
1561
|
+
* 但计数里还有没做完的项时,按真实计数降级,避免「已完成下面挂着没做完的活」。
|
|
1562
|
+
*
|
|
1563
|
+
* 计数未知(total=0,例如已取消或尚未拆解)时不动原始 phase——此时没有事实
|
|
1564
|
+
* 可以覆盖它,硬改会把「进行中/已完成」的分组依据一起抹掉。
|
|
1565
|
+
* 「integrating」只在计数证明全部完成时升级为 done,否则保留原语义(回放旧会话)。
|
|
1566
|
+
*/
|
|
1567
|
+
function planPhaseOf(rawPhase: any, counts: PlanCounts): string {
|
|
1568
|
+
const phase = String(rawPhase ?? 'pending')
|
|
1569
|
+
if (counts.total > 0) {
|
|
1570
|
+
if (counts.done === counts.total) return 'done'
|
|
1571
|
+
if (phase === 'done') {
|
|
1572
|
+
if (counts.blocked > 0) return 'blocked'
|
|
1573
|
+
if (counts.active > 0) return 'executing'
|
|
1574
|
+
return 'pending'
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return phase
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/*
|
|
1581
|
+
* [1.5.2] 加权总进度的展示口径:未全部完成时封顶 99。
|
|
1582
|
+
*
|
|
1583
|
+
* 服务端 `deriveAggregates` 已做同样封顶;这里再算一次是为了让旧载荷/手工
|
|
1584
|
+
* fixture 也守同一条不变量——「总体进度 100%」绝不能与「还有 N 项没完成」同屏。
|
|
1585
|
+
*/
|
|
1586
|
+
function planWeightedPercent(stats: any, counts: PlanCounts): number {
|
|
1587
|
+
const weighted = clampPct(stats?.progress ?? 0)
|
|
1588
|
+
if (counts.total > 0 && counts.done === counts.total) return 100
|
|
1589
|
+
return Math.min(99, weighted)
|
|
1477
1590
|
}
|
|
1478
1591
|
|
|
1479
1592
|
function planStatusMap(statuses: any): Map<string, any> {
|
|
@@ -1555,6 +1668,16 @@ window.__ModuleLoader__.load({
|
|
|
1555
1668
|
return { state: 'active', text: withTitle(t('stats.inProgress'), active?.title) }
|
|
1556
1669
|
}
|
|
1557
1670
|
if (counts.total > 0 && counts.done === counts.total) return { state: 'done', text: t('helpDone') }
|
|
1671
|
+
/*
|
|
1672
|
+
* [1.5.2] 部分完成不再落到「还没开始」。
|
|
1673
|
+
*
|
|
1674
|
+
* `done>0` 但没有 blocked、也没有 in_progress 时(预算把剩余项重置成
|
|
1675
|
+
* pending,或剩余项还没轮到),旧代码直接返回 `stats.pending`「还没开始」,
|
|
1676
|
+
* 与同一张卡上的「1/2 做好了」和 50% 直接冲突(T5 C5)。
|
|
1677
|
+
*/
|
|
1678
|
+
if (counts.done > 0) {
|
|
1679
|
+
return { state: 'active', text: fill(t('nowRemaining'), { n: counts.total - counts.done }) }
|
|
1680
|
+
}
|
|
1558
1681
|
return { state: 'pending', text: t('stats.pending') }
|
|
1559
1682
|
}
|
|
1560
1683
|
|
|
@@ -1643,7 +1766,9 @@ window.__ModuleLoader__.load({
|
|
|
1643
1766
|
if (rows.length < 1 || rows.length > 12) return null
|
|
1644
1767
|
return h('div', { className: 'dsh-wp-stepper', 'aria-hidden': 'true', 'data-stages': String(rows.length) },
|
|
1645
1768
|
rows.map((row) => {
|
|
1646
|
-
|
|
1769
|
+
// [1.5.2] 只按 done 计。旧式把 blocked/active 也算「触达」,于是环 50%
|
|
1770
|
+
// 时阶段条已经全满(T5 C7)——同一张卡两个「完成度」。
|
|
1771
|
+
const reach = row.total > 0 ? (row.done / row.total) * 100 : 0
|
|
1647
1772
|
const width = row.tone === 'pending' ? 0 : reach
|
|
1648
1773
|
return h('span', {
|
|
1649
1774
|
key: row.id,
|
|
@@ -1664,11 +1789,20 @@ window.__ModuleLoader__.load({
|
|
|
1664
1789
|
}
|
|
1665
1790
|
|
|
1666
1791
|
function PlanViz({ t, counts, now, rows }: { t: (key: string) => string; counts: PlanCounts; now: PlanNow; rows: PlanStageRow[] }) {
|
|
1792
|
+
/*
|
|
1793
|
+
* [1.5.2] data-state 也是「完成语义」的一个读者。
|
|
1794
|
+
*
|
|
1795
|
+
* 旧实现只看 percent:0% 的 pending 卡拿到 data-state="active",
|
|
1796
|
+
* 而 100% 的卡即使 phase 说 integrating 也是 "done"(T5 C13/C1)。
|
|
1797
|
+
* 现在与胶囊、分组、环共用计数:全 done 才 done,0 且有未完成才 idle。
|
|
1798
|
+
*/
|
|
1667
1799
|
const state = counts.percent === null
|
|
1668
1800
|
? 'idle'
|
|
1669
1801
|
: counts.blocked > 0
|
|
1670
1802
|
? 'blocked'
|
|
1671
|
-
: counts.
|
|
1803
|
+
: counts.total > 0 && counts.done === counts.total
|
|
1804
|
+
? 'done'
|
|
1805
|
+
: counts.done > 0 || counts.active > 0 ? 'active' : 'idle'
|
|
1672
1806
|
return h('div', {
|
|
1673
1807
|
className: 'dsh-wp-planviz',
|
|
1674
1808
|
'data-percent': counts.percent === null ? 'none' : String(counts.percent),
|
|
@@ -1708,7 +1842,7 @@ window.__ModuleLoader__.load({
|
|
|
1708
1842
|
h('span', null, `${counts.done}/${counts.total} ${t('stats.done')}`),
|
|
1709
1843
|
),
|
|
1710
1844
|
h('div', { className: 'dsh-wp-planoverview-stats' },
|
|
1711
|
-
h('span', null, `${t('vizWeighted')} ${
|
|
1845
|
+
h('span', null, `${t('vizWeighted')} ${planWeightedPercent(stats, counts)}%`),
|
|
1712
1846
|
h('span', null, `${counts.active} ${t('stats.inProgress')} · ${counts.pending} ${t('stats.pending')}`),
|
|
1713
1847
|
counts.blocked > 0 ? h('span', null, `${counts.blocked} ${t('stats.blocked')}`) : null,
|
|
1714
1848
|
),
|
|
@@ -1742,13 +1876,18 @@ window.__ModuleLoader__.load({
|
|
|
1742
1876
|
|
|
1743
1877
|
function SessionCard({ session, t, open, onToggle }: any) {
|
|
1744
1878
|
const domId = `dsh-wp-session-${session.sessionId}`
|
|
1745
|
-
const phase = session.phase || 'pending'
|
|
1746
1879
|
const stats = session.stats || {}
|
|
1747
|
-
const tone = phaseTone(phase)
|
|
1748
|
-
const departments = Array.isArray(session.departments) ? session.departments : []
|
|
1749
1880
|
// 1.5.0:折叠卡摘要=图表(环 + 百分比 + 四段构成条 + 图例 + 阶段步进条);
|
|
1750
1881
|
// 数字仍只来自 session.stats,与旧头部的口径完全一致,不引入第二套统计。
|
|
1751
1882
|
const counts = planCounts(stats)
|
|
1883
|
+
/*
|
|
1884
|
+
* [1.5.2] 卡片的色调与胶囊文案也走统一对账:只要计数显示全部完成,
|
|
1885
|
+
* 胶囊就必须是「已完成」,视觉档位也必须是 done。截图里那张卡之所以
|
|
1886
|
+
* 「100% + 正在收尾 + 留在进行中组」,就是因为这三个读者各读各的字段。
|
|
1887
|
+
*/
|
|
1888
|
+
const phase = planPhaseOf(session.phase || 'pending', counts)
|
|
1889
|
+
const tone = phaseTone(phase)
|
|
1890
|
+
const departments = Array.isArray(session.departments) ? session.departments : []
|
|
1752
1891
|
const statuses = Array.isArray(session.statuses) ? session.statuses : []
|
|
1753
1892
|
const stageRows = planStageRows(session.stages, statuses)
|
|
1754
1893
|
const now = planNow(t, counts, planTaskEntries(session.stages, statuses))
|
|
@@ -1761,7 +1900,7 @@ window.__ModuleLoader__.load({
|
|
|
1761
1900
|
panelClassName: 'dsh-wp-session-body',
|
|
1762
1901
|
title: session.title || t('panel'),
|
|
1763
1902
|
meta: h(PlanViz, { t, counts, now, rows: stageRows }),
|
|
1764
|
-
badge: h(Pill, { tone },
|
|
1903
|
+
badge: h(Pill, { tone }, phaseLabel(phase, session, t)),
|
|
1765
1904
|
},
|
|
1766
1905
|
session.updatedAt
|
|
1767
1906
|
? h('div', { className: 'dsh-wp-meta dsh-wp-session-updated' }, `${t('updatedAt')} ${formatTime(session.updatedAt)}`)
|
|
@@ -1865,7 +2004,7 @@ window.__ModuleLoader__.load({
|
|
|
1865
2004
|
charts.push(new lib(canvasA, {
|
|
1866
2005
|
type: 'doughnut',
|
|
1867
2006
|
data: {
|
|
1868
|
-
labels: [t('
|
|
2007
|
+
labels: [t('chartOpen'), t('chartDone')],
|
|
1869
2008
|
datasets: [{ data: [liveCount, doneCount], backgroundColor: [liveFill, doneFill], borderWidth: 0, hoverOffset: 5 }],
|
|
1870
2009
|
},
|
|
1871
2010
|
options: {
|
|
@@ -1920,16 +2059,16 @@ window.__ModuleLoader__.load({
|
|
|
1920
2059
|
// 「无 canvas → 不建图 → 不 ready → 无 canvas」的死循环(T3 实测
|
|
1921
2060
|
// 探针输出 canvasNodes:0 / fallbackBars:1)。改为并存:
|
|
1922
2061
|
// fallback 在 ready 前可见,ready 后由 CSS 隐藏,canvas 反向隐藏。
|
|
1923
|
-
h('div', { className: 'dsh-wp-chart-canvas' }, h('canvas', { 'data-chart': 'groups', role: 'img', 'aria-label': `${t('
|
|
2062
|
+
h('div', { className: 'dsh-wp-chart-canvas' }, h('canvas', { 'data-chart': 'groups', role: 'img', 'aria-label': `${t('chartOpen')} ${liveCount} / ${t('chartDone')} ${doneCount}` })),
|
|
1924
2063
|
chartState !== 'ready'
|
|
1925
2064
|
? h('div', { className: 'dsh-wp-fallback' },
|
|
1926
|
-
h('div', { className: 'dsh-wp-fallback-bar', role: 'img', 'aria-label': `${t('
|
|
2065
|
+
h('div', { className: 'dsh-wp-fallback-bar', role: 'img', 'aria-label': `${t('chartOpen')} ${liveCount} / ${t('chartDone')} ${doneCount}` },
|
|
1927
2066
|
h('span', { className: 'dsh-wp-fallback-live', style: { width: `${livePct}%` } }),
|
|
1928
2067
|
h('span', { className: 'dsh-wp-fallback-done', style: { width: `${donePct}%` } }),
|
|
1929
2068
|
),
|
|
1930
2069
|
h('div', { className: 'dsh-wp-fallback-legend' },
|
|
1931
|
-
h('span', null, h('i', { className: 'dsh-wp-fallback-key dsh-wp-fallback-key--live' }), `${t('
|
|
1932
|
-
h('span', null, h('i', { className: 'dsh-wp-fallback-key dsh-wp-fallback-key--done' }), `${t('
|
|
2070
|
+
h('span', null, h('i', { className: 'dsh-wp-fallback-key dsh-wp-fallback-key--live' }), `${t('chartOpen')} ${liveCount}`),
|
|
2071
|
+
h('span', null, h('i', { className: 'dsh-wp-fallback-key dsh-wp-fallback-key--done' }), `${t('chartDone')} ${doneCount}`),
|
|
1933
2072
|
),
|
|
1934
2073
|
)
|
|
1935
2074
|
: null,
|
|
@@ -1989,7 +2128,7 @@ window.__ModuleLoader__.load({
|
|
|
1989
2128
|
total,
|
|
1990
2129
|
done,
|
|
1991
2130
|
open: total - done,
|
|
1992
|
-
percent:
|
|
2131
|
+
percent: planPercent(done, total),
|
|
1993
2132
|
active: Math.max(0, active),
|
|
1994
2133
|
pending: Math.max(0, pending),
|
|
1995
2134
|
blocked: Math.max(0, blocked),
|
|
@@ -2046,18 +2185,25 @@ window.__ModuleLoader__.load({
|
|
|
2046
2185
|
}
|
|
2047
2186
|
|
|
2048
2187
|
function SessionGroup({ group, list, hasLive, urgent, t, openSessionId, setOpenSessionId }: any) {
|
|
2188
|
+
/*
|
|
2189
|
+
* [1.6.0] 三个分组共用一套组件:live(真的在跑)、ended(这一轮停了但没做完)、
|
|
2190
|
+
* done(全部完成)。`ended` 是本次事故的修复核心——它必须是一个**独立分组**,
|
|
2191
|
+
* 而不是被塞回「进行中」:会话已经结束的事实不允许用「进行中」这三个字表达。
|
|
2192
|
+
*/
|
|
2049
2193
|
const isDone = group === 'done'
|
|
2194
|
+
const isEnded = group === 'ended'
|
|
2195
|
+
const keySuffix = isDone ? 'Done' : isEnded ? 'Ended' : 'Live'
|
|
2050
2196
|
// 设计稿 §6.4 #1/#6:已完成组在有进行中任务时默认折叠,避免历史任务抢走
|
|
2051
2197
|
// 「进行中」的视觉权重;没有进行中任务时默认展开,空的进度栏仍是空的。
|
|
2052
|
-
//
|
|
2053
|
-
const [open, setOpen] = useState(!(isDone && hasLive))
|
|
2198
|
+
// [1.6.0] 「已结束」组同理:还有东西在跑时收起来,只留组头一行结论。
|
|
2199
|
+
const [open, setOpen] = useState(!((isDone || isEnded) && hasLive))
|
|
2054
2200
|
// §7.3「不打扰」的自动展开:只在这两种情形打开一次,不做反复弹开——
|
|
2055
2201
|
// ① 进行中清零(最后一个任务完成);② 用户正展开着的卡片被移进本组。
|
|
2056
2202
|
// 依赖都是布尔值,轮询刷新不会重复触发。
|
|
2057
2203
|
const holdsOpenCard = list.some((session: any) => session.sessionId === openSessionId)
|
|
2058
2204
|
useEffect(() => { if (!hasLive) setOpen(true) }, [hasLive])
|
|
2059
2205
|
useEffect(() => { if (holdsOpenCard) setOpen(true) }, [holdsOpenCard])
|
|
2060
|
-
const collapsible = isDone && list.length > 0
|
|
2206
|
+
const collapsible = (isDone || isEnded) && list.length > 0
|
|
2061
2207
|
// 折叠只隐藏、不卸载:`hidden` 必须是布尔 true,不能传空字符串——
|
|
2062
2208
|
// React 对 `hidden` 走 DOM 布尔属性赋值,`el.hidden = ''` 会得到 false,
|
|
2063
2209
|
// 只剩 CSS 类在遮挡;那样「折叠态 DOM 保留 + 无障碍语义」就不成立。
|
|
@@ -2068,8 +2214,8 @@ window.__ModuleLoader__.load({
|
|
|
2068
2214
|
// T6:组头完成度摘要 + 零依赖进度环。数值只来自 groupTotals(list),
|
|
2069
2215
|
// 先用纯文本给出「进行中 N / 已完成 M(完成率 x%)」,不点开、不看颜色也能读出进度。
|
|
2070
2216
|
const totals = useMemo(() => groupTotals(list), [list])
|
|
2071
|
-
const groupTitle = t(
|
|
2072
|
-
const hintText = t(
|
|
2217
|
+
const groupTitle = t(`group${keySuffix}`)
|
|
2218
|
+
const hintText = t(`group${keySuffix}Hint`)
|
|
2073
2219
|
// 用户 Q3:0/0 时不再显示「未完成 0 / 已完成 0(完成率暂无数据)」——
|
|
2074
2220
|
// 环上已经是「—」,组内空态也已说明没有工作项;再重复一遍 0/0 只是噪声。
|
|
2075
2221
|
// 全部做完(100%)仍保留整句,因为那时「未完成 0」正是用户要找的结论。
|
|
@@ -2136,7 +2282,7 @@ window.__ModuleLoader__.load({
|
|
|
2136
2282
|
open: openSessionId === session.sessionId,
|
|
2137
2283
|
onToggle: (next: boolean) => setOpenSessionId(next ? session.sessionId : null),
|
|
2138
2284
|
})))
|
|
2139
|
-
: h('div', { className: 'dsh-wp-group-empty' }, t(
|
|
2285
|
+
: h('div', { className: 'dsh-wp-group-empty' }, t(`group${keySuffix}Empty`)),
|
|
2140
2286
|
)
|
|
2141
2287
|
}
|
|
2142
2288
|
|
|
@@ -2209,10 +2355,11 @@ window.__ModuleLoader__.load({
|
|
|
2209
2355
|
h('div', { className: 'dsh-wp-section-title' }, t('taskChart')),
|
|
2210
2356
|
(Array.isArray(sessions) ? sessions : []).map((session: any) => {
|
|
2211
2357
|
const stats = session.stats || {}
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2214
|
-
const percent =
|
|
2215
|
-
const
|
|
2358
|
+
const counts = planCounts(stats)
|
|
2359
|
+
// 同一套计数与对账:柱状图的百分比、文案、色调都不得自成一套。
|
|
2360
|
+
const percent = counts.percent ?? 0
|
|
2361
|
+
const phase = planPhaseOf(session.phase || 'pending', counts)
|
|
2362
|
+
const tone = phaseTone(phase)
|
|
2216
2363
|
return h('button', {
|
|
2217
2364
|
key: session.sessionId,
|
|
2218
2365
|
type: 'button',
|
|
@@ -2222,13 +2369,13 @@ window.__ModuleLoader__.load({
|
|
|
2222
2369
|
},
|
|
2223
2370
|
h('div', { className: 'dsh-wp-bar-head' },
|
|
2224
2371
|
h('span', { className: 'dsh-wp-bar-title' }, session.title || t('panel')),
|
|
2225
|
-
h('span', { className: 'dsh-wp-bar-value' }, `${done}/${total}`),
|
|
2372
|
+
h('span', { className: 'dsh-wp-bar-value' }, `${counts.done}/${counts.total}`),
|
|
2226
2373
|
),
|
|
2227
2374
|
h('div', { className: 'dsh-wp-bar-track' },
|
|
2228
2375
|
h('div', { className: `dsh-wp-bar-fill dsh-wp-bar-fill--${tone}`, style: { width: `${percent}%` } }),
|
|
2229
2376
|
),
|
|
2230
2377
|
h('div', { className: 'dsh-wp-bar-meta' },
|
|
2231
|
-
h('span', null,
|
|
2378
|
+
h('span', null, phaseLabel(phase, session, t)),
|
|
2232
2379
|
h('span', null, `${percent}%`),
|
|
2233
2380
|
),
|
|
2234
2381
|
)
|
|
@@ -2280,10 +2427,25 @@ window.__ModuleLoader__.load({
|
|
|
2280
2427
|
}, [activeSessions])
|
|
2281
2428
|
const groups = useMemo(() => {
|
|
2282
2429
|
const live: any[] = []
|
|
2430
|
+
const ended: any[] = []
|
|
2283
2431
|
const finished: any[] = []
|
|
2284
2432
|
const urgentPhases = ['blocked', 'degraded', 'paused']
|
|
2433
|
+
/*
|
|
2434
|
+
* [1.5.2] 分组也按对账后的 phase,而不是 payload 里的原始 phase。
|
|
2435
|
+
*
|
|
2436
|
+
* 截图那张卡之所以停在「进行中」组,就是因为这里只读 `session.phase`:
|
|
2437
|
+
* 14/14、100% 的计划带着 integrating 被塞进 live 组,组头于是出现
|
|
2438
|
+
* 「未完成 0 / 已完成 14(完成率 100%)」却仍标着「进行中」。
|
|
2439
|
+
* `planPhaseOf` 让分组、组头计数、卡片胶囊共用同一个结论。
|
|
2440
|
+
*
|
|
2441
|
+
* [1.6.0] 新增 ended 分组:宿主已经 `turn/end`(会话这一轮结束、没有任何
|
|
2442
|
+
* 东西在跑),而计划还没做完的,一律进「已结束」,绝不进「进行中」。
|
|
2443
|
+
* 这正是本次事故的现象:会话结束 4 小时后仍显示「进行中」。
|
|
2444
|
+
*/
|
|
2285
2445
|
for (const session of activeSessions) {
|
|
2286
|
-
|
|
2446
|
+
const phase = planPhaseOf(session.phase, planCounts(session.stats))
|
|
2447
|
+
if (phase === 'done') finished.push(session)
|
|
2448
|
+
else if (phase === 'ended') ended.push(session)
|
|
2287
2449
|
else live.push(session)
|
|
2288
2450
|
}
|
|
2289
2451
|
/*
|
|
@@ -2300,11 +2462,19 @@ window.__ModuleLoader__.load({
|
|
|
2300
2462
|
integrating: 3,
|
|
2301
2463
|
planning: 4,
|
|
2302
2464
|
pending: 5,
|
|
2465
|
+
ended: 6,
|
|
2303
2466
|
}
|
|
2304
|
-
const rank = (session: any) => phaseRank[session.phase] ?? 5
|
|
2467
|
+
const rank = (session: any) => phaseRank[planPhaseOf(session.phase, planCounts(session.stats))] ?? 5
|
|
2305
2468
|
live.sort((a: any, b: any) => (rank(a) - rank(b))
|
|
2306
2469
|
|| ((Number(b.createdAt) || 0) - (Number(a.createdAt) || 0))
|
|
2307
2470
|
|| String(a.sessionId).localeCompare(String(b.sessionId)))
|
|
2471
|
+
/*
|
|
2472
|
+
* [1.6.0] 已结束组按「最近结束在前」:`endedAt` 是宿主 turn/end 的时间戳
|
|
2473
|
+
* (后端下发),比 updatedAt 更准确地表达「什么时候停的」。
|
|
2474
|
+
*/
|
|
2475
|
+
const endedAt = (session: any) => Number(session.endedAt) || Number(session.updatedAt) || Number(session.createdAt) || 0
|
|
2476
|
+
ended.sort((a: any, b: any) => (endedAt(b) - endedAt(a))
|
|
2477
|
+
|| ((Number(b.createdAt) || 0) - (Number(a.createdAt) || 0)))
|
|
2308
2478
|
/*
|
|
2309
2479
|
* 已完成组按「最近完成在前」(§3.3 / C8):优先取子任务
|
|
2310
2480
|
* statuses[].finishedAt 的最大值(忽略 null / 非数字),缺失则退回
|
|
@@ -2327,15 +2497,25 @@ window.__ModuleLoader__.load({
|
|
|
2327
2497
|
}
|
|
2328
2498
|
finished.sort((a: any, b: any) => (finishedAt(b) - finishedAt(a))
|
|
2329
2499
|
|| ((Number(b.createdAt) || 0) - (Number(a.createdAt) || 0)))
|
|
2330
|
-
return {
|
|
2500
|
+
return {
|
|
2501
|
+
live,
|
|
2502
|
+
ended,
|
|
2503
|
+
finished,
|
|
2504
|
+
urgent: live.filter((session: any) => urgentPhases.includes(session.phase)).length,
|
|
2505
|
+
/*
|
|
2506
|
+
* [1.6.0] 「已结束」组也需要「需处理」角标:卡住的计划停了仍然是卡住的,
|
|
2507
|
+
* 判定用后端下发的 suspendedPhase(原派发阶段),而不是被收敛后的 ended。
|
|
2508
|
+
*/
|
|
2509
|
+
urgentEnded: ended.filter((session: any) => urgentPhases.includes(String(session.suspendedPhase ?? ''))).length,
|
|
2510
|
+
}
|
|
2331
2511
|
}, [activeSessions])
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
const totalCount = session.stats?.total ?? 0
|
|
2512
|
+
// [1.6.0] 图表口径 = 未结束计划(进行中 + 已结束未完成),与环图标签一致。
|
|
2513
|
+
const liveBars = useMemo(() => [...groups.live, ...groups.ended].slice(0, 6).map((session: any) => {
|
|
2335
2514
|
const raw = String(session.title || t('panel'))
|
|
2336
2515
|
return {
|
|
2337
2516
|
label: raw.length > 14 ? `${raw.slice(0, 14)}…` : raw,
|
|
2338
|
-
|
|
2517
|
+
// [1.5.2] 与卡片/组头同一个百分比函数,杜绝第二套口径。
|
|
2518
|
+
value: planPercent(planCount(session.stats?.done), planCount(session.stats?.total)) ?? 0,
|
|
2339
2519
|
}
|
|
2340
2520
|
}), [groups, t])
|
|
2341
2521
|
return h('div', { className: 'dsh-wp' },
|
|
@@ -2362,8 +2542,16 @@ window.__ModuleLoader__.load({
|
|
|
2362
2542
|
activeSessions.length === 0
|
|
2363
2543
|
? h('div', { className: 'dsh-wp-empty' }, h('div', { style: { fontWeight: 650, marginBottom: '4px' } }, t('empty')), h('div', null, t('emptyHint')))
|
|
2364
2544
|
: h('div', { className: 'dsh-wp-groups' },
|
|
2365
|
-
|
|
2366
|
-
|
|
2545
|
+
/*
|
|
2546
|
+
* [1.6.0] 三组顺序:真的在跑 → 已结束(停在半路) → 已完成。
|
|
2547
|
+
* 「已结束」夹在中间是有意为之:它是用户最需要立刻知道的那一档
|
|
2548
|
+
* (刚停、还没做完),不能滑到历史组的下面去。
|
|
2549
|
+
*/
|
|
2550
|
+
h(SessionGroup, { key: 'live', group: 'live', list: groups.live, urgent: groups.urgent, t, openSessionId, setOpenSessionId }),
|
|
2551
|
+
groups.ended.length > 0
|
|
2552
|
+
? h(SessionGroup, { key: 'ended', group: 'ended', list: groups.ended, hasLive: groups.live.length > 0, urgent: groups.urgentEnded, t, openSessionId, setOpenSessionId })
|
|
2553
|
+
: null,
|
|
2554
|
+
h(SessionGroup, { key: 'done', group: 'done', list: groups.finished, hasLive: groups.live.length > 0 || groups.ended.length > 0, urgent: 0, t, openSessionId, setOpenSessionId }),
|
|
2367
2555
|
// 用户选择 B:总览块(完成率环 + 每个计划的进度条)同样下移到两组之后,
|
|
2368
2556
|
// 让开屏第一屏直接是「进行中」组的任务卡(此前统计条 → 总览 → 说明行 →
|
|
2369
2557
|
// 任务卡,第一张卡要下滑约一屏)。顶部只保留 5 格数字统计条与一行口径说明。
|
|
@@ -2373,7 +2561,8 @@ window.__ModuleLoader__.load({
|
|
|
2373
2561
|
) : null,
|
|
2374
2562
|
// 图表放两组之后:两张卡各 ≈246px,排在组前会让首屏看不到任何一张任务卡
|
|
2375
2563
|
// (设计稿 §8 第 12 条,T8 复核时复现在 1.4.2 源码里)。
|
|
2376
|
-
|
|
2564
|
+
// [1.6.0] 「未结束」= 进行中 + 已结束但未完成:两者都还没做完,都是用户要盯的对象。
|
|
2565
|
+
h(GroupCharts, { liveCount: groups.live.length + groups.ended.length, doneCount: groups.finished.length, liveBars, hiddenBars: Math.max(0, groups.live.length + groups.ended.length - 6), t }),
|
|
2377
2566
|
),
|
|
2378
2567
|
)
|
|
2379
2568
|
}
|
|
@@ -2389,12 +2578,19 @@ window.__ModuleLoader__.load({
|
|
|
2389
2578
|
const active = ['planning', 'executing', 'blocked'].includes(session.phase)
|
|
2390
2579
|
if (currentTurn > createdTurn && !active) return null
|
|
2391
2580
|
}
|
|
2581
|
+
/*
|
|
2582
|
+
* [1.5.2] 会话内联卡(TurnCard)也走同一套对账。
|
|
2583
|
+
*
|
|
2584
|
+
* 面板卡片修好了、内联卡却仍按原始 phase 渲染的话,同一张计划会在
|
|
2585
|
+
* 「侧栏已完成 / 对话里正在收尾」之间再次自相矛盾。
|
|
2586
|
+
*/
|
|
2392
2587
|
const stats = session.stats || {}
|
|
2393
|
-
const
|
|
2588
|
+
const phase = planPhaseOf(session.phase || 'pending', planCounts(stats))
|
|
2589
|
+
const tone = phaseTone(phase)
|
|
2394
2590
|
return h('div', { className: `dsh-wp-turn dsh-wp-tone-${tone}` },
|
|
2395
2591
|
h('div', { className: 'dsh-wp-turn-title' },
|
|
2396
2592
|
h('span', null, session.title || t('panel')),
|
|
2397
|
-
h(Pill, { tone },
|
|
2593
|
+
h(Pill, { tone }, phaseLabel(phase, session, t)),
|
|
2398
2594
|
),
|
|
2399
2595
|
h('div', { className: 'dsh-wp-muted', style: { marginTop: '6px' } },
|
|
2400
2596
|
`${t('helpWho')}:${workerText(session, t)}`),
|