| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>中科兴皖 · 工作台 + 样本360 原型</title>
- <link rel="preconnect" href="https://fonts.googleapis.com" />
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
- <style>
- :root {
- --primary: #10b981;
- --primary-dark: #059669;
- --primary-bg: #ecfdf5;
- --text: #1e293b;
- --text-muted: #64748b;
- --border: #d9e8e2;
- --bg-page: #fefefe;
- --surface: #ffffff;
- --table-head: #f8fafc;
- --row-alt: #fafafa;
- --row-hover: #f1f5f9;
- --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
- --radius: 10px;
- --warn: #d97706;
- --warn-bg: #fffbeb;
- --danger: #dc2626;
- --danger-bg: #fef2f2;
- --info: #64748b;
- }
- * { box-sizing: border-box; margin: 0; padding: 0; }
- body {
- font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
- font-size: 14px;
- line-height: 1.5;
- color: var(--text);
- background: var(--bg-page);
- }
- .layout { display: flex; min-height: 100vh; }
- /* Sidebar */
- .sidebar {
- width: 220px;
- flex-shrink: 0;
- background: var(--surface);
- border-right: 1px solid var(--border);
- padding: 24px 16px;
- }
- .logo {
- font-size: 16px;
- font-weight: 700;
- padding: 0 12px 20px;
- margin-bottom: 8px;
- border-bottom: 1px solid var(--border);
- }
- .logo span { color: var(--primary); }
- .nav-group { margin-bottom: 4px; }
- .nav-group-title {
- font-size: 12px;
- font-weight: 700;
- color: var(--text-muted);
- padding: 8px 12px;
- letter-spacing: 0.02em;
- }
- .nav-item {
- display: block;
- padding: 8px 12px 8px 20px;
- margin-left: 4px;
- border-left: 2px solid var(--border);
- color: var(--text-muted);
- text-decoration: none;
- border-radius: 8px;
- font-size: 13px;
- transition: all 0.15s;
- }
- .nav-item:hover { background: var(--row-alt); color: var(--text); }
- .nav-item.active {
- background: var(--primary-bg);
- color: var(--primary-dark);
- border-left-color: var(--primary);
- font-weight: 600;
- }
- .nav-badge {
- float: right;
- background: var(--primary);
- color: #fff;
- font-size: 11px;
- padding: 0 6px;
- border-radius: 10px;
- min-width: 18px;
- text-align: center;
- }
- /* Main */
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
- .topbar {
- height: 56px;
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 0 24px;
- background: var(--surface);
- border-bottom: 1px solid var(--border);
- }
- .topbar-title { font-weight: 600; font-size: 15px; white-space: nowrap; }
- .global-search {
- flex: 1;
- max-width: 480px;
- position: relative;
- }
- .global-search input {
- width: 100%;
- height: 36px;
- padding: 0 12px 0 36px;
- border: 1px solid var(--border);
- border-radius: 8px;
- font-size: 13px;
- outline: none;
- }
- .global-search input:focus {
- border-color: var(--primary);
- box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
- }
- .global-search svg {
- position: absolute;
- left: 12px;
- top: 50%;
- transform: translateY(-50%);
- width: 16px;
- height: 16px;
- color: var(--text-muted);
- }
- .search-hint {
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- font-size: 11px;
- color: var(--text-muted);
- background: var(--table-head);
- padding: 2px 6px;
- border-radius: 4px;
- }
- .user-chip {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 13px;
- color: var(--text-muted);
- }
- .avatar {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- background: var(--primary-bg);
- color: var(--primary-dark);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 13px;
- }
- .content { padding: 24px; flex: 1; overflow: auto; }
- /* Prototype banner */
- .proto-banner {
- background: linear-gradient(90deg, var(--primary-bg), #fff);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 12px 16px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .proto-banner strong { color: var(--primary-dark); }
- .proto-banner p { font-size: 13px; color: var(--text-muted); flex: 1; }
- /* Stats */
- .stats-row {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 12px;
- margin-bottom: 20px;
- }
- .stat-card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 16px;
- box-shadow: var(--shadow);
- cursor: pointer;
- transition: border-color 0.15s;
- }
- .stat-card:hover { border-color: var(--primary); }
- .stat-card.warn { border-color: rgba(217, 119, 6, 0.25); }
- .stat-label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
- .stat-num { font-size: 28px; font-weight: 700; color: var(--text); line-height: 1.2; }
- .stat-card.warn .stat-num { color: var(--warn); }
- .stat-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
- /* Panel */
- .panel {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- box-shadow: var(--shadow);
- margin-bottom: 20px;
- }
- .panel-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 20px;
- border-bottom: 1px solid var(--border);
- }
- .panel-top h2 {
- font-size: 15px;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .panel-top h2::before {
- content: '';
- width: 3px;
- height: 16px;
- background: var(--primary);
- border-radius: 2px;
- }
- .filter-tabs {
- display: flex;
- gap: 4px;
- padding: 0 20px;
- border-bottom: 1px solid var(--border);
- }
- .filter-tab {
- padding: 10px 14px;
- font-size: 13px;
- color: var(--text-muted);
- background: none;
- border: none;
- border-bottom: 2px solid transparent;
- cursor: pointer;
- margin-bottom: -1px;
- }
- .filter-tab:hover { color: var(--text); }
- .filter-tab.active {
- color: var(--primary-dark);
- border-bottom-color: var(--primary);
- font-weight: 600;
- }
- .filter-tab .count {
- display: inline-block;
- margin-left: 4px;
- font-size: 11px;
- background: var(--table-head);
- padding: 0 6px;
- border-radius: 8px;
- }
- .filter-tab.active .count { background: var(--primary-bg); color: var(--primary-dark); }
- /* Table */
- .list-body { padding: 0; }
- table { width: 100%; border-collapse: collapse; }
- th {
- background: var(--table-head);
- font-size: 12px;
- font-weight: 600;
- color: var(--text-muted);
- padding: 10px 12px;
- text-align: center;
- border-bottom: 1px solid var(--border);
- }
- td {
- padding: 12px;
- text-align: center;
- border-bottom: 1px solid var(--border);
- font-size: 13px;
- }
- tr:nth-child(even) td { background: var(--row-alt); }
- tr:hover td { background: var(--row-hover); }
- tr { cursor: pointer; }
- .link {
- color: var(--primary-dark);
- font-weight: 500;
- text-decoration: none;
- border-bottom: 1px dashed rgba(5, 150, 105, 0.4);
- }
- .link:hover { color: var(--primary); }
- .tag {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 500;
- }
- .tag.success { background: var(--primary-bg); color: var(--primary-dark); }
- .tag.warning { background: var(--warn-bg); color: var(--warn); }
- .tag.info { background: var(--table-head); color: var(--info); }
- .tag.danger { background: var(--danger-bg); color: var(--danger); }
- .btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- height: 32px;
- padding: 0 12px;
- border-radius: 8px;
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- border: 1px solid var(--border);
- background: var(--surface);
- color: var(--text);
- transition: all 0.15s;
- }
- .btn:hover { border-color: var(--primary); color: var(--primary-dark); }
- .btn-primary {
- background: var(--primary);
- border-color: var(--primary);
- color: #fff;
- }
- .btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); color: #fff; }
- .btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
- /* Two column layout */
- .grid-2 {
- display: grid;
- grid-template-columns: 1fr 360px;
- gap: 20px;
- }
- .quick-card {
- padding: 16px 20px;
- }
- .quick-card h3 {
- font-size: 13px;
- font-weight: 600;
- margin-bottom: 12px;
- color: var(--text-muted);
- }
- .quick-links { display: flex; flex-direction: column; gap: 8px; }
- .quick-link {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--border);
- border-radius: 8px;
- text-decoration: none;
- color: var(--text);
- font-size: 13px;
- transition: all 0.15s;
- }
- .quick-link:hover { border-color: var(--primary); background: var(--primary-bg); }
- .quick-link-icon {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- background: var(--primary-bg);
- color: var(--primary-dark);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- flex-shrink: 0;
- }
- .concept-box {
- padding: 16px 20px 20px;
- }
- .concept-step {
- display: flex;
- gap: 12px;
- margin-bottom: 16px;
- align-items: flex-start;
- }
- .step-num {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- background: var(--primary);
- color: #fff;
- font-size: 12px;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- .step-body h4 { font-size: 13px; font-weight: 600; margin-bottom: 2px; }
- .step-body p { font-size: 12px; color: var(--text-muted); }
- /* Drawer overlay */
- .backdrop {
- position: fixed;
- inset: 0;
- background: rgba(15, 23, 42, 0.35);
- z-index: 1000;
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.25s;
- }
- .backdrop.open { opacity: 1; pointer-events: auto; }
- .drawer {
- position: fixed;
- top: 0;
- right: 0;
- width: min(920px, 96vw);
- height: 100vh;
- background: var(--surface);
- z-index: 1001;
- box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
- transform: translateX(100%);
- transition: transform 0.3s ease;
- display: flex;
- flex-direction: column;
- }
- .drawer.open { transform: translateX(0); }
- .drawer-header {
- padding: 16px 24px;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-shrink: 0;
- }
- .drawer-header h2 {
- font-size: 16px;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .drawer-header .sample-code {
- font-family: ui-monospace, monospace;
- color: var(--primary-dark);
- background: var(--primary-bg);
- padding: 2px 8px;
- border-radius: 6px;
- font-size: 14px;
- }
- .drawer-close {
- width: 32px;
- height: 32px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: var(--surface);
- cursor: pointer;
- font-size: 18px;
- color: var(--text-muted);
- }
- .drawer-close:hover { background: var(--row-alt); }
- .drawer-body { flex: 1; overflow-y: auto; padding: 20px 24px 24px; }
- /* Timeline */
- .timeline {
- display: flex;
- align-items: flex-start;
- gap: 0;
- margin-bottom: 24px;
- padding: 16px;
- background: var(--table-head);
- border-radius: var(--radius);
- overflow-x: auto;
- }
- .tl-node {
- flex: 1;
- min-width: 100px;
- text-align: center;
- position: relative;
- }
- .tl-node:not(:last-child)::after {
- content: '';
- position: absolute;
- top: 14px;
- left: calc(50% + 16px);
- width: calc(100% - 32px);
- height: 2px;
- background: var(--border);
- }
- .tl-node.done:not(:last-child)::after { background: var(--primary); }
- .tl-dot {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- border: 2px solid var(--border);
- background: var(--surface);
- margin: 0 auto 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- position: relative;
- z-index: 1;
- }
- .tl-node.done .tl-dot {
- border-color: var(--primary);
- background: var(--primary-bg);
- color: var(--primary-dark);
- }
- .tl-node.current .tl-dot {
- border-color: var(--primary);
- background: var(--primary);
- color: #fff;
- box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
- }
- .tl-node.pending .tl-dot { color: var(--text-muted); }
- .tl-label { font-size: 12px; font-weight: 600; color: var(--text); }
- .tl-status { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
- /* Action bar in drawer */
- .action-bar {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- padding: 12px 16px;
- background: var(--primary-bg);
- border: 1px solid rgba(16, 185, 129, 0.25);
- border-radius: var(--radius);
- margin-bottom: 20px;
- }
- .action-bar-label {
- width: 100%;
- font-size: 12px;
- font-weight: 600;
- color: var(--primary-dark);
- margin-bottom: 4px;
- }
- /* Sections in drawer */
- .view-section {
- margin-bottom: 20px;
- border: 1px solid var(--border);
- border-radius: var(--radius);
- overflow: hidden;
- }
- .view-section-head {
- padding: 10px 16px;
- background: var(--table-head);
- font-size: 13px;
- font-weight: 600;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .view-section-body { padding: 12px 16px; }
- .kv-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 12px 16px;
- }
- .kv-item dt { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
- .kv-item dd { font-size: 13px; font-weight: 500; }
- /* Deep link toast */
- .toast {
- position: fixed;
- bottom: 24px;
- left: 50%;
- transform: translateX(-50%) translateY(80px);
- background: var(--text);
- color: #fff;
- padding: 12px 20px;
- border-radius: var(--radius);
- font-size: 13px;
- z-index: 2000;
- opacity: 0;
- transition: all 0.3s;
- max-width: 90vw;
- text-align: center;
- }
- .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
- .toast code {
- background: rgba(255,255,255,0.15);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 12px;
- }
- /* Search dropdown */
- .search-dropdown {
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- box-shadow: 0 8px 24px rgba(0,0,0,0.1);
- z-index: 100;
- display: none;
- }
- .search-dropdown.open { display: block; }
- .search-result {
- padding: 10px 14px;
- cursor: pointer;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .search-result:hover { background: var(--row-hover); }
- .search-result:last-child { border-bottom: none; }
- .search-result-meta { font-size: 12px; color: var(--text-muted); }
- @media (max-width: 1100px) {
- .stats-row { grid-template-columns: repeat(3, 1fr); }
- .grid-2 { grid-template-columns: 1fr; }
- .kv-grid { grid-template-columns: repeat(2, 1fr); }
- }
- </style>
- </head>
- <body>
- <div class="layout">
- <!-- 侧栏:重组后的菜单 IA(示意) -->
- <aside class="sidebar">
- <div class="logo">中科兴皖<span> · 管理端</span></div>
- <div class="nav-group">
- <div class="nav-group-title">日常作业</div>
- <a href="#" class="nav-item active">工作台 <span class="nav-badge">12</span></a>
- <a href="#" class="nav-item">样本中心</a>
- <a href="#" class="nav-item">质检作业</a>
- <a href="#" class="nav-item">生产作业</a>
- <a href="#" class="nav-item">服务订单</a>
- <a href="#" class="nav-item">财务确认</a>
- </div>
- <div class="nav-group">
- <div class="nav-group-title">配置</div>
- <a href="#" class="nav-item">主数据</a>
- <a href="#" class="nav-item">系统管理</a>
- </div>
- </aside>
- <div class="main">
- <!-- 顶栏:全局检索入口 -->
- <header class="topbar">
- <span class="topbar-title">工作台</span>
- <div class="global-search">
- <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>
- <input type="text" id="globalSearch" placeholder="搜索样本编码 / 协议号 / 工单号…" autocomplete="off" />
- <span class="search-hint">Enter</span>
- <div class="search-dropdown" id="searchDropdown">
- <div class="search-result" data-sample="ZK202506120001">
- <div><strong>ZK202506120001</strong> · 张明华 · 细胞存储</div>
- <span class="tag warning">待配置质检</span>
- </div>
- <div class="search-result" data-sample="ZK202506110008">
- <div><strong>ZK202506110008</strong> · 李芳 · 科研捐赠</div>
- <span class="tag info">待接收</span>
- </div>
- <div class="search-result" data-sample="XY202506100003">
- <div><strong>XY202506100003</strong> · 协议号 XY-2025-0088</div>
- <span class="tag success">已入库</span>
- </div>
- </div>
- </div>
- <div class="user-chip">
- <span>质检专员 · 张三</span>
- <div class="avatar">张</div>
- </div>
- </header>
- <main class="content">
- <div class="proto-banner">
- <strong>交互原型</strong>
- <p>演示「工作台聚合待办 → 点击样本编码打开 360 视图 → 一键深链到具体操作页」。点击表格行、统计卡片、快捷入口均可体验。</p>
- <button class="btn btn-primary btn-sm" onclick="openSample360('ZK202506120001')">打开样本 360 示例</button>
- </div>
- <!-- 统计卡片:各部门待办一目了然 -->
- <div class="stats-row">
- <div class="stat-card warn" onclick="filterTodos('reception')">
- <div class="stat-label">待接收样本</div>
- <div class="stat-num">3</div>
- <div class="stat-sub">质检接收岗</div>
- </div>
- <div class="stat-card warn" onclick="filterTodos('qc')">
- <div class="stat-label">待配置/执行质检</div>
- <div class="stat-num">5</div>
- <div class="stat-sub">质检专员</div>
- </div>
- <div class="stat-card" onclick="filterTodos('prod')">
- <div class="stat-label">生产工单待办</div>
- <div class="stat-num">2</div>
- <div class="stat-sub">生产岗</div>
- </div>
- <div class="stat-card" onclick="filterTodos('release')">
- <div class="stat-label">待放行/入库</div>
- <div class="stat-num">4</div>
- <div class="stat-sub">QA / 库管</div>
- </div>
- <div class="stat-card" onclick="filterTodos('finance')">
- <div class="stat-label">财务待确认</div>
- <div class="stat-num">2</div>
- <div class="stat-sub">财务岗</div>
- </div>
- </div>
- <div class="grid-2">
- <!-- 统一待办列表 -->
- <section class="panel">
- <div class="panel-top">
- <h2>我的待办(跨模块聚合)</h2>
- <span style="font-size:12px;color:var(--text-muted)">共 12 项 · 按紧急程度排序</span>
- </div>
- <div class="filter-tabs" id="todoTabs">
- <button class="filter-tab active" data-filter="all">全部 <span class="count">12</span></button>
- <button class="filter-tab" data-filter="reception">接收 <span class="count">3</span></button>
- <button class="filter-tab" data-filter="qc">质检 <span class="count">5</span></button>
- <button class="filter-tab" data-filter="prod">生产 <span class="count">2</span></button>
- <button class="filter-tab" data-filter="release">放行 <span class="count">4</span></button>
- <button class="filter-tab" data-filter="finance">财务 <span class="count">2</span></button>
- </div>
- <div class="list-body">
- <table>
- <thead>
- <tr>
- <th style="width:40px">序</th>
- <th>任务类型</th>
- <th>样本编码</th>
- <th>客户/协议</th>
- <th>当前环节</th>
- <th>等待时长</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody id="todoTableBody"></tbody>
- </table>
- </div>
- </section>
- <!-- 右侧说明 -->
- <div>
- <section class="panel">
- <div class="panel-top"><h2>三个概念示意</h2></div>
- <div class="concept-box">
- <div class="concept-step">
- <div class="step-num">1</div>
- <div class="step-body">
- <h4>工作台 = 统一待办入口</h4>
- <p>把分散在「样本接收、质检工单、生产工单、放行审核、财务确认」各菜单里的待处理项,按角色聚合成一张表。不用记流程顺序。</p>
- </div>
- </div>
- <div class="concept-step">
- <div class="step-num">2</div>
- <div class="step-body">
- <h4>样本 360 = 对象级枢纽</h4>
- <p>点任意样本编码,右侧抽屉展示全链路:协议→接收→质检→生产→放行→入库。各部门看同一份信息,不再各模块各查各的。</p>
- </div>
- </div>
- <div class="concept-step">
- <div class="step-num">3</div>
- <div class="step-body">
- <h4>深链 = 任务无缝衔接</h4>
- <p>360 视图里的「去配置检测」「去审核放行」等按钮,直接跳到对应模块并打开操作弹窗(带 sampleCode 参数),做完回到待办。</p>
- </div>
- </div>
- </div>
- </section>
- <section class="panel">
- <div class="panel-top"><h2>快捷入口</h2></div>
- <div class="quick-card">
- <div class="quick-links">
- <a href="#" class="quick-link" onclick="showDeepLink('/quality-manage/sample-reception'); return false;">
- <span class="quick-link-icon">📦</span>
- <span>样本接收登记</span>
- </a>
- <a href="#" class="quick-link" onclick="showDeepLink('/quality-manage/qc-work-order'); return false;">
- <span class="quick-link-icon">🔬</span>
- <span>质检工单</span>
- </a>
- <a href="#" class="quick-link" onclick="showDeepLink('/cell-storage/sample-info?tab=sample-qc'); return false;">
- <span class="quick-link-icon">📋</span>
- <span>样本中心 · 质检信息 Tab</span>
- </a>
- </div>
- </div>
- </section>
- </div>
- </div>
- </main>
- </div>
- </div>
- <!-- 样本 360 抽屉(复用 StorageReleaseViewDialog 思路) -->
- <div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
- <aside class="drawer" id="drawer">
- <div class="drawer-header">
- <h2>样本 360 <span class="sample-code" id="drawerSampleCode">ZK202506120001</span></h2>
- <button class="drawer-close" onclick="closeDrawer()" aria-label="关闭">×</button>
- </div>
- <div class="drawer-body" id="drawerBody"></div>
- </aside>
- <div class="toast" id="toast"></div>
- <script>
- const SAMPLES = {
- ZK202506120001: {
- code: 'ZK202506120001',
- customer: '张明华',
- protocol: 'XY-2025-0066',
- source: '细胞存储',
- stage: 'qc-config',
- timeline: [
- { key: 'protocol', label: '协议', status: 'done', text: '已审核' },
- { key: 'reception', label: '接收', status: 'done', text: '已接收' },
- { key: 'qc', label: '质检', status: 'current', text: '待配置' },
- { key: 'prod', label: '生产', status: 'pending', text: '待配置' },
- { key: 'release', label: '放行', status: 'pending', text: '未开始' },
- { key: 'storage', label: '入库', status: 'pending', text: '未开始' }
- ],
- actions: [
- { label: '配置检测项目', route: '/quality-manage/qc-work-order?sampleCode=ZK202506120001&action=configure', primary: true },
- { label: '查看接收详情', route: '/quality-manage/sample-reception?view=ZK202506120001' },
- { label: '打开样本中心', route: '/cell-storage/sample-info?tab=sample-qc&sampleCode=ZK202506120001' }
- ],
- protocolInfo: { no: 'XY-2025-0066', status: '已生效', customer: '张明华', type: '脐带血存储' },
- receptionInfo: { time: '2026-06-11 14:30', conclusion: '接收', handler: '李四', temp: '4.2℃' },
- qcInfo: { workOrder: 'ZJ202606110003', status: '待配置', tasks: '0/0' },
- prodInfo: { workOrder: 'SC202606110003', status: '待配置', tasks: '0/0' }
- },
- ZK202506110008: {
- code: 'ZK202506110008',
- customer: '李芳',
- protocol: 'JZ-2025-0012',
- source: '科研捐赠',
- stage: 'reception',
- timeline: [
- { key: 'protocol', label: '协议', status: 'done', text: '已审核' },
- { key: 'reception', label: '接收', status: 'current', text: '待接收' },
- { key: 'qc', label: '质检', status: 'pending', text: '未开始' },
- { key: 'prod', label: '生产', status: 'pending', text: '未开始' },
- { key: 'release', label: '放行', status: 'pending', text: '未开始' },
- { key: 'storage', label: '入库', status: 'pending', text: '未开始' }
- ],
- actions: [
- { label: '登记接收', route: '/quality-manage/sample-reception?sampleCode=ZK202506110008&action=receive', primary: true },
- { label: '查看捐赠协议', route: '/research-donation/audit?view=JZ-2025-0012' }
- ],
- protocolInfo: { no: 'JZ-2025-0012', status: '已生效', customer: '李芳', type: '科研捐赠' },
- receptionInfo: { time: '—', conclusion: '待接收', handler: '—', temp: '—' },
- qcInfo: { workOrder: '—', status: '—', tasks: '—' },
- prodInfo: { workOrder: '—', status: '—', tasks: '—' }
- },
- XY202506100003: {
- code: 'XY202506100003',
- customer: '王强',
- protocol: 'XY-2025-0088',
- source: '细胞存储',
- stage: 'storage',
- timeline: [
- { key: 'protocol', label: '协议', status: 'done', text: '已审核' },
- { key: 'reception', label: '接收', status: 'done', text: '已接收' },
- { key: 'qc', label: '质检', status: 'done', text: '已完成' },
- { key: 'prod', label: '生产', status: 'done', text: '已完成' },
- { key: 'release', label: '放行', status: 'done', text: '已通过' },
- { key: 'storage', label: '入库', status: 'done', text: '已入库' }
- ],
- actions: [
- { label: '查看入库记录', route: '/cell-storage/sample-info?tab=storage-entry&sampleCode=XY202506100003', primary: true },
- { label: '查看放行详情', route: '/cell-storage/storage-release-review?view=XY202506100003' }
- ],
- protocolInfo: { no: 'XY-2025-0088', status: '已生效', customer: '王强', type: '胎盘存储' },
- receptionInfo: { time: '2026-06-08 09:15', conclusion: '接收', handler: '李四', temp: '3.8℃' },
- qcInfo: { workOrder: 'ZJ202606080001', status: '已完成', tasks: '3/3' },
- prodInfo: { workOrder: 'SC202606080001', status: '已完成', tasks: '2/2' }
- }
- };
- const TODOS = [
- { 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' },
- { 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' },
- { 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' },
- { 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' },
- { 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' },
- { 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' },
- { 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' },
- { 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' }
- ];
- let currentFilter = 'all';
- function renderTodos() {
- const tbody = document.getElementById('todoTableBody');
- const filtered = currentFilter === 'all' ? TODOS : TODOS.filter(t => t.type === currentFilter);
- tbody.innerHTML = filtered.map((t, i) => `
- <tr onclick="openSample360('${t.sample}')" data-type="${t.type}">
- <td>${i + 1}</td>
- <td>${t.typeLabel}</td>
- <td>${t.sample !== '—' ? `<a class="link" href="#" onclick="event.stopPropagation(); openSample360('${t.sample}'); return false;">${t.sample}</a>` : '—'}</td>
- <td style="text-align:left">${t.customer}</td>
- <td><span class="tag ${t.tag}">${t.stage}</span></td>
- <td>${t.wait}</td>
- <td><button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); showDeepLink('${t.actionRoute}')">${t.action}</button></td>
- </tr>
- `).join('');
- }
- function filterTodos(type) {
- currentFilter = type;
- document.querySelectorAll('.filter-tab').forEach(tab => {
- tab.classList.toggle('active', tab.dataset.filter === type);
- });
- renderTodos();
- }
- document.getElementById('todoTabs').addEventListener('click', e => {
- const tab = e.target.closest('.filter-tab');
- if (tab) filterTodos(tab.dataset.filter);
- });
- function openSample360(code) {
- if (code === '—' || !code) return;
- const sample = SAMPLES[code] || SAMPLES['ZK202506120001'];
- document.getElementById('drawerSampleCode').textContent = sample.code;
- document.getElementById('drawerBody').innerHTML = buildDrawerContent(sample);
- document.getElementById('backdrop').classList.add('open');
- document.getElementById('drawer').classList.add('open');
- }
- function buildDrawerContent(s) {
- const tl = s.timeline.map(n => `
- <div class="tl-node ${n.status}">
- <div class="tl-dot">${n.status === 'done' ? '✓' : n.status === 'current' ? '●' : '○'}</div>
- <div class="tl-label">${n.label}</div>
- <div class="tl-status">${n.text}</div>
- </div>
- `).join('');
- const actions = s.actions.map(a =>
- `<button class="btn btn-sm ${a.primary ? 'btn-primary' : ''}" onclick="showDeepLink('${a.route}')">${a.label}</button>`
- ).join('');
- return `
- <div class="timeline">${tl}</div>
- <div class="action-bar">
- <div class="action-bar-label">▸ 根据当前环节,你可以直接执行(深链到原模块弹窗)</div>
- ${actions}
- </div>
- <div class="view-section">
- <div class="view-section-head">协议信息 <span class="tag success">${s.protocolInfo.status}</span></div>
- <div class="view-section-body">
- <div class="kv-grid">
- <div class="kv-item"><dt>协议号</dt><dd>${s.protocolInfo.no}</dd></div>
- <div class="kv-item"><dt>客户</dt><dd>${s.protocolInfo.customer}</dd></div>
- <div class="kv-item"><dt>业务来源</dt><dd>${s.source}</dd></div>
- <div class="kv-item"><dt>样本类型</dt><dd>${s.protocolInfo.type}</dd></div>
- </div>
- </div>
- </div>
- <div class="view-section">
- <div class="view-section-head">接收信息 <button class="btn btn-sm" onclick="showDeepLink('/quality-manage/sample-reception?view=${s.code}')">查看完整</button></div>
- <div class="view-section-body">
- <div class="kv-grid">
- <div class="kv-item"><dt>接收时间</dt><dd>${s.receptionInfo.time}</dd></div>
- <div class="kv-item"><dt>接收结论</dt><dd>${s.receptionInfo.conclusion}</dd></div>
- <div class="kv-item"><dt>接收人</dt><dd>${s.receptionInfo.handler}</dd></div>
- <div class="kv-item"><dt>接收温度</dt><dd>${s.receptionInfo.temp}</dd></div>
- </div>
- </div>
- </div>
- <div class="view-section">
- <div class="view-section-head">质检信息 <span class="tag ${s.qcInfo.status === '待配置' ? 'warning' : s.qcInfo.status === '已完成' ? 'success' : 'info'}">${s.qcInfo.status}</span></div>
- <div class="view-section-body">
- <div class="kv-grid">
- <div class="kv-item"><dt>质检工单</dt><dd>${s.qcInfo.workOrder}</dd></div>
- <div class="kv-item"><dt>任务进度</dt><dd>${s.qcInfo.tasks}</dd></div>
- </div>
- <p style="font-size:12px;color:var(--text-muted);margin-top:8px">此处复用现有 SampleQcInfoCards 组件(与 StorageReleaseViewDialog 一致)</p>
- </div>
- </div>
- <div class="view-section">
- <div class="view-section-head">生产信息 <span class="tag info">${s.prodInfo.status}</span></div>
- <div class="view-section-body">
- <div class="kv-grid">
- <div class="kv-item"><dt>生产工单</dt><dd>${s.prodInfo.workOrder}</dd></div>
- <div class="kv-item"><dt>任务进度</dt><dd>${s.prodInfo.tasks}</dd></div>
- </div>
- <p style="font-size:12px;color:var(--text-muted);margin-top:8px">此处复用现有 SampleProductionInfoCards 组件</p>
- </div>
- </div>
- <p style="font-size:12px;color:var(--text-muted);text-align:center;margin-top:16px">
- ↑ 这就是「样本 360」—— 一个抽屉/弹窗聚合全链路,替代在 6 个菜单间来回切换
- </p>
- `;
- }
- function closeDrawer() {
- document.getElementById('backdrop').classList.remove('open');
- document.getElementById('drawer').classList.remove('open');
- }
- function showDeepLink(route) {
- const toast = document.getElementById('toast');
- toast.innerHTML = `深链跳转(真实系统中会打开对应页面并弹出操作窗)<br/><code>/#${route}</code>`;
- toast.classList.add('show');
- setTimeout(() => toast.classList.remove('show'), 3500);
- }
- // Global search
- const searchInput = document.getElementById('globalSearch');
- const searchDropdown = document.getElementById('searchDropdown');
- searchInput.addEventListener('focus', () => searchDropdown.classList.add('open'));
- searchInput.addEventListener('input', () => {
- searchDropdown.classList.toggle('open', searchInput.value.length >= 0);
- });
- document.addEventListener('click', e => {
- if (!e.target.closest('.global-search')) searchDropdown.classList.remove('open');
- });
- searchDropdown.addEventListener('click', e => {
- const item = e.target.closest('.search-result');
- if (item) {
- openSample360(item.dataset.sample);
- searchDropdown.classList.remove('open');
- searchInput.value = item.dataset.sample;
- }
- });
- searchInput.addEventListener('keydown', e => {
- if (e.key === 'Enter' && searchInput.value.trim()) {
- const code = searchInput.value.trim().toUpperCase();
- openSample360(Object.keys(SAMPLES).find(k => k.includes(code)) || 'ZK202506120001');
- searchDropdown.classList.remove('open');
- }
- });
- document.addEventListener('keydown', e => {
- if (e.key === 'Escape') closeDrawer();
- });
- renderTodos();
- </script>
- </body>
- </html>
|