<!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.6</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);
--sub-panel-bg: rgba(12, 28, 50, 0.95);
--border-color: rgba(64, 116, 180, 0.4);
--text-main: #ffffff;
--text-sub: #8fb4d9;
--status-active: #00f0ff;
--status-completed: #30d158;
--status-warn: #ff9f0a;
--status-danger: #ff453a;
--status-offline: #666666;
--panel-width: 460px;
}
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;
}
::-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/119.2,32.3,7.5,0/1600x1200?access_token=pk.eyJ1IjoiZGVtb3VzZXIiLCJhIjoiY2x4eH..."');
background-size: cover; background-position: center;
overflow: hidden; z-index: 0;
}
.map-content-layer {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
transition: transform 1s cubic-bezier(0.2, 0.8, 0.2, 1);
transform-origin: 50% 50%;
}
.map-svg-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.traj-line { fill: none; stroke-width: 3; stroke-linecap: round; filter: drop-shadow(0 0 3px #000); opacity: 0.4; transition: 0.3s; }
.traj-line.active { stroke: var(--status-active); opacity: 1; stroke-dasharray: 10; animation: dash 20s linear infinite; }
.traj-line.history-path { stroke: #666; stroke-width: 2; opacity: 0.5; stroke-dasharray: 5; }
@keyframes dash { to { stroke-dashoffset: -500; } }
/* 地图点位 */
.map-marker-container {
position: absolute; transform: translate(-50%, -100%);
z-index: 10; cursor: pointer; transition: all 0.3s;
display: flex; flex-direction: column; align-items: center;
}
.map-marker-container.z-top { z-index: 30; }
.hud-card {
background: rgba(5, 15, 30, 0.9); border: 1px solid var(--status-active);
border-left: 3px solid var(--status-active); border-radius: 2px; padding: 8px 12px; min-width: 220px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5); backdrop-filter: blur(4px); margin-bottom: 8px;
}
.hud-row-1 { display: flex; justify-content: space-between; font-size: 14px; font-weight: bold; color: #fff; margin-bottom: 4px; }
.hud-row-2 { font-size: 11px; color: #aaa; margin-bottom: 6px; display: flex; gap: 5px; align-items: center; }
.hud-row-3 { display: flex; gap: 10px; font-size: 12px; padding-top: 4px; border-top: 1px solid rgba(255,255,255,0.1); }
.hud-val { color: var(--tech-cyan); font-weight: bold; font-family: monospace; }
.map-marker-container.warning .hud-card { border-color: var(--status-warn); border-left-color: var(--status-warn); }
.map-marker-container.warning .hud-val { color: var(--status-warn); }
.map-anchor {
width: 36px; height: 36px; background: #050b16; border: 2px solid var(--status-active);
border-radius: 4px; display: flex; justify-content: center; align-items: center;
color: var(--status-active); font-size: 18px; box-shadow: 0 0 15px rgba(0, 240, 255, 0.4); position: relative;
}
.map-marker-container.warning .map-anchor {
border-color: var(--status-warn); color: var(--status-warn);
box-shadow: 0 0 15px rgba(255, 159, 10, 0.5); animation: pulse-warn 2s infinite;
}
.map-marker-container.history-point .map-anchor {
border-color: #666; color: #888; box-shadow: none; width: 20px; height: 20px; font-size: 10px; border-radius: 50%;
}
@keyframes pulse-warn { 0% { box-shadow: 0 0 0 0 rgba(255,159,10, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(255,159,10, 0); } 100% { box-shadow: 0 0 0 0 rgba(255,159,10, 0); } }
/* ==================== 3. 调度弹窗 ==================== */
.map-popup {
position: absolute; width: 720px; height: 420px;
background: rgba(10, 20, 40, 0.98);
border: 1px solid var(--tech-cyan); border-radius: 4px;
box-shadow: 0 20px 50px rgba(0,0,0,0.9);
z-index: 100; transform: translate(-50%, 15px);
display: none; flex-direction: column; overflow: hidden;
backdrop-filter: blur(15px);
}
.map-popup.show { display: flex; animation: popupFadeIn 0.3s ease-out; }
.mp-header {
height: 44px; background: rgba(0, 240, 255, 0.1); border-bottom: 1px solid rgba(0,240,255,0.2);
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
font-weight: bold; color: var(--tech-cyan); font-size: 15px; letter-spacing: 1px;
}
.mp-close { cursor: pointer; transition: 0.2s; font-size: 16px; } .mp-close:hover { color: #fff; }
.mp-content { flex: 1; overflow: hidden; padding: 20px; display: flex; gap: 20px; }
/* Grid Layout */
.mp-route-left { width: 45%; display: flex; flex-direction: column; gap: 12px; border-right: 1px solid rgba(255,255,255,0.1); padding-right: 20px; }
.mp-route-right { width: 55%; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; }
.rig-item { margin-bottom: 10px; }
.rig-lbl { font-size: 12px; color: #888; display: block; margin-bottom: 4px; }
.rig-val-container {
display: flex; align-items: center; justify-content: space-between;
background: rgba(0,0,0,0.2); padding: 6px 10px; border-radius: 2px; border: 1px solid rgba(255,255,255,0.05);
}
.rig-val { font-size: 14px; color: #fff; font-weight: bold; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rig-edit-icon { color: #888; cursor: pointer; font-size: 12px; padding: 2px; transition: 0.2s; }
.rig-edit-icon:hover { color: var(--tech-cyan); }
.rig-input { width: 100%; background: rgba(0,0,0,0.5); border: 1px solid var(--primary-blue); color: #fff; padding: 5px 8px; font-size: 13px; border-radius: 2px; outline: none; }
.route-opt {
border: 1px solid #444; background: rgba(255,255,255,0.03); padding: 12px 15px;
cursor: pointer; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; transition: 0.2s;
}
.route-opt:hover { background: rgba(255,255,255,0.06); }
.route-opt.selected { border-color: var(--status-active); background: rgba(0, 122, 255, 0.15); box-shadow: 0 0 10px rgba(0,0,0,0.3); }
/* 底部栏 */
.mp-footer {
padding: 12px 20px; background: rgba(0,0,0,0.4); border-top: 1px solid rgba(255,255,255,0.1);
display: flex; justify-content: space-between; align-items: center;
}
.mp-footer-hint { font-size: 12px; color: #888; display: flex; align-items: center; gap: 6px; }
.mp-btn-confirm {
background: var(--primary-blue); color: #fff; border: none; border-radius: 4px;
padding: 8px 24px; font-size: 14px; cursor: pointer; font-weight: bold; transition: 0.2s;
}
.mp-btn-confirm:hover { background: var(--tech-cyan); color: #000; }
.mp-btn-confirm.danger { background: var(--status-danger); color: #fff; box-shadow: 0 0 15px rgba(255,69,58,0.4); }
.mp-btn-confirm.danger:hover { background: #ff6055; }
.mp-btn-confirm.disabled { background: #444; color: #888; cursor: not-allowed; box-shadow: none; }
/* 通讯录 Grid */
.mp-comm-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; width: 100%; align-content: start; }
.mp-contact-card {
background: rgba(255,255,255,0.05); border: 1px solid transparent; border-radius: 6px;
padding: 12px; cursor: pointer; display: flex; flex-direction: column; gap: 6px; transition: 0.2s;
}
.mp-contact-card:hover { background: rgba(255,255,255,0.08); }
.mp-contact-card.selected { border-color: var(--status-active); background: rgba(0,122,255,0.2); box-shadow: inset 0 0 15px rgba(0,122,255,0.1); }
.mp-contact-card .role { font-size: 11px; color: #888; display:flex; justify-content: space-between; }
.mp-contact-card .name { font-size: 13px; color: #fff; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Call Overlay */
.call-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #0a101e;
display: flex; flex-direction: column; z-index: 20; padding: 20px;
}
.call-header { text-align: center; margin-bottom: 20px; }
.call-timer-big { font-size: 32px; font-family: monospace; color: #fff; letter-spacing: 2px; }
.call-status-text { color: var(--status-active); font-size: 14px; margin-top: 5px; }
.call-participants { flex: 1; display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; align-content: center; }
.call-user { display: flex; flex-direction: column; align-items: center; gap: 8px; width: 100px; }
.call-user-avatar {
width: 60px; height: 60px; background: #333; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #fff;
border: 2px solid #444; position: relative;
}
.call-user.active .call-user-avatar { border-color: var(--status-completed); box-shadow: 0 0 15px rgba(48, 209, 88, 0.4); }
.call-user-name { font-size: 12px; color: #fff; text-align: center; }
.call-user-status { font-size: 10px; color: #888; }
.call-user.active .call-user-status { color: var(--status-completed); }
.call-controls { display: flex; justify-content: center; padding-top: 20px; }
.call-hangup-btn { width: 64px; height: 64px; border-radius: 50%; background: #ff453a; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28px; cursor: pointer; box-shadow: 0 4px 15px rgba(255, 69, 58, 0.4); transition: 0.2s; }
.call-hangup-btn:hover { transform: scale(1.1); background: #ff6055; }
@keyframes popupFadeIn { from { opacity: 0; transform: translate(-50%, 0); } to { opacity: 1; transform: translate(-50%, 15px); } }
/* ==================== 4. 面板系统 (Keep Left/Right Panels) ==================== */
.panel-container { width: var(--panel-width); height: 95vh; background: rgba(8, 26, 54, 0.96); border: 1px solid var(--border-color); box-shadow: 0 0 30px rgba(0, 0, 0, 0.8); display: flex; flex-direction: column; backdrop-filter: blur(12px); z-index: 50; margin-top: 2.5vh; transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
.panel-header { height: 48px; background: linear-gradient(90deg, rgba(0,76,163,0.4) 0%, transparent 100%); display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
.header-icon { width: 4px; height: 18px; background: var(--tech-cyan); margin-right: 12px; }
.panel-title { font-size: 18px; font-weight: bold; letter-spacing: 1px; color: #fff; text-shadow: 0 0 10px rgba(0,240,255,0.3); }
.list-content { flex: 1; overflow-y: auto; padding: 15px; }
.right-panel { position: absolute; right: 20px; }
.left-panel { position: absolute; left: 20px; transform: translateX(-120%); opacity: 0; z-index: 60; }
.left-panel.show { transform: translateX(0); opacity: 1; }
/* ==================== 5. 右侧列表组件 ==================== */
.kpi-toggle-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; padding: 10px 15px; border-bottom: 1px solid rgba(255,255,255,0.05); flex-shrink: 0; }
.kpi-btn { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 6px 4px; cursor: pointer; display: flex; flex-direction: column; align-items: center; transition: 0.2s; }
.kpi-btn.active { background: rgba(0, 122, 255, 0.2); border-color: var(--primary-blue); }
.kpi-val { font-size: 18px; font-weight: bold; font-family: Impact; margin-bottom: 2px; }
.kpi-lbl { font-size: 10px; color: var(--text-sub); }
.filter-time-row { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); background: rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-shrink: 0; }
.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: 11px; color: #fff; flex: 1; display: flex; align-items: center; }
.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: 3px 8px; cursor: pointer; border-radius: 2px; }
.time-btn.active { background: var(--primary-blue); color: white; font-weight: bold; }
.list-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05); border-left: 3px solid #666; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
.list-card:hover { background: rgba(255,255,255,0.08); }
.list-card.st-transit { border-left-color: var(--status-active); }
.list-card.st-completed { border-left-color: var(--status-completed); }
.list-card.st-warn { border-left-color: var(--status-warn); border: 1px solid rgba(255,159,10,0.3); background: rgba(255,159,10,0.05); }
.list-card.active { background: rgba(0, 122, 255, 0.2) !important; border: 1px solid var(--primary-blue) !important; box-shadow: inset 0 0 10px rgba(0,240,255,0.2); }
.lc-row1 { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.lc-id { font-size: 14px; font-weight: bold; color: #fff; }
.lc-organ { font-size: 11px; background: rgba(255,255,255,0.1); padding: 1px 6px; border-radius: 2px; margin-left: 8px; color: #ccc; }
.lc-tag { font-size: 10px; padding: 1px 6px; border-radius: 2px; border: 1px solid; }
.lc-row2 { font-size: 12px; color: #aaa; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
.lc-row3 { font-size: 11px; color: #666; border-top: 1px dashed rgba(255,255,255,0.1); padding-top: 6px; display: flex; justify-content: space-between; align-items: center; }
.lc-node { color: #d1d5db; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 60%; display: flex; align-items: center; }
.list-card.st-warn .lc-node { color: var(--status-warn); }
/* ==================== 6. 左侧详情组件 ==================== */
.sec-title { font-size: 13px; color: var(--tech-cyan); font-weight: bold; margin-bottom: 10px; border-left: 3px solid var(--tech-cyan); padding-left: 8px; }
.info-group { margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 8px; }
.info-group:last-child { border-bottom: none; }
.ig-row { display: flex; margin-bottom: 4px; font-size: 12px; align-items: flex-start; }
.ig-lbl { color: var(--text-sub); width: 70px; flex-shrink: 0; }
.ig-val { color: #fff; flex: 1; word-break: break-all; }
.change-arrow { color: var(--status-warn); margin: 0 5px; font-size: 10px; }
.new-val { color: var(--status-warn); }
.old-val { text-decoration: line-through; color: #666; font-size: 11px; }
.timing-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 4px; text-align: center; }
.t-item { border-right: 1px solid rgba(255,255,255,0.1); }
.t-item:last-child { border-right: none; }
.t-val { font-size: 18px; font-family: monospace; font-weight: bold; color: #fff; margin-bottom: 2px; }
.t-lbl { font-size: 11px; color: #888; }
.qc-container { display: flex; flex-direction: column; gap: 8px; }
.qc-card { background: rgba(255,255,255,0.03); padding: 10px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center; }
.qc-card.norm { border-color: rgba(0, 240, 255, 0.3); background: rgba(0, 240, 255, 0.05); }
.qc-card.norm .qc-icon-large { color: var(--tech-cyan); background: rgba(0, 240, 255, 0.1); }
.qc-card.warn { border-color: rgba(255,159,10,0.4); background: rgba(255,159,10,0.05); }
.qc-card.warn .qc-icon-large { color: var(--status-warn); background: rgba(255,159,10, 0.1); }
.qc-card.offline { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.02); }
.qc-card.offline .qc-icon-large { color: var(--status-offline); background: rgba(255,255,255,0.05); }
.qc-card.offline .qc-main-val { color: #888; }
.qc-icon-large { width: 40px; height: 40px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-right: 12px; flex-shrink: 0; }
.qc-content { flex: 1; }
.qc-main-val { font-size: 18px; font-weight: bold; color: #fff; line-height: 1.2; }
.qc-sub-lbl { font-size: 11px; color: #aaa; margin-bottom: 2px; }
.qc-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
.qc-tag { font-size: 10px; padding: 1px 6px; border-radius: 2px; border: 1px solid; }
.tag-norm { color: #30d158; border-color: rgba(48,209,88,0.3); background: rgba(48,209,88,0.1); }
.tag-warn { color: #ff9f0a; border-color: rgba(255,159,10,0.3); background: rgba(255,159,10,0.1); }
.tag-dang { color: #ff453a; border-color: rgba(255,69,58,0.3); background: rgba(255,69,58,0.1); }
.tag-offline { color: #888; border-color: #444; background: rgba(255,255,255,0.05); }
.cmd-grid { display: flex; gap: 15px; margin-bottom: 10px; }
.dispatch-card-btn { flex: 1; background: linear-gradient(180deg, rgba(0,122,255,0.15) 0%, rgba(0,122,255,0.05) 100%); border: 1px solid rgba(0,122,255,0.3); border-radius: 6px; padding: 15px; cursor: pointer; transition: all 0.3s; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.dispatch-card-btn:hover { background: rgba(0,122,255,0.3); border-color: var(--primary-blue); transform: translateY(-2px); }
.dispatch-card-btn i { font-size: 24px; margin-bottom: 8px; color: var(--tech-cyan); }
.dcb-title { font-size: 14px; font-weight: bold; color: #fff; margin-bottom: 4px; }
.dcb-hint { font-size: 10px; color: #8fb4d9; text-align: center; }
.text-green { color: #30d158 !important; }
.text-warn { color: #ff9f0a !important; }
.timeline { margin-left: 6px; border-left: 1px solid #444; padding-left: 15px; margin-top: 5px; }
.tl-node { position: relative; margin-bottom: 15px; }
.tl-dot { position: absolute; left: -20px; top: 4px; width: 9px; height: 9px; border-radius: 50%; background: #222; border: 2px solid #666; }
.tl-node.start .tl-dot { border-color: var(--status-active); background: var(--status-active); }
.tl-node.warn .tl-dot { border-color: var(--status-warn); background: var(--status-warn); box-shadow: 0 0 5px var(--status-warn); }
.tl-node.comm .tl-dot { border-color: #fff; background: var(--primary-blue); }
.tl-node.route .tl-dot { border-color: var(--tech-cyan); background: #000; }
.tl-node.reassign .tl-dot { border-color: var(--status-danger); background: var(--status-danger); }
.tl-node.end .tl-dot { border-color: var(--status-completed); background: var(--status-completed); }
.tl-time { font-size: 11px; color: #666; margin-bottom: 2px; }
.tl-text { font-size: 12px; color: #ccc; }
.playback-box { background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); padding: 15px; border-radius: 4px; margin-bottom: 15px; display: flex; flex-direction: column; gap: 10px; }
.pb-time-display { font-family: "Microsoft YaHei", monospace; font-size: 24px; font-weight: bold; color: var(--tech-cyan); text-align: center; text-shadow: 0 0 10px rgba(0, 240, 255, 0.4); margin-bottom: 5px; }
.pb-slider-container { width: 100%; position: relative; }
.pb-slider { width: 100%; height: 6px; background: #333; border-radius: 3px; appearance: none; outline: none; cursor: pointer; }
.pb-slider::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; background: #fff; border: 2px solid var(--primary-blue); border-radius: 50%; cursor: pointer; transition: transform 0.1s; }
.pb-labels { display: flex; justify-content: space-between; font-size: 10px; color: #666; margin-top: 4px; }
.pb-controls-row { display: flex; justify-content: center; align-items: center; gap: 20px; margin-top: 5px; position: relative; }
.pb-btn-main { width: 40px; height: 40px; border-radius: 50%; background: var(--primary-blue); color: #fff; border: none; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 16px; box-shadow: 0 0 10px rgba(0,122,255,0.4); }
.pb-btn-small { width: 30px; height: 30px; border-radius: 50%; background: rgba(255,255,255,0.1); color: #ccc; border: 1px solid rgba(255,255,255,0.1); cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 12px; }
.pb-speed-text { position: absolute; right: 0; font-size: 12px; color: var(--tech-cyan); font-family: monospace; }
</style>
</head>
<body>
<!-- 地图层 -->
<div class="map-viewport" id="mapContainer">
<div class="map-content-layer" id="mapContentLayer">
<svg class="map-svg-layer" id="trajSvg"></svg>
<div id="mapMarkers"></div>
<div id="popupContainer"></div>
</div>
</div>
<!-- 左侧面板:任务详情 -->
<div class="panel-container left-panel" id="leftPanel">
<div class="panel-header">
<div class="header-icon"></div>
<div class="panel-title">任务详情</div>
</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>
<div class="kpi-toggle-row">
<div class="kpi-btn active" id="btnAll" onclick="setFilter('all')"><span class="kpi-val text-white" id="kpiTotalNum">23</span><span class="kpi-lbl">任务总数</span></div>
<div class="kpi-btn" id="btnActive" onclick="setFilter('transit')"><span class="kpi-val text-cyan-400" id="kpiActiveNum">3</span><span class="kpi-lbl">运输中</span></div>
<div class="kpi-btn" id="btnHistory" onclick="setFilter('completed')"><span class="kpi-val text-green-500" id="kpiHistoryNum">20</span><span class="kpi-lbl">已完成</span></div>
</div>
<div class="filter-time-row">
<div class="date-display"><i class="far fa-calendar-alt mr-2 text-cyan-400"></i><span>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>
<div class="list-content" id="taskList"></div>
</div>
<script>
// ==================== DATA & CONSTANTS ====================
const locations = {
'NJ1': {x: 20, y: 30, name: '南京市第一医院'},
'WX1': {x: 80, y: 25, name: '无锡市人民医院'},
'SZ1': {x: 75, y: 75, name: '苏州一附院'},
'XZ1': {x: 15, y: 80, name: '徐州医科大附院'},
'TZ1': {x: 50, y: 50, name: '泰州市人民医院'}
};
const tasks = [
{
id: 'T-20231126-01', caseId: 'JSNJ0043',
organ: '心脏', organId: 'ORG-H01', box: 'BOX-209',
status: 'transit', isReassigned: false,
from: 'NJ1', to: 'WX1',
contact: '张伟 (138****1234)',
donor: { name: '王**', id: '3201**********1234' },
recipient: { name: '刘**', id: '3202**********5678' },
timing: { elapsed: '01:15:30', eta: '45分钟', cit: '<6h' },
qc: { temp: '4.2°C', traffic: '畅通', location: 'G42沪蓉高速 (常州段)', battery: '85%', alerts: [] },
timeline: [
{ type: 'comm', time: '2023-11-26 15:10', text: '通讯调度: 转运人(张伟)上报位置与状态正常' },
{ type: 'route', time: '2023-11-26 14:05', text: '路径调度: 系统推荐最优路线 G42沪蓉高速' },
{ type: 'start', time: '2023-11-26 14:00', text: '转运任务启动: 器官已装箱' }
]
},
// ... (Other tasks same as V2.4)
{
id: 'T-20231126-02', caseId: 'JSNJ0045',
organ: '肝脏', organId: 'ORG-L44', box: 'BOX-311',
status: 'transit', isReassigned: false,
from: 'SZ1', to: 'NJ1',
contact: '李强 (139****5678)',
donor: { name: '孙*', id: '3205**********9988' },
recipient: { name: '陈*', id: '3201**********0011' },
timing: { elapsed: '02:30:00', eta: '1小时20分', cit: '<12h' },
qc: { temp: '8.5°C', traffic: '拥堵', location: 'S58沪常高速 (太湖段)', battery: '20%', alerts: ['箱门未关闭', '温度过高'] },
timeline: [
{ type: 'comm', time: '2023-11-26 15:15', text: '通讯调度: 呼叫绿色通道 (江苏交警总队)' },
{ type: 'warn', time: '2023-11-26 15:10', text: '设备预警: 冷藏箱门未关闭' },
{ type: 'warn', time: '2023-11-26 15:05', text: '路况预警: S58沪常高速严重拥堵' },
{ type: 'start', time: '2023-11-26 12:40', text: '转运任务启动' }
]
},
{
id: 'T-20231126-03', caseId: 'JSNJ0050',
organ: '肾脏', organId: 'ORG-K12', box: 'BOX-404',
status: 'transit', isReassigned: true,
from: 'XZ1', to: 'TZ1', oldTo: 'NJ1',
contact: '王医生 (136****9988)',
donor: { name: '赵**', id: '3203**********2233' },
recipient: { name: '吴**', id: '3212**********6677' }, oldRecipient: { name: '郑*', id: '3201**********4455' },
timing: { elapsed: '00:45:00', eta: '2小时', cit: '<24h' },
qc: { temp: '4.0°C', traffic: '畅通', location: 'G2513淮徐高速', battery: '98%', alerts: [] },
timeline: [
{ type: 'route', time: '2023-11-26 10:20', text: '路径调度: 确认新路线至泰州市人民医院' },
{ type: 'reassign', time: '2023-11-26 10:15', text: '二次指派: 目的地变更为泰州市人民医院, 待移植: 吴**' },
{ type: 'start', time: '2023-11-26 09:30', text: '转运任务启动' }
]
},
{
id: 'T-20231125-08', caseId: 'JSNJ0022',
organ: '肺脏', organId: 'ORG-P09', box: 'BOX-550',
status: 'completed', isReassigned: false,
from: 'XZ1', to: 'NJ1',
contact: '赵云 (137****6666)',
donor: { name: '周*', id: '3203**********1111' },
recipient: { name: '钱*', id: '3201**********2222' },
timing: { elapsed: '03:45:00', eta: '-', cit: '<8h' },
qc: { temp: '4.0°C', traffic: '已到达', location: '南京市第一医院', battery: '-', alerts: [] },
timeline: [
{ type: 'end', time: '2023-11-25 11:45', text: '转运任务完成: 抵达南京市第一医院' },
{ type: 'comm', time: '2023-11-25 11:30', text: '通讯调度: 通知接收科室准备' },
{ type: 'route', time: '2023-11-25 08:10', text: '路径调度: 确认路线 G3京台高速 -> G36宁洛高速' },
{ type: 'start', time: '2023-11-25 08:00', text: '转运任务启动' }
]
}
];
const commContacts = { 'person': [ { name: 'OPO协调员-陈明明', role: '协调员' }, { name: '转运医生-张伟', role: '转运人' }, { name: '接收医院-刘主任', role: '医院联系人' } ], 'green': [ { name: '中国器官移植发展基金会', tel: '400-6686-836' }, { name: '交通运输部路网中心', tel: '010-65292200' }, { name: '民航局运行监控中心', tel: '010-64012907' }, { name: '中国人体器官捐献管理中心', tel: '010-65236997' }, { name: '铁路总公司运输局', tel: '12306' } ] };
let currentFilter = 'all';
let selectedId = null;
let isPlayback = false;
let playbackTimer = null;
let playbackSpeed = 1.0;
// Popup state
let isEditing = false;
let hasModified = false;
let secondConfirm = false;
let isExecuted = false;
function init() { updateKPIs(); renderMapHUD(); renderList(); }
function setFilter(type) {
currentFilter = type;
document.querySelectorAll('.kpi-btn').forEach(b => b.classList.remove('active'));
if(type === 'all') document.getElementById('btnAll').classList.add('active');
if(type === 'transit') document.getElementById('btnActive').classList.add('active');
if(type === 'completed') document.getElementById('btnHistory').classList.add('active');
selectedId = null; stopPlayback(); resetMapFocus(); closeMapPopup(); renderList(); renderMapHUD();
}
function updateKPIs() { document.getElementById('kpiTotalNum').innerText = tasks.length; document.getElementById('kpiActiveNum').innerText = tasks.filter(t=>t.status==='transit').length; document.getElementById('kpiHistoryNum').innerText = tasks.filter(t=>t.status==='completed').length; }
function renderList() {
const listEl = document.getElementById('taskList');
const data = tasks.filter(t => currentFilter === 'all' || t.status === currentFilter);
listEl.innerHTML = data.map(t => {
const latestNode = t.timeline[0];
const isWarn = latestNode.type === 'warn' || t.isReassigned || t.qc.alerts.length > 0;
const activeClass = selectedId === t.id ? 'active' : '';
let nodeIcon = '<i class="far fa-dot-circle mr-1 text-gray-400"></i>';
if(latestNode.type === 'warn') nodeIcon = '<i class="fas fa-exclamation-triangle mr-1 text-orange-500"></i>';
if(latestNode.type === 'comm') nodeIcon = '<i class="fas fa-headset mr-1 text-blue-400"></i>';
if(latestNode.type === 'route') nodeIcon = '<i class="fas fa-route mr-1 text-cyan-400"></i>';
if(latestNode.type === 'reassign') nodeIcon = '<i class="fas fa-random mr-1 text-red-500"></i>';
if(latestNode.type === 'end') nodeIcon = '<i class="fas fa-flag-checkered mr-1 text-green-500"></i>';
return `<div class="list-card st-${t.status} ${isWarn?'st-warn':''} ${activeClass}" onclick="selectTask('${t.id}')"><div class="lc-row1"><div class="flex items-center"><span class="lc-id">${t.caseId}</span><span class="lc-organ">${t.organ}</span></div><span class="lc-tag" style="color:${t.status==='transit'?'#00f0ff':'#30d158'};border-color:${t.status==='transit'?'#00f0ff':'#30d158'}">${t.status==='transit'?'转运中':'已完成'}</span></div><div class="lc-row2"><span>${locations[t.from].name}</span><i class="fas fa-arrow-right text-gray-500" style="font-size:10px"></i><span>${locations[t.to].name}</span></div><div class="lc-row3"><span class="lc-node">${nodeIcon}${latestNode.text}</span><span>${latestNode.time}</span></div></div>`;
}).join('');
}
function renderMapHUD() {
const container = document.getElementById('mapMarkers'); container.innerHTML = ''; document.getElementById('trajSvg').innerHTML = '';
tasks.forEach(t => {
if(currentFilter !== 'all' && t.status !== currentFilter) return;
if(t.status === 'completed' && selectedId !== t.id) return;
if(t.status === 'completed' && selectedId === t.id && !isPlayback) { drawHistoryEndpoints(t); return; }
if(t.status === 'transit') {
const loc = getMockLocation(t); const isSelected = selectedId === t.id; const isWarn = t.isReassigned || t.qc.alerts.length > 0 || t.qc.traffic==='拥堵';
const el = document.createElement('div'); el.className = `map-marker-container ${isWarn?'warning':''} ${isSelected?'z-top':''}`; el.style.left = loc.x + '%'; el.style.top = loc.y + '%';
el.innerHTML = `<div class="hud-card" onclick="selectTask('${t.id}', event)"><div class="hud-row-1"><span>${t.organId}</span></div><div class="hud-row-2"><span>${locations[t.from].name}</span><i class="fas fa-arrow-right"></i><span>${locations[t.to].name}</span></div><div class="hud-row-3"><div><span class="text-gray-500">用时:</span> <span class="hud-val text-white">${t.timing.elapsed}</span></div><div><span class="text-gray-500">温度:</span> <span class="hud-val">${t.qc.temp}</span></div></div></div><div class="map-anchor"><i class="fas fa-suitcase-medical"></i></div>`;
container.appendChild(el); if(isSelected) drawLine(t);
}
});
}
function drawHistoryEndpoints(t) { const container = document.getElementById('mapMarkers'); const s = locations[t.from]; const e = locations[t.to]; [s, e].forEach(p => { const el = document.createElement('div'); el.className = 'map-marker-container history-point'; el.style.left = p.x + '%'; el.style.top = p.y + '%'; el.innerHTML = `<div class="map-anchor"><i class="fas fa-circle"></i></div>`; container.appendChild(el); }); const svg = document.getElementById('trajSvg'); const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.setAttribute("x1", s.x+"%"); line.setAttribute("y1", s.y+"%"); line.setAttribute("x2", e.x+"%"); line.setAttribute("y2", e.y+"%"); line.setAttribute("class", "traj-line history-path"); svg.appendChild(line); }
function drawPlayback(t, progress) { const container = document.getElementById('mapMarkers'); container.innerHTML = ''; drawHistoryEndpoints(t); const s = locations[t.from]; const e = locations[t.to]; const curX = s.x + (e.x - s.x) * (progress / 100); const curY = s.y + (e.y - s.y) * (progress / 100); const el = document.createElement('div'); el.className = 'map-marker-container z-top'; el.style.left = curX + '%'; el.style.top = curY + '%'; el.innerHTML = `<div class="hud-card"><div class="hud-row-1"><span>${t.organId}</span><span style="font-size:10px;color:#00f0ff">回放中</span></div><div class="hud-row-2"><span>${locations[t.from].name}</span><i class="fas fa-arrow-right"></i><span>${locations[t.to].name}</span></div><div class="hud-row-3"><div><span class="text-gray-500">用时:</span> <span class="hud-val text-white">${t.timing.elapsed}</span></div></div></div><div class="map-anchor" style="background:var(--tech-cyan);color:#000;border-color:#fff"><i class="fas fa-suitcase-medical"></i></div>`; container.appendChild(el); }
function getMockLocation(t) { const s = locations[t.from]; const e = locations[t.to]; return { x: s.x + (e.x-s.x)*0.5, y: s.y + (e.y-s.y)*0.5 }; }
function drawLine(t) { const svg = document.getElementById('trajSvg'); const s = locations[t.from]; const e = locations[t.to]; const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.setAttribute("x1", s.x+"%"); line.setAttribute("y1", s.y+"%"); line.setAttribute("x2", e.x+"%"); line.setAttribute("y2", e.y+"%"); line.setAttribute("class", "traj-line active"); svg.appendChild(line); }
function focusMapOnTask(t) { let cx, cy; if(t.status === 'completed') { const s = locations[t.from]; const e = locations[t.to]; cx = (s.x + e.x) / 2; cy = (s.y + e.y) / 2; } else { const loc = getMockLocation(t); cx = loc.x; cy = loc.y; } const tx = 50 - cx; const ty = 50 - cy; document.getElementById('mapContentLayer').style.transform = `translate(${tx}%, ${ty}%) scale(1)`; }
function resetMapFocus() { document.getElementById('mapContentLayer').style.transform = `translate(0, 0) scale(1)`; }
window.selectTask = (id, e) => { if(e) e.stopPropagation(); selectedId = id; stopPlayback(); const t = tasks.find(x => x.id === id); focusMapOnTask(t); renderList(); renderMapHUD(); renderDetailPanel(t); document.getElementById('leftPanel').classList.add('show'); closeMapPopup(); }
function renderDetailPanel(t) { /* Same as V2.4, just update QC logic slightly if needed, code same as previous */
const container = document.getElementById('detailContent');
let originHtml = `<div class="ig-row"><span class="ig-lbl">起始:</span><span class="ig-val">${locations[t.from].name}</span></div><div class="ig-row"><span class="ig-lbl">捐赠者:</span><span class="ig-val">${t.donor.name} (${t.donor.id})</span></div>`;
let destHtml = '';
if(t.isReassigned) { destHtml = `<div class="ig-row"><span class="ig-lbl">到达:</span><span class="ig-val"><span class="old-val">${locations[t.oldTo].name}</span><i class="fas fa-arrow-right change-arrow"></i><span class="new-val">${locations[t.to].name}</span></span></div><div class="ig-row"><span class="ig-lbl">待移植:</span><span class="ig-val"><span class="old-val">${t.oldRecipient.name}</span><i class="fas fa-arrow-right change-arrow"></i><span class="new-val">${t.recipient.name} (${t.recipient.id})</span></span></div>`; }
else { destHtml = `<div class="ig-row"><span class="ig-lbl">到达:</span><span class="ig-val">${locations[t.to].name}</span></div><div class="ig-row"><span class="ig-lbl">待移植:</span><span class="ig-val">${t.recipient.name} (${t.recipient.id})</span></div>`; }
let qcHtml = '';
if(t.status === 'completed') { qcHtml = `<div class="qc-container"><div class="qc-card offline"><div class="qc-icon-large"><i class="fas fa-box"></i></div><div class="qc-content"><div class="qc-sub-lbl">冷藏箱监测</div><div class="qc-main-val">-</div><div class="qc-tags"><span class="qc-tag tag-offline">设备已离线</span></div></div></div><div class="qc-card offline"><div class="qc-icon-large"><i class="fas fa-traffic-light"></i></div><div class="qc-content"><div class="qc-sub-lbl">路况监测</div><div class="qc-main-val">已到达</div></div></div></div>`; }
else { let alertTags = t.qc.alerts.length > 0 ? t.qc.alerts.map(a => `<span class="qc-tag tag-dang">${a}</span>`).join('') : `<span class="qc-tag tag-norm">设备运行正常</span>`; qcHtml = `<div class="qc-container"><div class="qc-card ${t.qc.alerts.length>0 ? 'warn' : 'norm'}"><div class="qc-icon-large"><i class="fas fa-box"></i></div><div class="qc-content"><div class="qc-sub-lbl">冷藏箱监测</div><div class="qc-main-val">${t.qc.temp}</div><div class="qc-tags">${alertTags}<span class="qc-tag ${parseInt(t.qc.battery)<30?'tag-warn':'tag-norm'}">电量 ${t.qc.battery}</span></div></div></div><div class="qc-card ${t.qc.traffic==='拥堵' ? 'warn' : 'norm'}"><div class="qc-icon-large"><i class="fas fa-traffic-light"></i></div><div class="qc-content"><div class="qc-sub-lbl">路况监测</div><div class="qc-main-val">${t.qc.traffic}</div><div class="text-xs text-gray-400 mt-1"><i class="fas fa-map-marker-alt mr-1"></i>${t.qc.location}</div></div></div></div>`; }
let actionHtml = '';
if(t.status === 'transit') {
actionHtml = `<div class="sec-title">调度指挥</div><div class="cmd-grid"><div class="dispatch-card-btn" onclick="openMapPopup('comm', '${t.id}')"><i class="fas fa-headset"></i><span class="dcb-title">通讯指挥</span><span class="dcb-hint">人员呼叫 / <span class="text-green">绿色通道</span></span></div><div class="dispatch-card-btn" onclick="openMapPopup('route', '${t.id}')"><i class="fas fa-route"></i><span class="dcb-title">路径调度</span><span class="dcb-hint">推荐路线 / <span class="text-warn">二次指派</span></span></div></div><div style="height:20px"></div>`;
} else { actionHtml = `<div class="sec-title">轨迹回放</div><div class="playback-box"><div class="pb-time-display" id="pbClock">00:00:00</div><div class="pb-slider-container"><input type="range" class="pb-slider" id="pbSlider" min="0" max="100" value="0" oninput="manualSlide('${t.id}', this.value)"><div class="pb-labels"><span>00:00:00</span><span>${t.timing.elapsed}</span></div></div><div class="pb-controls-row"><button class="pb-btn-small" onclick="changeSpeed(-0.5)"><i class="fas fa-backward"></i></button><button class="pb-btn-main" onclick="togglePlayback('${t.id}')" id="btnPlay"><i class="fas fa-play"></i></button><button class="pb-btn-small" onclick="changeSpeed(0.5)"><i class="fas fa-forward"></i></button><span class="pb-speed-text" id="pbSpeed">x1.0</span></div></div>`; }
const tlHtml = t.timeline.map(node => `<div class="tl-node ${node.type}"><div class="tl-dot"></div><div class="tl-time">${node.time}</div><div class="tl-text">${node.text}</div></div>`).join('');
container.innerHTML = `<div class="sec-title">基本信息</div><div class="info-group"><div class="ig-row"><span class="ig-lbl">器官编号:</span><span class="ig-val">${t.organId} (${t.organ})</span></div><div class="ig-row"><span class="ig-lbl">所属案例:</span><span class="ig-val">${t.caseId}</span></div><div class="ig-row"><span class="ig-lbl">冷藏箱号:</span><span class="ig-val">${t.box}</span></div><div class="ig-row"><span class="ig-lbl">转运人:</span><span class="ig-val">${t.contact}</span></div></div><div class="info-group">${originHtml}</div><div class="info-group">${destHtml}</div><div style="height:15px"></div><div class="sec-title">任务用时</div><div class="timing-grid"><div class="t-item"><div class="t-val text-cyan-400">${t.timing.elapsed}</div><div class="t-lbl">在途时长</div></div><div class="t-item"><div class="t-val">${t.timing.eta}</div><div class="t-lbl">预计剩余</div></div><div class="t-item"><div class="t-val text-gray-400">${t.timing.cit}</div><div class="t-lbl">冷缺血参考</div></div></div><div style="height:15px"></div><div class="sec-title">转运状态</div>${qcHtml}<div style="height:20px"></div>${actionHtml}<div class="sec-title">任务流程</div><div class="timeline">${tlHtml}</div>`;
}
// ==================== V2.5 POPUP LOGIC ====================
window.openMapPopup = (type, taskId) => {
const popup = document.createElement('div');
popup.className = 'map-popup show'; popup.id = 'activePopup';
const t = tasks.find(x => x.id === taskId);
const loc = getMockLocation(t);
popup.style.left = loc.x + '%'; popup.style.top = loc.y + '%'; popup.style.marginTop = '45px';
const titleText = type === 'comm' ? '通讯指挥' : '路径调度';
let contentHtml = '';
// Reset State
isEditing = false; hasModified = false; secondConfirm = false; isExecuted = false;
if(type === 'comm') {
const pList = commContacts.person.map(p => `<div class="mp-contact-card" onclick="toggleSelect(this)"><div style="display:flex;justify-content:space-between"><span class="role">${p.role}</span><i class="fas fa-check text-xs hidden" style="color:#00f0ff"></i></div><div class="name">${p.name}</div></div>`).join('');
const gList = commContacts.green.map(g => `<div class="mp-contact-card" onclick="toggleSelect(this)"><div style="display:flex;justify-content:space-between"><span class="role" style="color:#30d158">绿色通道</span><i class="fas fa-check text-xs hidden" style="color:#00f0ff"></i></div><div class="name">${g.name}</div></div>`).join('');
contentHtml = `<div style="display:flex;flex-direction:column;width:100%;height:100%"><div style="flex:1;overflow-y:auto"><div style="font-size:12px;color:#888;margin-bottom:5px">任务相关人员</div><div class="mp-comm-grid" style="margin-bottom:15px">${pList}</div><div style="font-size:12px;color:#888;margin-bottom:5px">绿色通道 (24H应急)</div><div class="mp-comm-grid">${gList}</div></div></div>`;
popup.innerHTML = `<div class="mp-header"><span>${titleText}</span><span class="mp-close" onclick="closeMapPopup()"><i class="fas fa-times"></i></span></div><div class="mp-content">${contentHtml}</div><div class="mp-footer"><span class="mp-footer-hint" id="mpFooterHint"><i class="fas fa-info-circle"></i> 请选择通话对象</span><button class="mp-btn-confirm" onclick="startCall()">呼叫选中对象</button></div>`;
} else if(type === 'route') {
const leftCol = `
<div class="rig-item"><span class="rig-lbl">起始地点</span><span class="rig-val">${locations[t.from].name}</span></div>
<div class="rig-item"><span class="rig-lbl">捐赠者</span><span class="rig-val">${t.donor.name}</span></div>
<div class="rig-item">
<span class="rig-lbl">到达地点</span>
<div class="rig-val-container" id="dest-container">
<span class="rig-val" id="val-dest">${locations[t.to].name}</span>
<i class="fas fa-pen rig-edit-icon" onclick="enableEdit('dest')"></i>
</div>
</div>
<div class="rig-item">
<span class="rig-lbl">待移植</span>
<div class="rig-val-container" id="recip-container">
<span class="rig-val" id="val-recip">${t.recipient.name} (${t.recipient.id})</span>
<i class="fas fa-pen rig-edit-icon" onclick="enableEdit('recip')"></i>
</div>
</div>
`;
const rightCol = `
<div style="font-size:12px;color:#888;margin-bottom:8px">推荐运输路径</div>
<div class="route-opt selected" onclick="selectRoute(this)"><div><div style="color:#fff;font-weight:bold;font-size:13px">G42沪蓉高速 (推荐)</div><div style="font-size:11px;color:#00f0ff">预计 45分钟 <i class="fas fa-traffic-light"></i> 3</div></div><div style="text-align:right"><span style="font-size:12px;color:#ccc">150km</span><div style="font-size:10px;color:#30d158">畅通</div></div></div>
<div class="route-opt" onclick="selectRoute(this)"><div><div style="color:#fff;font-weight:bold;font-size:13px">G312国道</div><div style="font-size:11px;color:#aaa">预计 1小时10分 <i class="fas fa-traffic-light"></i> 12</div></div><div style="text-align:right"><span style="font-size:12px;color:#ccc">148km</span><div style="font-size:10px;color:#30d158">畅通</div></div></div>
<div class="route-opt" onclick="selectRoute(this)"><div><div style="color:#fff;font-weight:bold;font-size:13px">S58沪常高速</div><div style="font-size:11px;color:#aaa">预计 1小时30分 <i class="fas fa-traffic-light"></i> 5</div></div><div style="text-align:right"><span style="font-size:12px;color:#ccc">160km</span><div style="font-size:10px;color:#ff453a">拥堵</div></div></div>
`;
popup.innerHTML = `<div class="mp-header"><span>${titleText}</span><span class="mp-close" onclick="closeMapPopup()"><i class="fas fa-times"></i></span></div><div class="mp-content"><div class="mp-route-left">${leftCol}</div><div class="mp-route-right">${rightCol}</div></div><div class="mp-footer"><span class="mp-footer-hint" id="routeFooterHint"><i class="fas fa-info-circle"></i> 请确认路径方案</span><button class="mp-btn-confirm" id="btnRouteConfirm" onclick="confirmRoute()">确认调度</button></div>`;
}
closeMapPopup(); document.getElementById('popupContainer').appendChild(popup);
}
window.closeMapPopup = () => { const p = document.getElementById('activePopup'); if(p) p.remove(); }
window.toggleSelect = (el) => { el.classList.toggle('selected'); el.querySelector('.fa-check').classList.toggle('hidden'); }
window.selectRoute = (el) => { document.querySelectorAll('.route-opt').forEach(r => r.classList.remove('selected')); el.classList.add('selected'); }
window.startCall = () => { /* V2.4 Logic */ const selected = document.querySelectorAll('.mp-contact-card.selected'); if(selected.length === 0) { document.getElementById('mpFooterHint').innerHTML = `<span style="color:#ff453a"><i class="fas fa-times-circle"></i> 请先选择至少一个通讯对象</span>`; return; } let participantsHtml = ''; selected.forEach(el => { const name = el.querySelector('.name').innerText; participantsHtml += `<div class="call-user"><div class="call-user-avatar"><i class="fas fa-user"></i></div><div class="call-user-name">${name}</div><div class="call-user-status">连接中...</div></div>`; }); const pContent = document.querySelector('#activePopup .mp-content'); const overlay = document.createElement('div'); overlay.className = 'call-overlay'; overlay.innerHTML = `<div class="call-header"><div class="call-timer-big" id="callTimer">00:00</div><div class="call-status-text">正在呼叫...</div></div><div class="call-participants">${participantsHtml}</div><div class="call-controls"><div class="call-hangup-btn" onclick="this.closest('.call-overlay').remove()"><i class="fas fa-phone-slash"></i></div></div>`; pContent.appendChild(overlay); setTimeout(() => { overlay.querySelector('.call-status-text').innerText = "通话中"; document.querySelectorAll('.call-user-status').forEach(el => el.innerText = "已接通"); document.querySelectorAll('.call-user').forEach(el => el.classList.add('active')); }, 1500); }
// V2.5 Route Edit Logic
window.enableEdit = (field) => {
if(isExecuted) return;
const container = document.getElementById(field + '-container');
const valSpan = document.getElementById('val-' + field);
const originalVal = valSpan.innerText;
container.innerHTML = `<input type="text" class="rig-input" value="${originalVal}" oninput="checkModification()">`;
container.querySelector('input').focus();
}
window.checkModification = () => {
hasModified = true;
const hint = document.getElementById('routeFooterHint');
hint.innerHTML = `<span style="color:#ff9f0a"><i class="fas fa-exclamation-triangle"></i> 请注意,原任务指派信息变更</span>`;
// Reset confirm state if edited again
secondConfirm = false;
const btn = document.getElementById('btnRouteConfirm');
btn.classList.remove('danger');
btn.innerText = "确认调度";
}
window.confirmRoute = () => {
if(isExecuted) return;
const btn = document.getElementById('btnRouteConfirm');
const hint = document.getElementById('routeFooterHint');
if(hasModified) {
if(!secondConfirm) {
// Stage 1: Request Second Confirm
secondConfirm = true;
btn.classList.add('danger');
btn.innerText = "再次确认";
hint.innerHTML = `<span style="color:#ff453a"><i class="fas fa-exclamation-circle"></i>二次指派即将执行,请再次确认</span>`;
} else {
// Stage 2: Execute Modified
executeDispatch(true);
}
} else {
// Normal Execute
executeDispatch(false);
}
}
function executeDispatch(isReassign) {
isExecuted = true;
const btn = document.getElementById('btnRouteConfirm');
const hint = document.getElementById('routeFooterHint');
btn.classList.remove('danger');
btn.classList.add('disabled');
btn.innerText = isReassign ? "已变更" : "已确认";
if(isReassign) {
hint.innerHTML = `<span style="color:#30d158"><i class="fas fa-check-circle"></i> 变更指令已下发</span>`;
} else {
hint.innerHTML = `<span style="color:#30d158"><i class="fas fa-check-circle"></i> 调度方案已执行</span>`;
}
}
function stopPlayback(){ isPlayback=false; clearInterval(playbackTimer); }
window.togglePlayback=(id)=>{ const btn=document.getElementById('btnPlay');const slider=document.getElementById('pbSlider');const t=tasks.find(x=>x.id===id);if(isPlayback){isPlayback=false;clearInterval(playbackTimer);btn.innerHTML='<i class="fas fa-play"></i>';}else{isPlayback=true;btn.innerHTML='<i class="fas fa-pause"></i>';playbackTimer=setInterval(()=>{let val=parseInt(slider.value);if(val>=100){val=0;isPlayback=false;clearInterval(playbackTimer);btn.innerHTML='<i class="fas fa-play"></i>';return;}val+=1;slider.value=val;manualSlide(t.id,val);},100/playbackSpeed);}}
window.changeSpeed=(delta)=>{playbackSpeed+=delta;if(playbackSpeed<0.5)playbackSpeed=0.5;if(playbackSpeed>4.0)playbackSpeed=4.0;document.getElementById('pbSpeed').innerText='x'+playbackSpeed.toFixed(1);}
window.manualSlide=(id,val)=>{const t=tasks.find(x=>x.id===id);drawPlayback(t,val);const totalStr=t.timing.elapsed;const parts=totalStr.split(':');const totalSec=parseInt(parts[0])*3600+parseInt(parts[1])*60+parseInt(parts[2]);const currentSec=Math.floor(totalSec*(val/100));const cH=Math.floor(currentSec/3600);const cM=Math.floor((currentSec%3600)/60);const cS=currentSec%60;document.getElementById('pbClock').innerText=`${cH.toString().padStart(2,'0')}:${cM.toString().padStart(2,'0')}:${cS.toString().padStart(2,'0')}`;}
init();
</script>
</body>
</html>
index.html
style.css
index.js
index.html