sdd-view.html 15 KB
<!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&lt;<span class="p">{}</span>&gt;();   <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(() =&gt; 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&lt;ProjectState | null&gt;(<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&lt;Unit | null&gt;(<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&lt;<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>&gt;();
defineEmits&lt;<span class="p">{</span>
  feedback: <span class="p">[]</span>           <span class="c">// 点击 + NEW REQUIREMENT</span>
<span class="p">}</span>&gt;();

<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&lt;<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>&gt;();</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&lt;<span class="p">{</span>
  units: Unit<span class="p">[]</span>
<span class="p">}</span>&gt;();
defineEmits&lt;<span class="p">{</span>
  select: <span class="p">[</span>unit: Unit<span class="p">]</span>
<span class="p">}</span>&gt;();

<span class="c">// 内部使用 SVG &lt;rect&gt; + &lt;line&gt;</span>
<span class="c">// &gt; 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&lt;<span class="p">{</span>
  events: AgentStreamEvent<span class="p">[]</span>   <span class="c">// 由父组件从 SSE 过滤 agent.stream 事件</span>
<span class="p">}</span>&gt;();

<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&lt;<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>&gt;();

<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&lt;<span class="p">{</span> open: <span class="n">boolean</span> <span class="p">}</span>&gt;();
defineEmits&lt;<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>&gt;();

<span class="c">// 单 textarea + 字数计数 + Ctrl+Enter 提交</span>
<span class="c">// 提交后立即 emit close,父组件负责发请求</span></pre>
        <ul class="tests">
          <li>空文本 → 提交按钮 disabled</li>
          <li>&gt; 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&lt;<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>&gt;();
defineEmits&lt;<span class="p">{</span> close: <span class="p">[]</span> <span class="p">}</span>&gt;();

<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: () =&gt; <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: () =&gt; <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&lt;string, string&gt;</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>