/** * 模拟 API 接口层(动态数据版) * * 特性: * - 数据来源:mock_data.json(由 generate_mock_data.py 从真实 XLS 路口数据生成) * - 静态资源(视频/图片)在此处统一 import,接口直接返回完整数据 * - 支持分页、筛选、排序等动态查询 * - 每次请求信号倒计时、在线率、告警时间等实时数据会动态变化 * - 路口状态会随机波动,模拟真实监控场景 * * 使用:import { apiLogin, apiGetPoints, ... } from '@/pyscripts/api' * 所有接口返回 Promise<{ code, message, data }> */ import mockData from './mock_data.json' // ── 静态资源(模拟 CDN / 后端返回的资源 URL)───────────────────── import video1 from '@/assets/videos/video1.mp4' import video2 from '@/assets/videos/video2.mp4' import video3 from '@/assets/videos/video3.mp4' import video4 from '@/assets/videos/video4.mp4' import arrow1 from '@/assets/images/arrow_1.png' import arrow2 from '@/assets/images/arrow_2.png' import arrow3 from '@/assets/images/arrow_3.png' import arrow4 from '@/assets/images/arrow_4.png' const VIDEOS = [video1, video2, video3, video4] const ARROWS = [arrow1, arrow2, arrow3, arrow4] function pickVideo(i) { return VIDEOS[i % VIDEOS.length] } // ── 工具 ───────────────────────────────────────────────────────────── function sleep(ms) { return new Promise(r => setTimeout(r, ms)) } function delay(base = 200) { return sleep(base + Math.floor(Math.random() * 200)) } function ok(data) { return { code: 200, message: 'success', data } } function fail(msg, code = 400) { return { code, message: msg, data: null } } /** 基于当前秒数产生稳定随机(同一秒内多次调用返回相同值) */ function seededRand(seed) { const x = Math.sin(seed) * 10000 return x - Math.floor(x) } /** 当前时间 HH:MM:SS */ function nowTime() { return new Date().toLocaleTimeString() } function nowDate() { return new Date().toLocaleDateString() } // ── 数据缓存(首次 import 时加载) ────────────────────────────────── const DB = { points: mockData.points, menuTree: mockData.menuTree, tongzhouMenuTree: mockData.tongzhouMenuTree, trunkLineMenuTree: mockData.trunkLineMenuTree, homeData: mockData.homeData, deviceStatus: mockData.deviceStatus, timeSpaceData: mockData.timeSpaceData, crossingList: mockData.crossingList, securityRoutes: mockData.securityRoutes, securityTasks: mockData.securityTasks, filterOptions: mockData.filterOptions, signalTimings: mockData.sampleSignalTimings, intersectionConfigs: mockData.sampleIntersectionConfigs, } // ── 内部生成器 ─────────────────────────────────────────────────────── /** * 生成摄像机模拟数据(基于 XLS 摄像机 Sheet 字段结构) * 字段:路口名、路口编号、摄像机编号、登录名称、摄像头密码、摄像头类型、端口号、IP地址、是否启用、位置 */ function _makeCameras(id, name, seed) { const positions = ['北进口', '南进口', '东进口', '西进口'] const types = ['枪机', '球机'] const numCams = 2 + (seed % 3) // 2~4 个 return Array.from({ length: numCams }, (_, i) => ({ intersection: name || id, intersectionId: id, cameraId: `CAM${(id || '000').slice(-6)}_${String(i + 1).padStart(2, '0')}`, loginName: `admin_${String(seed + i).slice(-4)}`, password: '******', cameraType: types[(seed + i) % types.length], port: 554 + i, ip: `192.168.${10 + (seed % 200)}.${100 + i}`, enabled: (seed + i) % 20 !== 0, // ~5% 禁用 position: positions[i % positions.length], })) } /** 从摄像机列表推导各方向摄像头类型 (1枪机 2球机 0无) */ function _camerasToArmTypes(cameras) { const posMap = { '北进口': 'N', '南进口': 'S', '东进口': 'E', '西进口': 'W' } const typeMap = { '枪机': 1, '球机': 2 } const result = { N: 0, S: 0, E: 0, W: 0 } cameras.forEach(c => { if (c.enabled) { const dir = posMap[c.position] if (dir) result[dir] = typeMap[c.cameraType] || 0 } }) return result } function _makeIntersectionConfig(id, name, { fixedNsGreen } = {}) { const phases = ['南北直行', '东西直行', '北单放', '东单放'] const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false const countdown = 10 + Math.floor(Math.random() * 50) const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0 const cameras = _makeCameras(id, name, seed) const armCamTypes = _camerasToArmTypes(cameras) const lanePresets = [ ['U', 'L', 'S', 'R'], [null, 'L', 'S', 'R'], [null, 'L', 'S', null], ['U', 'L', 'S', null], ].sort(() => seededRand(seed) - 0.5) return { signals: { ns: { phaseName: phases[0], time: countdown, isGreen: nsGreen }, ew: { phaseName: phases[1], time: countdown, isGreen: !nsGreen }, }, armsConfig: { N: { cameraType: armCamTypes.N, lanes: lanePresets[0] }, S: { cameraType: armCamTypes.S, lanes: lanePresets[1] }, E: { cameraType: armCamTypes.E, lanes: lanePresets[2] }, W: { cameraType: armCamTypes.W, lanes: lanePresets[3] }, }, cameras, } } /** * 动态生成路口相位配时数据 * @param {number} cycleLength 周期总时长 * @param {boolean} isTwoRows 是否生成上下双排 8 相位 (默认 true) */ function _makePhaseData(cycleLength = 140, isTwoRows = true) { const n = 4; // 4个阶段 (S1-S4) const stageTime = Math.floor(cycleLength / n); const pd = []; // ========================================== // 修改点:将单个图标改为用逗号分隔的"成对图标"字符串 // 前端组件会按逗号切割并分别放到对角位置 // ========================================== const iconsUD = [ 'STRAIGHT_DOWN,STRAIGHT_UP', // 南北直行对放 'TURN_DOWN_LEFT,TURN_UP_LEFT', // 南北左转对放 'TURN_DOWN_LEFT_UTURN,TURN_UP_LEFT_UTURN' // 南北左转+掉头对放 ]; const iconsLR = [ 'STRAIGHT_LEFT,STRAIGHT_RIGHT', // 东西直行对放 'TURN_LEFT_DOWN,TURN_RIGHT_UP', // 东西左转对放 'TURN_LEFT_DOWN_UTURN,TURN_RIGHT_UP_UTURN' // 东西左转+掉头对放 ]; const getRandomIcon = (pool) => pool[Math.floor(Math.random() * pool.length)]; let t = 0; for (let i = 0; i < n; i++) { const stageStart = t; const stageEnd = stageStart + stageTime; const currentIconPool = (i < 2) ? iconsUD : iconsLR; // 辅助函数:生成单条轨道的一个阶段 const pushTrackData = (trackIdx, phaseNamePrefix) => { // 这里的 icon 现在抽出来的是诸如 "STRAIGHT_DOWN,STRAIGHT_UP" 的字符串 const icon = getRandomIcon(currentIconPool); const phaseName = `${phaseNamePrefix}${i + 1}`; const g = Math.floor(Math.random() * 11) + 20; // 绿灯 20-30s const s = 3; // 闪烁/条纹 3s const y = 2; // 黄灯 2s let curT = stageStart; // 1. 绿灯 (第6个索引项传入组装好的成对 icon 字符串) pd.push([trackIdx, curT, curT + g, phaseName, g, 'green', icon]); curT += g; // 2. 绿闪/条纹 pd.push([trackIdx, curT, curT + s, '', s, 'stripe', null]); curT += s; // 3. 黄灯 pd.push([trackIdx, curT, curT + y, '', y, 'yellow', null]); curT += y; // 4. 红灯补齐 (确保阶段对齐) let remainRed = stageEnd - curT; if (remainRed > 0) { pd.push([trackIdx, curT, stageEnd, '', remainRed, 'red', null]); } }; pushTrackData(0, 'P'); // 生成第一排 (P1-P4) if (isTwoRows) { pushTrackData(1, 'P'); // 生成第二排 (P5-P8,由于逻辑相同,名称可根据需要改为 i+5) } t = stageEnd; } return pd; } function _makeCornerVideos() { return { nw: video1, ne: video2, sw: video3, se: video4 } } function _makeStageList() { return [1, 2, 3, 4].map((_, i) => ({ value: String(i + 1), time: [30, 30, 50, 30][i], img: ARROWS[i], })) } function _makeCardPhases(activeIndex = 0) { return ARROWS.map((img, i) => ({ id: i + 1, icon: ['↑', '↰', '↑', '↰'][i], img, active: i === activeIndex, })) } /** 动态波动整数值(基于基准值上下浮动) */ function _fluctuate(base, range) { return base + Math.floor(Math.random() * range * 2) - range } /** 动态更新路口状态(每次调用随机波动少量路口) */ function _dynamicPoints() { return DB.points.map((p, i) => { const r = seededRand(i + 31) const status = r < 0.78 ? 'normal' : r < 0.92 ? 'busy' : 'alarm' return { ...p, status, updatedAt: Date.now() - Math.floor(r * 120000) } }) } // ═══════════════════════════════════════════════════════════════════════ // E: 认证 // ═══════════════════════════════════════════════════════════════════════ /** 验证码状态(模拟服务端 session 存储) */ let _captchaStore = { code: '', expireAt: 0 } /** * GET /api/auth/captcha — 获取验证码 * 返回 4 位随机字符 + base64 图片(Canvas 绘制) * 真实后端应返回图片流,这里模拟返回验证码文本 + 配置,由前端 Canvas 绘制 */ export async function apiGetCaptcha() { await delay(100) const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' const code = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('') // 存储验证码,有效期 60 秒 _captchaStore = { code: code.toUpperCase(), expireAt: Date.now() + 60000 } return ok({ captchaId: 'cap_' + Date.now(), code, // 模拟环境直接返回明文,真实环境不应返回 length: 4, expireIn: 60, // 秒 }) } /** * POST /api/auth/login — 登录(含验证码校验) */ export async function apiLogin({ username, password, captcha }) { await delay(300) // 验证码校验 if (_captchaStore.code && captcha) { if (Date.now() > _captchaStore.expireAt) { return fail('验证码已过期,请刷新', 401) } if (captcha.toUpperCase() !== _captchaStore.code) { return fail('验证码错误', 401) } } // 账号密码校验 if (username === 'admin' && password === '123456') { _captchaStore = { code: '', expireAt: 0 } // 登录成功后清除验证码 return ok({ token: 'demo_token_' + Date.now(), user: { name: 'admin', role: '管理员' } }) } return fail('账号或密码错误(演示账号:admin / 123456)', 401) } export async function apiChangePassword({ oldPassword, newPassword }) { await delay(300) if (oldPassword === '123456') return ok({ message: '密码修改成功' }) return fail('原密码错误') } // ═══════════════════════════════════════════════════════════════════════ // A: 路口基础数据 // ═══════════════════════════════════════════════════════════════════════ /** * GET /api/intersections — 路口点位列表 * 每次调用路口状态会动态波动 */ export async function apiGetPoints(filters = {}) { await delay(200) let list = _dynamicPoints() if (filters.node) list = list.filter(p => p.node === filters.node) if (filters.status) list = list.filter(p => p.status === filters.status) if (filters.keyword) { const kw = filters.keyword.toLowerCase() list = list.filter(p => p.name.toLowerCase().includes(kw) || p.id.toLowerCase().includes(kw)) } return ok(list) } /** * GET /api/intersections/:id — 路口详情 * 信号倒计时每次请求动态变化 */ export async function apiGetIntersectionData(id, { fixedNsGreen } = {}) { await delay(250) const base = DB.intersectionConfigs[id] const point = DB.points.find(p => p.id === id) const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0 // 动态倒计时:基于当前秒数计算 const nowSec = Math.floor(Date.now() / 1000) const cycle = 140 const elapsed = nowSec % cycle const nsGreenVal = fixedNsGreen !== undefined ? fixedNsGreen : false const nsGreen = nsGreenVal const config = base ? { signals: { ns: { ...base.signals.ns, time: Math.max(1, cycle - elapsed), isGreen: nsGreen }, ew: { ...base.signals.ew, time: elapsed || 1, isGreen: !nsGreen }, }, armsConfig: base.armsConfig, cameras: base.cameras, } : _makeIntersectionConfig(id, point ? point.name : id, { fixedNsGreen: nsGreenVal }) return ok({ ...config, id, name: point ? point.name : id, mainVideo: pickVideo(seed), cornerVideos: _makeCornerVideos(seed), }) } /** * GET /api/intersections/:id/signal-timing — 信号配时 * currentTime 随真实时间走动 */ export async function apiGetSignalTiming(id) { await delay(300) const preset = DB.signalTimings[id] if (preset) { const cycleLength = preset.data.cycleLength return { code: 200, message: 'success', data: { ...preset.data, currentTime: Math.floor(Date.now() / 1000) % cycleLength } } } const cycleLength = [100, 120, 130, 140, 150, 160][Math.floor(Math.random() * 6)] return ok({ cycleLength, currentTime: Math.floor(Date.now() / 1000) % cycleLength, phaseData: _makePhaseData(cycleLength, false), }) } /** GET /api/intersections/:id/stages */ export async function apiGetIntersectionStages(id) { await delay(200) const timing = DB.signalTimings[id] if (timing) { const hasTrack1 = timing.data.phaseData.some(p => p[0] === 1) const phases = timing.data.phaseData.filter(p => p[0] === (hasTrack1 ? 1 : 0) && p[4] !== null) return ok(phases.map((p, i) => ({ value: String(i + 1), time: p[4], phaseName: p[3], direction: p[6], img: ARROWS[i % ARROWS.length], }))) } return ok(_makeStageList()) } /** GET /api/intersections/:id/schemes */ export async function apiGetSchemes(id) { await delay(150) return ok(DB.filterOptions.schemeOptions) } // ═══════════════════════════════════════════════════════════════════════ // A4: 区域菜单树 // ═══════════════════════════════════════════════════════════════════════ export async function apiGetMenuTree(tabId = 'arterial') { await delay(250) return ok(tabId === 'arterial' ? DB.menuTree : DB.tongzhouMenuTree) } export async function apiGetTongzhouMenuTree() { await delay(250) return ok(DB.tongzhouMenuTree) } export async function apiGetTrunkLineMenuTree() { await delay(200) return ok(DB.trunkLineMenuTree) } // ═══════════════════════════════════════════════════════════════════════ // B: 设备状态 & 首页(动态波动) // ═══════════════════════════════════════════════════════════════════════ /** * GET /api/devices/status/summary * 在线数每次请求轻微波动 */ export async function apiGetDeviceStatus(type) { await delay(200) function fluctuateStats(base) { const online = base.chartData[0].value const total = online + base.chartData[1].value const newOnline = _fluctuate(online, Math.ceil(total * 0.02)) const clamped = Math.max(0, Math.min(total, newOnline)) const rate = Math.round(clamped / total * 100) return { centerTitle: rate + '%', centerSubTitle: `${clamped}/${total}`, chartData: [ { ...base.chartData[0], value: clamped }, { ...base.chartData[1], value: total - clamped }, ] } } if (type && DB.deviceStatus[type]) return ok(fluctuateStats(DB.deviceStatus[type])) return ok({ signalMachine: fluctuateStats(DB.deviceStatus.signalMachine), detector: fluctuateStats(DB.deviceStatus.detector), camera: fluctuateStats(DB.deviceStatus.camera), }) } /** * GET /api/home/snapshot — 首页快照 * 在线数波动 + 时间实时更新 + 告警时间刷新 */ export async function apiGetHomeSnapshot() { await delay(200) const total = DB.homeData.online.total const online = _fluctuate(Math.round(total * 0.93), 30) const clamped = Math.max(0, Math.min(total, online)) const fault = Math.floor(Math.random() * 5) return ok({ header: { ...DB.homeData.header, timeText: nowTime(), dateText: nowDate() }, online: { online: clamped, offline: total - clamped, total, rate: Math.round(clamped / total * 100) }, alarms: DB.homeData.alarms.map(a => ({ ...a, time: new Date(Date.now() - Math.floor(Math.random() * 3600000)).toLocaleTimeString(), })), duty: DB.homeData.duty, device: { normal: total - fault, fault }, controlModes: DB.homeData.controlModes, keyIntersections: DB.homeData.keyIntersections, }) } /** GET /api/home/control-mode-stats — 控制模式分布(轻微波动) */ export async function apiGetControlModeStats() { await delay(150) return ok(DB.homeData.controlModes.map(m => ({ ...m, value: _fluctuate(m.value, Math.ceil(m.value * 0.05)), }))) } /** * GET /api/alarms/latest — 告警列表(分页 + 动态时间) * @param {{ page?: number, pageSize?: number, level?: string }} params */ export async function apiGetLatestAlarms(params = {}) { await delay(200) const typeMap = { high: 'error', mid: 'warning', low: 'warning' } let alarms = DB.homeData.alarmList.map((a, i) => ({ id: a.id, title: a.title, type: a.type || typeMap[a.level] || 'warning', time: new Date(Date.now() - i * 180000 - Math.floor(Math.random() * 60000)).toLocaleTimeString(), description: a.description || `${a.loc}-${a.title}`, position: a.position, level: a.level, loc: a.loc, })) if (params.level) alarms = alarms.filter(a => a.level === params.level) const page = params.page || 1 const pageSize = params.pageSize || 10 const start = (page - 1) * pageSize return ok({ total: alarms.length, page, pageSize, list: alarms.slice(start, start + pageSize) }) } // ═══════════════════════════════════════════════════════════════════════ // C: 勤务 & 任务(分页 + 筛选) // ═══════════════════════════════════════════════════════════════════════ /** * GET /api/tasks — 勤务任务列表 * 支持 page / pageSize / status / keyword 筛选 */ export async function apiGetTasks(params = {}) { await delay(200) let list = [...DB.securityTasks] if (params.status) list = list.filter(t => t.status === params.status) if (params.keyword) { const kw = params.keyword.toLowerCase() list = list.filter(t => t.name.toLowerCase().includes(kw) || t.executor.toLowerCase().includes(kw)) } if (params.level) list = list.filter(t => t.level === params.level) const page = params.page || 1 const pageSize = params.pageSize || 5 const total = list.length const totalPages = Math.ceil(total / pageSize) const start = (page - 1) * pageSize return ok({ total, page, pageSize, totalPages, list: list.slice(start, start + pageSize), }) } /** * GET /api/security-routes — 勤务路线(含视频) */ export async function apiGetSecurityRoutes() { await delay(200) return ok(DB.securityRoutes.map((r, i) => ({ ...r, mainVideo: pickVideo(i), cornerVideos: _makeCornerVideos(i), }))) } /** GET /api/security-routes/:id */ export async function apiGetSecurityRouteDetail(id) { await delay(200) const idx = DB.securityRoutes.findIndex(r => r.id === id) const route = DB.securityRoutes[idx >= 0 ? idx : 0] if (!route) return fail('路线不存在', 404) return ok({ ...route, mainVideo: pickVideo(idx >= 0 ? idx : 0), cornerVideos: _makeCornerVideos(idx >= 0 ? idx : 0) }) } /** GET /api/key-intersections */ export async function apiGetKeyIntersections() { await delay(150) return ok(DB.homeData.keyIntersections) } // ═══════════════════════════════════════════════════════════════════════ // D: 交通时空图 // ═══════════════════════════════════════════════════════════════════════ export async function apiGetTrafficTimeSpace(opts = {}) { await delay(300) const { speed = 15, cycle = 120, band = 40, totalTime = 1800 } = opts const intersections = opts.intersections || DB.timeSpaceData.intersections const rawDistances = opts.distances || DB.timeSpaceData.distances // 将不均匀的物理距离归一化为等间距,保证绿波带视觉对齐 const step = 500 const distances = rawDistances.map((_, i) => i * step) const maxDist = distances[distances.length - 1] const waveData = [], greenData = [] for (let t = 0; t <= totalTime; t += cycle) { const upKmh = 45 + Math.random() * 10 // 45-55 km/h 随机 const downKmh = 45 + Math.random() * 10 const upSpd = upKmh / 3.6 // 转 m/s const downSpd = downKmh / 3.6 const ds = t + cycle / 2 waveData.push({ yBottom: 0, yTop: maxDist, xBL: t, xBR: t + band, xTL: t + maxDist / upSpd, xTR: t + maxDist / upSpd + band, label: Math.round(upKmh) + 'km/h', direction: 'up', speed: Math.round(upKmh) }) waveData.push({ yBottom: maxDist, yTop: 0, xBL: ds, xBR: ds + band, xTL: ds + maxDist / downSpd, xTR: ds + maxDist / downSpd + band, label: Math.round(downKmh) + 'km/h', direction: 'down', speed: Math.round(downKmh) }) distances.forEach(y => { greenData.push({ y, start: t + y / upSpd, end: t + y / upSpd + band }) greenData.push({ y, start: ds + (maxDist - y) / downSpd, end: ds + (maxDist - y) / downSpd + band }) }) } return ok({ intersections, distances, waveData, greenData }) } // ═══════════════════════════════════════════════════════════════════════ // 路口列表表格(分页 + 筛选 + 排序 + 动态状态) // ═══════════════════════════════════════════════════════════════════════ /** * GET /api/crossings — 路口列表(718条全量,支持翻页) * @param {{ keyword, subArea, status, node, isKey, page, pageSize, sortBy, sortOrder }} params */ export async function apiGetCrossingList(params = {}) { await delay(250) // 动态状态:每次请求路口状态会变化 const statuses = ['在线', '在线', '在线', '在线', '离线'] // 基于页码生成页级起始偏移(同一页固定,不同页不同) const page = params.page || 1 const pageOffset = Math.floor(seededRand(page * 97) * 120) let list = DB.crossingList.map((r, i) => { const preset = DB.signalTimings[r.id] const cycleLength = preset ? preset.data.cycleLength : r.cycle // const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false) // 强制全部用 _makePhaseData 动态生成成对箭头 const phaseData = _makePhaseData(cycleLength, false) return { ...r, status: statuses[Math.floor(seededRand(i + 42) * statuses.length)], cycle: cycleLength, phaseData, currentTime: Math.floor(seededRand(i * 31 + page * 97) * cycleLength), } }) // 筛选(兼容中英文值映射) if (params.keyword || params.name) { const kw = (params.keyword || params.name).toLowerCase() list = list.filter(r => r.name.toLowerCase().includes(kw) || r.id.toLowerCase().includes(kw)) } if (params.subArea) list = list.filter(r => r.subArea === params.subArea) if (params.status) { const statusMap = { 'online': '在线', 'offline': '离线', 'fault': '故障' } const mapped = statusMap[params.status] || params.status list = list.filter(r => r.status === mapped) } if (params.node) list = list.filter(r => r.node === params.node) if (params.isKey !== undefined && params.isKey !== '') { const boolVal = params.isKey === 'yes' || params.isKey === true list = list.filter(r => r.isKey === boolVal) } if (params.timeOffset) { if (params.timeOffset === 'none' || params.timeOffset === '无偏差') { list = list.filter(r => r.timeOffset === '无偏差') } else { list = list.filter(r => r.timeOffset !== '无偏差') } } // 排序 if (params.sortBy) { const key = params.sortBy const dir = params.sortOrder === 'desc' ? -1 : 1 list.sort((a, b) => { if (a[key] < b[key]) return -1 * dir if (a[key] > b[key]) return 1 * dir return 0 }) } // 分页 const pageSize = params.pageSize || 10 const total = list.length const totalPages = Math.ceil(total / pageSize) const start = (page - 1) * pageSize return ok({ total, page, pageSize, totalPages, list: list.slice(start, start + pageSize), }) } /** GET /api/dict/:type */ export async function apiGetDictOptions(type) { await delay(100) if (DB.filterOptions[type]) return ok(DB.filterOptions[type]) return fail('未知字典类型: ' + type, 404) } // ═══════════════════════════════════════════════════════════════════════ // F: 设备操作 // ═══════════════════════════════════════════════════════════════════════ export async function apiRestartDevice(id) { await delay(500) return ok({ message: `设备 ${id} 重启指令已下发` }) } export async function apiUpgradeDevice(id, file) { await delay(800) return ok({ message: `设备 ${id} 升级任务已创建`, version: 'V3.3.0' }) } // ═══════════════════════════════════════════════════════════════════════ // H: 弹窗专用(动态倒计时 + 实时状态) // ═══════════════════════════════════════════════════════════════════════ /** * GET /api/special-task/:id/monitor — 特勤监控面板 * 信号灯倒计时随真实时间变化 */ export async function apiGetSpecialTaskMonitorData(id, { fixedNsGreen } = {}) { await delay(400) // 用 id 生成稳定 seed,兼容数字/字符串/undefined let numId = typeof id === 'number' ? id : parseInt(id) if (!numId || isNaN(numId)) { // 非数字 id(如字符串),用 charCode 求和 numId = id ? Array.from(String(id)).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 1 } const seed = Math.abs(numId * 7) || 7 // 任务信息——不同任务不同数据 const taskIdx = Math.abs(numId - 1) % (DB.securityTasks.length || 1) const task = DB.securityTasks.find(t => t.id === id || t.id === numId) || DB.securityTasks[taskIdx] || {} const timeSlots = ['07:30-09:30', '09:00-11:00', '12:00-14:00', '14:00-16:00', '17:00-19:00', '19:00-21:00'] const statusColorMap = { '未开始': '#ffaa00', '进行中': '#ff4d4f', '已完成': '#8dc453', } const taskStatus = task.status || '未开始' const taskInfo = { name: task.name || '特勤路线', time: timeSlots[seed % timeSlots.length], manager: task.executor || DB.securityTasks[seed % DB.securityTasks.length]?.executor || '王建国', level: task.level || (seed % 3 === 0 ? '二级' : '一级'), status: taskStatus, statusColor: statusColorMap[taskStatus] || '#ffaa00', } // 根据任务 id 选取不同的关键路口(每个任务关联不同的4个路口) const allKeyPoints = DB.points.filter(p => p.isKey) const startIdx = (seed * 3) % Math.max(allKeyPoints.length - 4, 1) const keyPoints = allKeyPoints.slice(startIdx, startIdx + 4) // 视频 const videos = keyPoints.map((_, i) => ({ id: i + 1, url: pickVideo(seed + i) })) // 每个路口的控制模式和状态颜色根据路口 id 变化 const modeList = ['步进', '系统', '定周期', '感应', '手动'] const colorList = ['#ffaa00', '#00e5ff', '#68e75f', '#00e5ff'] const nowSec = Math.floor(Date.now() / 1000) const lanePresets = [ { N: ['U', 'L', 'S', 'R'], S: [null, 'L', 'S', 'R'], E: [null, 'L', 'S', null], W: ['U', 'L', 'S', null] }, { N: ['L', 'S', 'R'], S: ['L', 'S', 'R'], E: ['L', 'S', 'R'], W: ['L', 'S', 'R'] }, { N: ['L', 'S', 'S', 'R'], S: ['U', 'L', 'S', 'R'], E: ['L', 'S', null], W: [null, 'S', 'R'] }, { N: [null, 'L', 'S', 'R'], S: ['L', 'S', 'R', null], E: ['U', 'L', 'S', 'R'], W: ['L', 'S', 'R'] }, ] const intersections = keyPoints.map((jnc, i) => { const jncSeed = Array.from(jnc.id).reduce((s, c, idx) => s + c.charCodeAt(0) * (idx + 1), 0) const cycle = [100, 120, 130, 140, 150][jncSeed % 5] const elapsed = nowSec % cycle const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false const countdown = Math.max(1, cycle - elapsed) const laneSet = lanePresets[jncSeed % lanePresets.length] // 用摄像机字段结构生成,再推导 cameraType const cameras = _makeCameras(jnc.id, jnc.name, jncSeed) const armCamTypes = _camerasToArmTypes(cameras) return { id: jnc.id, name: jnc.name, statusColor: colorList[jncSeed % colorList.length], stage: (Math.floor(elapsed / (cycle / 4)) % 4) + 1, mode: modeList[jncSeed % modeList.length], timeLeft: countdown, btnText: i === 0 ? '立即解锁' : '立即执行', btnType: i === 0 ? 'normal' : 'primary', phases: _makeCardPhases(Math.floor(elapsed / (cycle / 4)) % 4), cameras, mapData: { armsConfig: { N: { lanes: laneSet.N, cameraType: armCamTypes.N }, S: { lanes: laneSet.S, cameraType: armCamTypes.S }, E: { lanes: laneSet.E, cameraType: armCamTypes.E }, W: { lanes: laneSet.W, cameraType: armCamTypes.W }, }, signals: { ns: { isGreen: nsGreen, time: countdown, phaseName: '南北直行' }, ew: { isGreen: !nsGreen, time: Math.max(1, cycle - countdown), phaseName: '东西直行' }, }, }, videoUrls: _makeCornerVideos(seed + i), } }) return ok({ taskInfo, videos, intersections }) } /** * GET /api/crossing/panel/:id — CrossingPanel 弹窗 * 倒计时实时变化 */ export async function apiGetCrossingPanelData(id) { await delay(300) const point = DB.points.find(p => p.id === id) const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false }) const seed = id ? id.charCodeAt(id.length - 1) : 0 const preset = DB.signalTimings[id] const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][Math.abs(seed) % 6] const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false) const currentTime = Math.floor(Date.now() / 1000) % cycleLength return ok({ currentRoute: { id, name: point ? point.name : id, level: point?.isKey ? '一级' : '二级', mode: '快进', time: cycleLength + 's', mainVideo: pickVideo(seed), cornerVideos: _makeCornerVideos(seed), }, intersectionData: config, phaseData, cycleLength, currentTime, }) } /** * GET /api/crossing/detail/:id — CrossingDetailPanel 弹窗 */ export async function apiGetCrossingDetailData(id) { await delay(350) const point = DB.points.find(p => p.id === id) const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false }) // 用 id 的全部字符生成稳定 seed(加权位置避免 charCode 总和碰撞) const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0 // 从真实阶段数据推导周期和相位 const preset = DB.signalTimings[id] const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][seed % 6] const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength || 140, false) // 从相位数据中提取阶段列表(优先上轨道绿灯相位,单轨道时取 track 0,最多4个) const hasTrack1 = phaseData.some(p => p[0] === 1) const greenPhases = phaseData.filter(p => p[0] === (hasTrack1 ? 1 : 0) && p[4] !== null).slice(0, 4) const stageList = greenPhases.map((p, i) => ({ value: String(i + 1), time: p[4], phaseName: p[3], direction: p[6], img: ARROWS[i], })) // 控制方式选项 + 根据路口选择不同的当前控制方式 const allMethods = [ { label: '定周期', value: 'fixed' }, { label: '黄闪', value: 'yellow_flash' }, { label: '关灯', value: 'lights_off' }, { label: '步进', value: 'step' }, { label: '系统方案', value: 'system' }, { label: '感应控制', value: 'sensor' }, { label: '临时方案', value: 'temp' }, ] const methodValues = ['fixed', 'step', 'system', 'sensor', 'temp'] const currentMethod = methodValues[seed % methodValues.length] // 控制模式(显示用) const controlModes = ['定周期控制', '感应控制', '干线协调', '自适应控制'] const currentMode = controlModes[seed % controlModes.length] // 控制方案:根据当前控制方式给出不同方案选项 const schemeMap = { 'fixed': [ { label: '早高峰', value: 'early_peak' }, { label: '晚高峰', value: 'evening_peak' }, { label: '平峰', value: 'normal' }, { label: '夜间', value: 'night' }, { label: '周末', value: 'weekend' }, ], 'sensor': [ { label: '全感应', value: 'full_actuated' }, { label: '半感应', value: 'semi_actuated' }, ], 'system': [ { label: '系统优化方案A', value: 'sys_a' }, { label: '系统优化方案B', value: 'sys_b' }, { label: '系统优化方案C', value: 'sys_c' }, ], 'step': [ { label: '步进方案1', value: 'step_1' }, { label: '步进方案2', value: 'step_2' }, ], 'temp': [ { label: '临时方案A', value: 'temp_a' }, { label: '临时方案B', value: 'temp_b' }, { label: '临时方案C', value: 'temp_c' }, ], 'yellow_flash': [{ label: '黄闪默认', value: 'yf_default' }], 'lights_off': [{ label: '关灯默认', value: 'lo_default' }], } const schemeOptions = schemeMap[currentMethod] || schemeMap['fixed'] // 相位差和协调时间基于 seed 稳定变化 const phaseDiff = (seed * 7) % 25 const coordTime = (seed * 13) % 60 return ok({ currentRoute: { id, name: point ? point.name : id, level: point?.isKey ? '一级' : '二级', mode: currentMode, time: cycleLength + 's', mainVideo: pickVideo(seed), cornerVideos: _makeCornerVideos(seed), }, intersectionData: config, phaseData, cycleLength, currentTime: Math.floor(Date.now() / 1000) % cycleLength, phaseDiff, coordTime, stageList, schemeOptions, currentScheme: schemeOptions[0].value, controlMode: currentMode, controlMethodOptions: allMethods, currentMethod, locktimeOptions: [ { label: '30', value: 30 }, { label: '50', value: 50 }, { label: '100', value: 100 }, { label: '300', value: 300 }, ], }) } /** * GET /api/crossing/top-charts — 路口Tab顶部圆环图(动态波动) */ export async function apiGetCrossingTopCharts() { await delay(150) const sm = DB.deviceStatus.signalMachine const baseOnline = sm.chartData[0].value const total = baseOnline + sm.chartData[1].value const online = _fluctuate(baseOnline, Math.ceil(total * 0.02)) const onlineChart = { chartData: [ { name: '在线', value: online, color: '#4DF5F8' }, { name: '离线', value: total - online, color: '#FFD369' }, ], centerTitle: Math.round(online / total * 100) + '%', centerSubTitle: `${online}/${total}`, } const faultTotal = total - online const comm = Math.floor(faultTotal * 0.26) const det = Math.floor(faultTotal * 0.21) const lamp = Math.floor(faultTotal * 0.38) const conflict = faultTotal - comm - det - lamp const faultChart = { chartData: [ { name: '通信', value: comm, color: '#4DF5F8' }, { name: '检测器', value: det, color: '#FFA033' }, { name: '灯控', value: lamp, color: '#FFF587' }, { name: '冲突', value: conflict, color: '#FF4D4F' }, ], centerTitle: Math.round(faultTotal / total * 100) + '%', centerSubTitle: `${faultTotal}/${total}`, } return ok({ onlineChart, faultChart }) } /** GET /api/overview/top-charts — 总览Tab顶部图表(动态) */ export async function apiGetOverviewTopCharts() { await delay(150) const res = await apiGetDeviceStatus() return ok({ onlineStatus: res.data, deviceStatus: res.data }) } /** * GET /api/map/legend-config — 地图标注线路配置 */ export async function apiGetMapLegendConfig() { await delay(150) const tzPoints = DB.points.filter(p => p.node && p.node.includes('通州')) const lngs = tzPoints.map(p => p.lng), lats = tzPoints.map(p => p.lat) const [minLng, maxLng] = [Math.min(...lngs), Math.max(...lngs)] const [minLat, maxLat] = [Math.min(...lats), Math.max(...lats)] const midLat = (minLat + maxLat) / 2 const hStep = (maxLat - minLat) / 5, vStep = (maxLng - minLng) / 8 return ok([ { name: '中心计划', start: [minLng, midLat + hStep], end: [maxLng, midLat + hStep], color: '#004CDE' }, { name: '干线协调', start: [minLng, midLat], end: [maxLng, midLat], color: '#13C373' }, { name: '勤务路线', start: [minLng, midLat - hStep], end: [maxLng, midLat - hStep], color: '#BC301D' }, { name: '定周期控制', start: [minLng + vStep, maxLat], end: [minLng + vStep, minLat], color: '#3296FA' }, { name: '感应控制', start: [minLng + vStep * 2, maxLat], end: [minLng + vStep * 2, minLat], color: '#FF864C' }, { name: '自适应控制', start: [minLng + vStep * 3, maxLat], end: [minLng + vStep * 3, minLat], color: '#9F6EFE' }, { name: '手动控制', start: [minLng + vStep * 4, maxLat], end: [minLng + vStep * 4, minLat], color: '#EB9F36' }, { name: '特殊控制', start: [minLng + vStep * 5, maxLat], end: [minLng + vStep * 5, minLat], color: '#A26218' }, { name: '离线', start: [minLng, maxLat - hStep * 0.3], end: [maxLng, maxLat - hStep * 0.3], color: '#7A7A7A' }, { name: '降级', start: [minLng, minLat + hStep * 0.3], end: [maxLng, minLat + hStep * 0.3], color: '#D9C13B' }, { name: '故障', start: [maxLng - vStep * 0.5, maxLat], end: [maxLng - vStep * 0.5, minLat], color: '#FF3938' }, ]) } /** * GET /api/devices/fault-status * 设备故障统计(匹配 DeviceStatusTabs 组件的 statusData 格式) * keys: signalMachineStatus, detectorStatus, trafficLightStatus */ export async function apiGetDeviceFaultStatus() { await delay(200) const sm = DB.deviceStatus.signalMachine const dt = DB.deviceStatus.detector const cam = DB.deviceStatus.camera // 从在线数据推算故障数,每次波动 const smTotal = sm.chartData[0].value + sm.chartData[1].value const smFault = _fluctuate(sm.chartData[1].value, 3) const dtTotal = dt.chartData[0].value + dt.chartData[1].value const dtFault = _fluctuate(dt.chartData[1].value, 5) const camTotal = cam.chartData[0].value + cam.chartData[1].value const camFault = _fluctuate(cam.chartData[1].value, 2) return ok({ signalMachineStatus: { centerTitle: Math.max(0, smFault) + '', centerSubTitle: `${Math.max(0, smFault)}/${smTotal}`, chartData: [ { name: '正常', value: Math.max(0, smTotal - smFault), color: '#A0E551' }, { name: '故障', value: Math.max(0, smFault), color: '#D03030' }, ] }, detectorStatus: { centerTitle: Math.max(0, dtFault) + '', centerSubTitle: `${Math.max(0, dtFault)}/${dtTotal}`, chartData: [ { name: '通信故障', value: Math.max(0, Math.floor(dtFault * 0.6)), color: '#C6302B' }, { name: '数据异常', value: Math.max(0, dtFault - Math.floor(dtFault * 0.6)), color: '#faad14' }, ] }, trafficLightStatus: { centerTitle: Math.max(0, camFault) + '', centerSubTitle: `${Math.max(0, camFault)}/${camTotal}`, chartData: [ { name: '红绿冲突', value: Math.max(0, Math.floor(camFault * 0.5)), color: '#C6302B' }, { name: '红灯故障', value: Math.max(0, camFault - Math.floor(camFault * 0.5)), color: '#8F1E1E' }, ] }, }) }