| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531 |
- /**
- * 模拟 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'
- import { simulateMaxband } from './_simulateMaxband'
- // ── 静态资源(模拟 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 } }
- /** 根据路口ID生成稳定的设备状态(所有API共用,确保一致) */
- function _getDeviceStatus(id) {
- const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
- const statusList = ['在线', '在线', '在线', '在线', '在线', '在线', '在线', '离线']
- return statusList[seed % statusList.length]
- }
- /**
- * 根据路口ID生成稳定的地图分类(在线→控制方式,离线→异常类型)
- * 地图标记和 API 共用,确保状态一致
- */
- export function getIntersectionCategory(id) {
- const status = _getDeviceStatus(id)
- const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
- if (status === '离线') {
- const abnormalTypes = ['离线', '降级', '故障']
- return abnormalTypes[seed % abnormalTypes.length]
- }
- // 用 floor(seed / 8) 解耦:_getDeviceStatus 已用 seed%8 决定在/离线,
- // 这里若再用同一模数,最后一个 index 永远拿不到数据(被离线分支吃掉了)
- const normalTypes = ['中心计划', '定周期控制', '感应控制', '自适应控制', '手动控制', '黄闪', '全红', '关灯']
- return normalTypes[Math.floor(seed / 8) % normalTypes.length]
- }
- /** 基于当前秒数产生稳定随机(同一秒内多次调用返回相同值) */
- function seededRand(seed) {
- const x = Math.sin(seed) * 10000
- return x - Math.floor(x)
- }
- /** 根据路口 ID 生成稳定 seed(全字符加权,所有 API 共用) */
- function _idSeed(id) {
- return id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
- }
- /** 根据路口 ID 获取稳定的 cycleLength(优先 preset → crossingList → seed 兜底) */
- function _getCycleLength(id) {
- const preset = DB.signalTimings[id]
- if (preset) return preset.data.cycleLength
- const crossing = DB.crossingList.find(r => r.id === id)
- if (crossing && crossing.cycle) return crossing.cycle
- return [100, 120, 130, 140, 150, 160][_idSeed(id) % 6]
- }
- /** 当前时间 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 = ['北进口', '南进口', '东进口', '西进口']
- // 4 方向各 1 个、统一枪机:视频模式下示意图 4 臂图标一致,避免 球机方框 与检测器视觉混淆
- return positions.map((pos, 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: '枪机',
- port: 554 + i,
- ip: `192.168.${10 + (seed % 200)}.${100 + i}`,
- enabled: true,
- position: pos,
- }))
- }
- /** 从摄像机列表推导各方向摄像头类型 (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
- }
- /**
- * 检测器单一数据源:4 方向各 1 条(与画布 ① ② ③ ④ 一一对应)。
- * 编号 1=北、2=东、3=南、4=西。
- * bucketIdx(5s 桶)决定确定性噪声,同桶内多次调用返回相同值,让画布与表格读数同步。
- */
- function _detectorBucketSnapshot(id, bucketIdx, name) {
- const seed = _idSeed(id || '')
- const dirs = [
- { dir: 'N', label: '北', index: 1 },
- { dir: 'E', label: '东', index: 2 },
- { dir: 'S', label: '南', index: 3 },
- { dir: 'W', label: '西', index: 4 },
- ]
- return dirs.map(d => {
- const i = d.index
- const baseFlow = 60 + ((seed * (i * 7 + 13)) % 160) // 60~220
- const baseOcc = 15 + ((seed * (i * 11 + 17)) % 50) // 15~65
- // 桶内确定性噪声(无 Math.random,保证同 bucketIdx 多次调用结果一致)
- const flowNoise01 = (((seed ^ (i * 257) ^ (bucketIdx * 9176)) >>> 0) % 10000) / 10000
- const occNoise01 = (((seed ^ (i * 521) ^ (bucketIdx * 4093)) >>> 0) % 10000) / 10000
- const flow = Math.max(0, Math.round(baseFlow * (1 + (flowNoise01 - 0.5) * 0.3))) // ±15%
- const occupancy = Math.max(0, Math.min(100, Math.round(baseOcc + (occNoise01 - 0.5) * 10))) // ±5
- return {
- intersection: name || id,
- intersectionId: id,
- detectorId: `DT${(id || '000').slice(-6)}_${String(i).padStart(2, '0')}`,
- index: i,
- position: `${d.label}进口`,
- direction: d.dir,
- name: `${d.label}进口`,
- flow,
- occupancy,
- enabled: true,
- }
- })
- }
- /** 4 条 lane → 4 方向 armsDetector(一一对应,无需挑选) */
- function _bucketArmsDetector(lanes) {
- const out = { N: null, S: null, E: null, W: null }
- for (const lane of lanes) {
- if (!out[lane.direction]) {
- out[lane.direction] = {
- index: lane.index,
- detectorId: lane.detectorId,
- flow: lane.flow,
- occupancy: lane.occupancy,
- }
- }
- }
- return out
- }
- /**
- * 按车道展开的检测器快照——与画布徽章一一对应。
- * 编号顺序:N→E→S→W 顺时针累加;每个方向内司机视角左→右(arm-local 最外侧 → 最内侧)。
- * 4 方向 × lanesPerDir 条车道 = 全局连续编号 1..(4*lanesPerDir)。
- */
- function _detectorLaneBucketSnapshot(id, bucketIdx, name, lanesPerDir = 4) {
- const seed = _idSeed(id || '')
- const dirs = [
- { dir: 'N', label: '北' },
- { dir: 'E', label: '东' },
- { dir: 'S', label: '南' },
- { dir: 'W', label: '西' },
- ]
- const out = []
- let badgeNum = 1
- dirs.forEach(d => {
- // 司机视角左→右:arm-local 最外侧(laneIdx = lanesPerDir-1)→ 最内侧(laneIdx = 0)
- for (let laneIdx = lanesPerDir - 1; laneIdx >= 0; laneIdx--) {
- const driverPos = lanesPerDir - laneIdx // 1..N,司机左数第几条
- const i = badgeNum
- const baseFlow = 60 + ((seed * (i * 7 + 13)) % 160)
- const baseOcc = 15 + ((seed * (i * 11 + 17)) % 50)
- const flowNoise01 = (((seed ^ (i * 257) ^ (bucketIdx * 9176)) >>> 0) % 10000) / 10000
- const occNoise01 = (((seed ^ (i * 521) ^ (bucketIdx * 4093)) >>> 0) % 10000) / 10000
- const flow = Math.max(0, Math.round(baseFlow * (1 + (flowNoise01 - 0.5) * 0.3)))
- const occupancy = Math.max(0, Math.min(100, Math.round(baseOcc + (occNoise01 - 0.5) * 10)))
- out.push({
- intersection: name || id,
- intersectionId: id,
- detectorId: `DT${(id || '000').slice(-6)}_${String(badgeNum).padStart(2, '0')}`,
- index: badgeNum,
- direction: d.dir,
- laneIndex: laneIdx,
- driverPos,
- position: `${d.label}进口`,
- name: `${d.label}进口 第${driverPos}车道`,
- flow,
- occupancy,
- enabled: true,
- })
- badgeNum++
- }
- })
- return out
- }
- // 保留旧入口名,让调用方零修改(_makeIntersectionConfig 等仍能用)
- function _makeDetectors(id, name /* seed 不再使用:bucket 自己用 _idSeed */) {
- const bucketIdx = Math.floor(Date.now() / 5000)
- return _detectorBucketSnapshot(id, bucketIdx, name)
- }
- function _detectorsToArmConfig(detectors) {
- return _bucketArmsDetector(detectors)
- }
- function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default' } = {}) {
- 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 detectors = _makeDetectors(id, name, seed)
- const armDetectors = _detectorsToArmConfig(detectors)
- const lanesPresetMap = {
- default: {
- N: ['U', 'L', 'S', 'R'],
- S: ['U', 'L', 'S', 'R'],
- E: ['U', 'L', 'S', 'R'],
- W: ['U', 'L', 'S', 'R'],
- },
- simple: {
- N: ['L', 'S', null, null], // 北:左转+直行
- S: ['U', 'S', null, null], // 南:掉头+直行
- E: ['L', 'S', null, null], // 东:掉头+直行
- W: ['U', 'S', null, null], // 西:左转+直行
- },
- }
- const lanesPreset = lanesPresetMap[iconMode] || lanesPresetMap.default
- return {
- status: _getDeviceStatus(id),
- signals: {
- ns: { phaseName: phases[0], time: countdown, isGreen: nsGreen },
- ew: { phaseName: phases[1], time: countdown, isGreen: !nsGreen },
- },
- armsConfig: {
- N: { cameraType: armCamTypes.N, lanes: lanesPreset.N, detector: armDetectors.N },
- S: { cameraType: armCamTypes.S, lanes: lanesPreset.S, detector: armDetectors.S },
- E: { cameraType: armCamTypes.E, lanes: lanesPreset.E, detector: armDetectors.E },
- W: { cameraType: armCamTypes.W, lanes: lanesPreset.W, detector: armDetectors.W },
- },
- cameras,
- detectors,
- }
- }
- /**
- * 动态生成路口相位配时数据
- * @param {number} cycleLength 周期总时长
- * @param {boolean} isTwoRows 是否生成上下双排 8 相位 (默认 true)
- */
- // 相位数据缓存:同一路口 (cycleLength+iconMode) 只生成一次,列表和详情弹窗共享
- const _phaseDataCache = {};
- function _makePhaseData(cycleLength = 140, isTwoRows = true, iconMode = 'default', id = '') {
- // 缓存 key:用路口 ID(有的话),保证同一路口列表和详情共享同一份数据
- const cacheKey = id || `${cycleLength}_${iconMode}`;
- if (_phaseDataCache[cacheKey]) return _phaseDataCache[cacheKey];
- const n = 4; // 4个阶段 (S1-S4)
- // 各阶段按比例分配时间,P1/P3较长,P2/P4较短
- const ratios = [0.3, 0.2, 0.3, 0.2];
- const stageTimes = ratios.map(r => Math.floor(cycleLength * r));
- // 把余数补到第一个阶段,确保总和 = cycleLength
- stageTimes[0] += cycleLength - stageTimes.reduce((a, b) => a + b, 0);
- const pd = [];
- // 固定4个阶段的图标和方向:P1南北直行、P2南北左转、P3东西直行、P4东西左转
- const phaseConfigMap = {
- default: [
- { icon: 'STRAIGHT_DOWN,STRAIGHT_UP', direction: 'ns' }, // P1: 南北直行
- { icon: 'TURN_DOWN_LEFT,TURN_UP_LEFT', direction: 'ns' }, // P2: 南北左转
- { icon: 'STRAIGHT_LEFT,STRAIGHT_RIGHT', direction: 'ew' }, // P3: 东西直行
- { icon: 'TURN_LEFT_DOWN,TURN_RIGHT_UP', direction: 'ew' }, // P4: 东西左转
- ],
- simple: [
- { icon: 'STRAIGHT_DOWN,STRAIGHT_UP', direction: 'ns' }, // P1: 南北直行
- { icon: 'TURN_DOWN_LEFT,TURN_UP_LEFT_UTURN', direction: 'ns' }, // P2: 北左转+南掉头
- { icon: 'STRAIGHT_LEFT,STRAIGHT_RIGHT', direction: 'ew' }, // P3: 东西直行
- { icon: 'TURN_LEFT_DOWN,TURN_RIGHT_UP_UTURN', direction: 'ew' }, // P4: 东左转+西掉头
- ],
- };
- const phaseConfig = phaseConfigMap[iconMode] || phaseConfigMap.default;
- let t = 0;
- for (let i = 0; i < n; i++) {
- const stageStart = t;
- const stageTime = stageTimes[i];
- const stageEnd = stageStart + stageTime;
- const { icon: stageIcon, direction } = phaseConfig[i];
- const pushTrackData = (trackIdx, phaseNamePrefix) => {
- const icon = stageIcon;
- const phaseName = `${phaseNamePrefix}${i + 1}`;
- const s = Math.floor(Math.random() * 3) + 3; // 绿闪/条纹 3-5s
- const y = 3; // 黄灯固定 3s
- const r = 2; // 全红间隔 2s
- const g = stageTime - s - y - r; // 绿灯 = 阶段总时长 - 条纹 - 黄灯 - 红灯
- let curT = stageStart;
- // 1. 绿灯 (phase[8]存阶段总时长,用于显示)
- pd.push([trackIdx, curT, curT + g, phaseName, g, 'green', icon, direction, stageTime]);
- curT += g;
- // 2. 绿闪/条纹
- pd.push([trackIdx, curT, curT + s, '', s, 'stripe', null, direction]);
- curT += s;
- // 3. 黄灯
- pd.push([trackIdx, curT, curT + y, '', y, 'yellow', null, direction]);
- curT += y;
- // 4. 红灯固定
- pd.push([trackIdx, curT, curT + r, '', r, 'red', null, direction]);
- };
- pushTrackData(0, 'P'); // 生成第一排 (P1-P4)
- if (isTwoRows) {
- pushTrackData(1, 'P'); // 生成第二排
- }
- t = stageEnd;
- }
- _phaseDataCache[cacheKey] = pd;
- return pd;
- }
- /**
- * 任意阶段数的相位数据生成(不缓存,每次新生成)
- * 用于 JNC900032 这类需要演示 N 阶段双相位图的示例路口。
- * 单轨道(trackIdx=0),4 方向 icon 轮转,子段长度按阶段时间自适应。
- * @param {number} cycleLength 周期总时长(秒)
- * @param {number} stageCount 阶段数
- */
- function _makeFlexiblePhaseData(cycleLength, stageCount) {
- const baseStage = Math.floor(cycleLength / stageCount);
- const remainder = cycleLength - baseStage * stageCount;
- const stageTimes = Array.from({ length: stageCount }, (_, i) =>
- baseStage + (i === 0 ? remainder : 0));
- // 与 _makePhaseData(iconMode='simple') 对齐:P2 北左转+南掉头、P4 东左转+西掉头
- // dual 示例路口(JNC900032/JNC900016)通过 apiGetCrossingDetailData 走 simple 模式,图标必须一致
- const iconCycle = [
- { icon: 'STRAIGHT_DOWN,STRAIGHT_UP', direction: 'ns' }, // P1 南北直行
- { icon: 'TURN_DOWN_LEFT,TURN_UP_LEFT_UTURN', direction: 'ns' }, // P2 北左转 + 南掉头
- { icon: 'STRAIGHT_LEFT,STRAIGHT_RIGHT', direction: 'ew' }, // P3 东西直行
- { icon: 'TURN_LEFT_DOWN,TURN_RIGHT_UP_UTURN', direction: 'ew' }, // P4 东左转 + 西掉头
- ];
- const pd = [];
- let t = 0;
- for (let i = 0; i < stageCount; i++) {
- const stageTime = stageTimes[i];
- const cfg = iconCycle[i % iconCycle.length];
- const phaseName = `P${i + 1}`;
- // 子段长度自适应:阶段时间太短就退化为纯 green,避免出现 0 时长子段
- const s = stageTime >= 4 ? Math.max(1, Math.min(3, Math.floor(stageTime * 0.15))) : 0;
- const y = stageTime >= 3 ? Math.max(1, Math.min(3, Math.floor(stageTime * 0.15))) : 0;
- const r = stageTime >= 8 ? Math.max(0, Math.min(2, Math.floor(stageTime * 0.10))) : 0;
- const g = stageTime - s - y - r;
- if (g <= 0) {
- pd.push([0, t, t + stageTime, phaseName, stageTime, 'green', cfg.icon, cfg.direction, stageTime]);
- t += stageTime;
- continue;
- }
- pd.push([0, t, t + g, phaseName, g, 'green', cfg.icon, cfg.direction, stageTime]);
- t += g;
- if (s > 0) { pd.push([0, t, t + s, '', s, 'stripe', null, cfg.direction]); t += s; }
- if (y > 0) { pd.push([0, t, t + y, '', y, 'yellow', null, cfg.direction]); t += y; }
- if (r > 0) { pd.push([0, t, t + r, '', r, 'red', null, cfg.direction]); t += r; }
- }
- 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 ? {
- status: base.status || _getDeviceStatus(id),
- 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 cycleLength = _getCycleLength(id)
- return ok({
- cycleLength,
- currentTime: Math.floor(Date.now() / 1000) % cycleLength,
- phaseData: _makePhaseData(cycleLength, false, 'simple', id),
- })
- }
- /** 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[5] === 'green')
- return ok(phases.map((p, i) => ({
- value: String(i + 1), time: p[8], 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
- * 在线数每次请求轻微波动
- */
- // ── 信号机故障数缓存:同一秒内多次调用返回同一份数据,保证在线离线与故障同步 ──
- let _smFaultCache = { ts: 0, total: 0, faultTotal: 0 }
- function _getSmFaultSnapshot() {
- const now = Math.floor(Date.now() / 1000)
- if (_smFaultCache.ts === now) return _smFaultCache
- const sm = DB.deviceStatus.signalMachine
- const total = sm.chartData[0].value + sm.chartData[1].value
- const yellowFlashMode = DB.homeData.controlModes.find(m => m.name === '黄闪控制')
- const yellowFlash = yellowFlashMode ? yellowFlashMode.value : 0
- const ctrlBoard = Math.max(0, _fluctuate(5, 3))
- const phaseBoard = Math.max(0, _fluctuate(4, 2))
- const detBoard = Math.max(0, _fluctuate(3, 2))
- const faultTotal = ctrlBoard + phaseBoard + detBoard + yellowFlash
- _smFaultCache = { ts: now, total, faultTotal, ctrlBoard, phaseBoard, detBoard, yellowFlash }
- return _smFaultCache
- }
- 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 },
- ]
- }
- }
- // 信号机:离线数 = 故障总数(与设备状态同步)
- function smStats() {
- const snap = _getSmFaultSnapshot()
- const online = snap.total - snap.faultTotal
- const rate = Math.round(online / snap.total * 100)
- return {
- centerTitle: rate + '%',
- centerSubTitle: `${online}/${snap.total}`,
- chartData: [
- { name: '在线', value: online, color: '#32F6F8' },
- { name: '离线', value: snap.faultTotal, color: '#E4D552' },
- ]
- }
- }
- if (type === 'signalMachine') return ok(smStats())
- if (type && DB.deviceStatus[type]) return ok(fluctuateStats(DB.deviceStatus[type]))
- return ok({
- signalMachine: smStats(),
- 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)
- const snap = _getSmFaultSnapshot()
- const onlineTotal = snap.total - snap.faultTotal
- const modes = DB.homeData.controlModes
- // 各项按基准值波动
- const fluctuated = modes.map(m => ({
- ...m, value: Math.max(0, _fluctuate(m.value, Math.ceil(m.value * 0.05))),
- }))
- // 修正总数:将差值补到第一项(定周期控制),保证总数 = 信号机在线数
- const currentSum = fluctuated.reduce((s, m) => s + m.value, 0)
- fluctuated[0].value = Math.max(0, fluctuated[0].value + (onlineTotal - currentSum))
- return ok(fluctuated)
- }
- /**
- * 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 { label, mode = 'balanced' } = opts // mode: 'forward' | 'reverse' | 'balanced'
- const labels = ['A', 'B', 'C', 'D']
- const prefix = label || '交叉口'
- // ==== "对称协调"完美参数 ====
- // 数学条件: 2 × 总路长 / 速度 ≡ k × cycle (k 为整数), 双向都能完美协调
- // 这里: speed=36 km/h (10 m/s), 路口间距 1000m, 单程走 100s = 1 个 cycle
- // 整条路 (3000m) 反向走 300s = 3 个 cycle, 所有路口 offset 都为 0 时双向都对齐绿/蓝条
- const cycle = 100
- const greenDuration = 40
- const bandwidth = 31.5
- const speedKmh = 36
- const speedKmhBackward = 36
- const dists = [1000, 1000, 1000, 0]
- const offsets = simulateMaxband({
- distances: dists,
- cycle,
- speedFwd: speedKmh,
- speedRev: speedKmhBackward,
- mode,
- })
- return ok({
- roadSegments: labels.map((l, i) => ({
- name: label ? `${prefix}-路口${l}` : `${prefix}${l}`,
- distanceNext: dists[i],
- offset: offsets[i]
- })),
- speedKmh,
- speedKmhBackward,
- cycle,
- greenDuration,
- bandwidth,
- scanLineStart: Math.round(Math.random() * 100) / 100,
- meta: {
- algorithm: `heuristic_v1_${mode}`,
- coordinationMode: mode,
- }
- })
- }
- // ═══════════════════════════════════════════════════════════════════════
- // 路口列表表格(分页 + 筛选 + 排序 + 动态状态)
- // ═══════════════════════════════════════════════════════════════════════
- /**
- * 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 cycleLength = _getCycleLength(r.id)
- const phaseData = _makePhaseData(cycleLength, false, 'simple', r.id)
- return {
- ...r,
- status: _getDeviceStatus(r.id),
- 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 allLanes = ['U', 'L', 'S', 'R'];
- const lanePresets = [
- { N: allLanes, S: allLanes, E: allLanes, W: allLanes },
- ]
- 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, iconMode: 'simple' })
- const seed = _idSeed(id)
- // 确保 config 有 status
- if (!config.status) {
- config.status = _getDeviceStatus(id)
- }
- const cycleLength = _getCycleLength(id)
- const phaseData = _makePhaseData(cycleLength, false, 'simple', id)
- 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, { iconMode = 'default' } = {}) {
- await delay(350)
- const point = DB.points.find(p => p.id === id)
- const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false, iconMode })
- const seed = _idSeed(id)
- // 确保 config 有 status 字段(预存配置可能缺失)
- if (!config.status) {
- config.status = _getDeviceStatus(id)
- }
- // 确保 detectors 存在(mock_data.json 的预存配置没有此字段,需补齐)
- if (!config.detectors) {
- const detectors = _makeDetectors(id, point ? point.name : id, seed)
- const armDetectors = _detectorsToArmConfig(detectors)
- config.detectors = detectors
- config.armsConfig = config.armsConfig || {}
- const existingKeys = Object.keys(config.armsConfig)
- if (existingKeys.length === 0) {
- // armsConfig 空:按 4 路口默认填充 NESW 结构
- ;['N', 'E', 'S', 'W'].forEach(d => {
- config.armsConfig[d] = { detector: armDetectors[d] }
- })
- } else {
- // 已有结构:只对已存在的 NESW key 注入 detector(4 路口兼容),
- // 不再创建新的 NESW key,避免污染多路口(arm_1/2/3 等)的 armsConfig
- ;['N', 'E', 'S', 'W'].forEach(d => {
- if (config.armsConfig[d]) {
- config.armsConfig[d].detector = armDetectors[d]
- }
- })
- }
- }
- // 从真实阶段数据推导周期和相位
- let cycleLength = _getCycleLength(id)
- let phaseData = _makePhaseData(cycleLength, false, 'simple', id)
- // 双相位图示例路口:N 阶段(≠4)走独立相位生成器,覆盖外层 phaseData/cycleLength
- // JNC900008 用来验证 CrossingDetailPanel "8 槽两行刚满, 不出滚动条" 的边界情况
- const _dualSamples = {
- JNC900032: { stageCount: 32, cycleLength: 160, schemeName: '32阶段示范方案' },
- JNC900016: { stageCount: 16, cycleLength: 160, schemeName: '16阶段示范方案' },
- JNC900008: { stageCount: 8, cycleLength: 120, schemeName: '8阶段示范方案' },
- }
- if (_dualSamples[id]) {
- const cfgSample = _dualSamples[id]
- cycleLength = cfgSample.cycleLength
- phaseData = _makeFlexiblePhaseData(cycleLength, cfgSample.stageCount)
- }
- // thisCycle / lastCycle:所有路口统一返回,让 CrossingDetailPanel 的双相位图布局对所有
- // 路口生效(普通 4 阶段路口直接复用 _makePhaseData 的结果;示例路口已用 flexible 覆盖)
- const _planSchemeName = (_dualSamples[id] && _dualSamples[id].schemeName) || '默认配时方案'
- const _nowSec = Math.floor(Date.now() / 1000)
- const _currentTimeIn = _nowSec % cycleLength
- const thisCycle = {
- schemeId: 'sys_a',
- schemeName: _planSchemeName,
- cycleLength,
- currentTime: _currentTimeIn,
- phaseData,
- phaseDiff: (seed * 7) % 25,
- coordTime: (seed * 13) % 60,
- }
- // 上周期:演示用复用同一份计划相位(真实后端应给上周期实绩),actualDuration 模拟 ±2s 拉伸
- const lastCycle = {
- schemeId: 'sys_a',
- schemeName: _planSchemeName,
- cycleLength,
- actualDuration: cycleLength + 2,
- endedAt: new Date((_nowSec - _currentTimeIn) * 1000).toISOString(),
- phaseData,
- }
- // 从相位数据中提取阶段列表(优先上轨道绿灯相位,单轨道时取 track 0)
- // 不再硬截 4 个 —— CrossingDetailPanel 现已支持 2 行 8 槽 + 滚动, 配合
- // _dualSamples (JNC900008/016/032) 可看到 8/16/32 阶段的完整渲染
- const hasTrack1 = phaseData.some(p => p[0] === 1)
- const greenPhases = phaseData.filter(p => p[0] === (hasTrack1 ? 1 : 0) && p[5] === 'green')
- // 每个阶段的锁定时间选项(不同阶段时长不同,锁定选项也不同)
- const stageLockOptions = [
- [{ label: '20', value: 20 }, { label: '30', value: 30 }, { label: '45', value: 45 }, { label: '60', value: 60 }],
- [{ label: '15', value: 15 }, { label: '25', value: 25 }, { label: '35', value: 35 }, { label: '50', value: 50 }],
- [{ label: '20', value: 20 }, { label: '30', value: 30 }, { label: '45', value: 45 }, { label: '60', value: 60 }],
- [{ label: '10', value: 10 }, { label: '20', value: 20 }, { label: '30', value: 30 }, { label: '40', value: 40 }],
- ]
- const stageList = greenPhases.map((p, i) => ({
- value: String(i + 1),
- time: p[8],
- phaseName: p[3],
- direction: p[6],
- img: ARROWS[i],
- locktimeOptions: stageLockOptions[i] || stageLockOptions[0],
- }))
- // 控制方式选项 + 根据路口选择不同的当前控制方式
- const allMethods = [
- { label: '关灯', value: 'lights_off' },
- { label: '黄闪', value: 'yellow_flash' },
- { label: '全红', value: 'all_red' },
- { label: '定周期', value: 'fixed' },
- { label: '步进', value: 'step' },
- { label: '中心计划', value: 'system' },
- { label: '感应控制', value: 'sensor' },
- { label: '临时方案', value: 'temp' },
- ]
- const methodValues = ['fixed', 'step', 'system', 'sensor', 'temp']
- // 示范多阶段路口固定走非-step 方法, 否则 step 模式会隐藏 button-group (取消/确认),
- // 用户点"手动控制"看不到确认按钮, 误以为坏掉
- const _isDualSampleId = id && /^JNC9000(?:08|16|32)$/.test(id)
- const currentMethod = _isDualSampleId ? 'fixed' : 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
- // 多路口(armsConfig key 不是 NESW)按 arm 顺序循环分配 4 个 mp4,让每条 arm 的视频独立对应
- const armKeys = (config.armsConfig && Object.keys(config.armsConfig)) || []
- const isMultiWay = armKeys.length > 0 && !(armKeys.length === 4 && ['N','E','S','W'].every(d => armKeys.includes(d)))
- let armVideos = null
- if (isMultiWay) {
- armVideos = {}
- armKeys.forEach((k, i) => { armVideos[k] = VIDEOS[i % VIDEOS.length] })
- }
- return ok({
- currentRoute: {
- id, name: point ? point.name : id,
- level: point?.isKey ? '一级' : '二级',
- mode: currentMode,
- time: cycleLength + 's',
- mainVideo: pickVideo(seed),
- cornerVideos: _makeCornerVideos(seed),
- armVideos,
- },
- intersectionData: config,
- phaseData,
- cycleLength,
- currentTime: Math.floor(Date.now() / 1000) % cycleLength,
- phaseDiff,
- coordTime,
- thisCycle,
- lastCycle,
- 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 },
- ],
- // 临时方案时间栏数据
- startDate: '2026-04-14',
- startTime: '08:00:00',
- endDate: '2026-04-14',
- endTime: '10:00:00',
- duration: 120,
- period: 1,
- })
- }
- /**
- * PUT /api/crossing/temp-scheme/:id — 修改临时配时方案
- * 入参:{ stages, timeRange, isFixedCycle }
- * 当前 mock 仅做参数校验和成功返回;真实后端要把 timeRange 转成排程任务、stages 落库
- */
- export async function apiSaveCrossingTempScheme(id, payload = {}) {
- await delay(200)
- if (!id) return fail('缺少路口 ID')
- const stages = Array.isArray(payload.stages) ? payload.stages : []
- if (stages.length === 0) return fail('阶段列表不能为空')
- const total = stages.reduce((a, b) => a + (Number(b.time) || 0), 0)
- if (total <= 0) return fail('阶段总时长必须 > 0')
- return ok({
- id,
- method: 'temp',
- cycleTotal: total,
- stages: stages.map((s, i) => ({ ...s, idx: i + 1 })),
- timeRange: payload.timeRange || null,
- isFixedCycle: !!payload.isFixedCycle,
- appliedAt: new Date().toISOString(),
- })
- }
- /**
- * PUT /api/crossing/scheme/:id — 修改配时方案(永久)
- * 入参:{ stages, isFixedCycle };schemeId 由前端 currentScheme 决定,约定走 query 或 body 字段
- */
- export async function apiSaveCrossingScheme(id, payload = {}) {
- await delay(200)
- if (!id) return fail('缺少路口 ID')
- const stages = Array.isArray(payload.stages) ? payload.stages : []
- if (stages.length === 0) return fail('阶段列表不能为空')
- const total = stages.reduce((a, b) => a + (Number(b.time) || 0), 0)
- if (total <= 0) return fail('阶段总时长必须 > 0')
- return ok({
- id,
- method: 'scheme',
- cycleTotal: total,
- stages: stages.map((s, i) => ({ ...s, idx: i + 1 })),
- isFixedCycle: !!payload.isFixedCycle,
- appliedAt: new Date().toISOString(),
- })
- }
- /** 多路口(非 NESW key)按实际 armsConfig 生成 per-arm 检测器数据 */
- function _detectorPerArmSnapshot(id, armsConfig, bucketIdx) {
- const seed = _idSeed(id || '')
- const armsDetector = {}
- const tableData = []
- let badgeNum = 1
- Object.keys(armsConfig).forEach((dir, dirIdx) => {
- const lanes = ((armsConfig[dir] && armsConfig[dir].lanes) || []).filter(l => l)
- if (lanes.length === 0) return
- const i = dirIdx + 1
- const baseFlow = 60 + ((seed * (i * 7 + 13)) % 160)
- const baseOcc = 15 + ((seed * (i * 11 + 17)) % 50)
- const flowNoise = (((seed ^ (i * 257) ^ (bucketIdx * 9176)) >>> 0) % 10000) / 10000
- const occNoise = (((seed ^ (i * 521) ^ (bucketIdx * 4093)) >>> 0) % 10000) / 10000
- const armFlow = Math.max(0, Math.round(baseFlow * (1 + (flowNoise - 0.5) * 0.3)))
- const armOcc = Math.max(0, Math.min(100, Math.round(baseOcc + (occNoise - 0.5) * 10)))
- armsDetector[dir] = { index: i, flow: armFlow, occupancy: armOcc }
- lanes.forEach((laneType, laneIdx) => {
- const j = badgeNum
- const lFlow = Math.max(0, Math.round((60 + ((seed * (j * 7 + 13)) % 160)) * (1 + ((((seed ^ (j * 257) ^ (bucketIdx * 9176)) >>> 0) % 10000) / 10000 - 0.5) * 0.3)))
- const lOcc = Math.max(0, Math.min(100, Math.round((15 + ((seed * (j * 11 + 17)) % 50)) + ((((seed ^ (j * 521) ^ (bucketIdx * 4093)) >>> 0) % 10000) / 10000 - 0.5) * 10)))
- tableData.push({
- id: badgeNum,
- name: `${dir} 第${laneIdx + 1}车道`,
- flow: lFlow,
- occupancy: `${lOcc}%`,
- })
- badgeNum++
- })
- })
- return { timeTicks: [180, 150, 120, 90, 60, 30], tableData, armsDetector }
- }
- /**
- * GET /api/detector/monitor/:id — 检测器运行数据监视
- * 返回两套视图:
- * - armsDetector: { N/E/S/W or arm_X → {index, flow, occupancy} }
- * - tableData: 按车道展开(4 路口 16 条;多路口按实际 arm × lane 数生成)
- * 同 5s 桶内重复调用返回相同值(确定性噪声),保证画布与弹窗轮询同窗口取数一致。
- */
- export async function apiGetDetectorMonitorData(id) {
- await delay(150)
- const bucketIdx = Math.floor(Date.now() / 5000)
- // 多路口路径:按实际 armsConfig 生成 per-arm 数据
- const config = DB.intersectionConfigs[id]
- const armsConfig = config && config.armsConfig
- if (armsConfig) {
- const armKeys = Object.keys(armsConfig)
- const isMultiWay = !(armKeys.length === 4 && ['N','E','S','W'].every(d => armKeys.includes(d)))
- if (isMultiWay) {
- return ok(_detectorPerArmSnapshot(id, armsConfig, bucketIdx))
- }
- }
- // 4 路口默认路径(原逻辑)
- const dirSnap = _detectorBucketSnapshot(id, bucketIdx)
- const armsDetector = _bucketArmsDetector(dirSnap)
- const laneSnap = _detectorLaneBucketSnapshot(id, bucketIdx)
- return ok({
- timeTicks: [180, 150, 120, 90, 60, 30],
- tableData: laneSnap.map(l => ({
- id: l.index,
- name: l.name,
- flow: l.flow,
- occupancy: `${l.occupancy}%`,
- })),
- armsDetector,
- })
- }
- /**
- * 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 dt = DB.deviceStatus.detector
- const cam = DB.deviceStatus.camera
- // ── 信号机故障:从共享缓存获取,确保与在线状态的离线数一致 ──
- const snap = _getSmFaultSnapshot()
- const smTotal = snap.total
- const smFaultTotal = snap.faultTotal
- const smFaultList = [
- { name: '正常', value: Math.max(0, smTotal - smFaultTotal), color: '#5EC8FF' },
- { name: '控制板报警', value: snap.ctrlBoard, color: '#FF7878' },
- { name: '相位板报警', value: snap.phaseBoard, color: '#E66565' },
- { name: '检测板报警', value: snap.detBoard, color: '#CC4848' },
- { name: '黄闪报警', value: snap.yellowFlash, color: '#B83838' },
- ]
- // ── 检测器故障 ──
- const dtTotal = dt.chartData[0].value + dt.chartData[1].value
- const dtFault = Math.max(0, _fluctuate(dt.chartData[1].value, 5))
- const dtCommFault = Math.max(0, Math.floor(dtFault * 0.6))
- // ── 红绿灯故障 ──
- const camTotal = cam.chartData[0].value + cam.chartData[1].value
- const camFault = Math.max(0, _fluctuate(cam.chartData[1].value, 2))
- const camConflict = Math.max(0, Math.floor(camFault * 0.5))
- return ok({
- signalMachineStatus: {
- centerTitle: smFaultTotal + '',
- centerSubTitle: `${smFaultTotal}/${smTotal}`,
- chartData: smFaultList,
- },
- detectorStatus: {
- centerTitle: dtFault + '',
- centerSubTitle: `${dtFault}/${dtTotal}`,
- chartData: [
- { name: '正常', value: Math.max(0, dtTotal - dtFault), color: '#5EC8FF' },
- { name: '通信故障', value: dtCommFault, color: '#C6302B' },
- { name: '数据异常', value: Math.max(0, dtFault - dtCommFault), color: '#faad14' },
- ]
- },
- trafficLightStatus: {
- centerTitle: camFault + '',
- centerSubTitle: `${camFault}/${camTotal}`,
- chartData: [
- { name: '正常', value: Math.max(0, camTotal - camFault), color: '#5EC8FF' },
- { name: '红绿冲突', value: camConflict, color: '#C6302B' },
- { name: '红灯故障', value: Math.max(0, camFault - camConflict), color: '#8F1E1E' },
- ]
- },
- })
- }
|