speccore 5.25.2 → 5.26.2

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.
Files changed (62) hide show
  1. package/README.md +93 -77
  2. package/dist/cli.js +123 -45
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/ask.d.ts +4 -5
  5. package/dist/commands/ask.d.ts.map +1 -1
  6. package/dist/commands/ask.js +257 -292
  7. package/dist/commands/ask.js.map +1 -1
  8. package/dist/commands/dashboard.d.ts +12 -0
  9. package/dist/commands/dashboard.d.ts.map +1 -1
  10. package/dist/commands/dashboard.js +442 -77
  11. package/dist/commands/dashboard.js.map +1 -1
  12. package/dist/commands/dev.d.ts +2 -0
  13. package/dist/commands/dev.d.ts.map +1 -1
  14. package/dist/commands/dev.js +282 -290
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doc2spec.d.ts +2 -0
  17. package/dist/commands/doc2spec.d.ts.map +1 -1
  18. package/dist/commands/doc2spec.js +22 -6
  19. package/dist/commands/doc2spec.js.map +1 -1
  20. package/dist/commands/help.d.ts.map +1 -1
  21. package/dist/commands/help.js +39 -0
  22. package/dist/commands/help.js.map +1 -1
  23. package/dist/commands/init.js +15 -5
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/commands/spec2doc.d.ts +11 -0
  26. package/dist/commands/spec2doc.d.ts.map +1 -0
  27. package/dist/commands/spec2doc.js +183 -0
  28. package/dist/commands/spec2doc.js.map +1 -0
  29. package/dist/commands/status-panel.d.ts +1 -0
  30. package/dist/commands/status-panel.d.ts.map +1 -1
  31. package/dist/commands/status-panel.js +97 -3
  32. package/dist/commands/status-panel.js.map +1 -1
  33. package/dist/commands/welcome.d.ts +10 -0
  34. package/dist/commands/welcome.d.ts.map +1 -1
  35. package/dist/commands/welcome.js +77 -30
  36. package/dist/commands/welcome.js.map +1 -1
  37. package/dist/core/ask-engine.d.ts +45 -0
  38. package/dist/core/ask-engine.d.ts.map +1 -0
  39. package/dist/core/ask-engine.js +405 -0
  40. package/dist/core/ask-engine.js.map +1 -0
  41. package/dist/core/ask-host-ai.d.ts +33 -0
  42. package/dist/core/ask-host-ai.d.ts.map +1 -0
  43. package/dist/core/ask-host-ai.js +156 -0
  44. package/dist/core/ask-host-ai.js.map +1 -0
  45. package/dist/core/ask-llm.d.ts +12 -0
  46. package/dist/core/ask-llm.d.ts.map +1 -0
  47. package/dist/core/ask-llm.js +166 -0
  48. package/dist/core/ask-llm.js.map +1 -0
  49. package/dist/core/dev-llm.d.ts +34 -0
  50. package/dist/core/dev-llm.d.ts.map +1 -0
  51. package/dist/core/dev-llm.js +209 -0
  52. package/dist/core/dev-llm.js.map +1 -0
  53. package/dist/core/help-panel.d.ts +1 -1
  54. package/dist/core/help-panel.d.ts.map +1 -1
  55. package/dist/core/help-panel.js +6 -5
  56. package/dist/core/help-panel.js.map +1 -1
  57. package/dist/core/intent-recognition.js +3 -3
  58. package/dist/core/intent-recognition.js.map +1 -1
  59. package/dist/utils/logger.d.ts.map +1 -1
  60. package/dist/utils/logger.js +5 -2
  61. package/dist/utils/logger.js.map +1 -1
  62. package/package.json +1 -1
@@ -5,6 +5,7 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.dashboardCommand = dashboardCommand;
8
+ exports.generateDashboardHtml = generateDashboardHtml;
8
9
  const logger_1 = require("../utils/logger");
9
10
  const global_layer_1 = require("../core/global-layer");
10
11
  const fs_extra_1 = require("fs-extra");
@@ -28,8 +29,25 @@ async function dashboardCommand(options) {
28
29
  // 项目分布数据
29
30
  const projectLabels = index.projects.map((p) => p.name);
30
31
  const projectReqs = index.projects.map((p) => p.reqCount);
32
+ // ── 新增:迭代活跃度 ──
33
+ const activeIterations = index.iterations.filter(i => i.status === 'active' || i.status === '进行中');
34
+ const iterationLabels = index.iterations.map(i => i.name).slice(0, 8);
35
+ const iterationReqCounts = index.iterations.map(i => i.reqs.length).slice(0, 8);
36
+ // ── 新增:期次完成统计 ──
37
+ const iterStats = index.iterations.map(it => {
38
+ const iterReqs = index.reqs.filter(r => it.reqs.includes(r.id));
39
+ const done = iterReqs.filter(r => r.status.includes('✅') || r.status.includes('已实现')).length;
40
+ return { name: it.name, total: it.reqs.length, done, pct: it.reqs.length > 0 ? Math.round(done / it.reqs.length * 100) : 0 };
41
+ });
42
+ // ── 新增:健康度评分 ──
43
+ const projectHealth = index.projects.map(p => ({
44
+ name: p.name,
45
+ reqCount: p.reqCount,
46
+ doneCount: index.reqs.filter(r => r.project === p.name && (r.status.includes('✅') || r.status.includes('已实现'))).length,
47
+ })).map(p => ({ ...p, pct: p.reqCount > 0 ? Math.round(p.doneCount / p.reqCount * 100) : 0 }))
48
+ .sort((a, b) => b.pct - a.pct);
31
49
  // 生成 HTML
32
- const html = generateDashboardHtml(index.projects.length, totalReqs, implemented, inProgress, pending, completionRate, projectLabels, projectReqs, index);
50
+ const html = generateDashboardHtml(index.projects.length, totalReqs, implemented, inProgress, pending, completionRate, projectLabels, projectReqs, iterationLabels, iterationReqCounts, iterStats, projectHealth, activeIterations.length, index);
33
51
  const outputPath = options.output || (0, path_1.join)(process.cwd(), 'speccore-dashboard.html');
34
52
  await (0, fs_extra_1.writeFile)(outputPath, html);
35
53
  logger_1.logger.info('');
@@ -51,157 +69,504 @@ async function dashboardCommand(options) {
51
69
  throw error;
52
70
  }
53
71
  }
