api.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039
  1. /**
  2. * 模拟 API 接口层(动态数据版)
  3. *
  4. * 特性:
  5. * - 数据来源:mock_data.json(由 generate_mock_data.py 从真实 XLS 路口数据生成)
  6. * - 静态资源(视频/图片)在此处统一 import,接口直接返回完整数据
  7. * - 支持分页、筛选、排序等动态查询
  8. * - 每次请求信号倒计时、在线率、告警时间等实时数据会动态变化
  9. * - 路口状态会随机波动,模拟真实监控场景
  10. *
  11. * 使用:import { apiLogin, apiGetPoints, ... } from '@/pyscripts/api'
  12. * 所有接口返回 Promise<{ code, message, data }>
  13. */
  14. import mockData from './mock_data.json'
  15. // ── 静态资源(模拟 CDN / 后端返回的资源 URL)─────────────────────
  16. import video1 from '@/assets/videos/video1.mp4'
  17. import video2 from '@/assets/videos/video2.mp4'
  18. import video3 from '@/assets/videos/video3.mp4'
  19. import video4 from '@/assets/videos/video4.mp4'
  20. import arrow1 from '@/assets/images/arrow_1.png'
  21. import arrow2 from '@/assets/images/arrow_2.png'
  22. import arrow3 from '@/assets/images/arrow_3.png'
  23. import arrow4 from '@/assets/images/arrow_4.png'
  24. const VIDEOS = [video1, video2, video3, video4]
  25. const ARROWS = [arrow1, arrow2, arrow3, arrow4]
  26. function pickVideo(i) { return VIDEOS[i % VIDEOS.length] }
  27. // ── 工具 ─────────────────────────────────────────────────────────────
  28. function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
  29. function delay(base = 200) { return sleep(base + Math.floor(Math.random() * 200)) }
  30. function ok(data) { return { code: 200, message: 'success', data } }
  31. function fail(msg, code = 400) { return { code, message: msg, data: null } }
  32. /** 基于当前秒数产生稳定随机(同一秒内多次调用返回相同值) */
  33. function seededRand(seed) {
  34. const x = Math.sin(seed) * 10000
  35. return x - Math.floor(x)
  36. }
  37. /** 当前时间 HH:MM:SS */
  38. function nowTime() { return new Date().toLocaleTimeString() }
  39. function nowDate() { return new Date().toLocaleDateString() }
  40. // ── 数据缓存(首次 import 时加载) ──────────────────────────────────
  41. const DB = {
  42. points: mockData.points,
  43. menuTree: mockData.menuTree,
  44. tongzhouMenuTree: mockData.tongzhouMenuTree,
  45. trunkLineMenuTree: mockData.trunkLineMenuTree,
  46. homeData: mockData.homeData,
  47. deviceStatus: mockData.deviceStatus,
  48. timeSpaceData: mockData.timeSpaceData,
  49. crossingList: mockData.crossingList,
  50. securityRoutes: mockData.securityRoutes,
  51. securityTasks: mockData.securityTasks,
  52. filterOptions: mockData.filterOptions,
  53. signalTimings: mockData.sampleSignalTimings,
  54. intersectionConfigs: mockData.sampleIntersectionConfigs,
  55. }
  56. // ── 内部生成器 ───────────────────────────────────────────────────────
  57. /**
  58. * 生成摄像机模拟数据(基于 XLS 摄像机 Sheet 字段结构)
  59. * 字段:路口名、路口编号、摄像机编号、登录名称、摄像头密码、摄像头类型、端口号、IP地址、是否启用、位置
  60. */
  61. function _makeCameras(id, name, seed) {
  62. const positions = ['北进口', '南进口', '东进口', '西进口']
  63. const types = ['枪机', '球机']
  64. const numCams = 2 + (seed % 3) // 2~4 个
  65. return Array.from({ length: numCams }, (_, i) => ({
  66. intersection: name || id,
  67. intersectionId: id,
  68. cameraId: `CAM${(id || '000').slice(-6)}_${String(i + 1).padStart(2, '0')}`,
  69. loginName: `admin_${String(seed + i).slice(-4)}`,
  70. password: '******',
  71. cameraType: types[(seed + i) % types.length],
  72. port: 554 + i,
  73. ip: `192.168.${10 + (seed % 200)}.${100 + i}`,
  74. enabled: (seed + i) % 20 !== 0, // ~5% 禁用
  75. position: positions[i % positions.length],
  76. }))
  77. }
  78. /** 从摄像机列表推导各方向摄像头类型 (1枪机 2球机 0无) */
  79. function _camerasToArmTypes(cameras) {
  80. const posMap = { '北进口': 'N', '南进口': 'S', '东进口': 'E', '西进口': 'W' }
  81. const typeMap = { '枪机': 1, '球机': 2 }
  82. const result = { N: 0, S: 0, E: 0, W: 0 }
  83. cameras.forEach(c => {
  84. if (c.enabled) {
  85. const dir = posMap[c.position]
  86. if (dir) result[dir] = typeMap[c.cameraType] || 0
  87. }
  88. })
  89. return result
  90. }
  91. function _makeIntersectionConfig(id, name, { fixedNsGreen } = {}) {
  92. const phases = ['南北直行', '东西直行', '北单放', '东单放']
  93. const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false
  94. const countdown = 10 + Math.floor(Math.random() * 50)
  95. const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
  96. const cameras = _makeCameras(id, name, seed)
  97. const armCamTypes = _camerasToArmTypes(cameras)
  98. const lanePresets = [
  99. ['U', 'L', 'S', 'R'], ['U', 'L', 'S', 'R'],
  100. ['U', 'L', 'S', 'R'], ['U', 'L', 'S', 'R'],
  101. ]
  102. return {
  103. signals: {
  104. ns: { phaseName: phases[0], time: countdown, isGreen: nsGreen },
  105. ew: { phaseName: phases[1], time: countdown, isGreen: !nsGreen },
  106. },
  107. armsConfig: {
  108. N: { cameraType: armCamTypes.N, lanes: lanePresets[0] },
  109. S: { cameraType: armCamTypes.S, lanes: lanePresets[1] },
  110. E: { cameraType: armCamTypes.E, lanes: lanePresets[2] },
  111. W: { cameraType: armCamTypes.W, lanes: lanePresets[3] },
  112. },
  113. cameras,
  114. }
  115. }
  116. /**
  117. * 动态生成路口相位配时数据
  118. * @param {number} cycleLength 周期总时长
  119. * @param {boolean} isTwoRows 是否生成上下双排 8 相位 (默认 true)
  120. */
  121. function _makePhaseData(cycleLength = 140, isTwoRows = true) {
  122. const n = 4; // 4个阶段 (S1-S4)
  123. const stageTime = Math.floor(cycleLength / n);
  124. const pd = [];
  125. // ==========================================
  126. // 修改点:将单个图标改为用逗号分隔的"成对图标"字符串
  127. // 前端组件会按逗号切割并分别放到对角位置
  128. // ==========================================
  129. const iconsUD = [
  130. 'STRAIGHT_DOWN,STRAIGHT_UP', // 南北直行对放
  131. 'TURN_DOWN_LEFT,TURN_UP_LEFT', // 南北左转对放
  132. 'TURN_DOWN_LEFT_UTURN,TURN_UP_LEFT_UTURN' // 南北左转+掉头对放
  133. ];
  134. const iconsLR = [
  135. 'STRAIGHT_LEFT,STRAIGHT_RIGHT', // 东西直行对放
  136. 'TURN_LEFT_DOWN,TURN_RIGHT_UP', // 东西左转对放
  137. 'TURN_LEFT_DOWN_UTURN,TURN_RIGHT_UP_UTURN' // 东西左转+掉头对放
  138. ];
  139. const getRandomIcon = (pool) => pool[Math.floor(Math.random() * pool.length)];
  140. let t = 0;
  141. for (let i = 0; i < n; i++) {
  142. const stageStart = t;
  143. const stageEnd = stageStart + stageTime;
  144. const currentIconPool = (i < 2) ? iconsUD : iconsLR;
  145. // 辅助函数:生成单条轨道的一个阶段
  146. // 第8列 [7] 标记方向: 'ns'(南北) 或 'ew'(东西)
  147. const direction = (i < 2) ? 'ns' : 'ew';
  148. const pushTrackData = (trackIdx, phaseNamePrefix) => {
  149. // 这里的 icon 现在抽出来的是诸如 "STRAIGHT_DOWN,STRAIGHT_UP" 的字符串
  150. const icon = getRandomIcon(currentIconPool);
  151. const phaseName = `${phaseNamePrefix}${i + 1}`;
  152. const g = Math.floor(Math.random() * 11) + 20; // 绿灯 20-30s
  153. const s = 3; // 闪烁/条纹 3s
  154. const y = 2; // 黄灯 2s
  155. let curT = stageStart;
  156. // 1. 绿灯 (第6个索引项传入组装好的成对 icon 字符串, 第7个索引项标记方向)
  157. pd.push([trackIdx, curT, curT + g, phaseName, g, 'green', icon, direction]);
  158. curT += g;
  159. // 2. 绿闪/条纹
  160. pd.push([trackIdx, curT, curT + s, '', s, 'stripe', null, direction]);
  161. curT += s;
  162. // 3. 黄灯
  163. pd.push([trackIdx, curT, curT + y, '', y, 'yellow', null, direction]);
  164. curT += y;
  165. // 4. 红灯补齐 (确保阶段对齐)
  166. let remainRed = stageEnd - curT;
  167. if (remainRed > 0) {
  168. pd.push([trackIdx, curT, stageEnd, '', remainRed, 'red', null, direction]);
  169. }
  170. };
  171. pushTrackData(0, 'P'); // 生成第一排 (P1-P4)
  172. if (isTwoRows) {
  173. pushTrackData(1, 'P'); // 生成第二排 (P5-P8,由于逻辑相同,名称可根据需要改为 i+5)
  174. }
  175. t = stageEnd;
  176. }
  177. return pd;
  178. }
  179. function _makeCornerVideos() {
  180. return { nw: video1, ne: video2, sw: video3, se: video4 }
  181. }
  182. function _makeStageList() {
  183. return [1, 2, 3, 4].map((_, i) => ({
  184. value: String(i + 1), time: [30, 30, 50, 30][i], img: ARROWS[i],
  185. }))
  186. }
  187. function _makeCardPhases(activeIndex = 0) {
  188. return ARROWS.map((img, i) => ({
  189. id: i + 1, icon: ['↑', '↰', '↑', '↰'][i], img, active: i === activeIndex,
  190. }))
  191. }
  192. /** 动态波动整数值(基于基准值上下浮动) */
  193. function _fluctuate(base, range) {
  194. return base + Math.floor(Math.random() * range * 2) - range
  195. }
  196. /** 动态更新路口状态(每次调用随机波动少量路口) */
  197. function _dynamicPoints() {
  198. return DB.points.map((p, i) => {
  199. const r = seededRand(i + 31)
  200. const status = r < 0.78 ? 'normal' : r < 0.92 ? 'busy' : 'alarm'
  201. return { ...p, status, updatedAt: Date.now() - Math.floor(r * 120000) }
  202. })
  203. }
  204. // ═══════════════════════════════════════════════════════════════════════
  205. // E: 认证
  206. // ═══════════════════════════════════════════════════════════════════════
  207. /** 验证码状态(模拟服务端 session 存储) */
  208. let _captchaStore = { code: '', expireAt: 0 }
  209. /**
  210. * GET /api/auth/captcha — 获取验证码
  211. * 返回 4 位随机字符 + base64 图片(Canvas 绘制)
  212. * 真实后端应返回图片流,这里模拟返回验证码文本 + 配置,由前端 Canvas 绘制
  213. */
  214. export async function apiGetCaptcha() {
  215. await delay(100)
  216. const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
  217. const code = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
  218. // 存储验证码,有效期 60 秒
  219. _captchaStore = { code: code.toUpperCase(), expireAt: Date.now() + 60000 }
  220. return ok({
  221. captchaId: 'cap_' + Date.now(),
  222. code, // 模拟环境直接返回明文,真实环境不应返回
  223. length: 4,
  224. expireIn: 60, // 秒
  225. })
  226. }
  227. /**
  228. * POST /api/auth/login — 登录(含验证码校验)
  229. */
  230. export async function apiLogin({ username, password, captcha }) {
  231. await delay(300)
  232. // 验证码校验
  233. if (_captchaStore.code && captcha) {
  234. if (Date.now() > _captchaStore.expireAt) {
  235. return fail('验证码已过期,请刷新', 401)
  236. }
  237. if (captcha.toUpperCase() !== _captchaStore.code) {
  238. return fail('验证码错误', 401)
  239. }
  240. }
  241. // 账号密码校验
  242. if (username === 'admin' && password === '123456') {
  243. _captchaStore = { code: '', expireAt: 0 } // 登录成功后清除验证码
  244. return ok({ token: 'demo_token_' + Date.now(), user: { name: 'admin', role: '管理员' } })
  245. }
  246. return fail('账号或密码错误(演示账号:admin / 123456)', 401)
  247. }
  248. export async function apiChangePassword({ oldPassword, newPassword }) {
  249. await delay(300)
  250. if (oldPassword === '123456') return ok({ message: '密码修改成功' })
  251. return fail('原密码错误')
  252. }
  253. // ═══════════════════════════════════════════════════════════════════════
  254. // A: 路口基础数据
  255. // ═══════════════════════════════════════════════════════════════════════
  256. /**
  257. * GET /api/intersections — 路口点位列表
  258. * 每次调用路口状态会动态波动
  259. */
  260. export async function apiGetPoints(filters = {}) {
  261. await delay(200)
  262. let list = _dynamicPoints()
  263. if (filters.node) list = list.filter(p => p.node === filters.node)
  264. if (filters.status) list = list.filter(p => p.status === filters.status)
  265. if (filters.keyword) {
  266. const kw = filters.keyword.toLowerCase()
  267. list = list.filter(p => p.name.toLowerCase().includes(kw) || p.id.toLowerCase().includes(kw))
  268. }
  269. return ok(list)
  270. }
  271. /**
  272. * GET /api/intersections/:id — 路口详情
  273. * 信号倒计时每次请求动态变化
  274. */
  275. export async function apiGetIntersectionData(id, { fixedNsGreen } = {}) {
  276. await delay(250)
  277. const base = DB.intersectionConfigs[id]
  278. const point = DB.points.find(p => p.id === id)
  279. const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
  280. // 动态倒计时:基于当前秒数计算
  281. const nowSec = Math.floor(Date.now() / 1000)
  282. const cycle = 140
  283. const elapsed = nowSec % cycle
  284. const nsGreenVal = fixedNsGreen !== undefined ? fixedNsGreen : false
  285. const nsGreen = nsGreenVal
  286. const config = base ? {
  287. signals: {
  288. ns: { ...base.signals.ns, time: Math.max(1, cycle - elapsed), isGreen: nsGreen },
  289. ew: { ...base.signals.ew, time: elapsed || 1, isGreen: !nsGreen },
  290. },
  291. armsConfig: base.armsConfig,
  292. cameras: base.cameras,
  293. } : _makeIntersectionConfig(id, point ? point.name : id, { fixedNsGreen: nsGreenVal })
  294. return ok({
  295. ...config,
  296. id, name: point ? point.name : id,
  297. mainVideo: pickVideo(seed),
  298. cornerVideos: _makeCornerVideos(seed),
  299. })
  300. }
  301. /**
  302. * GET /api/intersections/:id/signal-timing — 信号配时
  303. * currentTime 随真实时间走动
  304. */
  305. export async function apiGetSignalTiming(id) {
  306. await delay(300)
  307. const preset = DB.signalTimings[id]
  308. if (preset) {
  309. const cycleLength = preset.data.cycleLength
  310. return {
  311. code: 200, message: 'success',
  312. data: { ...preset.data, currentTime: Math.floor(Date.now() / 1000) % cycleLength }
  313. }
  314. }
  315. const cycleLength = [100, 120, 130, 140, 150, 160][Math.floor(Math.random() * 6)]
  316. return ok({
  317. cycleLength,
  318. currentTime: Math.floor(Date.now() / 1000) % cycleLength,
  319. phaseData: _makePhaseData(cycleLength, false),
  320. })
  321. }
  322. /** GET /api/intersections/:id/stages */
  323. export async function apiGetIntersectionStages(id) {
  324. await delay(200)
  325. const timing = DB.signalTimings[id]
  326. if (timing) {
  327. const hasTrack1 = timing.data.phaseData.some(p => p[0] === 1)
  328. const phases = timing.data.phaseData.filter(p => p[0] === (hasTrack1 ? 1 : 0) && p[4] !== null)
  329. return ok(phases.map((p, i) => ({
  330. value: String(i + 1), time: p[4], phaseName: p[3], direction: p[6], img: ARROWS[i % ARROWS.length],
  331. })))
  332. }
  333. return ok(_makeStageList())
  334. }
  335. /** GET /api/intersections/:id/schemes */
  336. export async function apiGetSchemes(id) {
  337. await delay(150)
  338. return ok(DB.filterOptions.schemeOptions)
  339. }
  340. // ═══════════════════════════════════════════════════════════════════════
  341. // A4: 区域菜单树
  342. // ═══════════════════════════════════════════════════════════════════════
  343. export async function apiGetMenuTree(tabId = 'arterial') {
  344. await delay(250)
  345. return ok(tabId === 'arterial' ? DB.menuTree : DB.tongzhouMenuTree)
  346. }
  347. export async function apiGetTongzhouMenuTree() {
  348. await delay(250)
  349. return ok(DB.tongzhouMenuTree)
  350. }
  351. export async function apiGetTrunkLineMenuTree() {
  352. await delay(200)
  353. return ok(DB.trunkLineMenuTree)
  354. }
  355. // ═══════════════════════════════════════════════════════════════════════
  356. // B: 设备状态 & 首页(动态波动)
  357. // ═══════════════════════════════════════════════════════════════════════
  358. /**
  359. * GET /api/devices/status/summary
  360. * 在线数每次请求轻微波动
  361. */
  362. export async function apiGetDeviceStatus(type) {
  363. await delay(200)
  364. function fluctuateStats(base) {
  365. const online = base.chartData[0].value
  366. const total = online + base.chartData[1].value
  367. const newOnline = _fluctuate(online, Math.ceil(total * 0.02))
  368. const clamped = Math.max(0, Math.min(total, newOnline))
  369. const rate = Math.round(clamped / total * 100)
  370. return {
  371. centerTitle: rate + '%',
  372. centerSubTitle: `${clamped}/${total}`,
  373. chartData: [
  374. { ...base.chartData[0], value: clamped },
  375. { ...base.chartData[1], value: total - clamped },
  376. ]
  377. }
  378. }
  379. if (type && DB.deviceStatus[type]) return ok(fluctuateStats(DB.deviceStatus[type]))
  380. return ok({
  381. signalMachine: fluctuateStats(DB.deviceStatus.signalMachine),
  382. detector: fluctuateStats(DB.deviceStatus.detector),
  383. camera: fluctuateStats(DB.deviceStatus.camera),
  384. })
  385. }
  386. /**
  387. * GET /api/home/snapshot — 首页快照
  388. * 在线数波动 + 时间实时更新 + 告警时间刷新
  389. */
  390. export async function apiGetHomeSnapshot() {
  391. await delay(200)
  392. const total = DB.homeData.online.total
  393. const online = _fluctuate(Math.round(total * 0.93), 30)
  394. const clamped = Math.max(0, Math.min(total, online))
  395. const fault = Math.floor(Math.random() * 5)
  396. return ok({
  397. header: { ...DB.homeData.header, timeText: nowTime(), dateText: nowDate() },
  398. online: { online: clamped, offline: total - clamped, total, rate: Math.round(clamped / total * 100) },
  399. alarms: DB.homeData.alarms.map(a => ({
  400. ...a,
  401. time: new Date(Date.now() - Math.floor(Math.random() * 3600000)).toLocaleTimeString(),
  402. })),
  403. duty: DB.homeData.duty,
  404. device: { normal: total - fault, fault },
  405. controlModes: DB.homeData.controlModes,
  406. keyIntersections: DB.homeData.keyIntersections,
  407. })
  408. }
  409. /** GET /api/home/control-mode-stats — 控制模式分布(轻微波动) */
  410. export async function apiGetControlModeStats() {
  411. await delay(150)
  412. return ok(DB.homeData.controlModes.map(m => ({
  413. ...m, value: _fluctuate(m.value, Math.ceil(m.value * 0.05)),
  414. })))
  415. }
  416. /**
  417. * GET /api/alarms/latest — 告警列表(分页 + 动态时间)
  418. * @param {{ page?: number, pageSize?: number, level?: string }} params
  419. */
  420. export async function apiGetLatestAlarms(params = {}) {
  421. await delay(200)
  422. const typeMap = { high: 'error', mid: 'warning', low: 'warning' }
  423. let alarms = DB.homeData.alarmList.map((a, i) => ({
  424. id: a.id, title: a.title, type: a.type || typeMap[a.level] || 'warning',
  425. time: new Date(Date.now() - i * 180000 - Math.floor(Math.random() * 60000)).toLocaleTimeString(),
  426. description: a.description || `${a.loc}-${a.title}`,
  427. position: a.position, level: a.level, loc: a.loc,
  428. }))
  429. if (params.level) alarms = alarms.filter(a => a.level === params.level)
  430. const page = params.page || 1
  431. const pageSize = params.pageSize || 10
  432. const start = (page - 1) * pageSize
  433. return ok({ total: alarms.length, page, pageSize, list: alarms.slice(start, start + pageSize) })
  434. }
  435. // ═══════════════════════════════════════════════════════════════════════
  436. // C: 勤务 & 任务(分页 + 筛选)
  437. // ═══════════════════════════════════════════════════════════════════════
  438. /**
  439. * GET /api/tasks — 勤务任务列表
  440. * 支持 page / pageSize / status / keyword 筛选
  441. */
  442. export async function apiGetTasks(params = {}) {
  443. await delay(200)
  444. let list = [...DB.securityTasks]
  445. if (params.status) list = list.filter(t => t.status === params.status)
  446. if (params.keyword) {
  447. const kw = params.keyword.toLowerCase()
  448. list = list.filter(t => t.name.toLowerCase().includes(kw) || t.executor.toLowerCase().includes(kw))
  449. }
  450. if (params.level) list = list.filter(t => t.level === params.level)
  451. const page = params.page || 1
  452. const pageSize = params.pageSize || 5
  453. const total = list.length
  454. const totalPages = Math.ceil(total / pageSize)
  455. const start = (page - 1) * pageSize
  456. return ok({
  457. total, page, pageSize, totalPages,
  458. list: list.slice(start, start + pageSize),
  459. })
  460. }
  461. /**
  462. * GET /api/security-routes — 勤务路线(含视频)
  463. */
  464. export async function apiGetSecurityRoutes() {
  465. await delay(200)
  466. return ok(DB.securityRoutes.map((r, i) => ({
  467. ...r, mainVideo: pickVideo(i), cornerVideos: _makeCornerVideos(i),
  468. })))
  469. }
  470. /** GET /api/security-routes/:id */
  471. export async function apiGetSecurityRouteDetail(id) {
  472. await delay(200)
  473. const idx = DB.securityRoutes.findIndex(r => r.id === id)
  474. const route = DB.securityRoutes[idx >= 0 ? idx : 0]
  475. if (!route) return fail('路线不存在', 404)
  476. return ok({ ...route, mainVideo: pickVideo(idx >= 0 ? idx : 0), cornerVideos: _makeCornerVideos(idx >= 0 ? idx : 0) })
  477. }
  478. /** GET /api/key-intersections */
  479. export async function apiGetKeyIntersections() {
  480. await delay(150)
  481. return ok(DB.homeData.keyIntersections)
  482. }
  483. // ═══════════════════════════════════════════════════════════════════════
  484. // D: 交通时空图
  485. // ═══════════════════════════════════════════════════════════════════════
  486. export async function apiGetTrafficTimeSpace(opts = {}) {
  487. await delay(300)
  488. const { speed = 15, cycle = 120, band = 40, totalTime = 1800 } = opts
  489. const intersections = opts.intersections || DB.timeSpaceData.intersections
  490. const rawDistances = opts.distances || DB.timeSpaceData.distances
  491. // 将不均匀的物理距离归一化为等间距,保证绿波带视觉对齐
  492. const step = 500
  493. const distances = rawDistances.map((_, i) => i * step)
  494. const maxDist = distances[distances.length - 1]
  495. const waveData = [], greenData = []
  496. for (let t = 0; t <= totalTime; t += cycle) {
  497. const upKmh = 45 + Math.random() * 10 // 45-55 km/h 随机
  498. const downKmh = 45 + Math.random() * 10
  499. const upSpd = upKmh / 3.6 // 转 m/s
  500. const downSpd = downKmh / 3.6
  501. const ds = t + cycle / 2
  502. 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) })
  503. 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) })
  504. distances.forEach(y => {
  505. greenData.push({ y, start: t + y / upSpd, end: t + y / upSpd + band })
  506. greenData.push({ y, start: ds + (maxDist - y) / downSpd, end: ds + (maxDist - y) / downSpd + band })
  507. })
  508. }
  509. return ok({ intersections, distances, waveData, greenData })
  510. }
  511. // ═══════════════════════════════════════════════════════════════════════
  512. // 路口列表表格(分页 + 筛选 + 排序 + 动态状态)
  513. // ═══════════════════════════════════════════════════════════════════════
  514. /**
  515. * GET /api/crossings — 路口列表(718条全量,支持翻页)
  516. * @param {{ keyword, subArea, status, node, isKey, page, pageSize, sortBy, sortOrder }} params
  517. */
  518. export async function apiGetCrossingList(params = {}) {
  519. await delay(250)
  520. // 动态状态:每次请求路口状态会变化
  521. const statuses = ['在线', '在线', '在线', '在线', '离线']
  522. // 基于页码生成页级起始偏移(同一页固定,不同页不同)
  523. const page = params.page || 1
  524. const pageOffset = Math.floor(seededRand(page * 97) * 120)
  525. let list = DB.crossingList.map((r, i) => {
  526. const preset = DB.signalTimings[r.id]
  527. const cycleLength = preset ? preset.data.cycleLength : r.cycle
  528. // const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false)
  529. // 强制全部用 _makePhaseData 动态生成成对箭头
  530. const phaseData = _makePhaseData(cycleLength, false)
  531. return {
  532. ...r,
  533. status: statuses[Math.floor(seededRand(i + 42) * statuses.length)],
  534. cycle: cycleLength,
  535. phaseData,
  536. currentTime: Math.floor(seededRand(i * 31 + page * 97) * cycleLength),
  537. }
  538. })
  539. // 筛选(兼容中英文值映射)
  540. if (params.keyword || params.name) {
  541. const kw = (params.keyword || params.name).toLowerCase()
  542. list = list.filter(r => r.name.toLowerCase().includes(kw) || r.id.toLowerCase().includes(kw))
  543. }
  544. if (params.subArea) list = list.filter(r => r.subArea === params.subArea)
  545. if (params.status) {
  546. const statusMap = { 'online': '在线', 'offline': '离线', 'fault': '故障' }
  547. const mapped = statusMap[params.status] || params.status
  548. list = list.filter(r => r.status === mapped)
  549. }
  550. if (params.node) list = list.filter(r => r.node === params.node)
  551. if (params.isKey !== undefined && params.isKey !== '') {
  552. const boolVal = params.isKey === 'yes' || params.isKey === true
  553. list = list.filter(r => r.isKey === boolVal)
  554. }
  555. if (params.timeOffset) {
  556. if (params.timeOffset === 'none' || params.timeOffset === '无偏差') {
  557. list = list.filter(r => r.timeOffset === '无偏差')
  558. } else {
  559. list = list.filter(r => r.timeOffset !== '无偏差')
  560. }
  561. }
  562. // 排序
  563. if (params.sortBy) {
  564. const key = params.sortBy
  565. const dir = params.sortOrder === 'desc' ? -1 : 1
  566. list.sort((a, b) => {
  567. if (a[key] < b[key]) return -1 * dir
  568. if (a[key] > b[key]) return 1 * dir
  569. return 0
  570. })
  571. }
  572. // 分页
  573. const pageSize = params.pageSize || 10
  574. const total = list.length
  575. const totalPages = Math.ceil(total / pageSize)
  576. const start = (page - 1) * pageSize
  577. return ok({
  578. total, page, pageSize, totalPages,
  579. list: list.slice(start, start + pageSize),
  580. })
  581. }
  582. /** GET /api/dict/:type */
  583. export async function apiGetDictOptions(type) {
  584. await delay(100)
  585. if (DB.filterOptions[type]) return ok(DB.filterOptions[type])
  586. return fail('未知字典类型: ' + type, 404)
  587. }
  588. // ═══════════════════════════════════════════════════════════════════════
  589. // F: 设备操作
  590. // ═══════════════════════════════════════════════════════════════════════
  591. export async function apiRestartDevice(id) {
  592. await delay(500)
  593. return ok({ message: `设备 ${id} 重启指令已下发` })
  594. }
  595. export async function apiUpgradeDevice(id, file) {
  596. await delay(800)
  597. return ok({ message: `设备 ${id} 升级任务已创建`, version: 'V3.3.0' })
  598. }
  599. // ═══════════════════════════════════════════════════════════════════════
  600. // H: 弹窗专用(动态倒计时 + 实时状态)
  601. // ═══════════════════════════════════════════════════════════════════════
  602. /**
  603. * GET /api/special-task/:id/monitor — 特勤监控面板
  604. * 信号灯倒计时随真实时间变化
  605. */
  606. export async function apiGetSpecialTaskMonitorData(id, { fixedNsGreen } = {}) {
  607. await delay(400)
  608. // 用 id 生成稳定 seed,兼容数字/字符串/undefined
  609. let numId = typeof id === 'number' ? id : parseInt(id)
  610. if (!numId || isNaN(numId)) {
  611. // 非数字 id(如字符串),用 charCode 求和
  612. numId = id ? Array.from(String(id)).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 1
  613. }
  614. const seed = Math.abs(numId * 7) || 7
  615. // 任务信息——不同任务不同数据
  616. const taskIdx = Math.abs(numId - 1) % (DB.securityTasks.length || 1)
  617. const task = DB.securityTasks.find(t => t.id === id || t.id === numId) || DB.securityTasks[taskIdx] || {}
  618. 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']
  619. const statusColorMap = {
  620. '未开始': '#ffaa00',
  621. '进行中': '#ff4d4f',
  622. '已完成': '#8dc453',
  623. }
  624. const taskStatus = task.status || '未开始'
  625. const taskInfo = {
  626. name: task.name || '特勤路线',
  627. time: timeSlots[seed % timeSlots.length],
  628. manager: task.executor || DB.securityTasks[seed % DB.securityTasks.length]?.executor || '王建国',
  629. level: task.level || (seed % 3 === 0 ? '二级' : '一级'),
  630. status: taskStatus,
  631. statusColor: statusColorMap[taskStatus] || '#ffaa00',
  632. }
  633. // 根据任务 id 选取不同的关键路口(每个任务关联不同的4个路口)
  634. const allKeyPoints = DB.points.filter(p => p.isKey)
  635. const startIdx = (seed * 3) % Math.max(allKeyPoints.length - 4, 1)
  636. const keyPoints = allKeyPoints.slice(startIdx, startIdx + 4)
  637. // 视频
  638. const videos = keyPoints.map((_, i) => ({ id: i + 1, url: pickVideo(seed + i) }))
  639. // 每个路口的控制模式和状态颜色根据路口 id 变化
  640. const modeList = ['步进', '系统', '定周期', '感应', '手动']
  641. const colorList = ['#ffaa00', '#00e5ff', '#68e75f', '#00e5ff']
  642. const nowSec = Math.floor(Date.now() / 1000)
  643. const allLanes = ['U', 'L', 'S', 'R'];
  644. const lanePresets = [
  645. { N: allLanes, S: allLanes, E: allLanes, W: allLanes },
  646. ]
  647. const intersections = keyPoints.map((jnc, i) => {
  648. const jncSeed = Array.from(jnc.id).reduce((s, c, idx) => s + c.charCodeAt(0) * (idx + 1), 0)
  649. const cycle = [100, 120, 130, 140, 150][jncSeed % 5]
  650. const elapsed = nowSec % cycle
  651. const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false
  652. const countdown = Math.max(1, cycle - elapsed)
  653. const laneSet = lanePresets[jncSeed % lanePresets.length]
  654. // 用摄像机字段结构生成,再推导 cameraType
  655. const cameras = _makeCameras(jnc.id, jnc.name, jncSeed)
  656. const armCamTypes = _camerasToArmTypes(cameras)
  657. return {
  658. id: jnc.id, name: jnc.name,
  659. statusColor: colorList[jncSeed % colorList.length],
  660. stage: (Math.floor(elapsed / (cycle / 4)) % 4) + 1,
  661. mode: modeList[jncSeed % modeList.length],
  662. timeLeft: countdown,
  663. btnText: i === 0 ? '立即解锁' : '立即执行',
  664. btnType: i === 0 ? 'normal' : 'primary',
  665. phases: _makeCardPhases(Math.floor(elapsed / (cycle / 4)) % 4),
  666. cameras,
  667. mapData: {
  668. armsConfig: {
  669. N: { lanes: laneSet.N, cameraType: armCamTypes.N },
  670. S: { lanes: laneSet.S, cameraType: armCamTypes.S },
  671. E: { lanes: laneSet.E, cameraType: armCamTypes.E },
  672. W: { lanes: laneSet.W, cameraType: armCamTypes.W },
  673. },
  674. signals: {
  675. ns: { isGreen: nsGreen, time: countdown, phaseName: '南北直行' },
  676. ew: { isGreen: !nsGreen, time: Math.max(1, cycle - countdown), phaseName: '东西直行' },
  677. },
  678. },
  679. videoUrls: _makeCornerVideos(seed + i),
  680. }
  681. })
  682. return ok({ taskInfo, videos, intersections })
  683. }
  684. /**
  685. * GET /api/crossing/panel/:id — CrossingPanel 弹窗
  686. * 倒计时实时变化
  687. */
  688. export async function apiGetCrossingPanelData(id) {
  689. await delay(300)
  690. const point = DB.points.find(p => p.id === id)
  691. const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false })
  692. const seed = id ? id.charCodeAt(id.length - 1) : 0
  693. const preset = DB.signalTimings[id]
  694. const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][Math.abs(seed) % 6]
  695. const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false)
  696. const currentTime = Math.floor(Date.now() / 1000) % cycleLength
  697. return ok({
  698. currentRoute: {
  699. id, name: point ? point.name : id,
  700. level: point?.isKey ? '一级' : '二级',
  701. mode: '快进', time: cycleLength + 's',
  702. mainVideo: pickVideo(seed), cornerVideos: _makeCornerVideos(seed),
  703. },
  704. intersectionData: config,
  705. phaseData,
  706. cycleLength, currentTime,
  707. })
  708. }
  709. /**
  710. * GET /api/crossing/detail/:id — CrossingDetailPanel 弹窗
  711. */
  712. export async function apiGetCrossingDetailData(id) {
  713. await delay(350)
  714. const point = DB.points.find(p => p.id === id)
  715. const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false })
  716. // 用 id 的全部字符生成稳定 seed(加权位置避免 charCode 总和碰撞)
  717. const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
  718. // 从真实阶段数据推导周期和相位
  719. const preset = DB.signalTimings[id]
  720. const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][seed % 6]
  721. const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength || 140, false)
  722. // 从相位数据中提取阶段列表(优先上轨道绿灯相位,单轨道时取 track 0,最多4个)
  723. const hasTrack1 = phaseData.some(p => p[0] === 1)
  724. const greenPhases = phaseData.filter(p => p[0] === (hasTrack1 ? 1 : 0) && p[4] !== null).slice(0, 4)
  725. const stageList = greenPhases.map((p, i) => ({
  726. value: String(i + 1),
  727. time: p[4],
  728. phaseName: p[3],
  729. direction: p[6],
  730. img: ARROWS[i],
  731. }))
  732. // 控制方式选项 + 根据路口选择不同的当前控制方式
  733. const allMethods = [
  734. { label: '定周期', value: 'fixed' },
  735. { label: '黄闪', value: 'yellow_flash' },
  736. { label: '关灯', value: 'lights_off' },
  737. { label: '步进', value: 'step' },
  738. { label: '系统方案', value: 'system' },
  739. { label: '感应控制', value: 'sensor' },
  740. { label: '临时方案', value: 'temp' },
  741. ]
  742. const methodValues = ['fixed', 'step', 'system', 'sensor', 'temp']
  743. const currentMethod = methodValues[seed % methodValues.length]
  744. // 控制模式(显示用)
  745. const controlModes = ['定周期控制', '感应控制', '干线协调', '自适应控制']
  746. const currentMode = controlModes[seed % controlModes.length]
  747. // 控制方案:根据当前控制方式给出不同方案选项
  748. const schemeMap = {
  749. 'fixed': [
  750. { label: '早高峰', value: 'early_peak' },
  751. { label: '晚高峰', value: 'evening_peak' },
  752. { label: '平峰', value: 'normal' },
  753. { label: '夜间', value: 'night' },
  754. { label: '周末', value: 'weekend' },
  755. ],
  756. 'sensor': [
  757. { label: '全感应', value: 'full_actuated' },
  758. { label: '半感应', value: 'semi_actuated' },
  759. ],
  760. 'system': [
  761. { label: '系统优化方案A', value: 'sys_a' },
  762. { label: '系统优化方案B', value: 'sys_b' },
  763. { label: '系统优化方案C', value: 'sys_c' },
  764. ],
  765. 'step': [
  766. { label: '步进方案1', value: 'step_1' },
  767. { label: '步进方案2', value: 'step_2' },
  768. ],
  769. 'temp': [
  770. { label: '临时方案A', value: 'temp_a' },
  771. { label: '临时方案B', value: 'temp_b' },
  772. { label: '临时方案C', value: 'temp_c' },
  773. ],
  774. 'yellow_flash': [{ label: '黄闪默认', value: 'yf_default' }],
  775. 'lights_off': [{ label: '关灯默认', value: 'lo_default' }],
  776. }
  777. const schemeOptions = schemeMap[currentMethod] || schemeMap['fixed']
  778. // 相位差和协调时间基于 seed 稳定变化
  779. const phaseDiff = (seed * 7) % 25
  780. const coordTime = (seed * 13) % 60
  781. return ok({
  782. currentRoute: {
  783. id, name: point ? point.name : id,
  784. level: point?.isKey ? '一级' : '二级',
  785. mode: currentMode,
  786. time: cycleLength + 's',
  787. mainVideo: pickVideo(seed),
  788. cornerVideos: _makeCornerVideos(seed),
  789. },
  790. intersectionData: config,
  791. phaseData,
  792. cycleLength,
  793. currentTime: Math.floor(Date.now() / 1000) % cycleLength,
  794. phaseDiff,
  795. coordTime,
  796. stageList,
  797. schemeOptions,
  798. currentScheme: schemeOptions[0].value,
  799. controlMode: currentMode,
  800. controlMethodOptions: allMethods,
  801. currentMethod,
  802. locktimeOptions: [
  803. { label: '30', value: 30 },
  804. { label: '50', value: 50 },
  805. { label: '100', value: 100 },
  806. { label: '300', value: 300 },
  807. ],
  808. })
  809. }
  810. /**
  811. * GET /api/crossing/top-charts — 路口Tab顶部圆环图(动态波动)
  812. */
  813. export async function apiGetCrossingTopCharts() {
  814. await delay(150)
  815. const sm = DB.deviceStatus.signalMachine
  816. const baseOnline = sm.chartData[0].value
  817. const total = baseOnline + sm.chartData[1].value
  818. const online = _fluctuate(baseOnline, Math.ceil(total * 0.02))
  819. const onlineChart = {
  820. chartData: [
  821. { name: '在线', value: online, color: '#4DF5F8' },
  822. { name: '离线', value: total - online, color: '#FFD369' },
  823. ],
  824. centerTitle: Math.round(online / total * 100) + '%',
  825. centerSubTitle: `${online}/${total}`,
  826. }
  827. const faultTotal = total - online
  828. const comm = Math.floor(faultTotal * 0.26)
  829. const det = Math.floor(faultTotal * 0.21)
  830. const lamp = Math.floor(faultTotal * 0.38)
  831. const conflict = faultTotal - comm - det - lamp
  832. const faultChart = {
  833. chartData: [
  834. { name: '通信', value: comm, color: '#4DF5F8' },
  835. { name: '检测器', value: det, color: '#FFA033' },
  836. { name: '灯控', value: lamp, color: '#FFF587' },
  837. { name: '冲突', value: conflict, color: '#FF4D4F' },
  838. ],
  839. centerTitle: Math.round(faultTotal / total * 100) + '%',
  840. centerSubTitle: `${faultTotal}/${total}`,
  841. }
  842. return ok({ onlineChart, faultChart })
  843. }
  844. /** GET /api/overview/top-charts — 总览Tab顶部图表(动态) */
  845. export async function apiGetOverviewTopCharts() {
  846. await delay(150)
  847. const res = await apiGetDeviceStatus()
  848. return ok({ onlineStatus: res.data, deviceStatus: res.data })
  849. }
  850. /**
  851. * GET /api/map/legend-config — 地图标注线路配置
  852. */
  853. export async function apiGetMapLegendConfig() {
  854. await delay(150)
  855. const tzPoints = DB.points.filter(p => p.node && p.node.includes('通州'))
  856. const lngs = tzPoints.map(p => p.lng), lats = tzPoints.map(p => p.lat)
  857. const [minLng, maxLng] = [Math.min(...lngs), Math.max(...lngs)]
  858. const [minLat, maxLat] = [Math.min(...lats), Math.max(...lats)]
  859. const midLat = (minLat + maxLat) / 2
  860. const hStep = (maxLat - minLat) / 5, vStep = (maxLng - minLng) / 8
  861. return ok([
  862. { name: '中心计划', start: [minLng, midLat + hStep], end: [maxLng, midLat + hStep], color: '#004CDE' },
  863. { name: '干线协调', start: [minLng, midLat], end: [maxLng, midLat], color: '#13C373' },
  864. { name: '勤务路线', start: [minLng, midLat - hStep], end: [maxLng, midLat - hStep], color: '#BC301D' },
  865. { name: '定周期控制', start: [minLng + vStep, maxLat], end: [minLng + vStep, minLat], color: '#3296FA' },
  866. { name: '感应控制', start: [minLng + vStep * 2, maxLat], end: [minLng + vStep * 2, minLat], color: '#FF864C' },
  867. { name: '自适应控制', start: [minLng + vStep * 3, maxLat], end: [minLng + vStep * 3, minLat], color: '#9F6EFE' },
  868. { name: '手动控制', start: [minLng + vStep * 4, maxLat], end: [minLng + vStep * 4, minLat], color: '#EB9F36' },
  869. { name: '特殊控制', start: [minLng + vStep * 5, maxLat], end: [minLng + vStep * 5, minLat], color: '#A26218' },
  870. { name: '离线', start: [minLng, maxLat - hStep * 0.3], end: [maxLng, maxLat - hStep * 0.3], color: '#7A7A7A' },
  871. { name: '降级', start: [minLng, minLat + hStep * 0.3], end: [maxLng, minLat + hStep * 0.3], color: '#D9C13B' },
  872. { name: '故障', start: [maxLng - vStep * 0.5, maxLat], end: [maxLng - vStep * 0.5, minLat], color: '#FF3938' },
  873. ])
  874. }
  875. /**
  876. * GET /api/devices/fault-status
  877. * 设备故障统计(匹配 DeviceStatusTabs 组件的 statusData 格式)
  878. * keys: signalMachineStatus, detectorStatus, trafficLightStatus
  879. */
  880. export async function apiGetDeviceFaultStatus() {
  881. await delay(200)
  882. const sm = DB.deviceStatus.signalMachine
  883. const dt = DB.deviceStatus.detector
  884. const cam = DB.deviceStatus.camera
  885. // 从在线数据推算故障数,每次波动
  886. const smTotal = sm.chartData[0].value + sm.chartData[1].value
  887. const smFault = _fluctuate(sm.chartData[1].value, 3)
  888. const dtTotal = dt.chartData[0].value + dt.chartData[1].value
  889. const dtFault = _fluctuate(dt.chartData[1].value, 5)
  890. const camTotal = cam.chartData[0].value + cam.chartData[1].value
  891. const camFault = _fluctuate(cam.chartData[1].value, 2)
  892. return ok({
  893. signalMachineStatus: {
  894. centerTitle: Math.max(0, smFault) + '',
  895. centerSubTitle: `${Math.max(0, smFault)}/${smTotal}`,
  896. chartData: [
  897. { name: '正常', value: Math.max(0, smTotal - smFault), color: '#A0E551' },
  898. { name: '故障', value: Math.max(0, smFault), color: '#D03030' },
  899. ]
  900. },
  901. detectorStatus: {
  902. centerTitle: Math.max(0, dtFault) + '',
  903. centerSubTitle: `${Math.max(0, dtFault)}/${dtTotal}`,
  904. chartData: [
  905. { name: '通信故障', value: Math.max(0, Math.floor(dtFault * 0.6)), color: '#C6302B' },
  906. { name: '数据异常', value: Math.max(0, dtFault - Math.floor(dtFault * 0.6)), color: '#faad14' },
  907. ]
  908. },
  909. trafficLightStatus: {
  910. centerTitle: Math.max(0, camFault) + '',
  911. centerSubTitle: `${Math.max(0, camFault)}/${camTotal}`,
  912. chartData: [
  913. { name: '红绿冲突', value: Math.max(0, Math.floor(camFault * 0.5)), color: '#C6302B' },
  914. { name: '红灯故障', value: Math.max(0, camFault - Math.floor(camFault * 0.5)), color: '#8F1E1E' },
  915. ]
  916. },
  917. })
  918. }