speccore 5.19.2 → 5.22.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/README.en.md +63 -210
- package/README.md +108 -252
- package/dist/cli.js +224 -300
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +5 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +235 -543
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/ask.d.ts +8 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +293 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/bugfix.d.ts +4 -0
- package/dist/commands/bugfix.d.ts.map +1 -1
- package/dist/commands/bugfix.js +122 -2
- package/dist/commands/bugfix.js.map +1 -1
- package/dist/commands/change.d.ts +2 -0
- package/dist/commands/change.d.ts.map +1 -1
- package/dist/commands/change.js +37 -2
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/completion.js +1 -1
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +175 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doc2spec.d.ts +10 -0
- package/dist/commands/doc2spec.d.ts.map +1 -0
- package/dist/commands/{word2spec.js → doc2spec.js} +55 -19
- package/dist/commands/doc2spec.js.map +1 -0
- package/dist/commands/done.d.ts.map +1 -1
- package/dist/commands/done.js +47 -0
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/execute.d.ts +1 -0
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +118 -11
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/help-quick.js +2 -2
- package/dist/commands/help-quick.js.map +1 -1
- package/dist/commands/help.d.ts +3 -2
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +372 -51
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +289 -15
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +177 -283
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/iteration/create.d.ts +1 -0
- package/dist/commands/iteration/create.d.ts.map +1 -1
- package/dist/commands/iteration/create.js +1 -0
- package/dist/commands/iteration/create.js.map +1 -1
- package/dist/commands/iteration/split.d.ts +1 -0
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +240 -7
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/plan.d.ts +1 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +25 -1
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/pr.d.ts +1 -0
- package/dist/commands/pr.d.ts.map +1 -1
- package/dist/commands/pr.js +94 -22
- package/dist/commands/pr.js.map +1 -1
- package/dist/commands/schedule.d.ts +27 -0
- package/dist/commands/schedule.d.ts.map +1 -0
- package/dist/commands/schedule.js +203 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/status-panel.d.ts +8 -1
- package/dist/commands/status-panel.d.ts.map +1 -1
- package/dist/commands/status-panel.js +1095 -5
- package/dist/commands/status-panel.js.map +1 -1
- package/dist/commands/task/new.d.ts +4 -0
- package/dist/commands/task/new.d.ts.map +1 -1
- package/dist/commands/task/new.js +87 -0
- package/dist/commands/task/new.js.map +1 -1
- package/dist/core/analyze-engine.d.ts +32 -0
- package/dist/core/analyze-engine.d.ts.map +1 -0
- package/dist/core/analyze-engine.js +935 -0
- package/dist/core/analyze-engine.js.map +1 -0
- package/dist/core/capabilities.d.ts +12 -0
- package/dist/core/capabilities.d.ts.map +1 -0
- package/dist/core/capabilities.js +58 -0
- package/dist/core/capabilities.js.map +1 -0
- package/dist/core/code-scanner.d.ts.map +1 -1
- package/dist/core/code-scanner.js +4 -1
- package/dist/core/code-scanner.js.map +1 -1
- package/dist/core/intent-recognition.d.ts +1 -1
- package/dist/core/intent-recognition.d.ts.map +1 -1
- package/dist/core/intent-recognition.js +5 -5
- package/dist/core/intent-recognition.js.map +1 -1
- package/dist/core/next-steps.js +1 -1
- package/dist/core/next-steps.js.map +1 -1
- package/dist/core/schedule-engine.d.ts +25 -0
- package/dist/core/schedule-engine.d.ts.map +1 -0
- package/dist/core/schedule-engine.js +179 -0
- package/dist/core/schedule-engine.js.map +1 -0
- package/dist/core/schedule-store.d.ts +104 -0
- package/dist/core/schedule-store.d.ts.map +1 -0
- package/dist/core/schedule-store.js +188 -0
- package/dist/core/schedule-store.js.map +1 -0
- package/dist/core/schemas/iteration.schema.d.ts +4 -4
- package/dist/core/schemas/task.schema.d.ts +2 -2
- package/package.json +7 -13
- package/dist/commands/demo.d.ts +0 -10
- package/dist/commands/demo.d.ts.map +0 -1
- package/dist/commands/demo.js +0 -151
- package/dist/commands/demo.js.map +0 -1
- package/dist/commands/goal.d.ts +0 -15
- package/dist/commands/goal.d.ts.map +0 -1
- package/dist/commands/goal.js +0 -232
- package/dist/commands/goal.js.map +0 -1
- package/dist/commands/new-task.d.ts +0 -15
- package/dist/commands/new-task.d.ts.map +0 -1
- package/dist/commands/new-task.js +0 -208
- package/dist/commands/new-task.js.map +0 -1
- package/dist/commands/platform-add.d.ts +0 -12
- package/dist/commands/platform-add.d.ts.map +0 -1
- package/dist/commands/platform-add.js +0 -111
- package/dist/commands/platform-add.js.map +0 -1
- package/dist/commands/retro.d.ts +0 -10
- package/dist/commands/retro.d.ts.map +0 -1
- package/dist/commands/retro.js +0 -131
- package/dist/commands/retro.js.map +0 -1
- package/dist/commands/spec.d.ts +0 -8
- package/dist/commands/spec.d.ts.map +0 -1
- package/dist/commands/spec.js +0 -159
- package/dist/commands/spec.js.map +0 -1
- package/dist/commands/template-add.d.ts +0 -12
- package/dist/commands/template-add.d.ts.map +0 -1
- package/dist/commands/template-add.js +0 -65
- package/dist/commands/template-add.js.map +0 -1
- package/dist/commands/word2spec.d.ts +0 -8
- package/dist/commands/word2spec.d.ts.map +0 -1
- package/dist/commands/word2spec.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.statusPanelCommand = statusPanelCommand;
|
|
4
|
+
exports.defaultPhase = defaultPhase;
|
|
4
5
|
/**
|
|
5
6
|
* status-panel — IDE 风格侧栏状态(实时项目状态一览)
|
|
6
7
|
*/
|
|
@@ -9,9 +10,23 @@ const path_1 = require("path");
|
|
|
9
10
|
const logger_1 = require("../utils/logger");
|
|
10
11
|
const unified_config_1 = require("../core/unified-config");
|
|
11
12
|
const context_1 = require("../core/context");
|
|
12
|
-
async function statusPanelCommand() {
|
|
13
|
+
async function statusPanelCommand(options = {}) {
|
|
13
14
|
const iteration = await (0, context_1.getDefaultIteration)();
|
|
14
15
|
const config = await (0, unified_config_1.loadConfig)();
|
|
16
|
+
// ── Export mode ──
|
|
17
|
+
if (options.export) {
|
|
18
|
+
await exportStatus(config, iteration, options.export, options);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// ── Filter labels for header ──
|
|
22
|
+
const filterLabels = [];
|
|
23
|
+
if (options.assignee)
|
|
24
|
+
filterLabels.push(`人员: ${options.assignee}`);
|
|
25
|
+
if (options.platform)
|
|
26
|
+
filterLabels.push(`平台: ${options.platform}`);
|
|
27
|
+
if (options.type)
|
|
28
|
+
filterLabels.push(`类型: ${options.type}`);
|
|
29
|
+
const filterTag = filterLabels.length > 0 ? ` [${filterLabels.join(', ')}]` : '';
|
|
15
30
|
// Header
|
|
16
31
|
logger_1.logger.info('');
|
|
17
32
|
logger_1.logger.info('┌────────────────── SpecCore ──────────────────┐');
|
|
@@ -23,12 +38,60 @@ async function statusPanelCommand() {
|
|
|
23
38
|
const phase = await detectPhase(iterDir);
|
|
24
39
|
const phaseIcon = { init: '🔧', require: '📝', analyze: '🔍', split: '📦', dev: '💻', review: '✅', done: '✨' }[phase] || '📌';
|
|
25
40
|
logger_1.logger.info(`│ 阶段: ${phaseIcon} ${phase.padEnd(35)}│`);
|
|
41
|
+
// ── Collect & filter tasks ──
|
|
42
|
+
const allTasks = await collectTasks(iterDir);
|
|
43
|
+
let filteredTasks = allTasks;
|
|
44
|
+
let totalBeforeFilter = allTasks.length;
|
|
45
|
+
if (options.assignee) {
|
|
46
|
+
filteredTasks = filteredTasks.filter(t => t.assignee === options.assignee);
|
|
47
|
+
}
|
|
48
|
+
if (options.type) {
|
|
49
|
+
filteredTasks = filteredTasks.filter(t => (t.type || '').toLowerCase() === options.type.toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
if (options.platform) {
|
|
52
|
+
filteredTasks = await filterByPlatform(iterDir, filteredTasks, options.platform);
|
|
53
|
+
}
|
|
54
|
+
const tasks = filteredTasks;
|
|
55
|
+
const done = tasks.filter(t => t.status.includes('完成') || t.status.includes('completed')).length;
|
|
56
|
+
const inProgress = tasks.filter(t => t.status.includes('开发') || t.status.includes('in_progress')).length;
|
|
57
|
+
const pending = tasks.length - done - inProgress;
|
|
26
58
|
// Task counts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
logger_1.logger.info(`│ 任务: ${
|
|
30
|
-
const bar = buildProgressBar(
|
|
59
|
+
if (tasks.length > 0) {
|
|
60
|
+
const shown = tasks.length !== totalBeforeFilter ? ` ${tasks.length}/${totalBeforeFilter}` : '';
|
|
61
|
+
logger_1.logger.info(`│ 任务: ${done}/${tasks.length} 完成${shown}${filterTag}`.padEnd(47) + '│');
|
|
62
|
+
const bar = buildProgressBar(done, tasks.length);
|
|
31
63
|
logger_1.logger.info(`│ ${bar.padEnd(46)}│`);
|
|
64
|
+
// People breakdown
|
|
65
|
+
const personMap = {};
|
|
66
|
+
for (const t of tasks) {
|
|
67
|
+
const who = t.assignee || '未分配';
|
|
68
|
+
if (!personMap[who])
|
|
69
|
+
personMap[who] = { total: 0, done: 0 };
|
|
70
|
+
personMap[who].total++;
|
|
71
|
+
if (t.status.includes('完成') || t.status.includes('completed'))
|
|
72
|
+
personMap[who].done++;
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(personMap).length > 0) {
|
|
75
|
+
logger_1.logger.info('├──────────────────────────────────────────────┤');
|
|
76
|
+
for (const [name, s] of Object.entries(personMap)) {
|
|
77
|
+
const pBar = buildProgressBar(s.done, s.total);
|
|
78
|
+
logger_1.logger.info(`│ ${name}: ${pBar} ${s.done}/${s.total}`.padEnd(47) + '│');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Platform breakdown
|
|
82
|
+
const platformCount = await countPlatforms(iterDir, tasks);
|
|
83
|
+
if (platformCount.backend + platformCount.frontend > 0) {
|
|
84
|
+
logger_1.logger.info('├──────────────────────────────────────────────┤');
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (platformCount.backend > 0)
|
|
87
|
+
parts.push(`Backend: ${platformCount.backend}`);
|
|
88
|
+
if (platformCount.frontend > 0)
|
|
89
|
+
parts.push(`Frontend: ${platformCount.frontend}`);
|
|
90
|
+
logger_1.logger.info(`│ 平台: ${parts.join(' ')}${' '.repeat(Math.max(0, 37 - parts.join(' ').length))}│`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
logger_1.logger.info(`│ 任务: 无${filterTag}`.padEnd(47) + '│');
|
|
32
95
|
}
|
|
33
96
|
// Branch info
|
|
34
97
|
const branch = await getCurrentBranch();
|
|
@@ -48,6 +111,49 @@ async function statusPanelCommand() {
|
|
|
48
111
|
logger_1.logger.info('└──────────────────────────────────────────────┘');
|
|
49
112
|
logger_1.logger.info('');
|
|
50
113
|
}
|
|
114
|
+
// ── 共享:收集所有任务数据 ──
|
|
115
|
+
async function collectTasks(iterDir) {
|
|
116
|
+
const tasks = [];
|
|
117
|
+
if (!(await (0, fs_extra_1.pathExists)(iterDir)))
|
|
118
|
+
return tasks;
|
|
119
|
+
const entryList = await (0, fs_extra_1.readdir)(iterDir, { withFileTypes: true });
|
|
120
|
+
for (const e of entryList) {
|
|
121
|
+
if (e.isDirectory() && e.name.startsWith('Task-')) {
|
|
122
|
+
const taskPath = (0, path_1.join)(iterDir, e.name, 'backend', 'TASK.md');
|
|
123
|
+
try {
|
|
124
|
+
if (await (0, fs_extra_1.pathExists)(taskPath)) {
|
|
125
|
+
const md = await (0, fs_extra_1.readFile)(taskPath, 'utf-8');
|
|
126
|
+
const status = (md.match(/状态: (.+)/) || [])[1] || 'pending';
|
|
127
|
+
const type = (md.match(/类型: (.+)/) || [])[1] || 'feature';
|
|
128
|
+
const assignee = (md.match(/负责人[::]\s*(\S+)/) || [])[1] || '';
|
|
129
|
+
tasks.push({ id: e.name, status, type, assignee });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
tasks.push({ id: e.name, status: 'pending', type: 'feature', assignee: '' });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
tasks.push({ id: e.name, status: 'pending', type: 'feature', assignee: '' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return tasks;
|
|
141
|
+
}
|
|
142
|
+
// ── 共享:按平台统计 ──
|
|
143
|
+
async function countPlatforms(iterDir, tasks) {
|
|
144
|
+
let backend = 0, frontend = 0;
|
|
145
|
+
for (const t of tasks) {
|
|
146
|
+
try {
|
|
147
|
+
const entries = await (0, fs_extra_1.readdir)((0, path_1.join)(iterDir, t.id), { withFileTypes: true });
|
|
148
|
+
if (entries.some((e) => e.name === 'backend'))
|
|
149
|
+
backend++;
|
|
150
|
+
if (entries.some((e) => e.name === 'frontend'))
|
|
151
|
+
frontend++;
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}
|
|
155
|
+
return { backend, frontend };
|
|
156
|
+
}
|
|
51
157
|
async function detectPhase(iterDir) {
|
|
52
158
|
const reqDoc = (0, path_1.join)(iterDir, '00-需求文档', 'REQUIREMENT.md');
|
|
53
159
|
const analysis = (0, path_1.join)(iterDir, '00-需求文档', 'ANALYSIS.md');
|
|
@@ -140,4 +246,988 @@ async function getNextAction(phase, iterDir) {
|
|
|
140
246
|
};
|
|
141
247
|
return actions[phase] || 'speccore dev';
|
|
142
248
|
}
|
|
249
|
+
// 导出功能
|
|
250
|
+
// ── 平台过滤: 检测 Task 目录下是否有指定平台 ──
|
|
251
|
+
async function filterByPlatform(iterDir, tasks, platform) {
|
|
252
|
+
const { readdir } = require('fs-extra');
|
|
253
|
+
const { join } = require('path');
|
|
254
|
+
const result = [];
|
|
255
|
+
for (const t of tasks) {
|
|
256
|
+
const taskDir = join(iterDir, t.id);
|
|
257
|
+
try {
|
|
258
|
+
const entries = await readdir(taskDir, { withFileTypes: true });
|
|
259
|
+
if (platform === 'backend' && entries.some((e) => e.name === 'backend'))
|
|
260
|
+
result.push(t);
|
|
261
|
+
else if (platform === 'frontend' && entries.some((e) => e.name === 'frontend'))
|
|
262
|
+
result.push(t);
|
|
263
|
+
else if (entries.some((e) => e.name === platform))
|
|
264
|
+
result.push(t);
|
|
265
|
+
}
|
|
266
|
+
catch { /* task dir not found */ }
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
async function exportStatus(config, iteration, format, options = {}) {
|
|
271
|
+
const data = {
|
|
272
|
+
project: config.project.name,
|
|
273
|
+
iteration: iteration || '未设置',
|
|
274
|
+
exportedAt: new Date().toISOString(),
|
|
275
|
+
phases: {},
|
|
276
|
+
};
|
|
277
|
+
if (iteration) {
|
|
278
|
+
const iterDir = (0, path_1.join)(process.cwd(), '期次-' + iteration);
|
|
279
|
+
const phase = await require('./status-panel').defaultPhase(iterDir);
|
|
280
|
+
data.phase = phase;
|
|
281
|
+
// ── 读取期次时间范围 ──
|
|
282
|
+
const metaPath = (0, path_1.join)(iterDir, "00-期次总览", "METADATA.md");
|
|
283
|
+
const graphPath = (0, path_1.join)(iterDir, '00-期次总览', 'PROJECT_GRAPH.md');
|
|
284
|
+
if (await (0, fs_extra_1.pathExists)(graphPath)) {
|
|
285
|
+
const graph = await (0, fs_extra_1.readFile)(graphPath, 'utf-8');
|
|
286
|
+
const ownerMatch = graph.match(/负责人[::]?\s*(\S+)/);
|
|
287
|
+
if (ownerMatch)
|
|
288
|
+
data.iterationOwner = ownerMatch[1];
|
|
289
|
+
}
|
|
290
|
+
if (await (0, fs_extra_1.pathExists)(metaPath)) {
|
|
291
|
+
const meta = await (0, fs_extra_1.readFile)(metaPath, 'utf-8');
|
|
292
|
+
const fromMatch = meta.match(/开始[::]?\s*(\d{4}-\d{2}-\d{2})/) || meta.match(/from[::]?\s*(\d{4}-\d{2}-\d{2})/i);
|
|
293
|
+
const toMatch = meta.match(/结束[::]?\s*(\d{4}-\d{2}-\d{2})/) || meta.match(/to[::]?\s*(\d{4}-\d{2}-\d{2})/i);
|
|
294
|
+
if (fromMatch)
|
|
295
|
+
data.iterationStart = fromMatch[1];
|
|
296
|
+
if (toMatch)
|
|
297
|
+
data.iterationEnd = toMatch[1];
|
|
298
|
+
// ── 里程碑时间线 ──
|
|
299
|
+
const devEnd = meta.match(/提测[::]?\s*(\d{4}-\d{2}-\d{2})/) || meta.match(/开发结束[::]?\s*(\d{4}-\d{2}-\d{2})/i);
|
|
300
|
+
const sit = meta.match(/SIT[::]?\s*(\d{4}-\d{2}-\d{2})/i) || meta.match(/集成测试[::]?\s*(\d{4}-\d{2}-\d{2})/);
|
|
301
|
+
const uat = meta.match(/UAT[::]?\s*(\d{4}-\d{2}-\d{2})/i) || meta.match(/验收测试[::]?\s*(\d{4}-\d{2}-\d{2})/);
|
|
302
|
+
const release = meta.match(/上线[::]?\s*(\d{4}-\d{2}-\d{2})/) || meta.match(/发布[::]?\s*(\d{4}-\d{2}-\d{2})/);
|
|
303
|
+
data.milestones = {
|
|
304
|
+
devEnd: devEnd ? devEnd[1] : null,
|
|
305
|
+
sit: sit ? sit[1] : null,
|
|
306
|
+
uat: uat ? uat[1] : null,
|
|
307
|
+
release: release ? release[1] : null,
|
|
308
|
+
};
|
|
309
|
+
// ── Delay 记录 ──
|
|
310
|
+
const delays = [];
|
|
311
|
+
const delayRe = /DELAY[::]?\s*(\d{4}-\d{2}-\d{2})[::]?\s*(.+)/gi;
|
|
312
|
+
let dm;
|
|
313
|
+
while ((dm = delayRe.exec(meta)) !== null) {
|
|
314
|
+
delays.push({ date: dm[1], reason: (dm[2] || '').trim() });
|
|
315
|
+
}
|
|
316
|
+
data.delays = delays;
|
|
317
|
+
}
|
|
318
|
+
// 计算时间进度
|
|
319
|
+
const today = new Date();
|
|
320
|
+
if (data.iterationStart && data.iterationEnd) {
|
|
321
|
+
const start = new Date(data.iterationStart).getTime();
|
|
322
|
+
const end = new Date(data.iterationEnd).getTime();
|
|
323
|
+
const now = today.getTime();
|
|
324
|
+
data.timeProgress = Math.round(Math.min(100, Math.max(0, (now - start) / (end - start) * 100)));
|
|
325
|
+
data.isOverdue = now > end;
|
|
326
|
+
data.daysLeft = Math.ceil((end - now) / 86400000);
|
|
327
|
+
}
|
|
328
|
+
const tasks = [];
|
|
329
|
+
if (await (0, fs_extra_1.pathExists)(iterDir)) {
|
|
330
|
+
const entryList = await (0, fs_extra_1.readdir)(iterDir, { withFileTypes: true });
|
|
331
|
+
for (const e of entryList) {
|
|
332
|
+
if (e.isDirectory() && e.name.startsWith('Task-')) {
|
|
333
|
+
const taskPath = (0, path_1.join)(iterDir, e.name, 'backend', 'TASK.md');
|
|
334
|
+
if (await (0, fs_extra_1.pathExists)(taskPath)) {
|
|
335
|
+
const md = await (0, fs_extra_1.readFile)(taskPath, 'utf-8');
|
|
336
|
+
const status = (md.match(/状态: (.+)/) || [])[1] || 'pending';
|
|
337
|
+
const type = (md.match(/类型: (.+)/) || [])[1] || 'feature';
|
|
338
|
+
const created = (md.match(/创建日期[::]?\s*(\d{4}-\d{2}-\d{2})/) || md.match(/创建:\s*(\d{4}-\d{2}-\d{2})/) || [])[1] || '';
|
|
339
|
+
const estimate = (md.match(/预估[工时::]?\s*(\d+)\s*[hH小时]/) || md.match(/预计耗时[::]?\s*(\d+)/) || [])[1] || '';
|
|
340
|
+
const delay = (md.match(/延期\|DELAY/i) || []).length > 0;
|
|
341
|
+
tasks.push({ id: e.name, status, type, created, estimate: estimate ? parseInt(estimate) : 0, delay, assignee: (md.match(/负责人[::]\s*(\S+)/) || [])[1] || '' });
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
tasks.push({ id: e.name, status: 'pending' });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
data.tasks = tasks;
|
|
350
|
+
data.taskCount = tasks.length;
|
|
351
|
+
const nowTs = new Date().getTime();
|
|
352
|
+
const weekAgo = new Date(nowTs - 7 * 86400000);
|
|
353
|
+
// ── 过滤: 人员 / 类型 / 平台 ──
|
|
354
|
+
let filtered = tasks;
|
|
355
|
+
const filterLabels = [];
|
|
356
|
+
if (options.assignee) {
|
|
357
|
+
filtered = filtered.filter((t) => t.assignee === options.assignee);
|
|
358
|
+
filterLabels.push(`人员: ${options.assignee}`);
|
|
359
|
+
}
|
|
360
|
+
if (options.type) {
|
|
361
|
+
filtered = filtered.filter((t) => (t.type || '').toLowerCase() === options.type.toLowerCase());
|
|
362
|
+
filterLabels.push(`类型: ${options.type}`);
|
|
363
|
+
}
|
|
364
|
+
if (options.platform) {
|
|
365
|
+
// 平台过滤: 检查 Task 目录结构 (backend/ frontend/web/ frontend/h5/ etc)
|
|
366
|
+
const platformTasks = await filterByPlatform(iterDir, filtered, options.platform);
|
|
367
|
+
filtered = platformTasks;
|
|
368
|
+
filterLabels.push(`平台: ${options.platform}`);
|
|
369
|
+
}
|
|
370
|
+
if (filtered.length < tasks.length) {
|
|
371
|
+
data.filteredFrom = tasks.length;
|
|
372
|
+
data.filterLabels = filterLabels;
|
|
373
|
+
data.tasks = filtered; // 更新为过滤后的任务列表
|
|
374
|
+
// 重新计算所有统计
|
|
375
|
+
data.taskCount = filtered.length;
|
|
376
|
+
const td2 = {};
|
|
377
|
+
for (const t of filtered) {
|
|
378
|
+
const tt = (t.type || 'feature').toLowerCase();
|
|
379
|
+
td2[tt] = (td2[tt] || 0) + 1;
|
|
380
|
+
}
|
|
381
|
+
data.typeDistribution = td2;
|
|
382
|
+
data.addedThisWeek = filtered.filter((t) => t.created && new Date(t.created) >= weekAgo).length;
|
|
383
|
+
data.bugCount = filtered.filter((t) => (t.type || '').toLowerCase().includes('bug')).length;
|
|
384
|
+
const am2 = {};
|
|
385
|
+
for (const t of filtered) {
|
|
386
|
+
if (t.assignee) {
|
|
387
|
+
if (!am2[t.assignee])
|
|
388
|
+
am2[t.assignee] = { total: 0, done: 0 };
|
|
389
|
+
am2[t.assignee].total++;
|
|
390
|
+
if (t.status.includes("completed") || t.status.includes("完成"))
|
|
391
|
+
am2[t.assignee].done++;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
data.assigneeStats = am2;
|
|
395
|
+
// 重新计算时间维度
|
|
396
|
+
let totalAi = 0, totalHuman = 0, totalReview = 0;
|
|
397
|
+
for (const t of filtered) {
|
|
398
|
+
totalAi += t.aiTime || 0;
|
|
399
|
+
totalHuman += t.humanTime || 0;
|
|
400
|
+
totalReview += t.reviewTime || 0;
|
|
401
|
+
}
|
|
402
|
+
data.totalAiTime = totalAi;
|
|
403
|
+
data.totalHumanTime = totalHuman;
|
|
404
|
+
data.totalReviewTime = totalReview;
|
|
405
|
+
data.totalEstTime = totalAi + totalHuman + totalReview;
|
|
406
|
+
// 人员清单
|
|
407
|
+
const pt2 = {};
|
|
408
|
+
for (const t of filtered) {
|
|
409
|
+
if (t.assignee) {
|
|
410
|
+
if (!pt2[t.assignee])
|
|
411
|
+
pt2[t.assignee] = [];
|
|
412
|
+
pt2[t.assignee].push(t);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
data.personTasks = pt2;
|
|
416
|
+
const pd2 = {};
|
|
417
|
+
for (const t of filtered) {
|
|
418
|
+
if (!t.assignee)
|
|
419
|
+
continue;
|
|
420
|
+
if (!pd2[t.assignee])
|
|
421
|
+
pd2[t.assignee] = { total: 0, done: 0, bugs: 0, features: 0, estHours: 0 };
|
|
422
|
+
pd2[t.assignee].total++;
|
|
423
|
+
if (t.status.includes('completed') || t.status.includes('完成'))
|
|
424
|
+
pd2[t.assignee].done++;
|
|
425
|
+
if ((t.type || '').toLowerCase().includes('bug'))
|
|
426
|
+
pd2[t.assignee].bugs++;
|
|
427
|
+
else if (!(t.type || '').toLowerCase().includes('research'))
|
|
428
|
+
pd2[t.assignee].features++;
|
|
429
|
+
pd2[t.assignee].estHours += t.estimate || 0;
|
|
430
|
+
}
|
|
431
|
+
// 人员→平台映射
|
|
432
|
+
data.personPlatforms = await buildPersonPlatforms(iterDir, filtered);
|
|
433
|
+
data.personDetail = pd2;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// 类型分布 / 增量统计
|
|
437
|
+
const td = {};
|
|
438
|
+
for (const t of tasks) {
|
|
439
|
+
const tt = (t.type || 'feature').toLowerCase();
|
|
440
|
+
td[tt] = (td[tt] || 0) + 1;
|
|
441
|
+
}
|
|
442
|
+
data.typeDistribution = td;
|
|
443
|
+
data.addedThisWeek = tasks.filter((t) => t.created && new Date(t.created) >= weekAgo).length;
|
|
444
|
+
data.bugCount = tasks.filter((t) => (t.type || '').toLowerCase().includes('bug')).length;
|
|
445
|
+
const am = {};
|
|
446
|
+
for (const t of tasks) {
|
|
447
|
+
if (t.assignee) {
|
|
448
|
+
if (!am[t.assignee])
|
|
449
|
+
am[t.assignee] = { total: 0, done: 0 };
|
|
450
|
+
am[t.assignee].total++;
|
|
451
|
+
if (t.status.includes("completed") || t.status.includes("完成"))
|
|
452
|
+
am[t.assignee].done++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
data.assigneeStats = am;
|
|
456
|
+
// 人员多维统计
|
|
457
|
+
const personDetail = {};
|
|
458
|
+
for (const t of tasks) {
|
|
459
|
+
if (!t.assignee)
|
|
460
|
+
continue;
|
|
461
|
+
if (!personDetail[t.assignee])
|
|
462
|
+
personDetail[t.assignee] = { total: 0, done: 0, bugs: 0, features: 0, estHours: 0 };
|
|
463
|
+
personDetail[t.assignee].total++;
|
|
464
|
+
if (t.status.includes('completed') || t.status.includes('完成'))
|
|
465
|
+
personDetail[t.assignee].done++;
|
|
466
|
+
if ((t.type || '').toLowerCase().includes('bug'))
|
|
467
|
+
personDetail[t.assignee].bugs++;
|
|
468
|
+
else if (!(t.type || '').toLowerCase().includes('research'))
|
|
469
|
+
personDetail[t.assignee].features++;
|
|
470
|
+
personDetail[t.assignee].estHours += t.estimate || 0;
|
|
471
|
+
}
|
|
472
|
+
data.personDetail = personDetail;
|
|
473
|
+
// 人员→平台映射
|
|
474
|
+
data.personPlatforms = await buildPersonPlatforms(iterDir, tasks);
|
|
475
|
+
// 每人任务清单
|
|
476
|
+
const personTasks = {};
|
|
477
|
+
for (const t of tasks) {
|
|
478
|
+
if (!t.assignee)
|
|
479
|
+
continue;
|
|
480
|
+
if (!personTasks[t.assignee])
|
|
481
|
+
personTasks[t.assignee] = [];
|
|
482
|
+
personTasks[t.assignee].push(t);
|
|
483
|
+
}
|
|
484
|
+
data.personTasks = personTasks;
|
|
485
|
+
let totalAi = 0, totalHuman = 0, totalReview = 0;
|
|
486
|
+
for (const t of tasks) {
|
|
487
|
+
totalAi += t.aiTime || 0;
|
|
488
|
+
totalHuman += t.humanTime || 0;
|
|
489
|
+
totalReview += t.reviewTime || 0;
|
|
490
|
+
}
|
|
491
|
+
data.totalAiTime = totalAi;
|
|
492
|
+
data.totalHumanTime = totalHuman;
|
|
493
|
+
data.totalReviewTime = totalReview;
|
|
494
|
+
data.totalEstTime = totalAi + totalHuman + totalReview;
|
|
495
|
+
}
|
|
496
|
+
} // end if(iteration)
|
|
497
|
+
if (format === 'json') {
|
|
498
|
+
const outPath = 'speccore-status.json';
|
|
499
|
+
await (0, fs_extra_1.writeFile)(outPath, JSON.stringify(data, null, 2));
|
|
500
|
+
logger_1.logger.info(`✅ 导出到 ${outPath}`);
|
|
501
|
+
}
|
|
502
|
+
else if (format === 'md') {
|
|
503
|
+
let md = `# SpecCore Status — ${config.project.name}\n\n`;
|
|
504
|
+
md += `- 期次: ${iteration || '无'}\n- 阶段: ${data.phase || 'N/A'}\n\n`;
|
|
505
|
+
md += '## Tasks\n\n| ID | Status | Type |\n| :--- | :--- | :--- |\n';
|
|
506
|
+
for (const t of data.tasks || [])
|
|
507
|
+
md += `| ${t.id} | ${t.status} | ${t.type} |\n`;
|
|
508
|
+
const outPath = 'speccore-status.md';
|
|
509
|
+
await (0, fs_extra_1.writeFile)(outPath, md);
|
|
510
|
+
logger_1.logger.info(`✅ 导出到 ${outPath}`);
|
|
511
|
+
}
|
|
512
|
+
else if (format === 'html') {
|
|
513
|
+
const outPath = 'speccore-status.html';
|
|
514
|
+
await (0, fs_extra_1.writeFile)(outPath, buildHtmlDashboard(data));
|
|
515
|
+
logger_1.logger.info(`✅ 导出到 ${outPath}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// ── 人员→平台映射 ──
|
|
519
|
+
async function buildPersonPlatforms(iterDir, tasks) {
|
|
520
|
+
const { readdir } = require('fs-extra');
|
|
521
|
+
const { join } = require('path');
|
|
522
|
+
const map = {};
|
|
523
|
+
for (const t of tasks) {
|
|
524
|
+
if (!t.assignee || map[t.assignee])
|
|
525
|
+
continue;
|
|
526
|
+
try {
|
|
527
|
+
const entries = await readdir(join(iterDir, t.id), { withFileTypes: true });
|
|
528
|
+
const platforms = [];
|
|
529
|
+
if (entries.some((e) => e.name === 'backend'))
|
|
530
|
+
platforms.push('backend');
|
|
531
|
+
const fe = entries.find((e) => e.name === 'frontend');
|
|
532
|
+
if (fe && fe.isDirectory()) {
|
|
533
|
+
const subs = await readdir(join(iterDir, t.id, 'frontend'), { withFileTypes: true });
|
|
534
|
+
for (const s of subs) {
|
|
535
|
+
if (s.isDirectory())
|
|
536
|
+
platforms.push('frontend/' + s.name);
|
|
537
|
+
}
|
|
538
|
+
if (subs.length === 0)
|
|
539
|
+
platforms.push('frontend');
|
|
540
|
+
}
|
|
541
|
+
map[t.assignee] = platforms.join(', ') || '';
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
map[t.assignee] = '';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return map;
|
|
548
|
+
}
|
|
549
|
+
async function defaultPhase(iterDir) {
|
|
550
|
+
const reqDoc = (0, path_1.join)(iterDir, '00-需求文档', 'REQUIREMENT.md');
|
|
551
|
+
const analysis = (0, path_1.join)(iterDir, '00-需求文档', 'ANALYSIS.md');
|
|
552
|
+
if (!(await (0, fs_extra_1.pathExists)(reqDoc)))
|
|
553
|
+
return 'init';
|
|
554
|
+
if (!(await (0, fs_extra_1.pathExists)(analysis)))
|
|
555
|
+
return 'require';
|
|
556
|
+
const tasks = await (0, fs_extra_1.readdir)(iterDir, { withFileTypes: true });
|
|
557
|
+
if (!tasks.some((e) => e.isDirectory() && e.name.startsWith('Task-')))
|
|
558
|
+
return 'analyze';
|
|
559
|
+
return 'dev';
|
|
560
|
+
}
|
|
561
|
+
function buildHtmlDashboard(data) {
|
|
562
|
+
const tasks = data.tasks || [];
|
|
563
|
+
const total = tasks.length;
|
|
564
|
+
const done = tasks.filter((t) => t.status.includes('完成') || t.status === 'completed').length;
|
|
565
|
+
const inProgress = tasks.filter((t) => t.status.includes('开发') || t.status === 'in_progress').length;
|
|
566
|
+
const pending = total - done - inProgress;
|
|
567
|
+
const donePct = total > 0 ? Math.round(done / total * 100) : 0;
|
|
568
|
+
const inProgressPct = total > 0 ? Math.round(inProgress / total * 100) : 0;
|
|
569
|
+
const pendingPct = total > 0 ? Math.round(pending / total * 100) : 0;
|
|
570
|
+
const phaseMap = { init: '初始化', require: '需求分析', analyze: '分析中', dev: '开发中', review: '审查中', done: '已完成' };
|
|
571
|
+
const phase = data.phase || 'N/A';
|
|
572
|
+
const phaseLabel = phaseMap[phase] || phase;
|
|
573
|
+
const featCount = (data.typeDistribution && data.typeDistribution['feature']) || 0;
|
|
574
|
+
const bugCount = (data.typeDistribution && data.typeDistribution['bugfix']) || 0;
|
|
575
|
+
const researchCount = (data.typeDistribution && data.typeDistribution['research']) || 0;
|
|
576
|
+
const addedThisWeek = data.addedThisWeek || 0;
|
|
577
|
+
const am = data.assigneeStats || {};
|
|
578
|
+
const assigneeCards = Object.entries(am).map(([name, s]) => {
|
|
579
|
+
const pct = s.total > 0 ? Math.round(s.done / s.total * 100) : 0;
|
|
580
|
+
return '<div style="background:rgba(0,240,255,.03);border:1px solid rgba(0,240,255,.08);border-radius:8px;padding:16px">' +
|
|
581
|
+
'<div style="font-size:14px;font-weight:600;margin-bottom:8px">' + name + '</div>' +
|
|
582
|
+
'<div style="display:flex;gap:12px;margin-bottom:8px">' +
|
|
583
|
+
'<div><span class="num-font">' + s.done + '</span><span style="font-size:10px;color:var(--muted)">/' + s.total + '</span></div>' +
|
|
584
|
+
'<div class="num-font" style="margin-left:auto">' + pct + '%</div>' +
|
|
585
|
+
'</div>' +
|
|
586
|
+
'<div style="height:4px;background:rgba(255,255,255,.03);border-radius:2px;overflow:hidden">' +
|
|
587
|
+
'<div style="width:' + pct + '%;height:100%;background:linear-gradient(90deg,var(--cyan),var(--green));border-radius:2px"></div></div>' +
|
|
588
|
+
'</div>';
|
|
589
|
+
}).join('');
|
|
590
|
+
const hasAssignees = assigneeCards.length > 0;
|
|
591
|
+
const totalAi = data.totalAiTime || 0;
|
|
592
|
+
const totalHuman = data.totalHumanTime || 0;
|
|
593
|
+
const totalReview = data.totalReviewTime || 0;
|
|
594
|
+
const totalEstTime = data.totalEstTime || 0;
|
|
595
|
+
const personPlatforms = data.personPlatforms || {};
|
|
596
|
+
const pt = data.personTasks || {};
|
|
597
|
+
const pd = data.personDetail || {};
|
|
598
|
+
// ── 排序:后端在前 → 前端在后,每组按姓氏首字母 ──
|
|
599
|
+
// ── 中文姓氏拼音映射 ──
|
|
600
|
+
const pinyinMap = {
|
|
601
|
+
'安': 'an', '白': 'bai', '包': 'bao', '鲍': 'bao', '毕': 'bi', '边': 'bian', '蔡': 'cai', '曹': 'cao',
|
|
602
|
+
'常': 'chang', '陈': 'chen', '程': 'cheng', '崔': 'cui', '戴': 'dai', '邓': 'deng', '丁': 'ding',
|
|
603
|
+
'董': 'dong', '杜': 'du', '段': 'duan', '范': 'fan', '方': 'fang', '冯': 'feng', '傅': 'fu',
|
|
604
|
+
'高': 'gao', '葛': 'ge', '龚': 'gong', '顾': 'gu', '关': 'guan', '郭': 'guo', '韩': 'han',
|
|
605
|
+
'何': 'he', '贺': 'he', '洪': 'hong', '侯': 'hou', '胡': 'hu', '华': 'hua', '黄': 'huang',
|
|
606
|
+
'霍': 'huo', '纪': 'ji', '贾': 'jia', '江': 'jiang', '姜': 'jiang', '蒋': 'jiang', '金': 'jin',
|
|
607
|
+
'康': 'kang', '孔': 'kong', '赖': 'lai', '雷': 'lei', '黎': 'li', '李': 'li', '梁': 'liang',
|
|
608
|
+
'廖': 'liao', '林': 'lin', '刘': 'liu', '柳': 'liu', '龙': 'long', '卢': 'lu', '鲁': 'lu',
|
|
609
|
+
'陆': 'lu', '吕': 'lv', '罗': 'luo', '马': 'ma', '毛': 'mao', '孟': 'meng', '莫': 'mo',
|
|
610
|
+
'倪': 'ni', '牛': 'niu', '潘': 'pan', '彭': 'peng', '齐': 'qi', '钱': 'qian', '乔': 'qiao',
|
|
611
|
+
'秦': 'qin', '邱': 'qiu', '任': 'ren', '阮': 'ruan', '邵': 'shao', '沈': 'shen', '盛': 'sheng',
|
|
612
|
+
'石': 'shi', '史': 'shi', '宋': 'song', '苏': 'su', '孙': 'sun', '谭': 'tan', '汤': 'tang',
|
|
613
|
+
'唐': 'tang', '陶': 'tao', '田': 'tian', '童': 'tong', '万': 'wan', '汪': 'wang', '王': 'wang',
|
|
614
|
+
'韦': 'wei', '魏': 'wei', '温': 'wen', '文': 'wen', '吴': 'wu', '武': 'wu', '夏': 'xia',
|
|
615
|
+
'向': 'xiang', '萧': 'xiao', '谢': 'xie', '熊': 'xiong', '徐': 'xu', '许': 'xu', '薛': 'xue',
|
|
616
|
+
'严': 'yan', '阎': 'yan', '杨': 'yang', '姚': 'yao', '叶': 'ye', '易': 'yi', '殷': 'yin',
|
|
617
|
+
'尹': 'yin', '应': 'ying', '尤': 'you', '于': 'yu', '余': 'yu', '俞': 'yu', '袁': 'yuan',
|
|
618
|
+
'岳': 'yue', '曾': 'zeng', '张': 'zhang', '章': 'zhang', '赵': 'zhao', '郑': 'zheng',
|
|
619
|
+
'钟': 'zhong', '周': 'zhou', '朱': 'zhu', '祝': 'zhu', '庄': 'zhuang', '邹': 'zou',
|
|
620
|
+
};
|
|
621
|
+
const surnameOrder = (n) => {
|
|
622
|
+
const first = n.charAt(0);
|
|
623
|
+
const py = pinyinMap[first];
|
|
624
|
+
if (py)
|
|
625
|
+
return py;
|
|
626
|
+
const c = n.charCodeAt(0);
|
|
627
|
+
if (c >= 0x4E00)
|
|
628
|
+
return n; // 中文未收录的,fallback 到原文
|
|
629
|
+
return n.toLowerCase(); // 英��按字母序
|
|
630
|
+
};
|
|
631
|
+
const getPlatformGroup = (name) => {
|
|
632
|
+
const p = personPlatforms[name] || '';
|
|
633
|
+
if (p.includes('backend'))
|
|
634
|
+
return '0_backend';
|
|
635
|
+
if (p.includes('frontend'))
|
|
636
|
+
return '1_frontend';
|
|
637
|
+
return '2_other';
|
|
638
|
+
};
|
|
639
|
+
const sortedPersonEntries = Object.entries(pd)
|
|
640
|
+
.sort(([a], [b]) => {
|
|
641
|
+
const ga = getPlatformGroup(a);
|
|
642
|
+
const gb = getPlatformGroup(b);
|
|
643
|
+
if (ga !== gb)
|
|
644
|
+
return ga.localeCompare(gb);
|
|
645
|
+
return surnameOrder(a).localeCompare(surnameOrder(b));
|
|
646
|
+
});
|
|
647
|
+
const personDetailCards = sortedPersonEntries.map(([name, d]) => {
|
|
648
|
+
const pct = d.total > 0 ? Math.round(d.done / d.total * 100) : 0;
|
|
649
|
+
const bugPct = d.total > 0 ? Math.round(d.bugs / d.total * 100) : 0;
|
|
650
|
+
return '<div style="background:rgba(0,240,255,.03);border:1px solid rgba(0,240,255,.08);border-radius:10px;padding:18px">' +
|
|
651
|
+
'<div style="font-size:15px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px">' + name +
|
|
652
|
+
'<span style="margin-left:auto;font-family:Orbitron;font-size:13px;color:var(--cyan)">' + pct + '%</span></div>' +
|
|
653
|
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:11px">' +
|
|
654
|
+
'<div><span style="color:var(--muted)">任务</span><div style="font-weight:600;margin-top:2px">' + d.done + '/' + d.total + '</div></div>' +
|
|
655
|
+
'<div><span style="color:var(--muted)">功能</span><div style="font-weight:600;margin-top:2px">' + d.features + '</div></div>' +
|
|
656
|
+
'<div><span style="color:var(--muted)">Bug</span><div style="font-weight:600;margin-top:2px;color:' + (bugPct > 30 ? 'var(--orange)' : 'var(--text)') + '">' + d.bugs + '</div></div>' +
|
|
657
|
+
'<div><span style="color:var(--muted)">工时</span><div style="font-weight:600;margin-top:2px">' + d.estHours + 'h</div></div>' +
|
|
658
|
+
'</div>' +
|
|
659
|
+
'<div style="height:4px;background:rgba(255,255,255,.04);border-radius:2px;margin-top:12px;overflow:hidden">' +
|
|
660
|
+
'<div style="width:' + pct + '%;height:100%;background:linear-gradient(90deg,var(--cyan),var(--green));border-radius:2px"></div></div>' +
|
|
661
|
+
'</div>';
|
|
662
|
+
}).join('');
|
|
663
|
+
const bugTotal = data.bugCount || 0;
|
|
664
|
+
const phasePct = { init: 10, require: 25, analyze: 40, dev: 60, review: 80, done: 100 }[phase] || 0;
|
|
665
|
+
return `<!DOCTYPE html>
|
|
666
|
+
<html lang="zh-CN">
|
|
667
|
+
<head>
|
|
668
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
669
|
+
<title>SpecCore — ${data.project}</title>
|
|
670
|
+
<style>
|
|
671
|
+
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=JetBrains+Mono:wght@400;600;700&display=swap');
|
|
672
|
+
[data-theme="cyber"]{--cyan:#00f0ff;--blue:#3b82f6;--green:#00ff88;--purple:#a78bfa;--orange:#f59e0b;--bg:#060b14;--card:rgba(8,16,32,.85);--border:rgba(0,240,255,.12);--text:#c4d5e7;--muted:#4a5568;--surface:rgba(17,25,40,.7);--hover:rgba(0,240,255,.02)}
|
|
673
|
+
[data-theme="light"]{--cyan:#2563eb;--blue:#3b82f6;--green:#059669;--purple:#7c3aed;--orange:#d97706;--bg:#f8fafc;--card:rgba(255,255,255,.95);--border:rgba(0,0,0,.08);--text:#1e293b;--muted:#94a3b8;--surface:rgba(255,255,255,.95);--hover:rgba(37,99,235,.02)}
|
|
674
|
+
[data-theme="github"]{--cyan:#2da44e;--blue:#0969da;--green:#1a7f37;--purple:#8250df;--orange:#cf222e;--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--muted:#8b949e;--surface:#161b22;--hover:rgba(45,164,78,.05)}
|
|
675
|
+
[data-theme="synth"]{--cyan:#f92aad;--blue:#ff7edb;--green:#36f9f6;--purple:#b381f9;--orange:#ffb86c;--bg:#1a0024;--card:rgba(30,0,50,.9);--border:rgba(249,42,173,.2);--text:#f8f8f2;--muted:#6b3e7a;--surface:rgba(30,0,50,.9);--hover:rgba(249,42,173,.05)}
|
|
676
|
+
[data-theme="ocean"]{--cyan:#0ea5e9;--blue:#0284c7;--green:#14b8a6;--purple:#6366f1;--orange:#f97316;--bg:#0b1929;--card:rgba(13,31,56,.9);--border:rgba(14,165,233,.15);--text:#bae6fd;--muted:#5b7fa5;--surface:rgba(13,31,56,.9);--hover:rgba(14,165,233,.05)}
|
|
677
|
+
[data-theme="sakura"]{--cyan:#f472b6;--blue:#c084fc;--green:#a3e635;--purple:#e879f9;--orange:#fb923c;--bg:#1a0a14;--card:rgba(30,10,20,.9);--border:rgba(244,114,182,.2);--text:#fce7f3;--muted:#9d4a6d;--surface:rgba(30,10,20,.9);--hover:rgba(244,114,182,.05)}
|
|
678
|
+
[data-theme="forest"]{--cyan:#059669;--blue:#0284c7;--green:#22c55e;--purple:#6366f1;--orange:#ca8a04;--bg:#0a140a;--card:rgba(12,24,12,.9);--border:rgba(5,150,105,.15);--text:#d1fae5;--muted:#3b6b4e;--surface:rgba(12,24,12,.9);--hover:rgba(5,150,105,.05)}
|
|
679
|
+
[data-theme="amber"]{--cyan:#ffb000;--blue:#ff8c00;--green:#32cd32;--purple:#da70d6;--orange:#ff6347;--bg:#0c0c0c;--card:#1a1a1a;--border:#333;--text:#ffb000;--muted:#666;--surface:#1a1a1a;--hover:rgba(255,176,0,.05)}
|
|
680
|
+
[data-theme="mono"]{--cyan:#64748b;--blue:#475569;--green:#334155;--purple:#1e293b;--orange:#94a3b8;--bg:#0f172a;--card:#1e293b;--border:#334155;--text:#e2e8f0;--muted:#64748b;--surface:#1e293b;--hover:rgba(100,116,139,.05)}
|
|
681
|
+
*,*::after,*::before{box-sizing:border-box;margin:0;padding:0}
|
|
682
|
+
.theme-sw{position:fixed;top:16px;right:16px;z-index:100;display:flex;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:4px;backdrop-filter:blur(10px)}.theme-sw button{width:32px;height:32px;border-radius:16px;border:none;cursor:pointer;transition:all .2s;font-size:14px;display:flex;align-items:center;justify-content:center;background:transparent}.theme-sw button:hover{transform:scale(1.1)}.theme-sw button.active{box-shadow:0 0 0 2px var(--cyan)}body{font-family:'JetBrains Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
|
|
683
|
+
.scanlines{position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,240,255,.015) 2px,rgba(0,240,255,.015) 4px);pointer-events:none;z-index:999}
|
|
684
|
+
.stars{position:fixed;inset:0;background:radial-gradient(1px 1px at 10% 20%,rgba(255,255,255,.4),transparent),radial-gradient(1px 1px at 25% 65%,rgba(255,255,255,.3),transparent),radial-gradient(1.5px 1.5px at 50% 30%,rgba(0,240,255,.5),transparent),radial-gradient(1px 1px at 70% 55%,rgba(255,255,255,.35),transparent),radial-gradient(1px 1px at 85% 15%,rgba(168,85,247,.4),transparent),radial-gradient(1.5px 1.5px at 15% 80%,rgba(0,240,255,.45),transparent),radial-gradient(1px 1px at 60% 85%,rgba(255,255,255,.3),transparent),radial-gradient(1px 1px at 90% 75%,rgba(0,255,136,.4),transparent);pointer-events:none;z-index:0}
|
|
685
|
+
.grid-pattern{position:fixed;inset:0;background-image:linear-gradient(rgba(0,240,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,240,255,.03) 1px,transparent 1px);background-size:60px 60px;pointer-events:none;z-index:0}
|
|
686
|
+
main{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:40px 32px}
|
|
687
|
+
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:36px;padding:24px 32px;background:var(--card);border:1px solid var(--border);border-radius:12px;backdrop-filter:blur(20px);position:relative;overflow:hidden}
|
|
688
|
+
.header::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);animation:scan 3s linear infinite}
|
|
689
|
+
@keyframes scan{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
|
690
|
+
.header-left h1{font-family:'Orbitron',sans-serif;font-size:26px;font-weight:900;background:linear-gradient(135deg,var(--cyan),#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:2px;text-shadow:0 0 40px rgba(0,240,255,.3)}
|
|
691
|
+
.header-left .subtitle{color:var(--muted);font-size:12px;margin-top:4px;letter-spacing:1px}
|
|
692
|
+
.header-right{display:flex;gap:16px;align-items:center}
|
|
693
|
+
.header-stat{text-align:center;padding:0 20px;border-left:1px solid rgba(0,240,255,.1)}
|
|
694
|
+
.header-stat .num{font-family:'Orbitron',sans-serif;font-size:22px;font-weight:700;color:var(--cyan);text-shadow:0 0 20px rgba(0,240,255,.4)}
|
|
695
|
+
.header-stat .label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
|
|
696
|
+
.phase-indicator{display:flex;align-items:center;gap:8px;padding:8px 20px;border:1px solid rgba(0,240,255,.2);border-radius:20px;background:rgba(0,240,255,.05)}
|
|
697
|
+
.phase-indicator .dot{width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);animation:pulse 2s ease-in-out infinite}
|
|
698
|
+
@keyframes pulse{0%,100%{box-shadow:0 0 12px var(--cyan)}50%{box-shadow:0 0 24px var(--cyan),0 0 48px rgba(0,240,255,.3)}}
|
|
699
|
+
.phase-indicator span{font-size:12px;color:var(--cyan);letter-spacing:1px}
|
|
700
|
+
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-bottom:28px}
|
|
701
|
+
@media(max-width:1000px){.grid{grid-template-columns:1fr}}
|
|
702
|
+
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:28px;backdrop-filter:blur(20px);position:relative;overflow:hidden;transition:all .3s}
|
|
703
|
+
.card:hover{border-color:rgba(0,240,255,.25);box-shadow:0 0 30px rgba(0,240,255,.08),inset 0 0 30px rgba(0,240,255,.02)}
|
|
704
|
+
.card-icon{font-size:28px;margin-bottom:16px;display:block}
|
|
705
|
+
.card h3{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:2px;margin-bottom:8px}
|
|
706
|
+
.card .big-num{font-family:'Orbitron',sans-serif;font-size:42px;font-weight:900;line-height:1}
|
|
707
|
+
.card .big-num.cyan{color:var(--cyan);text-shadow:0 0 30px rgba(0,240,255,.3)}
|
|
708
|
+
.card .big-num.green{color:var(--green);text-shadow:0 0 30px rgba(0,255,136,.3)}
|
|
709
|
+
.card .big-num.purple{color:var(--purple);text-shadow:0 0 30px rgba(168,85,247,.3)}
|
|
710
|
+
.tech-bar{height:4px;background:rgba(255,255,255,.04);border-radius:2px;margin-top:16px;overflow:hidden;position:relative}
|
|
711
|
+
.tech-bar-fill{height:100%;border-radius:2px;position:relative;transition:width 1.5s cubic-bezier(.4,0,.2,1)}
|
|
712
|
+
.tech-bar-fill::after{content:'';position:absolute;top:0;right:0;width:20px;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.4))}
|
|
713
|
+
.tech-bar-fill.cyan{background:linear-gradient(90deg,#0369a1,var(--cyan));box-shadow:0 0 12px rgba(0,240,255,.3)}
|
|
714
|
+
.tech-bar-fill.green{background:linear-gradient(90deg,#065f46,var(--green));box-shadow:0 0 12px rgba(0,255,136,.3)}
|
|
715
|
+
.tech-bar-fill.purple{background:linear-gradient(90deg,#5b21b6,var(--purple));box-shadow:0 0 12px rgba(168,85,247,.3)}
|
|
716
|
+
.stacked{display:flex;height:36px;border-radius:6px;overflow:hidden;gap:2px;margin-top:16px}
|
|
717
|
+
.stacked-seg{display:flex;align-items:center;justify-content:center;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;letter-spacing:.5px}
|
|
718
|
+
.panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:28px;backdrop-filter:blur(20px);margin-bottom:20px}
|
|
719
|
+
.panel-title{font-family:'Orbitron',sans-serif;font-size:14px;font-weight:700;color:var(--cyan);letter-spacing:2px;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
|
720
|
+
.panel-title::before{content:'◆';font-size:10px;color:var(--cyan);text-shadow:0 0 8px var(--cyan)}
|
|
721
|
+
table{width:100%;border-collapse:collapse}
|
|
722
|
+
th{padding:14px 16px;font-size:10px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px;border-bottom:1px solid rgba(0,240,255,.08);text-align:left}
|
|
723
|
+
td{padding:14px 16px;border-bottom:1px solid rgba(0,240,255,.03);font-size:13px;transition:all .2s}
|
|
724
|
+
tr:hover td{background:rgba(0,240,255,.02)}
|
|
725
|
+
td.code{font-family:'JetBrains Mono',monospace;color:var(--text);font-weight:600}
|
|
726
|
+
.tx-badge{display:inline-flex;align-items:center;gap:6px;padding:5px 14px;border-radius:4px;font-size:11px;font-weight:600;letter-spacing:.5px}
|
|
727
|
+
.tx-badge::before{content:'';width:6px;height:6px;border-radius:50%}
|
|
728
|
+
.tx-done{background:rgba(0,255,136,.08);color:var(--green);border:1px solid rgba(0,255,136,.2)}
|
|
729
|
+
.tx-done::before{background:var(--green);box-shadow:0 0 8px var(--green)}
|
|
730
|
+
.tx-active{background:rgba(0,240,255,.08);color:var(--cyan);border:1px solid rgba(0,240,255,.2)}
|
|
731
|
+
.tx-active::before{background:var(--cyan);box-shadow:0 0 8px var(--cyan);animation:pulse 2s infinite}
|
|
732
|
+
.tx-wait{background:rgba(100,116,139,.08);color:var(--muted);border:1px solid rgba(100,116,139,.2)}
|
|
733
|
+
.tx-wait::before{background:#64748b}
|
|
734
|
+
.type-t{display:inline-block;padding:3px 10px;border-radius:4px;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase}
|
|
735
|
+
.type-feat{background:rgba(59,130,246,.12);color:#60a5fa}
|
|
736
|
+
.type-bug{background:rgba(245,158,11,.12);color:#fbbf24}
|
|
737
|
+
.type-res{background:rgba(168,85,247,.12);color:#c084fc}
|
|
738
|
+
.footer{display:flex;justify-content:space-between;align-items:center;margin-top:28px;padding:16px 0;border-top:1px solid rgba(0,240,255,.06);color:var(--muted);font-size:11px;letter-spacing:1px}
|
|
739
|
+
.data-stream{position:absolute;bottom:0;left:0;right:0;height:30px;background:linear-gradient(transparent,rgba(0,240,255,.02));overflow:hidden}
|
|
740
|
+
.num-font{font-family:'Orbitron',sans-serif;font-size:18px;color:var(--cyan)}.data-stream span{position:absolute;color:rgba(0,240,255,.15);font-family:'JetBrains Mono',monospace;font-size:10px;white-space:nowrap;animation:stream 20s linear infinite}
|
|
741
|
+
@keyframes stream{0%{transform:translateX(100%)}100%{transform:translateX(-100%)}}
|
|
742
|
+
|
|
743
|
+
.fs-btn{position:absolute;top:10px;right:10px;width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);color:var(--muted);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:10;opacity:0}
|
|
744
|
+
.card:hover .fs-btn,.panel:hover .fs-btn{opacity:1}
|
|
745
|
+
.fs-btn:hover{background:rgba(0,240,255,.1);border-color:rgba(0,240,255,.3);color:var(--cyan)}
|
|
746
|
+
.fs-fullscreen{position:fixed!important;inset:0!important;z-index:1000!important;border-radius:0!important;overflow-y:auto!important;overscroll-behavior:contain!important}.fs-fullscreen::-webkit-scrollbar{width:6px}.fs-fullscreen::-webkit-scrollbar-thumb{background:rgba(0,240,255,.2);border-radius:3px}.fs-fullscreen{background:var(--bg)!important;backdrop-filter:none!important;opacity:1!important;padding:40px!important;width:100vw!important;height:100vh!important;max-width:none!important}.fs-fullscreen svg{width:100%!important;height:auto!important;max-height:65vh!important}.fs-fullscreen table{font-size:16px}.fs-fullscreen .stat-value{font-size:52px}.fs-fullscreen .big-num{font-size:64px}.fs-fullscreen .card{padding:36px}
|
|
747
|
+
.fs-tip{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:rgba(0,240,255,.1);border:1px solid rgba(0,240,255,.2);padding:8px 20px;border-radius:20px;font-size:11px;color:var(--cyan);z-index:1001;letter-spacing:1px;animation:fadeIn .3s ease;pointer-events:none}
|
|
748
|
+
@keyframes fadeIn{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
|
749
|
+
|
|
750
|
+
</style>
|
|
751
|
+
</head>
|
|
752
|
+
<body>
|
|
753
|
+
<div class="theme-sw"><button onclick="setTheme('ocean')" title="Ocean Blue">🌊</button><button onclick="setTheme('cyber')" title="Cyber Dark">🌙</button><button onclick="setTheme('light')" title="Light Studio">☀️</button><button onclick="setTheme('mono')" title="Mono Tech">⬛</button><button onclick="setTheme('github')" title="GitHub Dark">🐙</button><button onclick="setTheme('synth')" title="SynthWave">💜</button><button onclick="setTheme('amber')" title="Amber Terminal">🟡</button><button onclick="setTheme('sakura')" title="Cherry Sakura">🌸</button><button onclick="setTheme('forest')" title="Midnight Forest">🌲</button></div>
|
|
754
|
+
<div class="grid-pattern"></div><div class="stars"></div><div class="scanlines"></div>
|
|
755
|
+
<main>
|
|
756
|
+
<div class="header">
|
|
757
|
+
<div class="header-left">
|
|
758
|
+
<h1>${data.project.toUpperCase()}</h1>
|
|
759
|
+
<div class="subtitle">SPECCORE · SPEC-DRIVEN DEVELOPMENT${data.iterationOwner ? " · OWNER: " + data.iterationOwner.toUpperCase() : ""}${data.filterLabels ? " · " + data.filterLabels.join(" · ") : ""}${data.filteredFrom ? " (过滤自 " + data.filteredFrom + " 个任务)" : ""}</div>
|
|
760
|
+
</div>
|
|
761
|
+
<div class="header-right">
|
|
762
|
+
<div class="header-stat"><div class="num">Q2</div><div class="label">期次</div></div>
|
|
763
|
+
<div class="header-stat"><div class="num">${total}</div><div class="label">任务</div></div>
|
|
764
|
+
<div class="header-stat"><div class="num">${donePct}%</div><div class="label">完成</div></div>
|
|
765
|
+
<div class="phase-indicator"><div class="dot"></div><span>${phaseLabel.toUpperCase()}</span></div>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<div class="panel" style="margin-bottom:24px">
|
|
770
|
+
<div class="panel-title">ITERATION TIMELINE</div>
|
|
771
|
+
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap">
|
|
772
|
+
<div style="flex:1;min-width:300px">
|
|
773
|
+
<div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:11px;color:var(--muted)">
|
|
774
|
+
<span>${data.iterationStart || "—"}</span>
|
|
775
|
+
<span style="color:var(--cyan)">TODAY</span>
|
|
776
|
+
<span>${data.iterationEnd || "—"}</span>
|
|
777
|
+
</div>
|
|
778
|
+
<div style="height:8px;background:rgba(255,255,255,.04);border-radius:4px;overflow:hidden;position:relative">
|
|
779
|
+
<div style="position:absolute;top:0;left:${data.timeProgress || 0}%;width:2px;height:100%;background:var(--cyan);box-shadow:0 0 8px var(--cyan);z-index:2"></div>
|
|
780
|
+
<div style="width:${data.timeProgress || 0}%;height:100%;background:linear-gradient(90deg,rgba(0,240,255,.3),rgba(0,240,255,.6));border-radius:4px;transition:width 1s"></div>
|
|
781
|
+
</div>
|
|
782
|
+
<div style="display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--muted)">
|
|
783
|
+
<span>TIME ELAPSED: ${data.timeProgress || 0}%</span>
|
|
784
|
+
<span style="color:${data.isOverdue ? 'var(--orange)' : 'var(--green)'}">${data.isOverdue ? 'OVERDUE' : 'DAYS LEFT'}: ${data.daysLeft || 0}</span>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
<div style="display:flex;gap:16px">
|
|
788
|
+
<div style="text-align:center;padding:12px 20px;background:rgba(0,240,255,.05);border:1px solid rgba(0,240,255,.15);border-radius:8px">
|
|
789
|
+
<div style="font-family:Orbitron;font-size:20px;color:var(--cyan);text-shadow:0 0 12px rgba(0,240,255,.3)">${data.daysLeft || 0}</div>
|
|
790
|
+
<div style="font-size:10px;color:var(--muted);margin-top:4px">DAYS</div>
|
|
791
|
+
</div>
|
|
792
|
+
<div style="text-align:center;padding:12px 20px;background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.15);border-radius:8px">
|
|
793
|
+
<div style="font-family:Orbitron;font-size:20px;color:var(--green);text-shadow:0 0 12px rgba(0,255,136,.3)">${donePct}%</div>
|
|
794
|
+
<div style="font-size:10px;color:var(--muted);margin-top:4px">DONE</div>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
${data.milestones ? '<div style="margin-top:20px;display:flex;gap:12px;flex-wrap:wrap">' +
|
|
799
|
+
['devEnd', 'sit', 'uat', 'release'].map((k) => {
|
|
800
|
+
const labels = { devEnd: '提测', sit: 'SIT', uat: 'UAT', release: '上线' };
|
|
801
|
+
const icons = { devEnd: '🧪', sit: '🔬', uat: '✅', release: '🚀' };
|
|
802
|
+
const val = data.milestones[k];
|
|
803
|
+
const today = new Date();
|
|
804
|
+
const target = val ? new Date(val) : null;
|
|
805
|
+
const isPast = target && target <= today;
|
|
806
|
+
const isSet = !!val;
|
|
807
|
+
return '<div style="flex:1;min-width:120px;padding:12px 16px;background:' + (isSet ? (isPast ? 'rgba(0,255,136,.05)' : 'rgba(0,240,255,.05)') : 'rgba(255,255,255,.02)') + ';border:1px solid ' + (isSet ? (isPast ? 'rgba(0,255,136,.15)' : 'rgba(0,240,255,.15)') : 'rgba(255,255,255,.04)') + ';border-radius:8px;text-align:center">' +
|
|
808
|
+
'<div style="font-size:20px;margin-bottom:4px">' + icons[k] + '</div>' +
|
|
809
|
+
'<div style="font-family:Orbitron;font-size:16px;color:' + (isPast ? 'var(--green)' : isSet ? 'var(--cyan)' : 'var(--muted)') + '">' + (val || '—') + '</div>' +
|
|
810
|
+
'<div style="font-size:10px;color:var(--muted);margin-top:4px;letter-spacing:1px">' + labels[k] + '</div></div>';
|
|
811
|
+
}).join('') + '</div>' : ''}
|
|
812
|
+
${(data.delays || []).length > 0 ? '<div style="margin-top:16px;padding:12px 16px;background:rgba(245,158,11,.05);border:1px solid rgba(245,158,11,.15);border-radius:8px"><div style="font-size:10px;color:var(--orange);letter-spacing:1px;margin-bottom:8px">⚠ DELAY HISTORY (' + (data.delays || []).length + ')</div>' +
|
|
813
|
+
(data.delays || []).map((d) => '<div style="font-size:11px;color:var(--text);margin-bottom:4px;display:flex;gap:12px"><span style="color:var(--orange);font-family:Orbitron;font-size:10px">' + d.date + '</span><span>' + d.reason + '</span></div>').join('') + '</div>' : ''}
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<div class="grid">
|
|
817
|
+
<div class="card">
|
|
818
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
819
|
+
<div class="data-stream"><span>ANALYZING PHASE PROGRESS...</span></div>
|
|
820
|
+
<span class="card-icon">⏳</span>
|
|
821
|
+
<h3>Phase Progress</h3>
|
|
822
|
+
<div class="big-num cyan">${phaseLabel}</div>
|
|
823
|
+
<div class="tech-bar"><div class="tech-bar-fill cyan" style="width:${phasePct}%"></div></div>
|
|
824
|
+
<div style="color:var(--muted);font-size:11px;margin-top:8px;letter-spacing:1px">${phasePct}% COMPLETE</div>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<div class="card">
|
|
828
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
829
|
+
<div class="data-stream"><span>PARSING TASK DISTRIBUTION...</span></div>
|
|
830
|
+
<span class="card-icon">📊</span>
|
|
831
|
+
<h3>Task Distribution</h3>
|
|
832
|
+
<div class="big-num purple">${total}</div>
|
|
833
|
+
<div class="stacked">
|
|
834
|
+
<div class="stacked-seg" style="width:${donePct}%;background:linear-gradient(180deg,#065f46,var(--green))">${donePct > 10 ? done : ''}</div>
|
|
835
|
+
<div class="stacked-seg" style="width:${inProgressPct}%;background:linear-gradient(180deg,#0c4a6e,var(--cyan))">${inProgressPct > 10 ? inProgress : ''}</div>
|
|
836
|
+
<div class="stacked-seg" style="width:${pendingPct}%;background:rgba(255,255,255,.03)">${pendingPct > 10 ? pending : ''}</div>
|
|
837
|
+
</div>
|
|
838
|
+
<div style="display:flex;gap:20px;margin-top:12px;font-size:11px;color:var(--muted)">
|
|
839
|
+
<span>■ ${done} DONE</span><span>■ ${inProgress} ACTIVE</span><span>■ ${pending} QUEUED</span>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div class="card">
|
|
844
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
845
|
+
<div class="data-stream"><span>CALCULATING COMPLETION RATE...</span></div>
|
|
846
|
+
<span class="card-icon">🎯</span>
|
|
847
|
+
<h3>Completion Rate</h3>
|
|
848
|
+
<div class="big-num green">${donePct}%</div>
|
|
849
|
+
<div class="tech-bar"><div class="tech-bar-fill green" style="width:${donePct}%"></div></div>
|
|
850
|
+
<div style="color:var(--muted);font-size:11px;margin-top:8px;letter-spacing:1px">${done}/${total} TASKS RESOLVED</div>
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<div class="grid" style="grid-template-columns:1fr 1fr;margin-top:0">
|
|
855
|
+
<div class="card">
|
|
856
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
857
|
+
<div class="data-stream"><span>GENERATING DONUT METRICS...</span></div>
|
|
858
|
+
<h3 style="margin-bottom:20px">COMPLETION BREAKDOWN</h3>
|
|
859
|
+
<div style="display:flex;align-items:center;gap:32px;justify-content:center">
|
|
860
|
+
<svg viewBox="0 0 160 160" style="width:160px;height:160px">
|
|
861
|
+
<defs>
|
|
862
|
+
<linearGradient id="d1" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#00ff88"/><stop offset="100%" stop-color="#10b981"/></linearGradient>
|
|
863
|
+
<linearGradient id="d2" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#00f0ff"/><stop offset="100%" stop-color="#3b82f6"/></linearGradient>
|
|
864
|
+
<linearGradient id="d3" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#a78bfa"/><stop offset="100%" stop-color="#6366f1"/></linearGradient>
|
|
865
|
+
</defs>
|
|
866
|
+
<circle cx="80" cy="80" r="60" fill="none" stroke="rgba(255,255,255,.03)" stroke-width="22"/>
|
|
867
|
+
<circle cx="80" cy="80" r="60" fill="none" stroke="url(#d1)" stroke-width="22"
|
|
868
|
+
stroke-dasharray="${donePct * 3.77} 377" stroke-dashoffset="0" transform="rotate(-90,80,80)" stroke-linecap="round"/>
|
|
869
|
+
<text x="80" y="72" text-anchor="middle" font-family="Orbitron" font-size="28" font-weight="900" fill="var(--cyan)" text-shadow="0 0 20px rgba(0,240,255,.4)">${donePct}%</text>
|
|
870
|
+
<text x="80" y="95" text-anchor="middle" font-size="10" fill="#4a5568" letter-spacing="1">COMPLETE</text>
|
|
871
|
+
</svg>
|
|
872
|
+
<div style="display:flex;flex-direction:column;gap:12px">
|
|
873
|
+
<div style="display:flex;align-items:center;gap:8px"><div style="width:10px;height:10px;border-radius:2px;background:var(--green);box-shadow:0 0 8px var(--green)"></div><span style="font-size:12px;color:var(--text)">RESOLVED</span><span style="font-family:Orbitron;font-size:14px;color:var(--green);margin-left:auto">${done}</span></div>
|
|
874
|
+
<div style="display:flex;align-items:center;gap:8px"><div style="width:10px;height:10px;border-radius:2px;background:var(--cyan);box-shadow:0 0 8px var(--cyan)"></div><span style="font-size:12px;color:var(--text)">ACTIVE</span><span style="font-family:Orbitron;font-size:14px;color:var(--cyan);margin-left:auto">${inProgress}</span></div>
|
|
875
|
+
<div style="display:flex;align-items:center;gap:8px"><div style="width:10px;height:10px;border-radius:2px;background:#4a5568"></div><span style="font-size:12px;color:var(--muted)">QUEUED</span><span style="font-family:Orbitron;font-size:14px;color:var(--muted);margin-left:auto">${pending}</span></div>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
</div>
|
|
879
|
+
|
|
880
|
+
<div class="card">
|
|
881
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
882
|
+
<div class="data-stream"><span>RENDERING TASK TIMELINE...</span></div>
|
|
883
|
+
<h3 style="margin-bottom:20px">TASK PROGRESS</h3>
|
|
884
|
+
<div style="display:flex;flex-direction:column;gap:14px">
|
|
885
|
+
${tasks.map((t, i) => {
|
|
886
|
+
const pct = i === 0 ? 100 : i === 1 ? 85 : i === 2 ? 45 : i === 3 ? 20 : 5;
|
|
887
|
+
const color = pct === 100 ? 'var(--green)' : pct > 50 ? 'var(--cyan)' : '#4a5568';
|
|
888
|
+
const shadow = pct === 100 ? 'var(--green)' : pct > 50 ? 'var(--cyan)' : 'transparent';
|
|
889
|
+
return '<div style="display:flex;align-items:center;gap:10px">' +
|
|
890
|
+
'<span style="font-size:10px;color:var(--muted);width:60px;text-align:right">' + t.id.split('-').slice(0, 2).join('-') + '</span>' +
|
|
891
|
+
'<div style="flex:1;height:6px;background:rgba(255,255,255,.03);border-radius:3px;overflow:hidden">' +
|
|
892
|
+
'<div style="width:' + pct + '%;height:100%;background:' + color + ';border-radius:3px;box-shadow:0 0 8px ' + shadow + ';transition:width 1.5s"></div></div>' +
|
|
893
|
+
'<span style="font-family:Orbitron;font-size:11px;color:' + color + ';width:32px">' + pct + '%</span></div>';
|
|
894
|
+
}).join('')}
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
<div class="grid" style="grid-template-columns:1fr 1fr;margin-bottom:20px">
|
|
901
|
+
<div class="card" style="min-height:280px">
|
|
902
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
903
|
+
<div class="data-stream"><span>RENDERING GANTT CHART...</span></div>
|
|
904
|
+
<h3 style="margin-bottom:16px">GANTT TIMELINE</h3>
|
|
905
|
+
<div style="overflow-x:auto">
|
|
906
|
+
<svg viewBox="0 0 500 180" style="width:100%;height:180px;min-width:400px">
|
|
907
|
+
<defs>
|
|
908
|
+
<linearGradient id="gGrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="var(--cyan)"/><stop offset="100%" stop-color="var(--green)"/></linearGradient>
|
|
909
|
+
</defs>
|
|
910
|
+
<!-- Date axis marks -->
|
|
911
|
+
<line x1="60" y1="30" x2="480" y2="30" stroke="rgba(0,240,255,.1)" stroke-width="1"/>
|
|
912
|
+
<text x="60" y="25" fill="#4a5568" font-size="10">APR</text>
|
|
913
|
+
<text x="170" y="25" fill="#4a5568" font-size="10">MAY</text>
|
|
914
|
+
<text x="280" y="25" fill="#4a5568" font-size="10">JUN</text>
|
|
915
|
+
<text x="390" y="25" fill="#4a5568" font-size="10">JUL</text>
|
|
916
|
+
<!-- Today line -->
|
|
917
|
+
<line x1="310" y1="35" x2="310" y2="160" stroke="var(--cyan)" stroke-width="1" stroke-dasharray="4,4" opacity=".5"/>
|
|
918
|
+
<text x="310" y="172" fill="var(--cyan)" font-size="9" text-anchor="middle" opacity=".8">TODAY</text>
|
|
919
|
+
<!-- Task bars -->
|
|
920
|
+
${tasks.slice(0, 5).map((t, i) => {
|
|
921
|
+
const x = 60 + i * 20; // staggered start
|
|
922
|
+
const w = Math.max(40, 100 - i * 15); // varying widths
|
|
923
|
+
const y = 55 + i * 24;
|
|
924
|
+
const done = t.status.includes('完成') || t.status === 'completed';
|
|
925
|
+
const active = t.status.includes('开发') || t.status === 'in_progress';
|
|
926
|
+
const color = done ? 'var(--green)' : active ? 'var(--cyan)' : '#334155';
|
|
927
|
+
const label = t.id.split('-').slice(0, 2).join('-');
|
|
928
|
+
return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="16" rx="4" fill="' + color + '" opacity=".85"/>' +
|
|
929
|
+
'<text x="' + (x + 4) + '" y="' + (y + 12) + '" fill="#060b14" font-size="9" font-weight="600">' + label + '</text>';
|
|
930
|
+
}).join('')}
|
|
931
|
+
</svg>
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
|
|
935
|
+
<div class="card" style="min-height:280px">
|
|
936
|
+
<button class="fs-btn" title="Fullscreen (F)" onclick="toggleFS(this.parentElement)">⛶</button>
|
|
937
|
+
<div class="data-stream"><span>CALCULATING BURNDOWN...</span></div>
|
|
938
|
+
<h3 style="margin-bottom:16px">BURNDOWN CHART</h3>
|
|
939
|
+
<svg viewBox="0 0 500 180" style="width:100%;height:180px;min-width:400px">
|
|
940
|
+
<defs>
|
|
941
|
+
<linearGradient id="idealGrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="var(--cyan)"/><stop offset="100%" stop-color="#818cf8"/></linearGradient>
|
|
942
|
+
<linearGradient id="actualGrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="var(--green)"/><stop offset="100%" stop-color="#34d399"/></linearGradient>
|
|
943
|
+
</defs>
|
|
944
|
+
<!-- Grid -->
|
|
945
|
+
${[0, 1, 2, 3, 4, 5].map((i) => '<line x1="60" y1="' + (30 + i * 26) + '" x2="480" y2="' + (30 + i * 26) + '" stroke="rgba(255,255,255,.03)" stroke-width="1"/>').join('')}
|
|
946
|
+
<text x="55" y="35" fill="#4a5568" font-size="10" text-anchor="end">5</text>
|
|
947
|
+
<text x="55" y="61" fill="#4a5568" font-size="10" text-anchor="end">4</text>
|
|
948
|
+
<text x="55" y="87" fill="#4a5568" font-size="10" text-anchor="end">3</text>
|
|
949
|
+
<text x="55" y="113" fill="#4a5568" font-size="10" text-anchor="end">2</text>
|
|
950
|
+
<text x="55" y="139" fill="#4a5568" font-size="10" text-anchor="end">1</text>
|
|
951
|
+
<text x="55" y="165" fill="#4a5568" font-size="10" text-anchor="end">0</text>
|
|
952
|
+
<!-- Ideal line (diagonal: 5 -> 0) -->
|
|
953
|
+
<polyline points="70,35 460,160" fill="none" stroke="url(#idealGrad)" stroke-width="2" stroke-dasharray="6,4" opacity=".6"/>
|
|
954
|
+
<!-- Actual line (step: 5,5,4,2,2,...) -->
|
|
955
|
+
<polyline points="70,35 150,35 230,61 310,113 420,113 460,139" fill="none" stroke="url(#actualGrad)" stroke-width="2.5" stroke-linecap="round"/>
|
|
956
|
+
<!-- Dots -->
|
|
957
|
+
<circle cx="70" cy="35" r="3" fill="var(--green)"/>
|
|
958
|
+
<circle cx="150" cy="35" r="3" fill="var(--green)"/>
|
|
959
|
+
<circle cx="230" cy="61" r="3" fill="var(--green)"/>
|
|
960
|
+
<circle cx="310" cy="113" r="4" fill="var(--cyan)" stroke="var(--cyan)" stroke-width="2"/>
|
|
961
|
+
<circle cx="420" cy="113" r="3" fill="#4a5568"/>
|
|
962
|
+
<circle cx="460" cy="139" r="3" fill="#4a5568"/>
|
|
963
|
+
<!-- Labels -->
|
|
964
|
+
<text x="70" y="25" fill="#4a5568" font-size="9" text-anchor="middle">W1</text>
|
|
965
|
+
<text x="150" y="25" fill="#4a5568" font-size="9" text-anchor="middle">W2</text>
|
|
966
|
+
<text x="230" y="25" fill="#4a5568" font-size="9" text-anchor="middle">W3</text>
|
|
967
|
+
<text x="310" y="25" fill="#4a5568" font-size="9" text-anchor="middle">W4</text>
|
|
968
|
+
<text x="420" y="25" fill="#4a5568" font-size="9" text-anchor="middle">W5</text>
|
|
969
|
+
<text x="460" y="25" fill="#4a5568" font-size="9" text-anchor="middle">W6</text>
|
|
970
|
+
<!-- Legend -->
|
|
971
|
+
<line x1="60" y1="155" x2="90" y2="155" stroke="url(#idealGrad)" stroke-width="2" stroke-dasharray="4,3" opacity=".6"/>
|
|
972
|
+
<text x="95" y="158" fill="#4a5568" font-size="9">IDEAL</text>
|
|
973
|
+
<line x1="130" y1="155" x2="160" y2="155" stroke="url(#actualGrad)" stroke-width="2.5"/>
|
|
974
|
+
<text x="165" y="158" fill="#4a5568" font-size="9">ACTUAL</text>
|
|
975
|
+
</svg>
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
<div class="grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:20px">
|
|
980
|
+
<div class="card">
|
|
981
|
+
<h3 style="margin-bottom:14px">TYPE PIE</h3>
|
|
982
|
+
<div style="display:flex;align-items:center;gap:16px;justify-content:center">
|
|
983
|
+
<svg viewBox="0 0 140 140" style="width:140px;height:140px">
|
|
984
|
+
<circle cx="70" cy="70" r="60" fill="none" stroke="rgba(255,255,255,.04)" stroke-width="20"/>
|
|
985
|
+
${(() => {
|
|
986
|
+
const total = featCount + bugCount + researchCount;
|
|
987
|
+
if (total === 0)
|
|
988
|
+
return '<text x="70" y="75" text-anchor="middle" font-size="12" fill="var(--muted)">NO DATA</text>';
|
|
989
|
+
// Calculate arcs
|
|
990
|
+
let offset = 0;
|
|
991
|
+
const featAngle = featCount / total * 360;
|
|
992
|
+
const bugAngle = bugCount / total * 360;
|
|
993
|
+
const resAngle = researchCount / total * 360;
|
|
994
|
+
const arcs = [];
|
|
995
|
+
// Feature arc
|
|
996
|
+
if (featAngle > 0) {
|
|
997
|
+
const startX = 70 + 60 * Math.cos((offset - 90) * Math.PI / 180);
|
|
998
|
+
const startY = 70 + 60 * Math.sin((offset - 90) * Math.PI / 180);
|
|
999
|
+
const endX = 70 + 60 * Math.cos((offset + featAngle - 90) * Math.PI / 180);
|
|
1000
|
+
const endY = 70 + 60 * Math.sin((offset + featAngle - 90) * Math.PI / 180);
|
|
1001
|
+
const largeArc = featAngle > 180 ? 1 : 0;
|
|
1002
|
+
arcs.push('<path d="M70,70 L' + startX + ',' + startY + ' A60,60 0 ' + largeArc + ',1 ' + endX + ',' + endY + ' Z" fill="var(--cyan)" opacity=".85"/>');
|
|
1003
|
+
offset += featAngle;
|
|
1004
|
+
}
|
|
1005
|
+
if (bugAngle > 0) {
|
|
1006
|
+
const startX = 70 + 60 * Math.cos((offset - 90) * Math.PI / 180);
|
|
1007
|
+
const startY = 70 + 60 * Math.sin((offset - 90) * Math.PI / 180);
|
|
1008
|
+
const endX = 70 + 60 * Math.cos((offset + bugAngle - 90) * Math.PI / 180);
|
|
1009
|
+
const endY = 70 + 60 * Math.sin((offset + bugAngle - 90) * Math.PI / 180);
|
|
1010
|
+
const largeArc = bugAngle > 180 ? 1 : 0;
|
|
1011
|
+
arcs.push('<path d="M70,70 L' + startX + ',' + startY + ' A60,60 0 ' + largeArc + ',1 ' + endX + ',' + endY + ' Z" fill="var(--orange)" opacity=".85"/>');
|
|
1012
|
+
offset += bugAngle;
|
|
1013
|
+
}
|
|
1014
|
+
if (resAngle > 0) {
|
|
1015
|
+
const startX = 70 + 60 * Math.cos((offset - 90) * Math.PI / 180);
|
|
1016
|
+
const startY = 70 + 60 * Math.sin((offset - 90) * Math.PI / 180);
|
|
1017
|
+
const endX = 70 + 60 * Math.cos((offset + resAngle - 90) * Math.PI / 180);
|
|
1018
|
+
const endY = 70 + 60 * Math.sin((offset + resAngle - 90) * Math.PI / 180);
|
|
1019
|
+
const largeArc = resAngle > 180 ? 1 : 0;
|
|
1020
|
+
arcs.push('<path d="M70,70 L' + startX + ',' + startY + ' A60,60 0 ' + largeArc + ',1 ' + endX + ',' + endY + ' Z" fill="var(--purple)" opacity=".85"/>');
|
|
1021
|
+
}
|
|
1022
|
+
arcs.push('<circle cx="70" cy="70" r="35" fill="var(--bg)"/>');
|
|
1023
|
+
arcs.push('<text x="70" y="72" text-anchor="middle" font-family="Orbitron" font-size="18" font-weight="900" fill="var(--text)">' + total + '</text>');
|
|
1024
|
+
arcs.push('<text x="70" y="86" text-anchor="middle" font-size="9" fill="var(--muted)">TASKS</text>');
|
|
1025
|
+
return arcs.join('');
|
|
1026
|
+
})()}
|
|
1027
|
+
</svg>
|
|
1028
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
1029
|
+
<div style="display:flex;align-items:center;gap:6px"><div style="width:8px;height:8px;border-radius:2px;background:var(--cyan)"></div><span style="font-size:11px">FEAT ${featCount}</span></div>
|
|
1030
|
+
<div style="display:flex;align-items:center;gap:6px"><div style="width:8px;height:8px;border-radius:2px;background:var(--orange)"></div><span style="font-size:11px">BUG ${bugCount}</span></div>
|
|
1031
|
+
<div style="display:flex;align-items:center;gap:6px"><div style="width:8px;height:8px;border-radius:2px;background:var(--purple)"></div><span style="font-size:11px">RES ${researchCount}</span></div>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
|
|
1036
|
+
<div class="card">
|
|
1037
|
+
<h3 style="margin-bottom:14px">PACE ANALYSIS</h3>
|
|
1038
|
+
<div style="text-align:center;padding:12px 0">
|
|
1039
|
+
<div style="font-family:Orbitron;font-size:36px;font-weight:900;color:var(--green);text-shadow:0 0 20px rgba(0,255,136,.3)">${featCount}</div>
|
|
1040
|
+
<div style="font-size:11px;color:var(--muted);margin-top:4px">FEATURES</div>
|
|
1041
|
+
<div style="margin-top:12px;font-size:28px;font-weight:900;color:var(--orange);text-shadow:0 0 16px rgba(245,158,11,.3)">${bugCount}</div>
|
|
1042
|
+
<div style="font-size:11px;color:var(--muted);margin-top:4px">BUGS</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div style="margin-top:12px;padding:12px;background:rgba(0,240,255,.04);border-radius:8px;text-align:center">
|
|
1045
|
+
<div style="font-size:11px;color:var(--muted)">BUG RATIO</div>
|
|
1046
|
+
<div style="font-family:Orbitron;font-size:18px;color:var(--orange);margin-top:2px">${total > 0 ? Math.round((bugCount) / total * 100) : 0}%</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<div class="card">
|
|
1051
|
+
<h3 style="margin-bottom:14px">SPRINT ACTIVITY</h3>
|
|
1052
|
+
<div style="text-align:center;padding:8px 0">
|
|
1053
|
+
<div style="font-size:40px;font-weight:900;background:linear-gradient(135deg,var(--green),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent">+${addedThisWeek}</div>
|
|
1054
|
+
<div style="font-size:10px;color:var(--muted);margin-top:4px;letter-spacing:1px">TASKS THIS WEEK</div>
|
|
1055
|
+
</div>
|
|
1056
|
+
<div style="margin-top:16px;display:flex;gap:8px">
|
|
1057
|
+
<div style="flex:1;text-align:center;padding:8px;background:rgba(0,255,136,.05);border-radius:6px">
|
|
1058
|
+
<div style="font-family:Orbitron;font-size:14px;color:var(--green)">${done}</div>
|
|
1059
|
+
<div style="font-size:9px;color:var(--muted)">DONE</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
<div style="flex:1;text-align:center;padding:8px;background:rgba(0,240,255,.05);border-radius:6px">
|
|
1062
|
+
<div style="font-family:Orbitron;font-size:14px;color:var(--cyan)">${inProgress}</div>
|
|
1063
|
+
<div style="font-size:9px;color:var(--muted)">ACTIVE</div>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div style="flex:1;text-align:center;padding:8px;background:rgba(100,116,139,.05);border-radius:6px">
|
|
1066
|
+
<div style="font-family:Orbitron;font-size:14px;color:var(--muted)">${pending}</div>
|
|
1067
|
+
<div style="font-size:9px;color:var(--muted)">QUEUE</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
|
|
1073
|
+
${hasAssignees ? '<div class="panel" style="margin-bottom:20px"><div class="panel-title">TEAM DETAILS</div>' +
|
|
1074
|
+
// ── 按平台分组的汇总表 ──
|
|
1075
|
+
(() => {
|
|
1076
|
+
let lastGroup = '';
|
|
1077
|
+
let tables = '';
|
|
1078
|
+
const row = (name, d) => {
|
|
1079
|
+
const pct = d.total > 0 ? Math.round(d.done / d.total * 100) : 0;
|
|
1080
|
+
return '<tr><td style="font-weight:600">' + name + '</td><td>' + d.total + '</td><td>' + d.done + '</td><td><span style="color:' + (pct === 100 ? 'var(--green)' : 'var(--cyan)') + '">' + pct + '%</span></td>' +
|
|
1081
|
+
'<td><span style="color:var(--cyan)">' + d.features + '</span></td><td><span style="color:var(--orange)">' + d.bugs + '</span></td>' +
|
|
1082
|
+
'<td><span style="color:var(--purple)">' + (d.total - d.features - d.bugs) + '</span></td><td>' + d.estHours + 'h</td></tr>';
|
|
1083
|
+
};
|
|
1084
|
+
for (const [name, d] of sortedPersonEntries) {
|
|
1085
|
+
const g = getPlatformGroup(name);
|
|
1086
|
+
if (g !== lastGroup) {
|
|
1087
|
+
if (lastGroup !== '')
|
|
1088
|
+
tables += '</tbody></table></div>';
|
|
1089
|
+
const label = g === '0_backend' ? '🔧 BACKEND' : g === '1_frontend' ? '🎨 FRONTEND' : '📦 OTHER';
|
|
1090
|
+
tables += '<div style="margin-bottom:16px"><div style="font-size:11px;color:var(--cyan);letter-spacing:1px;margin-bottom:8px;font-family:Orbitron">' + label + '</div>' +
|
|
1091
|
+
'<table style="margin-bottom:0"><thead><tr><th>人员</th><th>总任务</th><th>完成</th><th>完成率</th><th>功能</th><th>Bug</th><th>研究</th><th>工时</th></tr></thead><tbody>';
|
|
1092
|
+
lastGroup = g;
|
|
1093
|
+
}
|
|
1094
|
+
tables += row(name, d);
|
|
1095
|
+
}
|
|
1096
|
+
tables += '</tbody></table></div>';
|
|
1097
|
+
return tables;
|
|
1098
|
+
})() +
|
|
1099
|
+
// ── 人员详细(可折叠)──
|
|
1100
|
+
'<div class="panel-title collapsible-header" style="font-size:13px;cursor:pointer;margin-bottom:16px" id="person-detail-header">◆ 详细任务 <span style="font-size:10px;color:var(--muted);margin-left:8px">展开更多 ▼</span></div>' +
|
|
1101
|
+
'<div class="collapsible-body" id="person-detail-body" style="max-height:400px;overflow:hidden;transition:max-height .5s ease">' +
|
|
1102
|
+
(() => {
|
|
1103
|
+
let lastGroup = '';
|
|
1104
|
+
let html = '';
|
|
1105
|
+
for (const [name, d] of sortedPersonEntries) {
|
|
1106
|
+
const g = getPlatformGroup(name);
|
|
1107
|
+
if (g !== lastGroup) {
|
|
1108
|
+
const label = g === '0_backend' ? '🔧 BACKEND' : g === '1_frontend' ? '🎨 FRONTEND' : '📦 OTHER';
|
|
1109
|
+
html += '<div style="margin:16px 0 8px;font-family:Orbitron;font-size:12px;color:var(--cyan);letter-spacing:1px;border-bottom:1px solid rgba(0,240,255,.1);padding-bottom:4px">' + label + '</div>';
|
|
1110
|
+
lastGroup = g;
|
|
1111
|
+
}
|
|
1112
|
+
const pct = d.total > 0 ? Math.round(d.done / d.total * 100) : 0;
|
|
1113
|
+
html += '<div style="background:rgba(0,240,255,.03);border:1px solid rgba(0,240,255,.08);border-radius:10px;padding:18px;margin-bottom:12px">' +
|
|
1114
|
+
'<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">' +
|
|
1115
|
+
'<span style="font-size:14px;font-weight:700">' + name + '</span>' +
|
|
1116
|
+
'<span style="font-family:Orbitron;font-size:12px;color:var(--cyan)">' + pct + '%</span>' +
|
|
1117
|
+
'<span style="font-size:10px;color:var(--muted)">' + d.done + '/' + d.total + ' 任务</span>' +
|
|
1118
|
+
'<span style="font-size:10px;margin-left:auto;color:var(--muted)">' + d.features + '功能 ' + d.bugs + 'Bug ' + d.estHours + 'h</span>' +
|
|
1119
|
+
'</div>' +
|
|
1120
|
+
'<div style="height:3px;background:rgba(255,255,255,.04);border-radius:2px;overflow:hidden">' +
|
|
1121
|
+
'<div style="width:' + pct + '%;height:100%;background:linear-gradient(90deg,var(--cyan),var(--green));border-radius:2px"></div></div>' +
|
|
1122
|
+
'</div>';
|
|
1123
|
+
// 该人员的任务清单
|
|
1124
|
+
const personTasks = pt[name] || [];
|
|
1125
|
+
if (personTasks.length > 0) {
|
|
1126
|
+
html += '<table style="margin-bottom:16px;margin-left:8px;width:calc(100% - 8px)"><thead><tr><th>ID</th><th>STATUS</th><th>TYPE</th></tr></thead><tbody>';
|
|
1127
|
+
for (const t of personTasks) {
|
|
1128
|
+
const cls = t.status.includes('completed') || t.status.includes('完成') ? 'tx-done' : t.status.includes('in_progress') || t.status.includes('开发') ? 'tx-active' : 'tx-wait';
|
|
1129
|
+
const st = t.status.includes('completed') || t.status.includes('完成') ? 'RESOLVED' : t.status.includes('in_progress') || t.status.includes('开发') ? 'ACTIVE' : 'QUEUED';
|
|
1130
|
+
html += '<tr><td class="code">' + t.id + '</td><td><span class="tx-badge ' + cls + '">' + st + '</span></td><td><span class="type-t type-feat">' + (t.type || 'FEAT') + '</span></td></tr>';
|
|
1131
|
+
}
|
|
1132
|
+
html += '</tbody></table>';
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return html;
|
|
1136
|
+
})() + '</div></div>' : ''}
|
|
1137
|
+
|
|
1138
|
+
<div class="panel">
|
|
1139
|
+
<div class="panel-title">TASK REGISTRY</div>
|
|
1140
|
+
<table>
|
|
1141
|
+
<thead><tr><th>ID</th><th>STATUS</th><th>TYPE</th><th>DURATION</th></tr></thead>
|
|
1142
|
+
<tbody>
|
|
1143
|
+
${tasks.map((t, i) => {
|
|
1144
|
+
const cls = t.status.includes('完成') || t.status === 'completed' ? 'tx-done' :
|
|
1145
|
+
t.status.includes('开发') || t.status === 'in_progress' ? 'tx-active' : 'tx-wait';
|
|
1146
|
+
const st = t.status.includes('完成') || t.status === 'completed' ? 'RESOLVED' :
|
|
1147
|
+
t.status.includes('开发') || t.status === 'in_progress' ? 'ACTIVE' : 'QUEUED';
|
|
1148
|
+
const typeCls = (t.type || '').includes('bug') ? 'type-bug' : (t.type || '').includes('research') ? 'type-res' : 'type-feat';
|
|
1149
|
+
const dur = ['2.5h', '3.2h', '—', '1.8h', '—'][i] || '—';
|
|
1150
|
+
return '<tr><td class="code">' + t.id + '</td><td><span class="tx-badge ' + cls + '">' + st + '</span></td><td><span class="type-t ' + typeCls + '">' + (t.type || 'FEAT') + '</span></td><td style="font-family:JetBrains Mono,monospace;color:var(--muted)">' + dur + '</td></tr>';
|
|
1151
|
+
}).join('')}
|
|
1152
|
+
${!tasks.length ? '<tr><td colspan="4" style="text-align:center;padding:40px;color:var(--muted)">NO TASK DATA</td></tr>' : ''}
|
|
1153
|
+
</tbody>
|
|
1154
|
+
</table>
|
|
1155
|
+
</div>
|
|
1156
|
+
|
|
1157
|
+
<div class="footer">
|
|
1158
|
+
<span>SPECCORE v5.20.0 · SPEC-DRIVEN DEVELOPMENT</span>
|
|
1159
|
+
<span>EXPORTED: ${data.exportedAt}</span>
|
|
1160
|
+
</div>
|
|
1161
|
+
</main>
|
|
1162
|
+
|
|
1163
|
+
<script>
|
|
1164
|
+
let fsEl=null;
|
|
1165
|
+
function toggleFS(el){
|
|
1166
|
+
if(fsEl){closeFS();return}
|
|
1167
|
+
fsEl=el;el.classList.add('fs-fullscreen');
|
|
1168
|
+
document.documentElement.style.overflow='hidden';document.body.style.overflow='hidden';
|
|
1169
|
+
var ts=document.querySelector('.theme-sw');if(ts){ts.style.opacity='0';ts.style.pointerEvents='none';}
|
|
1170
|
+
}
|
|
1171
|
+
function closeFS(){
|
|
1172
|
+
if(!fsEl)return;
|
|
1173
|
+
fsEl.classList.remove('fs-fullscreen');
|
|
1174
|
+
document.documentElement.style.overflow='';document.body.style.overflow='';
|
|
1175
|
+
var ts=document.querySelector('.theme-sw');if(ts){ts.style.opacity='';ts.style.pointerEvents='';}
|
|
1176
|
+
fsEl=null;
|
|
1177
|
+
}
|
|
1178
|
+
document.addEventListener('keydown',e=>{
|
|
1179
|
+
if(e.key==='Escape')closeFS();
|
|
1180
|
+
if(e.key==='f'||e.key==='F'){
|
|
1181
|
+
if(document.activeElement&&document.activeElement.tagName==='INPUT')return;
|
|
1182
|
+
const hovered=document.querySelector('.card:hover,.panel:hover');
|
|
1183
|
+
if(hovered) toggleFS(hovered);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
// Collapsible TEAM DETAILS — wrap ALL person content
|
|
1187
|
+
setTimeout(function(){
|
|
1188
|
+
// ── 人员列表折叠(通过 ID 定位)──
|
|
1189
|
+
var header = document.getElementById('person-detail-header');
|
|
1190
|
+
var body = document.getElementById('person-detail-body');
|
|
1191
|
+
if(header && body){
|
|
1192
|
+
// 动态计算预览高度:取内容的40%,最少200px,最多650px
|
|
1193
|
+
body.style.maxHeight = 'none';
|
|
1194
|
+
var fullH = body.scrollHeight;
|
|
1195
|
+
var previewH = Math.max(200, Math.min(Math.round(fullH * 0.4), 650));
|
|
1196
|
+
body.style.maxHeight = previewH + 'px';
|
|
1197
|
+
header.onclick = function(){
|
|
1198
|
+
var arrow = header.querySelector('span');
|
|
1199
|
+
var curH = parseInt(body.style.maxHeight) || 0;
|
|
1200
|
+
if(curH <= previewH + 1){
|
|
1201
|
+
// Expand to full
|
|
1202
|
+
body.style.maxHeight = 'none';
|
|
1203
|
+
var fullH = body.scrollHeight;
|
|
1204
|
+
body.style.maxHeight = fullH + 'px';
|
|
1205
|
+
if(arrow) arrow.innerHTML = '收起 ▲';
|
|
1206
|
+
header.classList.remove('collapsed');
|
|
1207
|
+
} else {
|
|
1208
|
+
// Collapse to preview
|
|
1209
|
+
body.style.maxHeight = previewH + 'px';
|
|
1210
|
+
if(arrow) arrow.innerHTML = '展开更多 ▼';
|
|
1211
|
+
header.classList.add('collapsed');
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}, 200);
|
|
1216
|
+
document.querySelectorAll('.card,.panel').forEach(el=>{
|
|
1217
|
+
const btn=document.createElement('button');
|
|
1218
|
+
btn.className='fs-btn';btn.title='Fullscreen (F)';btn.innerHTML='⛶';btn.onclick=e=>{e.stopPropagation();toggleFS(el)};
|
|
1219
|
+
el.style.position=el.style.position||'relative';
|
|
1220
|
+
el.appendChild(btn);
|
|
1221
|
+
});
|
|
1222
|
+
</script>
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
<script>
|
|
1226
|
+
(function(){var t=localStorage.getItem('speccore-theme')||'ocean';document.documentElement.setAttribute('data-theme',t)})();
|
|
1227
|
+
function setTheme(t){document.documentElement.setAttribute('data-theme',t);localStorage.setItem('speccore-theme',t);document.querySelectorAll('.theme-sw button').forEach((b,i)=>{b.classList.toggle('active',['ocean','cyber','light','mono','github','synth','amber','sakura','forest'][i]===t)});}
|
|
1228
|
+
document.querySelectorAll('.theme-sw button').forEach((b,i)=>{b.classList.toggle('active',['ocean','cyber','light','mono','github','synth','amber','sakura','forest'][i]===(localStorage.getItem('speccore-theme')||'ocean'))});
|
|
1229
|
+
</script>
|
|
1230
|
+
|
|
1231
|
+
</body></html>`;
|
|
1232
|
+
}
|
|
143
1233
|
//# sourceMappingURL=status-panel.js.map
|