sdd-view.html
15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SDD · View · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="sdd-view.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">03 · SDD</span><span class="sep">/</span><span>V · View</span></div>
<h1>View · UI 组件全集</h1>
<p class="deck">所有前端组件签名 + 路由表 + i18n keys。Coder Agent (fe 类) 读这页能生成全部 Vue 代码。8 个 Vue 组件 + 1 个路由表 + 1 个 i18n key 表。</p>
</header>
<article class="body">
<section data-def-name="DevLoopDashboard" data-mvcp="view">
<h2>V-1 · DevLoopDashboard.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/views/DevLoopDashboard.vue</code>
<pre class="code shape"><span class="c">// Composition API · setup script</span>
<span class="k">const</span> props = defineProps<<span class="p">{}</span>>(); <span class="c">// 无 props,从 route param 取 pid</span>
<span class="k">const</span> route = <span class="n">useRoute</span>();
<span class="k">const</span> pid = computed(() => route.<span class="n">params</span>.<span class="n">pid</span> <span class="k">as</span> <span class="n">string</span>);
<span class="k">const</span> state = ref<ProjectState | null>(<span class="v">null</span>);
<span class="k">const</span> sse = useEventSource(<span class="s">`/api/devloop/projects/</span><span class="v">${pid.value}</span><span class="s">/stream`</span>);
<span class="k">const</span> modalOpen = ref(<span class="v">false</span>);
<span class="k">const</span> drawer = ref<Unit | null>(<span class="v">null</span>);
<span class="c">// 组件树:</span>
<span class="c">// DashboardHeader → LoopStatusBar → UnitsDAG → LiveAgentStream + PrdDiffPanel</span>
<span class="c">// 浮层:FeedbackModal · UnitDrawer</span></pre>
<ul class="contract">
<li>挂载 5 秒内必须发起 GET /state 请求</li>
<li>SSE 断线自动重连(指数退避,最长 30s)</li>
<li>页面卸载时关 SSE</li>
</ul>
</section>
<section data-def-name="DashboardHeader" data-mvcp="view">
<h2>V-2 · DashboardHeader.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/DashboardHeader.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span>
project: <span class="p">{</span> id: <span class="n">string</span>, name: <span class="n">string</span>, status: <span class="n">string</span> <span class="p">}</span>
<span class="p">}</span>>();
defineEmits<<span class="p">{</span>
feedback: <span class="p">[]</span> <span class="c">// 点击 + NEW REQUIREMENT</span>
<span class="p">}</span>>();
<span class="c">// 渲染:</span>
<span class="c">// - 黑底标题栏</span>
<span class="c">// - 项目名(Fraunces 22px)</span>
<span class="c">// - 右上紫色按钮</span></pre>
</section>
<section data-def-name="LoopStatusBar" data-mvcp="view">
<h2>V-3 · LoopStatusBar.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/LoopStatusBar.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span>
l1: <span class="p">{</span> passed: <span class="n">number</span>, testing: <span class="n">number</span>, pending: <span class="n">number</span>, failed: <span class="n">number</span> <span class="p">}</span>,
l2: <span class="p">{</span> lastRunAt?: <span class="n">string</span>, status: <span class="s">'idle'|'running'|'failed'|'shipped'</span>, rounds: <span class="n">number</span> <span class="p">}</span>,
l3: <span class="p">{</span> rev: <span class="n">number</span>, awaitingFeedback: <span class="n">boolean</span> <span class="p">}</span>
<span class="p">}</span>>();</pre>
<ul class="contract">
<li>三张卡片对应 emerald / amber / violet 三色,CSS 用 <code>--L1/L2/L3</code> token</li>
<li>L2 running 时 amber 卡片有 pulse 动画</li>
<li>L3 awaitingFeedback=true 时显示 toast</li>
</ul>
</section>
<section data-def-name="UnitsDAG" data-mvcp="view">
<h2>V-4 · UnitsDAG.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/UnitsDAG.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span>
units: Unit<span class="p">[]</span>
<span class="p">}</span>>();
defineEmits<<span class="p">{</span>
select: <span class="p">[</span>unit: Unit<span class="p">]</span>
<span class="p">}</span>>();
<span class="c">// 内部使用 SVG <rect> + <line></span>
<span class="c">// > 50 unit 时切 d3-force 自动布局</span></pre>
<ul class="contract">
<li>节点配色由 unit.status 决定(见 ui-design-system 色板)</li>
<li>节点点击 emit select 事件,父组件打开 UnitDrawer</li>
<li>箭头方向 = depends_on (上游 → 下游)</li>
</ul>
</section>
<section data-def-name="LiveAgentStream" data-mvcp="view">
<h2>V-5 · LiveAgentStream.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/LiveAgentStream.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span>
events: AgentStreamEvent<span class="p">[]</span> <span class="c">// 由父组件从 SSE 过滤 agent.stream 事件</span>
<span class="p">}</span>>();
<span class="c">// 渲染:</span>
<span class="c">// - 黑底 #0E0E10 终端样式</span>
<span class="c">// - 最近 500 行,老的丢</span>
<span class="c">// - PASS / FAIL 着色(绿/橙)</span>
<span class="c">// - 自动滚到底部,除非用户手动滚</span></pre>
</section>
<section data-def-name="PrdDiffPanel" data-mvcp="view">
<h2>V-6 · PrdDiffPanel.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/PrdDiffPanel.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span>
prdHtml: <span class="n">string</span>,
prevRev: <span class="n">number</span> | <span class="n">null</span>
<span class="p">}</span>>();
<span class="c">// 内部用 diff-dom 客户端算 section 级 diff</span>
<span class="c">// 新增 section 绿色 + 标记 / 删除红色删除线 / 修改黄色</span></pre>
</section>
<section data-def-name="FeedbackModal" data-mvcp="view">
<h2>V-7 · FeedbackModal.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/FeedbackModal.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span> open: <span class="n">boolean</span> <span class="p">}</span>>();
defineEmits<<span class="p">{</span>
submit: <span class="p">[</span>text: <span class="n">string</span><span class="p">]</span>,
close: <span class="p">[]</span>,
<span class="p">}</span>>();
<span class="c">// 单 textarea + 字数计数 + Ctrl+Enter 提交</span>
<span class="c">// 提交后立即 emit close,父组件负责发请求</span></pre>
<ul class="tests">
<li>空文本 → 提交按钮 disabled</li>
<li>> 4000 字 → 警告但不阻止</li>
<li>Esc 触发 close</li>
</ul>
</section>
<section data-def-name="UnitDrawer" data-mvcp="view">
<h2>V-8 · UnitDrawer.vue</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/components/devloop/UnitDrawer.vue</code>
<pre class="code shape">defineProps<<span class="p">{</span>
unit: Unit | <span class="n">null</span>,
runs: RunResult<span class="p">[]</span> <span class="c">// 最近 3 次 run</span>
<span class="p">}</span>>();
defineEmits<<span class="p">{</span> close: <span class="p">[]</span> <span class="p">}</span>>();
<span class="c">// 右侧抽屉,从 right 滑入</span>
<span class="c">// 显示:spec.json 摘要 · contract.json · 最近 3 次 run 摘要 · 跳转完整日志按钮</span></pre>
</section>
<section data-def-name="Router" data-mvcp="view">
<h2>V-9 · 路由表</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/router/index.js</code>
<pre class="code shape"><span class="c">// 在现有 routes 数组追加</span>
<span class="p">{</span>
path: <span class="s">'/devloop/:pid'</span>,
name: <span class="s">'DevLoop'</span>,
component: () => <span class="n">import</span>(<span class="s">'@/views/DevLoopDashboard.vue'</span>),
meta: <span class="p">{</span> requiresAuth: <span class="v">true</span> <span class="p">}</span>,
<span class="p">}</span>,
<span class="p">{</span>
path: <span class="s">'/devloop'</span>, <span class="c">// 项目列表</span>
name: <span class="s">'DevLoopList'</span>,
component: () => <span class="n">import</span>(<span class="s">'@/views/DevLoopList.vue'</span>),
<span class="p">}</span></pre>
<p class="sig">DevLoopList.vue 在 P4 是可选的,先用 GET /api/devloop/projects 列出。</p>
</section>
<section data-def-name="I18nKeys" data-mvcp="view">
<h2>V-10 · i18n key 清单</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/i18n/{zh-CN,en-US,...}/devloop.json</code>
<pre class="code shape"><span class="c">// 每个 locale 一份扁平 JSON:Record<string, string></span>
<span class="p">{</span>
<span class="n">"devloop.loop.l1"</span>: <span class="s">"..."</span>,
<span class="n">"devloop.state.pending"</span>: <span class="s">"..."</span>,
<span class="n">"devloop.btn.newRequirement"</span>:<span class="s">"..."</span>
<span class="c">// ...完整 15 个 key 见下表</span>
<span class="p">}</span></pre>
<table class="dense">
<thead><tr><th>key</th><th>zh-CN</th><th>en-US</th></tr></thead>
<tbody>
<tr><td><code>devloop.loop.l1</code></td><td>单元循环</td><td>Unit Loop</td></tr>
<tr><td><code>devloop.loop.l2</code></td><td>集成循环</td><td>Integration Loop</td></tr>
<tr><td><code>devloop.loop.l3</code></td><td>人类反馈</td><td>Human Feedback</td></tr>
<tr><td><code>devloop.state.pending</code></td><td>待处理</td><td>Pending</td></tr>
<tr><td><code>devloop.state.coding</code></td><td>编码中</td><td>Coding</td></tr>
<tr><td><code>devloop.state.testing</code></td><td>测试中</td><td>Testing</td></tr>
<tr><td><code>devloop.state.failed</code></td><td>失败</td><td>Failed</td></tr>
<tr><td><code>devloop.state.passed</code></td><td>已通过</td><td>Passed</td></tr>
<tr><td><code>devloop.state.blocked</code></td><td>阻塞</td><td>Blocked</td></tr>
<tr><td><code>devloop.btn.newRequirement</code></td><td>+ 新增需求</td><td>+ New Requirement</td></tr>
<tr><td><code>devloop.btn.signOff</code></td><td>验收</td><td>Sign-off</td></tr>
<tr><td><code>devloop.btn.abandon</code></td><td>放弃项目</td><td>Abandon</td></tr>
<tr><td><code>devloop.toast.shipped</code></td><td>项目已交付!</td><td>Project shipped!</td></tr>
<tr><td><code>devloop.toast.needHelp</code></td><td>需要人工介入</td><td>Need human help</td></tr>
<tr><td><code>devloop.empty</code></td><td>尚无项目</td><td>No projects yet</td></tr>
</tbody>
</table>
<p class="sig">中英文必填,其他 4 种语言可走 fallback。</p>
</section>
<section data-def-name="DesignTokens" data-mvcp="view">
<h2>V-11 · 设计 token(CSS 变量)</h2>
<code class="path mono" style="color:var(--ink-soft)">web/src/styles/devloop.css</code>
<pre class="code shape">:root <span class="p">{</span>
--L1: <span class="s">#0E8C5A</span>; --L1-tint: <span class="s">#E2F2EA</span>;
--L2: <span class="s">#C77C2C</span>; --L2-tint: <span class="s">#F7E8D2</span>;
--L3: <span class="s">#6E48C3</span>; --L3-tint: <span class="s">#ECE4FB</span>;
--alert: <span class="s">#C7341A</span>;
<span class="p">}</span>
<span class="c">// 与 LemonBoss 全局 token 共存,前缀避免冲突</span></pre>
</section>
<section>
<h2>组件渲染依赖关系</h2>
<svg viewBox="0 0 1100 280" width="100%" role="img">
<defs><marker id="arrV" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#0E0E10"/></marker></defs>
<g font-family="JetBrains Mono" font-size="11">
<rect x="440" y="40" width="220" height="50" rx="6" fill="#ECE4FB" stroke="#6E48C3"/>
<text x="550" y="70" text-anchor="middle" font-weight="700">DevLoopDashboard</text>
<rect x="40" y="140" width="180" height="40" rx="6" fill="#fff" stroke="#0E0E10"/><text x="130" y="165" text-anchor="middle">DashboardHeader</text>
<rect x="240" y="140" width="180" height="40" rx="6" fill="#fff" stroke="#0E0E10"/><text x="330" y="165" text-anchor="middle">LoopStatusBar</text>
<rect x="440" y="140" width="180" height="40" rx="6" fill="#fff" stroke="#0E0E10"/><text x="530" y="165" text-anchor="middle">UnitsDAG</text>
<rect x="640" y="140" width="180" height="40" rx="6" fill="#fff" stroke="#0E0E10"/><text x="730" y="165" text-anchor="middle">LiveAgentStream</text>
<rect x="840" y="140" width="180" height="40" rx="6" fill="#fff" stroke="#0E0E10"/><text x="930" y="165" text-anchor="middle">PrdDiffPanel</text>
<rect x="240" y="220" width="180" height="40" rx="6" fill="#F7E8D2" stroke="#C77C2C"/><text x="330" y="245" text-anchor="middle">FeedbackModal (浮层)</text>
<rect x="440" y="220" width="180" height="40" rx="6" fill="#F7E8D2" stroke="#C77C2C"/><text x="530" y="245" text-anchor="middle">UnitDrawer (浮层)</text>
<g stroke="#0E0E10" fill="none" marker-end="url(#arrV)">
<line x1="500" y1="90" x2="160" y2="135"/>
<line x1="510" y1="90" x2="320" y2="135"/>
<line x1="540" y1="90" x2="540" y2="135"/>
<line x1="570" y1="90" x2="720" y2="135"/>
<line x1="580" y1="90" x2="900" y2="135"/>
<line x1="510" y1="90" x2="340" y2="215"/>
<line x1="540" y1="180" x2="530" y2="215"/>
</g>
</g>
</svg>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>03 · SDD</div><div><b>Page</b>sdd-view.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>