969 lines
30 KiB
HTML
969 lines
30 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>质检工单 · 同样本多工单 Tab 选型 · 对比总览</title>
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
<link rel="stylesheet" href="_shared.css" />
|
||
<style>
|
||
/* ── V0 现网基线 ── */
|
||
.v0-tabs {
|
||
padding: 8px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.v0-tabs__track {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
padding: 6px;
|
||
background: linear-gradient(180deg, var(--primary-bg) 0%, #fff 100%);
|
||
border: 1px solid #d1fae5;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.v0-tabs__btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-height: 34px;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
font-size: 12px;
|
||
color: #1e293b;
|
||
transition: background 0.15s, color 0.15s;
|
||
}
|
||
|
||
.v0-tabs__btn:hover:not(.is-active) {
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
.v0-tabs__btn.is-active {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
box-shadow: 0 1px 3px rgba(16, 185, 129, 0.28);
|
||
}
|
||
|
||
.v0-tabs__btn:not(.is-active) .v0-source {
|
||
color: #1e293b;
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
.v0-tabs__btn.is-active .v0-source {
|
||
color: #fff;
|
||
background: rgb(255 255 255 / 22%);
|
||
}
|
||
|
||
.v0-tabs__btn:not(.is-active) .tag {
|
||
background: #f1f5f9;
|
||
color: #1e293b;
|
||
border-color: #e2e8f0;
|
||
}
|
||
|
||
.v0-tabs__btn.is-active .tag {
|
||
background: rgb(255 255 255 / 22%);
|
||
color: #fff;
|
||
border-color: rgb(255 255 255 / 35%);
|
||
}
|
||
|
||
.v0-index {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.v0-tabs__btn.is-active .v0-index {
|
||
background: rgb(255 255 255 / 28%);
|
||
}
|
||
|
||
.v0-source {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v0-source__dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
}
|
||
|
||
.v0-no {
|
||
font-family: ui-monospace, Menlo, monospace;
|
||
}
|
||
|
||
/* ── V1 分类色保留 + 引导 ── */
|
||
.v1-zone {
|
||
padding: 10px 16px 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.v1-hint {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.v1-hint svg {
|
||
flex-shrink: 0;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.v1-hint strong {
|
||
color: var(--primary-dark);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v1-tabs__track {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.v1-tabs__btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 40px;
|
||
padding: 8px 14px;
|
||
border: 2px solid var(--border);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
|
||
}
|
||
|
||
.v1-tabs__btn:hover:not(.is-active) {
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.v1-tabs__btn.is-active {
|
||
border-color: var(--primary);
|
||
background: var(--primary-bg);
|
||
box-shadow: 0 0 0 1px var(--primary);
|
||
}
|
||
|
||
.v1-tabs__btn.is-active[data-source='reception'] {
|
||
border-color: var(--reception);
|
||
background: var(--reception-bg);
|
||
box-shadow: 0 0 0 1px var(--reception);
|
||
}
|
||
|
||
.v1-tabs__btn.is-active[data-source='production'] {
|
||
border-color: var(--production);
|
||
background: var(--production-bg);
|
||
box-shadow: 0 0 0 1px var(--production);
|
||
}
|
||
|
||
.v1-tabs__btn:not(.is-active)::after {
|
||
content: '点击查看';
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.v1-source-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 3px 8px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v1-source-badge--reception {
|
||
background: var(--reception-bg);
|
||
color: var(--reception);
|
||
border: 1px solid var(--reception-border);
|
||
}
|
||
|
||
.v1-source-badge--production {
|
||
background: var(--production-bg);
|
||
color: var(--production);
|
||
border: 1px solid var(--production-border);
|
||
}
|
||
|
||
/* ── V2 双卡片切换器 ── */
|
||
.v2-zone {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.v2-label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.v2-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
@media (max-width: 560px) {
|
||
.v2-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.v2-card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 12px 14px;
|
||
border: 2px solid var(--border);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
text-align: left;
|
||
transition: border-color 0.15s, box-shadow 0.15s;
|
||
}
|
||
|
||
.v2-card:hover:not(.is-active) {
|
||
border-color: #cbd5e1;
|
||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.v2-card.is-active {
|
||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
.v2-card[data-source='reception'].is-active {
|
||
border-color: var(--reception);
|
||
background: linear-gradient(180deg, var(--reception-bg) 0%, #fff 40%);
|
||
}
|
||
|
||
.v2-card[data-source='production'].is-active {
|
||
border-color: var(--production);
|
||
background: linear-gradient(180deg, var(--production-bg) 0%, #fff 40%);
|
||
}
|
||
|
||
.v2-card:not(.is-active)::before {
|
||
content: '点击切换';
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
background: #f1f5f9;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.v2-card__top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.v2-card__icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.v2-card[data-source='reception'] .v2-card__icon {
|
||
background: var(--reception-bg);
|
||
color: var(--reception);
|
||
}
|
||
|
||
.v2-card[data-source='production'] .v2-card__icon {
|
||
background: var(--production-bg);
|
||
color: var(--production);
|
||
}
|
||
|
||
.v2-card__title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v2-card__no {
|
||
font-size: 12px;
|
||
font-family: ui-monospace, Menlo, monospace;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.v2-card__foot {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ── V3 流程步骤 ── */
|
||
.v3-zone {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.v3-steps {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 0;
|
||
}
|
||
|
||
@media (max-width: 560px) {
|
||
.v3-steps {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.v3-step__connector {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.v3-step {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 12px 14px;
|
||
border: 2px solid var(--border);
|
||
background: #fff;
|
||
text-align: left;
|
||
transition: border-color 0.15s, background 0.15s;
|
||
}
|
||
|
||
.v3-step:first-child {
|
||
border-radius: 10px 0 0 10px;
|
||
}
|
||
|
||
.v3-step:last-child {
|
||
border-radius: 0 10px 10px 0;
|
||
}
|
||
|
||
.v3-step:hover:not(.is-active) {
|
||
background: #fafafa;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.v3-step.is-active[data-source='reception'] {
|
||
border-color: var(--reception);
|
||
background: var(--reception-bg);
|
||
}
|
||
|
||
.v3-step.is-active[data-source='production'] {
|
||
border-color: var(--production);
|
||
background: var(--production-bg);
|
||
}
|
||
|
||
.v3-step:not(.is-active) {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.v3-step:not(.is-active)::after {
|
||
content: '点击切换至此工单';
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.v3-step__connector {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
flex-shrink: 0;
|
||
color: #94a3b8;
|
||
background: #f1f5f9;
|
||
border-top: 2px solid var(--border);
|
||
border-bottom: 2px solid var(--border);
|
||
}
|
||
|
||
.v3-step__num {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.v3-step__name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v3-step__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* ── V4 全宽分段控制器 ── */
|
||
.v4-zone {
|
||
padding: 10px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.v4-segment {
|
||
display: flex;
|
||
width: 100%;
|
||
padding: 4px;
|
||
gap: 4px;
|
||
background: #e2e8f0;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.v4-segment__btn {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 10px 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||
}
|
||
|
||
.v4-segment__btn:hover:not(.is-active) {
|
||
background: rgb(255 255 255 / 60%);
|
||
color: var(--text);
|
||
}
|
||
|
||
.v4-segment__btn.is-active {
|
||
background: #fff;
|
||
color: var(--text);
|
||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.1);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v4-segment__btn.is-active[data-source='reception'] .v4-segment__type {
|
||
color: var(--reception);
|
||
}
|
||
|
||
.v4-segment__btn.is-active[data-source='production'] .v4-segment__type {
|
||
color: var(--production);
|
||
}
|
||
|
||
.v4-segment__type {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.v4-segment__sub {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
font-family: ui-monospace, Menlo, monospace;
|
||
}
|
||
|
||
/* ── V5 左侧竖条 + 大标签(推荐) ── */
|
||
.v5-zone {
|
||
padding: 10px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.v5-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.v5-prefix {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.v5-tabs {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.v5-tab {
|
||
display: inline-flex;
|
||
align-items: stretch;
|
||
min-height: 44px;
|
||
padding: 0;
|
||
border: 2px solid var(--border);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
overflow: hidden;
|
||
transition: border-color 0.15s, box-shadow 0.15s;
|
||
}
|
||
|
||
.v5-tab:hover:not(.is-active) {
|
||
border-color: #94a3b8;
|
||
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.v5-tab.is-active {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 1px var(--primary);
|
||
}
|
||
|
||
.v5-tab.is-active[data-source='reception'] {
|
||
border-color: var(--reception);
|
||
box-shadow: 0 0 0 1px var(--reception);
|
||
}
|
||
|
||
.v5-tab.is-active[data-source='production'] {
|
||
border-color: var(--production);
|
||
box-shadow: 0 0 0 1px var(--production);
|
||
}
|
||
|
||
.v5-tab__accent {
|
||
width: 5px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.v5-tab[data-source='reception'] .v5-tab__accent {
|
||
background: var(--reception);
|
||
}
|
||
|
||
.v5-tab[data-source='production'] .v5-tab__accent {
|
||
background: var(--production);
|
||
}
|
||
|
||
.v5-tab__body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
padding: 8px 14px 8px 10px;
|
||
text-align: left;
|
||
}
|
||
|
||
.v5-tab__type {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.v5-tab__meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.v5-tab__meta code {
|
||
font-family: ui-monospace, Menlo, monospace;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.v5-tab:not(.is-active) .v5-tab__hint {
|
||
font-size: 10px;
|
||
color: var(--primary-dark);
|
||
font-weight: 500;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="demo-banner">
|
||
<div>
|
||
<h1>质检工单 · 细胞存储 · 同样本多工单 Tab 选型</h1>
|
||
<p>
|
||
场景:同一样本编码下存在<strong>样本接收</strong>与<strong>生产过程</strong>两条质检工单,需在卡片内切换查看。
|
||
下方 6 套方案均可点击交互,请对比哪套最能传达「可切换、类别不同」。
|
||
</p>
|
||
</div>
|
||
<div class="demo-banner__actions">
|
||
<a class="demo-link" href="index.html">← 索引</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="page-container">
|
||
<div class="compare-stack">
|
||
<!-- V0 -->
|
||
<section class="compare-block" data-variant="v0">
|
||
<div class="compare-block__label">
|
||
<h2>
|
||
<span class="variant-tag variant-tag--baseline">V0</span>
|
||
现网基线(当前实现)
|
||
</h2>
|
||
<p>胶囊 Tab 内嵌序号 + 来源标签 + 工单号 + 状态;未选中时来源与状态均灰化,类别差异不明显。</p>
|
||
<div class="pros-cons">
|
||
<div class="con">✗ 未选中 Tab 全灰,样本接收 / 生产过程难以区分</div>
|
||
<div class="con">✗ 交互感弱,易被当成静态信息条</div>
|
||
</div>
|
||
</div>
|
||
<div class="compare-block__body" id="block-v0"></div>
|
||
</section>
|
||
|
||
<!-- V1 -->
|
||
<section class="compare-block" data-variant="v1">
|
||
<div class="compare-block__label">
|
||
<h2>
|
||
<span class="variant-tag">V1</span>
|
||
分类色保留 + 引导文案 +「点击查看」
|
||
</h2>
|
||
<p>渐进改良:保留横向 Tab 布局,未选中仍保留绿/琥珀来源色,并加一行引导 + 未选中 Tab 后缀「点击查看」。</p>
|
||
<div class="pros-cons">
|
||
<div class="pro">✓ 改动量小,与现网结构最接近</div>
|
||
<div class="pro">✓ 类别色始终可见</div>
|
||
<div class="con">✗ 引导文案熟悉后可能略冗余</div>
|
||
</div>
|
||
</div>
|
||
<div class="compare-block__body" id="block-v1"></div>
|
||
</section>
|
||
|
||
<!-- V5 推荐放前面一点 - actually keep order logical -->
|
||
|
||
<!-- V2 -->
|
||
<section class="compare-block" data-variant="v2">
|
||
<div class="compare-block__label">
|
||
<h2>
|
||
<span class="variant-tag variant-tag--rec">推荐</span>
|
||
<span class="variant-tag">V2</span>
|
||
双卡片切换器
|
||
</h2>
|
||
<p>两条工单并排展示为可点击卡片,图标 + 大标题区分来源,未选中角标「点击切换」,选中侧渐变高亮。</p>
|
||
<div class="pros-cons">
|
||
<div class="pro">✓ 可点击性最强,两类工单一眼可辨</div>
|
||
<div class="pro">✓ 移动端可纵向堆叠</div>
|
||
<div class="con">✗ 占用垂直空间略多</div>
|
||
</div>
|
||
</div>
|
||
<div class="compare-block__body" id="block-v2"></div>
|
||
</section>
|
||
|
||
<!-- V3 -->
|
||
<section class="compare-block" data-variant="v3">
|
||
<div class="compare-block__label">
|
||
<h2><span class="variant-tag">V3</span> 流程步骤条(接收 → 生产)</h2>
|
||
<p>强调业务先后顺序:样本接收在前、生产过程在后,中间箭头连接;未选中步骤提示「点击切换至此工单」。</p>
|
||
<div class="pros-cons">
|
||
<div class="pro">✓ 传达「同一流程两阶段」的业务语义</div>
|
||
<div class="con">✗ 若未来出现 3+ 条工单,横向会拥挤</div>
|
||
</div>
|
||
</div>
|
||
<div class="compare-block__body" id="block-v3"></div>
|
||
</section>
|
||
|
||
<!-- V4 -->
|
||
<section class="compare-block" data-variant="v4">
|
||
<div class="compare-block__label">
|
||
<h2><span class="variant-tag">V4</span> 全宽分段控制器(iOS Segmented)</h2>
|
||
<p>类似系统分段控件,占满卡片宽度;选中项白底浮起,类型名加粗,下方显示工单号与状态。</p>
|
||
<div class="pros-cons">
|
||
<div class="pro">✓ 切换控件形态 familiar,点击区域大</div>
|
||
<div class="con">✗ 类别色仅体现在选中态文字,未选中时仍偏中性</div>
|
||
</div>
|
||
</div>
|
||
<div class="compare-block__body" id="block-v4"></div>
|
||
</section>
|
||
|
||
<!-- V5 -->
|
||
<section class="compare-block" data-variant="v5">
|
||
<div class="compare-block__label">
|
||
<h2>
|
||
<span class="variant-tag variant-tag--rec">推荐</span>
|
||
<span class="variant-tag">V5</span>
|
||
左侧色条 + 大标签 Tab
|
||
</h2>
|
||
<p>
|
||
「切换工单:」前缀 + 带左侧竖条(绿=接收、琥珀=生产)的大按钮;未选中显示「点此查看」微提示,选中展示完整工单号与状态。
|
||
</p>
|
||
<div class="pros-cons">
|
||
<div class="pro">✓ 类别色条 + 44px 触控高度,可发现性与无障碍兼顾</div>
|
||
<div class="pro">✓ 信息密度适中,适合 2~3 条工单</div>
|
||
<div class="con">✗ 比 V0 略占高度</div>
|
||
</div>
|
||
</div>
|
||
<div class="compare-block__body" id="block-v5"></div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const MOCK = {
|
||
sampleCode: 'YB-2025-0618-0042',
|
||
subtitle: '张三 · 间充质干细胞 · 5×10⁶/支',
|
||
workOrders: [
|
||
{
|
||
id: 'wo-reception',
|
||
source: 'reception',
|
||
label: '样本接收',
|
||
no: 'QC-IN-20250618-0012',
|
||
status: 'ywc',
|
||
statusLabel: '已完成',
|
||
statusClass: 'tag--success'
|
||
},
|
||
{
|
||
id: 'wo-production',
|
||
source: 'production',
|
||
label: '生产过程',
|
||
no: 'QC-IN-20250620-0038',
|
||
status: 'ypz',
|
||
statusLabel: '已配置',
|
||
statusClass: 'tag--warning'
|
||
}
|
||
]
|
||
}
|
||
|
||
const ICONS = {
|
||
reception: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
|
||
production: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>`
|
||
}
|
||
|
||
function ctxHeader() {
|
||
return `
|
||
<div class="qc-sample-row__ctx">
|
||
<span class="qc-sample-row__idx">1</span>
|
||
<span class="qc-sample-row__code">${MOCK.sampleCode}</span>
|
||
<span class="qc-sample-row__sub">${MOCK.subtitle}</span>
|
||
<span class="qc-sample-row__badge">${MOCK.workOrders.length} 条质检</span>
|
||
</div>`
|
||
}
|
||
|
||
function cardPlaceholder(wo) {
|
||
return `
|
||
<div class="qc-card-placeholder">
|
||
<div class="qc-card-placeholder__head">
|
||
<span class="qc-card-placeholder__no">${wo.no}</span>
|
||
<span class="tag ${wo.statusClass}">${wo.statusLabel}</span>
|
||
<span class="tag tag--${wo.source}">${wo.label}</span>
|
||
</div>
|
||
<div class="qc-card-placeholder__meta">
|
||
<span><strong>触发来源</strong>${wo.label}</span>
|
||
<span><strong>细胞类型</strong>间充质干细胞</span>
|
||
<span><strong>创建时间</strong>2025-06-18 14:32</span>
|
||
</div>
|
||
<div class="qc-card-placeholder__tasks">▼ 质检任务列表(当前选中:${wo.label})</div>
|
||
</div>`
|
||
}
|
||
|
||
function wrapSampleRow(inner, activeWo) {
|
||
return `
|
||
<article class="qc-sample-row">
|
||
${ctxHeader()}
|
||
${inner}
|
||
${cardPlaceholder(activeWo)}
|
||
</article>`
|
||
}
|
||
|
||
/* ── V0 ── */
|
||
function renderV0(activeId) {
|
||
const active = MOCK.workOrders.find((w) => w.id === activeId)
|
||
const tabs = MOCK.workOrders
|
||
.map(
|
||
(wo, i) => `
|
||
<button type="button" class="v0-tabs__btn tab-btn ${wo.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo.id}" role="tab" aria-selected="${wo.id === activeId}">
|
||
<span class="v0-index">${i + 1}</span>
|
||
<span class="v0-source"><span class="v0-source__dot"></span>${wo.label}</span>
|
||
<span class="v0-no">${wo.no}</span>
|
||
<span class="tag ${wo.statusClass}">${wo.statusLabel}</span>
|
||
</button>`
|
||
)
|
||
.join('')
|
||
return wrapSampleRow(
|
||
`<div class="v0-tabs"><div class="v0-tabs__track" role="tablist">${tabs}</div></div>`,
|
||
active
|
||
)
|
||
}
|
||
|
||
/* ── V1 ── */
|
||
function renderV1(activeId) {
|
||
const active = MOCK.workOrders.find((w) => w.id === activeId)
|
||
const tabs = MOCK.workOrders
|
||
.map(
|
||
(wo) => `
|
||
<button type="button" class="v1-tabs__btn tab-btn ${wo.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo.id}" data-source="${wo.source}" role="tab">
|
||
<span class="v1-source-badge v1-source-badge--${wo.source}">${wo.label}</span>
|
||
<span style="font-family:ui-monospace,Menlo,monospace;font-size:12px">${wo.no}</span>
|
||
<span class="tag ${wo.statusClass}">${wo.statusLabel}</span>
|
||
</button>`
|
||
)
|
||
.join('')
|
||
return wrapSampleRow(
|
||
`<div class="v1-zone">
|
||
<div class="v1-hint">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
||
同一样本含 <strong>${MOCK.workOrders.length} 条质检工单</strong>,点击下方切换查看
|
||
</div>
|
||
<div class="v1-tabs__track" role="tablist">${tabs}</div>
|
||
</div>`,
|
||
active
|
||
)
|
||
}
|
||
|
||
/* ── V2 ── */
|
||
function renderV2(activeId) {
|
||
const active = MOCK.workOrders.find((w) => w.id === activeId)
|
||
const cards = MOCK.workOrders
|
||
.map(
|
||
(wo) => `
|
||
<button type="button" class="v2-card tab-btn ${wo.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo.id}" data-source="${wo.source}">
|
||
<div class="v2-card__top">
|
||
<span class="v2-card__icon">${ICONS[wo.source]}</span>
|
||
<div>
|
||
<div class="v2-card__title">${wo.label}</div>
|
||
<div class="v2-card__no">${wo.no}</div>
|
||
</div>
|
||
</div>
|
||
<div class="v2-card__foot">
|
||
<span class="tag ${wo.statusClass}">${wo.statusLabel}</span>
|
||
</div>
|
||
</button>`
|
||
)
|
||
.join('')
|
||
return wrapSampleRow(
|
||
`<div class="v2-zone">
|
||
<div class="v2-label">选择要查看的质检工单</div>
|
||
<div class="v2-grid">${cards}</div>
|
||
</div>`,
|
||
active
|
||
)
|
||
}
|
||
|
||
/* ── V3 ── */
|
||
function renderV3(activeId) {
|
||
const active = MOCK.workOrders.find((w) => w.id === activeId)
|
||
const [wo1, wo2] = MOCK.workOrders
|
||
return wrapSampleRow(
|
||
`<div class="v3-zone">
|
||
<div class="v3-steps" role="tablist">
|
||
<button type="button" class="v3-step tab-btn ${wo1.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo1.id}" data-source="${wo1.source}" role="tab">
|
||
<span class="v3-step__num">阶段 1</span>
|
||
<span class="v3-step__name">${wo1.label}</span>
|
||
<span class="v3-step__row">
|
||
<code style="font-size:11px">${wo1.no}</code>
|
||
<span class="tag ${wo1.statusClass}">${wo1.statusLabel}</span>
|
||
</span>
|
||
</button>
|
||
<div class="v3-step__connector" aria-hidden="true">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||
</div>
|
||
<button type="button" class="v3-step tab-btn ${wo2.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo2.id}" data-source="${wo2.source}" role="tab">
|
||
<span class="v3-step__num">阶段 2</span>
|
||
<span class="v3-step__name">${wo2.label}</span>
|
||
<span class="v3-step__row">
|
||
<code style="font-size:11px">${wo2.no}</code>
|
||
<span class="tag ${wo2.statusClass}">${wo2.statusLabel}</span>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>`,
|
||
active
|
||
)
|
||
}
|
||
|
||
/* ── V4 ── */
|
||
function renderV4(activeId) {
|
||
const active = MOCK.workOrders.find((w) => w.id === activeId)
|
||
const btns = MOCK.workOrders
|
||
.map(
|
||
(wo) => `
|
||
<button type="button" class="v4-segment__btn tab-btn ${wo.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo.id}" data-source="${wo.source}" role="tab">
|
||
<span class="v4-segment__type">${wo.label}</span>
|
||
<span class="v4-segment__sub">
|
||
<span>${wo.no.slice(-8)}</span>
|
||
<span class="tag ${wo.statusClass}" style="height:18px;padding:0 6px;font-size:10px">${wo.statusLabel}</span>
|
||
</span>
|
||
</button>`
|
||
)
|
||
.join('')
|
||
return wrapSampleRow(`<div class="v4-zone"><div class="v4-segment" role="tablist">${btns}</div></div>`, active)
|
||
}
|
||
|
||
/* ── V5 ── */
|
||
function renderV5(activeId) {
|
||
const active = MOCK.workOrders.find((w) => w.id === activeId)
|
||
const tabs = MOCK.workOrders
|
||
.map(
|
||
(wo) => `
|
||
<button type="button" class="v5-tab tab-btn ${wo.id === activeId ? 'is-active' : ''}"
|
||
data-id="${wo.id}" data-source="${wo.source}" role="tab">
|
||
<span class="v5-tab__accent"></span>
|
||
<span class="v5-tab__body">
|
||
<span class="v5-tab__type">${wo.label}</span>
|
||
<span class="v5-tab__meta">
|
||
${wo.id === activeId ? `<code>${wo.no}</code><span class="tag ${wo.statusClass}">${wo.statusLabel}</span>` : `<span class="v5-tab__hint">点此查看 →</span>`}
|
||
</span>
|
||
</span>
|
||
</button>`
|
||
)
|
||
.join('')
|
||
return wrapSampleRow(
|
||
`<div class="v5-zone">
|
||
<div class="v5-row">
|
||
<span class="v5-prefix">切换工单</span>
|
||
<div class="v5-tabs" role="tablist">${tabs}</div>
|
||
</div>
|
||
</div>`,
|
||
active
|
||
)
|
||
}
|
||
|
||
const VARIANTS = {
|
||
v0: { render: renderV0, default: 'wo-reception' },
|
||
v1: { render: renderV1, default: 'wo-reception' },
|
||
v2: { render: renderV2, default: 'wo-reception' },
|
||
v3: { render: renderV3, default: 'wo-reception' },
|
||
v4: { render: renderV4, default: 'wo-reception' },
|
||
v5: { render: renderV5, default: 'wo-reception' }
|
||
}
|
||
|
||
const state = {}
|
||
Object.keys(VARIANTS).forEach((k) => {
|
||
state[k] = VARIANTS[k].default
|
||
})
|
||
|
||
function mountVariant(key) {
|
||
const el = document.getElementById(`block-${key}`)
|
||
el.innerHTML = VARIANTS[key].render(state[key])
|
||
el.querySelectorAll('[data-id]').forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
state[key] = btn.dataset.id
|
||
mountVariant(key)
|
||
})
|
||
})
|
||
}
|
||
|
||
Object.keys(VARIANTS).forEach(mountVariant)
|
||
</script>
|
||
</body>
|
||
</html>
|