Files
zkxw/docs/storage-release-review-batch-dialog-demos/index.html
T
2026-08-19 10:45:07 +08:00

1497 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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;
--text-label: #334155;
--border: #d9e8e2;
--border-soft: #e2e8f0;
--bg-page: #f1f5f9;
--surface: #ffffff;
--table-head: #f8fafc;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--mask: rgba(15, 23, 42, 0.35);
--warn: #d97706;
--warn-bg: #fffbeb;
--danger: #dc2626;
--danger-bg: #fef2f2;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg-page);
}
button {
font-family: inherit;
}
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.banner {
position: sticky;
top: 0;
z-index: 20;
background: var(--surface);
border-bottom: 1px solid var(--border-soft);
padding: 14px 24px 16px;
box-shadow: var(--shadow);
}
.banner h1 {
margin: 0;
font-size: 16px;
font-weight: 700;
}
.banner p {
margin: 6px 0 0;
font-size: 13px;
color: var(--text-muted);
max-width: 960px;
}
.banner p strong {
color: var(--primary-dark);
font-weight: 600;
}
.scheme-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 12px;
}
.scheme-row > span {
font-size: 12px;
color: var(--text-muted);
margin-right: 4px;
}
.scheme-btn {
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.scheme-btn:hover {
background: #f8fafc;
}
.scheme-btn.is-active {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.scheme-btn.is-rec::after {
content: '推荐';
margin-left: 6px;
font-size: 10px;
font-weight: 700;
padding: 1px 5px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.22);
}
.scheme-btn:not(.is-active).is-rec::after {
background: var(--primary-bg);
color: var(--primary-dark);
}
.note {
margin-top: 10px;
font-size: 12px;
color: var(--text-muted);
padding: 8px 10px;
background: #f8fafc;
border: 1px solid var(--border-soft);
border-radius: 8px;
max-width: 960px;
}
.stage {
flex: 1;
position: relative;
min-height: 0;
}
.backdrop {
position: absolute;
inset: 0;
background: var(--mask);
display: flex;
align-items: flex-start;
justify-content: center;
padding: 16px 16px 24px;
overflow: auto;
}
.dialog {
display: none;
flex-direction: column;
background: var(--surface);
border-radius: var(--radius);
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.18);
max-height: calc(100vh - 140px);
overflow: hidden;
width: min(1280px, 100%);
}
.dialog.is-show {
display: flex;
}
.dialog--unified {
width: min(640px, 100%);
}
.dialog--table {
width: min(980px, 100%);
}
.dialog-head {
flex-shrink: 0;
padding: 16px 22px 14px;
border-bottom: 1px solid var(--border-soft);
}
.dialog-head__row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.dialog-head h2 {
margin: 0;
font-size: 16px;
font-weight: 700;
}
.dialog-head .sub {
margin: 4px 0 0;
font-size: 12px;
color: var(--text-muted);
}
.icon-btn {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon-btn:hover {
background: #f1f5f9;
color: var(--text);
}
.mode-tabs {
display: inline-flex;
gap: 4px;
margin-top: 12px;
padding: 3px;
background: #f1f5f9;
border-radius: 8px;
}
.mode-tab {
height: 28px;
padding: 0 12px;
border: none;
border-radius: 6px;
background: transparent;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
cursor: pointer;
}
.mode-tab.is-active {
background: #fff;
color: var(--primary-dark);
box-shadow: var(--shadow);
}
.dialog-body {
flex: 1;
min-height: 0;
overflow: hidden;
}
/* —— Scheme A: 队列逐条 —— */
.queue-layout {
display: grid;
grid-template-columns: 240px minmax(0, 1.55fr) minmax(300px, 0.9fr);
height: 100%;
min-height: 520px;
}
.queue-pane {
border-right: 1px solid var(--border-soft);
background: #fcfdfd;
display: flex;
flex-direction: column;
min-height: 0;
}
.queue-pane__h {
flex-shrink: 0;
padding: 12px 14px;
border-bottom: 1px solid var(--border-soft);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--text-muted);
display: flex;
justify-content: space-between;
align-items: center;
}
.queue-pane__h em {
font-style: normal;
color: var(--primary-dark);
font-weight: 700;
}
.queue-list {
flex: 1;
overflow: auto;
padding: 8px;
}
.queue-item {
width: 100%;
text-align: left;
border: 1px solid transparent;
background: transparent;
border-radius: 8px;
padding: 10px 10px;
cursor: pointer;
margin-bottom: 4px;
}
.queue-item:hover {
background: #f8fafc;
}
.queue-item.is-active {
background: var(--primary-bg);
border-color: #a7f3d0;
}
.queue-item.is-done {
opacity: 0.72;
}
.queue-item__code {
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.queue-item__meta {
margin-top: 2px;
font-size: 11px;
color: var(--text-muted);
display: flex;
justify-content: space-between;
gap: 8px;
}
.status-dot {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 600;
}
.status-dot::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: #94a3b8;
}
.status-dot--pending::before {
background: var(--warn);
}
.status-dot--ok::before {
background: var(--primary);
}
.status-dot--bad::before {
background: var(--danger);
}
.evidence-pane {
min-width: 0;
overflow: auto;
padding: 14px 16px 18px;
border-right: 1px solid var(--border-soft);
background: #fcfdfd;
}
.form-pane {
min-width: 0;
overflow: auto;
padding: 14px 18px 18px;
background: var(--surface);
display: flex;
flex-direction: column;
}
.hero {
padding: 10px 12px;
background: var(--table-head);
border: 1px solid var(--border-soft);
border-radius: 8px;
margin-bottom: 12px;
}
.hero__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
}
.meta-row {
display: flex;
gap: 8px;
font-size: 13px;
min-width: 0;
}
.meta-row .k {
color: var(--text-label);
flex-shrink: 0;
}
.meta-row .v {
font-weight: 600;
overflow-wrap: anywhere;
}
.verdict {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 4px;
height: 22px;
padding: 0 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
background: #f1f5f9;
color: var(--text-muted);
}
.chip--ok {
background: var(--primary-bg);
color: #047857;
}
.chip--warn {
background: var(--warn-bg);
color: var(--warn);
}
.block {
border: 1px solid var(--border-soft);
border-radius: 8px;
background: #fff;
margin-bottom: 8px;
overflow: hidden;
}
.block > summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 12px;
font-size: 13px;
font-weight: 700;
}
.block > summary::-webkit-details-marker {
display: none;
}
.block-body {
padding: 0 12px 12px;
border-top: 1px solid #f1f5f9;
}
.kv-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
padding-top: 10px;
}
.kv dt {
margin: 0;
font-size: 12px;
color: var(--text-label);
}
.kv dd {
margin: 2px 0 0;
font-size: 13px;
}
.tag {
display: inline-flex;
height: 20px;
padding: 0 7px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
align-items: center;
}
.tag--success {
background: var(--primary-bg);
color: #047857;
}
.tag--info {
background: #f1f5f9;
color: var(--text-muted);
}
.pane-title {
margin: 0 0 10px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--text-muted);
}
.progress-bar {
margin-bottom: 12px;
padding: 8px 10px;
background: var(--primary-bg);
border: 1px solid #a7f3d0;
border-radius: 8px;
font-size: 12px;
color: #047857;
font-weight: 600;
}
.field {
margin-bottom: 14px;
}
.field label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
}
.radios {
display: flex;
gap: 16px;
height: 36px;
align-items: center;
}
.radios label {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
cursor: pointer;
font-weight: 400;
}
textarea {
width: 100%;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
font: inherit;
font-size: 13px;
resize: vertical;
min-height: 72px;
}
textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
}
.upload-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: #047857;
font-size: 12px;
font-weight: 600;
background: #f8fffb;
cursor: pointer;
}
.apply-box {
margin-top: auto;
padding: 10px 12px;
background: #f8fafc;
border: 1px solid var(--border-soft);
border-radius: 8px;
font-size: 12px;
color: var(--text-label);
}
.apply-box label {
display: flex;
align-items: flex-start;
gap: 8px;
cursor: pointer;
}
.apply-box input {
margin-top: 2px;
accent-color: var(--primary);
}
.apply-box .hint {
margin-top: 4px;
color: var(--text-muted);
font-size: 11px;
padding-left: 22px;
}
.dialog-foot {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 12px 18px;
border-top: 1px solid var(--border-soft);
background: #fff;
}
.dialog-foot .spacer {
margin-right: auto;
font-size: 12px;
color: var(--text-muted);
}
.btn {
height: 36px;
padding: 0 14px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover {
background: #f8fafc;
}
.btn--primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn--primary:hover {
background: var(--primary-dark);
}
.btn--ghost-danger {
color: var(--danger);
border-color: #fecaca;
}
.btn--ghost-danger:hover {
background: var(--danger-bg);
}
/* —— Scheme B: 统一结论(现状增强) —— */
.unified-body {
padding: 18px 22px;
overflow: auto;
}
.target-hint {
margin: 0 0 14px;
padding: 10px 12px;
background: var(--warn-bg);
border: 1px solid #fde68a;
border-radius: 8px;
font-size: 13px;
color: #92400e;
}
.target-hint strong {
font-weight: 700;
}
.mini-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
margin-bottom: 16px;
}
.mini-table th,
.mini-table td {
padding: 8px 10px;
border-bottom: 1px solid var(--border-soft);
text-align: left;
}
.mini-table th {
background: var(--table-head);
font-weight: 600;
color: var(--text-label);
}
.mini-table tr:last-child td {
border-bottom: none;
}
/* —— Scheme C: 表格 + 行内 —— */
.table-body {
padding: 0;
overflow: auto;
max-height: calc(100vh - 280px);
}
.batch-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
padding: 12px 18px;
border-bottom: 1px solid var(--border-soft);
background: #f8fffb;
}
.batch-bar .label {
font-size: 12px;
font-weight: 700;
color: var(--text-muted);
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th,
.data-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-soft);
text-align: left;
vertical-align: middle;
}
.data-table th {
position: sticky;
top: 0;
background: var(--table-head);
font-size: 12px;
font-weight: 600;
color: var(--text-label);
z-index: 1;
}
.data-table .mono {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.row-actions {
display: flex;
gap: 6px;
}
.link-btn {
border: none;
background: none;
color: var(--primary-dark);
font-size: 12px;
font-weight: 600;
cursor: pointer;
padding: 0;
}
.link-btn:hover {
text-decoration: underline;
}
.expand-panel {
display: none;
background: #fcfdfd;
}
.expand-panel.is-open {
display: table-row;
}
.expand-panel td {
padding: 0 12px 14px 36px;
}
.expand-inner {
display: grid;
grid-template-columns: 1fr 280px;
gap: 12px;
padding: 12px;
border: 1px solid var(--border-soft);
border-radius: 8px;
background: #fff;
}
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #1e293b;
color: #fff;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
opacity: 0;
pointer-events: none;
transition:
opacity 0.2s,
transform 0.2s;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
@media (max-width: 1100px) {
.queue-layout {
grid-template-columns: 200px minmax(0, 1fr);
}
.form-pane {
grid-column: 1 / -1;
border-top: 1px solid var(--border-soft);
max-height: 320px;
}
.expand-inner {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="page">
<header class="banner">
<h1>入库放行审核 · 批量审核弹窗 Demo</h1>
<p>
列表勾选多条后点「批量审核」。目标:既可<strong>逐条核对证据后审核</strong>,也可<strong>对剩余项套用同一结论快速批量提交</strong>。
下方可切换三种方案对比(Fresh Mint)。
</p>
<div class="scheme-row">
<span>方案</span>
<button type="button" class="scheme-btn is-rec is-active" data-scheme="queue">
A · 队列逐条 + 套用剩余
</button>
<button type="button" class="scheme-btn" data-scheme="unified">
B · 统一结论(现状)
</button>
<button type="button" class="scheme-btn" data-scheme="table">
C · 表格行内审核
</button>
</div>
<div class="note" id="schemeNote">
<strong>A 推荐:</strong>左侧待审队列 → 中间复用单条审核的证据区 →
右侧结论表单。默认「提交本条并下一条」;勾选「将结论套用到剩余未审项」即可一键批量。与现有单条审核体验连续。
</div>
</header>
<div class="stage">
<div class="backdrop">
<!-- ========== Scheme A ========== -->
<div class="dialog is-show" id="dlg-queue" role="dialog" aria-label="批量审核-队列">
<div class="dialog-head">
<div class="dialog-head__row">
<div>
<h2>批量审核(5 条)</h2>
<p class="sub">逐条核对接收 / 质检 / 生产证据后给出结论;也可对剩余项统一套用。</p>
</div>
<button type="button" class="icon-btn" aria-label="关闭" title="关闭">✕</button>
</div>
<div class="mode-tabs" role="tablist">
<button type="button" class="mode-tab is-active" data-mode="per">逐条核对</button>
<button type="button" class="mode-tab" data-mode="batch">统一提交</button>
</div>
</div>
<div class="dialog-body" id="queue-per-body">
<div class="queue-layout">
<aside class="queue-pane">
<div class="queue-pane__h">
<span>待审队列</span>
<em id="queueProgress">1 / 5</em>
</div>
<div class="queue-list" id="queueList"></div>
</aside>
<section class="evidence-pane">
<div class="hero">
<div class="hero__grid">
<div class="meta-row">
<span class="k">实验室入库编码</span>
<span class="v" id="evLab">RK-2026-0819-003</span>
</div>
<div class="meta-row">
<span class="k">样本编码</span>
<span class="v" id="evCode">YP-CS-20260819-001</span>
</div>
</div>
<div class="verdict">
<span class="chip chip--ok">样本接收:接收</span>
<span class="chip chip--ok">质检工单:已完成</span>
<span class="chip chip--ok">生产工单:已完成</span>
</div>
</div>
<details class="block" open>
<summary><span>接收信息</span><span>▾</span></summary>
<div class="block-body">
<dl class="kv-grid">
<div class="kv">
<dt>接收人</dt>
<dd>张三</dd>
</div>
<div class="kv">
<dt>接收时间</dt>
<dd>2026-08-18 14:22</dd>
</div>
<div class="kv">
<dt>样本类型</dt>
<dd>脐带血</dd>
</div>
<div class="kv">
<dt>接收结论</dt>
<dd><span class="tag tag--success">接收</span></dd>
</div>
</dl>
</div>
</details>
<details class="block" open>
<summary><span>质检信息</span><span>▾</span></summary>
<div class="block-body">
<dl class="kv-grid">
<div class="kv">
<dt>工单状态</dt>
<dd><span class="tag tag--success">已完成</span></dd>
</div>
<div class="kv">
<dt>门禁结论</dt>
<dd><span class="tag tag--success">通过</span></dd>
</div>
<div class="kv">
<dt>过程质检</dt>
<dd>2 项已完成</dd>
</div>
<div class="kv">
<dt>异常项</dt>
<dd>无</dd>
</div>
</dl>
</div>
</details>
<details class="block" open>
<summary><span>生产信息</span><span>▾</span></summary>
<div class="block-body">
<dl class="kv-grid">
<div class="kv">
<dt>工单状态</dt>
<dd><span class="tag tag--success">已完成</span></dd>
</div>
<div class="kv">
<dt>产出数量</dt>
<dd>2 袋</dd>
</div>
<div class="kv">
<dt>废料</dt>
<dd>0</dd>
</div>
<div class="kv">
<dt>执行人</dt>
<dd>李四</dd>
</div>
</dl>
</div>
</details>
</section>
<section class="form-pane">
<p class="pane-title">本条审核</p>
<div class="progress-bar" id="formProgress">正在审核第 1 条 · 还剩 4 条未审</div>
<div class="field">
<label>审核结论</label>
<div class="radios">
<label><input type="radio" name="q-conc" value="tg" checked /> 通过</label>
<label><input type="radio" name="q-conc" value="btg" /> 不通过</label>
</div>
</div>
<div class="field">
<label>备注</label>
<textarea placeholder="可选" maxlength="1000"></textarea>
</div>
<div class="field">
<label>附件</label>
<div class="upload-row">
<div class="upload-card">
<span style="font-size: 20px">+</span>
<span>上传附件</span>
</div>
</div>
</div>
<div class="apply-box">
<label>
<input type="checkbox" id="applyRest" />
<span
>将当前结论 / 备注 / 附件<strong>套用到剩余未审项</strong>并一次性提交</span
>
</label>
<div class="hint">
勾选后底栏主按钮变为「批量提交剩余」;适合证据已预览、结论一致的场景。
</div>
</div>
</section>
</div>
</div>
<div class="dialog-body" id="queue-batch-body" style="display: none">
<div class="unified-body">
<p class="target-hint">
将对已选 <strong>5</strong> 条记录提交<strong>相同</strong>审核结果(不逐条展示证据)。
</p>
<table class="mini-table">
<thead>
<tr>
<th>样本编码</th>
<th>入库编码</th>
<th>样本名称</th>
</tr>
</thead>
<tbody id="unifiedMiniTbody"></tbody>
</table>
<div class="field">
<label>审核结论</label>
<div class="radios">
<label><input type="radio" name="qb-conc" value="tg" checked /> 通过</label>
<label><input type="radio" name="qb-conc" value="btg" /> 不通过</label>
</div>
</div>
<div class="field">
<label>备注</label>
<textarea placeholder="可选,将写入全部已选记录"></textarea>
</div>
<div class="field">
<label>附件</label>
<div class="upload-row">
<div class="upload-card">
<span style="font-size: 20px">+</span>
<span>上传附件</span>
</div>
</div>
</div>
</div>
</div>
<div class="dialog-foot" id="queueFoot">
<span class="spacer" id="queueFootHint">建议:有疑义的样本逐条看证据;同质样本可套用剩余。</span>
<button type="button" class="btn">取消</button>
<button type="button" class="btn btn--ghost-danger" id="btnSkip">跳过本条</button>
<button type="button" class="btn btn--primary" id="btnQueuePrimary">
✓ 提交本条并下一条
</button>
</div>
</div>
<!-- ========== Scheme B ========== -->
<div class="dialog dialog--unified" id="dlg-unified" role="dialog" aria-label="批量审核-统一">
<div class="dialog-head">
<div class="dialog-head__row">
<div>
<h2>批量审核(5 条)</h2>
<p class="sub">与当前线上一致:一份结论写入全部已选记录。</p>
</div>
<button type="button" class="icon-btn" aria-label="关闭">✕</button>
</div>
</div>
<div class="dialog-body" style="overflow: auto">
<div class="unified-body">
<p class="target-hint">
将对已选 <strong>5</strong> 条记录提交相同审核结果。
</p>
<table class="mini-table">
<thead>
<tr>
<th>#</th>
<th>样本编码</th>
<th>入库编码</th>
<th>名称</th>
</tr>
</thead>
<tbody id="schemeBTbody"></tbody>
</table>
<div class="field">
<label>审核结论</label>
<div class="radios">
<label><input type="radio" name="b-conc" value="tg" checked /> 通过</label>
<label><input type="radio" name="b-conc" value="btg" /> 不通过</label>
</div>
</div>
<div class="field">
<label>备注</label>
<textarea placeholder="可选"></textarea>
</div>
<div class="field">
<label>附件</label>
<div class="upload-row">
<div class="upload-card">
<span style="font-size: 20px">+</span>
<span>上传附件</span>
</div>
</div>
</div>
</div>
</div>
<div class="dialog-foot">
<button type="button" class="btn">取消</button>
<button type="button" class="btn btn--primary" data-toast="已批量提交 5 条(Demo)">
✓ 确定
</button>
</div>
</div>
<!-- ========== Scheme C ========== -->
<div class="dialog dialog--table" id="dlg-table" role="dialog" aria-label="批量审核-表格">
<div class="dialog-head">
<div class="dialog-head__row">
<div>
<h2>批量审核(5 条)</h2>
<p class="sub">表格内逐行结论;顶栏可对勾选行统一填结论后批量提交。</p>
</div>
<button type="button" class="icon-btn" aria-label="关闭">✕</button>
</div>
</div>
<div class="dialog-body table-body">
<div class="batch-bar">
<span class="label">批量填入</span>
<div class="radios" style="height: auto">
<label><input type="radio" name="c-bulk" value="tg" checked /> 通过</label>
<label><input type="radio" name="c-bulk" value="btg" /> 不通过</label>
</div>
<button type="button" class="btn" id="btnFillRows" style="height: 32px">
填入已勾选行
</button>
<span style="font-size: 12px; color: var(--text-muted)"
>也可点「展开」看证据后单独改结论</span
>
</div>
<table class="data-table">
<thead>
<tr>
<th style="width: 40px"><input type="checkbox" id="checkAll" checked /></th>
<th>样本编码</th>
<th>入库编码</th>
<th>质检</th>
<th>生产</th>
<th>结论</th>
<th style="width: 100px">操作</th>
</tr>
</thead>
<tbody id="schemeCTbody"></tbody>
</table>
</div>
<div class="dialog-foot">
<span class="spacer">未填结论的行提交时将提示补全</span>
<button type="button" class="btn">取消</button>
<button type="button" class="btn btn--primary" data-toast="已按行结论提交(Demo)">
✓ 提交全部
</button>
</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const SAMPLES = [
{
id: '1',
code: 'YP-CS-20260819-001',
lab: 'RK-2026-0819-003',
name: '脐带血样本 A',
qc: '已完成',
prod: '已完成'
},
{
id: '2',
code: 'YP-CS-20260819-002',
lab: 'RK-2026-0819-004',
name: '脐带血样本 B',
qc: '已完成',
prod: '已完成'
},
{
id: '3',
code: 'YP-CS-20260818-015',
lab: 'RK-2026-0818-021',
name: '胎盘组织 C',
qc: '已完成',
prod: '已完成'
},
{
id: '4',
code: 'YP-RD-20260817-008',
lab: 'RK-2026-0817-011',
name: '捐赠样本 D',
qc: '已完成',
prod: '已完成'
},
{
id: '5',
code: 'YP-CS-20260816-022',
lab: 'RK-2026-0816-030',
name: '脐带血样本 E',
qc: '已完成',
prod: '已完成'
}
]
const notes = {
queue:
'<strong>A 推荐:</strong>左侧待审队列 → 中间复用单条审核的证据区 → 右侧结论表单。默认「提交本条并下一条」;勾选「将结论套用到剩余未审项」即可一键批量。与现有单条审核体验连续。',
unified:
'<strong>B 现状:</strong>窄弹窗 + 统一结论。效率最高,但看不到各样本证据,误审风险大;适合「已在列表/查看里确认过、结论必然一致」的场景。建议在清单上增加已选摘要表(本 Demo 已补)。',
table:
'<strong>C 表格行内:</strong>一屏看到全部已选;顶栏批量填结论,行内可展开证据。适合条数不多(≤10)且需要「大部分相同、个别例外」的微调。证据区复用成本较高。'
}
let activeIdx = 0
const decisions = SAMPLES.map(() => null) // null | 'tg' | 'btg'
function toast(msg) {
const el = document.getElementById('toast')
el.textContent = msg
el.classList.add('is-show')
clearTimeout(toast._t)
toast._t = setTimeout(() => el.classList.remove('is-show'), 2200)
}
function doneCount() {
return decisions.filter(Boolean).length
}
function renderQueue() {
const list = document.getElementById('queueList')
list.innerHTML = SAMPLES.map((s, i) => {
const d = decisions[i]
const st =
d === 'tg'
? 'status-dot--ok">已通过'
: d === 'btg'
? 'status-dot--bad">不通过'
: 'status-dot--pending">待审'
return `<button type="button" class="queue-item ${i === activeIdx ? 'is-active' : ''} ${d ? 'is-done' : ''}" data-idx="${i}">
<div class="queue-item__code">${s.code}</div>
<div class="queue-item__meta">
<span>${s.lab}</span>
<span class="status-dot ${st}</span>
</div>
</button>`
}).join('')
document.getElementById('queueProgress').textContent = `${activeIdx + 1} / ${SAMPLES.length}`
const left = SAMPLES.length - doneCount()
document.getElementById('formProgress').textContent =
`正在审核第 ${activeIdx + 1} 条 · 还剩 ${Math.max(left - (decisions[activeIdx] ? 0 : 1), 0)} 条未审(已完成 ${doneCount()})`
const s = SAMPLES[activeIdx]
document.getElementById('evLab').textContent = s.lab
document.getElementById('evCode').textContent = s.code
}
function fillMiniTables() {
const rows = SAMPLES.map(
(s, i) =>
`<tr><td>${s.code}</td><td>${s.lab}</td><td>${s.name}</td></tr>`
).join('')
document.getElementById('unifiedMiniTbody').innerHTML = rows
document.getElementById('schemeBTbody').innerHTML = SAMPLES.map(
(s, i) =>
`<tr><td>${i + 1}</td><td>${s.code}</td><td>${s.lab}</td><td>${s.name}</td></tr>`
).join('')
}
function renderSchemeC() {
const tb = document.getElementById('schemeCTbody')
tb.innerHTML = SAMPLES.map((s, i) => {
return `
<tr data-row="${i}">
<td><input type="checkbox" class="row-check" checked /></td>
<td class="mono">${s.code}</td>
<td>${s.lab}</td>
<td><span class="tag tag--success">${s.qc}</span></td>
<td><span class="tag tag--success">${s.prod}</span></td>
<td>
<select class="row-conc" style="height:32px;border:1px solid var(--border);border-radius:6px;padding:0 8px;font:inherit;font-size:12px">
<option value="">未填</option>
<option value="tg">通过</option>
<option value="btg">不通过</option>
</select>
</td>
<td class="row-actions">
<button type="button" class="link-btn expand-btn" data-idx="${i}">展开</button>
</td>
</tr>
<tr class="expand-panel" id="exp-${i}">
<td colspan="7">
<div class="expand-inner">
<div>
<div class="pane-title">证据摘要 · ${s.code}</div>
<div class="verdict" style="margin:0 0 8px">
<span class="chip chip--ok">接收:接收</span>
<span class="chip chip--ok">质检:${s.qc}</span>
<span class="chip chip--ok">生产:${s.prod}</span>
</div>
<p style="margin:0;font-size:12px;color:var(--text-muted)">此处可嵌入与单条审核相同的折叠证据块(接收 / 质检 / 生产)。Demo 仅示意布局。</p>
</div>
<div>
<div class="field" style="margin:0">
<label>行备注</label>
<textarea rows="3" placeholder="仅本条"></textarea>
</div>
</div>
</div>
</td>
</tr>`
}).join('')
}
function showScheme(name) {
document.querySelectorAll('.scheme-btn').forEach((b) => {
b.classList.toggle('is-active', b.dataset.scheme === name)
})
document.getElementById('schemeNote').innerHTML = notes[name]
document.getElementById('dlg-queue').classList.toggle('is-show', name === 'queue')
document.getElementById('dlg-unified').classList.toggle('is-show', name === 'unified')
document.getElementById('dlg-table').classList.toggle('is-show', name === 'table')
}
function updatePrimaryBtn() {
const apply = document.getElementById('applyRest').checked
const btn = document.getElementById('btnQueuePrimary')
const left = SAMPLES.length - doneCount()
if (apply) {
btn.textContent = `✓ 批量提交剩余(${left || SAMPLES.length})`
} else {
btn.textContent =
activeIdx >= SAMPLES.length - 1 && decisions.every(Boolean)
? '✓ 完成并关闭'
: '✓ 提交本条并下一条'
}
}
document.querySelectorAll('.scheme-btn').forEach((btn) => {
btn.addEventListener('click', () => showScheme(btn.dataset.scheme))
})
document.querySelectorAll('.mode-tab').forEach((tab) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.mode-tab').forEach((t) => t.classList.remove('is-active'))
tab.classList.add('is-active')
const per = tab.dataset.mode === 'per'
document.getElementById('queue-per-body').style.display = per ? '' : 'none'
document.getElementById('queue-batch-body').style.display = per ? 'none' : ''
const foot = document.getElementById('queueFoot')
document.getElementById('btnSkip').style.display = per ? '' : 'none'
document.getElementById('queueFootHint').textContent = per
? '建议:有疑义的样本逐条看证据;同质样本可套用剩余。'
: '统一模式:一份结论写入全部 5 条,不等同于逐条核对。'
document.getElementById('btnQueuePrimary').textContent = per
? '✓ 提交本条并下一条'
: '✓ 统一提交 5 条'
if (!per) document.getElementById('applyRest').checked = false
})
})
document.getElementById('queueList').addEventListener('click', (e) => {
const item = e.target.closest('.queue-item')
if (!item) return
activeIdx = Number(item.dataset.idx)
renderQueue()
updatePrimaryBtn()
})
document.getElementById('applyRest').addEventListener('change', updatePrimaryBtn)
document.getElementById('btnSkip').addEventListener('click', () => {
if (activeIdx < SAMPLES.length - 1) {
activeIdx += 1
renderQueue()
updatePrimaryBtn()
toast('已跳过,进入下一条')
} else {
toast('已是最后一条')
}
})
document.getElementById('btnQueuePrimary').addEventListener('click', () => {
const modePer = document.querySelector('.mode-tab.is-active').dataset.mode === 'per'
if (!modePer) {
toast('已统一提交 5 条(Demo)')
return
}
const apply = document.getElementById('applyRest').checked
const conc = document.querySelector('input[name="q-conc"]:checked').value
if (apply) {
for (let i = 0; i < SAMPLES.length; i++) {
if (!decisions[i]) decisions[i] = conc
}
renderQueue()
updatePrimaryBtn()
toast(`已将结论套用并提交剩余项(Demo)`)
return
}
decisions[activeIdx] = conc
if (activeIdx < SAMPLES.length - 1) {
activeIdx += 1
toast(`第 ${activeIdx} 条已记录,继续下一条`)
} else {
toast('全部处理完毕(Demo)')
}
renderQueue()
updatePrimaryBtn()
})
document.getElementById('schemeCTbody').addEventListener('click', (e) => {
const btn = e.target.closest('.expand-btn')
if (!btn) return
const panel = document.getElementById('exp-' + btn.dataset.idx)
const open = panel.classList.toggle('is-open')
btn.textContent = open ? '收起' : '展开'
})
document.getElementById('btnFillRows').addEventListener('click', () => {
const v = document.querySelector('input[name="c-bulk"]:checked').value
document.querySelectorAll('#schemeCTbody tr[data-row]').forEach((tr) => {
const checked = tr.querySelector('.row-check').checked
if (checked) tr.querySelector('.row-conc').value = v
})
toast('已填入已勾选行(Demo)')
})
document.getElementById('checkAll').addEventListener('change', (e) => {
document.querySelectorAll('.row-check').forEach((c) => {
c.checked = e.target.checked
})
})
document.querySelectorAll('[data-toast]').forEach((btn) => {
btn.addEventListener('click', () => toast(btn.dataset.toast))
})
fillMiniTables()
renderQueue()
renderSchemeC()
updatePrimaryBtn()
</script>
</body>
</html>