demo-source-chain-graph.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>细胞产品来源链路 Demo</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-light: #ecfdf5;
  12. --primary-border: #a7f3d0;
  13. --text: #1f2937;
  14. --text-muted: #6b7280;
  15. --text-light: #9ca3af;
  16. --sample-bg: #eff6ff;
  17. --sample-border: #93c5fd;
  18. --line-color: #d1d5db;
  19. --line-active: #10b981;
  20. --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  21. }
  22. * { margin: 0; padding: 0; box-sizing: border-box; }
  23. body {
  24. font-family: var(--font-family);
  25. background: #f9fafb;
  26. color: var(--text);
  27. padding: 24px;
  28. }
  29. h1 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
  30. .subtitle { font-size: 13px; color: var(--text-muted); margin-bottom: 24px; }
  31. .scenario-tabs {
  32. display: flex;
  33. gap: 8px;
  34. margin-bottom: 8px;
  35. flex-wrap: wrap;
  36. }
  37. .scenario-tab {
  38. padding: 6px 16px;
  39. border-radius: 6px;
  40. font-size: 13px;
  41. cursor: pointer;
  42. border: 1px solid var(--line-color);
  43. background: white;
  44. color: var(--text-muted);
  45. transition: all 0.2s;
  46. }
  47. .scenario-tab:hover { border-color: var(--primary); color: var(--primary); }
  48. .scenario-tab.active { background: var(--primary); color: white; border-color: var(--primary); }
  49. .scenario-desc {
  50. font-size: 13px;
  51. color: var(--text-muted);
  52. background: #fffbeb;
  53. border: 1px solid #fde68a;
  54. border-radius: 8px;
  55. padding: 10px 16px;
  56. margin-bottom: 20px;
  57. line-height: 1.6;
  58. }
  59. .scenario-desc strong { color: var(--text); }
  60. .scenario-panel { display: none; }
  61. .scenario-panel.active { display: block; }
  62. .chain-container {
  63. background: white;
  64. border: 1px solid #e5e7eb;
  65. border-radius: 12px;
  66. padding: 32px 24px 24px;
  67. overflow-x: auto;
  68. min-height: 200px;
  69. }
  70. .chain-section-title {
  71. font-size: 13px;
  72. font-weight: 600;
  73. color: var(--text-muted);
  74. margin-bottom: 20px;
  75. display: flex;
  76. align-items: center;
  77. gap: 6px;
  78. }
  79. .chain-section-title::before {
  80. content: '';
  81. display: inline-block;
  82. width: 3px;
  83. height: 14px;
  84. background: var(--primary);
  85. border-radius: 2px;
  86. }
  87. .chain-row {
  88. display: flex;
  89. align-items: center;
  90. gap: 0;
  91. position: relative;
  92. }
  93. .chain-node {
  94. position: relative;
  95. display: flex;
  96. flex-direction: column;
  97. align-items: center;
  98. z-index: 2;
  99. flex-shrink: 0;
  100. }
  101. .node-card {
  102. width: 148px;
  103. padding: 10px 12px;
  104. border-radius: 8px;
  105. border: 1.5px solid var(--primary-border);
  106. background: var(--primary-light);
  107. cursor: pointer;
  108. transition: all 0.2s;
  109. position: relative;
  110. }
  111. .node-card:hover {
  112. box-shadow: 0 4px 12px rgba(16,185,129,0.2);
  113. transform: translateY(-1px);
  114. }
  115. .node-card.sample {
  116. background: var(--sample-bg);
  117. border-color: var(--sample-border);
  118. }
  119. .node-card.sample:hover {
  120. box-shadow: 0 4px 12px rgba(147,197,253,0.3);
  121. }
  122. .node-card.current {
  123. border-color: var(--primary);
  124. border-width: 2px;
  125. box-shadow: 0 0 0 3px rgba(16,185,129,0.15);
  126. }
  127. .node-card.current::after {
  128. content: '当前';
  129. position: absolute;
  130. top: -10px;
  131. right: -8px;
  132. background: var(--primary);
  133. color: white;
  134. font-size: 10px;
  135. padding: 1px 6px;
  136. border-radius: 4px;
  137. font-weight: 500;
  138. }
  139. .node-type-tag {
  140. display: inline-block;
  141. font-size: 10px;
  142. padding: 1px 5px;
  143. border-radius: 3px;
  144. margin-bottom: 4px;
  145. font-weight: 500;
  146. }
  147. .node-type-tag.product { background: #d1fae5; color: #065f46; }
  148. .node-type-tag.sample-type { background: #dbeafe; color: #1e40af; }
  149. .node-code { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  150. .node-name { font-size: 11px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  151. .node-passage { font-size: 10px; color: var(--text-light); margin-top: 2px; }
  152. /* 连线 — 加长到 180px */
  153. .chain-connector {
  154. display: flex;
  155. flex-direction: column;
  156. align-items: center;
  157. position: relative;
  158. min-width: 180px;
  159. height: 60px;
  160. z-index: 1;
  161. flex-shrink: 0;
  162. }
  163. .connector-label {
  164. display: flex;
  165. flex-direction: column;
  166. align-items: center;
  167. gap: 1px;
  168. font-size: 10px;
  169. color: var(--text-muted);
  170. white-space: nowrap;
  171. padding: 3px 8px;
  172. background: white;
  173. border: 1px solid #f3f4f6;
  174. border-radius: 4px;
  175. position: absolute;
  176. top: 2px;
  177. left: 50%;
  178. transform: translateX(-50%);
  179. }
  180. .connector-label .wo { color: var(--primary-dark); font-weight: 500; }
  181. .connector-label .task { color: var(--text-light); }
  182. .connector-line {
  183. position: absolute;
  184. bottom: 14px;
  185. left: 0;
  186. right: 0;
  187. height: 2px;
  188. background: var(--line-color);
  189. }
  190. .connector-line.active { background: var(--line-active); }
  191. .connector-arrow {
  192. position: absolute;
  193. bottom: 10px;
  194. right: -5px;
  195. width: 0;
  196. height: 0;
  197. border-top: 5px solid transparent;
  198. border-bottom: 5px solid transparent;
  199. border-left: 7px solid var(--line-color);
  200. }
  201. .connector-line.active + .connector-arrow { border-left-color: var(--line-active); }
  202. /* 分叉 */
  203. .fork-area {
  204. display: flex;
  205. flex-direction: column;
  206. position: relative;
  207. margin-left: 0;
  208. }
  209. .fork-row {
  210. display: flex;
  211. align-items: center;
  212. position: relative;
  213. min-height: 72px;
  214. padding-left: 40px;
  215. }
  216. .fork-area::before {
  217. content: '';
  218. position: absolute;
  219. left: 18px;
  220. top: 36px;
  221. bottom: 36px;
  222. width: 2px;
  223. background: var(--line-color);
  224. }
  225. .fork-row::before {
  226. content: '';
  227. position: absolute;
  228. left: 18px;
  229. top: 50%;
  230. width: 22px;
  231. height: 2px;
  232. background: var(--line-color);
  233. }
  234. .fork-row::after {
  235. content: '';
  236. position: absolute;
  237. left: 18px;
  238. top: 0;
  239. height: 50%;
  240. width: 2px;
  241. background: white;
  242. z-index: 1;
  243. }
  244. .fork-row:last-child::after {
  245. top: auto;
  246. bottom: 0;
  247. }
  248. .fork-row:first-child::after {
  249. top: 0;
  250. bottom: auto;
  251. height: 50%;
  252. background: white;
  253. }
  254. /* 图例 */
  255. .legend {
  256. display: flex;
  257. gap: 20px;
  258. margin-top: 24px;
  259. padding-top: 16px;
  260. border-top: 1px solid #f3f4f6;
  261. }
  262. .legend-item {
  263. display: flex;
  264. align-items: center;
  265. gap: 6px;
  266. font-size: 12px;
  267. color: var(--text-muted);
  268. }
  269. .legend-dot {
  270. width: 10px;
  271. height: 10px;
  272. border-radius: 3px;
  273. border: 1.5px solid;
  274. }
  275. .legend-dot.product { background: var(--primary-light); border-color: var(--primary-border); }
  276. .legend-dot.sample { background: var(--sample-bg); border-color: var(--sample-border); }
  277. .legend-dot.current-node { background: var(--primary-light); border-color: var(--primary); border-width: 2px; }
  278. .legend-dot.line-legend { background: transparent; border: none; width: 24px; height: 2px; border-radius: 0; background: var(--line-active); }
  279. .popover-detail {
  280. display: none;
  281. position: absolute;
  282. top: calc(100% + 8px);
  283. left: 50%;
  284. transform: translateX(-50%);
  285. background: white;
  286. border: 1px solid #e5e7eb;
  287. border-radius: 8px;
  288. padding: 12px;
  289. min-width: 200px;
  290. box-shadow: 0 8px 24px rgba(0,0,0,0.12);
  291. z-index: 100;
  292. font-size: 12px;
  293. }
  294. .popover-detail.visible { display: block; }
  295. .popover-detail .detail-row { display: flex; justify-content: space-between; padding: 3px 0; }
  296. .popover-detail .detail-label { color: var(--text-muted); }
  297. .popover-detail .detail-value { color: var(--text); font-weight: 500; }
  298. .empty-state {
  299. text-align: center;
  300. padding: 40px;
  301. color: var(--text-muted);
  302. font-size: 13px;
  303. }
  304. .empty-state .empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.4; }
  305. .sub-label {
  306. font-size: 11px;
  307. color: var(--text-light);
  308. margin-bottom: 10px;
  309. display: flex;
  310. align-items: center;
  311. gap: 4px;
  312. }
  313. .sub-label .arrow { color: var(--primary); }
  314. </style>
  315. </head>
  316. <body>
  317. <h1>细胞产品来源链路 — 设计 Demo</h1>
  318. <p class="subtitle">展示细胞产品查看弹窗中的「来源链路」可视化方案。点击任意节点可查看详情。</p>
  319. <div class="scenario-tabs">
  320. <div class="scenario-tab active" onclick="switchScenario('s1', this)">场景1:简单链路</div>
  321. <div class="scenario-tab" onclick="switchScenario('s2', this)">场景2:一生多(一个产品产出多个新产品)</div>
  322. <div class="scenario-tab" onclick="switchScenario('s3', this)">场景3:完整追溯(往前看祖先 + 往后看子代)</div>
  323. <div class="scenario-tab" onclick="switchScenario('s4', this)">场景4:手动录入(无祖先,但有子代)</div>
  324. </div>
  325. <div id="desc-s1" class="scenario-desc active">
  326. <strong>场景1 说明:</strong>最简单的情况。一个样本经过一次生产工单,产出一个细胞产品(P1代)。这个P1代产品又经过另一个生产工单,产出了当前查看的P2代产品。每个节点之间只有一条连线。
  327. </div>
  328. <div id="desc-s2" class="scenario-desc" style="display:none;">
  329. <strong>场景2 说明:</strong>当前产品(P1代)被进一步拿去做生产,<strong>一次生产产出了 3 个不同的新细胞产品</strong>。就像一块面团可以同时做出多个包子 — 连线从当前产品分叉出去,每条分叉线上面标注了对应的工单号和任务号。这就是「一生多」。
  330. </div>
  331. <div id="desc-s3" class="scenario-desc" style="display:none;">
  332. <strong>场景3 说明:</strong>最完整的情况。全部在<strong>同一行</strong>展示 — 从左到右:<strong>样本 → P1(父产品) → P2(当前) → 分叉出2个P3(子产品)</strong>。祖先在左边,当前在中间,子代直接从当前节点往右延伸分叉。不分两行,一条链路从头看到尾。
  333. </div>
  334. <div id="desc-s4" class="scenario-desc" style="display:none;">
  335. <strong>场景4 说明:</strong>手动录入的细胞产品是「起源」— 它没有祖先(不是从样本或别的产品生产来的),但后续它可以被拿去生产新的子产品。所以它左边没有祖先链路,但右边会分叉出子代。只有从未被用于生产的「孤立产品」才会显示空状态。
  336. </div>
  337. <div id="s1" class="scenario-panel active">
  338. <div class="chain-container">
  339. <div class="chain-section-title">来源链路</div>
  340. <div class="chain-row">
  341. <div class="chain-node">
  342. <div class="node-card sample" onclick="togglePopover(this)">
  343. <span class="node-type-tag sample-type">样本</span>
  344. <div class="node-code">ZKZX34000627</div>
  345. <div class="node-name">脐带血干细胞</div>
  346. </div>
  347. </div>
  348. <div class="chain-connector">
  349. <div class="connector-label">
  350. <span class="wo">工单 WO20260519001</span>
  351. <span class="task">任务 T20260519001</span>
  352. </div>
  353. <div class="connector-line active"></div>
  354. <div class="connector-arrow"></div>
  355. </div>
  356. <div class="chain-node">
  357. <div class="node-card" onclick="togglePopover(this)">
  358. <span class="node-type-tag product">细胞产品</span>
  359. <div class="node-code">CP20260520001</div>
  360. <div class="node-name">脐带间充质干细胞</div>
  361. <div class="node-passage">P1 代</div>
  362. </div>
  363. </div>
  364. <div class="chain-connector">
  365. <div class="connector-label">
  366. <span class="wo">工单 WO20260521002</span>
  367. <span class="task">任务 T20260521002</span>
  368. </div>
  369. <div class="connector-line active"></div>
  370. <div class="connector-arrow"></div>
  371. </div>
  372. <div class="chain-node">
  373. <div class="node-card current" onclick="togglePopover(this)">
  374. <span class="node-type-tag product">细胞产品</span>
  375. <div class="node-code">CP20260522001</div>
  376. <div class="node-name">脐带间充质干细胞</div>
  377. <div class="node-passage">P2 代</div>
  378. </div>
  379. </div>
  380. </div>
  381. <div class="legend">
  382. <div class="legend-item"><div class="legend-dot sample"></div>样本</div>
  383. <div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
  384. <div class="legend-item"><div class="legend-dot current-node"></div>当前查看</div>
  385. <div class="legend-item"><div class="legend-dot line-legend"></div>生产连线(标注工单/任务号)</div>
  386. </div>
  387. </div>
  388. </div>
  389. <div id="s2" class="scenario-panel">
  390. <div class="chain-container">
  391. <div class="chain-section-title">来源链路</div>
  392. <div class="sub-label"><span class="arrow">●</span> 当前产品被拿去生产,一次产出了 3 个新产品(分叉)</div>
  393. <div class="chain-row">
  394. <div class="chain-node">
  395. <div class="node-card sample" onclick="togglePopover(this)">
  396. <span class="node-type-tag sample-type">样本</span>
  397. <div class="node-code">ZKZX34000627</div>
  398. <div class="node-name">脐带血干细胞</div>
  399. </div>
  400. </div>
  401. <div class="chain-connector">
  402. <div class="connector-label">
  403. <span class="wo">工单 WO20260519001</span>
  404. <span class="task">任务 T20260519001</span>
  405. </div>
  406. <div class="connector-line active"></div>
  407. <div class="connector-arrow"></div>
  408. </div>
  409. <div class="chain-node">
  410. <div class="node-card current" onclick="togglePopover(this)">
  411. <span class="node-type-tag product">细胞产品</span>
  412. <div class="node-code">CP20260520001</div>
  413. <div class="node-name">脐带间充质干细胞</div>
  414. <div class="node-passage">P1 代</div>
  415. </div>
  416. </div>
  417. <div class="fork-area">
  418. <div class="fork-row">
  419. <div class="chain-connector" style="min-width:160px;">
  420. <div class="connector-label">
  421. <span class="wo">工单 WO20260522003</span>
  422. <span class="task">任务 T20260522003</span>
  423. </div>
  424. <div class="connector-line active"></div>
  425. <div class="connector-arrow"></div>
  426. </div>
  427. <div class="chain-node">
  428. <div class="node-card" onclick="togglePopover(this)">
  429. <span class="node-type-tag product">细胞产品</span>
  430. <div class="node-code">CP20260523001</div>
  431. <div class="node-name">脐带间充质干细胞</div>
  432. <div class="node-passage">P2 代</div>
  433. </div>
  434. </div>
  435. </div>
  436. <div class="fork-row">
  437. <div class="chain-connector" style="min-width:160px;">
  438. <div class="connector-label">
  439. <span class="wo">工单 WO20260522003</span>
  440. <span class="task">任务 T20260522004</span>
  441. </div>
  442. <div class="connector-line active"></div>
  443. <div class="connector-arrow"></div>
  444. </div>
  445. <div class="chain-node">
  446. <div class="node-card" onclick="togglePopover(this)">
  447. <span class="node-type-tag product">细胞产品</span>
  448. <div class="node-code">CP20260523002</div>
  449. <div class="node-name">脐带间充质干细胞</div>
  450. <div class="node-passage">P2 代</div>
  451. </div>
  452. </div>
  453. </div>
  454. <div class="fork-row">
  455. <div class="chain-connector" style="min-width:160px;">
  456. <div class="connector-label">
  457. <span class="wo">工单 WO20260522005</span>
  458. <span class="task">任务 T20260522005</span>
  459. </div>
  460. <div class="connector-line active"></div>
  461. <div class="connector-arrow"></div>
  462. </div>
  463. <div class="chain-node">
  464. <div class="node-card" onclick="togglePopover(this)">
  465. <span class="node-type-tag product">细胞产品</span>
  466. <div class="node-code">CP20260523003</div>
  467. <div class="node-name">围产期造血干细胞</div>
  468. <div class="node-passage">P1 代</div>
  469. </div>
  470. </div>
  471. </div>
  472. </div>
  473. </div>
  474. <div class="legend">
  475. <div class="legend-item"><div class="legend-dot sample"></div>样本</div>
  476. <div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
  477. <div class="legend-item"><div class="legend-dot current-node"></div>当前查看</div>
  478. <div class="legend-item"><div class="legend-dot line-legend"></div>生产连线</div>
  479. </div>
  480. </div>
  481. </div>
  482. <div id="s3" class="scenario-panel">
  483. <div class="chain-container">
  484. <div class="chain-section-title">来源链路(完整双向追溯)</div>
  485. <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>
  486. <div class="chain-row">
  487. <div class="chain-node">
  488. <div class="node-card sample" onclick="togglePopover(this)">
  489. <span class="node-type-tag sample-type">样本</span>
  490. <div class="node-code">ZKZX34000627</div>
  491. <div class="node-name">脐带血干细胞</div>
  492. </div>
  493. </div>
  494. <div class="chain-connector">
  495. <div class="connector-label">
  496. <span class="wo">工单 WO20260519001</span>
  497. <span class="task">任务 T20260519001</span>
  498. </div>
  499. <div class="connector-line active"></div>
  500. <div class="connector-arrow"></div>
  501. </div>
  502. <div class="chain-node">
  503. <div class="node-card" onclick="togglePopover(this)">
  504. <span class="node-type-tag product">细胞产品</span>
  505. <div class="node-code">CP20260520001</div>
  506. <div class="node-name">脐带间充质干细胞</div>
  507. <div class="node-passage">P1 代(父产品)</div>
  508. </div>
  509. </div>
  510. <div class="chain-connector">
  511. <div class="connector-label">
  512. <span class="wo">工单 WO20260521002</span>
  513. <span class="task">任务 T20260521002</span>
  514. </div>
  515. <div class="connector-line active"></div>
  516. <div class="connector-arrow"></div>
  517. </div>
  518. <div class="chain-node">
  519. <div class="node-card current" onclick="togglePopover(this)">
  520. <span class="node-type-tag product">细胞产品</span>
  521. <div class="node-code">CP20260522001</div>
  522. <div class="node-name">脐带间充质干细胞</div>
  523. <div class="node-passage">P2 代(当前)</div>
  524. </div>
  525. </div>
  526. <div class="fork-area">
  527. <div class="fork-row">
  528. <div class="chain-connector" style="min-width:160px;">
  529. <div class="connector-label">
  530. <span class="wo">工单 WO20260523003</span>
  531. <span class="task">任务 T20260523003</span>
  532. </div>
  533. <div class="connector-line active"></div>
  534. <div class="connector-arrow"></div>
  535. </div>
  536. <div class="chain-node">
  537. <div class="node-card" onclick="togglePopover(this)">
  538. <span class="node-type-tag product">细胞产品</span>
  539. <div class="node-code">CP20260524001</div>
  540. <div class="node-name">脐带间充质干细胞</div>
  541. <div class="node-passage">P3 代(子产品1)</div>
  542. </div>
  543. </div>
  544. </div>
  545. <div class="fork-row">
  546. <div class="chain-connector" style="min-width:160px;">
  547. <div class="connector-label">
  548. <span class="wo">工单 WO20260523004</span>
  549. <span class="task">任务 T20260523004</span>
  550. </div>
  551. <div class="connector-line active"></div>
  552. <div class="connector-arrow"></div>
  553. </div>
  554. <div class="chain-node">
  555. <div class="node-card" onclick="togglePopover(this)">
  556. <span class="node-type-tag product">细胞产品</span>
  557. <div class="node-code">CP20260524002</div>
  558. <div class="node-name">脐带间充质干细胞</div>
  559. <div class="node-passage">P3 代(子产品2)</div>
  560. </div>
  561. </div>
  562. </div>
  563. </div>
  564. </div>
  565. <div class="legend">
  566. <div class="legend-item"><div class="legend-dot sample"></div>样本</div>
  567. <div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
  568. <div class="legend-item"><div class="legend-dot current-node"></div>当前查看</div>
  569. <div class="legend-item"><div class="legend-dot line-legend"></div>生产连线</div>
  570. </div>
  571. </div>
  572. </div>
  573. <div id="s4" class="scenario-panel">
  574. <div class="chain-container">
  575. <div class="chain-section-title">来源链路</div>
  576. <div class="sub-label"><span class="arrow">●</span> 手动录入产品(起源节点),无祖先,但后续被拿去生产了子产品</div>
  577. <div class="chain-row">
  578. <div class="chain-node">
  579. <div class="node-card current" onclick="togglePopover(this)">
  580. <span class="node-type-tag product">细胞产品</span>
  581. <div class="node-code">CP20260520001</div>
  582. <div class="node-name">脐带间充质干细胞</div>
  583. <div class="node-passage">手动录入(起源)</div>
  584. </div>
  585. </div>
  586. <div class="fork-area">
  587. <div class="fork-row">
  588. <div class="chain-connector" style="min-width:160px;">
  589. <div class="connector-label">
  590. <span class="wo">工单 WO20260522003</span>
  591. <span class="task">任务 T20260522003</span>
  592. </div>
  593. <div class="connector-line active"></div>
  594. <div class="connector-arrow"></div>
  595. </div>
  596. <div class="chain-node">
  597. <div class="node-card" onclick="togglePopover(this)">
  598. <span class="node-type-tag product">细胞产品</span>
  599. <div class="node-code">CP20260523001</div>
  600. <div class="node-name">脐带间充质干细胞</div>
  601. <div class="node-passage">P1 代</div>
  602. </div>
  603. </div>
  604. </div>
  605. <div class="fork-row">
  606. <div class="chain-connector" style="min-width:160px;">
  607. <div class="connector-label">
  608. <span class="wo">工单 WO20260522004</span>
  609. <span class="task">任务 T20260522004</span>
  610. </div>
  611. <div class="connector-line active"></div>
  612. <div class="connector-arrow"></div>
  613. </div>
  614. <div class="chain-node">
  615. <div class="node-card" onclick="togglePopover(this)">
  616. <span class="node-type-tag product">细胞产品</span>
  617. <div class="node-code">CP20260523002</div>
  618. <div class="node-name">围产期造血干细胞</div>
  619. <div class="node-passage">P1 代</div>
  620. </div>
  621. </div>
  622. </div>
  623. </div>
  624. </div>
  625. <div class="legend">
  626. <div class="legend-item"><div class="legend-dot product"></div>细胞产品</div>
  627. <div class="legend-item"><div class="legend-dot current-node"></div>当前查看(手动录入/起源)</div>
  628. <div class="legend-item"><div class="legend-dot line-legend"></div>生产连线</div>
  629. </div>
  630. </div>
  631. </div>
  632. <script>
  633. function switchScenario(id, tab) {
  634. document.querySelectorAll('.scenario-panel').forEach(function(p) { p.classList.remove('active'); });
  635. document.querySelectorAll('.scenario-tab').forEach(function(t) { t.classList.remove('active'); });
  636. document.querySelectorAll('.scenario-desc').forEach(function(d) { d.style.display = 'none'; });
  637. document.getElementById(id).classList.add('active');
  638. tab.classList.add('active');
  639. var descMap = { s1: 'desc-s1', s2: 'desc-s2', s3: 'desc-s3', s4: 'desc-s4' };
  640. document.getElementById(descMap[id]).style.display = 'block';
  641. }
  642. function togglePopover(card) {
  643. var existing = card.querySelector('.popover-detail');
  644. if (existing) { existing.classList.toggle('visible'); return; }
  645. var code = card.querySelector('.node-code') ? card.querySelector('.node-code').textContent : '';
  646. var name = card.querySelector('.node-name') ? card.querySelector('.node-name').textContent : '';
  647. var isCurrent = card.classList.contains('current');
  648. var isSample = card.classList.contains('sample');
  649. var passage = card.querySelector('.node-passage') ? card.querySelector('.node-passage').textContent : '';
  650. var status = isSample ? '已入库' : '冷冻中';
  651. var popover = document.createElement('div');
  652. popover.className = 'popover-detail visible';
  653. var html = '<div class="detail-row"><span class="detail-label">编码</span><span class="detail-value">' + code + '</span></div>';
  654. html += '<div class="detail-row"><span class="detail-label">名称</span><span class="detail-value">' + name + '</span></div>';
  655. html += '<div class="detail-row"><span class="detail-label">状态</span><span class="detail-value">' + status + '</span></div>';
  656. if (!isSample) { html += '<div class="detail-row"><span class="detail-label">类型</span><span class="detail-value">细胞产品</span></div>'; }
  657. if (passage) { html += '<div class="detail-row"><span class="detail-label">代次</span><span class="detail-value">' + passage + '</span></div>'; }
  658. if (isCurrent) { html += '<div class="detail-row"><span class="detail-label">标识</span><span class="detail-value" style="color:#10b981">当前查看</span></div>'; }
  659. popover.innerHTML = html;
  660. card.appendChild(popover);
  661. setTimeout(function() {
  662. document.addEventListener('click', function close(e) {
  663. if (!popover.contains(e.target) && e.target !== card) {
  664. popover.classList.remove('visible');
  665. document.removeEventListener('click', close);
  666. }
  667. });
  668. }, 10);
  669. }
  670. </script>
  671. </body>
  672. </html>