| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>中科兴皖 · 一体化流程 + 角色聚焦(兼容原型)</title>
- <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;
- --surface: #fff; --table-head: #f8fafc; --row-hover: #f1f5f9;
- --radius: 10px; --warn: #d97706; --warn-bg: #fffbeb;
- --info: #3b82f6; --info-bg: #eff6ff;
- }
- * { box-sizing: border-box; margin: 0; padding: 0; }
- body { font-family: Inter, sans-serif; font-size: 14px; line-height: 1.5; color: var(--text); background: #fefefe; }
- .layout { display: flex; min-height: 100vh; }
- .sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); padding: 20px 14px; flex-shrink: 0; }
- .logo { font-size: 15px; font-weight: 700; padding: 0 10px 16px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
- .logo span { color: var(--primary); }
- .nav-item { display: block; padding: 8px 12px; color: var(--text-muted); text-decoration: none; border-radius: 8px; font-size: 13px; margin-bottom: 2px; }
- .nav-item.active { background: var(--primary-bg); color: var(--primary-dark); font-weight: 600; }
- .nav-sub { font-size: 11px; color: var(--text-muted); padding: 8px 12px 4px; }
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
- /* Role bar */
- .role-bar {
- background: var(--surface); border-bottom: 1px solid var(--border);
- padding: 10px 20px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
- }
- .role-bar-label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
- .role-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
- .role-tab {
- padding: 6px 14px; border-radius: 8px; border: 1px solid var(--border);
- background: var(--surface); font-size: 13px; cursor: pointer; transition: all 0.15s;
- }
- .role-tab:hover { border-color: var(--primary); }
- .role-tab.active { background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 600; }
- .role-desc { font-size: 12px; color: var(--text-muted); flex: 1; min-width: 200px; }
- .view-toggle { display: flex; gap: 4px; margin-left: auto; }
- .view-toggle button {
- padding: 5px 10px; font-size: 12px; border: 1px solid var(--border); border-radius: 6px;
- background: var(--surface); cursor: pointer;
- }
- .view-toggle button.active { background: var(--primary-bg); border-color: var(--primary); color: var(--primary-dark); font-weight: 600; }
- .topbar { padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--surface); }
- .topbar h1 { font-size: 15px; font-weight: 600; }
- .topbar p { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
- .content { flex: 1; padding: 16px 20px; overflow: auto; }
- .concept-banner {
- padding: 10px 14px; background: linear-gradient(90deg, var(--primary-bg), #fff);
- border: 1px solid var(--border); border-radius: var(--radius); font-size: 12px; color: var(--text-muted); margin-bottom: 14px;
- }
- .concept-banner strong { color: var(--primary-dark); }
- .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 14px; }
- .panel-head {
- padding: 12px 16px; border-bottom: 1px solid var(--border);
- display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px;
- }
- .panel-head h2 { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
- .panel-head h2::before { content: ''; width: 3px; height: 14px; background: var(--primary); border-radius: 2px; }
- .stat-row { display: flex; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
- .stat-pill {
- padding: 6px 12px; border-radius: 8px; background: var(--table-head); font-size: 12px;
- }
- .stat-pill em { font-style: normal; font-weight: 700; color: var(--warn); margin-left: 4px; }
- table { width: 100%; border-collapse: collapse; }
- th { background: var(--table-head); font-size: 12px; color: var(--text-muted); padding: 9px 12px; text-align: center; border-bottom: 1px solid var(--border); }
- td { padding: 11px 12px; text-align: center; border-bottom: 1px solid var(--border); font-size: 13px; }
- tr { cursor: pointer; }
- tr:hover td { background: var(--row-hover); }
- tr.selected td { background: var(--primary-bg); }
- .tag { display: inline-block; padding: 2px 7px; border-radius: 5px; font-size: 11px; 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(--info-bg); color: var(--info); }
- .tag.muted { background: var(--table-head); color: var(--text-muted); }
- .btn {
- height: 30px; padding: 0 11px; border-radius: 7px; font-size: 12px; font-weight: 500;
- border: 1px solid var(--border); background: var(--surface); cursor: pointer;
- }
- .btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
- /* ── Process Spine (shared component) ── */
- .spine-wrap {
- border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
- margin-bottom: 14px; overflow: hidden;
- }
- .spine-header {
- padding: 10px 14px; display: flex; align-items: center; justify-content: space-between;
- background: var(--table-head); cursor: pointer; user-select: none;
- }
- .spine-header h3 { font-size: 13px; font-weight: 600; }
- .spine-header .meta { font-size: 12px; color: var(--text-muted); }
- .spine-toggle { font-size: 12px; color: var(--primary-dark); font-weight: 500; }
- .spine-body { padding: 12px 14px 14px; border-top: 1px solid var(--border); }
- .spine-body.collapsed { display: none; }
- .spine-compact { display: flex; align-items: center; gap: 0; overflow-x: auto; padding: 4px 0; }
- .sp-node {
- flex: 0 0 auto; text-align: center; min-width: 72px; position: relative; padding: 0 2px;
- }
- .sp-node:not(:last-child)::after {
- content: ''; position: absolute; top: 11px; left: calc(50% + 18px); width: calc(100% - 36px); height: 2px; background: var(--border);
- }
- .sp-node.done:not(:last-child)::after { background: var(--primary); }
- .sp-dot {
- width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--border); margin: 0 auto 4px;
- font-size: 10px; display: flex; align-items: center; justify-content: center; background: var(--surface); position: relative; z-index: 1;
- }
- .sp-node.done .sp-dot { border-color: var(--primary); background: var(--primary-bg); color: var(--primary-dark); }
- .sp-node.you .sp-dot { border-color: var(--primary); background: var(--primary); color: #fff; box-shadow: 0 0 0 3px rgba(16,185,129,0.2); }
- .sp-node.parallel .sp-dot { border-color: var(--warn); background: var(--warn-bg); color: var(--warn); }
- .sp-node.indep .sp-dot { border-style: dashed; }
- .sp-name { font-size: 10px; font-weight: 600; line-height: 1.2; }
- .sp-node.dim .sp-name, .sp-node.dim .sp-dot { opacity: 0.35; }
- .spine-side {
- margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border);
- display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-muted);
- }
- /* Detail drawer / bottom panel */
- .detail-panel { display: none; }
- .detail-panel.open { display: block; }
- .detail-grid { display: grid; grid-template-columns: 1fr 340px; gap: 14px; }
- .work-area { padding: 16px; }
- .work-area h3 { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
- .form-row { margin-bottom: 12px; }
- .form-row label { display: block; font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 4px; }
- .form-row input, .form-row select { width: 100%; max-width: 360px; height: 34px; border: 1px solid var(--border); border-radius: 8px; padding: 0 10px; }
- .context-card {
- padding: 14px; background: var(--table-head); border-left: 1px solid var(--border);
- }
- .context-card h4 { font-size: 12px; font-weight: 600; color: var(--text-muted); margin-bottom: 10px; }
- .ctx-line { font-size: 12px; margin-bottom: 6px; }
- .ctx-line span { color: var(--text-muted); }
- /* Full process view (coordinator only) */
- .full-process { display: none; }
- .full-process.show { display: block; }
- .focused-view.hidden { display: none; }
- .fork-row { display: flex; gap: 20px; justify-content: center; margin: 8px 0; }
- .flow-card {
- width: 120px; padding: 10px; border: 2px solid var(--border); border-radius: 8px; text-align: center; font-size: 12px;
- }
- .flow-card.active { border-color: var(--primary); background: var(--primary-bg); }
- .flow-card.done { border-color: var(--primary); opacity: 0.85; }
- .toast {
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(60px);
- background: var(--text); color: #fff; padding: 10px 18px; border-radius: 8px; font-size: 13px;
- opacity: 0; transition: all 0.3s; z-index: 99;
- }
- .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
- @media (max-width: 900px) {
- .detail-grid { grid-template-columns: 1fr; }
- .layout { flex-direction: column; }
- .sidebar { width: 100%; }
- }
- </style>
- </head>
- <body>
- <div class="layout">
- <aside class="sidebar" id="sidebar"></aside>
- <div class="main">
- <div class="role-bar">
- <span class="role-bar-label">模拟角色(切换看兼容效果)</span>
- <div class="role-tabs" id="roleTabs"></div>
- <div class="view-toggle" id="viewToggle" style="display:none">
- <button type="button" data-view="focused" class="active">聚焦作业</button>
- <button type="button" data-view="full">流程全景</button>
- </div>
- </div>
- <p class="role-desc" id="roleDesc" style="padding:8px 20px 0;font-size:12px;color:var(--text-muted)"></p>
- <header class="topbar">
- <h1 id="pageTitle">—</h1>
- <p id="pageSub">—</p>
- </header>
- <div class="content">
- <div class="concept-banner">
- <strong>兼容模式:</strong>同一套流程数据 · 角色默认只看<strong>本岗列表+操作区</strong> · 流程通过可折叠<strong>脊柱</strong>保持一体清晰 · 跟单可切换<strong>流程全景</strong>
- </div>
- <!-- 聚焦视图:各角色主路径 -->
- <div class="focused-view" id="focusedView">
- <div class="panel">
- <div class="panel-head">
- <h2 id="listTitle">待办列表</h2>
- <span id="listHint" style="font-size:12px;color:var(--text-muted)"></span>
- </div>
- <div class="stat-row" id="statRow"></div>
- <table>
- <thead id="tableHead"></thead>
- <tbody id="tableBody"></tbody>
- </table>
- </div>
- <!-- 选中行后:脊柱 + 作业区 -->
- <div class="detail-panel" id="detailPanel">
- <div id="detailSpine"></div>
- <div class="panel detail-grid">
- <div class="work-area">
- <h3 id="workTitle">—</h3>
- <div id="workForm"></div>
- <div style="margin-top:14px" id="workActions"></div>
- </div>
- <div class="context-card">
- <h4>流程上下文(只读,跨部门共享)</h4>
- <div id="contextLines"></div>
- </div>
- </div>
- </div>
- </div>
- <!-- 全景视图:仅跟单 -->
- <div class="full-process" id="fullProcess">
- <div class="panel" style="padding:16px">
- <h2 style="font-size:14px;margin-bottom:12px">业务办理 · 依赖全景(跟单/coordinator 专用)</h2>
- <div id="fullFlowDiagram"></div>
- <p style="font-size:12px;color:var(--text-muted);margin-top:12px">全景与职能列表是同一数据;实验员/财务日常使用「聚焦作业」即可。</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- <script>
- const ROLES = {
- finance: {
- label: '财务', menu: ['缴退费确认'], defaultMenu: 0,
- desc: '主路径:确认列表。流程脊柱默认折叠,只看金额与协议摘要。',
- pageTitle: '缴退费财务确认', pageSub: '仅看待确认单据 · 不关心实验主链操作',
- highlightNode: 'payment', dimNodes: ['qc', 'production'],
- spineDefaultCollapsed: true,
- canFullView: false,
- stats: [{ label: '待确认', count: 3 }, { label: '今日已确认', count: 8 }],
- columns: ['协议号', '客户', '金额', '类型', '实验进度', '操作'],
- list: [
- { id: 1, protocol: 'XY-2025-0066', customer: '张明华', amount: '28,800', type: '缴费确认', milestone: '已接收', nodes: mkNodes({ payment: 'you', reception: 'done', qc: 'parallel', production: 'parallel' }) },
- { id: 2, protocol: 'XY-2025-0077', customer: '李芳', amount: '18,000', type: '缴费确认', milestone: '待采集', nodes: mkNodes({ payment: 'you', collection: 'parallel' }) },
- { id: 3, protocol: 'XY-2025-0044', customer: '陈静', amount: '2,000', type: '退费确认', milestone: '质检中', nodes: mkNodes({ payment: 'you', qc: 'parallel', production: 'parallel' }) }
- ]
- },
- qc_recv: {
- label: '质检接收', menu: ['样本接收', '接收检查项'], defaultMenu: 0,
- desc: '主路径:接收列表。脊柱高亮「接收」,缴费为虚线独立支路。',
- pageTitle: '样本接收', pageSub: '待接收 / 已接收 · 提交后并行创建质检+生产工单',
- highlightNode: 'reception', dimNodes: ['payment'],
- spineDefaultCollapsed: false,
- canFullView: false,
- stats: [{ label: '待接收', count: 5 }, { label: '今日已接收', count: 12 }],
- columns: ['样本编码', '协议号', '客户', '预计到达', '里程碑', '操作'],
- list: [
- { id: 1, sample: 'ZK202506120001', protocol: 'XY-2025-0066', customer: '张明华', eta: '今日 14:00', milestone: '待接收', nodes: mkNodes({ reception: 'you', payment: 'indep' }) },
- { id: 2, sample: 'ZK202506110008', protocol: 'XY-2025-0077', customer: '李芳', eta: '明日 10:00', milestone: '待接收', nodes: mkNodes({ reception: 'you', collection: 'done', payment: 'indep' }) }
- ]
- },
- qc_lab: {
- label: '质检实验', menu: ['我的质检任务', '质检工单'], defaultMenu: 0,
- desc: '主路径:我的任务。脊柱高亮「质检」,可看生产并行进度(只读)。',
- pageTitle: '我的质检任务', pageSub: '待执行 / 进行中 · 与生产工单并行',
- highlightNode: 'qc', dimNodes: ['payment'],
- spineDefaultCollapsed: false,
- canFullView: false,
- stats: [{ label: '待执行', count: 4 }, { label: '进行中', count: 2 }],
- columns: ['任务号', '样本编码', '检测项目', '工单状态', '生产(只读)', '操作'],
- list: [
- { id: 1, task: 'RW20260612001', sample: 'ZK202506120001', item: '细胞活性', wo: '配置中', prod: '待配置', nodes: mkNodes({ qc: 'you', production: 'parallel', reception: 'done' }) },
- { id: 2, task: 'RW20260612002', sample: 'XY202506100003', item: '无菌检测', wo: '进行中', prod: '进行中', nodes: mkNodes({ qc: 'you', production: 'parallel' }) }
- ]
- },
- production: {
- label: '生产', menu: ['生产工单', '生产任务'], defaultMenu: 0,
- desc: '主路径:生产工单。脊柱高亮「生产」,质检进度只读。',
- pageTitle: '入库生产工单', pageSub: '待配置 / 进行中 · 与质检并行',
- highlightNode: 'production', dimNodes: ['payment'],
- spineDefaultCollapsed: false,
- canFullView: false,
- stats: [{ label: '待配置', count: 2 }, { label: '进行中', count: 3 }],
- columns: ['工单号', '样本编码', '协议号', '状态', '质检(只读)', '操作'],
- list: [
- { id: 1, wo: 'SC202606110003', sample: 'ZK202506120001', protocol: 'XY-2025-0066', status: '待配置', qc: '配置中', nodes: mkNodes({ production: 'you', qc: 'parallel', reception: 'done' }) }
- ]
- },
- coordinator: {
- label: '市场跟单', menu: ['协议列表', '业务办理'], defaultMenu: 1,
- desc: '可选聚焦列表或流程全景。日常跟单用列表+脊柱;卡点时切换全景。',
- pageTitle: '细胞存储 · 业务办理', pageSub: '进行中协议 · 可切换「聚焦 / 全景」',
- highlightNode: null, dimNodes: [],
- spineDefaultCollapsed: false,
- canFullView: true,
- stats: [{ label: '进行中', count: 23 }, { label: '卡点', count: 4 }],
- columns: ['协议号', '客户', '样本', '当前里程碑', '并行状态', '操作'],
- list: [
- { id: 1, protocol: 'XY-2025-0066', customer: '张明华', sample: 'ZK…001', milestone: '接收完成', parallel: '质检+生产', nodes: mkNodes({ qc: 'parallel', production: 'parallel', reception: 'done', payment: 'indep' }) },
- { id: 2, protocol: 'XY-2025-0088', customer: '王强', sample: 'XY…003', milestone: '质检完成', parallel: '生产进行中', nodes: mkNodes({ qc: 'done', production: 'you', reception: 'done' }) }
- ]
- }
- };
- const SPINE_STEPS = [
- { id: 'protocol', name: '协议' },
- { id: 'collection', name: '采集' },
- { id: 'reception', name: '接收' },
- { id: 'qc', name: '质检' },
- { id: 'production', name: '生产' },
- { id: 'release', name: '放行' },
- { id: 'storage', name: '入库' }
- ];
- const SPINE_INDEP = { id: 'payment', name: '缴费' };
- function mkNodes(overrides) {
- const base = { protocol: 'done', collection: 'done', reception: 'done', qc: 'pending', production: 'pending', release: 'pending', storage: 'pending', payment: 'pending' };
- return { ...base, ...overrides };
- }
- let currentRole = 'finance';
- let currentView = 'focused';
- let selectedRow = null;
- let spineCollapsed = true;
- function renderRoleTabs() {
- document.getElementById('roleTabs').innerHTML = Object.entries(ROLES).map(([k, r]) =>
- `<button type="button" class="role-tab ${k === currentRole ? 'active' : ''}" onclick="setRole('${k}')">${r.label}</button>`
- ).join('');
- }
- function renderSidebar() {
- const r = ROLES[currentRole];
- document.getElementById('sidebar').innerHTML = `
- <div class="logo">中科兴皖<span> · 管理端</span></div>
- <div class="nav-sub">当前角色菜单(聚焦)</div>
- ${r.menu.map((m, i) => `<a href="#" class="nav-item ${i === r.defaultMenu ? 'active' : ''}">${m}</a>`).join('')}
- ${r.canFullView ? '<a href="#" class="nav-item" onclick="setView(\'full\');return false">业务办理全景</a>' : ''}
- `;
- }
- function setRole(role) {
- currentRole = role;
- selectedRow = null;
- spineCollapsed = ROLES[role].spineDefaultCollapsed;
- currentView = 'focused';
- renderAll();
- }
- function setView(v) {
- currentView = v;
- document.querySelectorAll('.view-toggle button').forEach(b => b.classList.toggle('active', b.dataset.view === v));
- document.getElementById('focusedView').classList.toggle('hidden', v === 'full');
- document.getElementById('fullProcess').classList.toggle('show', v === 'full');
- }
- function renderSpine(row, roleKey, opts = {}) {
- const r = ROLES[roleKey];
- const nodes = row.nodes || mkNodes({});
- const collapsed = opts.forceExpand ? false : spineCollapsed;
- const hl = r.highlightNode;
- const mainSteps = SPINE_STEPS.map(s => {
- const st = nodes[s.id] || 'pending';
- let cls = st === 'done' ? 'done' : st === 'you' ? 'you' : st === 'parallel' ? 'parallel' : '';
- if (r.dimNodes && r.dimNodes.includes(s.id)) cls += ' dim';
- if (hl === s.id && st !== 'you') cls = 'you';
- const icon = st === 'done' ? '✓' : st === 'you' ? '●' : st === 'parallel' ? '…' : '○';
- return `<div class="sp-node ${cls}"><div class="sp-dot">${icon}</div><div class="sp-name">${s.name}</div></div>`;
- }).join('');
- const paySt = nodes.payment || 'pending';
- const payCls = paySt === 'indep' ? 'indep' : paySt === 'you' ? 'you' : paySt === 'done' ? 'done' : '';
- const indep = `<div class="sp-node ${payCls}"><div class="sp-dot">${paySt === 'done' ? '✓' : paySt === 'you' ? '●' : '○'}</div><div class="sp-name">缴费</div></div>`;
- const title = row.protocol || row.sample || '—';
- const sub = row.customer ? `${row.customer} · ${row.milestone || row.type || ''}` : '';
- return `
- <div class="spine-wrap">
- <div class="spine-header" onclick="toggleSpine()">
- <div>
- <h3>流程脊柱 · ${title}</h3>
- <div class="meta">${sub} ${hl ? '· 高亮你的环节' : '· 跟单视图'}</div>
- </div>
- <span class="spine-toggle">${collapsed ? '展开流程 ▼' : '收起 ▲'}</span>
- </div>
- <div class="spine-body ${collapsed ? 'collapsed' : ''}">
- <div class="spine-compact">${mainSteps}</div>
- <div class="spine-side"><span>独立支路:</span>${indep}<span>— 不阻塞实验主链</span></div>
- </div>
- </div>`;
- }
- function toggleSpine() {
- spineCollapsed = !spineCollapsed;
- if (selectedRow) document.getElementById('detailSpine').innerHTML = renderSpine(selectedRow, currentRole);
- }
- function renderList(roleKey) {
- const r = ROLES[roleKey];
- document.getElementById('pageTitle').textContent = r.pageTitle;
- document.getElementById('pageSub').textContent = r.pageSub;
- document.getElementById('roleDesc').textContent = r.desc;
- document.getElementById('listTitle').textContent = r.pageTitle;
- document.getElementById('listHint').textContent = r.canFullView ? '可右上角切换流程全景' : '本岗聚焦列表';
- document.getElementById('viewToggle').style.display = r.canFullView ? 'flex' : 'none';
- document.getElementById('statRow').innerHTML = r.stats.map(s =>
- `<div class="stat-pill">${s.label}<em>${s.count}</em></div>`
- ).join('');
- document.getElementById('tableHead').innerHTML = `<tr>${r.columns.map(c => `<th>${c}</th>`).join('')}</tr>`;
- document.getElementById('tableBody').innerHTML = r.list.map((row, i) => {
- let cells = '';
- if (roleKey === 'finance') {
- cells = `<td>${row.protocol}</td><td>${row.customer}</td><td>${row.amount}</td><td>${row.type}</td><td><span class="tag info">${row.milestone}</span></td><td><button class="btn btn-primary btn-sm" onclick="event.stopPropagation();selectRow(${i})">确认</button></td>`;
- } else if (roleKey === 'qc_recv') {
- cells = `<td>${row.sample}</td><td>${row.protocol}</td><td>${row.customer}</td><td>${row.eta}</td><td><span class="tag warning">${row.milestone}</span></td><td><button class="btn btn-primary" onclick="event.stopPropagation();selectRow(${i})">接收</button></td>`;
- } else if (roleKey === 'qc_lab') {
- cells = `<td>${row.task}</td><td>${row.sample}</td><td>${row.item}</td><td>${row.wo}</td><td><span class="tag muted">${row.prod}</span></td><td><button class="btn btn-primary" onclick="event.stopPropagation();selectRow(${i})">执行</button></td>`;
- } else if (roleKey === 'production') {
- cells = `<td>${row.wo}</td><td>${row.sample}</td><td>${row.protocol}</td><td>${row.status}</td><td><span class="tag muted">${row.qc}</span></td><td><button class="btn btn-primary" onclick="event.stopPropagation();selectRow(${i})">配置</button></td>`;
- } else {
- cells = `<td>${row.protocol}</td><td>${row.customer}</td><td>${row.sample}</td><td>${row.milestone}</td><td>${row.parallel}</td><td><button class="btn" onclick="event.stopPropagation();selectRow(${i})">跟单</button></td>`;
- }
- return `<tr class="${selectedRow === row ? 'selected' : ''}" onclick="selectRow(${i})">${cells}</tr>`;
- }).join('');
- }
- function selectRow(index) {
- const r = ROLES[currentRole];
- selectedRow = r.list[index];
- document.getElementById('detailPanel').classList.add('open');
- document.getElementById('detailSpine').innerHTML = renderSpine(selectedRow, currentRole);
- const forms = {
- finance: `
- <div class="form-row"><label>到账金额</label><input value="${selectedRow.amount}" /></div>
- <div class="form-row"><label>到账日期</label><input type="date" value="2026-06-12" /></div>
- <div class="form-row"><label>财务备注</label><input placeholder="可选" /></div>`,
- qc_recv: `
- <div class="form-row"><label>接收时间</label><input type="datetime-local" /></div>
- <div class="form-row"><label>接收温度 ℃</label><input value="4.2" /></div>
- <div class="form-row"><label>接收结论</label><select><option>接收</option><option>拒收</option></select></div>`,
- qc_lab: `
- <div class="form-row"><label>检测项目</label><input value="${selectedRow.item}" readonly /></div>
- <div class="form-row"><label>采集值 / 结果</label><input placeholder="实验录入" /></div>`,
- production: `
- <div class="form-row"><label>工艺路线</label><select><option>入库标准工艺 v2</option></select></div>
- <div class="form-row"><label>生产任务</label><input placeholder="创建任务…" /></div>`,
- coordinator: `
- <div class="form-row"><label>跟单备注</label><input placeholder="记录协调事项" /></div>
- <p style="font-size:12px;color:var(--text-muted)">跟单在此查看脊柱;需要时可切「流程全景」。</p>`
- };
- const titles = { finance: '财务确认', qc_recv: '样本接收登记', qc_lab: '质检任务执行', production: '生产工单配置', coordinator: '跟单备注' };
- document.getElementById('workTitle').textContent = titles[currentRole];
- document.getElementById('workForm').innerHTML = forms[currentRole];
- document.getElementById('workActions').innerHTML = `<button class="btn btn-primary" onclick="toast('已提交 · 流程状态已同步更新')">提交</button><button class="btn" onclick="closeDetail()">关闭</button>`;
- document.getElementById('contextLines').innerHTML = `
- <div class="ctx-line"><span>协议:</span>${selectedRow.protocol || '—'}</div>
- <div class="ctx-line"><span>客户:</span>${selectedRow.customer || '—'}</div>
- <div class="ctx-line"><span>样本:</span>${selectedRow.sample || '—'}</div>
- <div class="ctx-line"><span>实验里程碑:</span>${selectedRow.milestone || selectedRow.type || '—'}</div>
- <div class="ctx-line" style="margin-top:10px;font-size:11px;color:var(--text-muted)">↑ 其他部门可见同一上下文,无需进同一操作页</div>`;
- renderList(currentRole);
- }
- function closeDetail() {
- selectedRow = null;
- document.getElementById('detailPanel').classList.remove('open');
- renderList(currentRole);
- }
- function renderFullProcess() {
- document.getElementById('fullFlowDiagram').innerHTML = `
- <div style="font-size:12px;color:var(--text-muted);margin-bottom:10px">协议 → 采集 → 接收 → (质检 ∥ 生产) → 放行 → 入库 · 缴费独立</div>
- <div class="fork-row">
- <div class="flow-card done">协议✓</div><span>→</span>
- <div class="flow-card done">采集✓</div><span>→</span>
- <div class="flow-card done">接收✓</div><span>→</span>
- <div class="flow-card active">质检…</div>
- <div class="flow-card active">生产…</div><span>→</span>
- <div class="flow-card">放行</div><span>→</span>
- <div class="flow-card">入库</div>
- </div>
- <div style="margin-top:12px;padding:10px;border:1px dashed var(--border);border-radius:8px;font-size:12px">缴费 ○ 独立支路(虚线关联,不阻塞主链)</div>`;
- }
- function renderAll() {
- renderRoleTabs();
- renderSidebar();
- renderList(currentRole);
- setView(currentView);
- renderFullProcess();
- if (selectedRow) document.getElementById('detailSpine').innerHTML = renderSpine(selectedRow, currentRole);
- }
- document.getElementById('viewToggle').addEventListener('click', e => {
- const btn = e.target.closest('button[data-view]');
- if (btn) setView(btn.dataset.view);
- });
- function toast(msg) {
- const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show');
- setTimeout(() => t.classList.remove('show'), 2500);
- }
- renderAll();
- </script>
- </body>
- </html>
|