sandtable 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dashboard/css/dashboard.css +731 -0
- package/dashboard/dashboard.html +1086 -0
- package/dashboard/js/conventions-viewer.js +37 -0
- package/dashboard/js/data-loader.js +147 -0
- package/dashboard/js/event-stream-renderer.js +158 -0
- package/dashboard/js/filter-controller.js +102 -0
- package/dashboard/js/journal-timeline.js +29 -0
- package/dashboard/js/roadmap-renderer.js +72 -0
- package/dashboard/js/timeline-renderer.js +283 -0
- package/dashboard/js/waterfall-renderer.js +189 -0
- package/harness/install-hooks.sh +34 -0
- package/harness/post-commit +17 -0
- package/harness/post-merge +17 -0
- package/harness/summary-hook.md +18 -0
- package/package.json +38 -0
- package/server.js +60 -0
- package/skills/build-json.md +36 -0
- package/skills/scan-docs.md +38 -0
- package/skills/summarize.md +66 -0
- package/src/builder/build.js +1019 -0
- package/src/cli/sandtable.js +970 -0
- package/src/scanner/scan.js +415 -0
- package/templates/.sandtable.template.json +51 -0
- package/templates/journal-entry.md +22 -0
- package/templates/summary-block.md +42 -0
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>沙盘 v0.3 · Sandtable</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f0f2f5; --card: #fff; --border: #e2e6ea;
|
|
10
|
+
--text: #1a1d23; --muted: #8b919b; --light: #5a6072;
|
|
11
|
+
--header-bg: #111827; --brief-bg: #16213e;
|
|
12
|
+
--past-bg: #e9edf2; --past-border: #c5cdd8;
|
|
13
|
+
--current-bg: #fff9e6; --current-border: #f0c000;
|
|
14
|
+
--future-bg: #f4f4f5; --future-border: #dcdce0;
|
|
15
|
+
--accent: #00b894; --accent2: #0984e3;
|
|
16
|
+
--completed: #00b894; --in_progress: #f0a000; --pending: #a0a7b4; --blocked: #e17055;
|
|
17
|
+
--secondary: #6c5ce7; --link: #0984e3;
|
|
18
|
+
--priority-must: #e17055; --priority-should: #f0a000; --priority-can: #a0a7b4;
|
|
19
|
+
--shadow-sm: 0 1px 3px rgba(0,0,0,.06); --shadow-md: 0 4px 12px rgba(0,0,0,.08);
|
|
20
|
+
}
|
|
21
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
22
|
+
body{font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans SC',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
23
|
+
|
|
24
|
+
/* Header */
|
|
25
|
+
#header{background:linear-gradient(135deg,#0f1419,#1a2332);color:#e8eaed;padding:10px 24px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 1px 4px rgba(0,0,0,.2);z-index:10}
|
|
26
|
+
#header h1{font-size:15px;font-weight:700;letter-spacing:-.3px;display:flex;align-items:center;gap:8px}
|
|
27
|
+
#header h1::before{content:'◆';color:var(--accent);font-size:10px}
|
|
28
|
+
.badge{background:rgba(255,255,255,.1);padding:2px 10px;border-radius:10px;font-size:10px;font-weight:500;letter-spacing:.3px}
|
|
29
|
+
#header .right{font-size:11px;display:flex;align-items:center;gap:12px;color:#9aa0ab;position:relative}
|
|
30
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--accent);display:inline-block;box-shadow:0 0 6px rgba(0,184,148,.4);animation:pulse 2s infinite}
|
|
31
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
|
32
|
+
|
|
33
|
+
/* Brief */
|
|
34
|
+
#brief{background:linear-gradient(135deg,#16213e,#0f3460);color:#b0bec5;padding:10px 24px;font-size:12px;line-height:1.6;border-bottom:2px solid rgba(0,184,148,.2)}
|
|
35
|
+
#brief strong{color:var(--accent);font-size:11px;margin-right:10px;text-transform:uppercase;letter-spacing:.5px}
|
|
36
|
+
#brief a{color:#64b5f6;text-decoration:none;cursor:pointer;transition:color .15s}
|
|
37
|
+
#brief a:hover{color:#90caf9;text-decoration:underline}
|
|
38
|
+
|
|
39
|
+
/* Tree tab bar — inside left panel */
|
|
40
|
+
#tree-tabs{display:flex;gap:4px;margin-bottom:10px;flex-wrap:wrap;align-items:center}
|
|
41
|
+
#tree-tabs button{padding:4px 10px;border:1px solid #d5dce6;background:#fff;border-radius:7px;font-size:10.5px;cursor:pointer;color:var(--muted);transition:all .15s;white-space:nowrap;font-weight:500}
|
|
42
|
+
#tree-tabs button:hover{border-color:var(--accent);color:var(--accent);background:#f0fdf9}
|
|
43
|
+
#tree-tabs button.on{background:var(--accent);color:#fff;border-color:var(--accent);font-weight:600}
|
|
44
|
+
|
|
45
|
+
/* Main dual layout */
|
|
46
|
+
#main{display:flex;flex:1;min-height:0}
|
|
47
|
+
#left{width:40%;border-right:1px solid var(--border);overflow-y:auto;padding:20px 24px;background:#fafbfc;transition:background .5s ease}
|
|
48
|
+
#right{width:60%;overflow-y:auto;padding:20px 24px;background:#f8f9fb;transition:background .5s ease}
|
|
49
|
+
#left::-webkit-scrollbar,#right::-webkit-scrollbar{width:5px}
|
|
50
|
+
#left::-webkit-scrollbar-thumb,#right::-webkit-scrollbar-thumb{background:#d0d5dd;border-radius:3px}
|
|
51
|
+
|
|
52
|
+
/* Filter-connected: unified panel tint when doc filter is active */
|
|
53
|
+
body.filter-active #left, body.filter-active #right{background:#eef2f7}
|
|
54
|
+
body.filter-active #left{border-right-color:#d5dce6}
|
|
55
|
+
.tree-card.filter-match{border-color:#4a90d9!important;box-shadow:0 0 0 3px rgba(74,144,217,.2)!important;background:#fff!important}
|
|
56
|
+
.tree-card.filter-match .tc-head{background:rgba(74,144,217,.06)}
|
|
57
|
+
.rp-card.filter-match{border-color:#4a90d9!important;box-shadow:0 0 0 3px rgba(74,144,217,.2)!important}
|
|
58
|
+
|
|
59
|
+
.panel-title{font-size:13px;font-weight:700;margin-bottom:14px;display:flex;justify-content:space-between;align-items:center;color:var(--text);letter-spacing:-.2px}
|
|
60
|
+
.panel-title .count{font-size:11px;color:var(--muted);font-weight:500}
|
|
61
|
+
|
|
62
|
+
/* Category section */
|
|
63
|
+
.cat-section{margin-bottom:16px}
|
|
64
|
+
.cat-header{font-size:11px;font-weight:600;padding:5px 10px;border-radius:5px;margin-bottom:8px;display:flex;justify-content:space-between;letter-spacing:.2px}
|
|
65
|
+
.cat-header.primary{background:linear-gradient(135deg,#e6f9f1,#d4f5e8);color:#00855a}
|
|
66
|
+
.cat-header.secondary{background:#f1f2f5;color:var(--muted)}
|
|
67
|
+
|
|
68
|
+
/* Element card */
|
|
69
|
+
.card{border:1px solid var(--border);border-radius:8px;margin-bottom:10px;overflow:hidden;box-shadow:var(--shadow-sm);transition:box-shadow .2s,transform .15s;cursor:default}
|
|
70
|
+
.card:hover{box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
71
|
+
.card.active-filter{border-color:var(--link);box-shadow:0 0 0 2px rgba(9,132,227,.25)}
|
|
72
|
+
.card[data-tg="past"]{background:var(--past-bg);border-color:var(--past-border)}
|
|
73
|
+
.card[data-tg="current"]{background:var(--current-bg);border-color:var(--current-border);box-shadow:0 2px 8px rgba(240,192,0,.12)}
|
|
74
|
+
.card[data-tg="future"]{background:var(--future-bg);border-color:var(--future-border)}
|
|
75
|
+
.card.secondary{border-left:3px solid var(--secondary)}
|
|
76
|
+
.card-head{padding:8px 14px;font-size:12.5px;font-weight:600;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
77
|
+
.card-head.completed{background:linear-gradient(135deg,#e6f9f1,#d4f5e8);color:#00855a}
|
|
78
|
+
.card-head.in_progress{background:linear-gradient(135deg,#fff8e6,#fff3d4);color:#b06d00}
|
|
79
|
+
.card-head.pending{background:#f8f9fb;color:var(--muted)}
|
|
80
|
+
.card-head.blocked{background:linear-gradient(135deg,#fff0ed,#ffe8e3);color:#c04030}
|
|
81
|
+
.card-head > span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
82
|
+
.card-meta{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:400;flex-shrink:0}
|
|
83
|
+
.card-type{background:rgba(0,0,0,.06);padding:2px 6px;border-radius:4px;font-size:9px;color:var(--muted);font-weight:500}
|
|
84
|
+
.card-src{font-size:9px;background:#e8f1fd;padding:2px 7px;border-radius:4px;color:var(--link);cursor:pointer;text-decoration:none;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;transition:all .15s}
|
|
85
|
+
.card-src:hover{background:var(--link);color:#fff}
|
|
86
|
+
.card-summary{padding:6px 14px;font-size:11.5px;color:var(--light);line-height:1.5}
|
|
87
|
+
.card-tags{padding:3px 14px 8px}
|
|
88
|
+
.tag{display:inline-block;background:rgba(0,0,0,.05);padding:2px 7px;border-radius:4px;font-size:9.5px;color:var(--light);margin-right:4px;margin-bottom:2px;font-weight:500}
|
|
89
|
+
.card-children{border-top:1px solid rgba(0,0,0,.06);padding:4px 12px 8px}
|
|
90
|
+
.sub{font-size:11.5px;padding:4px 0;display:flex;align-items:center;gap:6px;color:var(--light);transition:background .15s;border-radius:4px;cursor:default}
|
|
91
|
+
.sub:hover{background:rgba(0,0,0,.03)}
|
|
92
|
+
|
|
93
|
+
/* Time badge */
|
|
94
|
+
.tbadge{font-size:9px;padding:1px 7px;border-radius:4px;font-weight:600;letter-spacing:.2px}
|
|
95
|
+
.tb-past{background:rgba(96,125,139,.12);color:#546e7a}
|
|
96
|
+
.tb-current{background:rgba(240,160,0,.12);color:#b06d00}
|
|
97
|
+
.tb-future{background:rgba(0,0,0,.06);color:var(--muted)}
|
|
98
|
+
|
|
99
|
+
/* Event stream */
|
|
100
|
+
.ev-item{padding:10px 12px;border-left:4px solid var(--border);margin-bottom:6px;border-radius:0 6px 6px 0;background:#fff;font-size:11.5px;box-shadow:var(--shadow-sm);transition:box-shadow .2s,transform .15s}
|
|
101
|
+
.ev-item:hover{box-shadow:var(--shadow-md)}
|
|
102
|
+
.ev-item.must{border-left-width:6px;background:#fffef7;box-shadow:0 2px 8px rgba(225,112,85,.1)}
|
|
103
|
+
.ev-item.t1{border-left-color:#e17055} .ev-item.t2{border-left-color:#6c5ce7}
|
|
104
|
+
.ev-item.t3{border-left-color:#0984e3} .ev-item.t4{border-left-color:#00b894}
|
|
105
|
+
.ev-item.t5{border-left-color:#f0a000} .ev-item.t6{border-left-color:#795548}
|
|
106
|
+
.ev-item.t7{border-left-color:#e84393}
|
|
107
|
+
.ev-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
|
|
108
|
+
.ev-title{font-weight:600;flex:1;line-height:1.4;font-size:12px}
|
|
109
|
+
.ev-type{font-size:9.5px;padding:2px 7px;border-radius:4px;color:#fff;font-weight:600;white-space:nowrap;letter-spacing:.3px}
|
|
110
|
+
.ev-t1{background:#e17055} .ev-t2{background:#6c5ce7} .ev-t3{background:#0984e3}
|
|
111
|
+
.ev-t4{background:#00b894} .ev-t5{background:#f0a000} .ev-t6{background:#795548} .ev-t7{background:#e84393}
|
|
112
|
+
.ev-meta{font-size:10px;color:var(--muted);margin-top:4px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
|
113
|
+
.ev-time{font-family:'SF Mono',Consolas,monospace;font-size:11px;color:var(--text);font-weight:600;background:rgba(0,0,0,.04);padding:1px 6px;border-radius:3px}
|
|
114
|
+
.ev-ref{color:var(--link);cursor:pointer;text-decoration:none;font-weight:500}
|
|
115
|
+
.ev-ref:hover{text-decoration:underline;color:#0066cc}
|
|
116
|
+
.ev-actor{font-style:italic;color:var(--light)}
|
|
117
|
+
.ev-tag{background:rgba(0,0,0,.05);padding:1px 5px;border-radius:3px;font-size:9px;color:var(--light)}
|
|
118
|
+
.ev-pdot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:3px;flex-shrink:0}
|
|
119
|
+
.ev-p1{background:var(--priority-must);box-shadow:0 0 4px rgba(225,112,85,.4)}
|
|
120
|
+
.ev-p2{background:var(--priority-should)} .ev-p3{background:var(--priority-can)}
|
|
121
|
+
.ev-day{font-size:11px;font-weight:700;padding:12px 0 6px;border-bottom:1.5px solid var(--border);margin-bottom:6px;color:var(--light);letter-spacing:.3px}
|
|
122
|
+
.ev-day:first-child{padding-top:0}
|
|
123
|
+
.ev-item.hl{animation:flash .8s ease-in-out 3;background:#fff9c4!important}
|
|
124
|
+
@keyframes flash{0%,100%{background:#fff9c4}50%{background:#ffe082}}
|
|
125
|
+
|
|
126
|
+
/* Footer — removed conventions bar */
|
|
127
|
+
|
|
128
|
+
/* Floating panel */
|
|
129
|
+
#float{position:fixed;top:0;left:0;width:480px;max-width:94vw;height:100vh;background:#fff;box-shadow:8px 0 30px rgba(0,0,0,.18);z-index:1000;display:flex;flex-direction:column;transition:transform .3s cubic-bezier(.4,0,.2,1)}
|
|
130
|
+
#float.hide{transform:translateX(-110vw)}
|
|
131
|
+
#float.fr{left:auto;right:0;box-shadow:-8px 0 30px rgba(0,0,0,.18)}
|
|
132
|
+
#float.fr.hide{transform:translateX(110vw)}
|
|
133
|
+
#float-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--header-bg);color:#fff;font-size:13px;font-weight:600}
|
|
134
|
+
#float-body{flex:1;overflow-y:auto;padding:16px 20px;font-size:12.5px;line-height:1.8;white-space:pre-wrap;word-break:break-word;font-family:'SF Mono',Consolas,monospace;color:var(--text)}
|
|
135
|
+
#float-body::-webkit-scrollbar{width:5px}
|
|
136
|
+
#float-body::-webkit-scrollbar-thumb{background:#d0d5dd;border-radius:3px}
|
|
137
|
+
#float-close{background:none;border:none;color:#fff;font-size:22px;cursor:pointer;opacity:.8;transition:opacity .15s;padding:0 4px}
|
|
138
|
+
#float-close:hover{opacity:1}
|
|
139
|
+
#overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,.25);z-index:999;backdrop-filter:blur(2px)}
|
|
140
|
+
#overlay.hide{display:none}
|
|
141
|
+
|
|
142
|
+
/* Milestone tooltip */
|
|
143
|
+
.mtip{display:none;position:absolute;background:#fff;border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:10.5px;box-shadow:var(--shadow-md);z-index:50;max-width:260px;line-height:1.5;left:100%;top:0;margin-left:10px}
|
|
144
|
+
.sub:hover .mtip{display:block}
|
|
145
|
+
|
|
146
|
+
.empty{text-align:center;padding:50px 20px;color:var(--muted)}
|
|
147
|
+
.empty h3{font-size:15px;margin-bottom:8px;font-weight:600}
|
|
148
|
+
.empty p{font-size:12px;color:var(--muted)}
|
|
149
|
+
|
|
150
|
+
/* Filter bar */
|
|
151
|
+
#filter-bar{display:none;padding:8px 12px;background:#e8f1fd;border:1px solid #b8d4f7;border-radius:8px;margin-bottom:12px;font-size:11px;align-items:center;gap:8px}
|
|
152
|
+
#filter-bar.show{display:flex}
|
|
153
|
+
#filter-bar .ftext{flex:1;color:var(--link);font-weight:500}
|
|
154
|
+
#filter-bar .fclear{cursor:pointer;background:var(--link);color:#fff;border:none;padding:3px 10px;border-radius:4px;font-size:10px;font-weight:600;transition:opacity .15s}
|
|
155
|
+
#filter-bar .fclear:hover{opacity:.85}
|
|
156
|
+
|
|
157
|
+
/* Thread card */
|
|
158
|
+
.th-card{border:1.5px solid #d5dce6;border-radius:10px;margin-bottom:10px;overflow:hidden;background:#fff;box-shadow:var(--shadow-sm);transition:box-shadow .2s}
|
|
159
|
+
.th-card:hover{box-shadow:var(--shadow-md)}.th-card.th-active{border-color:#4a90d9;box-shadow:0 0 0 2px rgba(74,144,217,.3)}
|
|
160
|
+
.th-head{padding:10px 14px;background:#f4f6f9;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;user-select:none;transition:background .15s}
|
|
161
|
+
.th-head:hover{background:#edf0f4}
|
|
162
|
+
.th-head .th-title{font-weight:600;font-size:12px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
163
|
+
.th-head .th-count{font-size:10px;color:var(--muted);white-space:nowrap;background:rgba(0,0,0,.06);padding:2px 8px;border-radius:10px}
|
|
164
|
+
.th-head .th-arrow{font-size:10px;color:var(--muted);transition:transform .2s}
|
|
165
|
+
.th-card.open .th-arrow{transform:rotate(90deg)}
|
|
166
|
+
.th-body{display:none;border-top:1px solid var(--border)}
|
|
167
|
+
.th-card.open .th-body{display:block}
|
|
168
|
+
.th-body .ev-item{margin:0;border-radius:0;border-left-width:3px;box-shadow:none}
|
|
169
|
+
.th-body .ev-item:last-child{border-radius:0 0 8px 0}
|
|
170
|
+
|
|
171
|
+
/* Tag filter bar */
|
|
172
|
+
#tag-bar{padding:8px 0;margin-bottom:10px;border-bottom:1px solid var(--border)}
|
|
173
|
+
.tag-row{display:flex;flex-wrap:wrap;align-items:center;gap:5px;padding:2px 0}
|
|
174
|
+
.tag-row .tag-cat{font-size:9px;color:var(--muted);font-weight:600;white-space:nowrap;padding:2px 6px 2px 0;min-width:32px;text-align:right}
|
|
175
|
+
.tag-row .tag-btn{font-size:10px;padding:2px 8px;border-radius:10px;border:1px solid #e0e3e8;background:#fff;cursor:pointer;color:var(--light);transition:all .15s;white-space:nowrap;font-weight:500}
|
|
176
|
+
.tag-row .tag-btn em{font-style:normal;font-size:8px;color:var(--muted);margin-left:2px}
|
|
177
|
+
.tag-row .tag-btn:hover{border-color:var(--accent);color:var(--accent);background:#f0fdf9}
|
|
178
|
+
.tag-row .tag-btn.on{background:var(--accent);color:#fff;border-color:var(--accent);font-weight:600}
|
|
179
|
+
.tag-row .tag-btn.on em{color:rgba(255,255,255,.7)}
|
|
180
|
+
.tag-row .tag-more{font-size:9px;color:var(--link);padding:2px 4px;cursor:pointer;text-decoration:underline}
|
|
181
|
+
.tag-row .tag-more:hover{color:#0066cc}
|
|
182
|
+
|
|
183
|
+
/* Semi-expanded thread card */
|
|
184
|
+
.th-card.semi .th-body{display:block}
|
|
185
|
+
/* semi mode: JS renders only latest event + view-more button, no CSS hiding needed */
|
|
186
|
+
.th-more{cursor:pointer;padding:8px 12px;text-align:center;font-size:11px;color:var(--link);font-weight:500;background:#f8f9fb;border-top:1px dashed var(--border);transition:background .15s}
|
|
187
|
+
.th-more:hover{background:#edf0f4;color:#0066cc}
|
|
188
|
+
.th-collapse{cursor:pointer;padding:8px 12px;text-align:center;font-size:11px;color:var(--muted);font-weight:500;background:#f8f9fb;border-top:1px solid var(--border);transition:all .15s}
|
|
189
|
+
.th-collapse:hover{background:#edf0f4;color:var(--light)}
|
|
190
|
+
.th-typebar{font-size:10px;color:var(--muted);padding:4px 12px;background:#f8f9fb;border-bottom:1px solid var(--border)}
|
|
191
|
+
|
|
192
|
+
/* Sort + filter toolbar */
|
|
193
|
+
#toolbar{display:flex;align-items:center;gap:8px;padding:6px 0;margin-bottom:6px;flex-wrap:wrap}
|
|
194
|
+
#toolbar .sort-group{display:flex;gap:0;border:1px solid #d5dce6;border-radius:6px;overflow:hidden}
|
|
195
|
+
#toolbar .sort-btn{padding:3px 10px;font-size:10px;border:none;background:#fff;color:var(--muted);cursor:pointer;font-weight:500;transition:all .15s}
|
|
196
|
+
#toolbar .sort-btn.on{background:var(--accent);color:#fff}
|
|
197
|
+
#toolbar .sort-btn+.sort-btn{border-left:1px solid #d5dce6}
|
|
198
|
+
#toolbar .sort-label{font-size:9px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.3px}
|
|
199
|
+
#toolbar .fclear{display:none;cursor:pointer;background:var(--link);color:#fff;border:none;padding:3px 10px;border-radius:4px;font-size:10px;font-weight:600}
|
|
200
|
+
#toolbar .fclear.show{display:inline-block}
|
|
201
|
+
#toolbar .fclear:hover{opacity:.85}
|
|
202
|
+
#toolbar .ftext{font-size:10px;color:var(--link);font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
203
|
+
|
|
204
|
+
/* Event tag clickable */
|
|
205
|
+
.ev-tag{cursor:pointer;transition:all .15s}
|
|
206
|
+
.ev-tag:hover{background:var(--accent);color:#fff}
|
|
207
|
+
|
|
208
|
+
/* Roadmap version card */
|
|
209
|
+
.rp-card{border:1.5px solid #d5dce6;border-radius:10px;margin-bottom:10px;overflow:hidden;background:#fff;box-shadow:var(--shadow-sm);transition:box-shadow .2s,border-color .3s}
|
|
210
|
+
.rp-card:hover{box-shadow:var(--shadow-md)}
|
|
211
|
+
.rp-card.rp-done{background:#f0fdf6;border-color:#b7e4cf}
|
|
212
|
+
.rp-card.rp-mixed{background:#fffef7;border-color:#f0d000}
|
|
213
|
+
.rp-head{padding:10px 14px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;user-select:none;transition:background .15s;border-bottom:1px solid transparent}
|
|
214
|
+
.rp-card.open .rp-head{border-bottom-color:var(--border)}
|
|
215
|
+
.rp-head:hover{background:rgba(0,0,0,.02)}
|
|
216
|
+
.rp-head .rp-title{font-weight:600;font-size:12.5px;flex:1;min-width:0}
|
|
217
|
+
.rp-head .rp-meta{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--muted)}
|
|
218
|
+
.rp-head .rp-ratio{font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px}
|
|
219
|
+
.rp-head .rp-ratio.all-done{background:#d4f5e8;color:#00855a}
|
|
220
|
+
.rp-head .rp-ratio.partial{background:#fff3d4;color:#b06d00}
|
|
221
|
+
.rp-head .rp-arrow{font-size:10px;color:var(--muted);transition:transform .2s}
|
|
222
|
+
.rp-card.open .rp-arrow{transform:rotate(90deg)}
|
|
223
|
+
.rp-body{display:none}
|
|
224
|
+
.rp-card.open .rp-body{display:block}
|
|
225
|
+
.rp-card.semi .rp-body{display:block}
|
|
226
|
+
.rp-child{padding:7px 14px;border-bottom:1px dotted #eef0f4;font-size:11px;display:flex;align-items:center;gap:8px;transition:background .1s}
|
|
227
|
+
.rp-child:hover{background:rgba(0,0,0,.015)}
|
|
228
|
+
.rp-child:last-child{border-bottom:none}
|
|
229
|
+
.rp-child .rp-cicon{font-size:11px;flex-shrink:0}
|
|
230
|
+
.rp-child .rp-cname{flex:1;font-weight:500;color:var(--text)}
|
|
231
|
+
.rp-child .rp-ctime{font-size:9.5px;color:var(--muted);font-family:'SF Mono',Consolas,monospace;white-space:nowrap}
|
|
232
|
+
.rp-child .rp-clabel{font-size:9px;padding:1px 6px;border-radius:3px;font-weight:500;white-space:nowrap}
|
|
233
|
+
.rp-child .rp-clabel.done{background:#e6f9f1;color:#00855a}
|
|
234
|
+
.rp-child .rp-clabel.progress{background:#fff8e6;color:#b06d00}
|
|
235
|
+
.rp-child .rp-clabel.pending-label{background:#f1f2f5;color:var(--muted)}
|
|
236
|
+
.rp-more{cursor:pointer;padding:8px 12px;text-align:center;font-size:11px;color:var(--link);font-weight:500;border-top:1px dashed var(--border);transition:background .15s}
|
|
237
|
+
.rp-more:hover{background:#edf0f4;color:#0066cc}
|
|
238
|
+
|
|
239
|
+
/* Enhanced tree card */
|
|
240
|
+
.tree-card{border:1px solid var(--border);border-radius:8px;margin-bottom:8px;overflow:hidden;background:#fff;box-shadow:var(--shadow-sm);transition:box-shadow .2s,transform .15s,border-color .3s;cursor:pointer}
|
|
241
|
+
.tree-card:hover{box-shadow:var(--shadow-md);transform:translateY(-1px)}
|
|
242
|
+
.tree-card.secondary{border-left:3px solid var(--secondary)}
|
|
243
|
+
.tree-card[data-tg="past"]{background:var(--past-bg);border-color:var(--past-border)}
|
|
244
|
+
.tree-card[data-tg="current"]{background:var(--current-bg);border-color:var(--current-border)}
|
|
245
|
+
.tree-card[data-tg="future"]{background:var(--future-bg);border-color:var(--future-border)}
|
|
246
|
+
.tree-card .tc-head{padding:10px 14px;font-size:12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
247
|
+
.tree-card .tc-head > span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
248
|
+
.tree-card .tc-meta{display:flex;align-items:center;gap:6px;font-size:9.5px;flex-shrink:0;color:var(--muted)}
|
|
249
|
+
.tree-card .tc-time{font-family:'SF Mono',Consolas,monospace;font-size:9.5px;color:var(--muted)}
|
|
250
|
+
.tc-head .tc-time{font-size:10px}
|
|
251
|
+
.tree-card .tc-summary{padding:4px 12px 8px;font-size:11px;color:var(--light);line-height:1.5}
|
|
252
|
+
.tree-card .tc-tags{padding:2px 12px 6px}
|
|
253
|
+
.tree-card .tc-src{padding:2px 12px 6px}
|
|
254
|
+
.tree-card .tc-src a{font-size:9.5px;background:#e8f1fd;padding:2px 7px;border-radius:4px;color:var(--link);cursor:pointer;text-decoration:none;transition:all .15s}
|
|
255
|
+
.tree-card .tc-src a:hover{background:var(--link);color:#fff}
|
|
256
|
+
.tree-card .tc-type{font-size:9px;padding:1px 6px;border-radius:4px;font-weight:500}
|
|
257
|
+
.tree-card .tc-type.status-done{background:#e6f9f1;color:#00855a}
|
|
258
|
+
.tree-card .tc-type.status-progress{background:#fff8e6;color:#b06d00}
|
|
259
|
+
.tree-card .tc-type.status-pending{background:#f1f2f5;color:var(--muted)}
|
|
260
|
+
|
|
261
|
+
/* Token display in header */
|
|
262
|
+
#token-today{font-size:10px;color:#9aa0ab;cursor:default;position:relative;white-space:nowrap}
|
|
263
|
+
#token-today:hover{color:#b8c0c8}
|
|
264
|
+
#token-tip{display:none;position:absolute;top:100%;right:0;margin-top:8px;background:#1a2332;color:#e8eaed;padding:10px 14px;border-radius:8px;font-size:11px;line-height:1.6;z-index:100;box-shadow:0 4px 20px rgba(0,0,0,.35);min-width:200px;white-space:nowrap}
|
|
265
|
+
#token-tip.show{display:block}
|
|
266
|
+
#token-tip .tip-row{display:flex;justify-content:space-between;gap:16px;padding:2px 0}
|
|
267
|
+
#token-tip .tip-row .tip-name{color:#b0bec5}
|
|
268
|
+
#token-tip .tip-row .tip-val{color:#fff;font-weight:600;font-family:'SF Mono',Consolas,monospace}
|
|
269
|
+
#token-tip .tip-divider{border-top:1px solid rgba(255,255,255,.1);margin:4px 0}
|
|
270
|
+
|
|
271
|
+
</style>
|
|
272
|
+
</head>
|
|
273
|
+
<body>
|
|
274
|
+
|
|
275
|
+
<header id="header">
|
|
276
|
+
<div><h1>沙盘 · Sandtable</h1><span class="badge">v0.3</span></div>
|
|
277
|
+
<div class="right"><span id="token-today">工具token:--<span id="token-tip"></span></span><span id="upd">加载中...</span><span class="dot"></span></div>
|
|
278
|
+
</header>
|
|
279
|
+
|
|
280
|
+
<div id="brief"><strong>简报</strong><span id="btext">加载中...</span></div>
|
|
281
|
+
|
|
282
|
+
<div id="main">
|
|
283
|
+
<section id="left">
|
|
284
|
+
<div class="panel-title">节点树 <span class="count" id="cnt">0 项</span></div>
|
|
285
|
+
<div id="tree-tabs">
|
|
286
|
+
<button class="on" data-c="all">全部</button>
|
|
287
|
+
<button data-c="roadmap">路线图</button>
|
|
288
|
+
<button data-c="decision">决策</button>
|
|
289
|
+
<button data-c="spec">规格</button>
|
|
290
|
+
<button data-c="convention">纪律</button>
|
|
291
|
+
<button data-c="ops">运维</button>
|
|
292
|
+
<button data-c="archive">档案</button>
|
|
293
|
+
</div>
|
|
294
|
+
<div id="tree"></div>
|
|
295
|
+
</section>
|
|
296
|
+
<section id="right">
|
|
297
|
+
<div class="panel-title">事件流 <span class="count" id="evcnt"></span></div>
|
|
298
|
+
<div id="toolbar"><span class="sort-label">排列</span><span class="sort-group"><button class="sort-btn on" data-mode="grouped">按事件分类</button><button class="sort-btn" data-mode="flat">按触发时间</button></span><span class="ftext"></span><button class="fclear">✕ 清除</button></div>
|
|
299
|
+
<div id="tag-bar"></div>
|
|
300
|
+
<div id="events"></div>
|
|
301
|
+
</section>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div id="overlay" class="hide"></div>
|
|
305
|
+
<div id="float" class="hide">
|
|
306
|
+
<div id="float-head"><span id="ftitle">预览</span><button id="float-close">×</button></div>
|
|
307
|
+
<div id="float-body">加载中...</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<script>
|
|
311
|
+
// ====== DATA ======
|
|
312
|
+
var T = null; // timeline data
|
|
313
|
+
var activeCat = 'all';
|
|
314
|
+
var activeDocFilter = null; // current doc filter (null = show all)
|
|
315
|
+
var activeTagFilter = null; // current tag filter (null = show all)
|
|
316
|
+
var expandedThreads = {}; // threads user has manually expanded
|
|
317
|
+
var expandedRpCards = {}; // roadmap version cards user has manually expanded
|
|
318
|
+
var sortMode = 'grouped'; // 'grouped' | 'flat'
|
|
319
|
+
|
|
320
|
+
var TAG_CATS = {
|
|
321
|
+
"模块":["CLI","dashboard","builder","server","skill","npm","扫描器"],
|
|
322
|
+
"主题":["事件流","token","UX","安装方案","安全","编码","配置","IDE适配","双视图","设计原则","libero"],
|
|
323
|
+
"阶段":["v0.3","MVP","里程碑","集成","方向决策"],
|
|
324
|
+
"动作":["架构","设计","实现","调研","文档","流程","教训","命令","IDE","init","uninstall","summarize","scan"]
|
|
325
|
+
};
|
|
326
|
+
function catOfTag(tag){for(var c in TAG_CATS){if(TAG_CATS[c].indexOf(tag)!==-1)return c}return "其他"}
|
|
327
|
+
function evHasTag(ev,tag){return ev.tags&&ev.tags.indexOf(tag)!==-1}
|
|
328
|
+
function setTagFilter(tag){if(activeTagFilter===tag){activeTagFilter=null}else{activeTagFilter=tag};renderTagBar();renderEvents()}
|
|
329
|
+
function renderTagBar(){
|
|
330
|
+
var bar=document.getElementById("tag-bar");
|
|
331
|
+
if(!T||!T.events){bar.innerHTML="";return}
|
|
332
|
+
var allEvs=T.events||[],tagCount={};
|
|
333
|
+
for(var i=0;i<allEvs.length;i++){var tags=allEvs[i].tags||[];for(var j=0;j<tags.length;j++)tagCount[tags[j]]=(tagCount[tags[j]]||0)+1}
|
|
334
|
+
var catTags={};
|
|
335
|
+
for(var tag in tagCount){var c=catOfTag(tag);if(!catTags[c])catTags[c]=[];catTags[c].push({tag:tag,count:tagCount[tag]})}
|
|
336
|
+
var catOrder=["模块","主题","阶段","动作","其他"];
|
|
337
|
+
var h="";
|
|
338
|
+
for(var ci=0;ci<catOrder.length;ci++){
|
|
339
|
+
var cn=catOrder[ci],items=catTags[cn];
|
|
340
|
+
if(!items||!items.length)continue;
|
|
341
|
+
items.sort(function(a,b){return b.count-a.count});
|
|
342
|
+
var expand=!!expandedTagCats[cn];
|
|
343
|
+
var limit=expand?items.length:Math.min(3,items.length);
|
|
344
|
+
h+="<div class=\"tag-row\"><span class=\"tag-cat\">"+cn+"</span>";
|
|
345
|
+
for(var ii=0;ii<limit;ii++){
|
|
346
|
+
var item=items[ii],isOn=activeTagFilter===item.tag;
|
|
347
|
+
h+="<span class=\"tag-btn"+(isOn?" on":"")+"\" data-tag=\""+esc(item.tag)+"\" onclick=\"setTagFilter('"+esc(item.tag)+"');event.stopPropagation()\">"+esc(item.tag)+" <em>"+item.count+"</em></span>";
|
|
348
|
+
}
|
|
349
|
+
if(items.length>3&&!expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">+"+(items.length-3)+"</span>";
|
|
350
|
+
if(expand)h+="<span class=\"tag-more\" onclick=\"toggleTagCat('"+esc(cn)+"');event.stopPropagation()\">收起</span>";
|
|
351
|
+
h+="</div>";
|
|
352
|
+
}
|
|
353
|
+
bar.innerHTML=h;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var expandedTagCats = {};
|
|
357
|
+
function toggleTagCat(cn){
|
|
358
|
+
if(expandedTagCats[cn]){expandedTagCats[cn]=false}else{expandedTagCats[cn]=true}
|
|
359
|
+
renderTagBar();
|
|
360
|
+
}
|
|
361
|
+
var CLABEL = {roadmap:'路线图与进度',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
|
|
362
|
+
var PRIMARY = {roadmap:1,decision:1};
|
|
363
|
+
var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'roadmap',conclusion:'roadmap',decision:'decision',refactor:'decision',spec:'spec',intent:'spec',prompt:'spec',convention:'convention',agent:'ops',runbook:'ops',journal:'archive',handover:'archive',plan_doc:'archive',template:'template'};
|
|
364
|
+
function normCat(c){if(!c)return'unknown';var m={conventions:'convention',specs:'spec',decisions:'decision',plans:'roadmap',journals:'archive',journal:'archive'};return m[c]||c}
|
|
365
|
+
function normTg(tg){if(!tg||tg==='archived')return'past';return tg}
|
|
366
|
+
|
|
367
|
+
var SICON = {completed:'✅',in_progress:'⏳',pending:'⬜',blocked:'🚫',cancelled:'❌'};
|
|
368
|
+
function sicon(s){return SICON[s]||'⬜'}
|
|
369
|
+
function esc(s){if(!s)return'';var d=document.createElement('div');d.textContent=String(s);return d.innerHTML}
|
|
370
|
+
|
|
371
|
+
function generateBrief(){
|
|
372
|
+
var els=T.elements||[];
|
|
373
|
+
var allMilestones=[];
|
|
374
|
+
for(var i=0;i<els.length;i++){
|
|
375
|
+
var children=els[i].children||[];
|
|
376
|
+
for(var j=0;j<children.length;j++){
|
|
377
|
+
if(children[j].elementType==="milestone")allMilestones.push(children[j]);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
var verStats={};
|
|
381
|
+
for(var i=0;i<allMilestones.length;i++){
|
|
382
|
+
var ch=allMilestones[i],ver=extractVersion(ch.name);
|
|
383
|
+
if(!verStats[ver])verStats[ver]={total:0,done:0,inProgress:[]};
|
|
384
|
+
verStats[ver].total++;
|
|
385
|
+
if(ch.status==="completed"||ch.status==="cancelled")verStats[ver].done++;
|
|
386
|
+
else if(ch.status==="in_progress")verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
|
|
387
|
+
else verStats[ver].inProgress.push(ch.name.replace(/\s*\([^)]*\)\s*/g,'').replace(/^M\d+:\s*/,''));
|
|
388
|
+
}
|
|
389
|
+
var versions=Object.keys(verStats).sort(function(a,b){return versionOrder(b)-versionOrder(a)});
|
|
390
|
+
// Describe each version's milestone completion
|
|
391
|
+
var parts=[];
|
|
392
|
+
for(var i=0;i<versions.length;i++){
|
|
393
|
+
var v=versions[i],vs=verStats[v];
|
|
394
|
+
if(vs.done===vs.total){
|
|
395
|
+
parts.push(v+' '+vs.done+'/'+vs.total+' 里程碑全部完成');
|
|
396
|
+
}else{
|
|
397
|
+
var names=vs.inProgress.slice(0,3).join('、');
|
|
398
|
+
parts.push(v+' '+vs.done+'/'+vs.total+' 完成'+(names?','+names+'等推进中':''));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return parts.join(';')+'。'||'暂无简报';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ====== LOAD ======
|
|
405
|
+
fetch('../data/timeline.json').then(function(r){return r.json()}).then(function(t){
|
|
406
|
+
T = t;
|
|
407
|
+
document.getElementById('upd').textContent = '更新:'+fmtTime(t.updated||'');
|
|
408
|
+
document.getElementById('btext').innerHTML = generateBrief();
|
|
409
|
+
renderTree();
|
|
410
|
+
renderEvents();
|
|
411
|
+
setupTabs();
|
|
412
|
+
setupToolbar();
|
|
413
|
+
loadTokenToday();
|
|
414
|
+
}).catch(function(e){
|
|
415
|
+
document.getElementById('btext').textContent = '加载失败:'+e.message;
|
|
416
|
+
console.error(e);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ====== TABS ======
|
|
420
|
+
function setupTabs(){
|
|
421
|
+
var btns = document.querySelectorAll("#tree-tabs button[data-c]");
|
|
422
|
+
for(var i=0;i<btns.length;i++){
|
|
423
|
+
btns[i].addEventListener("click",function(){
|
|
424
|
+
for(var j=0;j<btns.length;j++)btns[j].classList.remove("on");
|
|
425
|
+
this.classList.add("on");
|
|
426
|
+
activeCat = this.getAttribute("data-c");
|
|
427
|
+
renderTree();
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ====== TOOLBAR ======
|
|
433
|
+
function setupToolbar(){
|
|
434
|
+
var sortBtns=document.querySelectorAll("#toolbar .sort-btn");
|
|
435
|
+
for(var i=0;i<sortBtns.length;i++){
|
|
436
|
+
sortBtns[i].addEventListener("click",function(){
|
|
437
|
+
for(var j=0;j<sortBtns.length;j++)sortBtns[j].classList.remove("on");
|
|
438
|
+
this.classList.add("on");
|
|
439
|
+
sortMode=this.getAttribute("data-mode");
|
|
440
|
+
renderEvents();
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
var fclear=document.querySelector("#toolbar .fclear");
|
|
444
|
+
if(fclear){
|
|
445
|
+
fclear.addEventListener("click",function(){
|
|
446
|
+
activeDocFilter=null;
|
|
447
|
+
activeTagFilter=null;
|
|
448
|
+
this.classList.remove("show");
|
|
449
|
+
document.querySelector("#toolbar .ftext").textContent="";
|
|
450
|
+
document.body.classList.remove("filter-active");
|
|
451
|
+
renderTree();
|
|
452
|
+
renderTagBar();
|
|
453
|
+
renderEvents();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ====== TREE ======
|
|
459
|
+
function catOf(el){return normCat(el.category || ETYPE[el.elementType] || "unknown")}
|
|
460
|
+
|
|
461
|
+
function hasChildCat(el,cat){
|
|
462
|
+
if(catOf(el)===cat)return true;
|
|
463
|
+
if(el.children)for(var i=0;i<el.children.length;i++){if(hasChildCat(el.children[i],cat))return true}
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function extractVersion(name){
|
|
468
|
+
var m=name.match(/\(v\d+\.\d+\)/);
|
|
469
|
+
if(m)return m[0].replace(/[()]/g,"");
|
|
470
|
+
m=name.match(/\(([A-D])\)/);
|
|
471
|
+
if(m)return "v0.3";
|
|
472
|
+
return "其他";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function versionOrder(v){
|
|
476
|
+
if(v==="v0.1")return 1;
|
|
477
|
+
if(v==="v0.2")return 2;
|
|
478
|
+
if(v==="v0.3")return 3;
|
|
479
|
+
return 99;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function fmtTime(ts){
|
|
483
|
+
if(!ts)return"";
|
|
484
|
+
var d=new Date(ts);
|
|
485
|
+
var mm=String(d.getMonth()+1).padStart(2,'0');
|
|
486
|
+
var dd=String(d.getDate()).padStart(2,'0');
|
|
487
|
+
var hh=String(d.getHours()).padStart(2,'0');
|
|
488
|
+
var mi=String(d.getMinutes()).padStart(2,'0');
|
|
489
|
+
return mm+'-'+dd+' '+hh+':'+mi;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getVersionTitle(ver,children,phaseName){
|
|
493
|
+
if(phaseName){
|
|
494
|
+
var parts=phaseName.split(/[·;;]/);
|
|
495
|
+
for(var i=0;i<parts.length;i++){
|
|
496
|
+
if(parts[i].indexOf(ver)!==-1)return parts[i].trim();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
var first=children[0];
|
|
500
|
+
if(first){
|
|
501
|
+
var nm=first.name.replace(/\s*\([^)]*\)\s*/g,"").replace(/^M\d+:\s*/,"");
|
|
502
|
+
return ver+" · "+nm;
|
|
503
|
+
}
|
|
504
|
+
return ver;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function getVersionUpdateTime(ver,children){
|
|
508
|
+
var latest="";
|
|
509
|
+
if(T&&T.events){
|
|
510
|
+
for(var i=0;i<T.events.length;i++){
|
|
511
|
+
var ev=T.events[i];
|
|
512
|
+
if(ev.tags&&ev.tags.indexOf(ver)!==-1){
|
|
513
|
+
if(!latest||ev.timestamp>latest)latest=ev.timestamp;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if(!latest){
|
|
518
|
+
for(var i=0;i<children.length;i++){
|
|
519
|
+
var d=children[i].date;
|
|
520
|
+
if(d&&(!latest||d>latest))latest=d;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return fmtTime(latest);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderTree(){
|
|
527
|
+
var els = T.elements||[];
|
|
528
|
+
var roadmapEls=[], otherEls=[];
|
|
529
|
+
for(var i=0;i<els.length;i++){
|
|
530
|
+
var el=els[i];
|
|
531
|
+
if(el.category==="roadmap" || el.elementType==="phase" || el.elementType==="milestone"){
|
|
532
|
+
roadmapEls.push(el);
|
|
533
|
+
}else{
|
|
534
|
+
if(activeCat!=="all" && catOf(el)!==activeCat && !hasChildCat(el,activeCat))continue;
|
|
535
|
+
otherEls.push(el);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
var total=otherEls.length;
|
|
540
|
+
var h="";
|
|
541
|
+
|
|
542
|
+
// ===== Roadmap section (路线图与进度) =====
|
|
543
|
+
if((activeCat==="all" || activeCat==="roadmap") && roadmapEls.length>0){
|
|
544
|
+
h+=renderRoadmap(roadmapEls);
|
|
545
|
+
for(var ri=0;ri<roadmapEls.length;ri++){
|
|
546
|
+
if(roadmapEls[ri].children)total+=roadmapEls[ri].children.length;
|
|
547
|
+
else total+=1;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 分支优化 — 归入 roadmap 板块,收纳所有未关联路线图的事件
|
|
551
|
+
var unmatchedCount=countUnmatchedEvents();
|
|
552
|
+
var branchNode={
|
|
553
|
+
id:"virtual-branch-optimizations",
|
|
554
|
+
kind:"primary",elementType:"catch-all",category:"roadmap",
|
|
555
|
+
name:"分支优化",status:"in_progress",timeGroup:"current",
|
|
556
|
+
timeLabel:"Bug修复 / 功能微调 / 零散优化",
|
|
557
|
+
source:{file:"__unmatched__",title:"分支优化"},
|
|
558
|
+
summary:"兜底节点:收纳所有未关联到具体路线图里程碑的事件(Bug修复、功能微调、零散优化等)",
|
|
559
|
+
tags:["兜底"],children:[],order:999,
|
|
560
|
+
date:new Date().toISOString().substring(0,10)
|
|
561
|
+
};
|
|
562
|
+
h+='<div class="cat-section"><div class="cat-header primary"><span>分支优化</span><span>'+unmatchedCount+' 条事件</span></div>';
|
|
563
|
+
h+=renderTreeCard(branchNode);
|
|
564
|
+
h+="</div>";
|
|
565
|
+
total+=1;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ===== 待办清单 section — 路线图之后、决策记录之前 =====
|
|
569
|
+
if(activeCat==="all"){
|
|
570
|
+
var todoNode={
|
|
571
|
+
id:"virtual-todo-list",
|
|
572
|
+
kind:"primary",elementType:"todo",category:"todo",
|
|
573
|
+
name:"待办清单",status:"pending",timeGroup:"future",
|
|
574
|
+
timeLabel:"发现但未解决的优化点 / 待办项",
|
|
575
|
+
source:{file:"__todo__",title:"待办清单"},
|
|
576
|
+
summary:"记录所有已发现但尚未解决的优化点、待办项,避免遗漏",
|
|
577
|
+
tags:["待办"],children:[],order:998,
|
|
578
|
+
date:new Date().toISOString().substring(0,10)
|
|
579
|
+
};
|
|
580
|
+
h+='<div class="cat-section"><div class="cat-header primary"><span>待办清单</span><span>待整理</span></div>';
|
|
581
|
+
h+=renderTreeCard(todoNode);
|
|
582
|
+
h+="</div>";
|
|
583
|
+
total+=1;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if(activeCat==="all" || activeCat!=="roadmap"){
|
|
587
|
+
var groups={},order=[];
|
|
588
|
+
for(var i=0;i<otherEls.length;i++){
|
|
589
|
+
var c=catOf(otherEls[i]);
|
|
590
|
+
if(!groups[c]){groups[c]=[];order.push(c)}
|
|
591
|
+
groups[c].push(otherEls[i]);
|
|
592
|
+
}
|
|
593
|
+
for(var k in groups){
|
|
594
|
+
groups[k].sort(function(a,b){
|
|
595
|
+
if(a.date&&b.date)return b.date.localeCompare(a.date);
|
|
596
|
+
return(b.order||0)-(a.order||0)
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
order.sort(function(a,b){
|
|
600
|
+
var pa=PRIMARY[a]?0:1,pb=PRIMARY[b]?0:1;
|
|
601
|
+
if(pa!==pb)return pa-pb;
|
|
602
|
+
if(a==="unknown")return 1;if(b==="unknown")return -1;
|
|
603
|
+
return 0;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
for(var i=0;i<order.length;i++){
|
|
607
|
+
var g=order[i],items=groups[g],label=CLABEL[g]||g,isP=PRIMARY[g];
|
|
608
|
+
h+='<div class="cat-section"><div class="cat-header '+(isP?"primary":"secondary")+'"><span>'+esc(label)+'</span><span>'+items.length+' 项</span></div>';
|
|
609
|
+
for(var j=0;j<items.length;j++){
|
|
610
|
+
h+=renderTreeCard(items[j]);
|
|
611
|
+
}
|
|
612
|
+
h+="</div>";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if(total===0){document.getElementById("tree").innerHTML='<div class="empty"><h3>暂无内容</h3><p>选择其他分类或运行 build 刷新</p></div>';return}
|
|
617
|
+
document.getElementById("cnt").textContent=total+" 项";
|
|
618
|
+
document.getElementById("tree").innerHTML=h;
|
|
619
|
+
bindDocClicks();
|
|
620
|
+
(function(){
|
|
621
|
+
var mores=document.querySelectorAll(".rp-more");
|
|
622
|
+
for(var i=0;i<mores.length;i++){
|
|
623
|
+
mores[i].onclick=function(e){
|
|
624
|
+
e.stopPropagation();
|
|
625
|
+
var card=this.parentNode.parentNode;
|
|
626
|
+
var ver=card.getAttribute("data-ver");
|
|
627
|
+
if(ver)expandedRpCards[ver]=true;
|
|
628
|
+
renderTree();
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
})();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function renderRoadmap(roadmapEls){
|
|
635
|
+
var verGroups={},verOrder=[];
|
|
636
|
+
for(var i=0;i<roadmapEls.length;i++){
|
|
637
|
+
var el=roadmapEls[i];
|
|
638
|
+
var children=el.children||[];
|
|
639
|
+
for(var j=0;j<children.length;j++){
|
|
640
|
+
var ch=children[j];
|
|
641
|
+
var ver=extractVersion(ch.name);
|
|
642
|
+
if(!verGroups[ver]){verGroups[ver]=[];verOrder.push(ver)}
|
|
643
|
+
verGroups[ver].push(ch);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
verOrder.sort(function(a,b){return versionOrder(a)-versionOrder(b)});
|
|
647
|
+
|
|
648
|
+
for(var v in verGroups){
|
|
649
|
+
verGroups[v].sort(function(a,b){
|
|
650
|
+
var da=a.date||"",db=b.date||"";
|
|
651
|
+
return db.localeCompare(da);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
var vgList=[];
|
|
656
|
+
for(var vi=0;vi<verOrder.length;vi++){
|
|
657
|
+
var v=verOrder[vi],g=verGroups[v];
|
|
658
|
+
vgList.push({ver:v,children:g});
|
|
659
|
+
}
|
|
660
|
+
// Sort by version descending (v0.3 → v0.2 → v0.1)
|
|
661
|
+
vgList.sort(function(a,b){return versionOrder(b.ver)-versionOrder(a.ver)});
|
|
662
|
+
|
|
663
|
+
var top3={};
|
|
664
|
+
for(var i=0;i<Math.min(3,vgList.length);i++){top3[vgList[i].ver]=true}
|
|
665
|
+
|
|
666
|
+
// Derive title parts from phase element name
|
|
667
|
+
var phaseName="";
|
|
668
|
+
for(var i=0;i<roadmapEls.length;i++){
|
|
669
|
+
if(roadmapEls[i].elementType==="phase"){phaseName=roadmapEls[i].name;break}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
var h="";
|
|
673
|
+
for(var i=0;i<vgList.length;i++){
|
|
674
|
+
var vg=vgList[i],children=vg.children,ver=vg.ver;
|
|
675
|
+
var done=0,total=children.length;
|
|
676
|
+
for(var j=0;j<children.length;j++){
|
|
677
|
+
if(children[j].status==="completed"||children[j].status==="cancelled")done++;
|
|
678
|
+
}
|
|
679
|
+
var allDone=(done===total);
|
|
680
|
+
var ratio=Math.round(done/total*100);
|
|
681
|
+
|
|
682
|
+
var stateCls=allDone?"rp-done":(done>0?"rp-mixed":"");
|
|
683
|
+
var ratioCls=allDone?"all-done":(done>0?"partial":"");
|
|
684
|
+
var ratioText=done+"/"+total;
|
|
685
|
+
|
|
686
|
+
var isTop3=!!top3[ver];
|
|
687
|
+
var manualOpen=!!expandedRpCards[ver];
|
|
688
|
+
var isSemi=isTop3 && !allDone && !manualOpen;
|
|
689
|
+
var isOpen=manualOpen;
|
|
690
|
+
|
|
691
|
+
var title=getVersionTitle(ver,children,phaseName);
|
|
692
|
+
var upTime=getVersionUpdateTime(ver,children);
|
|
693
|
+
|
|
694
|
+
var isFilterMatch=activeDocFilter && ver===activeDocFilter;
|
|
695
|
+
h+='<div class="rp-card '+stateCls+(isSemi?" semi":"")+(isOpen?" open":"")+(isFilterMatch?" filter-match":"")+'" data-ver="'+esc(ver)+'">';
|
|
696
|
+
h+='<div class="rp-head">';
|
|
697
|
+
h+='<span class="rp-arrow" onclick="toggleRpCard(this);event.stopPropagation()">\u25b6</span>';
|
|
698
|
+
h+='<span class="rp-title" onclick="setDocFilter(\''+esc(ver)+'\');event.stopPropagation()" title="筛选此版本事件">'+esc(title)+'</span>';
|
|
699
|
+
h+='<span class="rp-meta"><span class="rp-ratio '+ratioCls+'">'+ratioText+'</span><span class="tc-time">'+esc(upTime)+'</span></span>';
|
|
700
|
+
h+='</div><div class="rp-body">';
|
|
701
|
+
|
|
702
|
+
if(isSemi){
|
|
703
|
+
var incChildren=[],comChildren=[];
|
|
704
|
+
for(var j=0;j<children.length;j++){
|
|
705
|
+
if(children[j].status==="completed"||children[j].status==="cancelled"){
|
|
706
|
+
comChildren.push(children[j]);
|
|
707
|
+
}else{
|
|
708
|
+
incChildren.push(children[j]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for(var j=0;j<incChildren.length;j++){
|
|
712
|
+
h+=renderRoadmapChild(incChildren[j]);
|
|
713
|
+
}
|
|
714
|
+
if(comChildren.length>0){
|
|
715
|
+
h+='<div class="rp-more">\u25bc 查看全部 '+total+' 里程碑(含 '+comChildren.length+' 已完成)</div>';
|
|
716
|
+
}
|
|
717
|
+
}else{
|
|
718
|
+
for(var j=0;j<children.length;j++){
|
|
719
|
+
h+=renderRoadmapChild(children[j]);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
h+="</div></div>";
|
|
723
|
+
}
|
|
724
|
+
return h;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function toggleRpCard(el){
|
|
728
|
+
var card=el.parentNode.parentNode;
|
|
729
|
+
var ver=card.getAttribute("data-ver");
|
|
730
|
+
if(expandedRpCards[ver]){expandedRpCards[ver]=false}
|
|
731
|
+
else{expandedRpCards[ver]=true}
|
|
732
|
+
renderTree();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function renderRoadmapChild(ch){
|
|
736
|
+
var done=ch.status==="completed"||ch.status==="cancelled";
|
|
737
|
+
var icon=done?"\u2705":(ch.status==="in_progress"?"\u23f3":"\u2b1c");
|
|
738
|
+
var labelCls=done?"done":(ch.status==="in_progress"?"progress":"pending-label");
|
|
739
|
+
var labelText=done?"完成":(ch.status==="in_progress"?"进行中":"待开始");
|
|
740
|
+
var time=fmtTime(ch.date)||ch.timeLabel||"";
|
|
741
|
+
var ver=extractVersion(ch.name);
|
|
742
|
+
var h='<div class="rp-child" data-ver="'+esc(ver)+'" data-doc="'+(ch.source&&ch.source.file?esc(ch.source.file):"")+'">';
|
|
743
|
+
h+='<span class="rp-cicon">'+icon+'</span>';
|
|
744
|
+
h+='<span class="rp-cname">'+esc(ch.name||ch.id)+'</span>';
|
|
745
|
+
h+='<span class="rp-ctime">'+esc(time)+'</span>';
|
|
746
|
+
h+='<span class="rp-clabel '+labelCls+'">'+labelText+'</span>';
|
|
747
|
+
h+="</div>";
|
|
748
|
+
return h;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function renderTreeCard(el){
|
|
752
|
+
var isDoc=!!(el.source&&el.source.file);
|
|
753
|
+
var tg=normTg(el.timeGroup);
|
|
754
|
+
var time=fmtTime(el.date)||el.timeLabel||"";
|
|
755
|
+
var unk=catOf(el)==="unknown"?'<span style="background:#fff3e0;color:#e65100;font-size:9px;padding:1px 5px;border-radius:3px">未分类</span>':"";
|
|
756
|
+
|
|
757
|
+
// "新建"/"更新" badge for doc-type cards without status
|
|
758
|
+
var docBadge="";
|
|
759
|
+
if(isDoc || !el.status){
|
|
760
|
+
var elDate=el.date||"",updDate=T&&T.updated?T.updated:"";
|
|
761
|
+
var daysOld=99;
|
|
762
|
+
if(elDate&&updDate){daysOld=(new Date(updDate)-new Date(elDate))/(86400000)}
|
|
763
|
+
if(daysOld<=1)docBadge='<span class="tc-type status-done">新建</span>';
|
|
764
|
+
else if(daysOld<=3)docBadge='<span class="tc-type status-progress">更新</span>';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
var isFilterMatch=activeDocFilter && (isDoc&&el.source.file===activeDocFilter || (el.tags&&el.tags.indexOf(activeDocFilter)!==-1));
|
|
768
|
+
var h='<div class="tree-card'+(isFilterMatch?" filter-match":"")+'" data-tg="'+esc(tg)+'" data-doc="'+(isDoc?esc(el.source.file):"")+'" onclick="onTreeCardClick(event,\''+esc(isDoc?el.source.file:"")+'\')">';
|
|
769
|
+
|
|
770
|
+
h+='<div class="tc-head">';
|
|
771
|
+
if(!isDoc && el.status){
|
|
772
|
+
h+="<span>"+sicon(el.status)+" "+esc(el.name)+"</span>";
|
|
773
|
+
var stCls=el.status==="completed"?"status-done":(el.status==="in_progress"?"status-progress":"status-pending");
|
|
774
|
+
h+='<span class="tc-type '+stCls+'">'+(el.status==="completed"?"完成":(el.status==="in_progress"?"进行中":el.status))+'</span>';
|
|
775
|
+
}else{
|
|
776
|
+
h+="<span>"+esc(el.name)+"</span>";
|
|
777
|
+
h+='<span class="tc-meta">'+unk+docBadge+'<span class="tc-time">'+esc(time)+'</span></span>';
|
|
778
|
+
}
|
|
779
|
+
h+="</div>";
|
|
780
|
+
|
|
781
|
+
h+='<div class="tc-meta" style="padding:0 12px 4px">';
|
|
782
|
+
if(el.status)h+='<span class="tc-time">'+esc(time)+'</span>';
|
|
783
|
+
if(el.tags&&el.tags.length){
|
|
784
|
+
for(var i=0;i<el.tags.length;i++){
|
|
785
|
+
h+='<span class="tag">'+esc(el.tags[i])+'</span>';
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
h+="</div>";
|
|
789
|
+
|
|
790
|
+
if(el.summary)h+='<div class="tc-summary">'+esc(el.summary)+'</div>';
|
|
791
|
+
|
|
792
|
+
if(isDoc){
|
|
793
|
+
h+='<div class="tc-src"><a href="/'+esc(el.source.file)+'" onclick="preview(event,\''+esc(el.source.file)+'\',\'left\');event.stopPropagation()" title="'+esc(el.source.file)+'">'+esc(el.source.file.substring(0,45))+'</a></div>';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
h+="</div>";
|
|
797
|
+
return h;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function setDocFilter(doc){
|
|
801
|
+
if(activeDocFilter===doc){activeDocFilter=null}
|
|
802
|
+
else{activeDocFilter=doc}
|
|
803
|
+
var ft=document.querySelector("#toolbar .ftext");
|
|
804
|
+
var fc=document.querySelector("#toolbar .fclear");
|
|
805
|
+
if(activeDocFilter){
|
|
806
|
+
var label=activeDocFilter==='__unmatched__'?'分支优化(未归类事件)':activeDocFilter==='__todo__'?'待办清单':activeDocFilter;
|
|
807
|
+
ft.textContent="筛选: "+label;fc.classList.add("show");
|
|
808
|
+
document.body.classList.add("filter-active");
|
|
809
|
+
}else{
|
|
810
|
+
ft.textContent="";fc.classList.remove("show");
|
|
811
|
+
document.body.classList.remove("filter-active");
|
|
812
|
+
}
|
|
813
|
+
renderTree();
|
|
814
|
+
renderEvents();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function onTreeCardClick(e,doc){
|
|
818
|
+
if(e.target.closest("a"))return;
|
|
819
|
+
if(doc){setDocFilter(doc)}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function bindDocClicks(){
|
|
823
|
+
var rpChildren=document.querySelectorAll(".rp-child");
|
|
824
|
+
for(var i=0;i<rpChildren.length;i++){
|
|
825
|
+
rpChildren[i].addEventListener("click",function(e){
|
|
826
|
+
e.stopPropagation();
|
|
827
|
+
var ver=this.getAttribute("data-ver");
|
|
828
|
+
if(ver){setDocFilter(ver);return}
|
|
829
|
+
var doc=this.getAttribute("data-doc");
|
|
830
|
+
if(doc)setDocFilter(doc);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ====== EVENTS ======
|
|
836
|
+
var ETYPES = {'对齐与拍板':['ev-t1','t1'],'规格演进':['ev-t2','t2'],'代码变更':['ev-t3','t3'],'测试与质量':['ev-t4','t4'],'审批与交接':['ev-t5','t5'],'运维与基建':['ev-t6','t6'],'教训沉淀':['ev-t7','t7']};
|
|
837
|
+
var PCLASS = {'必记':'ev-p1','应记':'ev-p2','可记':'ev-p3'};
|
|
838
|
+
|
|
839
|
+
function evMatchesDoc(ev, doc){
|
|
840
|
+
if(!doc)return true;
|
|
841
|
+
if(doc==='__unmatched__')return !evHasAnyElementMatch(ev);
|
|
842
|
+
if(doc==='__todo__')return ev.tags&&ev.tags.indexOf('待办')!==-1;
|
|
843
|
+
if(ev.ref&&ev.ref.doc===doc)return true;
|
|
844
|
+
if(ev.tags&&ev.tags.indexOf(doc)!==-1)return true;
|
|
845
|
+
if(ev.title&&ev.title.indexOf(doc)!==-1)return true;
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function evHasAnyElementMatch(ev){
|
|
850
|
+
if(!T||!T.elements)return false;
|
|
851
|
+
for(var i=0;i<T.elements.length;i++){
|
|
852
|
+
var el=T.elements[i];
|
|
853
|
+
if(el.source&&el.source.file){
|
|
854
|
+
if(ev.ref&&ev.ref.doc===el.source.file)return true;
|
|
855
|
+
}
|
|
856
|
+
var elVer=extractVersion(el.name||'');
|
|
857
|
+
if(elVer&&ev.tags&&ev.tags.indexOf(elVer)!==-1)return true;
|
|
858
|
+
if(elVer&&ev.title&&ev.title.indexOf(elVer)!==-1)return true;
|
|
859
|
+
if(el.children){
|
|
860
|
+
for(var j=0;j<el.children.length;j++){
|
|
861
|
+
var ch=el.children[j];
|
|
862
|
+
if(ch.source&&ch.source.file){
|
|
863
|
+
if(ev.ref&&ev.ref.doc===ch.source.file)return true;
|
|
864
|
+
}
|
|
865
|
+
var chVer=extractVersion(ch.name||'');
|
|
866
|
+
if(chVer&&ev.tags&&ev.tags.indexOf(chVer)!==-1)return true;
|
|
867
|
+
if(chVer&&ev.title&&ev.title.indexOf(chVer)!==-1)return true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function countUnmatchedEvents(){
|
|
875
|
+
if(!T||!T.events)return 0;
|
|
876
|
+
var c=0;
|
|
877
|
+
for(var i=0;i<T.events.length;i++){
|
|
878
|
+
if(!evHasAnyElementMatch(T.events[i]))c++;
|
|
879
|
+
}
|
|
880
|
+
return c;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function renderSingleEv(ev){
|
|
884
|
+
var tinfo=ETYPES[ev.type]||['ev-t3','t3'];
|
|
885
|
+
var refs='';
|
|
886
|
+
if(ev.ref){
|
|
887
|
+
if(ev.ref.commit)refs+='<code style="font-size:10px;background:#f5f5f5;padding:0 3px;border-radius:2px">'+esc(ev.ref.commit)+'</code> ';
|
|
888
|
+
if(ev.ref.doc)refs+='<a class="ev-ref" data-d="'+esc(ev.ref.doc)+'" href="/'+esc(ev.ref.doc)+'" onclick="preview(event,\''+esc(ev.ref.doc)+'\',\'right\')">'+esc(ev.ref.doc.substring(0,35))+'</a> ';
|
|
889
|
+
}
|
|
890
|
+
var tags='';
|
|
891
|
+
if(ev.tags&&ev.tags.length)for(var ti=0;ti<ev.tags.length;ti++)tags+='<span class="ev-tag" data-tag="'+esc(ev.tags[ti])+'" onclick="setTagFilter(\''+esc(ev.tags[ti])+'\');event.stopPropagation()">'+esc(ev.tags[ti])+'</span>';
|
|
892
|
+
var time=fmtTime(ev.timestamp||'');
|
|
893
|
+
|
|
894
|
+
return '<div class="ev-item '+tinfo[1]+'">'+
|
|
895
|
+
'<div class="ev-head"><span class="ev-title">'+esc(ev.title)+'</span><span class="ev-type '+tinfo[0]+'">'+esc(ev.type||'')+'</span></div>'+
|
|
896
|
+
'<div class="ev-meta">'+(time?'<span class="ev-time">'+esc(time)+'</span>':'')+refs+tags+'</div>'+
|
|
897
|
+
'</div>';
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function renderEvents(){
|
|
901
|
+
var container=document.getElementById("events");
|
|
902
|
+
var evs=T?T.events||[]:[];
|
|
903
|
+
if(!evs.length){container.innerHTML="<div class=\"empty\"><h3>暂无事件</h3><p>运行 build 后 git log 事件将显示在这里</p></div>";renderTagBar();return}
|
|
904
|
+
// Apply doc filter (matches ref.doc OR tags)
|
|
905
|
+
if(activeDocFilter){var f=[];for(var i=0;i<evs.length;i++){if(evMatchesDoc(evs[i],activeDocFilter))f.push(evs[i])}evs=f}
|
|
906
|
+
// Apply tag filter
|
|
907
|
+
if(activeTagFilter){var f=[];for(var i=0;i<evs.length;i++){if(evHasTag(evs[i],activeTagFilter))f.push(evs[i])}evs=f}
|
|
908
|
+
if(evs.length===0){container.innerHTML="<div class=\"empty\"><h3>没有匹配的事件</h3><p>尝试清除筛选或运行 build 刷新</p></div>";renderTagBar();return}
|
|
909
|
+
// Build doc name map
|
|
910
|
+
var docNameMap={};
|
|
911
|
+
if(T&&T.elements){for(var ei=0;ei<T.elements.length;ei++){var el=T.elements[ei];if(el.source&&el.source.file&&el.name)docNameMap[el.source.file]=el.name}}
|
|
912
|
+
// Flat mode: all events sorted by timestamp, no grouping
|
|
913
|
+
if(sortMode==='flat'){
|
|
914
|
+
evs.sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")});
|
|
915
|
+
var h="",curDay="";
|
|
916
|
+
for(var i=0;i<evs.length;i++){
|
|
917
|
+
var dk3=(evs[i].timestamp||"").substring(0,10);
|
|
918
|
+
if(dk3!==curDay){curDay=dk3;h+="<div class=\"ev-day\">"+esc(curDay)+"</div>"}
|
|
919
|
+
h+=renderSingleEv(evs[i]);
|
|
920
|
+
}
|
|
921
|
+
container.innerHTML=h;
|
|
922
|
+
renderTagBar();
|
|
923
|
+
document.getElementById("evcnt").textContent=evs.length+" 条";
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// Grouped mode: merge all events by thread/doc across ALL dates (no daily headers)
|
|
927
|
+
var docThreads={},orphanThreads={},singles=[];
|
|
928
|
+
for(var i=0;i<evs.length;i++){var ev=evs[i];if(ev.ref&&ev.ref.doc){if(!docThreads[ev.ref.doc])docThreads[ev.ref.doc]=[];docThreads[ev.ref.doc].push(ev)}else if(ev.threadId){if(!orphanThreads[ev.threadId])orphanThreads[ev.threadId]=[];orphanThreads[ev.threadId].push(ev)}else{singles.push(ev)}}
|
|
929
|
+
for(var d in docThreads){docThreads[d].sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")})}
|
|
930
|
+
for(var t in orphanThreads){orphanThreads[t].sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")})}
|
|
931
|
+
singles.sort(function(a,b){return (b.timestamp||"").localeCompare(a.timestamp||"")});
|
|
932
|
+
// Merge doc + orphan threads into one list sorted by latest event timestamp
|
|
933
|
+
var merged=[];
|
|
934
|
+
for(var doc in docThreads){merged.push({tk:"doc:"+doc,te:docThreads[doc],title:docNameMap[doc]||doc.replace(/^.*[\\\/]/,"").replace(/\.md$/i,""),dataDoc:doc,ts:docThreads[doc][0].timestamp})}
|
|
935
|
+
for(var tid in orphanThreads){merged.push({tk:"tid:"+tid,te:orphanThreads[tid],title:"线程: "+tid,dataDoc:null,ts:orphanThreads[tid][0].timestamp})}
|
|
936
|
+
merged.sort(function(a,b){return (b.ts||"").localeCompare(a.ts||"")});
|
|
937
|
+
// Top 3 threads semi-collapsed
|
|
938
|
+
var top3={};
|
|
939
|
+
for(var ti=0;ti<Math.min(3,merged.length);ti++){top3[merged[ti].tk]=true}
|
|
940
|
+
// Render thread card helper
|
|
941
|
+
function rtc(tk,te,title,dataDoc,isSemi){
|
|
942
|
+
var tc2={};for(var i=0;i<te.length;i++){var tt=te[i].type;tc2[tt]=(tc2[tt]||0)+1}
|
|
943
|
+
var ts=[];for(var tk2 in tc2){ts.push(tk2+"\u00d7"+tc2[tk2])}
|
|
944
|
+
var manualOpen=!!expandedThreads[tk];var cls=(isSemi&&!manualOpen)?"semi":(manualOpen?"open":"");if(manualOpen)expandedThreads[tk]=true;
|
|
945
|
+
var r="<div class=\"th-card "+cls+"\" data-thread=\""+esc(tk)+"\""+(dataDoc?" data-doc=\""+esc(dataDoc)+"\"":"")+">";
|
|
946
|
+
r+="<div class=\"th-head\" onclick=\"toggleThread(this)\">";
|
|
947
|
+
r+="<span class=\"th-arrow\">\u25b6</span>";
|
|
948
|
+
r+="<span class=\"th-title\">"+esc(title)+"</span>";
|
|
949
|
+
r+="<span class=\"th-count\">"+te.length+" \u6761</span>";
|
|
950
|
+
r+="</div><div class=\"th-body\">";
|
|
951
|
+
r+="<div class=\"th-typebar\">"+esc(ts.join(" \u00b7 "))+"</div>";
|
|
952
|
+
if(isSemi&&!manualOpen){
|
|
953
|
+
r+=renderSingleEv(te[0]);
|
|
954
|
+
if(te.length>1)r+="<div class=\"th-more\" onclick=\"expandSemi(this);event.stopPropagation()\">\u25bc \u67e5\u770b\u5168\u90e8 "+te.length+" \u6761\u4e8b\u4ef6</div>";
|
|
955
|
+
}else{
|
|
956
|
+
for(var i=0;i<te.length;i++)r+=renderSingleEv(te[i]);
|
|
957
|
+
r+="<div class=\"th-collapse\" onclick=\"collapseThread(this);event.stopPropagation()\">\u25b2 \u6536\u8d77</div>";
|
|
958
|
+
}
|
|
959
|
+
r+="</div></div>";return r;
|
|
960
|
+
}
|
|
961
|
+
// Render: threads first, then orphan singles
|
|
962
|
+
var h="";
|
|
963
|
+
for(var mi=0;mi<merged.length;mi++){var m=merged[mi];h+=rtc(m.tk,m.te,m.title,m.dataDoc,!!top3[m.tk])}
|
|
964
|
+
for(var ui=0;ui<singles.length;ui++)h+=renderSingleEv(singles[ui]);
|
|
965
|
+
container.innerHTML=h;
|
|
966
|
+
if(activeDocFilter){
|
|
967
|
+
var mc=container.querySelector(".th-card[data-doc=\""+CSS.escape(activeDocFilter)+"\"]");
|
|
968
|
+
if(mc){mc.classList.add("th-active");mc.scrollIntoView({behavior:"smooth",block:"center"})}
|
|
969
|
+
}
|
|
970
|
+
renderTagBar();
|
|
971
|
+
document.getElementById("evcnt").textContent=evs.length+" 条";
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
function toggleThread(headEl){
|
|
976
|
+
var card=headEl.parentNode;
|
|
977
|
+
if(card.classList.contains('open')){
|
|
978
|
+
card.classList.remove('open');
|
|
979
|
+
}else{
|
|
980
|
+
card.classList.add('open');
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function expandSemi(el){
|
|
985
|
+
var card=el.parentNode.parentNode;
|
|
986
|
+
var tk=card.getAttribute("data-thread");
|
|
987
|
+
if(tk)expandedThreads[tk]=true;
|
|
988
|
+
card.classList.remove("semi");
|
|
989
|
+
card.classList.add("open");
|
|
990
|
+
renderEvents();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function collapseThread(el){
|
|
994
|
+
var card=el.parentNode.parentNode;
|
|
995
|
+
card.classList.remove("open");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ====== FLOATING PREVIEW ======
|
|
999
|
+
function preview(e,path,from){
|
|
1000
|
+
e.preventDefault();
|
|
1001
|
+
var float=document.getElementById('float'),overlay=document.getElementById('overlay');
|
|
1002
|
+
var needFr=(from==='left'); // tree → fr (right:0), events → no fr (left:0)
|
|
1003
|
+
var hasFr=float.classList.contains('fr');
|
|
1004
|
+
|
|
1005
|
+
document.getElementById('ftitle').textContent=path;
|
|
1006
|
+
document.getElementById('float-body').textContent='加载中...';
|
|
1007
|
+
fetch('/'+encodeURI(path)).then(function(r){if(!r.ok)throw Error('HTTP '+r.status);return r.text()}).then(function(t){
|
|
1008
|
+
t=t.replace(/<!--[\s\S]*?-->/g,'').trim();
|
|
1009
|
+
document.getElementById('float-body').textContent=t||'(空文件)';
|
|
1010
|
+
}).catch(function(err){document.getElementById('float-body').textContent='加载失败:'+err.message});
|
|
1011
|
+
|
|
1012
|
+
// If switching sides, hide first to avoid position jump
|
|
1013
|
+
if(!float.classList.contains('hide') && needFr!==hasFr){
|
|
1014
|
+
float.classList.add('hide');
|
|
1015
|
+
setTimeout(function(){
|
|
1016
|
+
if(needFr){float.classList.add('fr')}else{float.classList.remove('fr')}
|
|
1017
|
+
float.classList.remove('hide');
|
|
1018
|
+
},300);
|
|
1019
|
+
}else{
|
|
1020
|
+
if(needFr){float.classList.add('fr')}else{float.classList.remove('fr')}
|
|
1021
|
+
float.classList.remove('hide');
|
|
1022
|
+
}
|
|
1023
|
+
overlay.classList.remove('hide');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
document.getElementById('float-close').addEventListener('click',function(){
|
|
1027
|
+
document.getElementById('float').classList.add('hide');
|
|
1028
|
+
document.getElementById('overlay').classList.add('hide');
|
|
1029
|
+
});
|
|
1030
|
+
document.getElementById('overlay').addEventListener('click',function(){
|
|
1031
|
+
document.getElementById('float').classList.add('hide');
|
|
1032
|
+
document.getElementById('overlay').classList.add('hide');
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// ====== TOKEN PANEL ======
|
|
1036
|
+
function loadTokenToday(){
|
|
1037
|
+
fetch('../data/token-summary.json').then(function(r){return r.json()}).then(function(d){
|
|
1038
|
+
renderTokenToday(d);
|
|
1039
|
+
}).catch(function(){
|
|
1040
|
+
document.getElementById('token-today').childNodes[0].textContent='工具token:--';
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function renderTokenToday(d){
|
|
1045
|
+
var today=new Date().toISOString().substring(0,10);
|
|
1046
|
+
var td=d&&d.byDate?d.byDate[today]:null;
|
|
1047
|
+
var todayTokens=td?td.tokens:0;
|
|
1048
|
+
var todayCost=0;
|
|
1049
|
+
if(td&&d.bySkill){
|
|
1050
|
+
// Estimate today's cost from skills if available
|
|
1051
|
+
todayCost=d.totalCost||0;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
var label=document.getElementById('token-today');
|
|
1055
|
+
label.childNodes[0].textContent='工具token:'+(todayTokens?todayTokens.toLocaleString()+' tk':'--');
|
|
1056
|
+
|
|
1057
|
+
// Build tooltip content
|
|
1058
|
+
var tip=document.getElementById('token-tip');
|
|
1059
|
+
var h='';
|
|
1060
|
+
if(d&&d.totalTokens>0){
|
|
1061
|
+
h+='<div class="tip-row"><span class="tip-name">今日</span><span class="tip-val">'+(todayTokens?todayTokens.toLocaleString()+' tk':'--')+'</span></div>';
|
|
1062
|
+
h+='<div class="tip-row"><span class="tip-name">累计</span><span class="tip-val">'+d.totalTokens.toLocaleString()+' tk</span></div>';
|
|
1063
|
+
h+='<div class="tip-row"><span class="tip-name">估算费用</span><span class="tip-val">¥'+(d.totalCost||0).toFixed(3)+'</span></div>';
|
|
1064
|
+
if(d.bySkill){
|
|
1065
|
+
var skills=[];
|
|
1066
|
+
for(var k in d.bySkill){skills.push({name:k,tokens:d.bySkill[k].tokens||0,calls:d.bySkill[k].calls||0})}
|
|
1067
|
+
skills.sort(function(a,b){return b.tokens-a.tokens});
|
|
1068
|
+
if(skills.length>0){
|
|
1069
|
+
h+='<div class="tip-divider"></div>';
|
|
1070
|
+
for(var i=0;i<skills.length;i++){
|
|
1071
|
+
h+='<div class="tip-row"><span class="tip-name">'+esc(skills[i].name)+'</span><span class="tip-val">'+skills[i].tokens.toLocaleString()+' tk ×'+skills[i].calls+'</span></div>';
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}else{
|
|
1076
|
+
h+='<div class="tip-row"><span class="tip-name">暂无数据</span></div>';
|
|
1077
|
+
}
|
|
1078
|
+
tip.innerHTML=h;
|
|
1079
|
+
|
|
1080
|
+
// Hover toggling
|
|
1081
|
+
label.onmouseenter=function(){tip.classList.add('show')};
|
|
1082
|
+
label.onmouseleave=function(){tip.classList.remove('show')};
|
|
1083
|
+
}
|
|
1084
|
+
</script>
|
|
1085
|
+
</body>
|
|
1086
|
+
</html>
|