aifastdb-devplan 1.6.1 → 1.6.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/dist/dev-plan-document-store.d.ts +13 -1
- package/dist/dev-plan-document-store.d.ts.map +1 -1
- package/dist/dev-plan-document-store.js +119 -0
- package/dist/dev-plan-document-store.js.map +1 -1
- package/dist/dev-plan-factory.d.ts.map +1 -1
- package/dist/dev-plan-factory.js +2 -1
- package/dist/dev-plan-factory.js.map +1 -1
- package/dist/dev-plan-graph-store.d.ts +64 -1
- package/dist/dev-plan-graph-store.d.ts.map +1 -1
- package/dist/dev-plan-graph-store.js +364 -2
- package/dist/dev-plan-graph-store.js.map +1 -1
- package/dist/dev-plan-interface.d.ts +24 -1
- package/dist/dev-plan-interface.d.ts.map +1 -1
- package/dist/dev-plan-migrate.d.ts +1 -0
- package/dist/dev-plan-migrate.d.ts.map +1 -1
- package/dist/dev-plan-migrate.js +28 -2
- package/dist/dev-plan-migrate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.js +119 -0
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/types.d.ts +88 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/api-compat.js +22 -12
- package/dist/visualize/graph-canvas/api-compat.js.map +1 -1
- package/dist/visualize/graph-canvas/core.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/core.js +296 -4
- package/dist/visualize/graph-canvas/core.js.map +1 -1
- package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/interaction.js +11 -0
- package/dist/visualize/graph-canvas/interaction.js.map +1 -1
- package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/layout-worker.js +45 -9
- package/dist/visualize/graph-canvas/layout-worker.js.map +1 -1
- package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/renderer.js +164 -33
- package/dist/visualize/graph-canvas/renderer.js.map +1 -1
- package/dist/visualize/graph-canvas/styles.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/styles.js +136 -121
- package/dist/visualize/graph-canvas/styles.js.map +1 -1
- package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/viewport.js +10 -0
- package/dist/visualize/graph-canvas/viewport.js.map +1 -1
- package/dist/visualize/server.js +149 -32
- package/dist/visualize/server.js.map +1 -1
- package/dist/visualize/template-core.d.ts +9 -0
- package/dist/visualize/template-core.d.ts.map +1 -0
- package/dist/visualize/template-core.js +714 -0
- package/dist/visualize/template-core.js.map +1 -0
- package/dist/visualize/template-data-loading.d.ts +7 -0
- package/dist/visualize/template-data-loading.d.ts.map +1 -0
- package/dist/visualize/template-data-loading.js +677 -0
- package/dist/visualize/template-data-loading.js.map +1 -0
- package/dist/visualize/template-detail-panel.d.ts +14 -0
- package/dist/visualize/template-detail-panel.d.ts.map +1 -0
- package/dist/visualize/template-detail-panel.js +553 -0
- package/dist/visualize/template-detail-panel.js.map +1 -0
- package/dist/visualize/template-graph-3d.d.ts +7 -0
- package/dist/visualize/template-graph-3d.d.ts.map +1 -0
- package/dist/visualize/template-graph-3d.js +1112 -0
- package/dist/visualize/template-graph-3d.js.map +1 -0
- package/dist/visualize/template-graph-vis.d.ts +8 -0
- package/dist/visualize/template-graph-vis.d.ts.map +1 -0
- package/dist/visualize/template-graph-vis.js +1204 -0
- package/dist/visualize/template-graph-vis.js.map +1 -0
- package/dist/visualize/template-html.d.ts +9 -0
- package/dist/visualize/template-html.d.ts.map +1 -0
- package/dist/visualize/template-html.js +484 -0
- package/dist/visualize/template-html.js.map +1 -0
- package/dist/visualize/template-pages.d.ts +7 -0
- package/dist/visualize/template-pages.d.ts.map +1 -0
- package/dist/visualize/template-pages.js +806 -0
- package/dist/visualize/template-pages.js.map +1 -0
- package/dist/visualize/template-stats-modal.d.ts +7 -0
- package/dist/visualize/template-stats-modal.d.ts.map +1 -0
- package/dist/visualize/template-stats-modal.js +406 -0
- package/dist/visualize/template-stats-modal.js.map +1 -0
- package/dist/visualize/template-styles.d.ts +9 -0
- package/dist/visualize/template-styles.d.ts.map +1 -0
- package/dist/visualize/template-styles.js +487 -0
- package/dist/visualize/template-styles.js.map +1 -0
- package/dist/visualize/template.d.ts +14 -3
- package/dist/visualize/template.d.ts.map +1 -1
- package/dist/visualize/template.js +38 -3475
- package/dist/visualize/template.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevPlan 图可视化 — 页面模块
|
|
4
|
+
*
|
|
5
|
+
* 包含: 文档浏览页、RAG 聊天、统计仪表盘。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.getPagesScript = getPagesScript;
|
|
9
|
+
function getPagesScript() {
|
|
10
|
+
return `
|
|
11
|
+
// ========== Docs Browser ==========
|
|
12
|
+
var docsLoaded = false;
|
|
13
|
+
var docsData = []; // 全部文档列表
|
|
14
|
+
var currentDocKey = ''; // 当前选中文档的 key (section|subSection)
|
|
15
|
+
|
|
16
|
+
/** 根据 docKey 从 docsData 中查找文档标题 */
|
|
17
|
+
function findDocTitle(docKey) {
|
|
18
|
+
for (var i = 0; i < docsData.length; i++) {
|
|
19
|
+
var d = docsData[i];
|
|
20
|
+
var key = d.section + (d.subSection ? '|' + d.subSection : '');
|
|
21
|
+
if (key === docKey) return d.title;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Section 类型的中文名称映射 */
|
|
27
|
+
var SECTION_NAMES = {
|
|
28
|
+
overview: '概述', core_concepts: '核心概念', api_design: 'API 设计',
|
|
29
|
+
file_structure: '文件结构', config: '配置', examples: '使用示例',
|
|
30
|
+
technical_notes: '技术笔记', api_endpoints: 'API 端点',
|
|
31
|
+
milestones: '里程碑', changelog: '变更记录', custom: '自定义'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Section 图标映射(使用简洁符号替代 emoji) */
|
|
35
|
+
var SECTION_ICONS = {
|
|
36
|
+
overview: '▸', core_concepts: '▸', api_design: '▸',
|
|
37
|
+
file_structure: '▸', config: '▸', examples: '▸',
|
|
38
|
+
technical_notes: '▸', api_endpoints: '▸',
|
|
39
|
+
milestones: '▸', changelog: '▸', custom: '▸'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 加载文档数据(全局共享,仅请求一次) */
|
|
43
|
+
function loadDocsData(callback) {
|
|
44
|
+
if (docsLoaded && docsData.length > 0) {
|
|
45
|
+
if (callback) callback(docsData, null);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
fetch('/api/docs').then(function(r) { return r.json(); }).then(function(data) {
|
|
49
|
+
docsData = data.docs || [];
|
|
50
|
+
docsLoaded = true;
|
|
51
|
+
if (callback) callback(docsData, null);
|
|
52
|
+
}).catch(function(err) {
|
|
53
|
+
if (callback) callback(null, err);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadDocsPage() {
|
|
58
|
+
var list = document.getElementById('docsGroupList');
|
|
59
|
+
// 数据已加载(可能由全局文档弹层预先加载),直接渲染到侧边栏
|
|
60
|
+
if (docsLoaded && docsData.length > 0) {
|
|
61
|
+
renderDocsList(docsData);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (list) list.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;"><div class="spinner" style="margin:0 auto 12px;width:24px;height:24px;border-width:3px;"></div>加载文档列表...</div>';
|
|
65
|
+
|
|
66
|
+
loadDocsData(function(data, err) {
|
|
67
|
+
if (err) {
|
|
68
|
+
if (list) list.innerHTML = '<div style="text-align:center;padding:40px;color:#f87171;font-size:12px;">加载失败: ' + err.message + '<br><span style="cursor:pointer;color:#818cf8;text-decoration:underline;" onclick="docsLoaded=false;loadDocsPage();">重试</span></div>';
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
renderDocsList(docsData);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 获取文档的 key(唯一标识) */
|
|
76
|
+
function docItemKey(item) {
|
|
77
|
+
return item.section + (item.subSection ? '|' + item.subSection : '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 记录哪些父文档处于折叠状态(key → true 表示折叠) */
|
|
81
|
+
var docsCollapsedState = {};
|
|
82
|
+
|
|
83
|
+
/** 将文档列表按 section 分组渲染,支持 parentDoc 层级
|
|
84
|
+
* @param docs - 文档数组
|
|
85
|
+
* @param targetId - 渲染目标容器 ID (默认 'docsGroupList')
|
|
86
|
+
* @param selectFn - 点击文档时调用的函数名 (默认 'selectDoc')
|
|
87
|
+
*/
|
|
88
|
+
function renderDocsList(docs, targetId, selectFn) {
|
|
89
|
+
var list = document.getElementById(targetId || 'docsGroupList');
|
|
90
|
+
if (!list) return;
|
|
91
|
+
selectFn = selectFn || 'selectDoc';
|
|
92
|
+
|
|
93
|
+
// 建立 parentDoc → children 映射,区分顶级和子文档
|
|
94
|
+
var childrenMap = {}; // parentDocKey → [child items]
|
|
95
|
+
var childKeySet = {}; // 属于子文档的 key 集合
|
|
96
|
+
for (var i = 0; i < docs.length; i++) {
|
|
97
|
+
var d = docs[i];
|
|
98
|
+
if (d.parentDoc) {
|
|
99
|
+
if (!childrenMap[d.parentDoc]) childrenMap[d.parentDoc] = [];
|
|
100
|
+
childrenMap[d.parentDoc].push(d);
|
|
101
|
+
childKeySet[docItemKey(d)] = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 按 section 分组(只放顶级文档)
|
|
106
|
+
var groups = {};
|
|
107
|
+
var groupOrder = [];
|
|
108
|
+
for (var i = 0; i < docs.length; i++) {
|
|
109
|
+
var d = docs[i];
|
|
110
|
+
var key = docItemKey(d);
|
|
111
|
+
if (childKeySet[key]) continue; // 跳过子文档(由父文档渲染)
|
|
112
|
+
var sec = d.section;
|
|
113
|
+
if (!groups[sec]) {
|
|
114
|
+
groups[sec] = [];
|
|
115
|
+
groupOrder.push(sec);
|
|
116
|
+
}
|
|
117
|
+
groups[sec].push(d);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 每组内按 updatedAt 倒序排列(最新的在上方)
|
|
121
|
+
for (var gi = 0; gi < groupOrder.length; gi++) {
|
|
122
|
+
groups[groupOrder[gi]].sort(function(a, b) {
|
|
123
|
+
var ta = a.updatedAt || 0;
|
|
124
|
+
var tb = b.updatedAt || 0;
|
|
125
|
+
return tb - ta; // 降序
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 子文档也按 updatedAt 倒序
|
|
130
|
+
var parentKeys = Object.keys(childrenMap);
|
|
131
|
+
for (var pi = 0; pi < parentKeys.length; pi++) {
|
|
132
|
+
childrenMap[parentKeys[pi]].sort(function(a, b) {
|
|
133
|
+
var ta = a.updatedAt || 0;
|
|
134
|
+
var tb = b.updatedAt || 0;
|
|
135
|
+
return tb - ta;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 分组按最新文档日期排序(最新的分组在上)
|
|
140
|
+
groupOrder.sort(function(secA, secB) {
|
|
141
|
+
var maxA = 0, maxB = 0;
|
|
142
|
+
var itemsA = groups[secA] || [];
|
|
143
|
+
var itemsB = groups[secB] || [];
|
|
144
|
+
for (var k = 0; k < itemsA.length; k++) {
|
|
145
|
+
if ((itemsA[k].updatedAt || 0) > maxA) maxA = itemsA[k].updatedAt || 0;
|
|
146
|
+
}
|
|
147
|
+
for (var k = 0; k < itemsB.length; k++) {
|
|
148
|
+
if ((itemsB[k].updatedAt || 0) > maxB) maxB = itemsB[k].updatedAt || 0;
|
|
149
|
+
}
|
|
150
|
+
return maxB - maxA;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (groupOrder.length === 0) {
|
|
154
|
+
list.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;">暂无文档</div>';
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var html = '';
|
|
159
|
+
for (var gi = 0; gi < groupOrder.length; gi++) {
|
|
160
|
+
var sec = groupOrder[gi];
|
|
161
|
+
var items = groups[sec];
|
|
162
|
+
var secName = SECTION_NAMES[sec] || sec;
|
|
163
|
+
var secIcon = SECTION_ICONS[sec] || '▸';
|
|
164
|
+
|
|
165
|
+
// 计算此分组下文档总数(含子文档)
|
|
166
|
+
var totalCount = 0;
|
|
167
|
+
for (var ci = 0; ci < docs.length; ci++) {
|
|
168
|
+
if (docs[ci].section === sec) totalCount++;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
html += '<div class="docs-group" data-section="' + sec + '">';
|
|
172
|
+
html += '<div class="docs-group-title" onclick="toggleDocsGroup(this)">';
|
|
173
|
+
html += '<span class="docs-group-arrow">▼</span>';
|
|
174
|
+
html += '<span>' + secName + '</span>';
|
|
175
|
+
html += '<span class="docs-group-count">' + totalCount + '</span>';
|
|
176
|
+
html += '</div>';
|
|
177
|
+
html += '<div class="docs-group-items">';
|
|
178
|
+
|
|
179
|
+
for (var ii = 0; ii < items.length; ii++) {
|
|
180
|
+
html += renderDocItemWithChildren(items[ii], childrenMap, secIcon, selectFn);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
html += '</div></div>';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
list.innerHTML = html;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 递归渲染文档项及其子文档
|
|
190
|
+
* @param selectFn - 点击时调用的函数名 (默认 'selectDoc')
|
|
191
|
+
*/
|
|
192
|
+
function renderDocItemWithChildren(item, childrenMap, secIcon, selectFn) {
|
|
193
|
+
selectFn = selectFn || 'selectDoc';
|
|
194
|
+
var docKey = docItemKey(item);
|
|
195
|
+
var isActive = docKey === currentDocKey ? ' active' : '';
|
|
196
|
+
var children = childrenMap[docKey] || [];
|
|
197
|
+
var hasChildren = children.length > 0;
|
|
198
|
+
var isCollapsed = docsCollapsedState[docKey] === true;
|
|
199
|
+
|
|
200
|
+
var html = '<div class="docs-item-wrapper">';
|
|
201
|
+
|
|
202
|
+
// 文档项本身
|
|
203
|
+
html += '<div class="docs-item' + isActive + '" data-key="' + escHtml(docKey) + '" onclick="' + selectFn + '(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
204
|
+
|
|
205
|
+
if (hasChildren) {
|
|
206
|
+
var toggleIcon = isCollapsed ? '+' : '−';
|
|
207
|
+
html += '<span class="docs-item-toggle" onclick="event.stopPropagation();toggleDocChildren(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)" title="' + (isCollapsed ? '展开子文档' : '收起子文档') + '">' + toggleIcon + '</span>';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 不显示 emoji 图标,仅保留标题
|
|
211
|
+
html += '<span class="docs-item-text" title="' + escHtml(item.title) + '">' + escHtml(item.title) + '</span>';
|
|
212
|
+
if (hasChildren) {
|
|
213
|
+
html += '<span class="docs-item-sub" style="color:#818cf8;">' + children.length + ' 子文档</span>';
|
|
214
|
+
}
|
|
215
|
+
// 右侧显示日期(替代原来的 subSection 标签)
|
|
216
|
+
if (item.updatedAt) {
|
|
217
|
+
html += '<span class="docs-item-sub">' + fmtDateShort(item.updatedAt) + '</span>';
|
|
218
|
+
}
|
|
219
|
+
html += '</div>';
|
|
220
|
+
|
|
221
|
+
// 子文档列表
|
|
222
|
+
if (hasChildren) {
|
|
223
|
+
html += '<div class="docs-children' + (isCollapsed ? ' collapsed' : '') + '" data-parent="' + escHtml(docKey) + '">';
|
|
224
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
225
|
+
html += renderDocItemWithChildren(children[ci], childrenMap, secIcon, selectFn);
|
|
226
|
+
}
|
|
227
|
+
html += '</div>';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
html += '</div>';
|
|
231
|
+
return html;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 展开/折叠子文档 */
|
|
235
|
+
function toggleDocChildren(docKey) {
|
|
236
|
+
docsCollapsedState[docKey] = !docsCollapsedState[docKey];
|
|
237
|
+
var container = document.querySelector('.docs-children[data-parent="' + docKey + '"]');
|
|
238
|
+
if (!container) return;
|
|
239
|
+
container.classList.toggle('collapsed');
|
|
240
|
+
// 更新切换按钮图标
|
|
241
|
+
var wrapper = container.previousElementSibling;
|
|
242
|
+
if (wrapper) {
|
|
243
|
+
var toggle = wrapper.querySelector('.docs-item-toggle');
|
|
244
|
+
if (toggle) {
|
|
245
|
+
toggle.textContent = docsCollapsedState[docKey] ? '+' : '−';
|
|
246
|
+
toggle.title = docsCollapsedState[docKey] ? '展开子文档' : '收起子文档';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 展开/折叠文档分组 */
|
|
252
|
+
function toggleDocsGroup(el) {
|
|
253
|
+
var group = el.closest('.docs-group');
|
|
254
|
+
if (group) group.classList.toggle('collapsed');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 控制搜索框清除按钮的显示/隐藏
|
|
258
|
+
* @param searchId - 搜索输入框 ID (默认 'docsSearch')
|
|
259
|
+
* @param clearBtnId - 清除按钮 ID (默认 'docsSearchClear')
|
|
260
|
+
*/
|
|
261
|
+
function toggleSearchClear(searchId, clearBtnId) {
|
|
262
|
+
var input = document.getElementById(searchId || 'docsSearch');
|
|
263
|
+
var btn = document.getElementById(clearBtnId || 'docsSearchClear');
|
|
264
|
+
if (input && btn) {
|
|
265
|
+
if (input.value.length > 0) { btn.classList.add('show'); } else { btn.classList.remove('show'); }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 清空搜索框并重置列表
|
|
270
|
+
* @param searchId - 搜索输入框 ID (默认 'docsSearch')
|
|
271
|
+
* @param clearBtnId - 清除按钮 ID (默认 'docsSearchClear')
|
|
272
|
+
* @param targetId - 渲染目标容器 ID (默认 'docsGroupList')
|
|
273
|
+
* @param selectFn - 点击文档时调用的函数名 (默认 'selectDoc')
|
|
274
|
+
*/
|
|
275
|
+
function clearDocsSearch(searchId, clearBtnId, targetId, selectFn) {
|
|
276
|
+
var input = document.getElementById(searchId || 'docsSearch');
|
|
277
|
+
if (input) { input.value = ''; input.focus(); }
|
|
278
|
+
toggleSearchClear(searchId, clearBtnId);
|
|
279
|
+
filterDocs(searchId, targetId, selectFn);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 搜索过滤文档列表
|
|
283
|
+
* @param searchId - 搜索输入框 ID (默认 'docsSearch')
|
|
284
|
+
* @param targetId - 渲染目标容器 ID (默认 'docsGroupList')
|
|
285
|
+
* @param selectFn - 点击文档时调用的函数名 (默认 'selectDoc')
|
|
286
|
+
*/
|
|
287
|
+
function filterDocs(searchId, targetId, selectFn) {
|
|
288
|
+
var input = document.getElementById(searchId || 'docsSearch');
|
|
289
|
+
var query = (input ? input.value || '' : '').toLowerCase().trim();
|
|
290
|
+
if (!query) {
|
|
291
|
+
renderDocsList(docsData, targetId, selectFn);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
var filtered = [];
|
|
295
|
+
for (var i = 0; i < docsData.length; i++) {
|
|
296
|
+
var d = docsData[i];
|
|
297
|
+
var text = (d.title || '') + ' ' + (d.section || '') + ' ' + (d.subSection || '');
|
|
298
|
+
if (text.toLowerCase().indexOf(query) >= 0) {
|
|
299
|
+
filtered.push(d);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
renderDocsList(filtered, targetId, selectFn);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 选中并加载文档内容 */
|
|
306
|
+
function selectDoc(docKey) {
|
|
307
|
+
currentDocKey = docKey;
|
|
308
|
+
|
|
309
|
+
// 更新左侧选中状态
|
|
310
|
+
var items = document.querySelectorAll('.docs-item');
|
|
311
|
+
for (var i = 0; i < items.length; i++) {
|
|
312
|
+
items[i].classList.remove('active');
|
|
313
|
+
if (items[i].getAttribute('data-key') === docKey) {
|
|
314
|
+
items[i].classList.add('active');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 解析 key
|
|
319
|
+
var parts = docKey.split('|');
|
|
320
|
+
var section = parts[0];
|
|
321
|
+
var subSection = parts[1] || null;
|
|
322
|
+
|
|
323
|
+
// 显示内容区,隐藏空状态
|
|
324
|
+
document.getElementById('docsEmptyState').style.display = 'none';
|
|
325
|
+
var contentView = document.getElementById('docsContentView');
|
|
326
|
+
contentView.style.display = 'flex';
|
|
327
|
+
|
|
328
|
+
// 显示加载状态
|
|
329
|
+
document.getElementById('docsContentTitle').textContent = '加载中...';
|
|
330
|
+
document.getElementById('docsContentMeta').innerHTML = '';
|
|
331
|
+
document.getElementById('docsContentInner').innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;"><div class="spinner" style="margin:0 auto 12px;width:24px;height:24px;border-width:3px;"></div></div>';
|
|
332
|
+
|
|
333
|
+
// 请求文档内容
|
|
334
|
+
var url = '/api/doc?section=' + encodeURIComponent(section);
|
|
335
|
+
if (subSection) url += '&subSection=' + encodeURIComponent(subSection);
|
|
336
|
+
|
|
337
|
+
fetch(url).then(function(r) {
|
|
338
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
339
|
+
return r.json();
|
|
340
|
+
}).then(function(doc) {
|
|
341
|
+
renderDocContent(doc, section, subSection);
|
|
342
|
+
}).catch(function(err) {
|
|
343
|
+
document.getElementById('docsContentTitle').textContent = '加载失败';
|
|
344
|
+
document.getElementById('docsContentInner').innerHTML = '<div style="text-align:center;padding:40px;color:#f87171;">加载失败: ' + escHtml(err.message) + '</div>';
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 渲染文档内容到右侧面板 */
|
|
349
|
+
function renderDocContent(doc, section, subSection) {
|
|
350
|
+
var secName = SECTION_NAMES[section] || section;
|
|
351
|
+
|
|
352
|
+
// 标题
|
|
353
|
+
document.getElementById('docsContentTitle').textContent = doc.title || secName;
|
|
354
|
+
|
|
355
|
+
// 元信息标签
|
|
356
|
+
var metaHtml = '<span class="docs-content-tag section">' + secName + '</span>';
|
|
357
|
+
if (subSection) {
|
|
358
|
+
metaHtml += '<span class="docs-content-tag section">' + escHtml(subSection) + '</span>';
|
|
359
|
+
}
|
|
360
|
+
if (doc.version) {
|
|
361
|
+
metaHtml += '<span class="docs-content-tag version">v' + escHtml(doc.version) + '</span>';
|
|
362
|
+
}
|
|
363
|
+
if (doc.updatedAt) {
|
|
364
|
+
metaHtml += '<span class="docs-content-tag">' + fmtTime(doc.updatedAt) + '</span>';
|
|
365
|
+
}
|
|
366
|
+
document.getElementById('docsContentMeta').innerHTML = metaHtml;
|
|
367
|
+
|
|
368
|
+
// Markdown 内容
|
|
369
|
+
var contentHtml = '';
|
|
370
|
+
if (doc.content) {
|
|
371
|
+
contentHtml = renderMarkdown(doc.content);
|
|
372
|
+
} else {
|
|
373
|
+
contentHtml = '<div style="text-align:center;padding:40px;color:#6b7280;">文档内容为空</div>';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 父文档链接
|
|
377
|
+
if (doc.parentDoc) {
|
|
378
|
+
var parentTitle = findDocTitle(doc.parentDoc);
|
|
379
|
+
contentHtml += '<div class="docs-related" style="margin-top: 12px;">';
|
|
380
|
+
contentHtml += '<div class="docs-related-title">⬆️ 父文档</div>';
|
|
381
|
+
contentHtml += '<div class="docs-related-item" style="cursor:pointer;" onclick="selectDoc(\\x27' + doc.parentDoc.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
382
|
+
contentHtml += '<span class="rel-icon" style="background:#1e3a5f;color:#93c5fd;">📄</span>';
|
|
383
|
+
contentHtml += '<span style="flex:1;color:#818cf8;">' + escHtml(parentTitle || doc.parentDoc) + '</span>';
|
|
384
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(doc.parentDoc) + '</span>';
|
|
385
|
+
contentHtml += '</div></div>';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 子文档列表
|
|
389
|
+
var childDocs = doc.childDocs || [];
|
|
390
|
+
if (childDocs.length > 0) {
|
|
391
|
+
contentHtml += '<div class="docs-related" style="margin-top: 12px;">';
|
|
392
|
+
contentHtml += '<div class="docs-related-title">⬇️ 子文档 (' + childDocs.length + ')</div>';
|
|
393
|
+
for (var ci = 0; ci < childDocs.length; ci++) {
|
|
394
|
+
var childKey = childDocs[ci];
|
|
395
|
+
var childTitle = findDocTitle(childKey);
|
|
396
|
+
contentHtml += '<div class="docs-related-item" style="cursor:pointer;" onclick="selectDoc(\\x27' + childKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
397
|
+
contentHtml += '<span class="rel-icon" style="background:#1e1b4b;color:#c084fc;">📄</span>';
|
|
398
|
+
contentHtml += '<span style="flex:1;color:#c084fc;">' + escHtml(childTitle || childKey) + '</span>';
|
|
399
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(childKey) + '</span>';
|
|
400
|
+
contentHtml += '</div>';
|
|
401
|
+
}
|
|
402
|
+
contentHtml += '</div>';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 关联任务
|
|
406
|
+
var relatedTasks = doc.relatedTasks || [];
|
|
407
|
+
if (relatedTasks.length > 0) {
|
|
408
|
+
contentHtml += '<div class="docs-related">';
|
|
409
|
+
contentHtml += '<div class="docs-related-title">🔗 关联任务 (' + relatedTasks.length + ')</div>';
|
|
410
|
+
for (var i = 0; i < relatedTasks.length; i++) {
|
|
411
|
+
var t = relatedTasks[i];
|
|
412
|
+
var tStatus = t.status || 'pending';
|
|
413
|
+
var tIcon = tStatus === 'completed' ? '✓' : tStatus === 'in_progress' ? '▶' : '○';
|
|
414
|
+
var iconBg = tStatus === 'completed' ? '#064e3b' : tStatus === 'in_progress' ? '#1e3a5f' : '#374151';
|
|
415
|
+
var iconColor = tStatus === 'completed' ? '#6ee7b7' : tStatus === 'in_progress' ? '#93c5fd' : '#6b7280';
|
|
416
|
+
contentHtml += '<div class="docs-related-item">';
|
|
417
|
+
contentHtml += '<span class="rel-icon" style="background:' + iconBg + ';color:' + iconColor + ';">' + tIcon + '</span>';
|
|
418
|
+
contentHtml += '<span style="flex:1;">' + escHtml(t.title) + '</span>';
|
|
419
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(t.taskId) + '</span>';
|
|
420
|
+
if (t.priority) {
|
|
421
|
+
contentHtml += '<span class="status-badge priority-' + t.priority + '" style="font-size:10px;">' + t.priority + '</span>';
|
|
422
|
+
}
|
|
423
|
+
contentHtml += '</div>';
|
|
424
|
+
}
|
|
425
|
+
contentHtml += '</div>';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
document.getElementById('docsContentInner').innerHTML = contentHtml;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
// ========== RAG Chat ==========
|
|
433
|
+
var chatHistory = []; // [{role:'user'|'assistant', content:string, results?:array}]
|
|
434
|
+
var chatBusy = false;
|
|
435
|
+
|
|
436
|
+
/** 点击推荐话题 */
|
|
437
|
+
function chatSendTip(el) {
|
|
438
|
+
var input = document.getElementById('docsChatInput');
|
|
439
|
+
if (input) { input.value = el.textContent; chatSend(); }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Enter 发送(Shift+Enter 换行) */
|
|
443
|
+
function chatInputKeydown(e) {
|
|
444
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
chatSend();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** 自动调整 textarea 高度 */
|
|
451
|
+
function chatAutoResize(el) {
|
|
452
|
+
el.style.height = 'auto';
|
|
453
|
+
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** 发送消息并搜索 */
|
|
457
|
+
function chatSend() {
|
|
458
|
+
if (chatBusy) return;
|
|
459
|
+
var input = document.getElementById('docsChatInput');
|
|
460
|
+
var query = (input.value || '').trim();
|
|
461
|
+
if (!query) return;
|
|
462
|
+
|
|
463
|
+
// 隐藏欢迎信息
|
|
464
|
+
var welcome = document.getElementById('docsChatWelcome');
|
|
465
|
+
if (welcome) welcome.style.display = 'none';
|
|
466
|
+
|
|
467
|
+
// 添加用户消息
|
|
468
|
+
chatHistory.push({ role: 'user', content: query });
|
|
469
|
+
chatRenderBubble('user', query);
|
|
470
|
+
input.value = '';
|
|
471
|
+
chatAutoResize(input);
|
|
472
|
+
|
|
473
|
+
// 显示加载动画
|
|
474
|
+
chatBusy = true;
|
|
475
|
+
document.getElementById('docsChatSend').disabled = true;
|
|
476
|
+
var loadingId = 'chat-loading-' + Date.now();
|
|
477
|
+
var msgBox = document.getElementById('docsChatMessages');
|
|
478
|
+
var loadingHtml = '<div class="chat-bubble assistant" id="' + loadingId + '"><div class="chat-bubble-inner"><div class="chat-typing"><div class="chat-typing-dot"></div><div class="chat-typing-dot"></div><div class="chat-typing-dot"></div></div></div></div>';
|
|
479
|
+
msgBox.insertAdjacentHTML('beforeend', loadingHtml);
|
|
480
|
+
msgBox.scrollTop = msgBox.scrollHeight;
|
|
481
|
+
|
|
482
|
+
// 调用搜索 API
|
|
483
|
+
fetch('/api/chat', {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
486
|
+
body: JSON.stringify({ query: query, limit: 5 })
|
|
487
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
488
|
+
// 移除加载动画
|
|
489
|
+
var loadEl = document.getElementById(loadingId);
|
|
490
|
+
if (loadEl) loadEl.remove();
|
|
491
|
+
|
|
492
|
+
var replyHtml = '';
|
|
493
|
+
|
|
494
|
+
if (data.type === 'meta') {
|
|
495
|
+
// ---- 元信息直接回答 ----
|
|
496
|
+
replyHtml = chatFormatMarkdown(data.answer || '');
|
|
497
|
+
} else {
|
|
498
|
+
// ---- 文档搜索结果 ----
|
|
499
|
+
var results = data.results || [];
|
|
500
|
+
if (results.length > 0) {
|
|
501
|
+
replyHtml += '<div style="margin-bottom:8px;color:#9ca3af;font-size:12px;">找到 <strong style="color:#a5b4fc;">' + results.length + '</strong> 篇相关文档';
|
|
502
|
+
if (data.mode === 'hybrid') replyHtml += ' <span style="font-size:10px;color:#6b7280;">(语义+字面混合)</span>';
|
|
503
|
+
else if (data.mode === 'semantic') replyHtml += ' <span style="font-size:10px;color:#6b7280;">(语义搜索)</span>';
|
|
504
|
+
else replyHtml += ' <span style="font-size:10px;color:#6b7280;">(字面搜索)</span>';
|
|
505
|
+
replyHtml += '</div>';
|
|
506
|
+
|
|
507
|
+
for (var i = 0; i < results.length; i++) {
|
|
508
|
+
var r = results[i];
|
|
509
|
+
var docKey = r.section + (r.subSection ? '|' + r.subSection : '');
|
|
510
|
+
replyHtml += '<div class="chat-result-card" onclick="chatOpenDoc(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
511
|
+
replyHtml += '<div class="chat-result-title">';
|
|
512
|
+
replyHtml += '<span>📄 ' + escHtml(r.title) + '</span>';
|
|
513
|
+
if (r.score != null) replyHtml += '<span class="chat-result-score">' + r.score.toFixed(3) + '</span>';
|
|
514
|
+
replyHtml += '</div>';
|
|
515
|
+
if (r.snippet) replyHtml += '<div class="chat-result-snippet">' + escHtml(r.snippet) + '</div>';
|
|
516
|
+
var metaParts = [];
|
|
517
|
+
if (r.section) metaParts.push(r.section);
|
|
518
|
+
if (r.updatedAt) metaParts.push(fmtDateShort(r.updatedAt));
|
|
519
|
+
if (r.version) metaParts.push('v' + r.version);
|
|
520
|
+
if (metaParts.length > 0) replyHtml += '<div class="chat-result-meta">' + metaParts.join(' · ') + '</div>';
|
|
521
|
+
replyHtml += '</div>';
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
replyHtml += '<div class="chat-no-result">🤔 未找到高度相关的文档。</div>';
|
|
525
|
+
replyHtml += '<div style="margin-top:8px;font-size:12px;color:#6b7280;line-height:1.6;">';
|
|
526
|
+
replyHtml += '建议:<br>';
|
|
527
|
+
replyHtml += '• 尝试使用更具体的 <strong>关键词</strong>(如 "向量搜索"、"GPU"、"LanceDB")<br>';
|
|
528
|
+
replyHtml += '• 问项目统计问题(如 "有多少篇文档"、"项目进度"、"有哪些阶段")<br>';
|
|
529
|
+
replyHtml += '• 输入 <strong>"帮助"</strong> 查看我的全部能力';
|
|
530
|
+
replyHtml += '</div>';
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
chatHistory.push({ role: 'assistant', content: replyHtml, results: data.results || [] });
|
|
535
|
+
chatRenderBubble('assistant', replyHtml, true);
|
|
536
|
+
|
|
537
|
+
}).catch(function(err) {
|
|
538
|
+
var loadEl = document.getElementById(loadingId);
|
|
539
|
+
if (loadEl) loadEl.remove();
|
|
540
|
+
chatRenderBubble('assistant', '<span style="color:#f87171;">搜索出错: ' + escHtml(err.message) + '</span>', true);
|
|
541
|
+
}).finally(function() {
|
|
542
|
+
chatBusy = false;
|
|
543
|
+
document.getElementById('docsChatSend').disabled = false;
|
|
544
|
+
document.getElementById('docsChatInput').focus();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** 简单 Markdown → HTML 转换(用于元信息回答) */
|
|
549
|
+
function chatFormatMarkdown(text) {
|
|
550
|
+
return text
|
|
551
|
+
.replace(/\\*\\*(.+?)\\*\\*/g, '<strong style="color:#a5b4fc;">$1</strong>')
|
|
552
|
+
.replace(/\\n/g, '<br>');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** 渲染一条消息气泡 */
|
|
556
|
+
function chatRenderBubble(role, content, isHtml) {
|
|
557
|
+
var msgBox = document.getElementById('docsChatMessages');
|
|
558
|
+
var bubble = document.createElement('div');
|
|
559
|
+
bubble.className = 'chat-bubble ' + role;
|
|
560
|
+
var inner = document.createElement('div');
|
|
561
|
+
inner.className = 'chat-bubble-inner';
|
|
562
|
+
if (isHtml) { inner.innerHTML = content; }
|
|
563
|
+
else { inner.textContent = content; }
|
|
564
|
+
bubble.appendChild(inner);
|
|
565
|
+
msgBox.appendChild(bubble);
|
|
566
|
+
msgBox.scrollTop = msgBox.scrollHeight;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** 从聊天结果中点击打开文档 */
|
|
570
|
+
function chatOpenDoc(docKey) {
|
|
571
|
+
selectDoc(docKey);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** 返回聊天视图 */
|
|
575
|
+
function backToChat() {
|
|
576
|
+
document.getElementById('docsContentView').style.display = 'none';
|
|
577
|
+
document.getElementById('docsEmptyState').style.display = 'flex';
|
|
578
|
+
// 取消左侧选中
|
|
579
|
+
currentDocKey = '';
|
|
580
|
+
var items = document.querySelectorAll('.docs-item');
|
|
581
|
+
for (var i = 0; i < items.length; i++) items[i].classList.remove('active');
|
|
582
|
+
// 聚焦输入框
|
|
583
|
+
var input = document.getElementById('docsChatInput');
|
|
584
|
+
if (input) input.focus();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
// ========== Stats Dashboard ==========
|
|
589
|
+
var statsLoaded = false;
|
|
590
|
+
|
|
591
|
+
function loadStatsPage() {
|
|
592
|
+
var container = document.getElementById('statsContent');
|
|
593
|
+
if (!container) return;
|
|
594
|
+
container.innerHTML = '<div style="text-align:center;padding:60px;color:#6b7280;"><div class="spinner" style="margin:0 auto 12px;"></div>加载统计数据...</div>';
|
|
595
|
+
|
|
596
|
+
fetch('/api/stats').then(function(r) { return r.json(); }).then(function(data) {
|
|
597
|
+
statsLoaded = true;
|
|
598
|
+
renderStatsPage(data);
|
|
599
|
+
}).catch(function(err) {
|
|
600
|
+
container.innerHTML = '<div style="text-align:center;padding:60px;color:#f87171;">加载失败: ' + err.message + '<br><button class="refresh-btn" onclick="loadStatsPage()" style="margin-top:12px;">重试</button></div>';
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function renderStatsPage(data) {
|
|
605
|
+
var container = document.getElementById('statsContent');
|
|
606
|
+
if (!container) return;
|
|
607
|
+
|
|
608
|
+
var pct = data.overallPercent || 0;
|
|
609
|
+
var totalSub = data.subTaskCount || 0;
|
|
610
|
+
var doneSub = data.completedSubTasks || 0;
|
|
611
|
+
var totalMain = data.mainTaskCount || 0;
|
|
612
|
+
var doneMain = data.completedMainTasks || 0;
|
|
613
|
+
var docCount = data.docCount || 0;
|
|
614
|
+
var modCount = data.moduleCount || 0;
|
|
615
|
+
|
|
616
|
+
// 激励语
|
|
617
|
+
var motivate = '';
|
|
618
|
+
if (pct >= 100) motivate = '🎉 项目已全部完成!太棒了!';
|
|
619
|
+
else if (pct >= 75) motivate = '🚀 即将大功告成,冲刺阶段!';
|
|
620
|
+
else if (pct >= 50) motivate = '💪 已过半程,保持节奏!';
|
|
621
|
+
else if (pct >= 25) motivate = '🌱 稳步推进中,继续加油!';
|
|
622
|
+
else if (pct > 0) motivate = '🏗️ 万事开头难,已迈出第一步!';
|
|
623
|
+
else motivate = '📋 项目已规划就绪,开始行动吧!';
|
|
624
|
+
|
|
625
|
+
var html = '';
|
|
626
|
+
|
|
627
|
+
// ===== 总体进度环 =====
|
|
628
|
+
var ringR = 54;
|
|
629
|
+
var ringC = 2 * Math.PI * ringR;
|
|
630
|
+
var ringOffset = ringC - (pct / 100) * ringC;
|
|
631
|
+
html += '<div class="progress-ring-wrap">';
|
|
632
|
+
html += '<svg class="ring-svg" width="140" height="140" viewBox="0 0 140 140">';
|
|
633
|
+
html += '<circle cx="70" cy="70" r="' + ringR + '" stroke="#374151" stroke-width="10" fill="none"/>';
|
|
634
|
+
html += '<circle cx="70" cy="70" r="' + ringR + '" stroke="url(#ringGrad)" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="' + ringC + '" stroke-dashoffset="' + ringOffset + '" transform="rotate(-90 70 70)" style="transition:stroke-dashoffset 1s ease;"/>';
|
|
635
|
+
html += '<defs><linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#10b981"/><stop offset="100%" stop-color="#3b82f6"/></linearGradient></defs>';
|
|
636
|
+
html += '<text x="70" y="65" text-anchor="middle" fill="#f3f4f6" font-size="28" font-weight="800">' + pct + '%</text>';
|
|
637
|
+
html += '<text x="70" y="84" text-anchor="middle" fill="#6b7280" font-size="11">完成率</text>';
|
|
638
|
+
html += '</svg>';
|
|
639
|
+
html += '<div class="progress-ring-info">';
|
|
640
|
+
html += '<h3>项目总体进度</h3>';
|
|
641
|
+
html += '<p>子任务完成 <strong style="color:#10b981;">' + doneSub + '</strong> / ' + totalSub + ',主任务完成 <strong style="color:#3b82f6;">' + doneMain + '</strong> / ' + totalMain + '</p>';
|
|
642
|
+
html += '<div class="motivate">' + motivate + '</div>';
|
|
643
|
+
html += '</div></div>';
|
|
644
|
+
|
|
645
|
+
// ===== 概览卡片 =====
|
|
646
|
+
html += '<div class="stats-grid">';
|
|
647
|
+
html += statCard('📋', totalMain, '主任务', doneMain + ' 已完成', 'blue');
|
|
648
|
+
html += statCard('✅', doneSub, '已完成子任务', '共 ' + totalSub + ' 个子任务', 'green');
|
|
649
|
+
html += statCard('📄', docCount, '文档', Object.keys(data.docBySection || {}).length + ' 种类型', 'purple');
|
|
650
|
+
html += statCard('🧩', modCount, '功能模块', '', 'amber');
|
|
651
|
+
var remainSub = totalSub - doneSub;
|
|
652
|
+
html += statCard('⏳', remainSub, '待完成子任务', remainSub > 0 ? '继续努力!' : '全部完成!', 'rose');
|
|
653
|
+
html += '</div>';
|
|
654
|
+
|
|
655
|
+
// ===== 按优先级统计 =====
|
|
656
|
+
var bp = data.byPriority || {};
|
|
657
|
+
html += '<div class="stats-section">';
|
|
658
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🎯</span> 按优先级统计</div>';
|
|
659
|
+
html += '<div class="priority-bars">';
|
|
660
|
+
var priorities = ['P0', 'P1', 'P2'];
|
|
661
|
+
for (var pi = 0; pi < priorities.length; pi++) {
|
|
662
|
+
var pk = priorities[pi];
|
|
663
|
+
var pd = bp[pk] || { total: 0, completed: 0 };
|
|
664
|
+
var ppct = pd.total > 0 ? Math.round(pd.completed / pd.total * 100) : 0;
|
|
665
|
+
html += '<div class="priority-row">';
|
|
666
|
+
html += '<span class="priority-label ' + pk + '">' + pk + '</span>';
|
|
667
|
+
html += '<div class="priority-bar-track"><div class="priority-bar-fill ' + pk + '" style="width:' + ppct + '%"></div></div>';
|
|
668
|
+
html += '<span class="priority-nums">' + pd.completed + '/' + pd.total + ' (' + ppct + '%)</span>';
|
|
669
|
+
html += '</div>';
|
|
670
|
+
}
|
|
671
|
+
html += '</div></div>';
|
|
672
|
+
|
|
673
|
+
// ===== 进行中的任务 =====
|
|
674
|
+
var inProg = data.inProgressPhases || [];
|
|
675
|
+
if (inProg.length > 0) {
|
|
676
|
+
html += '<div class="stats-section">';
|
|
677
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🔄</span> 进行中 (' + inProg.length + ')</div>';
|
|
678
|
+
html += '<div class="phase-list">';
|
|
679
|
+
for (var ii = 0; ii < inProg.length; ii++) {
|
|
680
|
+
html += phaseItem(inProg[ii], 'in_progress', '▶');
|
|
681
|
+
}
|
|
682
|
+
html += '</div></div>';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ===== 已完成的里程碑 =====
|
|
686
|
+
var done = data.completedPhases || [];
|
|
687
|
+
if (done.length > 0) {
|
|
688
|
+
html += '<div class="stats-section">';
|
|
689
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🏆</span> 已完成里程碑 (' + done.length + ')</div>';
|
|
690
|
+
html += '<div class="phase-list">';
|
|
691
|
+
for (var di = 0; di < done.length; di++) {
|
|
692
|
+
html += phaseItem(done[di], 'completed', '✓');
|
|
693
|
+
}
|
|
694
|
+
html += '</div></div>';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ===== 待开始的任务 =====
|
|
698
|
+
var pending = data.pendingPhases || [];
|
|
699
|
+
if (pending.length > 0) {
|
|
700
|
+
html += '<div class="stats-section">';
|
|
701
|
+
html += '<div class="stats-section-title"><span class="sec-icon">📌</span> 待开始 (' + pending.length + ')</div>';
|
|
702
|
+
html += '<div class="phase-list">';
|
|
703
|
+
for (var qi = 0; qi < pending.length; qi++) {
|
|
704
|
+
html += phaseItem(pending[qi], 'pending', '○');
|
|
705
|
+
}
|
|
706
|
+
html += '</div></div>';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ===== 模块概览 =====
|
|
710
|
+
var mods = data.moduleStats || [];
|
|
711
|
+
if (mods.length > 0) {
|
|
712
|
+
html += '<div class="stats-section">';
|
|
713
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🧩</span> 模块概览</div>';
|
|
714
|
+
html += '<div class="module-grid">';
|
|
715
|
+
for (var mi = 0; mi < mods.length; mi++) {
|
|
716
|
+
var mod = mods[mi];
|
|
717
|
+
var mpct = mod.subTaskCount > 0 ? Math.round(mod.completedSubTaskCount / mod.subTaskCount * 100) : 0;
|
|
718
|
+
html += '<div class="module-card">';
|
|
719
|
+
html += '<div class="module-card-header"><div class="module-card-dot" style="background:' + (mpct >= 100 ? '#10b981' : mpct > 0 ? '#3b82f6' : '#4b5563') + ';"></div><span class="module-card-name">' + escHtml(mod.name) + '</span></div>';
|
|
720
|
+
html += '<div class="module-card-bar"><div class="module-card-bar-fill" style="width:' + mpct + '%"></div></div>';
|
|
721
|
+
html += '<div class="module-card-stats"><span>' + mod.completedSubTaskCount + '/' + mod.subTaskCount + ' 子任务</span><span>' + mpct + '%</span></div>';
|
|
722
|
+
html += '</div>';
|
|
723
|
+
}
|
|
724
|
+
html += '</div></div>';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ===== 文档分布 =====
|
|
728
|
+
var docSec = data.docBySection || {};
|
|
729
|
+
var docKeys = Object.keys(docSec);
|
|
730
|
+
if (docKeys.length > 0) {
|
|
731
|
+
html += '<div class="stats-section">';
|
|
732
|
+
html += '<div class="stats-section-title"><span class="sec-icon">📚</span> 文档分布</div>';
|
|
733
|
+
html += '<div class="stats-grid">';
|
|
734
|
+
var secNames = { overview: '概述', core_concepts: '核心概念', api_design: 'API 设计', file_structure: '文件结构', config: '配置', examples: '示例', technical_notes: '技术笔记', api_endpoints: 'API 端点', milestones: '里程碑', changelog: '变更日志', custom: '自定义' };
|
|
735
|
+
for (var si = 0; si < docKeys.length; si++) {
|
|
736
|
+
var sk = docKeys[si];
|
|
737
|
+
html += '<div class="stat-card purple" style="padding:14px;">';
|
|
738
|
+
html += '<div style="font-size:20px;font-weight:800;color:#a5b4fc;">' + docSec[sk] + '</div>';
|
|
739
|
+
html += '<div style="font-size:11px;color:#9ca3af;margin-top:4px;">' + (secNames[sk] || sk) + '</div>';
|
|
740
|
+
html += '</div>';
|
|
741
|
+
}
|
|
742
|
+
html += '</div></div>';
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
container.innerHTML = html;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function statCard(icon, value, label, sub, color) {
|
|
749
|
+
return '<div class="stat-card ' + color + '"><div class="stat-card-icon">' + icon + '</div><div class="stat-card-value">' + value + '</div><div class="stat-card-label">' + label + '</div>' + (sub ? '<div class="stat-card-sub">' + sub + '</div>' : '') + '</div>';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function phaseItem(task, status, icon) {
|
|
753
|
+
var ppct = task.percent || 0;
|
|
754
|
+
var subText = task.total !== undefined ? (task.completed || 0) + '/' + task.total + ' 子任务' : task.taskId;
|
|
755
|
+
var subs = task.subTasks || [];
|
|
756
|
+
var rDocsCheck = task.relatedDocs || [];
|
|
757
|
+
var hasSubs = subs.length > 0 || rDocsCheck.length > 0;
|
|
758
|
+
var subIcons = { completed: '✓', in_progress: '◉', pending: '○', cancelled: '⊘' };
|
|
759
|
+
var mainTime = task.completedAt ? fmtTime(task.completedAt) : '';
|
|
760
|
+
var h = '<div class="phase-item-wrap">';
|
|
761
|
+
h += '<div class="phase-item-main" ' + (hasSubs ? 'onclick="togglePhaseExpand(this)"' : '') + '>';
|
|
762
|
+
if (hasSubs) { h += '<div class="phase-expand-icon">▶</div>'; }
|
|
763
|
+
h += '<div class="phase-status-icon ' + status + '">' + icon + '</div>';
|
|
764
|
+
h += '<div class="phase-info" style="flex:1;min-width:0;"><div class="phase-info-title">' + escHtml(task.title) + '</div>';
|
|
765
|
+
h += '<div class="phase-info-sub">' + escHtml(task.taskId) + ' · ' + subText;
|
|
766
|
+
if (mainTime) { h += ' · <span class="phase-time">✓ ' + mainTime + '</span>'; }
|
|
767
|
+
h += '</div></div>';
|
|
768
|
+
h += '<div class="phase-bar-mini"><div class="phase-bar-mini-fill" style="width:' + ppct + '%"></div></div>';
|
|
769
|
+
h += '<div class="phase-pct">' + ppct + '%</div>';
|
|
770
|
+
h += '</div>';
|
|
771
|
+
var rDocs = task.relatedDocs || [];
|
|
772
|
+
if (hasSubs || rDocs.length > 0) {
|
|
773
|
+
h += '<div class="phase-subtasks">';
|
|
774
|
+
for (var si = 0; si < subs.length; si++) {
|
|
775
|
+
var s = subs[si];
|
|
776
|
+
var ss = s.status || 'pending';
|
|
777
|
+
var subTime = s.completedAt ? fmtTime(s.completedAt) : '';
|
|
778
|
+
h += '<div class="phase-sub-item">';
|
|
779
|
+
h += '<div class="phase-sub-icon ' + ss + '">' + (subIcons[ss] || '○') + '</div>';
|
|
780
|
+
h += '<span class="phase-sub-name ' + ss + '">' + escHtml(s.title) + '</span>';
|
|
781
|
+
if (subTime) { h += '<span class="phase-sub-time">' + subTime + '</span>'; }
|
|
782
|
+
h += '<span class="phase-sub-id">' + escHtml(s.taskId) + '</span>';
|
|
783
|
+
h += '</div>';
|
|
784
|
+
}
|
|
785
|
+
if (rDocs.length > 0) {
|
|
786
|
+
h += '<div style="padding:6px 0 2px 8px;font-size:11px;color:#f59e0b;font-weight:600;">关联文档</div>';
|
|
787
|
+
for (var rd = 0; rd < rDocs.length; rd++) {
|
|
788
|
+
var rdoc = rDocs[rd];
|
|
789
|
+
var rdLabel = rdoc.section || '';
|
|
790
|
+
if (rdoc.subSection) rdLabel += ' / ' + rdoc.subSection;
|
|
791
|
+
h += '<div class="phase-sub-item">';
|
|
792
|
+
h += '<div class="phase-sub-icon" style="color:#f59e0b;">📄</div>';
|
|
793
|
+
h += '<span class="phase-sub-name">' + escHtml(rdoc.title) + '</span>';
|
|
794
|
+
h += '<span class="phase-sub-id">' + escHtml(rdLabel) + '</span>';
|
|
795
|
+
h += '</div>';
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
h += '</div>';
|
|
799
|
+
}
|
|
800
|
+
h += '</div>';
|
|
801
|
+
return h;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
`;
|
|
805
|
+
}
|
|
806
|
+
//# sourceMappingURL=template-pages.js.map
|