|
|
@@ -50,6 +50,20 @@ function seededRand(seed) {
|
|
|
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() }
|
|
|
@@ -156,7 +170,14 @@ function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default'
|
|
|
* @param {number} cycleLength 周期总时长
|
|
|
* @param {boolean} isTwoRows 是否生成上下双排 8 相位 (默认 true)
|
|
|
*/
|
|
|
-function _makePhaseData(cycleLength = 140, isTwoRows = true, iconMode = 'default') {
|
|
|
+// 相位数据缓存:同一路口 (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];
|
|
|
@@ -165,10 +186,6 @@ function _makePhaseData(cycleLength = 140, isTwoRows = true, iconMode = 'default
|
|
|
stageTimes[0] += cycleLength - stageTimes.reduce((a, b) => a + b, 0);
|
|
|
const pd = [];
|
|
|
|
|
|
- // ==========================================
|
|
|
- // 修改点:将单个图标改为用逗号分隔的"成对图标"字符串
|
|
|
- // 前端组件会按逗号切割并分别放到对角位置
|
|
|
- // ==========================================
|
|
|
// 固定4个阶段的图标和方向:P1南北直行、P2南北左转、P3东西直行、P4东西左转
|
|
|
const phaseConfigMap = {
|
|
|
default: [
|
|
|
@@ -218,11 +235,13 @@ function _makePhaseData(cycleLength = 140, isTwoRows = true, iconMode = 'default
|
|
|
|
|
|
pushTrackData(0, 'P'); // 生成第一排 (P1-P4)
|
|
|
if (isTwoRows) {
|
|
|
- pushTrackData(1, 'P'); // 生成第二排 (P5-P8,由于逻辑相同,名称可根据需要改为 i+5)
|
|
|
+ pushTrackData(1, 'P'); // 生成第二排
|
|
|
}
|
|
|
|
|
|
t = stageEnd;
|
|
|
}
|
|
|
+
|
|
|
+ _phaseDataCache[cacheKey] = pd;
|
|
|
return pd;
|
|
|
}
|
|
|
|
|
|
@@ -375,19 +394,11 @@ export async function apiGetIntersectionData(id, { fixedNsGreen } = {}) {
|
|
|
*/
|
|
|
export async function apiGetSignalTiming(id) {
|
|
|
await delay(300)
|
|
|
- const preset = DB.signalTimings[id]
|
|
|
- if (preset) {
|
|
|
- const cycleLength = preset.data.cycleLength
|
|
|
- return {
|
|
|
- code: 200, message: 'success',
|
|
|
- data: { ...preset.data, currentTime: Math.floor(Date.now() / 1000) % cycleLength }
|
|
|
- }
|
|
|
- }
|
|
|
- const cycleLength = [100, 120, 130, 140, 150, 160][Math.floor(Math.random() * 6)]
|
|
|
+ const cycleLength = _getCycleLength(id)
|
|
|
return ok({
|
|
|
cycleLength,
|
|
|
currentTime: Math.floor(Date.now() / 1000) % cycleLength,
|
|
|
- phaseData: _makePhaseData(cycleLength, false),
|
|
|
+ phaseData: _makePhaseData(cycleLength, false, 'simple', id),
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -438,6 +449,23 @@ export async function apiGetTrunkLineMenuTree() {
|
|
|
* 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) {
|
|
|
@@ -455,9 +483,24 @@ export async function apiGetDeviceStatus(type) {
|
|
|
]
|
|
|
}
|
|
|
}
|
|
|
+ // 信号机:离线数 = 故障总数(与设备状态同步)
|
|
|
+ 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: fluctuateStats(DB.deviceStatus.signalMachine),
|
|
|
+ signalMachine: smStats(),
|
|
|
detector: fluctuateStats(DB.deviceStatus.detector),
|
|
|
camera: fluctuateStats(DB.deviceStatus.camera),
|
|
|
})
|
|
|
@@ -488,12 +531,20 @@ export async function apiGetHomeSnapshot() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-/** GET /api/home/control-mode-stats — 控制模式分布(轻微波动) */
|
|
|
+/** GET /api/home/control-mode-stats — 控制模式分布(总数与信号机总数同步,内部重分配) */
|
|
|
export async function apiGetControlModeStats() {
|
|
|
await delay(150)
|
|
|
- return ok(DB.homeData.controlModes.map(m => ({
|
|
|
- ...m, value: _fluctuate(m.value, Math.ceil(m.value * 0.05)),
|
|
|
- })))
|
|
|
+ const sm = DB.deviceStatus.signalMachine
|
|
|
+ const total = sm.chartData[0].value + sm.chartData[1].value
|
|
|
+ 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 + (total - currentSum))
|
|
|
+ return ok(fluctuated)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -621,12 +672,8 @@ export async function apiGetCrossingList(params = {}) {
|
|
|
const page = params.page || 1
|
|
|
const pageOffset = Math.floor(seededRand(page * 97) * 120)
|
|
|
let list = DB.crossingList.map((r, i) => {
|
|
|
- const preset = DB.signalTimings[r.id]
|
|
|
- const cycleLength = preset ? preset.data.cycleLength : r.cycle
|
|
|
- // const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false)
|
|
|
-
|
|
|
- // 强制全部用 _makePhaseData 动态生成成对箭头
|
|
|
- const phaseData = _makePhaseData(cycleLength, false)
|
|
|
+ const cycleLength = _getCycleLength(r.id)
|
|
|
+ const phaseData = _makePhaseData(cycleLength, false, 'simple', r.id)
|
|
|
return {
|
|
|
...r,
|
|
|
status: _getDeviceStatus(r.id),
|
|
|
@@ -810,15 +857,14 @@ 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 = id ? id.charCodeAt(id.length - 1) : 0
|
|
|
+ const seed = _idSeed(id)
|
|
|
// 确保 config 有 status
|
|
|
if (!config.status) {
|
|
|
config.status = _getDeviceStatus(id)
|
|
|
}
|
|
|
|
|
|
- const preset = DB.signalTimings[id]
|
|
|
- const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][Math.abs(seed) % 6]
|
|
|
- const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength, false, 'simple')
|
|
|
+ const cycleLength = _getCycleLength(id)
|
|
|
+ const phaseData = _makePhaseData(cycleLength, false, 'simple', id)
|
|
|
const currentTime = Math.floor(Date.now() / 1000) % cycleLength
|
|
|
|
|
|
return ok({
|
|
|
@@ -842,8 +888,7 @@ export async function apiGetCrossingDetailData(id, { iconMode = 'default' } = {}
|
|
|
const point = DB.points.find(p => p.id === id)
|
|
|
const config = DB.intersectionConfigs[id] || _makeIntersectionConfig(id, null, { fixedNsGreen: false, iconMode })
|
|
|
|
|
|
- // 用 id 的全部字符生成稳定 seed(加权位置避免 charCode 总和碰撞)
|
|
|
- const seed = id ? Array.from(id).reduce((s, c, i) => s + c.charCodeAt(0) * (i + 1), 0) : 0
|
|
|
+ const seed = _idSeed(id)
|
|
|
|
|
|
// 确保 config 有 status 字段(预存配置可能缺失)
|
|
|
if (!config.status) {
|
|
|
@@ -851,9 +896,8 @@ export async function apiGetCrossingDetailData(id, { iconMode = 'default' } = {}
|
|
|
}
|
|
|
|
|
|
// 从真实阶段数据推导周期和相位
|
|
|
- const preset = DB.signalTimings[id]
|
|
|
- const cycleLength = preset ? preset.data.cycleLength : [100, 120, 130, 140, 150, 160][seed % 6]
|
|
|
- const phaseData = preset ? preset.data.phaseData : _makePhaseData(cycleLength || 140, false, iconMode)
|
|
|
+ const cycleLength = _getCycleLength(id)
|
|
|
+ const phaseData = _makePhaseData(cycleLength, false, 'simple', id)
|
|
|
|
|
|
// 从相位数据中提取阶段列表(优先上轨道绿灯相位,单轨道时取 track 0,最多4个)
|
|
|
const hasTrack1 = phaseData.some(p => p[0] === 1)
|
|
|
@@ -868,11 +912,12 @@ export async function apiGetCrossingDetailData(id, { iconMode = 'default' } = {}
|
|
|
|
|
|
// 控制方式选项 + 根据路口选择不同的当前控制方式
|
|
|
const allMethods = [
|
|
|
- { label: '定周期', value: 'fixed' },
|
|
|
- { label: '黄闪', value: 'yellow_flash' },
|
|
|
{ label: '关灯', value: 'lights_off' },
|
|
|
+ { label: '黄闪', value: 'yellow_flash' },
|
|
|
+ { label: '全红', value: 'all_red' },
|
|
|
+ { label: '定周期', value: 'fixed' },
|
|
|
{ label: '步进', value: 'step' },
|
|
|
- { label: '系统方案', value: 'system' },
|
|
|
+ { label: '中心控制', value: 'system' },
|
|
|
{ label: '感应控制', value: 'sensor' },
|
|
|
{ label: '临时方案', value: 'temp' },
|
|
|
]
|
|
|
@@ -1029,41 +1074,51 @@ export async function apiGetMapLegendConfig() {
|
|
|
*/
|
|
|
export async function apiGetDeviceFaultStatus() {
|
|
|
await delay(200)
|
|
|
- const sm = DB.deviceStatus.signalMachine
|
|
|
const dt = DB.deviceStatus.detector
|
|
|
const cam = DB.deviceStatus.camera
|
|
|
|
|
|
- // 从在线数据推算故障数,每次波动
|
|
|
- const smTotal = sm.chartData[0].value + sm.chartData[1].value
|
|
|
- const smFault = 0 // 信号机无故障,用于测试无故障状态
|
|
|
+ // ── 信号机故障:从共享缓存获取,确保与在线状态的离线数一致 ──
|
|
|
+ const snap = _getSmFaultSnapshot()
|
|
|
+ const smTotal = snap.total
|
|
|
+ const smFaultTotal = snap.faultTotal
|
|
|
+ const smFaultList = [
|
|
|
+ { name: '正常', value: Math.max(0, smTotal - smFaultTotal), color: '#A0E551' },
|
|
|
+ { name: '控制板报警', value: snap.ctrlBoard, color: '#FF4545' },
|
|
|
+ { name: '相位板报警', value: snap.phaseBoard, color: '#D42A2A' },
|
|
|
+ { name: '检测板报警', value: snap.detBoard, color: '#9B1B1B' },
|
|
|
+ { name: '黄闪报警', value: snap.yellowFlash, color: '#5C0E0E' },
|
|
|
+ ]
|
|
|
+
|
|
|
+ // ── 检测器故障 ──
|
|
|
const dtTotal = dt.chartData[0].value + dt.chartData[1].value
|
|
|
- const dtFault = _fluctuate(dt.chartData[1].value, 5)
|
|
|
+ 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 = _fluctuate(cam.chartData[1].value, 2)
|
|
|
+ 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: Math.max(0, smFault) + '',
|
|
|
- centerSubTitle: `${Math.max(0, smFault)}/${smTotal}`,
|
|
|
- chartData: [
|
|
|
- { name: '正常', value: Math.max(0, smTotal - smFault), color: '#A0E551' },
|
|
|
- { name: '故障', value: Math.max(0, smFault), color: '#D03030' },
|
|
|
- ]
|
|
|
+ centerTitle: smFaultTotal + '',
|
|
|
+ centerSubTitle: `${smFaultTotal}/${smTotal}`,
|
|
|
+ chartData: smFaultList,
|
|
|
},
|
|
|
detectorStatus: {
|
|
|
- centerTitle: Math.max(0, dtFault) + '',
|
|
|
- centerSubTitle: `${Math.max(0, dtFault)}/${dtTotal}`,
|
|
|
+ centerTitle: dtFault + '',
|
|
|
+ centerSubTitle: `${dtFault}/${dtTotal}`,
|
|
|
chartData: [
|
|
|
- { name: '通信故障', value: Math.max(0, Math.floor(dtFault * 0.6)), color: '#C6302B' },
|
|
|
- { name: '数据异常', value: Math.max(0, dtFault - Math.floor(dtFault * 0.6)), color: '#faad14' },
|
|
|
+ { name: '通信故障', value: dtCommFault, color: '#C6302B' },
|
|
|
+ { name: '数据异常', value: Math.max(0, dtFault - dtCommFault), color: '#faad14' },
|
|
|
]
|
|
|
},
|
|
|
trafficLightStatus: {
|
|
|
- centerTitle: Math.max(0, camFault) + '',
|
|
|
- centerSubTitle: `${Math.max(0, camFault)}/${camTotal}`,
|
|
|
+ centerTitle: camFault + '',
|
|
|
+ centerSubTitle: `${camFault}/${camTotal}`,
|
|
|
chartData: [
|
|
|
- { name: '红绿冲突', value: Math.max(0, Math.floor(camFault * 0.5)), color: '#C6302B' },
|
|
|
- { name: '红灯故障', value: Math.max(0, camFault - Math.floor(camFault * 0.5)), color: '#8F1E1E' },
|
|
|
+ { name: '红绿冲突', value: camConflict, color: '#C6302B' },
|
|
|
+ { name: '红灯故障', value: Math.max(0, camFault - camConflict), color: '#8F1E1E' },
|
|
|
]
|
|
|
},
|
|
|
})
|