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:
1 parent
63074c0032
commit
38f18f1926
3 files changed
+1201
No files matched your search
+596
@@ -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 错误) |
|
||||
Reference in new issue
Block a user