Procházet zdrojové kódy

docs: mark storage-release ready filter as implemented

Add design spec, plan checkboxes, and interactive filter demo for the pending readyForReviewOnly feature.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu před 3 měsíci
rodič
revize
38f18f1926

+ 596 - 0
docs/superpowers/demos/2026-07-29-storage-release-pending-both-completed-filter-demo.html

@@ -0,0 +1,596 @@
+<!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;
+      --text: #1e293b;
+      --text-muted: #64748b;
+      --border: #d9e8e2;
+      --bg-page: #f5f7f6;
+      --surface: #ffffff;
+      --table-head: #f8fafc;
+      --row-alt: #fafafa;
+      --row-hover: #f1f5f9;
+      --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+      --radius: 10px;
+      --danger: #ef4444;
+      --warn: #d97706;
+    }
+
+    * { 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);
+    }
+
+    .page {
+      max-width: 1120px;
+      margin: 0 auto;
+      padding: 28px 24px 48px;
+    }
+
+    .hero {
+      margin-bottom: 20px;
+    }
+    .hero h1 {
+      margin: 0 0 6px;
+      font-size: 22px;
+      font-weight: 700;
+      letter-spacing: -0.02em;
+    }
+    .hero p {
+      margin: 0;
+      color: var(--text-muted);
+      max-width: 720px;
+    }
+
+    .legend {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      margin: 16px 0 20px;
+    }
+    .chip {
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      padding: 6px 12px;
+      border-radius: 999px;
+      border: 1px solid var(--border);
+      background: var(--surface);
+      color: var(--text-muted);
+      font-size: 12px;
+      font-weight: 500;
+    }
+    .chip strong { color: var(--text); font-weight: 600; }
+    .dot {
+      width: 8px; height: 8px; border-radius: 50%;
+    }
+    .dot.ready { background: var(--primary); }
+    .dot.hidden { background: #94a3b8; }
+    .dot.all { background: var(--warn); }
+
+    .panel {
+      background: var(--surface);
+      border: 1px solid var(--border);
+      border-radius: var(--radius);
+      box-shadow: var(--shadow);
+      overflow: hidden;
+    }
+
+    .panel-top {
+      padding: 16px 20px 0;
+    }
+    .panel-top h2 {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 600;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+    .panel-top h2::before {
+      content: "";
+      width: 3px;
+      height: 16px;
+      border-radius: 2px;
+      background: var(--primary);
+    }
+
+    .pill-tabs {
+      padding: 14px 20px 0;
+    }
+    .pill-track {
+      display: inline-flex;
+      padding: 4px;
+      background: #f1f5f9;
+      border-radius: 999px;
+      gap: 4px;
+    }
+    .pill-btn {
+      border: none;
+      background: transparent;
+      padding: 8px 18px;
+      border-radius: 999px;
+      font: inherit;
+      font-weight: 500;
+      color: var(--text-muted);
+      cursor: default;
+    }
+    .pill-btn.is-active {
+      background: var(--surface);
+      color: var(--primary-dark);
+      box-shadow: 0 1px 2px rgba(0,0,0,0.06);
+    }
+
+    .filter-block {
+      padding: 16px 20px 12px;
+      border-bottom: 1px solid var(--border);
+    }
+    .filter-grid {
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 12px 16px;
+      margin-bottom: 12px;
+    }
+    .field label {
+      display: block;
+      font-size: 11px;
+      color: var(--text-muted);
+      margin-bottom: 6px;
+      font-weight: 500;
+    }
+    .control {
+      height: 36px;
+      width: 100%;
+      border: 1px solid var(--border);
+      border-radius: 8px;
+      padding: 0 12px;
+      background: #fff;
+      color: var(--text);
+      display: flex;
+      align-items: center;
+      font-size: 14px;
+    }
+    .control.muted { color: #94a3b8; }
+    .control.select::after {
+      content: "▾";
+      margin-left: auto;
+      color: #94a3b8;
+      font-size: 12px;
+    }
+
+    .filter-extra {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 16px;
+      flex-wrap: wrap;
+      padding-top: 4px;
+    }
+
+    .check-option {
+      display: inline-flex;
+      align-items: flex-start;
+      gap: 10px;
+      padding: 10px 12px;
+      border: 1px dashed #a7f3d0;
+      border-radius: 8px;
+      background: var(--primary-bg);
+      max-width: 520px;
+      cursor: pointer;
+      user-select: none;
+    }
+    .check-option input {
+      margin-top: 3px;
+      width: 16px;
+      height: 16px;
+      accent-color: var(--primary);
+      cursor: pointer;
+    }
+    .check-option .copy .title {
+      font-weight: 600;
+      color: var(--text);
+      font-size: 13px;
+    }
+    .check-option .copy .hint {
+      margin-top: 2px;
+      font-size: 12px;
+      color: var(--text-muted);
+      line-height: 1.45;
+    }
+
+    .actions {
+      display: flex;
+      gap: 8px;
+      margin-left: auto;
+    }
+    .btn {
+      height: 36px;
+      padding: 0 14px;
+      border-radius: 8px;
+      border: 1px solid var(--border);
+      background: #fff;
+      color: var(--text);
+      font: inherit;
+      font-weight: 500;
+      cursor: pointer;
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+    }
+    .btn-main {
+      background: var(--primary);
+      border-color: var(--primary);
+      color: #fff;
+    }
+    .btn-main:hover { background: var(--primary-dark); }
+
+    .toolbar {
+      padding: 12px 20px;
+      background: var(--table-head);
+      border-bottom: 1px solid var(--border);
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      color: var(--text-muted);
+      font-size: 13px;
+    }
+    .sel-num { color: var(--primary-dark); font-weight: 600; }
+
+    .mode-banner {
+      margin: 0 20px;
+      margin-top: 14px;
+      padding: 10px 12px;
+      border-radius: 8px;
+      font-size: 13px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+    .mode-banner.ready {
+      background: var(--primary-bg);
+      color: var(--primary-dark);
+      border: 1px solid #a7f3d0;
+    }
+    .mode-banner.all {
+      background: #fffbeb;
+      color: #92400e;
+      border: 1px solid rgba(217, 119, 6, 0.25);
+    }
+
+    table {
+      width: 100%;
+      border-collapse: collapse;
+      margin-top: 4px;
+    }
+    th, td {
+      padding: 12px 10px;
+      text-align: center;
+      border-bottom: 1px solid var(--border);
+      font-size: 13px;
+    }
+    th {
+      background: var(--table-head);
+      color: var(--text-muted);
+      font-weight: 500;
+      font-size: 12px;
+    }
+    tbody tr:nth-child(even) { background: var(--row-alt); }
+    tbody tr.is-hidden {
+      display: none;
+    }
+    tbody tr.is-dim {
+      opacity: 0.55;
+    }
+    .code {
+      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+      font-size: 12px;
+    }
+    .tag {
+      display: inline-block;
+      padding: 2px 8px;
+      border-radius: 4px;
+      font-size: 12px;
+      font-weight: 500;
+    }
+    .tag.ok { background: var(--primary-bg); color: var(--primary-dark); }
+    .tag.wait { background: #fff7ed; color: var(--warn); }
+    .tag.muted { background: #f1f5f9; color: var(--text-muted); }
+    .tag.danger { background: #fef2f2; color: var(--danger); }
+
+    .pager {
+      padding: 12px 20px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      color: var(--text-muted);
+      font-size: 13px;
+      border-top: 1px solid var(--border);
+    }
+
+    .compare {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 16px;
+      margin-top: 24px;
+    }
+    @media (max-width: 900px) {
+      .filter-grid { grid-template-columns: 1fr 1fr; }
+      .compare { grid-template-columns: 1fr; }
+    }
+    .card {
+      background: var(--surface);
+      border: 1px solid var(--border);
+      border-radius: var(--radius);
+      padding: 16px;
+      box-shadow: var(--shadow);
+    }
+    .card h3 {
+      margin: 0 0 8px;
+      font-size: 14px;
+    }
+    .card p {
+      margin: 0;
+      color: var(--text-muted);
+      font-size: 13px;
+    }
+    .card ul {
+      margin: 10px 0 0;
+      padding-left: 18px;
+      color: var(--text);
+      font-size: 13px;
+    }
+    .card.recommend {
+      border-color: #6ee7b7;
+      background: linear-gradient(180deg, #ecfdf5 0%, #fff 48%);
+    }
+    .badge {
+      display: inline-block;
+      margin-left: 6px;
+      padding: 1px 6px;
+      border-radius: 4px;
+      background: var(--primary);
+      color: #fff;
+      font-size: 11px;
+      font-weight: 600;
+      vertical-align: middle;
+    }
+    .note {
+      margin-top: 16px;
+      font-size: 12px;
+      color: var(--text-muted);
+    }
+  </style>
+</head>
+<body>
+  <div class="page">
+    <div class="hero">
+      <h1>入库放行审核 · 待审核过滤 Demo</h1>
+      <p>
+        客户诉求:【待审核】默认只展示「质检工单」与「生产工单」均为完成的记录;
+        增加一个选项,<strong>默认不勾选</strong>;勾选后展示全部待审核信息。
+      </p>
+    </div>
+
+    <div class="legend">
+      <span class="chip"><span class="dot ready"></span><strong>默认(未勾选)</strong>:仅双工单都完成</span>
+      <span class="chip"><span class="dot all"></span><strong>勾选后</strong>:展示全部待审核</span>
+      <span class="chip"><span class="dot hidden"></span>默认会被隐藏的行</span>
+    </div>
+
+    <section class="panel" aria-label="待审核列表 Demo">
+      <div class="panel-top">
+        <h2>审核列表</h2>
+      </div>
+
+      <div class="pill-tabs">
+        <div class="pill-track" role="tablist">
+          <button type="button" class="pill-btn is-active" role="tab" aria-selected="true">待审核</button>
+          <button type="button" class="pill-btn" role="tab" aria-selected="false" tabindex="-1">已审核</button>
+        </div>
+      </div>
+
+      <div class="filter-block">
+        <div class="filter-grid">
+          <div class="field">
+            <label>关键词</label>
+            <div class="control muted">样本编码/入库编码/样本名称</div>
+          </div>
+          <div class="field">
+            <label>协议类别</label>
+            <div class="control select muted">全部</div>
+          </div>
+          <div class="field">
+            <label>审核结论</label>
+            <div class="control select muted">全部</div>
+          </div>
+          <div class="field">
+            <label>入库状态</label>
+            <div class="control select muted">全部</div>
+          </div>
+        </div>
+
+        <div class="filter-extra">
+          <label class="check-option" for="showAll">
+            <input id="showAll" type="checkbox" />
+            <span class="copy">
+              <span class="title">展示全部待审核</span>
+              <span class="hint">默认不勾选:仅展示质检工单与生产工单均为「完成」的记录。勾选后取消该限制。</span>
+            </span>
+          </label>
+          <div class="actions">
+            <button type="button" class="btn">↻ 重置</button>
+            <button type="button" class="btn btn-main">⌕ 搜索</button>
+          </div>
+        </div>
+      </div>
+
+      <div class="toolbar">
+        已选 <span class="sel-num">0</span> 项
+        <button type="button" class="btn btn-main" style="margin-left:8px" disabled>批量审核</button>
+        <button type="button" class="btn" disabled>批量入库</button>
+      </div>
+
+      <div id="modeBanner" class="mode-banner ready">
+        当前模式:仅展示质检 + 生产均已完成(默认)
+      </div>
+
+      <div style="overflow-x:auto; padding: 0 8px 0;">
+        <table>
+          <thead>
+            <tr>
+              <th style="width:48px"></th>
+              <th>样本编码</th>
+              <th>样本名称</th>
+              <th>质检工单</th>
+              <th>生产工单</th>
+              <th>审核结论</th>
+              <th>说明</th>
+            </tr>
+          </thead>
+          <tbody id="tbody">
+            <tr data-ready="1">
+              <td>☐</td>
+              <td class="code">YB-202607-001</td>
+              <td>脐带血样本 A</td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>默认可见</td>
+            </tr>
+            <tr data-ready="1">
+              <td>☐</td>
+              <td class="code">YB-202607-002</td>
+              <td>外周血样本 B</td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>默认可见</td>
+            </tr>
+            <tr data-ready="0">
+              <td>☐</td>
+              <td class="code">YB-202607-003</td>
+              <td>脂肪组织 C</td>
+              <td><span class="tag wait">进行中</span></td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>质检未完成 → 默认隐藏</td>
+            </tr>
+            <tr data-ready="0">
+              <td>☐</td>
+              <td class="code">YB-202607-004</td>
+              <td>胎盘组织 D</td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag wait">待配置</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>生产未完成 → 默认隐藏</td>
+            </tr>
+            <tr data-ready="0">
+              <td>☐</td>
+              <td class="code">YB-202607-005</td>
+              <td>骨髓样本 E</td>
+              <td><span class="tag muted">无工单</span></td>
+              <td><span class="tag wait">进行中</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>无质检算完成,但生产未完成 → 默认隐藏</td>
+            </tr>
+            <tr data-ready="1">
+              <td>☐</td>
+              <td class="code">YB-202607-006</td>
+              <td>脐带组织 F</td>
+              <td><span class="tag ok">已完成</span></td>
+              <td><span class="tag muted">无工单</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>无生产工单算完成 → 默认可见</td>
+            </tr>
+            <tr data-ready="1">
+              <td>☐</td>
+              <td class="code">YB-202607-007</td>
+              <td>科研样本 G</td>
+              <td><span class="tag muted">无工单</span></td>
+              <td><span class="tag muted">无工单</span></td>
+              <td><span class="tag muted">待审核</span></td>
+              <td>双无工单均算完成 → 默认可见</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+
+      <div class="pager">
+        <span id="totalText">共 4 条(默认过滤后)</span>
+        <span>第 1 页 / 10 条每页</span>
+      </div>
+    </section>
+
+    <div class="compare">
+      <div class="card">
+        <h3>现状(已上线)</h3>
+        <p>待审核筛选区有「质检工单是否完成」下拉:是 / 否 / 全部,默认「是」。</p>
+        <ul>
+          <li>只约束质检工单</li>
+          <li>不约束生产工单</li>
+          <li>三态下拉,交互偏重</li>
+        </ul>
+      </div>
+      <div class="card recommend">
+        <h3>本 Demo 提案 <span class="badge">推荐方向</span></h3>
+        <p>用一个复选框替换原下拉:未勾选 = 质检侧+生产侧均完成;勾选 = 全部。无工单算该侧完成。</p>
+        <ul>
+          <li>与客户描述「默认不勾选 / 勾选展示全部」一致</li>
+          <li>去掉「否」单独筛未完成的能力</li>
+          <li>仅作用于「待审核」Tab;API 参数 <code>readyForReviewOnly</code></li>
+        </ul>
+      </div>
+    </div>
+
+    <p class="note">
+      文件位置:<code>docs/superpowers/demos/2026-07-29-storage-release-pending-both-completed-filter-demo.html</code>
+      · 本地打开即可交互。本页仅演示交互,不接真实 API。
+    </p>
+  </div>
+
+  <script>
+    const checkbox = document.getElementById('showAll');
+    const rows = Array.from(document.querySelectorAll('#tbody tr'));
+    const banner = document.getElementById('modeBanner');
+    const totalText = document.getElementById('totalText');
+
+    function render() {
+      const showAll = checkbox.checked;
+      let visible = 0;
+      rows.forEach((row) => {
+        const ready = row.getAttribute('data-ready') === '1';
+        const show = showAll || ready;
+        row.classList.toggle('is-hidden', !show);
+        if (show) visible += 1;
+      });
+      if (showAll) {
+        banner.className = 'mode-banner all';
+        banner.textContent = '当前模式:展示全部待审核(已勾选)';
+        totalText.textContent = '共 ' + visible + ' 条(未按工单完成度过滤)';
+      } else {
+        banner.className = 'mode-banner ready';
+        banner.textContent = '当前模式:仅展示质检 + 生产均已完成(默认)';
+        totalText.textContent = '共 ' + visible + ' 条(默认过滤后)';
+      }
+    }
+
+    checkbox.addEventListener('change', render);
+    render();
+  </script>
+</body>
+</html>

+ 434 - 0
docs/superpowers/plans/2026-07-29-storage-release-pending-ready-filter.md

@@ -0,0 +1,434 @@
+# 入库放行审核 · 待审核双工单就绪过滤 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 待审核 Tab 默认只展示质检侧与生产侧均「已完成」的入库放行记录;用「展示全部待审核」复选框替换原质检下拉,勾选后展示全部待审核。
+
+**Architecture:** 列表 API 新增可选布尔参数 `readyForReviewOnly`;为 `true` 时在 `list_reviews` 上对质检/生产未完成状态做双 `NOT EXISTS`(无工单算该侧完成)。前端仅在待审核展示复选框,默认/重置/切回待审核均为不勾选,并停止传 `qcWorkOrderCompleted`。
+
+**Tech Stack:** FastAPI + SQLAlchemy(backend)、Vue 3 + Element Plus(web-admin)、pytest
+
+**Spec:** `docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md`
+
+---
+
+## File Map
+
+| 文件 | 职责 |
+|------|------|
+| `backend/tests/test_storage_release_review_ready_filter.py` | **新建** `normalize_ready_for_review_only` 单测 |
+| `backend/app/crud/storage_release_review.py` | normalize + 生产未完成 EXISTS + `list_reviews` 过滤 |
+| `backend/app/api/storage_release_review.py` | Query `readyForReviewOnly` |
+| `web-admin/src/api/storage-release-review.ts` | 列表 params 增加 `readyForReviewOnly?: boolean` |
+| `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | 复选框替换下拉;默认/重置/切 Tab;请求传参 |
+
+---
+
+### Task 1: Backend — normalize + 双 NOT EXISTS 过滤 + API
+
+**Files:**
+- Create: `backend/tests/test_storage_release_review_ready_filter.py`
+- Modify: `backend/app/crud/storage_release_review.py`
+- Modify: `backend/app/api/storage_release_review.py`
+
+- [x] **Step 1: 写失败单测(规范化函数)**
+
+创建 `backend/tests/test_storage_release_review_ready_filter.py`:
+
+```python
+from app.crud.storage_release_review import normalize_ready_for_review_only
+
+
+def test_normalize_ready_for_review_only_true_values():
+    assert normalize_ready_for_review_only(True) is True
+    assert normalize_ready_for_review_only("true") is True
+    assert normalize_ready_for_review_only("TRUE") is True
+    assert normalize_ready_for_review_only("1") is True
+    assert normalize_ready_for_review_only("yes") is True
+    assert normalize_ready_for_review_only(" YES ") is True
+
+
+def test_normalize_ready_for_review_only_non_true_is_none():
+    """Only explicit true enables the filter; false/unknown/absent → no filter."""
+    assert normalize_ready_for_review_only(None) is None
+    assert normalize_ready_for_review_only(False) is None
+    assert normalize_ready_for_review_only("") is None
+    assert normalize_ready_for_review_only("false") is None
+    assert normalize_ready_for_review_only("0") is None
+    assert normalize_ready_for_review_only("no") is None
+    assert normalize_ready_for_review_only("maybe") is None
+```
+
+- [x] **Step 2: 跑单测确认失败**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_ready_filter.py -v
+```
+
+Expected: FAIL(`normalize_ready_for_review_only` 未定义或导入失败)
+
+- [x] **Step 3: 实现 normalize、生产未完成 EXISTS,并接入 `list_reviews`**
+
+在 `backend/app/crud/storage_release_review.py`,于 `normalize_qc_work_order_completed` **之后**增加:
+
+```python
+def normalize_ready_for_review_only(raw) -> Optional[bool]:
+    """Return True when ready-only filter should apply; None means no filter."""
+    if raw is None:
+        return None
+    if isinstance(raw, bool):
+        return True if raw else None
+    s = str(raw).strip().lower()
+    if s in ("true", "1", "yes"):
+        return True
+    return None
+
+
+def _production_work_order_unfinished_exists_for_review_sample(tenant_no: str):
+    """True if sample has any unfinished inbound-side production WO (dpz/jxz)."""
+    conds = [
+        BizProductionWorkOrder.tenant_no == tenant_no,
+        BizProductionWorkOrder.del_flag == 0,
+        BizProductionWorkOrder.sample_code.isnot(None),
+        BizProductionWorkOrder.sample_code != "",
+        eq_unicode(
+            BizProductionWorkOrder.sample_code, BizStorageReleaseReview.sample_code
+        ),
+        BizStorageReleaseReview.sample_code.isnot(None),
+        BizStorageReleaseReview.sample_code != "",
+        BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")),
+    ]
+    return (
+        exists()
+        .where(and_(*conds))
+        .correlate(BizStorageReleaseReview)
+    )
+```
+
+(`BizProductionWorkOrder`、`eq_unicode`、`exists`、`and_` 已在文件中导入,无需新增 import。)
+
+在 `list_reviews` 签名中增加参数(放在 `qc_work_order_completed` 之后):
+
+```python
+    ready_for_review_only=None,
+```
+
+在现有 `qc_done = normalize_qc_work_order_completed(...)` 块**之后**、`if user_info:` **之前**插入:
+
+```python
+    if normalize_ready_for_review_only(ready_for_review_only) is True:
+        # 无未完成质检/生产工单 ⇒ 该侧完成(含零工单)
+        q0 = q0.filter(
+            ~_qc_work_order_exists_for_review_sample(
+                tenant_no, unfinished_only=True
+            ),
+            ~_production_work_order_unfinished_exists_for_review_sample(tenant_no),
+        )
+```
+
+保留现有 `qc_work_order_completed` 逻辑不动(本页前端停用;API 兼容)。
+
+- [x] **Step 4: API 增加 Query 并下传**
+
+修改 `backend/app/api/storage_release_review.py` 的 `list_storage_release_reviews`:
+
+在 `qcWorkOrderCompleted: Optional[bool] = Query(None),` 后增加:
+
+```python
+    readyForReviewOnly: Optional[bool] = Query(None),
+```
+
+在 `srr_svc.list_reviews(...)` 调用中增加:
+
+```python
+            ready_for_review_only=readyForReviewOnly,
+```
+
+- [x] **Step 5: 跑单测确认通过**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_ready_filter.py tests/test_storage_release_review_qc_completed.py -v
+```
+
+Expected: PASS(新旧 normalize 单测均绿)
+
+- [x] **Step 6: Commit**
+
+```bash
+git add backend/tests/test_storage_release_review_ready_filter.py \
+  backend/app/crud/storage_release_review.py \
+  backend/app/api/storage_release_review.py
+git commit -m "$(cat <<'EOF'
+feat(storage-release): add readyForReviewOnly list filter
+
+Default pending list can exclude rows with unfinished QC or production work orders via SQL NOT EXISTS.
+EOF
+)"
+```
+
+---
+
+### Task 2: Frontend — API 类型 + 复选框替换下拉
+
+**Files:**
+- Modify: `web-admin/src/api/storage-release-review.ts`
+- Modify: `web-admin/src/views/quality-inspection/storage-release-review/index.vue`
+
+- [x] **Step 1: API params 增加 `readyForReviewOnly`**
+
+修改 `web-admin/src/api/storage-release-review.ts` 中 `fetchStorageReleaseReviewList`:
+
+```typescript
+export function fetchStorageReleaseReviewList(params?: {
+  page?: number
+  pageSize?: number
+  receptionSource?: string
+  reviewConclusion?: string
+  storageStatus?: string
+  keyword?: string
+  reviewPhase?: 'pending' | 'reviewed'
+  qcWorkOrderCompleted?: boolean
+  readyForReviewOnly?: boolean
+}) {
+  return request.get<PageList<StorageReleaseReview>>({
+    url: '/web/admin/storage-release-reviews',
+    params
+  })
+}
+```
+
+(保留 `qcWorkOrderCompleted` 类型以免其它调用方类型报错;本页不再传。)
+
+- [x] **Step 2: 模板 — 去掉质检下拉与 5 列栅格,加复选框**
+
+在 `index.vue` 模板中:
+
+1) 将 `filter-grid` 改回普通 4 列(去掉 `storage-release-filter-grid--5` 与内联 `gridTemplateColumns`):
+
+```vue
+          <div class="filter-grid">
+```
+
+2) **删除**整块:
+
+```vue
+            <el-form-item v-if="activeTab === 'pending'" label="质检工单是否完成">
+              ...
+            </el-form-item>
+```
+
+3) 在 `filter-actions-row` 内、`.filter-actions-end` **之前**插入(仅待审核):
+
+```vue
+          <div class="filter-actions-row">
+            <label
+              v-if="activeTab === 'pending'"
+              class="storage-release-ready-check"
+            >
+              <el-checkbox v-model="searchForm.showAllPending" />
+              <span class="storage-release-ready-check__copy">
+                <span class="storage-release-ready-check__title">展示全部待审核</span>
+                <span class="storage-release-ready-check__hint">
+                  默认不勾选:仅展示质检与生产工单均为完成的记录;勾选后展示全部。
+                </span>
+              </span>
+            </label>
+            <div class="filter-actions-end">
+              ...
+            </div>
+          </div>
+```
+
+(若项目 `filter-actions-row` 默认把内容推到右侧,给 `.storage-release-ready-check` 设 `margin-right: auto`,见 Step 4 样式。)
+
+- [x] **Step 3: script — 状态、请求、重置、切 Tab**
+
+删除:
+
+```typescript
+  const qcWorkOrderCompletedOptions = [ ... ]
+```
+
+将 `searchForm` 改为:
+
+```typescript
+  const searchForm = reactive({
+    receptionSource: '',
+    reviewConclusion: '',
+    storageStatus: '',
+    keyword: '',
+    showAllPending: false
+  })
+```
+
+`loadData` 中替换 `qcParam` 逻辑:
+
+```typescript
+  async function loadData() {
+    loading.value = true
+    try {
+      const readyOnly =
+        activeTab.value === 'pending' && !searchForm.showAllPending ? true : undefined
+      const res = await fetchStorageReleaseReviewList({
+        page: pagination.page,
+        pageSize: pagination.pageSize,
+        receptionSource: searchForm.receptionSource || undefined,
+        reviewConclusion: searchForm.reviewConclusion || undefined,
+        storageStatus: searchForm.storageStatus || undefined,
+        keyword: searchForm.keyword || undefined,
+        reviewPhase: activeTab.value,
+        readyForReviewOnly: readyOnly
+      })
+      tableData.value = res.list || []
+      pagination.total = res.total ?? 0
+    } finally {
+      loading.value = false
+    }
+  }
+```
+
+`handleTabChange`:
+
+```typescript
+  function handleTabChange(name: ReviewTab) {
+    if (activeTab.value === name) return
+    activeTab.value = name
+    pagination.page = 1
+    selectedRows.value = []
+    if (name === 'pending') {
+      searchForm.showAllPending = false
+    }
+    loadData()
+  }
+```
+
+`handleReset`:
+
+```typescript
+  function handleReset() {
+    searchForm.receptionSource = ''
+    searchForm.reviewConclusion = ''
+    searchForm.storageStatus = ''
+    searchForm.keyword = ''
+    searchForm.showAllPending = false
+    pagination.page = 1
+    loadData()
+  }
+```
+
+说明:`watchDebounced(searchForm, ...)` 已存在;勾选复选框会自动触发搜索,无需额外 `@change`。
+
+- [x] **Step 4: 样式 — 复选框行 + 去掉 5 列窄屏规则**
+
+在 `<style scoped>` 中:
+
+删除(或整段替换)针对 `storage-release-filter-grid--5` 的 `@media` 规则。
+
+增加:
+
+```scss
+  .filter-actions-row {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: flex-start;
+    gap: 12px;
+  }
+
+  .storage-release-ready-check {
+    display: inline-flex;
+    align-items: flex-start;
+    gap: 10px;
+    margin-right: auto;
+    max-width: 520px;
+    padding: 10px 12px;
+    border: 1px dashed #a7f3d0;
+    border-radius: 8px;
+    background: var(--primary-bg, #ecfdf5);
+    cursor: pointer;
+    user-select: none;
+  }
+
+  .storage-release-ready-check__copy {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+  }
+
+  .storage-release-ready-check__title {
+    font-size: 13px;
+    font-weight: 600;
+    color: var(--text, #1e293b);
+    line-height: 1.4;
+  }
+
+  .storage-release-ready-check__hint {
+    font-size: 12px;
+    color: var(--text-muted, #64748b);
+    line-height: 1.45;
+  }
+```
+
+若全局已有 `.filter-actions-row` 布局,只补复选框相关类,避免重复覆盖冲突;以页面可对齐 Demo 为准。
+
+- [x] **Step 5: 手工验收**
+
+启动后端与 `web-admin`(`pnpm dev`),打开  
+`http://localhost:3006/#/market/cell-storage/storage-release-review`:
+
+1. 进入待审核:复选框未勾选;无「质检工单是否完成」下拉;默认列表不含质检 `dpz`/`ypz` 或生产 `dpz`/`jxz` 的行;双无工单行可见  
+2. 勾选「展示全部待审核」→ 出现原先被隐藏的未完成行;`total` 变大  
+3. 取消勾选 / 重置 → 回到默认过滤  
+4. 切到已审核:复选框消失;切回待审核:复选框不勾选  
+5. Network:未勾选时请求带 `readyForReviewOnly=true`;勾选或不在待审核时不带该参数;不再带 `qcWorkOrderCompleted`
+
+- [x] **Step 6: Commit**
+
+```bash
+git add web-admin/src/api/storage-release-review.ts \
+  web-admin/src/views/quality-inspection/storage-release-review/index.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): pending storage-release ready checkbox filter
+
+Replace QC-completed select with show-all checkbox; request readyForReviewOnly when unchecked.
+EOF
+)"
+```
+
+---
+
+### Task 3: 文档状态(可选收尾)
+
+**Files:**
+- Modify: `docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md`(头部状态可改为「已实现」)
+- Modify: `docs/superpowers/plans/2026-07-29-storage-release-pending-ready-filter.md`(本文件,勾选完成项)
+
+- [x] **Step 1: 实现完成后更新 spec 状态为「已实现」并 commit**
+
+```bash
+git add docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md \
+  docs/superpowers/plans/2026-07-29-storage-release-pending-ready-filter.md
+git commit -m "$(cat <<'EOF'
+docs: mark storage-release ready filter spec as implemented
+EOF
+)"
+```
+
+---
+
+## Spec Coverage Checklist
+
+| Spec 要求 | Task |
+|-----------|------|
+| 新参数 `readyForReviewOnly` + SQL 双 NOT EXISTS | Task 1 |
+| 无工单算完成;质检 dpz/ypz;生产 dpz/jxz | Task 1 Step 3 |
+| 复选框替换质检下拉;默认不勾选 | Task 2 |
+| 仅待审核;重置/切回恢复不勾选 | Task 2 Step 3 |
+| 本页停用 `qcWorkOrderCompleted` | Task 2 Step 3 |
+| 分页 total 正确 | Task 1(过滤在 COUNT 前) |
+| 保留旧 qc 参数兼容 | Task 1(不删旧逻辑) |
+
+## Self-Review Notes
+
+- 无 TBD/占位步骤;normalize 对 `false` 返回 `None`,与 spec「仅 true 启用」一致。
+- 复用现有 `_qc_work_order_exists_for_review_sample(..., unfinished_only=True)`,避免重复质检 EXISTS。
+- `watchDebounced` 会因 `showAllPending` 变化自动刷新;`handleReset`/`handleTabChange` 仍显式 `loadData`,与现页一致。

+ 171 - 0
docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md

@@ -0,0 +1,171 @@
+# 入库放行审核 · 待审核默认仅展示双工单侧就绪
+
+> 日期:2026-07-29  
+> 状态:已实现  
+> 模块:细胞存储 / 质检 → 入库放行审核  
+> 关联:  
+> - `docs/superpowers/specs/2026-07-16-storage-release-review-tabs-design.md`  
+> - `docs/superpowers/specs/2026-07-16-storage-release-review-qc-completed-filter-design.md`(本需求**替换**其待审核筛选项交互与默认过滤语义)  
+> 界面模式:P22(审核列表)  
+> 参考页:`web-admin/src/views/quality-inspection/storage-release-review/index.vue`  
+> Demo:`docs/superpowers/demos/2026-07-29-storage-release-pending-both-completed-filter-demo.html`
+
+## 1. 背景与目标
+
+### 现状
+
+「待审核」Tab 已有「质检工单是否完成」下拉(是 / 否 / 全部,默认「是」),仅按质检完成度过滤;**不约束生产工单**。且旧语义下「无质检工单」视为未完成,默认被隐藏。
+
+### 目标
+
+1. 待审核默认只展示:质检侧与生产侧均判定为「已完成」的记录。
+2. 用复选框「展示全部待审核」**替换**「质检工单是否完成」下拉;**默认不勾选**;勾选后展示全部待审核。
+3. 分页与 `total` 与过滤条件一致。
+
+### 范围外
+
+- 不改审核弹窗、查看弹窗、入库业务规则
+- 不在后端强制「工单未完成禁止审核」(仅列表默认隐藏;勾选后仍可审核)
+- 不改「已审核」Tab 的筛选与列表语义
+- 不新增菜单/路由;不改表结构
+- 不在本需求中删除后端 `qcWorkOrderCompleted` 参数(本页停用即可;清理可另开)
+
+## 2. 需求确认记录
+
+| 维度 | 决定 |
+|------|------|
+| UI | 复选框替换质检下拉 |
+| 无质检工单 | 质检侧算**已完成** |
+| 无生产工单 | 生产侧算**已完成** |
+| 质检未完成状态 | 存在 `dpz` / `ypz` |
+| 生产未完成状态 | 存在 `dpz` / `jxz` |
+| 作废 `yzf` | 两侧均算已完成(与 `ywc` 同等) |
+| 作用 Tab | **仅「待审核」** |
+| 默认 | 复选框**不勾选** |
+| 重置 | 恢复不勾选 |
+| 从已审核切回待审核 | 恢复不勾选 |
+| 实现路径 | 方案 1:新查询参数 + 后端 SQL 过滤 |
+
+## 3. 「侧完成」语义
+
+在租户 `tenant_no` 下,关联键为审核行的 `sample_code`;工单条件:`del_flag = 0`。
+
+| 侧 | 已完成 | 未完成 |
+|----|--------|--------|
+| **质检** | **无**有效质检工单;或有工单且**不存在** `work_order_status IN ('dpz','ypz')`(允许全为 `ywc` / `yzf`) | 存在至少一条 `dpz` / `ypz` |
+| **生产** | **无**有效生产工单;或有工单且**不存在** `work_order_status IN ('dpz','jxz')`(允许全为 `ywc` / `yzf`) | 存在至少一条 `dpz` / `jxz` |
+
+默认列表(复选框未勾选)= 质检已完成 **AND** 生产已完成。
+
+说明:
+
+- 仅作废工单(全是 `yzf`、无未完成状态码)→ 该侧已完成
+- 零工单 → 该侧已完成
+- 软删工单(`del_flag=1`)不参与统计
+- 空 `sample_code`:两侧均按「无工单」→ 两侧已完成 → **默认可见**
+- 生产侧按 `sample_code` 关联(与详情中生产信息主查找路径一致);不额外按 `work_order_type` 收窄(与现有详情一致)
+
+### 相对旧质检过滤的差异
+
+| 场景 | 旧(质检下拉「是」) | 新(默认未勾选) |
+|------|----------------------|------------------|
+| 无质检工单 | 隐藏 | **显示**(若生产侧也完成) |
+| 质检完成、生产未完成 | 显示 | **隐藏** |
+
+## 4. UI 与交互
+
+### 4.1 筛选区(仅待审核可见)
+
+- **移除**「质检工单是否完成」`el-select` 及选项数据。
+- **新增**复选框,主文案:**展示全部待审核**。
+- 可选辅助说明:默认不勾选时仅展示质检与生产均为完成的记录;勾选后取消该限制。
+- 主筛选项栅格恢复为 4 列(关键词、协议类别、审核结论、入库状态);复选框放在筛选区下方操作行左侧(与 Demo 一致),主按钮仍在右侧。
+
+「已审核」Tab:**不渲染**该复选框。
+
+### 4.2 默认与重置
+
+| 场景 | 复选框 |
+|------|--------|
+| 首次进入页面(默认待审核) | 不勾选 |
+| 点击「重置」(待审核) | 不勾选(其它筛选项清空逻辑不变) |
+| 从「已审核」切回「待审核」 | **恢复不勾选**,页码回 1,清空表格勾选,重新请求 |
+| 待审核内切换其它筛选项 / 搜索 | 保留当前勾选状态 |
+
+### 4.3 请求行为
+
+- 待审核 + 未勾选 → 传 `readyForReviewOnly=true`
+- 待审核 + 已勾选 → **不传**该参数
+- 已审核 → **不传**该参数
+- 本页**不再传** `qcWorkOrderCompleted`
+
+## 5. API 与数据流
+
+### 5.1 接口
+
+沿用 `GET /web/admin/storage-release-reviews`,新增可选查询参数:
+
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| `readyForReviewOnly` | `true` \| `false`(查询字符串布尔) | `true` = 仅双工单侧均「已完成」;缺省、`false` 或不识别 → 不按此规则过滤 |
+
+其余参数不变:`page`、`pageSize`、`keyword`、`receptionSource`、`reviewConclusion`、`storageStatus`、`reviewPhase`。`qcWorkOrderCompleted` 后端可暂时保留,本页不使用。
+
+### 5.2 后端过滤(叠在现有条件之后)
+
+当 `readyForReviewOnly` 解析为 `true` 时,审核行须同时满足:
+
+**质检侧已完成:**
+
+```text
+NOT EXISTS (
+  同租户、del_flag=0、sample_code 匹配审核行、
+  work_order_status IN ('dpz', 'ypz') 的质检工单
+)
+```
+
+**生产侧已完成:**
+
+```text
+NOT EXISTS (
+  同租户、del_flag=0、sample_code 匹配审核行、
+  work_order_status IN ('dpz', 'jxz') 的生产工单
+)
+```
+
+过滤必须在 **COUNT 与分页查询之前**应用,保证 `total` 正确。
+
+### 5.3 实现提示
+
+- 优先用 SQLAlchemy `exists()` 子查询,避免先拉全量再过滤。
+- 可抽 `normalize_ready_for_review_only(raw) -> Optional[bool]`,与 `normalize_review_phase` / `normalize_qc_work_order_completed` 风格一致;仅明确为 true 时启用过滤。
+- `sample_code` 关联若存在 collation 差异,质检侧继续用现有 `eq_unicode`;生产侧同样处理。
+
+## 6. 改动面
+
+| 层 | 路径 | 变更 |
+|----|------|------|
+| Backend API | `backend/app/api/storage_release_review.py` | Query 增加 `readyForReviewOnly` |
+| Backend CRUD | `backend/app/crud/storage_release_review.py` | `list_reviews` 双 `NOT EXISTS`;normalize 辅助 |
+| Frontend API | `web-admin/src/api/storage-release-review.ts` | 列表 params 增加 `readyForReviewOnly?: boolean` |
+| Frontend 页 | `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | 复选框替换下拉;默认/重置/切 Tab;请求传参;栅格回 4 列 |
+
+不改:审核/查看弹窗、菜单路由、表结构、已审核列表逻辑(除切回待审核时重置复选框)。
+
+## 7. 测试要点
+
+1. 待审核默认:存在质检 `dpz`/`ypz` 或生产 `dpz`/`jxz` → 不出现;两侧均无未完成(含双无工单)→ 出现。
+2. 勾选「展示全部待审核」→ 与仅 `reviewPhase=pending` 的待审核全集一致。
+3. 仅 `ywc`/`yzf`、无未完成状态码 → 默认可见。
+4. 无质检有生产已完成 → 默认可见;有质检完成但生产 `jxz` → 默认隐藏。
+5. 已审核:无复选框;结果不受该参数影响。
+6. 重置、从已审核切回待审核 → 复选框不勾选。
+7. 分页 `total` 与当前过滤一致。
+
+## 8. 方案取舍(记录)
+
+| 方案 | 结论 |
+|------|------|
+| 1. 新参数 `readyForReviewOnly` + SQL 过滤 | **采用** |
+| 2. 改写并扩展现有 `qcWorkOrderCompleted` + 生产参数 | 拒绝(破坏旧「无工单」语义;UI 单开关却双布尔) |
+| 3. 前端拉全量后过滤 | 拒绝(分页/total 错误) |