浏览代码

docs: add sample freight menu design and list demo

Document the cell-storage freight info menu with rowspan list, dual-source samples, and dual entry with protocol forms; include a static HTML preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 周之前
父节点
当前提交
57d6241804

+ 617 - 0
docs/demos/2026-10-08-sample-freight-menu-demo.html

@@ -0,0 +1,617 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>运费信息 · 列表 Demo(细胞存储)</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=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
+  <style>
+    :root {
+      --primary: #10b981;
+      --primary-dark: #059669;
+      --primary-bg: #ecfdf5;
+      --bg-page: #f8fafc;
+      --panel: #ffffff;
+      --text: #0f172a;
+      --text-secondary: #334155;
+      --text-muted: #64748b;
+      --border: #e2e8f0;
+      --radius: 10px;
+      --radius-sm: 8px;
+      --danger: #ef4444;
+      --warning: #d97706;
+      --info: #64748b;
+      --sidebar: 220px;
+      --topbar: 56px;
+    }
+
+    * { box-sizing: border-box; }
+    body {
+      margin: 0;
+      font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+      font-size: 14px;
+      line-height: 1.5;
+      color: var(--text);
+      background: var(--bg-page);
+    }
+
+    .app {
+      display: grid;
+      grid-template-columns: var(--sidebar) 1fr;
+      min-height: 100vh;
+    }
+
+    .sidebar {
+      background: #0f172a;
+      color: #e2e8f0;
+      padding: 16px 0;
+    }
+    .sidebar .brand {
+      padding: 8px 20px 20px;
+      font-weight: 700;
+      font-size: 15px;
+      color: #fff;
+    }
+    .sidebar .group-title {
+      padding: 12px 20px 6px;
+      font-size: 11px;
+      color: #94a3b8;
+      letter-spacing: 0.04em;
+    }
+    .sidebar a {
+      display: block;
+      padding: 9px 20px 9px 28px;
+      color: #cbd5e1;
+      text-decoration: none;
+      font-size: 13px;
+    }
+    .sidebar a:hover { background: rgba(255,255,255,0.04); color: #fff; }
+    .sidebar a.active {
+      background: rgba(16, 185, 129, 0.18);
+      color: #6ee7b7;
+      border-right: 3px solid var(--primary);
+      font-weight: 600;
+    }
+    .sidebar a.l1 {
+      padding-left: 20px;
+      font-weight: 600;
+      color: #f1f5f9;
+    }
+
+    .main { display: flex; flex-direction: column; min-width: 0; }
+    .topbar {
+      height: var(--topbar);
+      background: var(--panel);
+      border-bottom: 1px solid var(--border);
+      display: flex;
+      align-items: center;
+      padding: 0 24px;
+      color: var(--text-muted);
+      font-size: 13px;
+    }
+    .topbar strong { color: var(--text-secondary); font-weight: 600; }
+
+    .content { padding: 24px; }
+
+    .panel {
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: var(--radius);
+      overflow: hidden;
+    }
+
+    .panel-top {
+      padding: 16px 20px 12px;
+      border-bottom: 1px solid var(--border);
+    }
+    .panel-top h2 {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 600;
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+    .panel-top h2::before {
+      content: "";
+      width: 3px;
+      height: 16px;
+      border-radius: 2px;
+      background: var(--primary);
+    }
+    .panel-top .hint {
+      margin: 8px 0 0;
+      font-size: 12px;
+      color: var(--text-muted);
+    }
+
+    .filter-block {
+      padding: 16px 20px;
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 12px 16px;
+    }
+    .field label {
+      display: block;
+      font-size: 11px;
+      color: var(--text-muted);
+      margin-bottom: 4px;
+    }
+    .field input, .field select {
+      width: 100%;
+      height: 36px;
+      border: 1px solid var(--border);
+      border-radius: var(--radius-sm);
+      padding: 0 10px;
+      font: inherit;
+      color: var(--text);
+      background: #fff;
+    }
+    .field input::placeholder { color: #94a3b8; }
+
+    .toolbar {
+      padding: 12px 20px;
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex-wrap: wrap;
+      border-top: 1px solid var(--border);
+    }
+    .selected-tip {
+      font-size: 13px;
+      color: var(--text-muted);
+      margin-right: 4px;
+    }
+
+    .btn {
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      height: 36px;
+      padding: 0 14px;
+      border-radius: var(--radius-sm);
+      border: 1px solid var(--border);
+      background: #fff;
+      color: var(--text-secondary);
+      font: inherit;
+      font-weight: 500;
+      cursor: pointer;
+    }
+    .btn:hover { border-color: #cbd5e1; }
+    .btn-main {
+      background: var(--primary);
+      border-color: var(--primary);
+      color: #fff;
+    }
+    .btn-main:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
+    .btn:disabled {
+      opacity: 0.45;
+      cursor: not-allowed;
+    }
+    .btn-op {
+      height: 28px;
+      padding: 0 10px;
+      font-size: 12px;
+      border-radius: 6px;
+    }
+
+    .list-body { border-top: 1px solid var(--border); }
+    table.list-table {
+      width: 100%;
+      border-collapse: collapse;
+      table-layout: fixed;
+    }
+    .list-table th {
+      background: #f8fafc;
+      color: var(--text-muted);
+      font-weight: 600;
+      font-size: 12px;
+      text-align: left;
+      padding: 10px 12px;
+      border-bottom: 1px solid var(--border);
+      white-space: nowrap;
+    }
+    .list-table td {
+      padding: 10px 12px;
+      border-bottom: 1px solid var(--border);
+      vertical-align: middle;
+      color: var(--text-secondary);
+      font-size: 13px;
+    }
+    .list-table tbody tr:nth-child(even) td:not(.merged) {
+      background: #fafbfc;
+    }
+    .list-table td.merged {
+      background: #fff;
+      border-right: 1px solid var(--border);
+      vertical-align: middle;
+    }
+    .list-table tr:hover td:not(.merged) { background: var(--primary-bg); }
+    .list-table .mono { font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
+    .list-table .amount { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text); }
+    .list-table .center { text-align: center; }
+
+    .tag {
+      display: inline-flex;
+      align-items: center;
+      height: 22px;
+      padding: 0 8px;
+      border-radius: 6px;
+      font-size: 12px;
+      font-weight: 500;
+    }
+    .tag.success { background: var(--primary-bg); color: var(--primary-dark); }
+    .tag.info { background: #f1f5f9; color: var(--info); }
+    .tag.warn { background: #fffbeb; color: var(--warning); }
+
+    .pager {
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      gap: 12px;
+      padding: 12px 20px;
+      border-top: 1px solid var(--border);
+      color: var(--text-muted);
+      font-size: 13px;
+    }
+
+    .legend {
+      margin-top: 12px;
+      padding: 12px 14px;
+      background: var(--primary-bg);
+      border: 1px solid #a7f3d0;
+      border-radius: var(--radius-sm);
+      color: var(--primary-dark);
+      font-size: 12px;
+    }
+    .legend code {
+      background: rgba(255,255,255,0.7);
+      padding: 1px 6px;
+      border-radius: 4px;
+    }
+
+    /* modal */
+    .mask {
+      position: fixed; inset: 0;
+      background: rgba(15, 23, 42, 0.45);
+      display: none;
+      align-items: center;
+      justify-content: center;
+      z-index: 50;
+      backdrop-filter: none;
+    }
+    .mask.open { display: flex; }
+    .dialog {
+      width: min(720px, calc(100vw - 32px));
+      max-height: calc(100vh - 100px);
+      background: #fff;
+      border-radius: var(--radius);
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+      box-shadow: 0 20px 40px rgba(15,23,42,0.18);
+    }
+    .dialog header, .dialog footer {
+      padding: 14px 20px;
+      border-bottom: 1px solid var(--border);
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+    .dialog footer {
+      border-bottom: 0;
+      border-top: 1px solid var(--border);
+      justify-content: flex-end;
+      gap: 8px;
+    }
+    .dialog h3 { margin: 0; font-size: 16px; }
+    .dialog .body { padding: 16px 20px; overflow: auto; }
+    .form-grid-2 {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 12px;
+    }
+    .form-grid-2 .full { grid-column: 1 / -1; }
+    .tabs {
+      display: inline-flex;
+      border: 1px solid var(--border);
+      border-radius: var(--radius-sm);
+      overflow: hidden;
+      margin-bottom: 14px;
+    }
+    .tabs button {
+      border: 0;
+      background: #fff;
+      padding: 8px 14px;
+      cursor: pointer;
+      font: inherit;
+      color: var(--text-muted);
+    }
+    .tabs button.active {
+      background: var(--primary-bg);
+      color: var(--primary-dark);
+      font-weight: 600;
+    }
+    .sample-pick {
+      border: 1px solid var(--border);
+      border-radius: var(--radius-sm);
+      max-height: 220px;
+      overflow: auto;
+    }
+    .sample-pick label {
+      display: flex;
+      gap: 10px;
+      align-items: center;
+      padding: 8px 12px;
+      border-bottom: 1px solid var(--border);
+      cursor: pointer;
+    }
+    .sample-pick label:last-child { border-bottom: 0; }
+    .sample-pick label:hover { background: var(--primary-bg); }
+
+    @media (max-width: 1100px) {
+      .filter-block { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+      .app { grid-template-columns: 1fr; }
+      .sidebar { display: none; }
+    }
+  </style>
+</head>
+<body>
+  <div class="app">
+    <aside class="sidebar">
+      <div class="brand">中科兴皖 · Demo</div>
+      <div class="group-title">市场运营</div>
+      <a class="l1" href="javascript:;">细胞存储</a>
+      <a href="javascript:;">预签约订单</a>
+      <a href="javascript:;">缴费信息</a>
+      <a href="javascript:;">样本信息管理</a>
+      <a class="active" href="javascript:;">运费信息</a>
+      <a href="javascript:;">缴费财务确认及开票</a>
+      <div class="group-title">说明</div>
+      <a href="javascript:;">协议单内仍可快捷配置</a>
+    </aside>
+
+    <div class="main">
+      <div class="topbar">
+        细胞存储 / <strong>运费信息</strong>
+        <span style="margin-left:auto">静态 Demo · Fresh Mint</span>
+      </div>
+
+      <div class="content">
+        <div class="panel">
+          <div class="panel-top">
+            <h2>运费信息</h2>
+            <p class="hint">
+              一行逻辑对应一笔运费批次;左侧「运输方式 / 运费 / 关联数」合并单元格。
+              右侧按实际关联展示细胞存储与科研捐赠样本(一次专线车可混装)。
+            </p>
+          </div>
+
+          <div class="filter-block">
+            <div class="field">
+              <label>运输方式</label>
+              <select>
+                <option value="">全部</option>
+                <option>顺丰快递</option>
+                <option selected>专线车</option>
+              </select>
+            </div>
+            <div class="field">
+              <label>来源</label>
+              <select>
+                <option selected>全部(含细胞存储 / 科研捐赠)</option>
+                <option>仅细胞存储</option>
+                <option>仅科研捐赠</option>
+              </select>
+            </div>
+            <div class="field">
+              <label>协议单号 / 样本编码</label>
+              <input placeholder="输入检索" />
+            </div>
+            <div class="field">
+              <label>创建时间</label>
+              <input type="text" placeholder="起止日期" value="2026-10-01 ~ 2026-10-08" />
+            </div>
+          </div>
+
+          <div class="toolbar">
+            <span class="selected-tip" id="selTip">已选 0 项</span>
+            <button class="btn btn-main" type="button" onclick="openDialog()">
+              <span>+</span> 配置运费
+            </button>
+            <button class="btn" type="button" id="btnUnlink" disabled onclick="alert('Demo:取消所选样本的运费关联')">
+              取消运费关联
+            </button>
+            <button class="btn" type="button" onclick="alert('Demo:编辑批次运输方式/金额')">编辑运费</button>
+          </div>
+
+          <div class="list-body">
+            <table class="list-table">
+              <thead>
+                <tr>
+                  <th style="width:44px" class="center"><input type="checkbox" title="demo" disabled /></th>
+                  <th style="width:100px">运输方式</th>
+                  <th style="width:100px">运费(元)</th>
+                  <th style="width:72px" class="center">关联数</th>
+                  <th style="width:96px">来源</th>
+                  <th style="width:140px">协议单号</th>
+                  <th style="width:160px">样本编码</th>
+                  <th style="width:120px">样本类型</th>
+                  <th style="width:150px">创建时间</th>
+                  <th style="width:160px">操作</th>
+                </tr>
+              </thead>
+              <tbody>
+                <!-- 批次 A:专线车 800,3 个样本(合并) -->
+                <tr>
+                  <td class="center merged" rowspan="3"><input type="checkbox" class="row-check" data-batch="A" /></td>
+                  <td class="merged" rowspan="3"><span class="tag success">专线车</span></td>
+                  <td class="merged amount" rowspan="3">¥800.00</td>
+                  <td class="merged center" rowspan="3">3</td>
+                  <td><span class="tag success">细胞存储</span></td>
+                  <td class="mono">CP20261008001</td>
+                  <td class="mono">ZK-UC-20261008-01</td>
+                  <td>脐带血</td>
+                  <td class="merged" rowspan="3">2026-10-08 09:12:00</td>
+                  <td class="merged" rowspan="3">
+                    <button class="btn btn-op" type="button">编辑</button>
+                    <button class="btn btn-op" type="button">取消关联</button>
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="tag info">科研捐赠</span></td>
+                  <td class="mono">RD20261008012</td>
+                  <td class="mono">RD-PL-20261008-03</td>
+                  <td>胎盘</td>
+                </tr>
+                <tr>
+                  <td><span class="tag success">细胞存储</span></td>
+                  <td class="mono">CP20261008007</td>
+                  <td class="mono">ZK-UC-20261008-07</td>
+                  <td>脐带血</td>
+                </tr>
+
+                <!-- 批次 B:顺丰 120,1 个样本 -->
+                <tr>
+                  <td class="center merged"><input type="checkbox" class="row-check" data-batch="B" /></td>
+                  <td class="merged"><span class="tag warn">顺丰快递</span></td>
+                  <td class="merged amount">¥120.00</td>
+                  <td class="merged center">1</td>
+                  <td><span class="tag success">细胞存储</span></td>
+                  <td class="mono">CP20261007022</td>
+                  <td class="mono">ZK-UC-20261007-22</td>
+                  <td>脐带血</td>
+                  <td class="merged">2026-10-07 16:40:11</td>
+                  <td class="merged">
+                    <button class="btn btn-op" type="button">编辑</button>
+                    <button class="btn btn-op" type="button">取消关联</button>
+                  </td>
+                </tr>
+
+                <!-- 批次 C:同城闪送 260,2 个捐赠样本 -->
+                <tr>
+                  <td class="center merged" rowspan="2"><input type="checkbox" class="row-check" data-batch="C" /></td>
+                  <td class="merged" rowspan="2"><span class="tag info">同城闪送</span></td>
+                  <td class="merged amount" rowspan="2">¥260.00</td>
+                  <td class="merged center" rowspan="2">2</td>
+                  <td><span class="tag info">科研捐赠</span></td>
+                  <td class="mono">RD20261006003</td>
+                  <td class="mono">RD-CB-20261006-01</td>
+                  <td>脐带</td>
+                  <td class="merged" rowspan="2">2026-10-06 11:05:33</td>
+                  <td class="merged" rowspan="2">
+                    <button class="btn btn-op" type="button">编辑</button>
+                    <button class="btn btn-op" type="button">取消关联</button>
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="tag info">科研捐赠</span></td>
+                  <td class="mono">RD20261006003</td>
+                  <td class="mono">RD-CB-20261006-02</td>
+                  <td>脐带</td>
+                </tr>
+
+                <!-- 批次 D:空批次示意 -->
+                <tr>
+                  <td class="center merged"><input type="checkbox" class="row-check" data-batch="D" /></td>
+                  <td class="merged"><span class="tag success">专线车</span></td>
+                  <td class="merged amount">¥500.00</td>
+                  <td class="merged center">0</td>
+                  <td colspan="3" style="color:var(--text-muted)">— 暂无关联样本 —</td>
+                  <td class="merged">2026-10-05 18:20:00</td>
+                  <td class="merged">
+                    <button class="btn btn-op" type="button">编辑</button>
+                    <button class="btn btn-op btn-main" type="button" style="color:#fff;background:var(--primary);border-color:var(--primary)">关联样本</button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+          <div class="pager">
+            <span>共 4 笔运费 · 6 条样本明细</span>
+            <button class="btn btn-op" type="button" disabled>上一页</button>
+            <span>1 / 1</span>
+            <button class="btn btn-op" type="button" disabled>下一页</button>
+          </div>
+        </div>
+
+        <div class="legend">
+          <strong>Demo 说明:</strong>
+          浅色竖条标题对齐 Fresh Mint;批次 A 展示「专线车一次拉 3 个样本(含细胞存储 + 科研捐赠)」的合并单元格效果。
+          正式实现走 Vue + Element Plus,文件路径:
+          <code>docs/demos/2026-10-08-sample-freight-menu-demo.html</code>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="mask" id="mask" onclick="if(event.target===this)closeDialog()">
+    <div class="dialog" role="dialog" aria-modal="true">
+      <header>
+        <h3>配置运费</h3>
+        <button class="btn btn-op" type="button" onclick="closeDialog()">关闭</button>
+      </header>
+      <div class="body">
+        <div class="tabs">
+          <button type="button" class="active" data-tab="create" onclick="switchTab(this)">新建运费</button>
+          <button type="button" data-tab="link" onclick="switchTab(this)">关联已有</button>
+        </div>
+        <div id="tab-create">
+          <div class="form-grid-2">
+            <div class="field">
+              <label>运输方式</label>
+              <input list="modes" placeholder="选择或输入(可自动入字典)" value="专线车" />
+              <datalist id="modes">
+                <option value="顺丰快递"></option>
+                <option value="专线车"></option>
+                <option value="同城闪送"></option>
+              </datalist>
+            </div>
+            <div class="field">
+              <label>运费(元)</label>
+              <input type="number" min="0" step="0.01" value="800" />
+            </div>
+            <div class="field full">
+              <label>勾选要挂上的样本(可混选细胞存储 / 科研捐赠)</label>
+              <div class="sample-pick">
+                <label><input type="checkbox" checked /> <span class="tag success">细胞存储</span> CP20261008001 · ZK-UC-20261008-01</label>
+                <label><input type="checkbox" checked /> <span class="tag info">科研捐赠</span> RD20261008012 · RD-PL-20261008-03</label>
+                <label><input type="checkbox" /> <span class="tag success">细胞存储</span> CP20261008009 · ZK-UC-20261008-09</label>
+                <label><input type="checkbox" /> <span class="tag info">科研捐赠</span> RD20261008001 · RD-CB-20261008-01</label>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div id="tab-link" style="display:none;color:var(--text-muted);font-size:13px">
+          Demo 占位:此处为近期运费批次单选列表(方式 / 金额 / 关联数 / 时间),确定后挂到已选样本。
+        </div>
+      </div>
+      <footer>
+        <button class="btn" type="button" onclick="closeDialog()">取消</button>
+        <button class="btn btn-main" type="button" onclick="alert('Demo:调用 create/link API'); closeDialog()">确定应用</button>
+      </footer>
+    </div>
+  </div>
+
+  <script>
+    function openDialog() { document.getElementById('mask').classList.add('open') }
+    function closeDialog() { document.getElementById('mask').classList.remove('open') }
+    function switchTab(btn) {
+      document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'))
+      btn.classList.add('active')
+      const create = btn.dataset.tab === 'create'
+      document.getElementById('tab-create').style.display = create ? '' : 'none'
+      document.getElementById('tab-link').style.display = create ? 'none' : ''
+    }
+    function refreshSel() {
+      const n = document.querySelectorAll('.row-check:checked').length
+      document.getElementById('selTip').textContent = '已选 ' + n + ' 项'
+      document.getElementById('btnUnlink').disabled = n === 0
+    }
+    document.querySelectorAll('.row-check').forEach(el => el.addEventListener('change', refreshSel))
+  </script>
+</body>
+</html>

+ 197 - 0
docs/superpowers/specs/2026-10-08-sample-freight-menu-design.md

@@ -0,0 +1,197 @@
+# 运费信息菜单 — 实现设计
+
+> 日期:2026-10-08  
+> 状态:已确认(2026-10-08)  
+> 关联:`docs/superpowers/specs/2026-10-07-sample-freight-batch-design.md`(运费批次实体已落地)  
+> Demo:`docs/demos/2026-10-08-sample-freight-menu-demo.html`
+
+---
+
+## 1. 背景
+
+运费批次(`biz_sample_freight` / `biz_sample_freight_item`)与协议单内「配置运费」已实现。原设计将「独立运费中心」列为非目标。业务需要在细胞存储侧提供全局台账:一次专线车/快递常混装**细胞存储 + 科研捐赠**样本,需在一处查看与配置。
+
+---
+
+## 2. 已确认决策
+
+| 项 | 结论 |
+|----|------|
+| 菜单位置 | 细胞存储 →「样本信息管理」下方 → **运费信息** |
+| 协议单入口 | **两边保留**:菜单管全局,协议单作快捷 |
+| 列表粒度 | 一笔运费批次;样本多行展示,运费列 **合并单元格(rowspan)** |
+| 样本来源 | 细胞存储 + 科研捐赠均可关联与展示 |
+| 数据模型 | **不新建表**;复用现有运费 API |
+
+---
+
+## 3. 目标与非目标
+
+### 3.1 目标
+
+- 新增菜单与列表页:管理全部运费批次。
+- 列表:运输方式 / 运费 / 关联数等批次字段合并单元格;右侧展示关联样本(来源、协议单号、样本编码、样本类型等)。
+- 支持:配置运费(新建 / 关联已有)、取消运费关联、编辑运输方式与金额。
+- 样本选择器支持混选细胞存储与科研捐赠样本。
+- 协议单内现有配置运费能力保持不变,共用 API。
+
+### 3.2 非目标
+
+- 科研捐赠侧栏再挂一份「运费信息」菜单。
+- 运费计入合同/缴费。
+- 物流明细字段(单号、站点等)扩展。
+- 旧运输数据迁移。
+- 小程序端。
+
+---
+
+## 4. 菜单与路由
+
+| 项 | 值 |
+|----|-----|
+| 侧栏父级 | 细胞存储 |
+| 标题 | 运费信息 |
+| 顺序 | 紧挨「样本信息管理」之后 |
+| 路由 path(建议) | `sample-freight` |
+| 组件 | `/cell-storage/sample-freight/index` |
+| 权限码 | `cell-storage:sample-freight:list` |
+| Hash 示例 | `/#/market/cell-storage/sample-freight` |
+
+- `web-admin/src/router/modules/sqxp.ts` 增加路由。
+- SQL:`docs/sql/migrations/2026-10-08-sample-freight-menu.sql` 写入 `sys_menu`(及按需角色授权),`order_num` 排在样本信息管理之后。
+- `App.vue` `panelTitleBarTextRules` 注册「运费信息」完整标题(若需竖条)。
+
+---
+
+## 5. 列表 UI
+
+对齐标准列表(筛选 → 工具栏 → 表格 → 分页)与 Fresh Mint;参考 Demo HTML。
+
+### 5.1 筛选
+
+- 运输方式(字典 `sample_transport_mode`)
+- 来源:全部 / 仅细胞存储 / 仅科研捐赠(按关联 items 过滤批次)
+- 协议单号 / 样本编码(关键字命中 items)
+- 创建时间范围
+
+### 5.2 工具栏
+
+- 最前「已选 N 项」(按**批次**勾选,合并行共用一个 checkbox)
+- **配置运费**(唯一 `type="primary"`)
+- **取消运费关联**(无勾选禁用)
+- **编辑运费**(可选工具栏 + 行内编辑)
+
+按钮均带 `<el-icon>`。
+
+### 5.3 表格(合并单元格)
+
+批次字段(rowspan):运输方式、运费(元)、关联数、创建时间、操作(或操作仅行内按需)。
+
+样本字段(每关联样本一行):来源 Tag、协议单号、样本编码、样本类型。
+
+- 无关联样本:样本列合并展示「—」,关联数 0;操作提供「关联样本」。
+- 来源 Tag:`细胞存储` / `科研捐赠`(语义色 Tag)。
+- 运输方式可用 `el-tag`。
+- 列一律 `min-width`;操作列 `fixed="right"`,`min-width` ≥ 260px。
+- Element Plus `el-table` 用 `span-method` 实现 rowspan。
+
+### 5.4 分页
+
+按**运费批次**分页(不是按样本行)。页脚文案可附带「本页样本明细行数」提示。
+
+---
+
+## 6. 交互
+
+### 6.1 配置运费
+
+弹窗(`dialog-form-layout`,`label-position="top"`,无 backdrop blur):
+
+- Tab:**新建运费** | **关联已有**
+- 新建:运输方式(可搜索 + allow-create)+ 运费(元) + 样本多选 → `POST /web/admin/sample-freights`
+- 关联已有:批次单选列表 + 样本多选 → `POST /web/admin/sample-freights/{id}/link`
+- 样本选择:支持来源筛选、协议单号/样本编码检索;可混选两种来源
+
+### 6.2 取消运费关联
+
+- 工具栏:对勾选批次解绑其下全部 items,空批次软删;确认框提示。
+- 行内「取消关联」:可弹出该批次 items 勾选后部分 unlink(`POST .../unlink`)。
+
+### 6.3 编辑运费
+
+- `PUT /web/admin/sample-freights/{id}` 改运输方式/金额。
+- 提示将同步所有关联样本的展示。
+
+### 6.4 与协议单
+
+协议单 `SampleTransportFormSection` 行为不变;两边写同一套后端接口。
+
+---
+
+## 7. 后端调整
+
+现有 CRUD/API 已覆盖主路径。本需求增量:
+
+1. **列表带 items**  
+   `GET /web/admin/sample-freights` 增加查询参数 `includeItems=true`(默认 false 保持兼容)。  
+   为 true 时每条返回 `items[]`:`sampleSource`、`sampleId`、`sampleCode`、`protocolId`、`protocolNo`(需联表协议号)、`sampleTypeName`(可选联表)。
+
+2. **筛选增强**(同列表接口)  
+   - `sampleSource`:`cell_storage` \| `research_donation`(有匹配 item 的批次)  
+   - `keyword`:命中 `transport_mode` / 协议单号 / 样本编码  
+   - 创建时间起止  
+
+3. **样本待选列表**(供配置弹窗)  
+   - 复用/轻量封装:细胞存储与科研捐赠样本检索 API(只读、租户 + 数据权限),或新增  
+     `GET /web/admin/sample-freights/sample-options`  
+     参数:`sampleSource?`、`keyword`、`page`、`pageSize`  
+     返回可挂运费的样本摘要(已挂其他运费的样本仍可选,link 时覆盖并提示)。
+
+权限:菜单码 `cell-storage:sample-freight:list`;接口沿用 admin 鉴权,数据范围与协议样本一致。
+
+---
+
+## 8. 前端文件(建议)
+
+| 文件 | 说明 |
+|------|------|
+| `web-admin/src/views/cell-storage/sample-freight/index.vue` | 列表页 |
+| `web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue` | 配置运费弹窗(可抽公共,供协议单后续复用) |
+| `web-admin/src/api/sample-freight.ts` | 扩展 list 参数、`sample-options` |
+| `web-admin/src/router/modules/sqxp.ts` | 路由 |
+| `docs/sql/migrations/2026-10-08-sample-freight-menu.sql` | 菜单 |
+
+协议单组件可不在本迭代强制抽取公共弹窗;行为一致即可。
+
+---
+
+## 9. 验收标准
+
+1. 侧栏「细胞存储」下、样本信息管理下可见「运费信息」。
+2. 列表按批次分页;多样本时运输方式/运费/关联数合并单元格正确。
+3. 同一批次可同时展示细胞存储与科研捐赠样本。
+4. 本页可新建运费并混选两来源样本;可关联已有;可取消关联;可改方式/金额。
+5. 协议单内配置运费仍可用,与本页数据一致。
+6. 无权限用户不可见菜单(或接口 403)。
+7. UI 符合 Fresh Mint / 按钮图标 / Tag / 表格列宽约定。
+
+---
+
+## 10. 实现顺序(建议)
+
+1. 菜单 SQL + 路由 + 空列表页骨架  
+2. 后端 list `includeItems` + 筛选 + protocolNo/typeName enrichment;样本 options API  
+3. 前端合并单元格表格 + 筛选分页  
+4. 配置 / 取消 / 编辑弹窗对接 API  
+5. 权限与手测(含跨来源一车多样本)  
+6. 增量更新 graphify(web-admin,必要时 backend)
+
+---
+
+## 11. 风险
+
+| 风险 | 对策 |
+|------|------|
+| el-table span-method 与勾选/斑马纹 | 按批次生成扁平行 + `span-method`;勾选绑定 freightId |
+| 列表 items 过大 | 分页按批次;单批次 items 异常多时详情懒加载(首版可全带) |
+| 样本 options 性能 | 远程搜索 + 分页,禁止一次拉全表 |