opo-案例管理-1edit icon

作者:
Fadinghaze
Fork(复制)
下载
嵌入
BUG反馈
index.html
style.css
index.js
现在支持上传本地图片了!
index.html
            
            <!-- 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>
        
编辑器加载中
预览
控制台