docs: add storage-cert QC info design and HTML demo.

Capture the confirmed sample-level JSON payload approach, field maps from the Excel sheets, and UI entry points for QC fill and market view.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-09 12:22:23 +08:00
1 parent 776031cb4c
commit 55ed0d8929
2 files changed
+1139

No files matched your search

@@ -0,0 +1,855 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>储存证质检信息 · 填写 / 查看 Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--bg-page: #f8fafc;
--panel: #ffffff;
--text: #0f172a;
--text-secondary: #334155;
--text-muted: #64748b;
--border: #e2e8f0;
--border-mint: #d9e8e2;
--radius: 10px;
--radius-sm: 8px;
--shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
--warning: #d97706;
--warning-bg: #fffbeb;
--danger: #dc2626;
--info: #64748b;
}
* { 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: 1180px;
margin: 0 auto;
padding: 24px 20px 80px;
}
.hero { margin-bottom: 20px; max-width: 920px; }
.hero h1 {
margin: 0 0 8px;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.02em;
}
.hero h1::before {
content: "";
display: inline-block;
width: 4px;
height: 1.1em;
margin-right: 10px;
vertical-align: -0.15em;
border-radius: 2px;
background: var(--primary);
}
.hero p { margin: 0 0 8px; color: var(--text-muted); }
.context-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px; border-radius: 999px; font-size: 12px;
background: var(--primary-bg); color: var(--primary-dark);
border: 1px solid #a7f3d0;
}
.pill.muted {
background: #f1f5f9; color: var(--text-muted); border-color: var(--border);
}
/* Scene tabs */
.scene-tabs {
display: flex; flex-wrap: wrap; gap: 8px;
margin-bottom: 16px; padding: 4px;
background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
}
.scene-tab {
border: 1px solid transparent; background: transparent;
padding: 8px 14px; border-radius: 8px; cursor: pointer;
font-size: 13px; font-weight: 500; color: var(--text-secondary);
}
.scene-tab:hover { background: #f8fafc; }
.scene-tab.active {
background: var(--primary-bg); color: var(--primary-dark);
border-color: #a7f3d0;
}
.scene { display: none; }
.scene.active { display: block; }
.section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px 20px;
margin-bottom: 16px;
box-shadow: var(--shadow);
}
.section-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px; margin-bottom: 14px;
}
.section-head h2 {
margin: 0; font-size: 16px; font-weight: 600;
}
.section-head .hint { margin: 4px 0 0; font-size: 12px; color: var(--text-muted); }
.note {
padding: 10px 12px; border-radius: 8px;
background: var(--warning-bg); border: 1px solid #fde68a;
color: #92400e; font-size: 13px; margin-bottom: 14px;
}
.note.ok {
background: var(--primary-bg); border-color: #a7f3d0; color: var(--primary-dark);
}
/* QC card mock */
.protocol-bar {
display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center;
padding: 10px 12px; background: #f8fafc; border: 1px solid var(--border);
border-radius: 8px; margin-bottom: 12px; font-size: 13px;
}
.protocol-bar strong { color: var(--text); }
.sample-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.sample-pill {
padding: 6px 12px; border-radius: 999px; font-size: 12px;
border: 1px solid var(--border); background: #fff; color: var(--text-secondary);
cursor: pointer;
}
.sample-pill.active {
border-color: var(--primary); background: var(--primary-bg); color: var(--primary-dark);
font-weight: 600;
}
.wo-card {
border: 1px solid var(--border-mint);
border-radius: 10px;
padding: 14px 16px;
background: #fff;
}
.wo-card-top {
display: flex; flex-wrap: wrap; justify-content: space-between;
gap: 10px; margin-bottom: 10px;
}
.wo-meta { font-size: 13px; color: var(--text-secondary); }
.wo-meta .code { font-weight: 600; color: var(--text); }
.tag {
display: inline-flex; align-items: center;
padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500;
}
.tag.success { background: var(--primary-bg); color: var(--primary-dark); }
.tag.warning { background: var(--warning-bg); color: var(--warning); }
.tag.info { background: #f1f5f9; color: var(--info); }
.wo-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.btn {
display: inline-flex; align-items: center; gap: 6px;
height: 32px; padding: 0 12px; border-radius: 8px;
border: 1px solid var(--border); background: #fff;
font-size: 13px; color: var(--text-secondary); cursor: pointer;
}
.btn:hover { border-color: #94a3b8; }
.btn.primary {
background: var(--primary); border-color: var(--primary);
color: #fff; font-weight: 500;
}
.btn.primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
.btn.accent {
background: var(--primary-bg); border-color: #a7f3d0;
color: var(--primary-dark); font-weight: 500;
}
.btn.ghost { background: transparent; }
.btn.sm { height: 28px; padding: 0 10px; font-size: 12px; }
.btn-icon {
width: 14px; height: 14px; display: inline-block;
border: 1.5px solid currentColor; border-radius: 3px; opacity: 0.85;
}
/* Sample info table mock */
.toolbar {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
margin-bottom: 12px;
}
.toolbar .selected { font-size: 13px; color: var(--text-muted); margin-right: 4px; }
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; }
table.list {
width: 100%; border-collapse: collapse; min-width: 900px;
}
table.list th, table.list td {
padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border);
font-size: 13px; white-space: nowrap;
}
table.list th {
background: #f8fafc; color: var(--text-muted); font-weight: 600; font-size: 12px;
}
table.list tr:nth-child(even) td { background: #fafafa; }
table.list tr:hover td { background: #f1f5f9; }
table.list tr.highlight td { background: #ecfdf5; }
.ops { display: flex; gap: 6px; flex-wrap: wrap; }
/* Dialog */
.dialog-mask {
position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45);
display: none; align-items: flex-start; justify-content: center;
padding: 40px 16px; z-index: 50; overflow-y: auto;
backdrop-filter: none;
}
.dialog-mask.open { display: flex; }
.dialog {
width: min(920px, 100%);
background: #fff; border-radius: 12px;
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.18);
display: flex; flex-direction: column;
max-height: calc(100vh - 80px);
}
.dialog.wide { width: min(1020px, 100%); }
.dialog-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px; border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.dialog-header h3 { margin: 0; font-size: 16px; font-weight: 600; }
.dialog-header .sub {
margin: 4px 0 0; font-size: 12px; color: var(--text-muted); font-weight: 400;
}
.dialog-close {
width: 32px; height: 32px; border: none; background: #f1f5f9;
border-radius: 8px; cursor: pointer; font-size: 18px; color: var(--text-muted);
}
.dialog-body {
padding: 16px 20px; overflow-y: auto; flex: 1;
}
.dialog-footer {
display: flex; justify-content: flex-end; gap: 8px;
padding: 12px 20px; border-top: 1px solid var(--border);
flex-shrink: 0;
}
.ctx-row {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
margin-bottom: 16px; padding: 12px; background: #f8fafc;
border: 1px solid var(--border); border-radius: 8px;
}
@media (max-width: 800px) {
.ctx-row { grid-template-columns: 1fr 1fr; }
}
.ctx-item label {
display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 2px;
}
.ctx-item .val { font-size: 13px; font-weight: 500; color: var(--text); }
.field-group {
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 14px;
overflow: hidden;
}
.field-group-title {
padding: 10px 14px;
background: var(--primary-bg);
color: var(--primary-dark);
font-size: 13px; font-weight: 600;
border-bottom: 1px solid #a7f3d0;
}
.field-group-title.alt { background: #f1f5f9; color: var(--text-secondary); border-bottom-color: var(--border); }
.field-group-title.meta { background: #fffbeb; color: #92400e; border-bottom-color: #fde68a; }
.form-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 14px;
padding: 14px;
}
@media (max-width: 900px) {
.form-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
.form-grid { grid-template-columns: 1fr; }
}
.form-item { display: flex; flex-direction: column; gap: 4px; }
.form-item.span-2 { grid-column: span 2; }
.form-item.span-3 { grid-column: 1 / -1; }
.form-item label {
font-size: 11px; color: var(--text-muted); font-weight: 500;
}
.form-item input, .form-item select, .form-item textarea {
height: 36px; padding: 0 10px;
border: 1px solid var(--border); border-radius: 8px;
font-size: 13px; color: var(--text); background: #fff;
font-family: inherit;
}
.form-item textarea {
height: auto; min-height: 72px; padding: 8px 10px; resize: vertical;
}
.form-item input:disabled, .form-item textarea:disabled, .form-item select:disabled {
background: #f8fafc; color: var(--text-secondary);
}
.form-item.readonly .val-box {
min-height: 36px; padding: 8px 10px;
border: 1px solid var(--border); border-radius: 8px;
background: #f8fafc; font-size: 13px; color: var(--text-secondary);
white-space: pre-wrap;
}
.type-switch {
display: inline-flex; gap: 6px; margin-bottom: 12px;
}
.field-count {
font-size: 12px; color: var(--text-muted);
}
.flow {
display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
font-size: 13px; color: var(--text-secondary);
}
.flow .step {
padding: 6px 10px; background: #fff; border: 1px solid var(--border);
border-radius: 8px;
}
.flow .arrow { color: var(--text-muted); }
.legend {
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
}
@media (max-width: 700px) {
.legend { grid-template-columns: 1fr; }
}
.legend-card {
border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px;
background: #fff;
}
.legend-card h4 { margin: 0 0 8px; font-size: 14px; }
.legend-card ul {
margin: 0; padding-left: 18px; color: var(--text-secondary); font-size: 12px;
}
.legend-card li { margin-bottom: 4px; }
</style>
</head>
<body>
<div class="page">
<div class="hero">
<h1>储存证质检信息 · Demo</h1>
<p>
质检在「细胞存储质检」工单卡片上<strong>填写</strong>储存证质检信息(字段对齐 Excel 两个 sheet);
市场在「样本信息管理 · 细胞存储」上<strong>查看</strong>同一份数据,用于誊写到储存证。
</p>
<div class="context-bar">
<span class="pill">Excel:围产期造血干细胞</span>
<span class="pill">Excel:脐带间充质干细胞</span>
<span class="pill muted">质检工单 / 我的质检任务</span>
<span class="pill muted">样本信息管理</span>
</div>
</div>
<div class="section">
<div class="section-head">
<div>
<h2>业务链路(示意)</h2>
<p class="hint">当前 Demo 不改业务逻辑,只对齐交互与字段分组</p>
</div>
</div>
<div class="flow">
<span class="step">质检完成检测</span>
<span class="arrow">→</span>
<span class="step" style="border-color:#a7f3d0;background:var(--primary-bg);color:var(--primary-dark);font-weight:600;">填写储存证质检信息</span>
<span class="arrow">→</span>
<span class="step">市场「样本信息」查看</span>
<span class="arrow">→</span>
<span class="step">誊写 / 办理储存证</span>
</div>
</div>
<div class="scene-tabs" role="tablist">
<button type="button" class="scene-tab active" data-scene="overview">① 字段对照</button>
<button type="button" class="scene-tab" data-scene="qc-card">② 质检卡片入口</button>
<button type="button" class="scene-tab" data-scene="fill-hsc">③ 填写 · 围产期造血</button>
<button type="button" class="scene-tab" data-scene="fill-msc">④ 填写 · 脐带间充质</button>
<button type="button" class="scene-tab" data-scene="sample-info">⑤ 样本信息查看入口</button>
</div>
<!-- ① overview -->
<div class="scene active" id="scene-overview">
<div class="note ok">
两个 sheet 共用「身份键 + 母亲血检」骨架,检测报告区字段按样本类型分叉;右侧收尾字段也略有差异。
</div>
<div class="legend">
<div class="legend-card">
<h4>围产期造血干细胞(约 34 列)</h4>
<ul>
<li><strong>身份</strong>:序号、实验室流转编码、协议编码、母亲姓名</li>
<li><strong>采集运输</strong>:采集时间、运输温度</li>
<li><strong>造血干细胞检测报告</strong>:接收/处理/冻存时间、重量、冻存体积、TNC、CD34+、CD34+CD45dim、活性、细菌、HBsAg/HCV/TP/HIV、CMV-IgM</li>
<li><strong>母亲血液检测报告</strong>:HBsAg、HBV DNA、抗HCV、HCV RNA、抗TP、CMV IgM、抗HIV、HIV-1RNA</li>
<li><strong>收尾</strong>:HLA 分型、储存日期、储存年限、检测结果评论、报告发出日期</li>
</ul>
</div>
<div class="legend-card">
<h4>脐带间充质干细胞(约 34 列)</h4>
<ul>
<li><strong>额外</strong>:是否邮寄</li>
<li><strong>身份</strong>:序号、实验室流转编码、协议编码、母亲姓名</li>
<li><strong>采集运输</strong>:脐带采集时间、运输温度</li>
<li><strong>脐带间充质检测报告</strong>:接收/处理/冻存时间、储存总量(管)、可提供细胞类型/数量/存活率、内毒素/细菌/真菌/支原体、形态、免疫表型</li>
<li><strong>母亲血液检测报告</strong>:与造血 sheet 同一组 8 项</li>
<li><strong>收尾</strong>:检测结果评判、报告发出日期、储存日期(P2)、冷冻方式、储存温度、储存年限</li>
</ul>
</div>
</div>
<div class="section" style="margin-top:16px;">
<div class="section-head">
<div>
<h2>Demo 建议排除出「质检填写」的列</h2>
<p class="hint">这些更像列表管理列 / 市场侧信息,可在澄清时确认</p>
</div>
</div>
<p style="margin:0;font-size:13px;color:var(--text-secondary);">
建议默认<strong>只读预填、不让质检手填</strong>:序号、协议编码、母亲姓名(从样本/协议带出)。
「是否邮寄」偏市场物流,是否纳入质检表待确认。
「实验室流转编码」是否等于系统样本编码 / 入库编码,待确认。
</p>
</div>
</div>
<!-- ② qc card -->
<div class="scene" id="scene-qc-card">
<div class="section">
<div class="section-head">
<div>
<h2>质检工单 / 我的质检任务 · 细胞存储质检</h2>
<p class="hint">在工单卡片操作区新增「填写储存证质检信息」(示意高亮)</p>
</div>
</div>
<div class="note">入口放在<strong>工单卡片</strong>(非任务子卡「填写结果」),因为储存证信息是样本级整表,不是单项检测任务结果。</div>
<div class="protocol-bar">
<span>协议:<strong>ZKZX34000852</strong></span>
<span>客户:高居巧</span>
<span>来源:细胞存储</span>
</div>
<div class="sample-pills">
<span class="sample-pill active">B0000002 · 围产期造血干细胞</span>
<span class="sample-pill">B0000002-MSC · 脐带间充质(示意)</span>
</div>
<div class="wo-card">
<div class="wo-card-top">
<div class="wo-meta">
<div class="code">入库质检工单 WO-2026-1008-012</div>
<div style="margin-top:4px;">
<span class="tag success">已完成</span>
<span class="tag info" style="margin-left:6px;">结论:通过</span>
</div>
</div>
<div class="field-count">样本类型 → 决定弹窗字段模板</div>
</div>
<div class="wo-actions">
<button type="button" class="btn">查看</button>
<button type="button" class="btn">特殊样本</button>
<button type="button" class="btn accent" onclick="openFill('hsc')">
<span class="btn-icon"></span> 填写储存证质检信息
</button>
<button type="button" class="btn ghost">更多</button>
</div>
</div>
<div style="height:14px;"></div>
<div class="wo-card" style="opacity:0.92;">
<div class="wo-card-top">
<div class="wo-meta">
<div class="code">入库质检工单 WO-2026-1009-003</div>
<div style="margin-top:4px;">
<span class="tag warning">进行中</span>
</div>
</div>
</div>
<div class="wo-actions">
<button type="button" class="btn primary">配置检测</button>
<button type="button" class="btn accent" onclick="openFill('msc')">
<span class="btn-icon"></span> 填写储存证质检信息
</button>
</div>
</div>
</div>
</div>
<!-- ③ fill HSC summary (opens dialog) -->
<div class="scene" id="scene-fill-hsc">
<div class="section">
<div class="section-head">
<div>
<h2>填写弹窗 · 围产期造血干细胞</h2>
<p class="hint">分组 = Excel 合并表头;三列栅格;长文本(HLA / 评论)跨列</p>
</div>
<button type="button" class="btn primary" onclick="openFill('hsc')">打开填写弹窗</button>
</div>
<p style="margin:0;font-size:13px;color:var(--text-muted);">
字段按 Excel sheet「围产期造血干细胞」展开:造血检测报告 15 项 + 母亲血检 8 项 + HLA/储存/评论/报告日。
</p>
</div>
</div>
<!-- ④ fill MSC -->
<div class="scene" id="scene-fill-msc">
<div class="section">
<div class="section-head">
<div>
<h2>填写弹窗 · 脐带间充质干细胞</h2>
<p class="hint">与造血模板共用母亲血检区块;检测报告与收尾字段不同</p>
</div>
<button type="button" class="btn primary" onclick="openFill('msc')">打开填写弹窗</button>
</div>
<p style="margin:0;font-size:13px;color:var(--text-muted);">
含「是否邮寄」、储存总量(管)、可提供细胞*、内毒素/真菌/支原体、冷冻方式、储存温度等。
</p>
</div>
</div>
<!-- ⑤ sample info -->
<div class="scene" id="scene-sample-info">
<div class="section">
<div class="section-head">
<div>
<h2>样本信息管理 · 细胞存储 Tab</h2>
<p class="hint">行操作增加「查看储存证的质检信息」;无数据时按钮可禁用或提示未填写</p>
</div>
</div>
<div class="toolbar">
<span class="selected">已选 0 项</span>
<button type="button" class="btn primary">登记入库</button>
<button type="button" class="btn">登记储存证</button>
<button type="button" class="btn">登记保险</button>
</div>
<div class="table-wrap">
<table class="list">
<thead>
<tr>
<th>样本编码</th>
<th>协议编码</th>
<th>母亲姓名</th>
<th>样本类型</th>
<th>质检</th>
<th>储存证</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr class="highlight">
<td>B0000002</td>
<td>ZKZX34000852</td>
<td>高居巧</td>
<td>围产期造血干细胞</td>
<td><span class="tag success">已完成</span></td>
<td><span class="tag warning">未办理</span></td>
<td>
<div class="ops">
<button type="button" class="btn sm">查看</button>
<button type="button" class="btn sm accent" onclick="openView('hsc')">查看储存证的质检信息</button>
</div>
</td>
</tr>
<tr>
<td>BC0000003</td>
<td>ZKQD34000831</td>
<td>牛晨晨</td>
<td>脐带间充质干细胞</td>
<td><span class="tag success">已完成</span></td>
<td><span class="tag success">已办理</span></td>
<td>
<div class="ops">
<button type="button" class="btn sm">查看</button>
<button type="button" class="btn sm accent" onclick="openView('msc')">查看储存证的质检信息</button>
</div>
</td>
</tr>
<tr>
<td>B0000099</td>
<td>ZKZX34000999</td>
<td>示例未填</td>
<td>围产期造血干细胞</td>
<td><span class="tag warning">进行中</span></td>
<td><span class="tag info">—</span></td>
<td>
<div class="ops">
<button type="button" class="btn sm">查看</button>
<button type="button" class="btn sm" disabled style="opacity:0.45;cursor:not-allowed;">查看储存证的质检信息</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Fill dialog -->
<div class="dialog-mask" id="fillMask" onclick="maskClose(event,'fillMask')">
<div class="dialog wide" role="dialog" aria-modal="true">
<div class="dialog-header">
<div>
<h3 id="fillTitle">填写储存证质检信息</h3>
<div class="sub" id="fillSub">按样本类型切换字段模板</div>
</div>
<button type="button" class="dialog-close" onclick="closeMask('fillMask')" aria-label="关闭">×</button>
</div>
<div class="dialog-body">
<div class="ctx-row">
<div class="ctx-item"><label>实验室流转编码</label><div class="val" id="ctxLab">B0000002</div></div>
<div class="ctx-item"><label>协议编码</label><div class="val" id="ctxProto">ZKZX34000852</div></div>
<div class="ctx-item"><label>母亲姓名</label><div class="val" id="ctxMother">高居巧</div></div>
<div class="ctx-item"><label>样本类型</label><div class="val" id="ctxType">围产期造血干细胞</div></div>
</div>
<div id="formHsc">
<div class="field-group">
<div class="field-group-title alt">采集与运输</div>
<div class="form-grid">
<div class="form-item"><label>造血干细胞采集时间</label><input value="2025年9月7日15时36分" /></div>
<div class="form-item"><label>运输温度 /℃</label><input value="17" /></div>
</div>
</div>
<div class="field-group">
<div class="field-group-title">造血干细胞检测报告</div>
<div class="form-grid">
<div class="form-item"><label>造血干细胞接收时间</label><input value="2025.9.8 9:00" /></div>
<div class="form-item"><label>处理时间</label><input value="2025.9.8 9:30" /></div>
<div class="form-item"><label>冻存时间</label><input value="2025.9.8 11:57" /></div>
<div class="form-item"><label>造血干细胞重量/g</label><input value="56" /></div>
<div class="form-item"><label>造血干细胞冻存体积/mL</label><input value="21.8" /></div>
<div class="form-item"><label>有核细胞数 TNC / *10E8</label><input value="2.28" /></div>
<div class="form-item"><label>CD34+ 细胞计数 / *10E6</label><input value="16.3" /></div>
<div class="form-item"><label>CD34+ CD45dim / *10E5</label><input value="1.27" /></div>
<div class="form-item"><label>细胞活性/%</label><input value="90.44" /></div>
<div class="form-item"><label>细菌检测</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
<div class="form-item"><label>乙型肝炎病毒表面抗原(HBsAg)</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
<div class="form-item"><label>丙型肝炎病毒抗体(抗HCV)</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
<div class="form-item"><label>梅毒螺旋体特异性抗体(抗TP)</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
<div class="form-item"><label>人类免疫缺陷病毒抗体(抗HIV-1/2)</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
<div class="form-item"><label>巨细胞病毒 IgM 抗体 (CMV-IgM)</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
</div>
</div>
<div class="field-group">
<div class="field-group-title">母亲血液检测报告</div>
<div class="form-grid">
<div class="form-item"><label>乙型肝炎病毒表面抗原(HBsAg)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>乙型肝炎病毒核酸(HBV DNA)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>丙型肝炎病毒抗体(抗HCV)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>丙型肝炎病毒核酸(HCV RNA)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>梅毒螺旋体特异性抗体(抗TP)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>巨细胞病毒 IgM 抗体 (CMV IgM)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>人类免疫缺陷病毒抗体(抗HIV-1/2)</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>人类免疫缺陷病毒核酸(1型) (HIV-1RNA)</label><select><option>阴</option><option>/</option></select></div>
</div>
</div>
<div class="field-group">
<div class="field-group-title meta">储存证收尾信息</div>
<div class="form-grid">
<div class="form-item span-3"><label>HLA 分型</label><textarea>A 02:06/24:02
B 07:02(M)/57:01(T)
C 06:02/07:02
DRB1 13:01/15:01
DQB1 06:02/06:03</textarea></div>
<div class="form-item"><label>储存日期</label><input placeholder="YYYY-MM-DD" /></div>
<div class="form-item"><label>储存年限</label><input value="20" /></div>
<div class="form-item"><label>检测结果评论</label><input value="合格" /></div>
<div class="form-item"><label>报告发出日期</label><input placeholder="YYYY-MM-DD" /></div>
</div>
</div>
</div>
<div id="formMsc" style="display:none;">
<div class="field-group">
<div class="field-group-title alt">采集与运输 / 邮寄</div>
<div class="form-grid">
<div class="form-item"><label>是否邮寄</label><select><option>是</option><option selected>否</option></select></div>
<div class="form-item"><label>脐带采集时间</label><input value="2025.11.25 15:49" /></div>
<div class="form-item"><label>运输温度 /℃</label><input value="" placeholder="可填「常温」或数值" /></div>
</div>
</div>
<div class="field-group">
<div class="field-group-title">脐带间充质干细胞检测报告</div>
<div class="form-grid">
<div class="form-item"><label>脐带接收时间</label><input value="2025.11.26 10:00" /></div>
<div class="form-item"><label>处理时间</label><input value="2025.11.26 16:00" /></div>
<div class="form-item"><label>冻存时间</label><input value="2025.12.21" /></div>
<div class="form-item"><label>储存总量(管)</label><input value="17" placeholder="Excel 中有 =0+17 公式" /></div>
<div class="form-item"><label>可提供细胞类型</label><input value="间充质干细胞" /></div>
<div class="form-item"><label>可提供细胞数量 / *10E7</label><input value="8.5" /></div>
<div class="form-item"><label>可提供细胞存活率 %</label><input value="97.1" /></div>
<div class="form-item"><label>内毒素检测</label><select><option>阴</option><option>阳</option><option>/</option></select></div>
<div class="form-item"><label>细菌检测</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>真菌检测</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>支原体检测</label><select><option>阴</option><option>/</option></select></div>
<div class="form-item"><label>可提供细胞形态</label><input value="短梭/贴壁型" /></div>
<div class="form-item span-2"><label>可提供细胞免疫表型特征</label><input value="符合间充质干细胞表型特性" /></div>
</div>
</div>
<div class="field-group">
<div class="field-group-title">母亲血液检测报告</div>
<div class="form-grid">
<div class="form-item"><label>乙型肝炎病毒表面抗原(HBsAg)</label><select><option>阴</option></select></div>
<div class="form-item"><label>乙型肝炎病毒核酸(HBV DNA)</label><select><option>/</option><option>阴</option></select></div>
<div class="form-item"><label>丙型肝炎病毒抗体(抗HCV)</label><select><option>阴</option></select></div>
<div class="form-item"><label>丙型肝炎病毒核酸(HCV RNA)</label><select><option>/</option><option>阴</option></select></div>
<div class="form-item"><label>梅毒螺旋体特异性抗体(抗TP)</label><select><option>阴</option></select></div>
<div class="form-item"><label>巨细胞病毒 IgM 抗体 (CMV IgM)</label><select><option>阴</option></select></div>
<div class="form-item"><label>人类免疫缺陷病毒抗体(抗HIV-1/2)</label><select><option>阴</option></select></div>
<div class="form-item"><label>人类免疫缺陷病毒核酸(1型) (HIV-1RNA)</label><select><option>阴</option></select></div>
</div>
</div>
<div class="field-group">
<div class="field-group-title meta">储存证收尾信息</div>
<div class="form-grid">
<div class="form-item"><label>检测结果评判</label><input value="合格" /></div>
<div class="form-item"><label>报告发出日期</label><input value="2026-03-14" /></div>
<div class="form-item"><label>储存日期 (P2冻存日期)</label><input value="2025.12.22" /></div>
<div class="form-item"><label>冷冻方式</label><input value="程序降温盒降温" /></div>
<div class="form-item"><label>储存温度</label><input value="-196℃" /></div>
<div class="form-item"><label>储存年限</label><input value="20" /></div>
</div>
</div>
</div>
</div>
<div class="dialog-footer">
<button type="button" class="btn" onclick="closeMask('fillMask')">取消</button>
<button type="button" class="btn primary" onclick="alert('Demo:保存成功(未接后端)'); closeMask('fillMask');">确定</button>
</div>
</div>
</div>
<!-- View dialog -->
<div class="dialog-mask" id="viewMask" onclick="maskClose(event,'viewMask')">
<div class="dialog wide" role="dialog" aria-modal="true">
<div class="dialog-header">
<div>
<h3>查看储存证的质检信息</h3>
<div class="sub" id="viewSub">只读 · 字段与质检填写一致</div>
</div>
<button type="button" class="dialog-close" onclick="closeMask('viewMask')" aria-label="关闭">×</button>
</div>
<div class="dialog-body" id="viewBody">
<!-- filled by JS -->
</div>
<div class="dialog-footer">
<button type="button" class="btn primary" onclick="closeMask('viewMask')">关闭</button>
</div>
</div>
</div>
<script>
// Scene tabs
document.querySelectorAll('.scene-tab').forEach(function (tab) {
tab.addEventListener('click', function () {
document.querySelectorAll('.scene-tab').forEach(function (t) { t.classList.remove('active'); });
document.querySelectorAll('.scene').forEach(function (s) { s.classList.remove('active'); });
tab.classList.add('active');
document.getElementById('scene-' + tab.dataset.scene).classList.add('active');
});
});
function closeMask(id) {
document.getElementById(id).classList.remove('open');
}
function maskClose(e, id) {
if (e.target.id === id) closeMask(id);
}
function openFill(type) {
var isHsc = type === 'hsc';
document.getElementById('formHsc').style.display = isHsc ? '' : 'none';
document.getElementById('formMsc').style.display = isHsc ? 'none' : '';
document.getElementById('fillTitle').textContent = '填写储存证质检信息';
document.getElementById('fillSub').textContent = isHsc
? '模板:围产期造血干细胞(对齐 Excel sheet)'
: '模板:脐带间充质干细胞(对齐 Excel sheet)';
document.getElementById('ctxLab').textContent = isHsc ? 'B0000002' : 'BC0000003';
document.getElementById('ctxProto').textContent = isHsc ? 'ZKZX34000852' : 'ZKQD34000831';
document.getElementById('ctxMother').textContent = isHsc ? '高居巧' : '牛晨晨';
document.getElementById('ctxType').textContent = isHsc ? '围产期造血干细胞' : '脐带间充质干细胞';
document.getElementById('fillMask').classList.add('open');
}
function ro(label, val, span) {
var cls = 'form-item readonly' + (span ? ' span-' + span : '');
return '<div class="' + cls + '"><label>' + label + '</label><div class="val-box">' + (val || '—') + '</div></div>';
}
function openView(type) {
var isHsc = type === 'hsc';
document.getElementById('viewSub').textContent = isHsc
? '样本 B0000002 · 围产期造血干细胞 · 只读'
: '样本 BC0000003 · 脐带间充质干细胞 · 只读';
var html = '';
html += '<div class="ctx-row">' +
'<div class="ctx-item"><label>实验室流转编码</label><div class="val">' + (isHsc ? 'B0000002' : 'BC0000003') + '</div></div>' +
'<div class="ctx-item"><label>协议编码</label><div class="val">' + (isHsc ? 'ZKZX34000852' : 'ZKQD34000831') + '</div></div>' +
'<div class="ctx-item"><label>母亲姓名</label><div class="val">' + (isHsc ? '高居巧' : '牛晨晨') + '</div></div>' +
'<div class="ctx-item"><label>样本类型</label><div class="val">' + (isHsc ? '围产期造血干细胞' : '脐带间充质干细胞') + '</div></div>' +
'</div>';
if (isHsc) {
html += '<div class="field-group"><div class="field-group-title alt">采集与运输</div><div class="form-grid">' +
ro('造血干细胞采集时间', '2025年9月7日15时36分') +
ro('运输温度 /℃', '17') +
'</div></div>';
html += '<div class="field-group"><div class="field-group-title">造血干细胞检测报告</div><div class="form-grid">' +
ro('造血干细胞接收时间', '2025.9.8 9:00') +
ro('处理时间', '2025.9.8 9:30') +
ro('冻存时间', '2025.9.8 11:57') +
ro('造血干细胞重量/g', '56') +
ro('造血干细胞冻存体积/mL', '21.8') +
ro('有核细胞数 TNC / *10E8', '2.28') +
ro('CD34+ 细胞计数 / *10E6', '16.3') +
ro('CD34+ CD45dim / *10E5', '1.27') +
ro('细胞活性/%', '90.44') +
ro('细菌检测', '阴') +
ro('乙型肝炎病毒表面抗原(HBsAg)', '阴') +
ro('丙型肝炎病毒抗体(抗HCV)', '阴') +
ro('梅毒螺旋体特异性抗体(抗TP)', '阴') +
ro('人类免疫缺陷病毒抗体(抗HIV-1/2)', '阴') +
ro('巨细胞病毒 IgM 抗体 (CMV-IgM)', '阴') +
'</div></div>';
html += '<div class="field-group"><div class="field-group-title">母亲血液检测报告</div><div class="form-grid">' +
ro('HBsAg', '阴') + ro('HBV DNA', '阴') + ro('抗HCV', '阴') + ro('HCV RNA', '阴') +
ro('抗TP', '阴') + ro('CMV IgM', '阴') + ro('抗HIV-1/2', '阴') + ro('HIV-1RNA', '阴') +
'</div></div>';
html += '<div class="field-group"><div class="field-group-title meta">储存证收尾信息</div><div class="form-grid">' +
ro('HLA 分型', 'A 02:06/24:02\\nB 07:02(M)/57:01(T)\\nC 06:02/07:02\\nDRB1 13:01/15:01\\nDQB1 06:02/06:03', 3) +
ro('储存日期', '—') + ro('储存年限', '20') + ro('检测结果评论', '合格') + ro('报告发出日期', '—') +
'</div></div>';
} else {
html += '<div class="field-group"><div class="field-group-title alt">采集与运输 / 邮寄</div><div class="form-grid">' +
ro('是否邮寄', '—') + ro('脐带采集时间', '2025.11.25 15:49') + ro('运输温度 /℃', '—') +
'</div></div>';
html += '<div class="field-group"><div class="field-group-title">脐带间充质干细胞检测报告</div><div class="form-grid">' +
ro('脐带接收时间', '2025.11.26 10:00') + ro('处理时间', '2025.11.26 16:00') + ro('冻存时间', '2025.12.21') +
ro('储存总量(管)', '17') + ro('可提供细胞类型', '间充质干细胞') +
ro('可提供细胞数量 / *10E7', '8.5') + ro('可提供细胞存活率 %', '97.1') +
ro('内毒素检测', '阴') + ro('细菌检测', '阴') + ro('真菌检测', '阴') + ro('支原体检测', '阴') +
ro('可提供细胞形态', '短梭/贴壁型') +
ro('可提供细胞免疫表型特征', '符合间充质干细胞表型特性', 2) +
'</div></div>';
html += '<div class="field-group"><div class="field-group-title">母亲血液检测报告</div><div class="form-grid">' +
ro('HBsAg', '阴') + ro('HBV DNA', '/') + ro('抗HCV', '阴') + ro('HCV RNA', '/') +
ro('抗TP', '阴') + ro('CMV IgM', '阴') + ro('抗HIV-1/2', '阴') + ro('HIV-1RNA', '阴') +
'</div></div>';
html += '<div class="field-group"><div class="field-group-title meta">储存证收尾信息</div><div class="form-grid">' +
ro('检测结果评判', '合格') + ro('报告发出日期', '2026-03-14') +
ro('储存日期 (P2冻存日期)', '2025.12.22') + ro('冷冻方式', '程序降温盒降温') +
ro('储存温度', '-196℃') + ro('储存年限', '20') +
'</div></div>';
}
document.getElementById('viewBody').innerHTML = html;
document.getElementById('viewMask').classList.add('open');
}
</script>
</body>
</html>
@@ -0,0 +1,284 @@
# 储存证质检信息 — 实现设计(方案 A)
> 日期:2026-10-09
> 状态:已确认(2026-10-09)
> 关联:质检工单 / 我的质检任务「细胞存储质检」;样本信息管理「细胞存储」;Excel《干细胞储存证数据》
> Demo:`docs/demos/2026-10-09-storage-cert-qc-info-demo.html`
---
## 1. 背景
市场办理储存证时,质检数据目前来自 Excel 手工表(两个 sheet):
| Sheet | 样本类型 |
|-------|----------|
| 围产期造血干细胞 | 围产期造血干细胞 |
| 脐带间充质干细胞 | 脐带间充质干细胞 |
流程为:质检部门填表 → 市场部门誊写到储存证。系统内已有入库质检工单与样本信息管理,但**没有**与储存证誊写对齐的结构化质检信息落库与查看入口。
现有 `biz_protocol_storage_cert` 为**协议级**办理状态(办理时间、附件等),不承载上述检测明细。
---
## 2. 目标与非目标
### 2.1 目标
- 质检在「质检工单」「我的质检任务」的「细胞存储质检」**工单卡片**上,可「填写储存证质检信息」(按整份样本一张表)。
- 字段分组与 Excel 对应 sheet 业务列一致;按样本类型切换两套模板。
- 市场在「样本信息管理 · 细胞存储」行操作可「查看储存证的质检信息」(只读);未填时提示「质检尚未填写」。
- 数据按**样本级**持久化,可反复编辑覆盖;允许部分填写即保存。
### 2.2 非目标
- 不自动生成 / 打印储存证 PDF。
- 不改「登记储存证」办理流,不扩展 `biz_protocol_storage_cert` 明细列。
- 不从接收单 / 质检任务结果自动回填检测数值(本期全部手填)。
- 不做 Excel 批量导入。
- 不为其它样本类型(如胎盘亚全能)做模板;其它类型不显示填写/查看按钮。
- 不做必填校验、不做乐观锁。
---
## 3. 已确认产品决策
| 决策点 | 结论 |
|--------|------|
| 填写入口 | 工单卡片(与查看 / 配置检测同级),非整条子任务「填写结果」 |
| 粒度 | 整份样本一张表 |
| 查看可用性 | 始终可点;未填弹窗提示「质检尚未填写」 |
| 样本类型范围 | 仅围产期造血干细胞、脐带间充质干细胞 |
| 再次编辑 | 可反复打开保存,覆盖上次 |
| 数值来源 | 全部手填;系统只带出身份信息 |
| 校验 | 允许部分填写即保存(草稿式) |
| 存储方案 | 方案 A:样本级新表 + JSON `payload` |
---
## 4. 数据模型
### 4.1 表 `biz_sample_storage_cert_qc`
| 字段 | 类型(示意) | 说明 |
|------|--------------|------|
| `id` | VARCHAR(64) | UUID PK |
| `sample_code` | VARCHAR(64) | 样本编码;跨表关联键之一 |
| `collection_protocol_id` | VARCHAR(64) | 冗余协议 id,便于权限/展示;**非**唯一键 |
| `qc_work_order_id` | VARCHAR(64) NULL | 可选,最近一次从哪张工单填写 |
| `sample_type_key` | VARCHAR(32) | 模板键:`perinatal_hsc` / `uc_msc` |
| `payload` | JSON / TEXT | 业务字段对象(见 §5) |
| `filled_flag` | CHAR(1) | `0`/`1`;任意一次成功保存后为 `1` |
| 公共字段 | | `tenant_no` … `del_flag`(对齐 database-conventions) |
**唯一性**:`tenant_no + sample_code` 在 `del_flag=0` 下唯一;写入走 upsert。
**关联键**:读写与列表关联一律用 `tenant_no + sample_code`(对齐细胞存储样本跨表约定),不用 `collection_protocol_id` 作唯一匹配。
### 4.2 模板键映射
与现网名称常量对齐(`SAMPLE_TYPE_PERINATAL_HSC` / `SAMPLE_TYPE_UC_MSC`):
| 样本类型名称 | `sample_type_key` |
|--------------|-------------------|
| 围产期造血干细胞 | `perinatal_hsc` |
| 脐带间充质干细胞 | `uc_msc` |
服务端根据样本当前类型解析 `sample_type_key`,**不信任**客户端随意改模板。非以上类型 → 拒绝写入。
### 4.3 身份信息(不进 payload)
弹窗顶部只读展示(从协议 / 样本主数据带出):
- 实验室流转编码:本期约定展示 **`sample_code`**(与系统样本编码一致)
- 协议编码
- 母亲姓名
- 样本类型名称
排除 Excel「序号」列(列表管理用,不入库)。
---
## 5. Payload 字段(对齐 Excel)
键名用稳定英文 snake_case;界面 label 用中文(与 Excel 表头一致)。值为字符串或数字均可,空字段可省略或存 `null`/`""`。阴阳性常见值:`阴` / `阳` / `/`。
### 5.1 `perinatal_hsc`(围产期造血干细胞)
**采集与运输**
| key | 中文 |
|-----|------|
| `collection_time` | 造血干细胞采集时间 |
| `transport_temp_c` | 运输温度 /℃ |
**造血干细胞检测报告**
| key | 中文 |
|-----|------|
| `receive_time` | 造血干细胞接收时间 |
| `process_time` | 处理时间 |
| `cryo_time` | 冻存时间 |
| `weight_g` | 造血干细胞重量/g |
| `cryo_volume_ml` | 造血干细胞冻存体积/mL |
| `tnc_1e8` | 有核细胞数 TNC / *10E8 |
| `cd34_count_1e6` | CD34+ 细胞计数 / *10E6 |
| `cd34_cd45dim_1e5` | CD34+ CD45dim 细胞计数 / *10E5 |
| `viability_pct` | 细胞活性/% |
| `bacteria_test` | 细菌检测 |
| `hbsag` | 乙型肝炎病毒表面抗原(HBsAg) |
| `anti_hcv` | 丙型肝炎病毒抗体(抗HCV) |
| `anti_tp` | 梅毒螺旋体特异性抗体(抗TP) |
| `anti_hiv` | 人类免疫缺陷病毒抗体(抗HIV-1/2) |
| `cmv_igm` | 巨细胞病毒 IgM 抗体 (CMV-IgM) |
**母亲血液检测报告**
| key | 中文 |
|-----|------|
| `mother_hbsag` | 乙型肝炎病毒表面抗原(HBsAg) |
| `mother_hbv_dna` | 乙型肝炎病毒核酸(HBV DNA) |
| `mother_anti_hcv` | 丙型肝炎病毒抗体(抗HCV) |
| `mother_hcv_rna` | 丙型肝炎病毒核酸(HCV RNA) |
| `mother_anti_tp` | 梅毒螺旋体特异性抗体(抗TP) |
| `mother_cmv_igm` | 巨细胞病毒 IgM 抗体 (CMV IgM) |
| `mother_anti_hiv` | 人类免疫缺陷病毒抗体(抗HIV-1/2) |
| `mother_hiv1_rna` | 人类免疫缺陷病毒核酸(1型) (HIV-1RNA) |
**储存证收尾**
| key | 中文 |
|-----|------|
| `hla_typing` | HLA 分型 |
| `storage_date` | 储存日期 |
| `storage_years` | 储存年限 |
| `result_comment` | 检测结果评论 |
| `report_issue_date` | 报告发出日期 |
### 5.2 `uc_msc`(脐带间充质干细胞)
**采集与运输 / 邮寄**
| key | 中文 |
|-----|------|
| `mail_flag` | 是否邮寄 |
| `collection_time` | 脐带采集时间 |
| `transport_temp_c` | 运输温度 /℃ |
**脐带间充质干细胞检测报告**
| key | 中文 |
|-----|------|
| `receive_time` | 脐带接收时间 |
| `process_time` | 处理时间 |
| `cryo_time` | 冻存时间 |
| `storage_tube_count` | 储存总量(管) |
| `available_cell_type` | 可提供细胞类型 |
| `available_cell_count_1e7` | 可提供细胞数量 / *10E7 |
| `available_viability_pct` | 可提供细胞存活率 % |
| `endotoxin_test` | 内毒素检测 |
| `bacteria_test` | 细菌检测 |
| `fungus_test` | 真菌检测 |
| `mycoplasma_test` | 支原体检测 |
| `available_cell_morphology` | 可提供细胞形态 |
| `available_immunophenotype` | 可提供细胞免疫表型特征 |
**母亲血液检测报告**:与 §5.1 母亲血检 8 个 key 相同。
**储存证收尾**
| key | 中文 |
|-----|------|
| `result_judgment` | 检测结果评判 |
| `report_issue_date` | 报告发出日期 |
| `storage_date_p2` | 储存日期 (P2冻存日期) |
| `freeze_method` | 冷冻方式 |
| `storage_temp` | 储存温度 |
| `storage_years` | 储存年限 |
---
## 6. API
建议前缀:`/web/admin/sample-storage-cert-qc`。
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/{sample_code}` | 读详情:身份只读字段 + `sampleTypeKey` + `filledFlag` + `payload` |
| PUT | `/{sample_code}` | Upsert;body:`payload`(object),可选 `qcWorkOrderId` |
### 6.1 行为
- GET 无行:HTTP 200,`filledFlag=false`,`payload={}`(或 null),**不** 404。
- PUT:校验样本存在、租户、数据权限、类型为两种之一;写 `sample_type_key`;`filled_flag=1`;覆盖 `payload`。
- 列表接口本期不扩展整份 payload;样本信息列表点「查看」再 GET。
### 6.2 权限
- PUT:具备质检工单或我的质检任务相关菜单权限(与工单卡片操作对齐;不额外强制「必须是任务执行人」,除非现有卡片按钮已有更严规则则跟随)。
- GET:样本信息管理菜单可访问;质检填写弹窗预填亦用同一 GET。
---
## 7. 前端
### 7.1 质检入口
- 页面:`qc-work-order`、`my-qc-task`,Tab「细胞存储质检」(`inbound_cell_storage`)。
- 组件:`QcWorkOrderSingleCard` 操作区增加「填写储存证质检信息」。
- 显示条件:当前样本类型名为围产期造血干细胞或脐带间充质干细胞。
- 弹窗:可编辑;GET 预填 → PUT 保存;manage / mine 均显示(规则相同)。
### 7.2 样本信息入口
- 页面:`/#/market/cell-storage/sample-info`,「细胞存储」Tab。
- 行操作增加「查看储存证的质检信息」;显示条件同两种类型。
- 始终可点;`filledFlag=false` 时弹窗空态文案「质检尚未填写」(可保留身份区);已填则只读同模板字段。
- 市场侧不可编辑。
### 7.3 UI 约定
- `label-position="top"`;分组对齐 Demo / Excel 合并表头;栅格最多约 3 列,窄屏降列。
- 一套字段元数据配置驱动填写(可编)与查看(只读)。
- 对齐 Fresh Mint / `dialog-form-layout`;遮罩无 backdrop blur。
- Demo:`docs/demos/2026-10-09-storage-cert-qc-info-demo.html`。
### 7.4 样本类型变更(边界)
极少见。打开表单按**当前**样本类型选模板;保存时按当前模板**整包覆盖** `payload`(实现简单,可接受)。
---
## 8. 错误处理摘要
| 场景 | 行为 |
|------|------|
| 不支持类型 PUT | 400;前端不展示按钮 |
| 样本不存在 / 无权限 | 统一 403/404 |
| GET 无记录 | 200 + 未填形态 |
| payload 非 object | 400 |
| 并发保存 | 后写覆盖 |
---
## 9. 测试要点
- 后端:upsert 覆盖、租户隔离、`filled_flag`、类型门禁、GET 空记录。
- 前端:两类型按钮显隐;质检保存后市场只读可见;未填空态;manage/mine 均有入口。
---
## 10. 实现落点(指引,非任务清单)
| 层 | 预期触点 |
|----|----------|
| SQL | `docs/sql/migrations/2026-10-09-sample-storage-cert-qc.sql` |
| Model / CRUD / API | `backend/app/models`、`crud`、`api`(新模块或挂入 qc / sample 相关路由并注册) |
| FE API | `web-admin/src/api/` |
| 字段元数据 | `web-admin/src/utils/` 或业务组件内配置 |
| 填写/查看弹窗 | 共用业务组件 |
| 工单卡片 | `QcWorkOrderSingleCard.vue` |
| 样本信息 | `UnifiedSampleInfoPage.vue`(细胞存储操作列) |
详细步骤在实现计划中展开(writing-plans)。