<!-- START OF FILE 捐献案例管理_V2.txt -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>江苏省OPO可视化调度指挥平台 - 捐献案例管理 V2</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* ==================== 1. 全局定义 ==================== */
:root {
--primary-blue: #007aff;
--tech-cyan: #00f0ff;
--tech-bg: #050b16;
--card-bg: rgba(16, 36, 68, 0.95);
--border-color: rgba(64, 116, 180, 0.4);
--text-main: #ffffff;
--text-sub: #8fb4d9;
--label-color: #6a8bad;
--value-color: #ffffff;
/* 状态色 - 与线索管理保持色系一致 */
--status-pending: #ff9f0a; /* 待审核/维护中(橙) */
--status-approved: #30d158; /* 已完成(绿) */
--status-rejected: #ff453a; /* 终止(红) */
--status-transport: #00f0ff; /* 转运中高亮 */
--status-realloc: #ff453a; /* 再分配 */
--panel-width: 480px;
}
body {
margin: 0; padding: 0;
background-color: var(--tech-bg);
font-family: "Microsoft YaHei", sans-serif;
color: var(--text-main);
height: 100vh; width: 100vw;
overflow: hidden;
display: flex;
justify-content: space-between;
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: rgba(0, 240, 255, 0.3); border-radius: 2px; }
::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); }
/* ==================== 2. 地图层 ==================== */
.map-viewport {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-image: radial-gradient(circle at 50% 50%, #112240 0%, #02050a 100%),
url('https://api.mapbox.com/styles/v1/mapbox/dark-v10/static/118.78,32.07,7,0/1600x1200?access_token=pk.eyJ1IjoiZGVtb3VzZXIiLCJhIjoiY2x4eH..."');
background-size: cover; background-position: center;
overflow: hidden; z-index: 0;
}
.map-layer { width: 100%; height: 100%; position: absolute; top: 0; left: 0; transition: transform 1s cubic-bezier(0.19, 1, 0.22, 1); }
/* 点位样式 */
.map-point {
position: absolute; transform: translate(-50%, -100%);
display: flex; flex-direction: column; align-items: center;
z-index: 10; cursor: pointer; transition: 0.3s;
}
.map-point.active { z-index: 20; transform: translate(-50%, -100%) scale(1.1); }
.point-hud { margin-bottom: 8px; transition: 0.3s; }
.hud-card {
background: rgba(8, 20, 40, 0.9); border: 1px solid var(--tech-cyan); border-radius: 4px;
padding: 6px 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.6); backdrop-filter: blur(4px); min-width: 140px;
}
.map-point.active .hud-card { background: rgba(0, 50, 100, 0.95); border-color: #fff; }
.hud-title { font-size: 13px; font-weight: bold; color: #fff; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 4px; margin-bottom: 4px; }
.hud-grid { display: flex; justify-content: space-around; text-align: center; }
.hud-item { display: flex; flex-direction: column; }
.hud-label { font-size: 10px; color: #aaa; margin-bottom: 2px;}
.hud-num { font-family: Impact, sans-serif; font-size: 16px; color: #888; line-height: 1.2; }
.hud-num.success { color: var(--status-approved); }
.point-icon {
width: 36px; height: 36px; background: #050b16; border: 2px solid var(--tech-cyan);
border-radius: 50%; display: flex; justify-content: center; align-items: center;
color: var(--tech-cyan); font-size: 16px; position: relative; z-index: 2;
box-shadow: 0 0 10px var(--tech-cyan); transition: 0.3s;
}
.map-point.active .point-icon { background: var(--tech-cyan); color: #000; box-shadow: 0 0 25px var(--tech-cyan); }
/* ==================== 3. 面板容器通用 ==================== */
.panel-container {
width: var(--panel-width); height: 95vh;
background: rgba(8, 26, 54, 0.95);
border: 1px solid var(--border-color);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.6);
display: flex; flex-direction: column;
backdrop-filter: blur(12px);
z-index: 50;
margin-top: 2.5vh;
transition: transform 0.4s ease, opacity 0.4s ease;
}
.panel-header { height: 44px; background: linear-gradient(90deg, rgba(0,76,163,0.6) 0%, rgba(0,0,0,0) 100%); display: flex; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
.header-icon { width: 4px; height: 18px; background: var(--tech-cyan); margin-right: 10px; box-shadow: 0 0 8px var(--tech-cyan); }
.panel-title { font-size: 18px; font-weight: bold; letter-spacing: 1px; flex: 1; color: #fff; }
.list-content { flex: 1; overflow-y: auto; padding: 15px; }
/* ==================== 4. 左侧详情面板 ==================== */
.left-panel {
position: absolute; left: 20px;
transform: translateX(-120%); opacity: 0;
}
.left-panel.show { transform: translateX(0); opacity: 1; }
.lp-section-title {
font-size: 14px; font-weight: bold; color: var(--tech-cyan);
border-left: 3px solid var(--tech-cyan); padding-left: 8px;
margin: 15px 0 10px 0; background: linear-gradient(90deg, rgba(0,240,255,0.1) 0%, transparent 100%);
padding-top: 2px; padding-bottom: 2px;
}
.lp-section-title:first-child { margin-top: 0; }
.lp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; font-size: 12px; margin-bottom: 10px; }
.lp-field { display: flex; flex-direction: column; }
.lp-label { color: var(--label-color); margin-bottom: 3px; font-size: 11px; }
.lp-val { color: var(--value-color); border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px; min-height: 20px; }
/* V2 时间轴样式 */
.timeline-container { padding-left: 10px; margin-top: 10px; border-left: 2px solid rgba(255,255,255,0.1); }
.tl-item { position: relative; padding-left: 20px; margin-bottom: 15px; }
.tl-dot {
position: absolute; left: -6px; top: 2px; width: 10px; height: 10px; border-radius: 50%;
background: #444; border: 2px solid var(--tech-bg); transition: 0.3s;
}
/* 已审核 (绿色) */
.tl-item.audited .tl-dot { background: var(--status-approved); box-shadow: 0 0 5px var(--status-approved); }
/* 待审核 (橙色) */
.tl-item.pending_audit .tl-dot { background: var(--status-pending); box-shadow: 0 0 5px var(--status-pending); }
/* 登记中 (蓝色脉冲) */
.tl-item.registering .tl-dot { background: var(--primary-blue); animation: pulse-blue 1.5s infinite; }
.tl-content { display: flex; flex-direction: column; }
.tl-title { font-size: 13px; color: #fff; font-weight: 500; display: flex; align-items: center; justify-content: space-between; }
.tl-item.registering .tl-title { color: var(--tech-cyan); font-weight: bold; }
.tl-meta { font-size: 11px; color: #888; margin-top: 2px; display: flex; gap: 8px; }
.tl-tag { font-size: 9px; padding: 0px 4px; border-radius: 2px; margin-left: 5px; border: 1px solid; }
.tag-pending { color: var(--status-pending); border-color: var(--status-pending); }
@keyframes pulse-blue { 0% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(0, 122, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0); } }
/* 器官追踪卡片 */
.organ-card {
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
border-radius: 4px; padding: 10px; margin-bottom: 8px; font-size: 12px;
display: flex; flex-direction: column; gap: 6px; position: relative; overflow: hidden;
}
.organ-card.transit { border-left: 3px solid var(--status-transport); }
.organ-card.realloc { border-left: 3px solid var(--status-realloc); background: rgba(255, 69, 58, 0.05); }
.organ-card.completed { border-left: 3px solid #888; }
.oc-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed rgba(255,255,255,0.1); padding-bottom: 4px;}
.oc-type { font-weight: bold; font-size: 13px; color: #fff; }
.oc-id { color: var(--tech-cyan); font-family: monospace; }
.oc-row { display: flex; justify-content: space-between; color: #ccc; }
.oc-status-badge { font-size: 10px; padding: 2px 6px; border-radius: 2px; background: rgba(0,0,0,0.3); }
.st-bg-transit { background: rgba(0, 240, 255, 0.2); color: var(--status-transport); }
.st-bg-completed { background: rgba(142, 142, 147, 0.2); color: #ccc; }
.st-bg-realloc { background: rgba(255, 69, 58, 0.2); color: var(--status-realloc); }
/* 质控总结 */
.qc-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.qc-item { background: rgba(0,0,0,0.2); padding: 8px; border-radius: 4px; display: flex; flex-direction: column; align-items: center; text-align: center; }
.qc-lbl { font-size: 11px; color: #888; }
.qc-val { font-size: 14px; font-weight: bold; margin-top: 2px; }
.text-green { color: var(--status-approved); }
.text-red { color: var(--status-rejected); }
.text-white { color: #fff; }
/* ==================== 5. 右侧列表面板 ==================== */
.right-panel { position: absolute; right: 20px; }
/* KPI - V2颜色对齐线索管理 (White, Green, Orange, Red) */
.kpi-section { display: grid; grid-template-columns: repeat(4, 1fr); padding: 10px; gap: 5px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.kpi-item { background: rgba(255,255,255,0.05); border-radius: 4px; padding: 6px 0; text-align: center; cursor: pointer; transition: 0.2s; }
.kpi-item:hover, .kpi-item.active { background: rgba(0,122,255,0.2); border: 1px solid var(--primary-blue); }
.kpi-num { font-size: 18px; font-weight: bold; display: block; }
.kpi-lbl { font-size: 10px; color: var(--text-sub); }
/* 筛选区 */
.filter-time-row, .filter-area-row { padding: 8px 12px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; gap: 6px; flex-shrink: 0; background: rgba(0,0,0,0.2);}
.date-display { background: rgba(0,0,0,0.3); border: 1px solid rgba(143, 180, 217, 0.3); border-radius: 3px; padding: 4px 8px; font-size: 12px; color: #fff; flex: 1; display: flex; align-items: center; justify-content: space-between;}
.time-controls { display: flex; background: rgba(255,255,255,0.05); border-radius: 3px; padding: 2px; }
.time-btn { border: none; background: none; color: var(--text-sub); font-size: 11px; padding: 4px 8px; cursor: pointer; border-radius: 2px; transition: all 0.2s; }
.time-btn.active { background: var(--primary-blue); color: white; font-weight: bold; }
.filter-select { background: rgba(0,0,0,0.4); border: 1px solid rgba(64,116,180,0.5); color: #fff; padding: 3px 6px; border-radius: 3px; font-size: 12px; flex: 1; outline: none; cursor: pointer; }
.filter-select option { background: #050b16; color: #fff; }
.reset-btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #aaa; width: 24px; height: 24px; border-radius: 3px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; }
.reset-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
.reset-btn.active { color: var(--tech-cyan); border-color: var(--tech-cyan); background: rgba(0,240,255,0.1); }
/* 列表卡片 */
.list-card {
background: var(--card-bg);
border: 1px solid rgba(0, 240, 255, 0.1);
border-left: 3px solid #888;
border-radius: 4px; margin-bottom: 10px; padding: 10px;
cursor: pointer; transition: 0.2s; position: relative;
}
.list-card:hover { background: rgba(20, 45, 85, 0.9); transform: translateX(-4px); }
.list-card.active-select { border: 1px solid var(--tech-cyan); background: rgba(20, 45, 85, 1); box-shadow: inset 0 0 10px rgba(0,240,255,0.2); }
/* V2 List Status Colors */
.list-card.pending { border-left-color: var(--status-pending); }
.list-card.maintaining { border-left-color: var(--status-pending); } /* 维护中也是橙色系 */
.list-card.completed { border-left-color: var(--status-approved); }
.list-card.terminated { border-left-color: var(--status-rejected); }
.lc-top { display: flex; justify-content: space-between; margin-bottom: 6px; align-items: center; }
.lc-id { font-size: 14px; font-weight: bold; color: var(--tech-cyan); }
.lc-status { font-size: 10px; padding: 1px 6px; border-radius: 2px; border: 1px solid; }
.st-pending { color: var(--status-pending); border-color: var(--status-pending); background: rgba(255, 159, 10, 0.1); }
.st-maintaining { color: var(--primary-blue); border-color: var(--primary-blue); background: rgba(0, 122, 255, 0.1); }
.st-completed { color: var(--status-approved); border-color: var(--status-approved); background: rgba(48, 209, 88, 0.1); }
.st-terminated { color: var(--status-rejected); border-color: var(--status-rejected); background: rgba(255, 69, 58, 0.1); }
.lc-hospital { font-size: 13px; color: #fff; margin-bottom: 8px; font-weight: 500; display: flex; justify-content: space-between; align-items: center; }
.trans-tag { font-size: 10px; background: rgba(0, 240, 255, 0.2); color: var(--status-transport); padding: 1px 5px; border-radius: 2px; display: flex; align-items: center; gap: 4px; border: 1px solid rgba(0, 240, 255, 0.4); animation: flash-blue 2s infinite; }
@keyframes flash-blue { 0% { opacity: 0.7; } 50% { opacity: 1; box-shadow: 0 0 5px var(--status-transport); } 100% { opacity: 0.7; } }
.lc-footer { border-top: 1px dashed rgba(255,255,255,0.1); padding-top: 8px; display: flex; align-items: center; font-size: 11px; color: #aaa; gap: 8px; }
.progress-track { flex: 1; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
.progress-bar { height: 100%; background: var(--tech-cyan); transition: width 0.5s; }
</style>
</head>
<body>
<!-- 地图底座 -->
<div class="map-viewport" id="mapContainer">
<div class="map-layer" id="mapLayer"></div>
</div>
<!-- 左侧详情面板 -->
<div class="panel-container left-panel" id="leftPanel">
<div class="panel-header">
<div class="header-icon"></div>
<div class="panel-title">捐献案例详情</div> <!-- V2 修改标题 -->
</div>
<div class="list-content" id="detailContent">
<!-- JS 渲染 -->
</div>
</div>
<!-- 右侧列表面板 -->
<div class="panel-container right-panel">
<div class="panel-header">
<div class="header-icon"></div>
<div class="panel-title">捐献案例管理</div>
</div>
<!-- KPI (V2: 颜色对齐线索管理 - 白/绿/橙/红) -->
<div class="kpi-section">
<div class="kpi-item active"><span class="kpi-num text-white">45</span><span class="kpi-lbl">案例总数</span></div>
<div class="kpi-item"><span class="kpi-num text-green-400">22</span><span class="kpi-lbl">已完成</span></div>
<div class="kpi-item"><span class="kpi-num text-orange-400">18</span><span class="kpi-lbl">维护中</span></div>
<div class="kpi-item"><span class="kpi-num text-red-400">5</span><span class="kpi-lbl">终止维护</span></div>
</div>
<!-- 筛选 1:时间 -->
<div class="filter-time-row">
<div class="date-display">
<span><i class="far fa-calendar-alt mr-2 text-cyan-400"></i>2023-11-20 ~ 2023-11-26</span>
</div>
<div class="time-controls">
<button class="time-btn active">本周</button>
<button class="time-btn">本月</button>
<button class="time-btn">本年</button>
</div>
</div>
<!-- 筛选 2:区域与医院 (V2: 默认为空) -->
<div class="filter-area-row">
<select class="filter-select" id="selCity">
<option>选择地市</option><option>南京市</option><option>苏州市</option><option>无锡市</option>
</select>
<select class="filter-select" id="selDistrict">
<option>选择区域</option><option>秦淮区</option><option>鼓楼区</option><option>玄武区</option>
</select>
<select class="filter-select" id="selHospital" onchange="filterByHospital(this.value)">
<option value="">选择医院</option>
<option value="H001">南京市第一医院</option>
<option value="H002">南京市鼓楼医院</option>
<option value="H003">江苏省人民医院</option>
<option value="H004">无锡市人民医院</option>
</select>
<button class="reset-btn" id="btnReset" onclick="resetFilter()" title="重置筛选">
<i class="fas fa-undo"></i>
</button>
</div>
<!-- 列表 -->
<div class="list-content" id="rightListContent">
<!-- JS 渲染列表 -->
</div>
</div>
<script>
// ==================== 数据源 (V2: 模拟4种状态) ====================
// V2 节点逻辑: Merged Step 1&2 -> "潜在捐献上报", Removed "捐献完成审核"
const caseDatabase = [
{
// 状态 1: 维护中 (Maintaining)
id: 'JSNJ2023088', name: '张**', status: 'maintaining', hospitalId: 'H001', hospitalName: '南京市第一医院',
basicInfo: { dept: 'ICU', sex: '男', age: '57', inpatientNo: 'A3', blood: 'A型', idCard: '3201**********1234', diag: '脑干出血' },
progress: 4,
transporting: true,
// V2 Timeline logic: node status [audited, pending_audit, registering, not_started]
timeline: [
{ step: 1, time: '2023-11-26 14:20', user: '凌晓红', status: 'audited' }, // 潜在捐献上报
{ step: 2, time: '2023-11-26 16:30', user: '凌晓红', status: 'audited' }, // 医学评估
{ step: 3, time: '2023-11-26 17:00', user: '凌晓红', status: 'audited' }, // 社会学评估
{ step: 4, time: null, user: null, status: 'registering' }, // 捐献确认登记 (登记中)
// ... 后续未开始
],
organs: [
{ type: '肝脏', id: 'ORG-L-001', to: '江苏省人民医院', recipient: '李**(3201...11)', status: 'transit' },
{ type: '左肾', id: 'ORG-K-001', to: '苏州附一院', recipient: '王**(3205...99)', status: 'realloc' },
{ type: '左肾', id: 'ORG-K-001', to: '无锡人民医院', recipient: '赵**(3202...77)', status: 'transit' }
],
qc: { auditTime: '40分钟', regTime: '-', completeness: '44%', result: '进行中' },
hospitalData: { x: 45, y: 40, success: 12, total: 45 }
},
{
// 状态 2: 已完成 (Completed)
id: 'JSNJ2023085', name: '李**', status: 'completed', hospitalId: 'H002', hospitalName: '南京市鼓楼医院',
basicInfo: { dept: '神经外科', sex: '女', age: '32', inpatientNo: 'N09', blood: 'O型', idCard: '3201**********5566', diag: '重度颅脑损伤' },
progress: 9, // V2 Total steps is 9
transporting: false,
timeline: [
{ step: 1, time: '2023-11-24 09:00', user: '陈明明', status: 'audited' },
{ step: 2, time: '2023-11-24 11:30', user: '陈明明', status: 'audited' },
{ step: 3, time: '2023-11-24 14:30', user: '陈明明', status: 'audited' },
{ step: 4, time: '2023-11-24 16:30', user: '陈明明', status: 'audited' },
{ step: 5, time: '2023-11-24 18:30', user: '陈明明', status: 'audited' },
{ step: 6, time: '2023-11-25 08:30', user: '陈明明', status: 'audited' },
{ step: 7, time: '2023-11-25 09:30', user: '陈明明', status: 'audited' },
{ step: 8, time: '2023-11-25 12:30', user: '陈明明', status: 'audited' },
{ step: 9, time: '2023-11-26 10:00', user: '陈明明', status: 'audited' }
],
organs: [
{ type: '心脏', id: 'ORG-H-002', to: '南京市鼓楼医院', recipient: '孙**(3201...22)', status: 'completed' }
],
qc: { auditTime: '1小时30分', regTime: '12小时', completeness: '100%', result: '移植成功' },
hospitalData: { x: 50, y: 60, success: 40, total: 156 }
},
{
// 状态 3: 待审核 (Pending Audit) - 指某个节点登记完了在等审核
id: 'JSCZ00041', name: '陈**', status: 'pending', hospitalId: 'H003', hospitalName: '江苏省人民医院',
basicInfo: { dept: '急诊科', sex: '男', age: '44', inpatientNo: 'B22', blood: 'B型', idCard: '3203**********2233', diag: '车祸外伤' },
progress: 1,
transporting: false,
timeline: [
{ step: 1, time: '2023-11-27 10:00', user: '张护士', status: 'pending_audit' } // 待审核
],
organs: [],
qc: { auditTime: '5分钟', regTime: '-', completeness: '11%', result: '等待中' },
hospitalData: { x: 55, y: 55, success: 20, total: 100 }
},
{
// 状态 4: 终止维护 (Terminated)
id: 'JSCZ00035', name: '王*', status: 'terminated', hospitalId: 'H004', hospitalName: '无锡市人民医院',
basicInfo: { dept: '肿瘤科', sex: '男', age: '65', inpatientNo: 'C01', blood: 'AB型', idCard: '3202**********8877', diag: '恶性肿瘤' },
progress: 2,
transporting: false,
timeline: [
{ step: 1, time: '2023-11-20 14:20', user: '周云', status: 'audited' },
{ step: 2, time: '2023-11-20 15:30', user: '周云', status: 'audited' } // 终止于此
],
organs: [],
qc: { auditTime: '10分钟', regTime: '-', completeness: '22%', result: '医学禁忌终止' },
hospitalData: { x: 65, y: 75, success: 8, total: 89 }
}
];
// V2 步骤名称 (Merged 1&2, Removed 11 -> 9 Steps)
const STEPS_NEW = [
"潜在捐献上报", "医学评估资料登记", "社会学评估资料登记",
"捐献确认登记", "伦理审查登记", "分配管理登记", "捐献见证登记",
"捐献完成登记", "后续材料补充"
];
// ==================== 初始化 ====================
const mapLayer = document.getElementById('mapLayer');
const rightList = document.getElementById('rightListContent');
const leftPanel = document.getElementById('leftPanel');
const detailContent = document.getElementById('detailContent');
const selHospital = document.getElementById('selHospital');
const btnReset = document.getElementById('btnReset');
// 1. 渲染地图
const hospitalMap = {};
caseDatabase.forEach(item => {
if(!hospitalMap[item.hospitalId]) {
hospitalMap[item.hospitalId] = item.hospitalData;
hospitalMap[item.hospitalId].name = item.hospitalName;
hospitalMap[item.hospitalId].id = item.hospitalId;
}
});
function initMap() {
for (let hId in hospitalMap) {
const h = hospitalMap[hId];
const el = document.createElement('div');
el.className = `map-point`;
el.style.left = h.x + '%';
el.style.top = h.y + '%';
el.id = `pt-${hId}`;
el.onclick = (e) => {
e.stopPropagation();
selHospital.value = hId;
filterByHospital(hId);
};
const html = `
<div class="point-hud">
<div class="hud-card">
<div class="hud-title">${h.name}</div>
<div class="hud-grid">
<div class="hud-item">
<span class="hud-label">成功捐献</span>
<span class="hud-num success">${h.success}</span>
</div>
<div class="hud-item">
<span class="hud-label">案例总数</span>
<span class="hud-num">${h.total}</span>
</div>
</div>
</div>
</div>
<div class="point-icon"><i class="fas fa-hospital"></i></div>
`;
el.innerHTML = html;
mapLayer.appendChild(el);
}
}
// 2. 渲染右侧列表
function renderList(data = caseDatabase) {
if(data.length === 0) {
rightList.innerHTML = `<div class="text-center text-gray-500 mt-10 text-sm">暂无案例数据</div>`;
return;
}
rightList.innerHTML = data.map(item => {
let stClass = '', stText = '';
if(item.status==='pending') { stClass='st-pending'; stText='待审核'; }
if(item.status==='maintaining') { stClass='st-maintaining'; stText='维护中'; }
if(item.status==='completed') { stClass='st-completed'; stText='已完成'; }
if(item.status==='terminated') { stClass='st-terminated'; stText='终止维护'; }
// 进度条百分比 (Based on 9 steps)
const pct = Math.round((item.progress / 9) * 100);
const transTag = item.transporting ?
`<span class="trans-tag"><i class="fas fa-truck"></i> 转运中</span>` : '';
return `
<div class="list-card ${item.status}" id="card-${item.id}" onclick="selectCaseById('${item.id}')">
<div class="lc-top">
<div class="flex items-center gap-2">
<span class="lc-id">${item.id}</span>
<span class="text-white text-sm">| ${item.name}</span>
</div>
<span class="lc-status ${stClass}">${stText}</span>
</div>
<div class="lc-hospital">
<span>${item.hospitalName}</span>
${transTag}
</div>
<div class="lc-footer">
<span>资料完成度</span>
<div class="progress-track">
<div class="progress-bar" style="width: ${pct}%"></div>
</div>
<span class="text-white">${item.progress}/9</span>
</div>
</div>
`;
}).join('');
}
// ==================== 联动逻辑 ====================
window.filterByHospital = (hId) => {
if (!hId) {
resetFilter();
return;
}
const filtered = caseDatabase.filter(d => d.hospitalId === hId);
renderList(filtered);
btnReset.classList.add('active');
if(filtered.length > 0) selectCase(filtered[0]);
}
window.resetFilter = () => {
selHospital.value = "";
renderList(caseDatabase);
btnReset.classList.remove('active');
mapLayer.style.transform = `translate(0, 0) scale(1)`;
document.querySelectorAll('.map-point').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.list-card').forEach(c => c.classList.remove('active-select'));
leftPanel.classList.remove('show');
}
window.selectCaseById = (id) => {
const item = caseDatabase.find(d => d.id === id);
if(item) selectCase(item);
}
function selectCase(item) {
const h = item.hospitalData;
mapLayer.style.transform = `translate(${50 - h.x}%, ${50 - h.y}%) scale(1.1)`;
document.querySelectorAll('.map-point').forEach(p => p.classList.remove('active'));
document.getElementById(`pt-${item.hospitalId}`).classList.add('active');
document.querySelectorAll('.list-card').forEach(c => c.classList.remove('active-select'));
const card = document.getElementById(`card-${item.id}`);
if(card) {
card.classList.add('active-select');
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
renderLeftDetail(item);
leftPanel.classList.add('show');
}
function renderLeftDetail(item) {
const b = item.basicInfo;
const q = item.qc;
// V2 Timeline Logic
let timelineHtml = '';
STEPS_NEW.forEach((stepName, idx) => {
const stepNum = idx + 1;
const nodeData = item.timeline.find(t => t.step === stepNum);
let liClass = '';
let contentHtml = '';
let statusTag = '';
// States: audited (done), pending_audit, registering, not_started
if (nodeData) {
if (nodeData.status === 'audited') {
liClass = 'audited'; // Green dot
contentHtml = `
<div class="tl-title">${stepName}</div>
<div class="tl-meta">
<span>${nodeData.time}</span>
<span>${nodeData.user}</span>
</div>
`;
} else if (nodeData.status === 'pending_audit') {
liClass = 'pending_audit'; // Orange dot
contentHtml = `
<div class="tl-title">${stepName} <span class="tl-tag tag-pending">待审核</span></div>
<div class="tl-meta">
<span>${nodeData.time}</span>
<span>${nodeData.user}</span>
</div>
`;
} else if (nodeData.status === 'registering') {
liClass = 'registering'; // Blue pulse
contentHtml = `
<div class="tl-title">${stepName}</div>
<div class="tl-meta">登记中...</div>
`;
}
} else {
liClass = ''; // Gray
contentHtml = `<div class="tl-title" style="color:#666">${stepName}</div>`;
}
timelineHtml += `
<div class="tl-item ${liClass}">
<div class="tl-dot"></div>
<div class="tl-content">${contentHtml}</div>
</div>
`;
});
// 器官列表
let organsHtml = '';
if (item.organs && item.organs.length > 0) {
organsHtml = item.organs.map(o => {
let stBadgeClass = 'st-bg-completed';
let stText = '已完成';
let cardClass = 'completed';
if(o.status === 'transit') {
stBadgeClass = 'st-bg-transit'; stText = '转运中'; cardClass = 'transit';
} else if (o.status === 'realloc') {
stBadgeClass = 'st-bg-realloc'; stText = '再分配'; cardClass = 'realloc';
}
return `
<div class="organ-card ${cardClass}">
<div class="oc-header">
<span class="oc-type">${o.type}</span>
<span class="oc-status-badge ${stBadgeClass}">${stText}</span>
</div>
<div class="oc-row">
<span style="color:#888">器官编号</span>
<span class="oc-id">${o.id}</span>
</div>
<div class="oc-row mt-1">
<span style="color:#888">接收单位</span>
<span class="text-white">${o.to}</span>
</div>
<div class="oc-row mt-1">
<span style="color:#888">移植等待者</span>
<span class="text-white">${o.recipient}</span>
</div>
</div>
`;
}).join('');
} else {
organsHtml = '<div class="text-gray-500 text-xs italic">暂无转运信息</div>';
}
// V2 质控: 改为资料完整度 & 捐赠案例跟踪
const isAuditLate = q.auditTime.includes('小时') && parseInt(q.auditTime) >= 1;
const isRegLate = q.regTime.includes('天') || (q.regTime.includes('小时') && parseInt(q.regTime) > 24);
const html = `
<div class="lp-section-title">基本信息</div>
<div class="lp-grid">
<div class="lp-field"><span class="lp-label">潜在登记编号</span><span class="lp-val highlight">${item.id}</span></div>
<div class="lp-field"><span class="lp-label">医院</span><span class="lp-val">${item.hospitalName}</span></div>
<div class="lp-field"><span class="lp-label">姓名</span><span class="lp-val">${item.name}</span></div>
<div class="lp-field"><span class="lp-label">科室</span><span class="lp-val">${b.dept}</span></div>
<div class="lp-field"><span class="lp-label">主要诊断</span><span class="lp-val">${b.diag}</span></div>
<div class="lp-field"><span class="lp-label">身份证号</span><span class="lp-val">${b.idCard}</span></div>
</div>
<div class="lp-section-title">案例资料</div> <!-- V2 Title -->
<div class="timeline-container">
${timelineHtml}
</div>
<div class="lp-section-title">转运信息</div> <!-- V2 Title -->
<div>${organsHtml}</div>
<div class="lp-section-title">质控总结</div>
<div class="qc-grid">
<div class="qc-item">
<span class="qc-lbl">线索审核耗时</span>
<span class="qc-val ${isAuditLate?'text-red':'text-green'}">${q.auditTime}</span>
</div>
<div class="qc-item">
<span class="qc-lbl">案例登记耗时</span>
<span class="qc-val ${isRegLate?'text-red':'text-green'}">${q.regTime}</span>
</div>
<div class="qc-item">
<span class="qc-lbl">资料完整度</span> <!-- V2 Updated -->
<span class="qc-val text-white">${q.completeness}</span>
</div>
<div class="qc-item">
<span class="qc-lbl">捐赠案例跟踪</span> <!-- V2 Updated -->
<span class="qc-val text-white">${q.result}</span>
</div>
</div>
`;
detailContent.innerHTML = html;
}
// 启动
initMap();
renderList(caseDatabase);
</script>
</body>
</html>
index.html
style.css
index.js
index.html