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.
- package/README.md +93 -77
- package/dist/cli.js +123 -45
- package/dist/cli.js.map +1 -1
- package/dist/commands/ask.d.ts +4 -5
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +257 -292
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/dashboard.d.ts +12 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +442 -77
- package/dist/commands/dashboard.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 +282 -290
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doc2spec.d.ts +2 -0
- package/dist/commands/doc2spec.d.ts.map +1 -1
- package/dist/commands/doc2spec.js +22 -6
- package/dist/commands/doc2spec.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +39 -0
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/init.js +15 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/spec2doc.d.ts +11 -0
- package/dist/commands/spec2doc.d.ts.map +1 -0
- package/dist/commands/spec2doc.js +183 -0
- package/dist/commands/spec2doc.js.map +1 -0
- package/dist/commands/status-panel.d.ts +1 -0
- package/dist/commands/status-panel.d.ts.map +1 -1
- package/dist/commands/status-panel.js +97 -3
- package/dist/commands/status-panel.js.map +1 -1
- package/dist/commands/welcome.d.ts +10 -0
- package/dist/commands/welcome.d.ts.map +1 -1
- package/dist/commands/welcome.js +77 -30
- package/dist/commands/welcome.js.map +1 -1
- package/dist/core/ask-engine.d.ts +45 -0
- package/dist/core/ask-engine.d.ts.map +1 -0
- package/dist/core/ask-engine.js +405 -0
- package/dist/core/ask-engine.js.map +1 -0
- package/dist/core/ask-host-ai.d.ts +33 -0
- package/dist/core/ask-host-ai.d.ts.map +1 -0
- package/dist/core/ask-host-ai.js +156 -0
- package/dist/core/ask-host-ai.js.map +1 -0
- package/dist/core/ask-llm.d.ts +12 -0
- package/dist/core/ask-llm.d.ts.map +1 -0
- package/dist/core/ask-llm.js +166 -0
- package/dist/core/ask-llm.js.map +1 -0
- package/dist/core/dev-llm.d.ts +34 -0
- package/dist/core/dev-llm.d.ts.map +1 -0
- package/dist/core/dev-llm.js +209 -0
- package/dist/core/dev-llm.js.map +1 -0
- package/dist/core/help-panel.d.ts +1 -1
- package/dist/core/help-panel.d.ts.map +1 -1
- package/dist/core/help-panel.js +6 -5
- package/dist/core/help-panel.js.map +1 -1
- package/dist/core/intent-recognition.js +3 -3
- package/dist/core/intent-recognition.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +5 -2
- package/dist/utils/logger.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.stat-card .
|
|
88
|
-
.stat-card .
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
.
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
.
|
|
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="
|
|
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
|
-
<
|
|
117
|
-
|
|
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
|
-
<
|
|
123
|
-
<div class="
|
|
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
|
-
<
|
|
128
|
-
<div class="
|
|
129
|
-
<div class="
|
|
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
|
-
<
|
|
133
|
-
<div class="
|
|
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
|
-
<
|
|
138
|
-
<div class="
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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="
|
|
155
|
-
<
|
|
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="
|
|
163
|
-
<
|
|
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"
|
|
171
|
-
</
|
|
402
|
+
<div class="footer"><span data-i18n="powered">由 SpecCore 驱动</span> ${index.version || 'v1.0'} | Generated ${now}</div>
|
|
403
|
+
</main>
|
|
172
404
|
|
|
173
405
|
<script>
|
|
174
|
-
|
|
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: ['#
|
|
515
|
+
backgroundColor: ['#00ff88', '#00f0ff', 'rgba(255,255,255,.05)'],
|
|
181
516
|
borderWidth: 2,
|
|
182
|
-
borderColor: '
|
|
517
|
+
borderColor: 'var(--bg)'
|
|
183
518
|
}]
|
|
184
519
|
},
|
|
185
|
-
options: {
|
|
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: '#
|
|
196
|
-
borderRadius:
|
|
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: {
|
|
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>`;
|