Files
zkxw/docs/demo-source-chain-graph.html
T
2026-05-24 16:58:51 +08:00

732 lines
26 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>
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-light: #ecfdf5;
--primary-border: #a7f3d0;
--text: #1f2937;
--text-muted: #6b7280;
--text-light: #9ca3af;
--sample-bg: #eff6ff;
--sample-border: #93c5fd;
--line-color: #d1d5db;
--line-active: #10b981;
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-family);
background: #f9fafb;
color: var(--text);
padding: 24px;
}
h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
.subtitle { font-size: 13px; color: var(--text-muted); margin-bottom: 24px; }
.scenario-tabs {
display: flex;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.scenario-tab {
padding: 6px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
border: 1px solid var(--line-color);
background: white;
color: var(--text-muted);
transition: all 0.2s;
}
.scenario-tab:hover { border-color: var(--primary); color: var(--primary); }
.scenario-tab.active { background: var(--primary); color: white; border-color: var(--primary); }
.scenario-desc {
font-size: 13px;
color: var(--text-muted);
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 10px 16px;
margin-bottom: 20px;
line-height: 1.6;
}
.scenario-desc strong { color: var(--text); }
.scenario-panel { display: none; }
.scenario-panel.active { display: block; }
.chain-container {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 32px 24px 24px;
overflow-x: auto;
min-height: 200px;
}
.chain-section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 6px;
}
.chain-section-title::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background: var(--primary);
border-radius: 2px;
}
.chain-row {
display: flex;
align-items: center;
gap: 0;
position: relative;
}
.chain-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
z-index: 2;
flex-shrink: 0;
}
.node-card {
width: 148px;
padding: 10px 12px;
border-radius: 8px;
border: 1.5px solid var(--primary-border);
background: var(--primary-light);
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.node-card:hover {
box-shadow: 0 4px 12px rgba(16,185,129,0.2);
transform: translateY(-1px);
}
.node-card.sample {
background: var(--sample-bg);
border-color: var(--sample-border);
}
.node-card.sample:hover {
box-shadow: 0 4px 12px rgba(147,197,253,0.3);
}
.node-card.current {
border-color: var(--primary);
border-width: 2px;
box-shadow: 0 0 0 3px rgba(16,185,129,0.15);
}
.node-card.current::after {
content: '当前';
position: absolute;
top: -10px;
right: -8px;
background: var(--primary);
color: white;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
font-weight: 500;
}
.node-type-tag {
display: inline-block;
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
margin-bottom: 4px;
font-weight: 500;
}
.node-type-tag.product { background: #d1fae5; color: #065f46; }
.node-type-tag.sample-type { background: #dbeafe; color: #1e40af; }
.node-code { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.node-name { font-size: 11px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.node-passage { font-size: 10px; color: var(--text-light); margin-top: 2px; }
/* 连线 — 加长到 180px */
.chain-connector {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
min-width: 180px;
height: 60px;
z-index: 1;
flex-shrink: 0;
}
.connector-label {
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
font-size: 10px;
color: var(--text-muted);
white-space: nowrap;
padding: 3px 8px;
background: white;
border: 1px solid #f3f4f6;
border-radius: 4px;
position: absolute;
top: 2px;
left: 50%;
transform: translateX(-50%);
}
.connector-label .wo { color: var(--primary-dark); font-weight: 500; }
.connector-label .task { color: var(--text-light); }
.connector-line {
position: absolute;
bottom: 14px;
left: 0;
right: 0;
height: 2px;
background: var(--line-color);
}
.connector-line.active { background: var(--line-active); }
.connector-arrow {
position: absolute;
bottom: 10px;
right: -5px;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 7px solid var(--line-color);
}
.connector-line.active + .connector-arrow { border-left-color: var(--line-active); }
/* 分叉 */
.fork-area {
display: flex;
flex-direction: column;
position: relative;
margin-left: 0;
}
.fork-row {
display: flex;
align-items: center;
position: relative;
min-height: 72px;
padding-left: 40px;
}
.fork-area::before {
content: '';
position: absolute;
left: 18px;
top: 36px;
bottom: 36px;
width: 2px;
background: var(--line-color);
}
.fork-row::before {
content: '';
position: absolute;
left: 18px;
top: 50%;
width: 22px;
height: 2px;
background: var(--line-color);
}
.fork-row::after {
content: '';
position: absolute;
left: 18px;
top: 0;
height: 50%;
width: 2px;
background: white;
z-index: 1;
}
.fork-row:last-child::after {
top: auto;
bottom: 0;
}
.fork-row:first-child::after {
top: 0;
bottom: auto;
height: 50%;
background: white;
}
/* 图例 */
.legend {
display: flex;
gap: 20px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f3f4f6;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 3px;
border: 1.5px solid;
}
.legend-dot.product { background: var(--primary-light); border-color: var(--primary-border); }
.legend-dot.sample { background: var(--sample-bg); border-color: var(--sample-border); }
.legend-dot.current-node { background: var(--primary-light); border-color: var(--primary); border-width: 2px; }
.legend-dot.line-legend { background: transparent; border: none; width: 24px; height: 2px; border-radius: 0; background: var(--line-active); }
.popover-detail {
display: none;
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
min-width: 200px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
z-index: 100;
font-size: 12px;
}
.popover-detail.visible { display: block; }
.popover-detail .detail-row { display: flex; justify-content: space-between; padding: 3px 0; }
.popover-detail .detail-label { color: var(--text-muted); }
.popover-detail .detail-value { color: var(--text); font-weight: 500; }
.empty-state {
text-align: center;
padding: 40px;
color: var(--text-muted);
font-size: 13px;
}
.empty-state .empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.4; }
.sub-label {
font-size: 11px;
color: var(--text-light);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 4px;
}
.sub-label .arrow { color: var(--primary); }
</style>
</head>
<body>
<h1>细胞产品来源链路 — 设计 Demo</h1>
<p class="subtitle">展示细胞产品查看弹窗中的「来源链路」可视化方案。点击任意节点可查看详情。</p>
<div class="scenario-tabs">
<div class="scenario-tab active" onclick="switchScenario('s1', this)">场景1:简单链路</div>
<div class="scenario-tab" onclick="switchScenario('s2', this)">场景2:一生多(一个产品产出多个新产品)</div>
<div class="scenario-tab" onclick="switchScenario('s3', this)">场景3:完整追溯(往前看祖先 + 往后看子代)</div>
<div class="scenario-tab" onclick="switchScenario('s4', this)">场景4:手动录入(无祖先,但有子代)</div>
</div>
<div id="desc-s1" class="scenario-desc active">
<strong>场景1 说明:</strong>最简单的情况。一个样本经过一次生产工单,产出一个细胞产品(P1代)。这个P1代产品又经过另一个生产工单,产出了当前查看的P2代产品。每个节点之间只有一条连线。
</div>
<div id="desc-s2" class="scenario-desc" style="display:none;">
<strong>场景2 说明:</strong>当前产品(P1代)被进一步拿去做生产,<strong>一次生产产出了 3 个不同的新细胞产品</strong>。就像一块面团可以同时做出多个包子 — 连线从当前产品分叉出去,每条分叉线上面标注了对应的工单号和任务号。这就是「一生多」。
</div>
<div id="desc-s3" class="scenario-desc" style="display:none;">
<strong>场景3 说明:</strong>最完整的情况。全部在<strong>同一行</strong>展示 — 从左到右:<strong>样本 → P1(父产品) → P2(当前) → 分叉出2个P3(子产品)</strong>。祖先在左边,当前在中间,子代直接从当前节点往右延伸分叉。不分两行,一条链路从头看到尾。
</div>
<div id="desc-s4" class="scenario-desc" style="display:none;">
<strong>场景4 说明:</strong>手动录入的细胞产品是「起源」— 它没有祖先(不是从样本或别的产品生产来的),但后续它可以被拿去生产新的子产品。所以它左边没有祖先链路,但右边会分叉出子代。只有从未被用于生产的「孤立产品」才会显示空状态。
</div>
<div id="s1" class="scenario-panel active">
<div class="chain-container">
<div class="chain-section-title">来源链路</div>
<div class="chain-row">
<div class="chain-node">
<div class="node-card sample" onclick="togglePopover(this)">
<span class="node-type-tag sample-type">样本</span>
<div class="node-code">ZKZX34000627</div>
<div class="node-name">脐带血干细胞</div>
</div>
</div>
<div class="chain-connector">
<div class="connector-label">
<span class="wo">工单 WO20260519001</span>
<span class="task">任务 T20260519001</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260520001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P1 代</div>
</div>
</div>
<div class="chain-connector">
<div class="connector-label">
<span class="wo">工单 WO20260521002</span>
<span class="task">任务 T20260521002</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card current" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260522001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P2 代</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot sample"></div>样本</div>
<div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
<div class="legend-item"><div class="legend-dot current-node"></div>当前查看</div>
<div class="legend-item"><div class="legend-dot line-legend"></div>生产连线(标注工单/任务号)</div>
</div>
</div>
</div>
<div id="s2" class="scenario-panel">
<div class="chain-container">
<div class="chain-section-title">来源链路</div>
<div class="sub-label"><span class="arrow">●</span> 当前产品被拿去生产,一次产出了 3 个新产品(分叉)</div>
<div class="chain-row">
<div class="chain-node">
<div class="node-card sample" onclick="togglePopover(this)">
<span class="node-type-tag sample-type">样本</span>
<div class="node-code">ZKZX34000627</div>
<div class="node-name">脐带血干细胞</div>
</div>
</div>
<div class="chain-connector">
<div class="connector-label">
<span class="wo">工单 WO20260519001</span>
<span class="task">任务 T20260519001</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card current" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260520001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P1 代</div>
</div>
</div>
<div class="fork-area">
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260522003</span>
<span class="task">任务 T20260522003</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260523001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P2 代</div>
</div>
</div>
</div>
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260522003</span>
<span class="task">任务 T20260522004</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260523002</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P2 代</div>
</div>
</div>
</div>
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260522005</span>
<span class="task">任务 T20260522005</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260523003</div>
<div class="node-name">围产期造血干细胞</div>
<div class="node-passage">P1 代</div>
</div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot sample"></div>样本</div>
<div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
<div class="legend-item"><div class="legend-dot current-node"></div>当前查看</div>
<div class="legend-item"><div class="legend-dot line-legend"></div>生产连线</div>
</div>
</div>
</div>
<div id="s3" class="scenario-panel">
<div class="chain-container">
<div class="chain-section-title">来源链路(完整双向追溯)</div>
<div class="sub-label"><span class="arrow">←</span> 祖先(从哪来)<span style="margin: 0 12px; color: #d1d5db;">|</span> 当前产品 <span style="margin: 0 12px; color: #d1d5db;">|</span> <span class="arrow">→</span> 子代(被拿去做什么)— 全部在同一行</div>
<div class="chain-row">
<div class="chain-node">
<div class="node-card sample" onclick="togglePopover(this)">
<span class="node-type-tag sample-type">样本</span>
<div class="node-code">ZKZX34000627</div>
<div class="node-name">脐带血干细胞</div>
</div>
</div>
<div class="chain-connector">
<div class="connector-label">
<span class="wo">工单 WO20260519001</span>
<span class="task">任务 T20260519001</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260520001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P1 代(父产品)</div>
</div>
</div>
<div class="chain-connector">
<div class="connector-label">
<span class="wo">工单 WO20260521002</span>
<span class="task">任务 T20260521002</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card current" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260522001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P2 代(当前)</div>
</div>
</div>
<div class="fork-area">
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260523003</span>
<span class="task">任务 T20260523003</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260524001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P3 代(子产品1)</div>
</div>
</div>
</div>
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260523004</span>
<span class="task">任务 T20260523004</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260524002</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P3 代(子产品2)</div>
</div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot sample"></div>样本</div>
<div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
<div class="legend-item"><div class="legend-dot current-node"></div>当前查看</div>
<div class="legend-item"><div class="legend-dot line-legend"></div>生产连线</div>
</div>
</div>
</div>
<div id="s4" class="scenario-panel">
<div class="chain-container">
<div class="chain-section-title">来源链路</div>
<div class="sub-label"><span class="arrow">●</span> 手动录入产品(起源节点),无祖先,但后续被拿去生产了子产品</div>
<div class="chain-row">
<div class="chain-node">
<div class="node-card current" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260520001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">手动录入(起源)</div>
</div>
</div>
<div class="fork-area">
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260522003</span>
<span class="task">任务 T20260522003</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260523001</div>
<div class="node-name">脐带间充质干细胞</div>
<div class="node-passage">P1 代</div>
</div>
</div>
</div>
<div class="fork-row">
<div class="chain-connector" style="min-width:160px;">
<div class="connector-label">
<span class="wo">工单 WO20260522004</span>
<span class="task">任务 T20260522004</span>
</div>
<div class="connector-line active"></div>
<div class="connector-arrow"></div>
</div>
<div class="chain-node">
<div class="node-card" onclick="togglePopover(this)">
<span class="node-type-tag product">细胞产品</span>
<div class="node-code">CP20260523002</div>
<div class="node-name">围产期造血干细胞</div>
<div class="node-passage">P1 代</div>
</div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
<div class="legend-item"><div class="legend-dot current-node"></div>当前查看(手动录入/起源)</div>
<div class="legend-item"><div class="legend-dot line-legend"></div>生产连线</div>
</div>
</div>
</div>
<script>
function switchScenario(id, tab) {
document.querySelectorAll('.scenario-panel').forEach(function(p) { p.classList.remove('active'); });
document.querySelectorAll('.scenario-tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.scenario-desc').forEach(function(d) { d.style.display = 'none'; });
document.getElementById(id).classList.add('active');
tab.classList.add('active');
var descMap = { s1: 'desc-s1', s2: 'desc-s2', s3: 'desc-s3', s4: 'desc-s4' };
document.getElementById(descMap[id]).style.display = 'block';
}
function togglePopover(card) {
var existing = card.querySelector('.popover-detail');
if (existing) { existing.classList.toggle('visible'); return; }
var code = card.querySelector('.node-code') ? card.querySelector('.node-code').textContent : '';
var name = card.querySelector('.node-name') ? card.querySelector('.node-name').textContent : '';
var isCurrent = card.classList.contains('current');
var isSample = card.classList.contains('sample');
var passage = card.querySelector('.node-passage') ? card.querySelector('.node-passage').textContent : '';
var status = isSample ? '已入库' : '冷冻中';
var popover = document.createElement('div');
popover.className = 'popover-detail visible';
var html = '<div class="detail-row"><span class="detail-label">编码</span><span class="detail-value">' + code + '</span></div>';
html += '<div class="detail-row"><span class="detail-label">名称</span><span class="detail-value">' + name + '</span></div>';
html += '<div class="detail-row"><span class="detail-label">状态</span><span class="detail-value">' + status + '</span></div>';
if (!isSample) { html += '<div class="detail-row"><span class="detail-label">类型</span><span class="detail-value">细胞产品</span></div>'; }
if (passage) { html += '<div class="detail-row"><span class="detail-label">代次</span><span class="detail-value">' + passage + '</span></div>'; }
if (isCurrent) { html += '<div class="detail-row"><span class="detail-label">标识</span><span class="detail-value" style="color:#10b981">当前查看</span></div>'; }
popover.innerHTML = html;
card.appendChild(popover);
setTimeout(function() {
document.addEventListener('click', function close(e) {
if (!popover.contains(e.target) && e.target !== card) {
popover.classList.remove('visible');
document.removeEventListener('click', close);
}
});
}, 10);
}
</script>
</body>
</html>