54
- function generateDashboardHtml(projectCount, totalReqs, implemented, inProgress, pending, completionRate, projectLabels, projectReqs, index) {
72
+ function generateDashboardHtml(projectCount, totalReqs, implemented, inProgress, pending, completionRate, projectLabels, projectReqs, iterationLabels, iterationReqCounts, iterStats, projectHealth, activeIterCount, index) {
55
73
  const now = new Date().toISOString().split('T')[0];
56
- // 生成项目表格行
74
+ // 生成项目表格行(按需求数降序)
57
75
  let projectRows = '';
58
- for (const proj of index.projects) {
76
+ const sortedProjects = [...index.projects].sort((a, b) => b.reqCount - a.reqCount);
77
+ for (const proj of sortedProjects) {
59
78
  projectRows += `<tr><td>${proj.name}</td><td><span class="badge badge-${proj.type}">${proj.type}</span></td><td>${proj.reqCount}</td><td>${proj.lastImport}</td></tr>`;
60
79
  }
61
- // 生成需求表格行
80
+ // 生成需求表格行(按期次倒序:Q3→Q2→Q1,无期次排最后)
62
81
  let reqRows = '';
63
- for (const req of index.reqs.slice(0, 20)) {
82
+ const sortIter = (a, b) => {
83
+ const na = parseInt(a.replace(/[^0-9]/g, '')) || 0;
84
+ const nb = parseInt(b.replace(/[^0-9]/g, '')) || 0;
85
+ if (!na && !nb)
86
+ return 0;
87
+ if (!na)
88
+ return 1;
89
+ if (!nb)
90
+ return -1;
91
+ return nb - na;
92
+ };
93
+ const sortedReqs = [...index.reqs].sort((a, b) => {
94
+ const ia = sortIter(a.iteration || '', b.iteration || '');
95
+ if (ia !== 0)
96
+ return ia;
97
+ // 同期次内按状态排:已实现 > 进行中 > 待开发
98
+ const order = { '✅': 1, '🔄': 2, '🔲': 3 };
99
+ return (order[a.status.charAt(0)] || 99) - (order[b.status.charAt(0)] || 99);
100
+ });
101
+ for (const req of sortedReqs.slice(0, 30)) {
64
102
  const statusClass = req.status.includes('✅') ? 'done' : req.status.includes('🔄') ? 'progress' : 'pending';
65
103
  reqRows += `<tr><td>${req.id}</td><td>${req.name}</td><td>${req.project}</td><td><span class="badge badge-${statusClass}">${req.status}</span></td><td>${req.iteration || '-'}</td></tr>`;
66
104
  }
67
105
  return `<!DOCTYPE html>
68
- <html lang="zh-CN">
106
+ <html lang="zh-CN" data-theme="ocean">
69
107
  <head>
70
- <meta charset="UTF-8">
71
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
72
- <title>SpecCore 项目仪表盘</title>
108
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
109
+ <title>SpecCore 全量仪表盘</title>
73
110
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
74
111
  <style>
75
- * { margin: 0; padding: 0; box-sizing: border-box; }
76
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', sans-serif; background: #f0f2f5; padding: 24px; color: #1a1a2e; }
77
- .container { max-width: 1400px; margin: 0 auto; }
78
- .header { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: white; padding: 28px 32px; border-radius: 16px; margin-bottom: 24px; }
79
- .header h1 { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; }
80
- .header .meta { opacity: 0.6; margin-top: 6px; font-size: 14px; }
81
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
82
- .stat-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); text-align: center; }
83
- .stat-card .label { font-size: 13px; color: #64748b; margin-bottom: 8px; font-weight: 500; }
84
- .stat-card .value { font-size: 36px; font-weight: 800; letter-spacing: -1px; }
85
- .stat-card .sub { font-size: 13px; color: #94a3b8; margin-top: 4px; }
86
- .stat-card .value.green { color: #22c55e; }
87
- .stat-card .value.blue { color: #3b82f6; }
88
- .stat-card .value.amber { color: #f59e0b; }
89
- .stat-card .value.gray { color: #64748b; }
90
- .charts { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
91
- @media (max-width: 768px) { .charts { grid-template-columns: 1fr; } .stats { grid-template-columns: 1fr 1fr; } }
92
- .chart-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
93
- .chart-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: #334155; }
94
- .chart-card canvas { max-height: 300px; }
95
- .full { grid-column: 1 / -1; }
96
- .table-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); margin-bottom: 24px; overflow-x: auto; }
97
- .table-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: #334155; }
98
- table { width: 100%; border-collapse: collapse; font-size: 14px; }
99
- thead tr { border-bottom: 2px solid #e2e8f0; }
100
- th { text-align: left; padding: 10px 8px; color: #64748b; font-weight: 600; font-size: 13px; }
101
- td { padding: 10px 8px; border-bottom: 1px solid #f1f5f9; }
102
- .badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; }
103
- .badge-backend { background: #dbeafe; color: #1e40af; }
104
- .badge-web { background: #dcfce7; color: #166534; }
105
- .badge-h5 { background: #fef3c7; color: #92400e; }
106
- .badge-miniapp { background: #f3e8ff; color: #6b21a8; }
107
- .badge-done { background: #dcfce7; color: #166534; }
108
- .badge-progress { background: #dbeafe; color: #1e40af; }
109
- .badge-pending { background: #f1f5f9; color: #64748b; }
110
- .footer { text-align: center; color: #94a3b8; font-size: 12px; margin-top: 32px; padding: 16px; }
112
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=JetBrains+Mono:wght@400;600;700&display=swap');
113
+ [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)}
114
+ [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)}
115
+ [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)}
116
+ [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)}
117
+ [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)}
118
+ [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)}
119
+ [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)}
120
+ [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)}
121
+ [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)}
122
+ [data-theme="light"] table th{border-bottom-color:rgba(0,0,0,.15)!important}
123
+ [data-theme="light"] table td{border-bottom-color:rgba(0,0,0,.08)!important}
124
+ [data-theme="light"] .card,[data-theme="light"] .panel,[data-theme="light"] .header,[data-theme="light"] .stat-card,[data-theme="light"] .chart-card{border-color:rgba(0,0,0,.1)!important}
125
+ [data-theme="light"] .card:hover,[data-theme="light"] .panel:hover,[data-theme="light"] .stat-card:hover,[data-theme="light"] .chart-card:hover{border-color:rgba(37,99,235,.25)!important;box-shadow:0 0 24px rgba(37,99,235,.06),inset 0 0 24px rgba(37,99,235,.02)!important}
126
+ [data-theme="light"] .footer{border-top-color:rgba(0,0,0,.08)!important}
127
+ [data-theme="light"] .scanlines{opacity:.03!important}.stars{opacity:.02!important}
128
+ [data-theme="light"] .fs-btn{background:rgba(0,0,0,.03)!important;border-color:rgba(0,0,0,.08)!important}
129
+ [data-theme="light"] .data-stream span{color:rgba(0,0,0,.04)!important}
130
+ [data-theme="light"] .theme-sw,.lang-sw{border-color:rgba(0,0,0,.1)!important}
131
+ [data-theme="light"] tr:hover td{background:rgba(0,0,0,.02)!important}
132
+ [data-theme="light"] .header-stat{border-left-color:rgba(0,0,0,.1)!important}
133
+ [data-theme="light"] .panel-title::before{text-shadow:none!important}
134
+ [data-theme="light"] .grid-pattern{opacity:.3!important}
135
+ [data-fs="sm"]{font-size:13px}[data-fs="md"]{font-size:15px}[data-fs="lg"]{font-size:17px}[data-fs="xl"]{font-size:20px}
136
+ *,*::after,*::before{box-sizing:border-box;margin:0;padding:0}
137
+ .ctrl-panel{position:fixed;top:72px;right:16px;z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:6px}
138
+ .ctrl-toggle{width:56px;height:56px;border-radius:50%;border:1px solid var(--border);cursor:pointer;font-size:38px;display:flex;align-items:center;justify-content:center;line-height:0;padding:0;user-select:none;background:var(--surface);color:var(--muted);backdrop-filter:blur(10px);transition:all .3s}
139
+ .ctrl-toggle:hover{color:var(--cyan);border-color:var(--cyan);transform:rotate(90deg);transform-origin:center}
140
+ .ctrl-toggle.open{color:var(--cyan);border-color:var(--cyan);transform:rotate(90deg);transform-origin:center}
141
+ .ctrl-body{max-height:0;overflow:hidden;display:flex;flex-direction:column;gap:6px;align-items:flex-end;transition:max-height .4s ease,opacity .3s ease;opacity:0}
142
+ .ctrl-panel.open .ctrl-body{max-height:280px;opacity:1}
143
+ .theme-sw{display:flex;gap:6px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:4px;backdrop-filter:blur(10px)}
144
+ .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}
145
+ .theme-sw button:hover{transform:scale(1.1)}
146
+ .theme-sw button.active{box-shadow:0 0 0 2px var(--cyan);background:rgba(0,240,255,.15)}
147
+ .lang-sw{display:flex;gap:4px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:4px;backdrop-filter:blur(10px)}
148
+ .lang-sw button{padding:4px 10px;border-radius:12px;border:1px solid var(--border);cursor:pointer;font-size:11px;font-family:"JetBrains Mono",monospace;background:transparent;color:var(--muted)}
149
+ .lang-sw button:hover{color:var(--text)}
150
+ .lang-sw button.active{color:var(--cyan);border-color:var(--cyan);background:rgba(0,240,255,.1)}
151
+ html{font-size:18px}html.fs-sm{font-size:11px}html.fs-md{font-size:14px}html.fs-lg{font-size:18px}html.fs-xl{font-size:22px}body{font-family:'JetBrains Mono',monospace!important}.font-jetbrains h1,.font-jetbrains h3,.font-jetbrains .panel-title,.font-jetbrains .num-font,.font-jetbrains .big-num,.font-jetbrains .header-stat .num,.font-jetbrains .card-icon{font-family:'JetBrains Mono',monospace!important}
152
+ .font-hybrid,.font-hybrid html{font-size:18px}html.fs-sm{font-size:11px}html.fs-md{font-size:14px}html.fs-lg{font-size:18px}html.fs-xl{font-size:22px}body{font-family:'JetBrains Mono',monospace!important}.font-hybrid h1,.font-hybrid h3,.font-hybrid .panel-title,.font-hybrid .num-font,.font-hybrid .big-num,.font-hybrid .header-stat .num,.font-hybrid .card-icon{font-family:Orbitron,sans-serif!important}
153
+ .font-orbitron,.font-orbitron html{font-size:18px}html.fs-sm{font-size:11px}html.fs-md{font-size:14px}html.fs-lg{font-size:18px}html.fs-xl{font-size:22px}body{font-family:Orbitron,sans-serif!important}.font-orbitron h1,.font-orbitron h3,.font-orbitron .panel-title,.font-orbitron .num-font,.font-orbitron .big-num,.font-orbitron .header-stat .num,.font-orbitron .card-icon{font-family:Orbitron,sans-serif!important}
154
+ body{font-family:'JetBrains Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
155
+ .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}
156
+ .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}
157
+ .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}
158
+ main{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:40px 32px}
159
+ .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}
160
+ .header::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);animation:scanX 3s linear infinite}
161
+ .header::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);animation:scanX-rev 3s linear infinite}
162
+ @keyframes scanX{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
163
+ @keyframes scanX-rev{0%{transform:translateX(100%)}100%{transform:translateX(-100%)}}
164
+ @keyframes scanY{0%{transform:translateY(-100%)}100%{transform:translateY(100%)}}
165
+ @keyframes scanY-rev{0%{transform:translateY(100%)}100%{transform:translateY(-100%)}}
166
+ .header-left::before{content:'';position:absolute;top:0;left:0;width:1px;bottom:0;background:linear-gradient(180deg,transparent,var(--cyan),transparent);animation:scanY-rev 3s linear infinite;pointer-events:none}
167
+ .header-right::after{content:'';position:absolute;top:0;right:0;width:1px;bottom:0;background:linear-gradient(180deg,transparent,var(--cyan),transparent);animation:scanY 3s linear infinite;pointer-events:none}
168
+ .header-left h1{font-family:'Orbitron',sans-serif;font-size:26px;font-weight:900;background:linear-gradient(135deg,var(--cyan),var(--purple));-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)}
169
+ .header-left .subtitle{color:var(--muted);font-size:12px;margin-top:4px;letter-spacing:1px}
170
+ .header-right{display:flex;gap:16px;align-items:center}
171
+ .header-stat{text-align:center;padding:0 20px;border-left:1px solid rgba(0,240,255,.1)}
172
+ .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)}
173
+ .header-stat .label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
174
+ .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)}
175
+ .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}
176
+ @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)}}
177
+ .phase-indicator span{font-size:12px;color:var(--cyan);letter-spacing:1px}
178
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:28px}
179
+ .stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;backdrop-filter:blur(20px);text-align:center;transition:all .3s;position:relative;overflow:hidden}
180
+ .stat-card:hover{border-color:rgba(0,240,255,.25);box-shadow:0 0 30px rgba(0,240,255,.08)}
181
+ .stat-card .label{font-size:12px;color:var(--muted);margin-bottom:12px;font-weight:500;text-transform:uppercase;letter-spacing:1px}
182
+ .stat-card .value{font-family:'Orbitron',sans-serif;font-size:38px;font-weight:900;letter-spacing:-1px}
183
+ .stat-card .value.c-cyan{color:var(--cyan);text-shadow:0 0 30px rgba(0,240,255,.3)}
184
+ .stat-card .value.c-green{color:var(--green);text-shadow:0 0 30px rgba(0,255,136,.3)}
185
+ .stat-card .value.c-orange{color:var(--orange);text-shadow:0 0 20px rgba(245,158,11,.3)}
186
+ .stat-card .value.c-muted{color:var(--muted)}
187
+ .stat-card .sub{font-size:11px;color:var(--muted);margin-top:6px}
188
+ .data-stream{position:absolute;bottom:6px;left:0;right:0;height:18px;overflow:hidden}
189
+ .data-stream span{position:absolute;top:3px;color:rgba(0,240,255,.08);font-family:'JetBrains Mono',monospace;font-size:9px;white-space:nowrap;animation:stream 25s linear infinite}
190
+ @keyframes stream{0%{transform:translateX(100%)}100%{transform:translateX(-100%)}}
191
+ .charts{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:28px}
192
+ @media(max-width:768px){.charts{grid-template-columns:1fr}.stats{grid-template-columns:1fr 1fr}}
193
+ .chart-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;backdrop-filter:blur(20px);position:relative;transition:all .3s}
194
+ .chart-card:hover{border-color:rgba(0,240,255,.25);box-shadow:0 0 30px rgba(0,240,255,.08)}
195
+ .chart-card:hover .fs-btn{opacity:1}
196
+ .chart-card h3{font-size:14px;font-weight:600;margin-bottom:16px;color:var(--muted);letter-spacing:.5px;text-transform:uppercase}
197
+ .chart-card canvas{max-height:300px}
198
+ .panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:28px;backdrop-filter:blur(20px);margin-bottom:28px;position:relative;transition:all .3s}
199
+ .panel:hover{border-color:rgba(0,240,255,.25);box-shadow:0 0 30px rgba(0,240,255,.08)}
200
+ .panel:hover .fs-btn{opacity:1}
201
+ .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}
202
+ .panel-title::before{content:'◆';font-size:10px;color:var(--cyan);text-shadow:0 0 8px var(--cyan)}
203
+ table{width:100%;border-collapse:collapse;font-size:13px}
204
+ 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}
205
+ td{padding:14px 16px;border-bottom:1px solid rgba(0,240,255,.03);color:var(--text);transition:all .2s}
206
+ tr:hover td{background:var(--hover)}
207
+ .badge{display:inline-block;padding:3px 12px;border-radius:12px;font-size:11px;font-weight:500;letter-spacing:.5px}
208
+ .badge-backend{background:rgba(59,130,246,.15);color:var(--blue)}
209
+ .badge-web{background:rgba(0,255,136,.1);color:var(--green)}
210
+ .badge-h5{background:rgba(245,158,11,.1);color:var(--orange)}
211
+ .badge-miniapp{background:rgba(168,85,247,.15);color:var(--purple)}
212
+ .badge-done{background:rgba(0,255,136,.1);color:var(--green)}
213
+ .badge-progress{background:rgba(59,130,246,.1);color:var(--blue)}
214
+ .badge-pending{background:rgba(255,255,255,.03);color:var(--muted)}
215
+ .footer{text-align:center;color:var(--muted);font-size:11px;margin-top:40px;padding:16px 0;border-top:1px solid rgba(0,240,255,.06);letter-spacing:1px}
216
+ .scroll-table{max-height:70vh;overflow-y:auto}.scroll-table thead{position:sticky;top:0;z-index:2;background:var(--card)}
217
+ .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}
218
+ .stat-card:hover .fs-btn,.chart-card:hover .fs-btn,.panel:hover .fs-btn{opacity:1}
219
+ .fs-btn:hover{background:rgba(0,240,255,.1);border-color:rgba(0,240,255,.3);color:var(--cyan)}
220
+ .fs-fullscreen{position:fixed!important;inset:0!important;z-index:1000!important;border-radius:0!important;overflow-y:auto!important;background:var(--bg)!important;backdrop-filter:none!important;padding:40px!important;width:100vw!important;height:100vh!important;max-width:none!important}
221
+ .fs-fullscreen canvas{max-height:60vh!important}
222
+ .fs-fullscreen .scroll-table{max-height:none!important}
223
+ .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}
224
+ @keyframes fadeIn{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
111
225
  </style>
112
226
  </head>
113
227
  <body>
114
- <div class="container">
228
+ <div class="ctrl-panel" id="ctrlPanel">
229
+ <button class="ctrl-toggle" onclick="toggleCtrl()" title="设置">⚙</button>
230
+ <div class="ctrl-body">
231
+ <div class="theme-sw">
232
+ <button onclick="setTheme('ocean')" title="Ocean Blue">🌊</button><button onclick="setTheme('cyber')" title="Cyber Dark">🌙</button>
233
+ <button onclick="setTheme('light')" title="Light Studio">☀️</button><button onclick="setTheme('mono')" title="Mono Tech">⬛</button>
234
+ <button onclick="setTheme('github')" title="GitHub Dark">🐙</button><button onclick="setTheme('synth')" title="SynthWave">💜</button>
235
+ <button onclick="setTheme('amber')" title="Amber Terminal">🟡</button><button onclick="setTheme('sakura')" title="Cherry Sakura">🌸</button>
236
+ <button onclick="setTheme('forest')" title="Midnight Forest">🌲</button>
237
+ </div>
238
+ <div class="lang-sw">
239
+ <button data-lang="zh" class="active" onclick="setLang('zh')">中文</button>
240
+ <button data-lang="en" onclick="setLang('en')">EN</button>
241
+ </div>
242
+ <div class="lang-sw">
243
+ <button onclick="setFont('hybrid')" class="active" data-font="hybrid">Hybrid</button>
244
+ <button onclick="setFont('orbitron')" data-font="orbitron">Orbit</button>
245
+ <button onclick="setFont('jetbrains')" data-font="jetbrains">Mono</button>
246
+ </div>
247
+ <div class="lang-sw">
248
+ <button onclick="setSize('sm')" data-size="sm">S</button>
249
+ <button onclick="setSize('md')" data-size="md">M</button>
250
+ <button onclick="setSize('lg')" class="active" data-size="lg">L</button>
251
+ <button onclick="setSize('xl')" data-size="xl">XL</button>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ <div class="scanlines"></div><div class="stars"></div><div class="grid-pattern"></div>
256
+ <main>
115
257
  <div class="header">
116
- <h1>📊 SpecCore 项目仪表盘</h1>
117
- <div class="meta">生成时间: ${now} | 全量层版本: ${index.version} | 项目数: ${projectCount}</div>
258
+ <div class="header-left">
259
+ <h1>SPECCORE</h1>
260
+ <div class="subtitle"><span data-i18n="globalDash">全局仪表盘</span> · ${projectCount} <span data-i18n="projects">项目</span> · ${totalReqs} <span data-i18n="reqs">需求</span></div>
261
+ </div>
262
+ <div class="header-right">
263
+ <div class="header-stat"><div class="num">${projectCount}</div><div class="label" data-i18n="projects">项目</div></div>
264
+ <div class="header-stat"><div class="num">${totalReqs}</div><div class="label" data-i18n="reqs">需求</div></div>
265
+ <div class="header-stat"><div class="num">${completionRate}%</div><div class="label" data-i18n="done2">完成</div></div>
266
+ <div class="phase-indicator"><div class="dot"></div><span>LIVE</span></div>
267
+ </div>
268
+ <div class="data-stream" style="position:absolute"><span>SPECCORE · DASHBOARD · ${projectCount} PROJECTS · ${totalReqs} REQUIREMENTS · ${completionRate}% COMPLETE · POWERED BY SPECCORE</span></div>
118
269
  </div>
119
270
 
120
271
  <div class="stats">
121
272
  <div class="stat-card">
122
- <div class="label">📋 总需求数</div>
123
- <div class="value blue">${totalReqs}</div>
273
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
274
+ <div class="label">📋 <span data-i18n="total">总需求数</span></div>
275
+ <div class="value c-cyan">${totalReqs}</div>
124
276
  <div class="sub">${projectCount} 个项目</div>
277
+ <div class="data-stream"><span>SPECCORE · TOTAL REQUIREMENTS · ${now} · V${index.version || '1.0'}</span></div>
125
278
  </div>
126
279
  <div class="stat-card">
127
- <div class="label">✅ 已完成</div>
128
- <div class="value green">${implemented}</div>
129
- <div class="sub">完成率 ${completionRate}%</div>
280
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
281
+ <div class="label">✅ <span data-i18n="done">已完成</span></div>
282
+ <div class="value c-green">${implemented}</div>
283
+ <div class="sub"><span data-i18n="done2">完成</span>率 ${completionRate}%</div>
284
+ <div class="data-stream"><span>DONE · ${completionRate}% COMPLETION · ${implemented} IMPLEMENTED</span></div>
130
285
  </div>
131
286
  <div class="stat-card">
132
- <div class="label">🔄 进行中</div>
133
- <div class="value amber">${inProgress}</div>
287
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
288
+ <div class="label">🔄 <span data-i18n="progress">进行中</span></div>
289
+ <div class="value c-orange">${inProgress}</div>
134
290
  <div class="sub">${Math.round((inProgress / (totalReqs || 1)) * 100)}%</div>
291
+ <div class="data-stream"><span>IN PROGRESS · ${inProgress} ACTIVE · ITERATIONS ACTIVE</span></div>
135
292
  </div>
136
293
  <div class="stat-card">
137
- <div class="label">🔲 待开发</div>
138
- <div class="value gray">${pending}</div>
294
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
295
+ <div class="label">🔲 <span data-i18n="pending">待开发</span></div>
296
+ <div class="value c-muted">${pending}</div>
139
297
  <div class="sub">${Math.round((pending / (totalReqs || 1)) * 100)}%</div>
298
+ <div class="data-stream"><span>PENDING · ${pending} BACKLOG · AWAITING SPRINT</span></div>
140
299
  </div>
141
300
  </div>
142
301
 
143
302
  <div class="charts">
144
303
  <div class="chart-card">
145
- <h3>📊 需求状态分布</h3>
304
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
305
+ <h3>📊 <span data-i18n="statusDist">需求状态分布</span></h3>
146
306
  <canvas id="statusChart"></canvas>
307
+ <div class="data-stream"><span>STATUS · ${implemented} DONE · ${inProgress} ACTIVE · ${pending} BACKLOG</span></div>
147
308
  </div>
148
309
  <div class="chart-card">
149
- <h3>📈 项目需求分布</h3>
310
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
311
+ <h3>📈 <span data-i18n="projDist">项目需求分布</span></h3>
150
312
  <canvas id="projectChart"></canvas>
151
313
  </div>
152
314
  </div>
153
315
 
154
- <div class="table-card">
155
- <h3>📋 项目列表</h3>
316
+ <div class="panel">
317
+ <div class="panel-title"><span data-i18n="projHealth">项目健康度</span></div>
318
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px">
319
+ ${projectHealth.map(p => `
320
+ <div style="background:var(--surface);border-radius:8px;padding:14px 16px;display:flex;align-items:center;gap:14px">
321
+ <div style="font-family:Orbitron;font-size:24px;font-weight:900;color:${p.pct >= 80 ? 'var(--green)' : p.pct >= 40 ? 'var(--orange)' : 'var(--muted)'};min-width:50px">${p.pct}%</div>
322
+ <div style="flex:1">
323
+ <div style="font-weight:600;margin-bottom:4px">${p.name}</div>
324
+ <div style="height:4px;background:rgba(255,255,255,.04);border-radius:2px;overflow:hidden">
325
+ <div style="width:${p.pct}%;height:100%;background:linear-gradient(90deg,var(--cyan),var(--green));border-radius:2px"></div>
326
+ </div>
327
+ <div style="font-size:10px;color:var(--muted);margin-top:4px">${p.doneCount}/${p.reqCount} 完成</div>
328
+ </div>
329
+ </div>
330
+ `).join('')}
331
+ </div>
332
+ </div>
333
+
334
+ <div class="charts">
335
+ <div class="chart-card">
336
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
337
+ <h3>⚡ <span data-i18n="createdVsResolved">需求创建 vs 完成</span></h3>
338
+ <canvas id="resolvedChart"></canvas>
339
+ </div>
340
+ <div class="chart-card">
341
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
342
+ <h3>📅 <span data-i18n="iterProgress">期次进度</span></h3>
343
+ <div style="display:flex;flex-direction:column;gap:10px;margin-top:8px;max-height:280px;overflow-y:auto">
344
+ ${iterStats.map(it => `
345
+ <div style="display:flex;align-items:center;gap:10px">
346
+ <span style="font-family:Orbitron;font-size:11px;color:var(--cyan);min-width:50px">${it.name}</span>
347
+ <div style="flex:1;height:6px;background:rgba(255,255,255,.04);border-radius:3px;overflow:hidden">
348
+ <div style="width:${it.pct}%;height:100%;background:linear-gradient(90deg,var(--cyan),var(--green));border-radius:3px;transition:width 1s"></div>
349
+ </div>
350
+ <span style="font-size:10px;color:var(--muted);min-width:55px;text-align:right">${it.done}/${it.total}</span>
351
+ </div>
352
+ `).join('')}
353
+ ${iterStats.length === 0 ? '<div style="color:var(--muted);text-align:center;padding:20px">暂无期次数据</div>' : ''}
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="stats">
359
+ <div class="stat-card">
360
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
361
+ <div class="label">🏥 <span data-i18n="health">项目健康度</span></div>
362
+ <div class="value c-cyan">${projectHealth.filter(p => p.pct >= 80).length}/${projectHealth.length}</div>
363
+ <div class="sub">健康项目 / 全部</div>
364
+ <div class="data-stream"><span>HEALTH · ${projectHealth.filter(p => p.pct >= 80).length} GREEN · ${projectHealth.filter(p => p.pct < 40).length} AT RISK</span></div>
365
+ </div>
366
+ <div class="stat-card">
367
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
368
+ <div class="label">📅 <span data-i18n="activeIter">活跃期次</span></div>
369
+ <div class="value c-green">${activeIterCount}</div>
370
+ <div class="sub">共 ${index.iterations.length} 个期次</div>
371
+ <div class="data-stream"><span>ITERATIONS · ${activeIterCount} ACTIVE · ${index.iterations.length} TOTAL</span></div>
372
+ </div>
373
+ <div class="stat-card">
374
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
375
+ <div class="label">📈 <span data-i18n="velocity">交付速率</span></div>
376
+ <div class="value c-orange">${completionRate}%</div>
377
+ <div class="sub">已完成 / 总需求</div>
378
+ <div class="data-stream"><span>VELOCITY · ${implemented} DONE · ${totalReqs} TOTAL · ${completionRate}%</span></div>
379
+ </div>
380
+ </div>
381
+
382
+ <div class="panel">
383
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
384
+ <div class="panel-title"><span data-i18n="projList">项目列表</span></div>
156
385
  <table>
157
- <thead><tr><th>项目名称</th><th>类型</th><th>需求数</th><th>最后导入</th></tr></thead>
386
+ <thead><tr><th><span data-i18n="projName">项目名称</span></th><th><span data-i18n="type">类型</span></th><th><span data-i18n="reqCount">需求数</span></th><th><span data-i18n="lastImport">最后导入</span></th></tr></thead>
158
387
  <tbody>${projectRows || '<tr><td colspan="4">暂无项目</td></tr>'}</tbody>
159
388
  </table>
160
389
  </div>
161
390
 
162
- <div class="table-card">
163
- <h3>📝 需求详情</h3>
391
+ <div class="panel">
392
+ <button class="fs-btn" title="全屏 (F)" onclick="toggleFS(this.parentElement)">⛶</button>
393
+ <div class="panel-title"><span data-i18n="reqDetail">需求详情</span></div>
394
+ <div class="scroll-table">
164
395
  <table>
165
- <thead><tr><th>需求 ID</th><th>名称</th><th>项目</th><th>状态</th><th>关联期次</th></tr></thead>
396
+ <thead><tr><th><span data-i18n="reqId">需求 ID</span></th><th><span data-i18n="name">名称</span></th><th><span data-i18n="project">项目</span></th><th><span data-i18n="status">状态</span></th><th><span data-i18n="iteration">关联期次</span></th></tr></thead>
166
397
  <tbody>${reqRows || '<tr><td colspan="5">暂无需求</td></tr>'}</tbody>
167
398
  </table>
399
+ </div>
168
400
  </div>
169
401
 
170
- <div class="footer">Powered by SpecCore | Generated ${now}</div>
171
- </div>
402
+ <div class="footer"><span data-i18n="powered">由 SpecCore 驱动</span> ${index.version || 'v1.0'} &nbsp;&nbsp;|&nbsp;&nbsp; Generated ${now}</div>
403
+ </main>
172
404
 
173
405
  <script>
174
- new Chart(document.getElementById('statusChart'), {
406
+ let fsEl = null;
407
+
408
+ // Fullscreen toggle
409
+ function toggleFS(el) {
410
+ if (fsEl === el) { el.classList.remove('fs-fullscreen'); fsEl = null; document.body.style.overflow = ''; return; }
411
+ if (fsEl) { fsEl.classList.remove('fs-fullscreen'); }
412
+ fsEl = el; el.classList.add('fs-fullscreen');
413
+ document.body.style.overflow = 'hidden';
414
+ const tip = document.createElement('div'); tip.className = 'fs-tip'; tip.textContent = '按 ESC 退出全屏 · 滚动查看详情';
415
+ document.body.appendChild(tip);
416
+ setTimeout(() => tip.remove(), 2500);
417
+ }
418
+ document.addEventListener('keydown',e=>{
419
+ if(e.key==='Escape'&&fsEl){fsEl.classList.remove('fs-fullscreen');document.body.style.overflow='';fsEl=null}
420
+ if(e.key==='f'||e.key==='F'){
421
+ if(document.activeElement&&document.activeElement.tagName==='INPUT')return;
422
+ const hovered=document.querySelector('.stat-card:hover,.chart-card:hover,.panel:hover');
423
+ if(hovered)toggleFS(hovered);
424
+ }
425
+ });
426
+
427
+ // Export functions
428
+ function exportJSON() {
429
+ const data = { projects: ${JSON.stringify(index.projects.map(p => ({ name: p.name, type: p.type, reqCount: p.reqCount, lastImport: p.lastImport })))} };
430
+ download(JSON.stringify(data, null, 2), 'dashboard-data.json', 'application/json');
431
+ }
432
+ function exportCSV() {
433
+ const rows = [['需求ID','项目','名称','状态','版本','期次'].join(',')];
434
+ ${JSON.stringify(index.reqs)}.forEach(r => rows.push([r.id,r.project,r.name,r.status,r.version,r.iteration||'-'].join(',')));
435
+ download(rows.join('\\n'), 'dashboard-data.csv', 'text/csv');
436
+ }
437
+ function download(content, name, type) {
438
+ const b = new Blob([content], { type });
439
+ const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = name; a.click();
440
+ }
441
+
442
+ // Settings toggle (ctrl-panel)
443
+ function toggleCtrl() {
444
+ document.getElementById('ctrlPanel').classList.toggle('open');
445
+ document.querySelector('.ctrl-toggle').classList.toggle('open');
446
+ }
447
+
448
+ // Theme - index based (order: ocean,cyber,light,mono,github,synth,amber,sakura,forest)
449
+ const THEMES = ['ocean','cyber','light','mono','github','synth','amber','sakura','forest'];
450
+ function setTheme(t) {
451
+ document.documentElement.setAttribute('data-theme', t);
452
+ localStorage.setItem('speccore-theme', t);
453
+ document.querySelectorAll('.theme-sw button').forEach((b,i) => b.classList.toggle('active', THEMES[i]===t));
454
+ updateChartColors(t);
455
+ }
456
+ function updateChartColors(t) {
457
+ if(typeof statusChart === 'undefined') return;
458
+ const light = t === 'light';
459
+ const tc = light ? '#64748b' : '#4a5568';
460
+ const gc = light ? 'rgba(0,0,0,.06)' : 'rgba(255,255,255,.06)';
461
+ [statusChart, projectChart, resolvedChart].forEach(ch => {
462
+ if(!ch) return;
463
+ if(ch.config.type !== 'doughnut') {
464
+ ch.options.scales.x.ticks.color = tc; ch.options.scales.y.ticks.color = tc;
465
+ ch.options.scales.x.grid.color = gc; ch.options.scales.y.grid.color = gc;
466
+ }
467
+ if(ch.options.plugins.legend) ch.options.plugins.legend.labels.color = tc;
468
+ ch.update();
469
+ });
470
+ }
471
+
472
+ // Lang
473
+ var I18N={zh:{total:'总需求数',done:'已完成',progress:'进行中',pending:'待开发',health:'项目健康度',activeIter:'活跃期次',velocity:'交付速率',statusDist:'需求状态分布',projDist:'项目需求分布',createdVsResolved:'需求创建 vs 完成',iterProgress:'期次进度',projHealth:'项目健康度',projList:'项目列表',reqDetail:'需求详情',projName:'项目名称',type:'类型',reqCount:'需求数',lastImport:'最后导入',reqId:'需求 ID',name:'名称',project:'项目',status:'状态',iteration:'关联期次',projects:'项目',reqs:'需求',done2:'完成',live:'LIVE',globalDash:'全局仪表盘',powered:'由 SpecCore 驱动'},en:{total:'Total Reqs',done:'Done',progress:'In Progress',pending:'Backlog',health:'Project Health',activeIter:'Active Iterations',velocity:'Velocity',statusDist:'Status Distribution',projDist:'Project Distribution',createdVsResolved:'Created vs Resolved',iterProgress:'Iteration Progress',projHealth:'PROJECT HEALTH',projList:'Project List',reqDetail:'Requirement Details',projName:'Project Name',type:'Type',reqCount:'Reqs',lastImport:'Last Import',reqId:'Req ID',name:'Name',project:'Project',status:'Status',iteration:'Iteration',projects:'Projects',reqs:'Reqs',done2:'Done',live:'LIVE',globalDash:'GLOBAL DASHBOARD',powered:'Powered by SpecCore'}};
474
+ function setLang(l) {
475
+ document.querySelectorAll('.lang-sw button[data-lang]').forEach(b => b.classList.toggle('active', b.dataset.lang===l));
476
+ document.querySelectorAll('[data-i18n]').forEach(el => { var k=el.getAttribute('data-i18n'); if(I18N[l]&&I18N[l][k]) el.textContent=I18N[l][k] });
477
+ localStorage.setItem('speccore-lang', l);
478
+ }
479
+
480
+ // Font family
481
+ function setFont(f) {
482
+ document.documentElement.className = document.documentElement.className.replace(/font-\w+/g,'');
483
+ if(f!=='hybrid') document.documentElement.classList.add('font-'+f);
484
+ document.querySelectorAll('.lang-sw button[data-font]').forEach(b => b.classList.toggle('active', b.dataset.font===f));
485
+ localStorage.setItem('speccore-font', f);
486
+ }
487
+
488
+ // Font size
489
+ function setSize(s) {
490
+ document.documentElement.className = document.documentElement.className.replace(/fs-\w+/g,'');
491
+ document.documentElement.classList.add('fs-'+s);
492
+ document.querySelectorAll('.lang-sw button[data-size]').forEach(b => b.classList.toggle('active', b.dataset.size===s));
493
+ localStorage.setItem('speccore-size', s);
494
+ }
495
+
496
+ // Init saved settings
497
+ (function(){
498
+ setTheme(localStorage.getItem('speccore-theme') || 'ocean');
499
+ setLang(localStorage.getItem('speccore-lang') || 'zh');
500
+ setFont(localStorage.getItem('speccore-font') || 'hybrid');
501
+ setSize(localStorage.getItem('speccore-size') || 'lg');
502
+ })();
503
+
504
+ // Charts
505
+ const isLight = document.documentElement.getAttribute('data-theme') === 'light';
506
+ const gridColor = isLight ? 'rgba(0,0,0,.06)' : 'rgba(255,255,255,.06)';
507
+ const textColor = isLight ? '#64748b' : '#4a5568';
508
+
509
+ const statusChart = new Chart(document.getElementById('statusChart'), {
175
510
  type: 'doughnut',
176
511
  data: {
177
- labels: ['已实现', '进行中', '待开发'],
512
+ labels: ['已完成', '进行中', '待开发'],
178
513
  datasets: [{
179
514
  data: [${implemented}, ${inProgress}, ${pending}],
180
- backgroundColor: ['#22c55e', '#3b82f6', '#e2e8f0'],
515
+ backgroundColor: ['#00ff88', '#00f0ff', 'rgba(255,255,255,.05)'],
181
516
  borderWidth: 2,
182
- borderColor: '#fff'
517
+ borderColor: 'var(--bg)'
183
518
  }]
184
519
  },
185
- options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
520
+ options: {
521
+ responsive: true,
522
+ cutout: '60%',
523
+ plugins: {
524
+ legend: { position: 'bottom', labels: { color: textColor, font: { family: 'JetBrains Mono', size: 11 }, padding: 16 } }
525
+ }
526
+ }
186
527
  });
187
528
 
188
- new Chart(document.getElementById('projectChart'), {
529
+ const projectChart = new Chart(document.getElementById('projectChart'), {
189
530
  type: 'bar',
190
531
  data: {
191
532
  labels: ${JSON.stringify(projectLabels)},
192
533
  datasets: [{
193
534
  label: '需求数',
194
535
  data: ${JSON.stringify(projectReqs)},
195
- backgroundColor: '#3b82f6',
196
- borderRadius: 8
536
+ backgroundColor: '#00f0ff',
537
+ borderRadius: 6
197
538
  }]
198
539
  },
199
540
  options: {
200
541
  responsive: true,
201
542
  plugins: { legend: { display: false } },
202
- scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }
543
+ scales: {
544
+ x: { ticks: { color: textColor, font: { size: 10 } }, grid: { color: gridColor } },
545
+ y: { beginAtZero: true, ticks: { stepSize: 1, color: textColor, font: { size: 10 } }, grid: { color: gridColor } }
546
+ }
547
+ }
548
+ });
549
+
550
+ const resolvedChart = new Chart(document.getElementById('resolvedChart'), {
551
+ type: 'bar',
552
+ data: {
553
+ labels: ['已实现', '进行中', '待开发'],
554
+ datasets: [{
555
+ data: [${implemented}, ${inProgress}, ${pending}],
556
+ backgroundColor: ['rgba(0,255,136,.6)', 'rgba(0,240,255,.6)', 'rgba(255,255,255,.06)'],
557
+ borderRadius: 4
558
+ }]
559
+ },
560
+ options: {
561
+ responsive: true, indexAxis: 'y',
562
+ plugins: { legend: { display: false } },
563
+ scales: {
564
+ x: { ticks: { color: textColor, font: { size: 10 } }, grid: { color: gridColor } },
565
+ y: { ticks: { color: textColor, font: { size: 10 } }, grid: { display: false } }
566
+ }
203
567
  }
204
568
  });
569
+ updateChartColors(document.documentElement.getAttribute('data-theme'));
205
570
  </script>
206
571
  </body>
207
572
  </html>`;