opo-协调员管理-1edit icon

作者:
Fadinghaze
Fork(复制)
下载
嵌入
BUG反馈
index.html
style.css
index.js
现在支持上传本地图片了!
index.html
            
            <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>江苏省OPO可视化调度平台 - 协调员管理 V6</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. 设计系统定义 (V6) ==================== */
        :root {
            --primary-blue: #007aff;
            --tech-cyan: #00f0ff;
            --tech-orange: #ff9f0a;
            --tech-bg: #050b16;
            --panel-bg: rgba(16, 36, 68, 0.95);
            --border-color: rgba(64, 116, 180, 0.4);
            --text-main: #ffffff;
            --text-sub: #8fb4d9;
            
            /* 状态色 */
            --st-online: #30d158;
            --st-task: #ff9f0a;
            --st-offline: #8e8e93;
        }

        body {
            margin: 0; padding: 0;
            background-color: var(--tech-bg);
            font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
            color: var(--text-main);
            height: 100vh; width: 100vw;
            overflow: hidden;
            display: flex;
        }

        ::-webkit-scrollbar { width: 4px; }
        ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; }
        ::-webkit-scrollbar-track { background: transparent; }

        /* ==================== 2. 地图层 ==================== */
        .map-viewport {
            flex: 1; position: relative;
            background-image: radial-gradient(circle at 50% 50%, #112035 0%, #02050a 100%),
                              url('https://api.mapbox.com/styles/v1/mapbox/dark-v10/static/118.78,32.3,7.8,0/1600x1200?access_token=pk.eyJ1IjoiZGVtb3VzZXIiLCJhIjoiY2x4eH..."'); 
            background-size: cover; background-position: center;
            overflow: hidden;
            transition: all 0.5s ease;
        }
        
        .trajectory-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity 0.5s; z-index: 1;}
        .trajectory-layer.active { opacity: 1; }
        path.trace-line { fill: none; stroke: var(--tech-cyan); stroke-width: 2; stroke-dasharray: 10; stroke-dashoffset: 100; animation: dash 20s linear infinite; filter: drop-shadow(0 0 4px var(--tech-cyan)); }
        @keyframes dash { to { stroke-dashoffset: 0; } }

        .coord-marker { position: absolute; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; cursor: pointer; z-index: 10; transition: all 0.3s; }
        .coord-avatar-box { width: 40px; height: 40px; background: #004e92; border-radius: 50%; border: 2px solid var(--st-offline); box-shadow: 0 0 10px rgba(0,0,0,0.8); display: flex; justify-content: center; align-items: center; font-size: 18px; font-weight: bold; color: #fff; position: relative; z-index: 2; }
        .coord-marker.online .coord-avatar-box { border-color: var(--st-online); box-shadow: 0 0 15px rgba(48, 209, 88, 0.6); }
        .coord-marker.task .coord-avatar-box { border-color: var(--st-task); box-shadow: 0 0 15px rgba(255, 159, 10, 0.6); }
        .coord-marker.active { z-index: 50; transform: translate(-50%, -60%) scale(1.2); }
        .coord-label { margin-top: 6px; background: rgba(0, 0, 0, 0.7); border: 1px solid rgba(255,255,255,0.2); backdrop-filter: blur(4px); padding: 2px 8px; border-radius: 12px; font-size: 11px; color: #fff; white-space: nowrap; display: flex; align-items: center; gap: 4px; }
        .coord-dot { width: 6px; height: 6px; border-radius: 50%; background: #888; }
        .online .coord-dot { background: var(--st-online); }
        .task .coord-dot { background: var(--st-task); }

        /* ==================== 3. 面板通用 ==================== */
        .panel {
            width: 400px; height: 100vh;
            background: var(--panel-bg);
            border-left: 1px solid var(--border-color);
            border-right: 1px solid var(--border-color);
            display: flex; flex-direction: column;
            backdrop-filter: blur(12px);
            z-index: 30; flex-shrink: 0;
            box-shadow: 0 0 40px rgba(0,0,0,0.6);
        }
        
        .panel-left {
            position: absolute; left: 0; top: 0;
            transform: translateX(-100%);
            transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
            border-right: 1px solid var(--border-color); border-left: none;
        }
        .panel-left.active { transform: translateX(0); }

        .p-header {
            height: 56px; flex-shrink: 0;
            display: flex; align-items: center; padding: 0 20px;
            background: linear-gradient(90deg, rgba(0,122,255,0.15), transparent);
            border-bottom: 1px solid var(--border-color);
        }
        .header-deco { width: 4px; height: 18px; background: var(--tech-cyan); margin-right: 12px; box-shadow: 0 0 8px var(--tech-cyan); }
        .p-title { font-size: 18px; font-weight: bold; color: #fff; letter-spacing: 1px; flex: 1; text-align: left; }
        
        .p-content { flex: 1; overflow-y: auto; padding: 20px; }

        /* ==================== 4. 左侧面板 (Detail V5) ==================== */
        .info-card { display: flex; gap: 15px; margin-bottom: 25px; padding-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.1); }
        .info-avatar { width: 64px; height: 64px; background: #004e92; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 24px; font-weight: bold; border: 2px solid rgba(255,255,255,0.2); flex-shrink: 0; }
        .info-details { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 6px; }
        .info-name-row { display: flex; align-items: center; gap: 10px; }
        .info-name { font-size: 20px; font-weight: bold; color: #fff; }
        .info-gender { font-size: 12px; background: rgba(255,255,255,0.1); padding: 1px 6px; border-radius: 4px; color: #ccc; }
        .info-sub { font-size: 13px; color: var(--text-sub); display: flex; gap: 15px; }

        .perf-container { display: flex; justify-content: space-between; margin-bottom: 30px; padding: 0 10px; }
        .perf-item { text-align: center; }
        .perf-ring { width: 50px; height: 50px; border-radius: 50%; border: 4px solid #333; display: flex; justify-content: center; align-items: center; font-size: 12px; font-weight: bold; margin: 0 auto 8px; position: relative; }
        .perf-lbl { font-size: 12px; color: var(--text-sub); }

        .section-title { font-size: 15px; font-weight: bold; color: var(--tech-cyan); border-left: 3px solid var(--tech-cyan); padding-left: 10px; margin-bottom: 12px; }
        
        .task-card-v4 { background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(255, 159, 10, 0.3); border-radius: 6px; padding: 15px; margin-bottom: 25px; }
        .task-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 13px; }
        .task-row:last-child { margin-bottom: 0; }
        .tk-label { color: #888; }
        .tk-val { color: #fff; font-weight: 500; text-align: right; }
        .tk-val.cyan { color: var(--tech-cyan); font-family: monospace; letter-spacing: 1px; }
        .tk-val.orange { color: var(--tech-orange); font-weight: bold; }

        .loc-box { margin-bottom: 25px; }
        .loc-content { display: flex; align-items: center; gap: 10px; color: #fff; font-size: 14px; margin-top: 5px; }
        .loc-icon { color: #ef4444; font-size: 16px; }

        .comm-box { margin-bottom: 25px; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.05); }
        .comm-screen { height: 40px; display: flex; align-items: center; justify-content: center; font-family: monospace; font-size: 14px; color: #888; margin-bottom: 10px; background: rgba(0,0,0,0.3); border-radius: 4px; }
        .btn-call { width: 100%; padding: 10px; background: rgba(48, 209, 88, 0.15); border: 1px solid var(--st-online); color: var(--st-online); border-radius: 4px; font-weight: bold; cursor: pointer; transition: 0.2s; display: flex; justify-content: center; align-items: center; gap: 8px; }
        .btn-call:hover { background: var(--st-online); color: #000; }
        .btn-call.hangup { background: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #ef4444; }
        .btn-call.hangup:hover { background: #ef4444; color: #fff; }

        .trace-card-v4 { background: rgba(0,0,0,0.3); border-radius: 6px; padding: 15px; border: 1px solid rgba(255,255,255,0.05); }
        .trace-header { display: flex; gap: 10px; margin-bottom: 15px; }
        .trace-sel { background: #0f172a; border: 1px solid #334155; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; outline: none; }
        
        .trace-time-display { text-align: center; font-size: 20px; font-weight: bold; color: var(--tech-cyan); margin-bottom: 10px; font-family: Impact, monospace; }
        .trace-slider { height: 4px; background: #334155; position: relative; margin-bottom: 8px; cursor: pointer; }
        .ts-fill { height: 100%; background: var(--tech-cyan); width: 0%; position: absolute; left: 0; }
        .ts-knob { width: 14px; height: 14px; background: #fff; border-radius: 50%; position: absolute; left: 0%; top: -5px; transform: translateX(-50%); box-shadow: 0 0 5px rgba(0,0,0,0.5); }
        .ts-labels { display: flex; justify-content: space-between; font-size: 10px; color: #64748b; margin-bottom: 15px; font-family: monospace; }
        .trace-controls { display: flex; justify-content: center; gap: 20px; align-items: center; margin-bottom: 15px; }
        .tc-btn { color: #94a3b8; cursor: pointer; font-size: 14px; }
        .tc-play { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--tech-cyan); display: flex; justify-content: center; align-items: center; color: var(--tech-cyan); cursor: pointer; transition: 0.2s; }
        .tc-play:hover { background: var(--tech-cyan); color: #000; }
        .trace-logs { border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px; max-height: 120px; overflow-y: auto; }
        .log-item { font-size: 12px; margin-bottom: 6px; color: #94a3b8; }
        .log-time { color: var(--tech-cyan); font-weight: bold; margin-right: 6px; }

        /* ==================== 5. 右侧面板 (List V6) ==================== */
        .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 12px 10px; border-bottom: 1px solid rgba(255,255,255,0.1); }
        .kpi-box { background: rgba(255,255,255,0.05); border: 1px solid transparent; border-radius: 4px; padding: 8px 0; text-align: center; cursor: pointer; transition: 0.2s; }
        .kpi-box:hover { background: rgba(255,255,255,0.1); }
        .kpi-box.active { background: rgba(0,122,255,0.25); border-color: var(--primary-blue); box-shadow: inset 0 0 10px rgba(0,122,255,0.3); }
        .kpi-val { font-family: Impact; font-size: 20px; display: block; margin-bottom: 2px; color:#fff; }
        .kpi-name { font-size: 11px; color: var(--text-sub); }

        .search-bar { padding: 12px; display: flex; gap: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); }
        .s-select { background: rgba(0,0,0,0.3); color: #fff; border: 1px solid rgba(255,255,255,0.2); padding: 5px; font-size: 12px; border-radius: 4px; }
        .s-input { flex: 1; background: rgba(0,0,0,0.3); color: #fff; border: 1px solid rgba(255,255,255,0.2); padding: 5px 10px; font-size: 12px; border-radius: 4px; }

        /* V6 List Card Improvements */
        .list-card-v6 {
            background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
            border-radius: 6px; margin-bottom: 8px; padding: 12px; cursor: pointer; transition: 0.2s;
        }
        .list-card-v6:hover { background: rgba(255,255,255,0.08); }
        .list-card-v6.active { border-color: var(--tech-cyan); background: rgba(0, 240, 255, 0.08); }
        
        .l6-row { display: flex; align-items: center; margin-bottom: 6px; }
        .l6-avatar { width: 30px; height: 30px; background: #004e92; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 13px; font-weight: bold; margin-right: 10px; border: 1px solid rgba(255,255,255,0.3); flex-shrink: 0; }
        
        /* Name & Identity Section */
        .l6-name-group { flex: 1; display: flex; align-items: center; gap: 8px; overflow: hidden; }
        .l6-name { font-size: 14px; font-weight: bold; color: #fff; white-space: nowrap; }
        .l6-tags-wrapper { display: flex; gap: 4px; overflow: hidden; }
        /* Identity Tag Style (Restored from V3) */
        .l6-id-tag { font-size: 10px; padding: 0 4px; border-radius: 2px; border: 1px solid rgba(255,255,255,0.15); color: #ccc; background: rgba(255,255,255,0.05); white-space: nowrap; }
        
        .l6-status-tag { font-size: 10px; padding: 1px 4px; border-radius: 2px; margin-left: auto; flex-shrink: 0; }
        .tag-online { background: rgba(48, 209, 88, 0.2); color: var(--st-online); }
        .tag-task { background: rgba(255, 159, 10, 0.2); color: var(--st-task); }
        .tag-off { background: rgba(255,255,255,0.1); color: #888; }

        .l6-info-row { font-size: 11px; color: #aaa; display: flex; align-items: center; margin-top: 4px; }
        .l6-icon { width: 14px; text-align: center; margin-right: 4px; }
        .l6-val { color: #ddd; }
        .val-task { color: var(--st-task); }

    </style>
</head>
<body>

    <!-- 1. 左侧详情面板 (Same as V5) -->
    <div class="panel panel-left" id="panelLeft">
        <div class="p-header">
            <div class="header-deco"></div>
            <div class="p-title">协调员详情</div>
            <i class="fas fa-times cursor-pointer text-gray-400 hover:text-white" onclick="closeLeftPanel()"></i>
        </div>
        <div class="p-content">
            <div class="info-card">
                <div class="info-avatar" id="d-avatar">--</div>
                <div class="info-details">
                    <div class="info-name-row"><span class="info-name" id="d-name">--</span><span class="info-gender" id="d-gender">--</span></div>
                    <div class="info-sub"><span><i class="fas fa-phone mr-1"></i><span id="d-phone">--</span></span><span><i class="fas fa-map-marker-alt mr-1"></i><span id="d-region">--</span></span></div>
                </div>
            </div>
            <div class="perf-container">
                <div class="perf-item"><div class="perf-ring" style="border-color:var(--st-online); color:var(--st-online)">95%</div><span class="perf-lbl">及时性</span></div>
                <div class="perf-item"><div class="perf-ring" style="border-color:#00f0ff; color:#00f0ff">100%</div><span class="perf-lbl">合规性</span></div>
                <div class="perf-item"><div class="perf-ring" style="border-color:#ff9f0a; color:#ff9f0a">75%</div><span class="perf-lbl">转化率</span></div>
            </div>
            <div class="section-title">当前任务状态</div>
            <div class="task-card-v4" id="d-task-box"></div>
            <div class="section-title">当前实时位置</div>
            <div class="loc-box"><div class="loc-content"><i class="fas fa-map-marker-alt loc-icon"></i><span id="d-location-text">--</span></div></div>
            <div class="comm-box">
                <div class="comm-screen" id="comm-screen">待机中</div>
                <button class="btn-call" id="btn-call" onclick="toggleCall()"><i class="fas fa-phone-alt"></i> 呼叫协调员</button>
            </div>
            <div class="section-title"><i class="fas fa-map mr-2"></i>轨迹回放</div>
            <div class="trace-card-v4">
                <div class="trace-header"><select class="trace-sel flex-1"><option>今天 (2023-11-27)</option></select><select class="trace-sel w-20"><option>1x</option></select></div>
                <div class="trace-time-display" id="trace-clock">08:00:00</div>
                <div class="trace-slider"><div class="ts-fill" id="ts-fill"></div><div class="ts-knob" id="ts-knob"></div></div>
                <div class="ts-labels"><span>00:00</span><span>12:00</span><span>23:59</span></div>
                <div class="trace-controls"><i class="fas fa-step-backward tc-btn"></i><div class="tc-play" onclick="playTrace()"><i class="fas fa-play" id="play-icon"></i></div><i class="fas fa-step-forward tc-btn"></i></div>
                <div class="trace-logs" id="trace-logs"></div>
            </div>
        </div>
    </div>

    <!-- 2. 中央地图 -->
    <div class="map-viewport" id="mapView">
        <svg class="trajectory-layer" id="trajLayer"><path class="trace-line" d="M 600 400 Q 650 350 700 420 T 800 380" /></svg>
        <div id="marker-container"></div>
    </div>

    <!-- 3. 右侧列表面板 -->
    <div class="panel">
        <div class="p-header"><div class="header-deco"></div><div class="p-title">协调员管理</div></div>
        <div class="kpi-grid">
            <div class="kpi-box active" onclick="filter('all', this)"><span class="kpi-val">12</span><span class="kpi-name">总人数</span></div>
            <div class="kpi-box" onclick="filter('online', this)"><span class="kpi-val" style="color:var(--st-online)">6</span><span class="kpi-name">在线</span></div>
            <div class="kpi-box" onclick="filter('task', this)"><span class="kpi-val" style="color:var(--st-task)">4</span><span class="kpi-name">任务中</span></div>
            <div class="kpi-box" onclick="filter('offline', this)"><span class="kpi-val" style="color:var(--st-offline)">2</span><span class="kpi-name">离线</span></div>
        </div>
        <div class="search-bar"><select class="s-select"><option>区域</option><option>南京</option><option>徐州</option></select><input type="text" class="s-input" placeholder="输入姓名..."></div>
        <div class="p-content" id="list-container"></div>
    </div>

<script>
    // ==================== 数据 (Updated for V6 with Tags) ====================
    const users = [
        {
            id: 'u1', name: '王强', sex: '男', region: '南京', phone: '13812345678',
            tags: ['ICU背景', '资深', '金牌'], // V6: 新增标签数据
            status: 'task',
            pos: { x: 45, y: 40, loc: '南京市鼓楼医院 ICU' },
            task: { type: '器官转运', code: 'T-JS099', start: '10:00', duration: '2h 50m' },
            logs: [{ time: '08:30', msg: '打卡上班 @南京办事处' }, { time: '09:00', msg: '任务开始:器官转运 T-JS099' }]
        },
        {
            id: 'u2', name: '李晓雯', sex: '女', region: '南京', phone: '13900001111',
            tags: ['心理咨询师', '社工'], // V6: 新增标签数据
            status: 'online',
            pos: { x: 50, y: 55, loc: '南京办事处' },
            task: null,
            logs: [{ time: '08:50', msg: '打卡上班 @南京办事处' }]
        },
        {
            id: 'u3', name: '赵刚', sex: '男', region: '徐州', phone: '15066667777',
            tags: ['苏北片区'], // V6: 新增标签数据
            status: 'offline',
            pos: { x: 20, y: 30, loc: '徐州医科大附院 (最后位置)' },
            task: null,
            logs: [{ time: '08:30', msg: '打卡上班 @徐州办事处' }, { time: '17:30', msg: '打卡下班' }]
        }
    ];

    let currentUserId = null;

    function init() {
        renderList('all');
        renderMap();
    }

    // ==================== 右侧列表 (V6: 增加身份标签) ====================
    function renderList(filterType) {
        const container = document.getElementById('list-container');
        container.innerHTML = '';
        
        const filtered = users.filter(u => filterType === 'all' || u.status === filterType);

        filtered.forEach(u => {
            const card = document.createElement('div');
            card.className = `list-card-v6`;
            card.id = `card-${u.id}`;
            card.onclick = () => selectUser(u.id);
            
            // 状态标签
            let statusTagHTML = '';
            if(u.status === 'task') statusTagHTML = '<span class="l6-status-tag tag-task">任务中</span>';
            else if(u.status === 'online') statusTagHTML = '<span class="l6-status-tag tag-online">在线</span>';
            else statusTagHTML = '<span class="l6-status-tag tag-off">离线</span>';

            // 身份标签 (V6 新增)
            const identityTagsHTML = u.tags.map(t => `<span class="l6-id-tag">${t}</span>`).join('');

            // 任务详情
            let taskRowHTML = '';
            if(u.status === 'task') {
                taskRowHTML = `<div class="l6-info-row"><i class="fas fa-briefcase l6-icon val-task"></i> <span class="l6-val val-task">${u.task.type} (${u.task.duration})</span></div>`;
            } else {
                taskRowHTML = `<div class="l6-info-row"><i class="fas fa-coffee l6-icon"></i> <span class="l6-val" style="color:#666">无进行中任务</span></div>`;
            }

            // 位置
            let locRowHTML = `<div class="l6-info-row"><i class="fas fa-map-marker-alt l6-icon"></i> <span class="l6-val">${u.pos.loc}</span></div>`;

            card.innerHTML = `
                <div class="l6-row">
                    <div class="l6-avatar">${u.name.charAt(0)}</div>
                    <div class="l6-name-group">
                        <span class="l6-name">${u.name}</span>
                        <div class="l6-tags-wrapper">${identityTagsHTML}</div>
                    </div>
                    ${statusTagHTML}
                </div>
                <div style="margin-top:8px; padding-top:8px; border-top:1px dashed rgba(255,255,255,0.1)">
                    ${locRowHTML}
                    ${taskRowHTML}
                </div>
            `;
            container.appendChild(card);
        });
    }

    // ==================== 左侧详情与交互 (Same as V5) ====================
    function selectUser(uid) {
        currentUserId = uid;
        const u = users.find(user => user.id === uid);
        document.getElementById('d-avatar').innerText = u.name.charAt(0);
        document.getElementById('d-name').innerText = u.name;
        document.getElementById('d-gender').innerText = u.sex;
        document.getElementById('d-phone').innerText = u.phone;
        document.getElementById('d-region').innerText = u.region;
        const taskBox = document.getElementById('d-task-box');
        if(u.status === 'task') {
            taskBox.innerHTML = `
                <div class="task-row"><span class="tk-label">任务类型:</span><span class="tk-val">${u.task.type}</span></div>
                <div class="task-row"><span class="tk-label">登记编号:</span><span class="tk-val cyan">${u.task.code}</span></div>
                <div class="task-row"><span class="tk-label">开始时间:</span><span class="tk-val">${u.task.start}</span></div>
                <div class="task-row"><span class="tk-label">已持续:</span><span class="tk-val orange">${u.task.duration}</span></div>
            `;
        } else {
            taskBox.innerHTML = `<div class="text-center text-xs text-gray-500 py-4">无进行中任务</div>`;
        }
        document.getElementById('d-location-text').innerText = u.pos.loc;
        const logBox = document.getElementById('trace-logs');
        logBox.innerHTML = u.logs.map(l => `<div class="log-item"><span class="log-time">${l.time}</span> ${l.msg}</div>`).join('');

        document.querySelectorAll('.list-card-v6').forEach(c => c.classList.remove('active'));
        document.getElementById(`card-${uid}`).classList.add('active');
        document.getElementById('panelLeft').classList.add('active');
        document.getElementById('mapView').style.transform = 'scale(1.05)';
        
        resetCall(); stopTrace(); renderMap(uid);
    }

    function closeLeftPanel() {
        document.getElementById('panelLeft').classList.remove('active');
        document.getElementById('mapView').style.transform = 'scale(1)';
        document.querySelectorAll('.active').forEach(e => e.classList.remove('active'));
        renderMap(); resetCall(); stopTrace();
    }

    function filter(type, el) {
        document.querySelectorAll('.kpi-box').forEach(k => k.classList.remove('active'));
        el.classList.add('active');
        renderList(type);
    }

    function renderMap(activeId = null) {
        const con = document.getElementById('marker-container');
        con.innerHTML = '';
        users.forEach(u => {
            const isActive = u.id === activeId ? 'active' : '';
            const el = document.createElement('div');
            el.className = `coord-marker ${u.status} ${isActive}`;
            el.style.left = u.pos.x + '%'; el.style.top = u.pos.y + '%';
            el.onclick = () => selectUser(u.id);
            el.innerHTML = `<div class="coord-avatar-box">${u.name.charAt(0)}</div><div class="coord-label"><div class="coord-dot"></div> ${u.name}</div>`;
            con.appendChild(el);
        });
    }

    // Call Logic
    let callTimer = null; let callSeconds = 0;
    function toggleCall() {
        const btn = document.getElementById('btn-call');
        const screen = document.getElementById('comm-screen');
        if (btn.classList.contains('hangup')) { resetCall(); } else {
            btn.innerHTML = '<i class="fas fa-phone-slash"></i> 取消拨号'; btn.classList.add('hangup');
            screen.innerText = "拨号中..."; screen.style.color = "#00f0ff";
            setTimeout(() => {
                if(btn.classList.contains('hangup')) {
                    screen.style.color = "#30d158"; btn.innerHTML = '<i class="fas fa-phone-slash"></i> 挂断';
                    callSeconds = 0; screen.innerText = "通话中 00:00";
                    callTimer = setInterval(() => {
                        callSeconds++;
                        const m = Math.floor(callSeconds / 60).toString().padStart(2, '0');
                        const s = (callSeconds % 60).toString().padStart(2, '0');
                        screen.innerText = `通话中 ${m}:${s}`;
                    }, 1000);
                }
            }, 2000);
        }
    }
    function resetCall() {
        clearInterval(callTimer);
        const btn = document.getElementById('btn-call'); const screen = document.getElementById('comm-screen');
        btn.classList.remove('hangup'); btn.innerHTML = '<i class="fas fa-phone-alt"></i> 呼叫协调员';
        screen.innerText = "待机中"; screen.style.color = "#888";
    }

    // Trace Logic
    let traceAnim = null; let isPlaying = false; let progress = 0;
    function playTrace() {
        const icon = document.getElementById('play-icon'); const layer = document.getElementById('trajLayer');
        if (isPlaying) { stopTrace(); } else {
            isPlaying = true; icon.className = "fas fa-pause"; layer.classList.add('active');
            traceAnim = setInterval(() => {
                progress += 0.5; if (progress > 100) progress = 0;
                updateTraceUI(progress);
            }, 50);
        }
    }
    function stopTrace() {
        isPlaying = false; clearInterval(traceAnim);
        const icon = document.getElementById('play-icon'); if(icon) icon.className = "fas fa-play";
        document.getElementById('trajLayer').classList.remove('active');
    }
    function updateTraceUI(pct) {
        document.getElementById('ts-fill').style.width = pct + '%'; document.getElementById('ts-knob').style.left = pct + '%';
        const totalMins = 960 * (pct / 100);
        const h = 8 + Math.floor(totalMins / 60); const m = Math.floor(totalMins % 60);
        document.getElementById('trace-clock').innerText = `${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:00`;
    }

    init();
</script>
</body>
</html>
        
编辑器加载中
预览
控制台