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>
This commit is contained in:
zlwuandCursor committed 2026-07-29 11:25:04 +08:00
1 parent 63074c0032
commit 38f18f1926
3 files changed
+1201

No files matched your search

@@ -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>
@@ -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`,与现页一致。
@@ -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 错误) |