sample-workbench-360-prototype.html 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  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>中科兴皖 · 工作台 + 样本360 原型</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  10. <style>
  11. :root {
  12. --primary: #10b981;
  13. --primary-dark: #059669;
  14. --primary-bg: #ecfdf5;
  15. --text: #1e293b;
  16. --text-muted: #64748b;
  17. --border: #d9e8e2;
  18. --bg-page: #fefefe;
  19. --surface: #ffffff;
  20. --table-head: #f8fafc;
  21. --row-alt: #fafafa;
  22. --row-hover: #f1f5f9;
  23. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  24. --radius: 10px;
  25. --warn: #d97706;
  26. --warn-bg: #fffbeb;
  27. --danger: #dc2626;
  28. --danger-bg: #fef2f2;
  29. --info: #64748b;
  30. }
  31. * { box-sizing: border-box; margin: 0; padding: 0; }
  32. body {
  33. font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
  34. font-size: 14px;
  35. line-height: 1.5;
  36. color: var(--text);
  37. background: var(--bg-page);
  38. }
  39. .layout { display: flex; min-height: 100vh; }
  40. /* Sidebar */
  41. .sidebar {
  42. width: 220px;
  43. flex-shrink: 0;
  44. background: var(--surface);
  45. border-right: 1px solid var(--border);
  46. padding: 24px 16px;
  47. }
  48. .logo {
  49. font-size: 16px;
  50. font-weight: 700;
  51. padding: 0 12px 20px;
  52. margin-bottom: 8px;
  53. border-bottom: 1px solid var(--border);
  54. }
  55. .logo span { color: var(--primary); }
  56. .nav-group { margin-bottom: 4px; }
  57. .nav-group-title {
  58. font-size: 12px;
  59. font-weight: 700;
  60. color: var(--text-muted);
  61. padding: 8px 12px;
  62. letter-spacing: 0.02em;
  63. }
  64. .nav-item {
  65. display: block;
  66. padding: 8px 12px 8px 20px;
  67. margin-left: 4px;
  68. border-left: 2px solid var(--border);
  69. color: var(--text-muted);
  70. text-decoration: none;
  71. border-radius: 8px;
  72. font-size: 13px;
  73. transition: all 0.15s;
  74. }
  75. .nav-item:hover { background: var(--row-alt); color: var(--text); }
  76. .nav-item.active {
  77. background: var(--primary-bg);
  78. color: var(--primary-dark);
  79. border-left-color: var(--primary);
  80. font-weight: 600;
  81. }
  82. .nav-badge {
  83. float: right;
  84. background: var(--primary);
  85. color: #fff;
  86. font-size: 11px;
  87. padding: 0 6px;
  88. border-radius: 10px;
  89. min-width: 18px;
  90. text-align: center;
  91. }
  92. /* Main */
  93. .main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
  94. .topbar {
  95. height: 56px;
  96. display: flex;
  97. align-items: center;
  98. gap: 16px;
  99. padding: 0 24px;
  100. background: var(--surface);
  101. border-bottom: 1px solid var(--border);
  102. }
  103. .topbar-title { font-weight: 600; font-size: 15px; white-space: nowrap; }
  104. .global-search {
  105. flex: 1;
  106. max-width: 480px;
  107. position: relative;
  108. }
  109. .global-search input {
  110. width: 100%;
  111. height: 36px;
  112. padding: 0 12px 0 36px;
  113. border: 1px solid var(--border);
  114. border-radius: 8px;
  115. font-size: 13px;
  116. outline: none;
  117. }
  118. .global-search input:focus {
  119. border-color: var(--primary);
  120. box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
  121. }
  122. .global-search svg {
  123. position: absolute;
  124. left: 12px;
  125. top: 50%;
  126. transform: translateY(-50%);
  127. width: 16px;
  128. height: 16px;
  129. color: var(--text-muted);
  130. }
  131. .search-hint {
  132. position: absolute;
  133. right: 10px;
  134. top: 50%;
  135. transform: translateY(-50%);
  136. font-size: 11px;
  137. color: var(--text-muted);
  138. background: var(--table-head);
  139. padding: 2px 6px;
  140. border-radius: 4px;
  141. }
  142. .user-chip {
  143. display: flex;
  144. align-items: center;
  145. gap: 8px;
  146. font-size: 13px;
  147. color: var(--text-muted);
  148. }
  149. .avatar {
  150. width: 32px;
  151. height: 32px;
  152. border-radius: 50%;
  153. background: var(--primary-bg);
  154. color: var(--primary-dark);
  155. display: flex;
  156. align-items: center;
  157. justify-content: center;
  158. font-weight: 600;
  159. font-size: 13px;
  160. }
  161. .content { padding: 24px; flex: 1; overflow: auto; }
  162. /* Prototype banner */
  163. .proto-banner {
  164. background: linear-gradient(90deg, var(--primary-bg), #fff);
  165. border: 1px solid var(--border);
  166. border-radius: var(--radius);
  167. padding: 12px 16px;
  168. margin-bottom: 20px;
  169. display: flex;
  170. align-items: center;
  171. gap: 12px;
  172. }
  173. .proto-banner strong { color: var(--primary-dark); }
  174. .proto-banner p { font-size: 13px; color: var(--text-muted); flex: 1; }
  175. /* Stats */
  176. .stats-row {
  177. display: grid;
  178. grid-template-columns: repeat(5, 1fr);
  179. gap: 12px;
  180. margin-bottom: 20px;
  181. }
  182. .stat-card {
  183. background: var(--surface);
  184. border: 1px solid var(--border);
  185. border-radius: var(--radius);
  186. padding: 16px;
  187. box-shadow: var(--shadow);
  188. cursor: pointer;
  189. transition: border-color 0.15s;
  190. }
  191. .stat-card:hover { border-color: var(--primary); }
  192. .stat-card.warn { border-color: rgba(217, 119, 6, 0.25); }
  193. .stat-label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
  194. .stat-num { font-size: 28px; font-weight: 700; color: var(--text); line-height: 1.2; }
  195. .stat-card.warn .stat-num { color: var(--warn); }
  196. .stat-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
  197. /* Panel */
  198. .panel {
  199. background: var(--surface);
  200. border: 1px solid var(--border);
  201. border-radius: var(--radius);
  202. box-shadow: var(--shadow);
  203. margin-bottom: 20px;
  204. }
  205. .panel-top {
  206. display: flex;
  207. align-items: center;
  208. justify-content: space-between;
  209. padding: 16px 20px;
  210. border-bottom: 1px solid var(--border);
  211. }
  212. .panel-top h2 {
  213. font-size: 15px;
  214. font-weight: 600;
  215. display: flex;
  216. align-items: center;
  217. gap: 8px;
  218. }
  219. .panel-top h2::before {
  220. content: '';
  221. width: 3px;
  222. height: 16px;
  223. background: var(--primary);
  224. border-radius: 2px;
  225. }
  226. .filter-tabs {
  227. display: flex;
  228. gap: 4px;
  229. padding: 0 20px;
  230. border-bottom: 1px solid var(--border);
  231. }
  232. .filter-tab {
  233. padding: 10px 14px;
  234. font-size: 13px;
  235. color: var(--text-muted);
  236. background: none;
  237. border: none;
  238. border-bottom: 2px solid transparent;
  239. cursor: pointer;
  240. margin-bottom: -1px;
  241. }
  242. .filter-tab:hover { color: var(--text); }
  243. .filter-tab.active {
  244. color: var(--primary-dark);
  245. border-bottom-color: var(--primary);
  246. font-weight: 600;
  247. }
  248. .filter-tab .count {
  249. display: inline-block;
  250. margin-left: 4px;
  251. font-size: 11px;
  252. background: var(--table-head);
  253. padding: 0 6px;
  254. border-radius: 8px;
  255. }
  256. .filter-tab.active .count { background: var(--primary-bg); color: var(--primary-dark); }
  257. /* Table */
  258. .list-body { padding: 0; }
  259. table { width: 100%; border-collapse: collapse; }
  260. th {
  261. background: var(--table-head);
  262. font-size: 12px;
  263. font-weight: 600;
  264. color: var(--text-muted);
  265. padding: 10px 12px;
  266. text-align: center;
  267. border-bottom: 1px solid var(--border);
  268. }
  269. td {
  270. padding: 12px;
  271. text-align: center;
  272. border-bottom: 1px solid var(--border);
  273. font-size: 13px;
  274. }
  275. tr:nth-child(even) td { background: var(--row-alt); }
  276. tr:hover td { background: var(--row-hover); }
  277. tr { cursor: pointer; }
  278. .link {
  279. color: var(--primary-dark);
  280. font-weight: 500;
  281. text-decoration: none;
  282. border-bottom: 1px dashed rgba(5, 150, 105, 0.4);
  283. }
  284. .link:hover { color: var(--primary); }
  285. .tag {
  286. display: inline-block;
  287. padding: 2px 8px;
  288. border-radius: 6px;
  289. font-size: 12px;
  290. font-weight: 500;
  291. }
  292. .tag.success { background: var(--primary-bg); color: var(--primary-dark); }
  293. .tag.warning { background: var(--warn-bg); color: var(--warn); }
  294. .tag.info { background: var(--table-head); color: var(--info); }
  295. .tag.danger { background: var(--danger-bg); color: var(--danger); }
  296. .btn {
  297. display: inline-flex;
  298. align-items: center;
  299. gap: 6px;
  300. height: 32px;
  301. padding: 0 12px;
  302. border-radius: 8px;
  303. font-size: 13px;
  304. font-weight: 500;
  305. cursor: pointer;
  306. border: 1px solid var(--border);
  307. background: var(--surface);
  308. color: var(--text);
  309. transition: all 0.15s;
  310. }
  311. .btn:hover { border-color: var(--primary); color: var(--primary-dark); }
  312. .btn-primary {
  313. background: var(--primary);
  314. border-color: var(--primary);
  315. color: #fff;
  316. }
  317. .btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); color: #fff; }
  318. .btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
  319. /* Two column layout */
  320. .grid-2 {
  321. display: grid;
  322. grid-template-columns: 1fr 360px;
  323. gap: 20px;
  324. }
  325. .quick-card {
  326. padding: 16px 20px;
  327. }
  328. .quick-card h3 {
  329. font-size: 13px;
  330. font-weight: 600;
  331. margin-bottom: 12px;
  332. color: var(--text-muted);
  333. }
  334. .quick-links { display: flex; flex-direction: column; gap: 8px; }
  335. .quick-link {
  336. display: flex;
  337. align-items: center;
  338. gap: 10px;
  339. padding: 10px 12px;
  340. border: 1px solid var(--border);
  341. border-radius: 8px;
  342. text-decoration: none;
  343. color: var(--text);
  344. font-size: 13px;
  345. transition: all 0.15s;
  346. }
  347. .quick-link:hover { border-color: var(--primary); background: var(--primary-bg); }
  348. .quick-link-icon {
  349. width: 32px;
  350. height: 32px;
  351. border-radius: 8px;
  352. background: var(--primary-bg);
  353. color: var(--primary-dark);
  354. display: flex;
  355. align-items: center;
  356. justify-content: center;
  357. font-size: 16px;
  358. flex-shrink: 0;
  359. }
  360. .concept-box {
  361. padding: 16px 20px 20px;
  362. }
  363. .concept-step {
  364. display: flex;
  365. gap: 12px;
  366. margin-bottom: 16px;
  367. align-items: flex-start;
  368. }
  369. .step-num {
  370. width: 24px;
  371. height: 24px;
  372. border-radius: 50%;
  373. background: var(--primary);
  374. color: #fff;
  375. font-size: 12px;
  376. font-weight: 700;
  377. display: flex;
  378. align-items: center;
  379. justify-content: center;
  380. flex-shrink: 0;
  381. }
  382. .step-body h4 { font-size: 13px; font-weight: 600; margin-bottom: 2px; }
  383. .step-body p { font-size: 12px; color: var(--text-muted); }
  384. /* Drawer overlay */
  385. .backdrop {
  386. position: fixed;
  387. inset: 0;
  388. background: rgba(15, 23, 42, 0.35);
  389. z-index: 1000;
  390. opacity: 0;
  391. pointer-events: none;
  392. transition: opacity 0.25s;
  393. }
  394. .backdrop.open { opacity: 1; pointer-events: auto; }
  395. .drawer {
  396. position: fixed;
  397. top: 0;
  398. right: 0;
  399. width: min(920px, 96vw);
  400. height: 100vh;
  401. background: var(--surface);
  402. z-index: 1001;
  403. box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
  404. transform: translateX(100%);
  405. transition: transform 0.3s ease;
  406. display: flex;
  407. flex-direction: column;
  408. }
  409. .drawer.open { transform: translateX(0); }
  410. .drawer-header {
  411. padding: 16px 24px;
  412. border-bottom: 1px solid var(--border);
  413. display: flex;
  414. align-items: center;
  415. justify-content: space-between;
  416. flex-shrink: 0;
  417. }
  418. .drawer-header h2 {
  419. font-size: 16px;
  420. font-weight: 600;
  421. display: flex;
  422. align-items: center;
  423. gap: 10px;
  424. }
  425. .drawer-header .sample-code {
  426. font-family: ui-monospace, monospace;
  427. color: var(--primary-dark);
  428. background: var(--primary-bg);
  429. padding: 2px 8px;
  430. border-radius: 6px;
  431. font-size: 14px;
  432. }
  433. .drawer-close {
  434. width: 32px;
  435. height: 32px;
  436. border: 1px solid var(--border);
  437. border-radius: 8px;
  438. background: var(--surface);
  439. cursor: pointer;
  440. font-size: 18px;
  441. color: var(--text-muted);
  442. }
  443. .drawer-close:hover { background: var(--row-alt); }
  444. .drawer-body { flex: 1; overflow-y: auto; padding: 20px 24px 24px; }
  445. /* Timeline */
  446. .timeline {
  447. display: flex;
  448. align-items: flex-start;
  449. gap: 0;
  450. margin-bottom: 24px;
  451. padding: 16px;
  452. background: var(--table-head);
  453. border-radius: var(--radius);
  454. overflow-x: auto;
  455. }
  456. .tl-node {
  457. flex: 1;
  458. min-width: 100px;
  459. text-align: center;
  460. position: relative;
  461. }
  462. .tl-node:not(:last-child)::after {
  463. content: '';
  464. position: absolute;
  465. top: 14px;
  466. left: calc(50% + 16px);
  467. width: calc(100% - 32px);
  468. height: 2px;
  469. background: var(--border);
  470. }
  471. .tl-node.done:not(:last-child)::after { background: var(--primary); }
  472. .tl-dot {
  473. width: 28px;
  474. height: 28px;
  475. border-radius: 50%;
  476. border: 2px solid var(--border);
  477. background: var(--surface);
  478. margin: 0 auto 8px;
  479. display: flex;
  480. align-items: center;
  481. justify-content: center;
  482. font-size: 12px;
  483. position: relative;
  484. z-index: 1;
  485. }
  486. .tl-node.done .tl-dot {
  487. border-color: var(--primary);
  488. background: var(--primary-bg);
  489. color: var(--primary-dark);
  490. }
  491. .tl-node.current .tl-dot {
  492. border-color: var(--primary);
  493. background: var(--primary);
  494. color: #fff;
  495. box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
  496. }
  497. .tl-node.pending .tl-dot { color: var(--text-muted); }
  498. .tl-label { font-size: 12px; font-weight: 600; color: var(--text); }
  499. .tl-status { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
  500. /* Action bar in drawer */
  501. .action-bar {
  502. display: flex;
  503. flex-wrap: wrap;
  504. gap: 8px;
  505. padding: 12px 16px;
  506. background: var(--primary-bg);
  507. border: 1px solid rgba(16, 185, 129, 0.25);
  508. border-radius: var(--radius);
  509. margin-bottom: 20px;
  510. }
  511. .action-bar-label {
  512. width: 100%;
  513. font-size: 12px;
  514. font-weight: 600;
  515. color: var(--primary-dark);
  516. margin-bottom: 4px;
  517. }
  518. /* Sections in drawer */
  519. .view-section {
  520. margin-bottom: 20px;
  521. border: 1px solid var(--border);
  522. border-radius: var(--radius);
  523. overflow: hidden;
  524. }
  525. .view-section-head {
  526. padding: 10px 16px;
  527. background: var(--table-head);
  528. font-size: 13px;
  529. font-weight: 600;
  530. border-bottom: 1px solid var(--border);
  531. display: flex;
  532. align-items: center;
  533. justify-content: space-between;
  534. }
  535. .view-section-body { padding: 12px 16px; }
  536. .kv-grid {
  537. display: grid;
  538. grid-template-columns: repeat(4, 1fr);
  539. gap: 12px 16px;
  540. }
  541. .kv-item dt { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
  542. .kv-item dd { font-size: 13px; font-weight: 500; }
  543. /* Deep link toast */
  544. .toast {
  545. position: fixed;
  546. bottom: 24px;
  547. left: 50%;
  548. transform: translateX(-50%) translateY(80px);
  549. background: var(--text);
  550. color: #fff;
  551. padding: 12px 20px;
  552. border-radius: var(--radius);
  553. font-size: 13px;
  554. z-index: 2000;
  555. opacity: 0;
  556. transition: all 0.3s;
  557. max-width: 90vw;
  558. text-align: center;
  559. }
  560. .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
  561. .toast code {
  562. background: rgba(255,255,255,0.15);
  563. padding: 2px 6px;
  564. border-radius: 4px;
  565. font-size: 12px;
  566. }
  567. /* Search dropdown */
  568. .search-dropdown {
  569. position: absolute;
  570. top: calc(100% + 4px);
  571. left: 0;
  572. right: 0;
  573. background: var(--surface);
  574. border: 1px solid var(--border);
  575. border-radius: var(--radius);
  576. box-shadow: 0 8px 24px rgba(0,0,0,0.1);
  577. z-index: 100;
  578. display: none;
  579. }
  580. .search-dropdown.open { display: block; }
  581. .search-result {
  582. padding: 10px 14px;
  583. cursor: pointer;
  584. border-bottom: 1px solid var(--border);
  585. display: flex;
  586. align-items: center;
  587. justify-content: space-between;
  588. }
  589. .search-result:hover { background: var(--row-hover); }
  590. .search-result:last-child { border-bottom: none; }
  591. .search-result-meta { font-size: 12px; color: var(--text-muted); }
  592. @media (max-width: 1100px) {
  593. .stats-row { grid-template-columns: repeat(3, 1fr); }
  594. .grid-2 { grid-template-columns: 1fr; }
  595. .kv-grid { grid-template-columns: repeat(2, 1fr); }
  596. }
  597. </style>
  598. </head>
  599. <body>
  600. <div class="layout">
  601. <!-- 侧栏:重组后的菜单 IA(示意) -->
  602. <aside class="sidebar">
  603. <div class="logo">中科兴皖<span> · 管理端</span></div>
  604. <div class="nav-group">
  605. <div class="nav-group-title">日常作业</div>
  606. <a href="#" class="nav-item active">工作台 <span class="nav-badge">12</span></a>
  607. <a href="#" class="nav-item">样本中心</a>
  608. <a href="#" class="nav-item">质检作业</a>
  609. <a href="#" class="nav-item">生产作业</a>
  610. <a href="#" class="nav-item">服务订单</a>
  611. <a href="#" class="nav-item">财务确认</a>
  612. </div>
  613. <div class="nav-group">
  614. <div class="nav-group-title">配置</div>
  615. <a href="#" class="nav-item">主数据</a>
  616. <a href="#" class="nav-item">系统管理</a>
  617. </div>
  618. </aside>
  619. <div class="main">
  620. <!-- 顶栏:全局检索入口 -->
  621. <header class="topbar">
  622. <span class="topbar-title">工作台</span>
  623. <div class="global-search">
  624. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
  625. <input type="text" id="globalSearch" placeholder="搜索样本编码 / 协议号 / 工单号…" autocomplete="off" />
  626. <span class="search-hint">Enter</span>
  627. <div class="search-dropdown" id="searchDropdown">
  628. <div class="search-result" data-sample="ZK202506120001">
  629. <div><strong>ZK202506120001</strong> · 张明华 · 细胞存储</div>
  630. <span class="tag warning">待配置质检</span>
  631. </div>
  632. <div class="search-result" data-sample="ZK202506110008">
  633. <div><strong>ZK202506110008</strong> · 李芳 · 科研捐赠</div>
  634. <span class="tag info">待接收</span>
  635. </div>
  636. <div class="search-result" data-sample="XY202506100003">
  637. <div><strong>XY202506100003</strong> · 协议号 XY-2025-0088</div>
  638. <span class="tag success">已入库</span>
  639. </div>
  640. </div>
  641. </div>
  642. <div class="user-chip">
  643. <span>质检专员 · 张三</span>
  644. <div class="avatar">张</div>
  645. </div>
  646. </header>
  647. <main class="content">
  648. <div class="proto-banner">
  649. <strong>交互原型</strong>
  650. <p>演示「工作台聚合待办 → 点击样本编码打开 360 视图 → 一键深链到具体操作页」。点击表格行、统计卡片、快捷入口均可体验。</p>
  651. <button class="btn btn-primary btn-sm" onclick="openSample360('ZK202506120001')">打开样本 360 示例</button>
  652. </div>
  653. <!-- 统计卡片:各部门待办一目了然 -->
  654. <div class="stats-row">
  655. <div class="stat-card warn" onclick="filterTodos('reception')">
  656. <div class="stat-label">待接收样本</div>
  657. <div class="stat-num">3</div>
  658. <div class="stat-sub">质检接收岗</div>
  659. </div>
  660. <div class="stat-card warn" onclick="filterTodos('qc')">
  661. <div class="stat-label">待配置/执行质检</div>
  662. <div class="stat-num">5</div>
  663. <div class="stat-sub">质检专员</div>
  664. </div>
  665. <div class="stat-card" onclick="filterTodos('prod')">
  666. <div class="stat-label">生产工单待办</div>
  667. <div class="stat-num">2</div>
  668. <div class="stat-sub">生产岗</div>
  669. </div>
  670. <div class="stat-card" onclick="filterTodos('release')">
  671. <div class="stat-label">待放行/入库</div>
  672. <div class="stat-num">4</div>
  673. <div class="stat-sub">QA / 库管</div>
  674. </div>
  675. <div class="stat-card" onclick="filterTodos('finance')">
  676. <div class="stat-label">财务待确认</div>
  677. <div class="stat-num">2</div>
  678. <div class="stat-sub">财务岗</div>
  679. </div>
  680. </div>
  681. <div class="grid-2">
  682. <!-- 统一待办列表 -->
  683. <section class="panel">
  684. <div class="panel-top">
  685. <h2>我的待办(跨模块聚合)</h2>
  686. <span style="font-size:12px;color:var(--text-muted)">共 12 项 · 按紧急程度排序</span>
  687. </div>
  688. <div class="filter-tabs" id="todoTabs">
  689. <button class="filter-tab active" data-filter="all">全部 <span class="count">12</span></button>
  690. <button class="filter-tab" data-filter="reception">接收 <span class="count">3</span></button>
  691. <button class="filter-tab" data-filter="qc">质检 <span class="count">5</span></button>
  692. <button class="filter-tab" data-filter="prod">生产 <span class="count">2</span></button>
  693. <button class="filter-tab" data-filter="release">放行 <span class="count">4</span></button>
  694. <button class="filter-tab" data-filter="finance">财务 <span class="count">2</span></button>
  695. </div>
  696. <div class="list-body">
  697. <table>
  698. <thead>
  699. <tr>
  700. <th style="width:40px">序</th>
  701. <th>任务类型</th>
  702. <th>样本编码</th>
  703. <th>客户/协议</th>
  704. <th>当前环节</th>
  705. <th>等待时长</th>
  706. <th>操作</th>
  707. </tr>
  708. </thead>
  709. <tbody id="todoTableBody"></tbody>
  710. </table>
  711. </div>
  712. </section>
  713. <!-- 右侧说明 -->
  714. <div>
  715. <section class="panel">
  716. <div class="panel-top"><h2>三个概念示意</h2></div>
  717. <div class="concept-box">
  718. <div class="concept-step">
  719. <div class="step-num">1</div>
  720. <div class="step-body">
  721. <h4>工作台 = 统一待办入口</h4>
  722. <p>把分散在「样本接收、质检工单、生产工单、放行审核、财务确认」各菜单里的待处理项,按角色聚合成一张表。不用记流程顺序。</p>
  723. </div>
  724. </div>
  725. <div class="concept-step">
  726. <div class="step-num">2</div>
  727. <div class="step-body">
  728. <h4>样本 360 = 对象级枢纽</h4>
  729. <p>点任意样本编码,右侧抽屉展示全链路:协议→接收→质检→生产→放行→入库。各部门看同一份信息,不再各模块各查各的。</p>
  730. </div>
  731. </div>
  732. <div class="concept-step">
  733. <div class="step-num">3</div>
  734. <div class="step-body">
  735. <h4>深链 = 任务无缝衔接</h4>
  736. <p>360 视图里的「去配置检测」「去审核放行」等按钮,直接跳到对应模块并打开操作弹窗(带 sampleCode 参数),做完回到待办。</p>
  737. </div>
  738. </div>
  739. </div>
  740. </section>
  741. <section class="panel">
  742. <div class="panel-top"><h2>快捷入口</h2></div>
  743. <div class="quick-card">
  744. <div class="quick-links">
  745. <a href="#" class="quick-link" onclick="showDeepLink('/quality-manage/sample-reception'); return false;">
  746. <span class="quick-link-icon">📦</span>
  747. <span>样本接收登记</span>
  748. </a>
  749. <a href="#" class="quick-link" onclick="showDeepLink('/quality-manage/qc-work-order'); return false;">
  750. <span class="quick-link-icon">🔬</span>
  751. <span>质检工单</span>
  752. </a>
  753. <a href="#" class="quick-link" onclick="showDeepLink('/cell-storage/sample-info?tab=sample-qc'); return false;">
  754. <span class="quick-link-icon">📋</span>
  755. <span>样本中心 · 质检信息 Tab</span>
  756. </a>
  757. </div>
  758. </div>
  759. </section>
  760. </div>
  761. </div>
  762. </main>
  763. </div>
  764. </div>
  765. <!-- 样本 360 抽屉(复用 StorageReleaseViewDialog 思路) -->
  766. <div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
  767. <aside class="drawer" id="drawer">
  768. <div class="drawer-header">
  769. <h2>样本 360 <span class="sample-code" id="drawerSampleCode">ZK202506120001</span></h2>
  770. <button class="drawer-close" onclick="closeDrawer()" aria-label="关闭">×</button>
  771. </div>
  772. <div class="drawer-body" id="drawerBody"></div>
  773. </aside>
  774. <div class="toast" id="toast"></div>
  775. <script>
  776. const SAMPLES = {
  777. ZK202506120001: {
  778. code: 'ZK202506120001',
  779. customer: '张明华',
  780. protocol: 'XY-2025-0066',
  781. source: '细胞存储',
  782. stage: 'qc-config',
  783. timeline: [
  784. { key: 'protocol', label: '协议', status: 'done', text: '已审核' },
  785. { key: 'reception', label: '接收', status: 'done', text: '已接收' },
  786. { key: 'qc', label: '质检', status: 'current', text: '待配置' },
  787. { key: 'prod', label: '生产', status: 'pending', text: '待配置' },
  788. { key: 'release', label: '放行', status: 'pending', text: '未开始' },
  789. { key: 'storage', label: '入库', status: 'pending', text: '未开始' }
  790. ],
  791. actions: [
  792. { label: '配置检测项目', route: '/quality-manage/qc-work-order?sampleCode=ZK202506120001&action=configure', primary: true },
  793. { label: '查看接收详情', route: '/quality-manage/sample-reception?view=ZK202506120001' },
  794. { label: '打开样本中心', route: '/cell-storage/sample-info?tab=sample-qc&sampleCode=ZK202506120001' }
  795. ],
  796. protocolInfo: { no: 'XY-2025-0066', status: '已生效', customer: '张明华', type: '脐带血存储' },
  797. receptionInfo: { time: '2026-06-11 14:30', conclusion: '接收', handler: '李四', temp: '4.2℃' },
  798. qcInfo: { workOrder: 'ZJ202606110003', status: '待配置', tasks: '0/0' },
  799. prodInfo: { workOrder: 'SC202606110003', status: '待配置', tasks: '0/0' }
  800. },
  801. ZK202506110008: {
  802. code: 'ZK202506110008',
  803. customer: '李芳',
  804. protocol: 'JZ-2025-0012',
  805. source: '科研捐赠',
  806. stage: 'reception',
  807. timeline: [
  808. { key: 'protocol', label: '协议', status: 'done', text: '已审核' },
  809. { key: 'reception', label: '接收', status: 'current', text: '待接收' },
  810. { key: 'qc', label: '质检', status: 'pending', text: '未开始' },
  811. { key: 'prod', label: '生产', status: 'pending', text: '未开始' },
  812. { key: 'release', label: '放行', status: 'pending', text: '未开始' },
  813. { key: 'storage', label: '入库', status: 'pending', text: '未开始' }
  814. ],
  815. actions: [
  816. { label: '登记接收', route: '/quality-manage/sample-reception?sampleCode=ZK202506110008&action=receive', primary: true },
  817. { label: '查看捐赠协议', route: '/research-donation/audit?view=JZ-2025-0012' }
  818. ],
  819. protocolInfo: { no: 'JZ-2025-0012', status: '已生效', customer: '李芳', type: '科研捐赠' },
  820. receptionInfo: { time: '—', conclusion: '待接收', handler: '—', temp: '—' },
  821. qcInfo: { workOrder: '—', status: '—', tasks: '—' },
  822. prodInfo: { workOrder: '—', status: '—', tasks: '—' }
  823. },
  824. XY202506100003: {
  825. code: 'XY202506100003',
  826. customer: '王强',
  827. protocol: 'XY-2025-0088',
  828. source: '细胞存储',
  829. stage: 'storage',
  830. timeline: [
  831. { key: 'protocol', label: '协议', status: 'done', text: '已审核' },
  832. { key: 'reception', label: '接收', status: 'done', text: '已接收' },
  833. { key: 'qc', label: '质检', status: 'done', text: '已完成' },
  834. { key: 'prod', label: '生产', status: 'done', text: '已完成' },
  835. { key: 'release', label: '放行', status: 'done', text: '已通过' },
  836. { key: 'storage', label: '入库', status: 'done', text: '已入库' }
  837. ],
  838. actions: [
  839. { label: '查看入库记录', route: '/cell-storage/sample-info?tab=storage-entry&sampleCode=XY202506100003', primary: true },
  840. { label: '查看放行详情', route: '/cell-storage/storage-release-review?view=XY202506100003' }
  841. ],
  842. protocolInfo: { no: 'XY-2025-0088', status: '已生效', customer: '王强', type: '胎盘存储' },
  843. receptionInfo: { time: '2026-06-08 09:15', conclusion: '接收', handler: '李四', temp: '3.8℃' },
  844. qcInfo: { workOrder: 'ZJ202606080001', status: '已完成', tasks: '3/3' },
  845. prodInfo: { workOrder: 'SC202606080001', status: '已完成', tasks: '2/2' }
  846. }
  847. };
  848. const TODOS = [
  849. { id: 1, type: 'reception', typeLabel: '样本接收', sample: 'ZK202506110008', customer: '李芳 / JZ-2025-0012', stage: '待接收登记', wait: '2天', tag: 'info', action: '登记接收', actionRoute: '/quality-manage/sample-reception?sampleCode=ZK202506110008&action=receive' },
  850. { id: 2, type: 'qc', typeLabel: '质检工单', sample: 'ZK202506120001', customer: '张明华 / XY-2025-0066', stage: '待配置检测', wait: '6小时', tag: 'warning', action: '配置检测', actionRoute: '/quality-manage/qc-work-order?sampleCode=ZK202506120001&action=configure' },
  851. { id: 3, type: 'qc', typeLabel: '质检任务', sample: 'ZK202506090005', customer: '赵敏 / XY-2025-0055', stage: '实验室待执行', wait: '1天', tag: 'warning', action: '执行任务', actionRoute: '/quality-manage/my-qc-task?taskId=xxx' },
  852. { id: 4, type: 'prod', typeLabel: '生产工单', sample: 'ZK202506120001', customer: '张明华 / XY-2025-0066', stage: '待配置工艺', wait: '6小时', tag: 'info', action: '创建任务', actionRoute: '/production/production-work-order?sampleCode=ZK202506120001&action=configure' },
  853. { id: 5, type: 'release', typeLabel: '入库放行', sample: 'XY202506070002', customer: '陈静 / XY-2025-0044', stage: '待审核放行', wait: '3小时', tag: 'warning', action: '审核放行', actionRoute: '/cell-storage/storage-release-review?sampleCode=XY202506070002&action=review' },
  854. { id: 6, type: 'finance', typeLabel: '缴费确认', sample: '—', customer: '协议 XY-2025-0077', stage: '待财务确认', wait: '1天', tag: 'info', action: '确认到账', actionRoute: '/cell-storage/payment-confirm?protocolNo=XY-2025-0077' },
  855. { id: 7, type: 'reception', typeLabel: '样本接收', sample: 'ZK202506100012', customer: '刘洋 / XY-2025-0080', stage: '待接收登记', wait: '1天', tag: 'info', action: '登记接收', actionRoute: '/quality-manage/sample-reception?sampleCode=ZK202506100012&action=receive' },
  856. { id: 8, type: 'release', typeLabel: '样本入库', sample: 'XY202506070002', customer: '陈静 / XY-2025-0044', stage: '放行通过·待入库', wait: '1小时', tag: 'success', action: '执行入库', actionRoute: '/cell-storage/storage-release-review?sampleCode=XY202506070002&action=storage' }
  857. ];
  858. let currentFilter = 'all';
  859. function renderTodos() {
  860. const tbody = document.getElementById('todoTableBody');
  861. const filtered = currentFilter === 'all' ? TODOS : TODOS.filter(t => t.type === currentFilter);
  862. tbody.innerHTML = filtered.map((t, i) => `
  863. <tr onclick="openSample360('${t.sample}')" data-type="${t.type}">
  864. <td>${i + 1}</td>
  865. <td>${t.typeLabel}</td>
  866. <td>${t.sample !== '—' ? `<a class="link" href="#" onclick="event.stopPropagation(); openSample360('${t.sample}'); return false;">${t.sample}</a>` : '—'}</td>
  867. <td style="text-align:left">${t.customer}</td>
  868. <td><span class="tag ${t.tag}">${t.stage}</span></td>
  869. <td>${t.wait}</td>
  870. <td><button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); showDeepLink('${t.actionRoute}')">${t.action}</button></td>
  871. </tr>
  872. `).join('');
  873. }
  874. function filterTodos(type) {
  875. currentFilter = type;
  876. document.querySelectorAll('.filter-tab').forEach(tab => {
  877. tab.classList.toggle('active', tab.dataset.filter === type);
  878. });
  879. renderTodos();
  880. }
  881. document.getElementById('todoTabs').addEventListener('click', e => {
  882. const tab = e.target.closest('.filter-tab');
  883. if (tab) filterTodos(tab.dataset.filter);
  884. });
  885. function openSample360(code) {
  886. if (code === '—' || !code) return;
  887. const sample = SAMPLES[code] || SAMPLES['ZK202506120001'];
  888. document.getElementById('drawerSampleCode').textContent = sample.code;
  889. document.getElementById('drawerBody').innerHTML = buildDrawerContent(sample);
  890. document.getElementById('backdrop').classList.add('open');
  891. document.getElementById('drawer').classList.add('open');
  892. }
  893. function buildDrawerContent(s) {
  894. const tl = s.timeline.map(n => `
  895. <div class="tl-node ${n.status}">
  896. <div class="tl-dot">${n.status === 'done' ? '✓' : n.status === 'current' ? '●' : '○'}</div>
  897. <div class="tl-label">${n.label}</div>
  898. <div class="tl-status">${n.text}</div>
  899. </div>
  900. `).join('');
  901. const actions = s.actions.map(a =>
  902. `<button class="btn btn-sm ${a.primary ? 'btn-primary' : ''}" onclick="showDeepLink('${a.route}')">${a.label}</button>`
  903. ).join('');
  904. return `
  905. <div class="timeline">${tl}</div>
  906. <div class="action-bar">
  907. <div class="action-bar-label">▸ 根据当前环节,你可以直接执行(深链到原模块弹窗)</div>
  908. ${actions}
  909. </div>
  910. <div class="view-section">
  911. <div class="view-section-head">协议信息 <span class="tag success">${s.protocolInfo.status}</span></div>
  912. <div class="view-section-body">
  913. <div class="kv-grid">
  914. <div class="kv-item"><dt>协议号</dt><dd>${s.protocolInfo.no}</dd></div>
  915. <div class="kv-item"><dt>客户</dt><dd>${s.protocolInfo.customer}</dd></div>
  916. <div class="kv-item"><dt>业务来源</dt><dd>${s.source}</dd></div>
  917. <div class="kv-item"><dt>样本类型</dt><dd>${s.protocolInfo.type}</dd></div>
  918. </div>
  919. </div>
  920. </div>
  921. <div class="view-section">
  922. <div class="view-section-head">接收信息 <button class="btn btn-sm" onclick="showDeepLink('/quality-manage/sample-reception?view=${s.code}')">查看完整</button></div>
  923. <div class="view-section-body">
  924. <div class="kv-grid">
  925. <div class="kv-item"><dt>接收时间</dt><dd>${s.receptionInfo.time}</dd></div>
  926. <div class="kv-item"><dt>接收结论</dt><dd>${s.receptionInfo.conclusion}</dd></div>
  927. <div class="kv-item"><dt>接收人</dt><dd>${s.receptionInfo.handler}</dd></div>
  928. <div class="kv-item"><dt>接收温度</dt><dd>${s.receptionInfo.temp}</dd></div>
  929. </div>
  930. </div>
  931. </div>
  932. <div class="view-section">
  933. <div class="view-section-head">质检信息 <span class="tag ${s.qcInfo.status === '待配置' ? 'warning' : s.qcInfo.status === '已完成' ? 'success' : 'info'}">${s.qcInfo.status}</span></div>
  934. <div class="view-section-body">
  935. <div class="kv-grid">
  936. <div class="kv-item"><dt>质检工单</dt><dd>${s.qcInfo.workOrder}</dd></div>
  937. <div class="kv-item"><dt>任务进度</dt><dd>${s.qcInfo.tasks}</dd></div>
  938. </div>
  939. <p style="font-size:12px;color:var(--text-muted);margin-top:8px">此处复用现有 SampleQcInfoCards 组件(与 StorageReleaseViewDialog 一致)</p>
  940. </div>
  941. </div>
  942. <div class="view-section">
  943. <div class="view-section-head">生产信息 <span class="tag info">${s.prodInfo.status}</span></div>
  944. <div class="view-section-body">
  945. <div class="kv-grid">
  946. <div class="kv-item"><dt>生产工单</dt><dd>${s.prodInfo.workOrder}</dd></div>
  947. <div class="kv-item"><dt>任务进度</dt><dd>${s.prodInfo.tasks}</dd></div>
  948. </div>
  949. <p style="font-size:12px;color:var(--text-muted);margin-top:8px">此处复用现有 SampleProductionInfoCards 组件</p>
  950. </div>
  951. </div>
  952. <p style="font-size:12px;color:var(--text-muted);text-align:center;margin-top:16px">
  953. ↑ 这就是「样本 360」—— 一个抽屉/弹窗聚合全链路,替代在 6 个菜单间来回切换
  954. </p>
  955. `;
  956. }
  957. function closeDrawer() {
  958. document.getElementById('backdrop').classList.remove('open');
  959. document.getElementById('drawer').classList.remove('open');
  960. }
  961. function showDeepLink(route) {
  962. const toast = document.getElementById('toast');
  963. toast.innerHTML = `深链跳转(真实系统中会打开对应页面并弹出操作窗)<br/><code>/#${route}</code>`;
  964. toast.classList.add('show');
  965. setTimeout(() => toast.classList.remove('show'), 3500);
  966. }
  967. // Global search
  968. const searchInput = document.getElementById('globalSearch');
  969. const searchDropdown = document.getElementById('searchDropdown');
  970. searchInput.addEventListener('focus', () => searchDropdown.classList.add('open'));
  971. searchInput.addEventListener('input', () => {
  972. searchDropdown.classList.toggle('open', searchInput.value.length >= 0);
  973. });
  974. document.addEventListener('click', e => {
  975. if (!e.target.closest('.global-search')) searchDropdown.classList.remove('open');
  976. });
  977. searchDropdown.addEventListener('click', e => {
  978. const item = e.target.closest('.search-result');
  979. if (item) {
  980. openSample360(item.dataset.sample);
  981. searchDropdown.classList.remove('open');
  982. searchInput.value = item.dataset.sample;
  983. }
  984. });
  985. searchInput.addEventListener('keydown', e => {
  986. if (e.key === 'Enter' && searchInput.value.trim()) {
  987. const code = searchInput.value.trim().toUpperCase();
  988. openSample360(Object.keys(SAMPLES).find(k => k.includes(code)) || 'ZK202506120001');
  989. searchDropdown.classList.remove('open');
  990. }
  991. });
  992. document.addEventListener('keydown', e => {
  993. if (e.key === 'Escape') closeDrawer();
  994. });
  995. renderTodos();
  996. </script>
  997. </body>
  998. </html>