api.js 46 KB

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