Bladeren bron

接口改造:统一接入 mock API 层,替换全部硬编码数据

画安 1 maand geleden
bovenliggende
commit
4d96d7d122

+ 41 - 5
package-lock.json

@@ -9,7 +9,7 @@
       "version": "0.1.0",
       "dependencies": {
         "@amap/amap-jsapi-loader": "^1.0.1",
-        "axios": "^1.13.5",
+        "axios": "^1.13.6",
         "cesium": "^1.105.1",
         "china-map-geojson": "^1.0.4",
         "core-js": "^3.8.3",
@@ -28,6 +28,7 @@
         "@vue/cli-plugin-babel": "~5.0.0",
         "@vue/cli-plugin-eslint": "~5.0.0",
         "@vue/cli-service": "~5.0.0",
+        "axios-mock-adapter": "^2.1.0",
         "eslint": "^7.32.0",
         "eslint-plugin-vue": "^8.0.3",
         "postcss-pxtorem": "^5.1.1",
@@ -3670,16 +3671,28 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.13.5",
-      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.5.tgz",
-      "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
-      "license": "MIT",
+      "version": "1.13.6",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+      "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
       "dependencies": {
         "follow-redirects": "^1.15.11",
         "form-data": "^4.0.5",
         "proxy-from-env": "^1.1.0"
       }
     },
+    "node_modules/axios-mock-adapter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz",
+      "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "is-buffer": "^2.0.5"
+      },
+      "peerDependencies": {
+        "axios": ">= 0.17.0"
+      }
+    },
     "node_modules/babel-loader": {
       "version": "8.4.1",
       "resolved": "https://registry.npmmirror.com/babel-loader/-/babel-loader-8.4.1.tgz",
@@ -7409,6 +7422,29 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-buffer": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+      "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/is-ci": {
       "version": "1.2.1",
       "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz",

+ 2 - 1
package.json

@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
-    "axios": "^1.13.5",
+    "axios": "^1.13.6",
     "cesium": "^1.105.1",
     "china-map-geojson": "^1.0.4",
     "core-js": "^3.8.3",
@@ -28,6 +28,7 @@
     "@vue/cli-plugin-babel": "~5.0.0",
     "@vue/cli-plugin-eslint": "~5.0.0",
     "@vue/cli-service": "~5.0.0",
+    "axios-mock-adapter": "^2.1.0",
     "eslint": "^7.32.0",
     "eslint-plugin-vue": "^8.0.3",
     "postcss-pxtorem": "^5.1.1",

+ 108 - 0
src/api/index.js

@@ -0,0 +1,108 @@
+/**
+ * 统一 API 入口
+ *
+ * 所有接口通过 http (request.js) 发出请求。
+ * 开发期由 mockAdapter.js 拦截返回模拟数据。
+ * 切真实后端时,只需删除 main.js 中的 `import '@/mock/mockAdapter'`。
+ *
+ * 响应拦截器已自动脱壳:接口返回值直接是 data(不是 { code, data })。
+ * 业务错误走 catch。
+ */
+
+import { http } from '@/utils/request'
+
+// ── 认证 ──
+export const apiGetCaptcha = () =>
+  http.get('/auth/captcha')
+
+export const apiLogin = (data) =>
+  http.post('/auth/login', data, { withToken: false, skipGlobalError: true })
+
+export const apiChangePassword = (data) =>
+  http.post('/auth/change-password', data)
+
+// ── 路口基础数据 ──
+export const apiGetPoints = (params) =>
+  http.get('/intersections', { params })
+
+export const apiGetIntersectionData = (id) =>
+  http.get(`/intersections/${id}`)
+
+export const apiGetSignalTiming = (id) =>
+  http.get(`/intersections/${id}/signal-timing`)
+
+export const apiGetIntersectionStages = (id) =>
+  http.get(`/intersections/${id}/stages`)
+
+export const apiGetSchemes = (id) =>
+  http.get(`/intersections/${id}/schemes`)
+
+// ── 区域菜单树 ──
+export const apiGetMenuTree = (tabId) =>
+  http.get('/regions/tree', { params: { tabId } })
+
+export const apiGetTongzhouMenuTree = () =>
+  http.get('/regions/tree/tongzhou')
+
+// ── 设备状态 & 首页 ──
+export const apiGetDeviceStatus = (type) =>
+  http.get('/devices/status/summary', { params: { type } })
+
+export const apiGetDeviceFaultStatus = () =>
+  http.get('/devices/fault-status')
+
+export const apiGetHomeSnapshot = () =>
+  http.get('/home/snapshot')
+
+export const apiGetControlModeStats = () =>
+  http.get('/home/control-mode-stats')
+
+export const apiGetLatestAlarms = (params) =>
+  http.get('/alarms/latest', { params })
+
+// ── 勤务 & 任务 ──
+export const apiGetTasks = (params) =>
+  http.get('/tasks', { params })
+
+export const apiGetSecurityRoutes = () =>
+  http.get('/security-routes')
+
+export const apiGetSecurityRouteDetail = (id) =>
+  http.get(`/security-routes/${id}`)
+
+export const apiGetKeyIntersections = () =>
+  http.get('/key-intersections')
+
+// ── 交通时空图 ──
+export const apiGetTrafficTimeSpace = (params) =>
+  http.get('/traffic/time-space', { params })
+
+// ── 路口列表 & 字典 ──
+export const apiGetCrossingList = (params) =>
+  http.get('/crossings', { params })
+
+export const apiGetDictOptions = (type) =>
+  http.get(`/dict/${type}`)
+
+// ── 设备操作 ──
+export const apiRestartDevice = (id) =>
+  http.post(`/devices/${id}/restart`)
+
+export const apiUpgradeDevice = (id, data) =>
+  http.post(`/devices/${id}/upgrade`, data)
+
+// ── 弹窗专用 ──
+export const apiGetSpecialTaskMonitorData = (id) =>
+  http.get(`/special-task/${id}/monitor`)
+
+export const apiGetCrossingPanelData = (id) =>
+  http.get(`/crossing/panel/${id}`)
+
+export const apiGetCrossingDetailData = (id) =>
+  http.get(`/crossing/detail/${id}`)
+
+export const apiGetCrossingTopCharts = () =>
+  http.get('/crossing/top-charts')
+
+export const apiGetOverviewTopCharts = () =>
+  http.get('/overview/top-charts')

+ 3 - 4
src/components/IntersectionSignalMonitoring.vue

@@ -22,7 +22,7 @@
 
 import SignalTimingChart from '@/components/SignalTimingChart.vue';
 import IntersectionMap from '@/components/ui/IntersectionMap.vue';
-import { fetchSignalTimingData, getIntersectionData } from '@/mock/data';
+import { apiGetSignalTiming, apiGetIntersectionData } from '@/api';
 import video1 from '@/assets/videos/video1.mp4';
 import video2 from '@/assets/videos/video2.mp4';
 
@@ -72,9 +72,8 @@ export default {
         this._resizeObserver.observe(this.$refs.Container);
 
         this.loading = true;
-        const signalRes = await fetchSignalTimingData(this.nodeData.id);
-        this.signalTimingData = signalRes.data;
-        this.intersectionData = await getIntersectionData(this.nodeData.id);
+        this.signalTimingData = await apiGetSignalTiming(this.nodeData.id);
+        this.intersectionData = await apiGetIntersectionData(this.nodeData.id) || {};
         this.loading = false;
 
         this.startSimulationTimer();

+ 1 - 1
src/components/ui/AlarmMessageList.vue

@@ -7,7 +7,7 @@
         <div class="alarm-item" v-for="(item, index) in listData" :key="item.id || index">
             <div class="item-header">
                 <span class="title" :class="getTitleClass(item.type)">
-                    {{ item.title }}
+                    {{index + 1}}.{{ item.title }}
                 </span>
                 <span class="time" v-if="item.time">{{ item.time }}</span>
             </div>

+ 29 - 77
src/components/ui/CrossingDetailPanel.vue

@@ -8,7 +8,7 @@
                 <div class="header">
                     <div class="title-area">
                         <span class="main-title">方案状态</span>
-                        <span class="sub-info">(周期: {{ cycleLength }} 相位差: 0 协调时间: 0)</span>
+                        <span class="sub-info">(周期: {{ cycleLength }} 相位差: {{ phaseDiff }} 协调时间: {{ coordTime }})</span>
                     </div>
                     <div class="checkbox-area">
                         <div class="checkbox-mock" :class="{ 'is-checked': followPhase }"
@@ -41,7 +41,7 @@
 
                     <div class="form-interactive-area" :class="{ 'is-disabled': !isManualMode }">
                         <div class="control-method-content">
-                            <SegmentedRadio v-model="currentMethod" />
+                            <SegmentedRadio v-model="currentMethod" :options="controlMethodOptions" />
                         </div>
 
                         <div class="control-scheme">
@@ -59,7 +59,7 @@
                                             :class="{ 'is-active': item.value === currentStage }"
                                             @click="currentStage = item.value"
                                         >
-                                            <img :src="require(`@/assets/images/${item.img}`)" alt="stage" class="phase-image" />
+                                            <img :src="item.img" alt="stage" class="phase-image" />
                                         </div>
                                         
                                         <input 
@@ -119,7 +119,7 @@ import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
 import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
 import DropdownSelect from '@/components/ui/DropdownSelect.vue';
 
-import { getIntersectionData } from '@/mock/data';
+import { apiGetCrossingDetailData } from '@/api';
 
 export default {
     name: 'CrossingPanel',
@@ -138,87 +138,23 @@ export default {
 
             followPhase: false,
             intersectionData: {},
-            currentRoute: {
-                id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
-                mainVideo: require('@/assets/videos/video1.mp4'),
-                cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-            },
+            currentRoute: {},
             cycleLength: 140,
             currentSec: 15,
-            mockPhaseData: [
-                // ================= 上轨道 (Track 0) =================
-                // S1阶段 (0-30s): P1 直行
-                [0, 0, 23, 'P1', 30, 'green', 'UP'],
-                [0, 23, 26, '', 3, 'stripe', ''],
-                [0, 26, 29, '', 3, 'yellow', ''],
-                [0, 29, 30, '', 1, 'red', ''],
-
-                // S2阶段 (30-60s): P2 左转
-                [0, 30, 53, 'P2', 30, 'green', 'TURN_LEFT'],
-                [0, 53, 56, '', 3, 'stripe', ''],
-                [0, 56, 59, '', 3, 'yellow', ''],
-                [0, 59, 60, '', 1, 'red', ''],
-
-                // S3阶段 (60-110s): P3 侧向左转 (使用向左箭头)
-                [0, 60, 103, 'P3', 50, 'green', 'TURN_LEFT'],
-                [0, 103, 106, '', 3, 'stripe', ''],
-                [0, 106, 109, '', 3, 'yellow', ''],
-                [0, 109, 110, '', 1, 'red', ''],
-
-                // S4阶段 (110-140s): P4 掉头
-                [0, 110, 133, 'P4', 30, 'green', 'UTURN'],
-                [0, 133, 136, '', 3, 'stripe', ''],
-                [0, 136, 139, '', 3, 'yellow', ''],
-                [0, 139, 140, '', 1, 'red', ''],
-
-                // ================= 下轨道 (Track 1) =================
-                // S1阶段 (0-30s): P5 直行
-                [1, 0, 23, 'P5', 30, 'green', 'UP'],
-                [1, 23, 26, '', 3, 'stripe', ''],
-                [1, 26, 29, '', 3, 'yellow', ''],
-                [1, 29, 30, '', 1, 'red', ''],
-
-                // S2阶段 (30-60s): P6 左转
-                [1, 30, 53, 'P6', 30, 'green', 'TURN_LEFT'],
-                [1, 53, 56, '', 3, 'stripe', ''],
-                [1, 56, 59, '', 3, 'yellow', ''],
-                [1, 59, 60, '', 1, 'red', ''],
-
-                // S3阶段 (60-110s): P7 侧向右转 (使用向右箭头)
-                [1, 60, 103, 'P7', 50, 'green', 'TURN_RIGHT'],
-                [1, 103, 106, '', 3, 'stripe', ''],
-                [1, 106, 109, '', 3, 'yellow', ''],
-                [1, 109, 110, '', 1, 'red', ''],
-
-                // S4阶段 (110-140s): P8 左转
-                [1, 110, 133, 'P8', 30, 'green', 'TURN_LEFT'],
-                [1, 133, 136, '', 3, 'stripe', ''],
-                [1, 136, 139, '', 3, 'yellow', ''],
-                [1, 139, 140, '', 1, 'red', '']
-            ],
+            phaseDiff: 0,
+            coordTime: 0,
+            mockPhaseData: [],
             
             // 控制方式数据
+            controlMethodOptions: [],
             currentMethod: 'temp',
             currentScheme: 'early_peak',
-            schemeOptions: [
-                { label: '早高峰', value: 'early_peak' },
-                { label: '晚高峰', value: 'evening_peak' },
-                { label: '平峰', value: 'normal' }
-            ],
+            schemeOptions: [],
             currentLocktime: 50,
-            locktimeOptions: [
-                { label: '50', value: 50 },
-                { label: '100', value: 100 },
-                { label: '300', value: 300 }
-            ],
+            locktimeOptions: [],
             currentStage: '1', 
             // 补充了 time 属性,用于双向绑定输入框的时间
-            currentStageList: [
-                { value: '1', time: 30, img: 'arrow_1.png' },
-                { value: '2', time: 30, img: 'arrow_2.png' },
-                { value: '3', time: 50, img: 'arrow_3.png' },
-                { value: '4', time: 30, img: 'arrow_4.png' }
-            ]
+            currentStageList: []
         }
     },
     watch: {
@@ -236,7 +172,23 @@ export default {
         }
     },
     async mounted() {
-        this.intersectionData = await getIntersectionData();
+        const nodeId = this.$attrs.id || this.id;
+        const data = await apiGetCrossingDetailData(nodeId);
+        if (data) {
+            this.currentRoute = data.currentRoute || {};
+            this.intersectionData = data.intersectionData || {};
+            this.mockPhaseData = data.phaseData || [];
+            this.cycleLength = data.cycleLength || 140;
+            this.currentSec = data.currentTime || 0;
+            this.phaseDiff = data.phaseDiff || 0;
+            this.coordTime = data.coordTime || 0;
+            this.currentStageList = data.stageList || [];
+            this.schemeOptions = data.schemeOptions || [];
+            if (data.currentScheme) this.currentScheme = data.currentScheme;
+            if (data.controlMethodOptions) this.controlMethodOptions = data.controlMethodOptions;
+            if (data.currentMethod) this.currentMethod = data.currentMethod;
+            if (data.locktimeOptions) this.locktimeOptions = data.locktimeOptions;
+        }
     },
     methods: {
         // 切换手动控制模式

+ 24 - 41
src/components/ui/CrossingListPanel.vue

@@ -44,6 +44,7 @@ import TechFilterBar from '@/components/ui/TechFilterBar.vue';
 import TechTable from '@/components/ui/TechTable.vue';
 import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
 import TechPagination from '@/components/ui/TechPagination.vue';
+import { apiGetCrossingList, apiGetDictOptions } from '@/api';
 
 export default {
     name: 'IntersectionManage',
@@ -77,11 +78,7 @@ export default {
                 { type: 'input', label: '路口名称', key: 'name', placeholder: '请输入路口名' },
                 {
                     type: 'select', label: '子区', key: 'subArea',
-                    options: [
-                        { label: '全部', value: '' },
-                        { label: '石景山区', value: 'shijingshan' },
-                        { label: '海淀区', value: 'haidian' }
-                    ]
+                    options: [{ label: '全部', value: '' }] // mounted 中从接口加载
                 },
                 {
                     type: 'select', label: '状态', key: 'status',
@@ -94,6 +91,7 @@ export default {
                 {
                     type: 'select', label: '时间偏差', key: 'timeOffset',
                     options: [
+                        { label: '全部', value: '' },
                         { label: '无偏差', value: 'none' },
                         { label: '有偏差', value: 'has' }
                     ]
@@ -101,7 +99,7 @@ export default {
                 {
                     type: 'select', label: '关键路口', key: 'isKey',
                     options: [
-                        { label: '请选择', value: '' },
+                        { label: '全部', value: '' },
                         { label: '是', value: 'yes' },
                         { label: '否', value: 'no' }
                     ]
@@ -129,49 +127,34 @@ export default {
             }
         };
     },
-    mounted() {
-        this.fetchData(); // 初始化拉取数据
+    async mounted() {
+        // 加载子区下拉选项
+        try {
+            const regions = await apiGetDictOptions('regions');
+            if (regions) {
+                const subAreaConfig = this.filterConfig.find(c => c.key === 'subArea');
+                if (subAreaConfig) {
+                    subAreaConfig.options = [{ label: '全部', value: '' }, ...regions];
+                }
+            }
+        } catch (e) { /* ignore */ }
+        this.fetchData();
     },
     methods: {
-        // 模拟接口请求
+        // 接口请求
         async fetchData() {
             this.loading = true; // 开启 Loading 遮罩
             try {
-                const apiParams = {
+                const params = {
                     ...this.searchParams,
                     page: this.pagination.currentPage,
-                    size: this.pagination.pageSize
+                    pageSize: this.pagination.pageSize
                 };
-                console.log('发起请求,参数:', apiParams);
-
-                // 模拟网络延迟 (500ms),让你能看清 Loading 动画
-                await new Promise(resolve => setTimeout(resolve, 500));
-
-                // 模拟生成列表数据
-                this.tableList = Array.from({ length: this.pagination.pageSize }).map((_, index) => {
-                    const actualIndex = (this.pagination.currentPage - 1) * this.pagination.pageSize + index + 1;
-                    return {
-                        id: `uuid-${Date.now()}-${index}`,
-                        index: actualIndex,
-                        name: '北京路南京路',
-                        subArea: '石景山区',
-                        ip: '41.32.32.131',
-                        status: '在线',
-                        timeOffset: '无偏差',
-                        cycle: 120,
-                        version: '54827345623452756',
-                        // 模拟相位图表数据
-                        phaseData: [
-                            [0, 0, 30, '阶段1', 30, 'green', 'UP'],
-                            [0, 30, 35, '阶段2', 5, 'yellow', ''],
-                            [0, 35, 60, '阶段3', 25, 'green', 'TURN_LEFT'],
-                            // 👇 加一段红灯或者绿灯,把 60 秒到 120 秒补齐
-                            [0, 60, 120, '阶段4', 60, 'red', '']
-                        ]
-                    };
-                });
-
-                this.pagination.total = 32;
+                console.log('发起请求,参数:', params);
+
+                const data = await apiGetCrossingList(params);
+                this.tableList = data?.list || data || [];
+                this.pagination.total = data?.total || 0;
             } catch (error) {
                 console.error('获取列表失败', error);
             } finally {

+ 12 - 58
src/components/ui/CrossingPanel.vue

@@ -13,7 +13,7 @@
 import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
 import IntersectionMapVideos from '@/components/ui/IntersectionMapVideos.vue';
 
-import { getIntersectionData } from '@/mock/data';
+import { apiGetCrossingPanelData } from '@/api';
 
 export default {
     name: 'CrossingPanel',
@@ -28,68 +28,22 @@ export default {
         return {
             followPhase: false,
             intersectionData: {},
-            currentRoute: {
-                id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
-                mainVideo: require('@/assets/videos/video1.mp4'),
-                cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-            },
+            currentRoute: {},
             cycleLength: 140,
             currentSec: 15,
-            mockPhaseData: [
-                // ================= 上轨道 (Track 0) =================
-                // S1阶段 (0-30s): P1 直行
-                [0, 0, 23, 'P1', 30, 'green', 'UP'],
-                [0, 23, 26, '', 3, 'stripe', ''],
-                [0, 26, 29, '', 3, 'yellow', ''],
-                [0, 29, 30, '', 1, 'red', ''],
-
-                // S2阶段 (30-60s): P2 左转
-                [0, 30, 53, 'P2', 30, 'green', 'TURN_LEFT'],
-                [0, 53, 56, '', 3, 'stripe', ''],
-                [0, 56, 59, '', 3, 'yellow', ''],
-                [0, 59, 60, '', 1, 'red', ''],
-
-                // S3阶段 (60-110s): P3 侧向左转 (使用向左箭头)
-                [0, 60, 103, 'P3', 50, 'green', 'TURN_LEFT'],
-                [0, 103, 106, '', 3, 'stripe', ''],
-                [0, 106, 109, '', 3, 'yellow', ''],
-                [0, 109, 110, '', 1, 'red', ''],
-
-                // S4阶段 (110-140s): P4 掉头
-                [0, 110, 133, 'P4', 30, 'green', 'UTURN'],
-                [0, 133, 136, '', 3, 'stripe', ''],
-                [0, 136, 139, '', 3, 'yellow', ''],
-                [0, 139, 140, '', 1, 'red', ''],
-
-                // ================= 下轨道 (Track 1) =================
-                // S1阶段 (0-30s): P5 直行
-                [1, 0, 23, 'P5', 30, 'green', 'UP'],
-                [1, 23, 26, '', 3, 'stripe', ''],
-                [1, 26, 29, '', 3, 'yellow', ''],
-                [1, 29, 30, '', 1, 'red', ''],
-
-                // S2阶段 (30-60s): P6 左转
-                [1, 30, 53, 'P6', 30, 'green', 'TURN_LEFT'],
-                [1, 53, 56, '', 3, 'stripe', ''],
-                [1, 56, 59, '', 3, 'yellow', ''],
-                [1, 59, 60, '', 1, 'red', ''],
-
-                // S3阶段 (60-110s): P7 侧向右转 (使用向右箭头)
-                [1, 60, 103, 'P7', 50, 'green', 'TURN_RIGHT'],
-                [1, 103, 106, '', 3, 'stripe', ''],
-                [1, 106, 109, '', 3, 'yellow', ''],
-                [1, 109, 110, '', 1, 'red', ''],
-
-                // S4阶段 (110-140s): P8 左转
-                [1, 110, 133, 'P8', 30, 'green', 'TURN_LEFT'],
-                [1, 133, 136, '', 3, 'stripe', ''],
-                [1, 136, 139, '', 3, 'yellow', ''],
-                [1, 139, 140, '', 1, 'red', '']
-            ]
+            mockPhaseData: []
         }
     },
     async mounted() {
-        this.intersectionData = await getIntersectionData();
+        const nodeId = this.$attrs.id || this.id;
+        const data = await apiGetCrossingPanelData(nodeId);
+        if (data) {
+            this.currentRoute = data.currentRoute || {};
+            this.intersectionData = data.intersectionData || {};
+            this.mockPhaseData = data.phaseData || [];
+            if (data.cycleLength) this.cycleLength = data.cycleLength;
+            if (data.currentTime !== undefined) this.currentSec = data.currentTime;
+        }
     },
 }
 </script>

+ 3 - 2
src/components/ui/SeamlessScroll.vue

@@ -58,7 +58,8 @@ export default {
       this.currentTop = 0;
       if (this.$refs.scrollRef) this.$refs.scrollRef.scrollTop = 0;
 
-      if (!this.isScrollable) {
+      // 直接判断数据量,不依赖 computed 副作用的时序
+      if (!this.data || this.data.length <= this.limit) {
         console.log('🛑 SeamlessScroll: 数据量不足,无需滚动');
         return;
       }
@@ -88,7 +89,7 @@ export default {
       });
     },
     resume() {
-      if (!this.isScrollable || this.resetHeight <= 0) return;
+      if ((!this.data || this.data.length <= this.limit) || this.resetHeight <= 0) return;
       const step = () => {
         const wrapper = this.$refs.scrollRef;
         if (!wrapper) return;

+ 2 - 4
src/components/ui/SecurityRoutePanel.vue

@@ -33,7 +33,7 @@
 
 <script>
 import IntersectionMap from '@/components/ui/IntersectionMapVideos.vue';
-import { getIntersectionData } from '@/mock/data';
+import { apiGetIntersectionData } from '@/api';
 
 export default {
   name: 'SecurityRoutePanel',
@@ -53,9 +53,7 @@ export default {
   async created() {
 },
 async mounted() {
-      this.intersectionData = await getIntersectionData(); // 获取交叉口数据
-      console.log(this.intersectionData);
-    
+      this.intersectionData = await apiGetIntersectionData() || {};
   },
   beforeDestroy() {
     

+ 4 - 29
src/components/ui/SecurityRoutePanelSwitch.vue

@@ -76,7 +76,7 @@
 
 <script>
 import IntersectionMap from '@/components/ui/IntersectionMapVideos.vue';
-import { getIntersectionData } from '@/mock/data';
+import { apiGetSecurityRoutes, apiGetIntersectionData } from '@/api';
 
 export default {
   name: 'SecurityRoutePanelSwitch',
@@ -90,33 +90,7 @@ export default {
       currentIndex: 0, 
       pageSize: 3, 
       
-      routeList: [
-        { 
-          id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 2, name: '靖远路与北公路交叉口 2', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 3, name: '靖远路与北公路交叉口 3', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 4, name: '靖远路与北公路交叉口 4', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 5, name: '靖远路与北公路交叉口 5', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        }
-      ]
+      routeList: []
     };
   },
   computed: {
@@ -131,7 +105,8 @@ export default {
     }
   },
   async mounted() {
-    this.intersectionData = await getIntersectionData(); 
+    this.routeList = await apiGetSecurityRoutes() || [];
+    this.intersectionData = await apiGetIntersectionData() || {};
   },
   methods: {
     handlePrev() {

+ 4 - 29
src/components/ui/SecurityRoutePanelSwitchSmall.vue

@@ -80,7 +80,7 @@
 
 <script>
 import IntersectionMap from '@/components/ui/IntersectionMapVideos.vue';
-import { getIntersectionData } from '@/mock/data';
+import { apiGetSecurityRoutes, apiGetIntersectionData } from '@/api';
 
 export default {
   name: 'SecurityRoutePanelSwitchSmall',
@@ -100,33 +100,7 @@ export default {
       currentIndex: 0, 
       pageSize: 3, 
       
-      routeList: [
-        { 
-          id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 2, name: '靖远路与北公路交叉口 2', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 3, name: '靖远路与北公路交叉口 3', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 4, name: '靖远路与北公路交叉口 4', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        },
-        { 
-          id: 5, name: '靖远路与北公路交叉口 5', level: '一级', mode: '快进', time: '30s',
-          mainVideo: require('@/assets/videos/video1.mp4'),
-          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
-        }
-      ]
+      routeList: []
     };
   },
   computed: {
@@ -141,7 +115,8 @@ export default {
     }
   },
   async mounted() {
-    this.intersectionData = await getIntersectionData(); 
+    this.routeList = await apiGetSecurityRoutes() || [];
+    this.intersectionData = await apiGetIntersectionData() || {};
   },
   methods: {
     handlePrev() {

+ 5 - 44
src/components/ui/SidebarMenu.vue

@@ -12,6 +12,7 @@
 <script>
 // 引入刚刚写的递归组件
 import MenuItem from './MenuItem.vue';
+import { apiGetMenuTree } from '@/api';
 
 export default {
   name: 'SidebarMenu',
@@ -20,52 +21,12 @@ export default {
   },
   data() {
     return {
-      // 模拟后端返回的树形结构数据
-      menuData: [
-        {
-          id: 'root-1',
-          label: '主控中心',
-          icon: 'el-icon-monitor', // 这里可以替换为你项目用的图标类名,比如 iconfont
-          children: [
-            {
-              id: 'team-1',
-              label: '北京市交警总队',
-              children: [
-                {
-                  id: 'dist-1',
-                  label: '昌平区',
-                  children: [
-                    {
-                      id: 'street-1',
-                      label: '龙泽园街道',
-                      children: [
-                        { id: 'node-101', label: '文华路口', lng: 116.3, lat: 40.1 },
-                        { id: 'node-102', label: '同城街路口', lng: 116.4, lat: 40.2 }
-                      ]
-                    }
-                  ]
-                },
-                {
-                  id: 'dist-2',
-                  label: '丰台区',
-                  children: [
-                    {
-                      id: 'street-2',
-                      label: '龙泽园街道', // 图里似乎数据有重复,这里照猫画虎
-                      children: [
-                        { id: 'node-201', label: '文华路口' },
-                        { id: 'node-202', label: '同城街路口' }
-                      ]
-                    }
-                  ]
-                }
-              ]
-            }
-          ]
-        }
-      ]
+      menuData: []
     };
   },
+  async mounted() {
+    this.menuData = await apiGetMenuTree() || [];
+  },
   methods: {
     // 捕获最底层路口的点击事件
     handleLeafNodeClick(nodeData) {

+ 3 - 0
src/main.js

@@ -3,6 +3,9 @@ import App from "./App.vue";
 import router from "./router";
 import "./styles/base.css";
 import '@/utils/rem.js';
+
+// Mock 拦截器(切真实后端时删除下面这行)
+import '@/mock/mockAdapter';
 import MessageDialog from "./components/ui/MessageDialog/index.js";
 
 // 挂载为全局方法

+ 948 - 15
src/mock/api.js

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

+ 203 - 0
src/mock/mockAdapter.js

@@ -0,0 +1,203 @@
+/**
+ * axios-mock-adapter 路由注册
+ * 拦截 http 实例的请求,内部调用 mock/api.js 生成数据
+ * 在 main.js 中 import 一次即可激活,切真实后端时删除该 import
+ */
+import MockAdapter from 'axios-mock-adapter'
+import { http } from '@/utils/request'
+import * as mockApi from './api'
+
+const mock = new MockAdapter(http.instance, { delayResponse: 0 }) // delay 由 mock/api.js 内部控制
+
+// ── 辅助:从 axios config 中提取参数 ────────────────────────────────
+
+function getParams(config) {
+  return config.params || {}
+}
+
+function getBody(config) {
+  try { return JSON.parse(config.data) } catch { return config.data || {} }
+}
+
+function getPathId(url, prefix) {
+  return url.replace(prefix, '').replace(/^\//, '')
+}
+
+// ── 认证 ─────────────────────────────────────────────────────────────
+
+mock.onGet('/auth/captcha').reply(async () => {
+  const res = await mockApi.apiGetCaptcha()
+  return [200, res]
+})
+
+mock.onPost('/auth/login').reply(async (config) => {
+  const res = await mockApi.apiLogin(getBody(config))
+  return [200, res]
+})
+
+mock.onPost('/auth/change-password').reply(async (config) => {
+  const res = await mockApi.apiChangePassword(getBody(config))
+  return [200, res]
+})
+
+// ── 路口基础数据 ─────────────────────────────────────────────────────
+
+mock.onGet(/\/intersections\/(.+)\/signal-timing/).reply(async (config) => {
+  const id = config.url.match(/\/intersections\/(.+)\/signal-timing/)[1]
+  const res = await mockApi.apiGetSignalTiming(id)
+  return [200, res]
+})
+
+mock.onGet(/\/intersections\/(.+)\/stages/).reply(async (config) => {
+  const id = config.url.match(/\/intersections\/(.+)\/stages/)[1]
+  const res = await mockApi.apiGetIntersectionStages(id)
+  return [200, res]
+})
+
+mock.onGet(/\/intersections\/(.+)\/schemes/).reply(async (config) => {
+  const id = config.url.match(/\/intersections\/(.+)\/schemes/)[1]
+  const res = await mockApi.apiGetSchemes(id)
+  return [200, res]
+})
+
+// 注意:带参数的路由要放在精确路由之后
+mock.onGet(/\/intersections\/(?!$)(.+)/).reply(async (config) => {
+  const id = config.url.match(/\/intersections\/(.+)/)[1]
+  // 排除子路由
+  if (id.includes('/')) return [404]
+  const res = await mockApi.apiGetIntersectionData(id)
+  return [200, res]
+})
+
+mock.onGet('/intersections').reply(async (config) => {
+  const res = await mockApi.apiGetPoints(getParams(config))
+  return [200, res]
+})
+
+// ── 区域菜单树 ──────────────────────────────────────────────────────
+
+mock.onGet('/regions/tree/tongzhou').reply(async () => {
+  const res = await mockApi.apiGetTongzhouMenuTree()
+  return [200, res]
+})
+
+mock.onGet('/regions/tree').reply(async (config) => {
+  const res = await mockApi.apiGetMenuTree(getParams(config).tabId)
+  return [200, res]
+})
+
+// ── 设备状态 & 首页 ─────────────────────────────────────────────────
+
+mock.onGet('/devices/status/summary').reply(async (config) => {
+  const res = await mockApi.apiGetDeviceStatus(getParams(config).type)
+  return [200, res]
+})
+
+mock.onGet('/devices/fault-status').reply(async () => {
+  const res = await mockApi.apiGetDeviceFaultStatus()
+  return [200, res]
+})
+
+mock.onGet('/home/snapshot').reply(async () => {
+  const res = await mockApi.apiGetHomeSnapshot()
+  return [200, res]
+})
+
+mock.onGet('/home/control-mode-stats').reply(async () => {
+  const res = await mockApi.apiGetControlModeStats()
+  return [200, res]
+})
+
+mock.onGet('/alarms/latest').reply(async (config) => {
+  const res = await mockApi.apiGetLatestAlarms(getParams(config))
+  return [200, res]
+})
+
+// ── 勤务 & 任务 ─────────────────────────────────────────────────────
+
+mock.onGet('/tasks').reply(async (config) => {
+  const res = await mockApi.apiGetTasks(getParams(config))
+  return [200, res]
+})
+
+mock.onGet(/\/security-routes\/(.+)/).reply(async (config) => {
+  const id = config.url.match(/\/security-routes\/(.+)/)[1]
+  const res = await mockApi.apiGetSecurityRouteDetail(Number(id) || id)
+  return [200, res]
+})
+
+mock.onGet('/security-routes').reply(async () => {
+  const res = await mockApi.apiGetSecurityRoutes()
+  return [200, res]
+})
+
+mock.onGet('/key-intersections').reply(async () => {
+  const res = await mockApi.apiGetKeyIntersections()
+  return [200, res]
+})
+
+// ── 交通时空图 ──────────────────────────────────────────────────────
+
+mock.onGet('/traffic/time-space').reply(async (config) => {
+  const res = await mockApi.apiGetTrafficTimeSpace(getParams(config))
+  return [200, res]
+})
+
+// ── 路口列表 & 字典 ─────────────────────────────────────────────────
+
+mock.onGet('/crossings').reply(async (config) => {
+  const res = await mockApi.apiGetCrossingList(getParams(config))
+  return [200, res]
+})
+
+mock.onGet(/\/dict\/(.+)/).reply(async (config) => {
+  const type = config.url.match(/\/dict\/(.+)/)[1]
+  const res = await mockApi.apiGetDictOptions(type)
+  return [200, res]
+})
+
+// ── 设备操作 ─────────────────────────────────────────────────────────
+
+mock.onPost(/\/devices\/(.+)\/restart/).reply(async (config) => {
+  const id = config.url.match(/\/devices\/(.+)\/restart/)[1]
+  const res = await mockApi.apiRestartDevice(id)
+  return [200, res]
+})
+
+mock.onPost(/\/devices\/(.+)\/upgrade/).reply(async (config) => {
+  const id = config.url.match(/\/devices\/(.+)\/upgrade/)[1]
+  const res = await mockApi.apiUpgradeDevice(id)
+  return [200, res]
+})
+
+// ── 弹窗专用 ─────────────────────────────────────────────────────────
+
+mock.onGet(/\/special-task\/(.+)\/monitor/).reply(async (config) => {
+  const id = config.url.match(/\/special-task\/(.+)\/monitor/)[1]
+  const res = await mockApi.apiGetSpecialTaskMonitorData(Number(id) || id)
+  return [200, res]
+})
+
+mock.onGet(/\/crossing\/panel\/(.+)/).reply(async (config) => {
+  const id = config.url.match(/\/crossing\/panel\/(.+)/)[1]
+  const res = await mockApi.apiGetCrossingPanelData(id)
+  return [200, res]
+})
+
+mock.onGet(/\/crossing\/detail\/(.+)/).reply(async (config) => {
+  const id = config.url.match(/\/crossing\/detail\/(.+)/)[1]
+  const res = await mockApi.apiGetCrossingDetailData(id)
+  return [200, res]
+})
+
+mock.onGet('/crossing/top-charts').reply(async () => {
+  const res = await mockApi.apiGetCrossingTopCharts()
+  return [200, res]
+})
+
+mock.onGet('/overview/top-charts').reply(async () => {
+  const res = await mockApi.apiGetOverviewTopCharts()
+  return [200, res]
+})
+
+export default mock

File diff suppressed because it is too large
+ 30415 - 0
src/mock/mock_data.json


+ 134 - 0
src/utils/request.js

@@ -0,0 +1,134 @@
+import axios from 'axios';
+
+class RequestHttp {
+  constructor(config) {
+    this.instance = axios.create(config);
+    this.abortControllers = new Map();
+    this._setupInterceptors();
+  }
+
+  _generateRequestKey(config) {
+    const { method, url, params, data } = config;
+    return `${method}-${url}-${JSON.stringify(params || {})}-${JSON.stringify(data || {})}`;
+  }
+
+  _addPendingRequest(config) {
+    const requestKey = this._generateRequestKey(config);
+    const controller = new AbortController();
+    config.signal = controller.signal;
+    this.abortControllers.set(requestKey, controller);
+  }
+
+  _removePendingRequest(config, cancel = false) {
+    const requestKey = this._generateRequestKey(config);
+    if (this.abortControllers.has(requestKey)) {
+      if (cancel) {
+        this.abortControllers.get(requestKey).abort();
+      }
+      this.abortControllers.delete(requestKey);
+    }
+  }
+
+  _setupInterceptors() {
+    // 请求拦截器
+    this.instance.interceptors.request.use(
+      (config) => {
+        // 防重复请求
+        if (config.cancelDuplicate !== false) {
+          this._removePendingRequest(config, true);
+          this._addPendingRequest(config);
+        }
+
+        // 注入 Token
+        if (config.withToken !== false) {
+          const token = localStorage.getItem('token');
+          if (token && config.headers) {
+            config.headers.Authorization = `Bearer ${token}`;
+          }
+        }
+
+        return config;
+      },
+      (error) => {
+        return Promise.reject(error);
+      }
+    );
+
+    // 响应拦截器
+    this.instance.interceptors.response.use(
+      (response) => {
+        this._removePendingRequest(response.config);
+
+        const { data, config } = response;
+
+        // 业务成功:脱壳返回
+        if (data.code === 200) {
+          return data.data;
+        }
+
+        // 业务错误
+        if (!config.skipGlobalError) {
+          console.error('Business Error:', data.message);
+        }
+
+        // Token 过期
+        if (data.code === 401) {
+          console.warn('Token已过期,请重新登录');
+          localStorage.removeItem('token');
+          window.location.href = '/#/login';
+        }
+
+        return Promise.reject(new Error(data.message || 'Error'));
+      },
+      (error) => {
+        if (axios.isCancel(error)) {
+          return Promise.reject('Request Canceled by Duplicate Prevention');
+        }
+
+        if (error.config) {
+          this._removePendingRequest(error.config);
+        }
+
+        const status = error.response?.status;
+        let errorMsg = error.message;
+
+        switch (status) {
+          case 400: errorMsg = '请求参数错误'; break;
+          case 401: errorMsg = '未授权,请重新登录'; break;
+          case 403: errorMsg = '拒绝访问'; break;
+          case 404: errorMsg = '请求地址不存在'; break;
+          case 500: errorMsg = '服务器内部错误'; break;
+          case 502: errorMsg = '网关错误'; break;
+          case 503: errorMsg = '服务不可用'; break;
+          case 504: errorMsg = '网关超时'; break;
+          default: errorMsg = '网络连接异常';
+        }
+
+        console.error('HTTP Error:', errorMsg);
+        return Promise.reject(error);
+      }
+    );
+  }
+
+  get(url, config) {
+    return this.instance.get(url, config);
+  }
+
+  post(url, data, config) {
+    return this.instance.post(url, data, config);
+  }
+
+  put(url, data, config) {
+    return this.instance.put(url, data, config);
+  }
+
+  delete(url, config) {
+    return this.instance.delete(url, config);
+  }
+}
+
+export const http = new RequestHttp({
+  baseURL: process.env.VUE_APP_API_BASE || '',
+  timeout: 15000,
+  cancelDuplicate: true,
+});

+ 7 - 7
src/views/DataAnalysis.vue

@@ -63,7 +63,7 @@ import TechTabPane from '@/components/ui/TechTabPane.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import MenuItem from '@/components/ui/MenuItem.vue';
 
-import { makeTrafficTimeSpaceData } from '@/mock/data';
+import { apiGetTongzhouMenuTree, apiGetTrafficTimeSpace } from '@/api';
 
 
 export default {
@@ -208,8 +208,8 @@ export default {
     created() {
 
     },
-    mounted() {
-
+    async mounted() {
+        this.menuData = await apiGetTongzhouMenuTree() || [];
     },
     methods: {
         handleMenuClick(nodeData) {
@@ -239,17 +239,17 @@ export default {
         // ================= 测试用例:模拟各种点击行为 =================
 
         // 模拟 1:打开特勤安保路线面板
-        testOpenSecurityRoute(data) {
+        async testOpenSecurityRoute(data) {
+            const tsData = await apiGetTrafficTimeSpace();
             this.$refs.layout.openDialog({
-                id: data.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                id: data.id,
                 title: data.label,
                 component: 'TrafficTimeSpace',
                 width: 1000,
                 height: 500,
                 center: true,
                 showClose: true,
-                // position: { x: 400, y: 450 },
-                data: makeTrafficTimeSpaceData(),
+                data: tsData,
             });
         },
 

+ 33 - 58
src/views/Home.vue

@@ -23,13 +23,13 @@
         <div class="panel-item">
           <PanelContainer title="在线状态">
 
-            <OnlineStatusTabs />
+            <OnlineStatusTabs :deviceData="onlineStatusData" />
 
           </PanelContainer>
         </div>
         <div class="panel-item">
           <PanelContainer title="控制模式">
-            <TickDonutChart :chartData="controlInfoData" centerTitle="666个" centerSubTitle="控制信息" />
+            <TickDonutChart :chartData="controlInfoData" :centerTitle="controlTotal + '个'" centerSubTitle="控制信息" />
           </PanelContainer>
         </div>
         <div class="panel-item">
@@ -45,7 +45,7 @@
         <div class="panel-item">
           <PanelContainer title="设备状态">
 
-            <DeviceStatusTabs />
+            <DeviceStatusTabs :statusData="deviceFaultData" />
 
           </PanelContainer>
         </div>
@@ -111,7 +111,7 @@ import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
 import SeamlessScroll from '@/components/ui/SeamlessScroll.vue';
-
+import { apiGetControlModeStats, apiGetLatestAlarms, apiGetTasks, apiGetKeyIntersections, apiGetDeviceStatus, apiGetDeviceFaultStatus } from '@/api';
 
 export default {
   name: "HomePage",
@@ -134,44 +134,10 @@ export default {
       currentScrollTop: 0,   // 记录当前的精确滚动像素
       isDataDoubled: false,  // 记录数据是否已经克隆翻倍
       scrollContainer: null, // 存放表格内部滚动容器的 DOM
-      // 在线状态面板
-      controlInfoData: [
-        { name: '定周期控制', value: 400, color: '#33a3ff' }, // 蓝色
-        { name: '感应控制', value: 50, color: '#e6734d' }, // 橙色
-        { name: '干线协调', value: 200, color: '#10b981' }, // 绿色
-        { name: '黄闪控制', value: 6, color: '#eab308' }, // 黄色
-        // { name: '关灯控制',   value: null,color: '#64748b' }, // 灰色 (没有值传入null即可隐藏数字)
-        { name: '自适应控制', value: 10, color: '#2dd4bf' }, // 青色
-        // { name: '中心控制',   value: null,color: '#8b5cf6' }, // 紫色
-        // { name: '全红控制',   value: null,color: '#f43f5e' }  // 红色
-      ],
-      // 消息模拟数据
-      alarmData: [
-        {
-          id: '1',
-          title: '1.通讯中断',
-          type: 'error', // 渲染为红色
-          time: '16:28:28',
-          description: '中关村大街-科学院南路口-设备离线',
-          position: [116.695702, 39.892886]
-        },
-        {
-          id: '2',
-          title: '2.降级黄闪',
-          type: 'warning', // 渲染为黄色
-          time: '16:28:28', // 
-          description: '中关村大街-科学院南路口-设备离线',
-          position: [116.6365, 39.8850]
-        },
-        {
-          id: '3',
-          title: '3.降级黄闪',
-          type: 'warning',
-          time: '16:28:28',
-          description: '中关村大街-科学院南路口-设备离线',
-          position: [116.6800, 39.8860]
-        }
-      ],
+      controlInfoData: [],
+      alarmData: [],
+      onlineStatusData: null,
+      deviceFaultData: null,
       // 1. 表头
       tableColumns: [
         { label: '序号', key: 'id', width: '14%' },
@@ -182,26 +148,14 @@ export default {
         { label: '操作', key: 'action', width: '14%' }
       ],
 
-      // 2. 模拟数据源
-      tableData: [
-        { id: 1, name: '大型活动交通安保', executor: '张明', level: '一级', status: '未开始' },
-        { id: 2, name: '道路施工路段交通引导', executor: '李强', level: '一级', status: '未开始' },
-        { id: 3, name: '酒驾醉驾专项查缉', executor: '王芳', level: '二级', status: '进行中' },
-        { id: 4, name: '交通信号灯故障排查', executor: '赵伟', level: '一级', status: '未开始' },
-        { id: 5, name: '应急救援通道清障', executor: '陈静', level: '一级', status: '未开始' }
-      ],
+      tableData: [],
       // 1. 表头
       keyIntersectionColumns: [
         { label: '路口', key: 'intersection', align: 'left' }, // 路口名称较长,建议左对齐更好看
         { label: '运营模式', key: 'mode', width: '120px' },
         { label: '方案号', key: 'plan', width: '80px' }
       ],
-      // 2. 模拟数据源 (完美还原截图内容)
-      keyIntersectionData: [
-        { id: 1, intersection: '实行东街双园路交叉路口', mode: '定周期控制', plan: '4' },
-        { id: 2, intersection: '实行东街双园路交叉路口', mode: '自适应控制', plan: '1' },
-        { id: 3, intersection: '实行东街双园路交叉路口', mode: '感应控制', plan: '5' }
-      ],
+      keyIntersectionData: [],
       // 搜索数据
       currentMapSearch: 'all',
       mapSearchOptions: [
@@ -211,13 +165,34 @@ export default {
       ]
     };
   },
-  mounted() {
-    
+  computed: {
+    controlTotal() {
+      return this.controlInfoData.reduce((s, m) => s + (m.value || 0), 0);
+    }
+  },
+  async mounted() {
+    await this.fetchPageData();
   },
   beforeDestroy() {
   
   },
   methods: {
+    async fetchPageData() {
+      const [controlData, alarmData, taskData, keyData, onlineData, faultData] = await Promise.all([
+        apiGetControlModeStats(),
+        apiGetLatestAlarms(),
+        apiGetTasks({ pageSize: 10 }),
+        apiGetKeyIntersections(),
+        apiGetDeviceStatus(),
+        apiGetDeviceFaultStatus(),
+      ]);
+      this.controlInfoData = controlData || [];
+      this.alarmData = alarmData?.list || alarmData || [];
+      this.tableData = taskData?.list || taskData || [];
+      this.keyIntersectionData = keyData || [];
+      this.onlineStatusData = onlineData || null;
+      this.deviceFaultData = faultData || null;
+    },
     // 处理忽略逻辑
     onAlarmIgnore({ item, index }) {
       console.log('点击了忽略:', item.title);

+ 12 - 9
src/views/Login.vue

@@ -76,7 +76,7 @@
 <script>
 import CaptchaCanvas from "@/components/CaptchaCanvas.vue";
 import LoginLayout from "@/layouts/LoginLayout.vue";
-import { mockLogin } from "@/mock/api";
+import { apiLogin, apiGetCaptcha } from "@/api";
 
 export default {
   name: "LoginPage",
@@ -118,17 +118,20 @@ export default {
     async onLogin() {
       this.hint = "";
 
-      const res = await mockLogin({
-        username: this.username,
-        password: this.password,
-        captcha: this.captchaInput
-      });
-
-      if (!res.ok) {
-        this.hint = res.message;
+      let data;
+      try {
+        data = await apiLogin({
+          username: this.username,
+          password: this.password,
+          captcha: this.captchaInput
+        });
+      } catch (e) {
+        this.hint = e.message || '登录失败';
         return;
       }
 
+      localStorage.setItem('token', data.token);
+
 
       this.doorNavigated = false;
       this.isDoorOpening = false;

+ 31 - 279
src/views/StatusMonitoring.vue

@@ -94,15 +94,9 @@ import TechTabPane from '@/components/ui/TechTabPane.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import MenuItem from '@/components/ui/MenuItem.vue';
 import ButtonGroup from '@/components/ui/ButtonGroup.vue';
-import { makeTrafficTimeSpaceData } from '@/mock/data';
-import testVideo1 from '@/assets/videos/video1.mp4';
-import testVideo2 from '@/assets/videos/video2.mp4';
-import arrow1 from '@/assets/images/arrow_1.png';
-import arrow2 from '@/assets/images/arrow_2.png';
-import arrow3 from '@/assets/images/arrow_3.png';
-import arrow4 from '@/assets/images/arrow_4.png';
 import TechTable from '@/components/ui/TechTable.vue';
 import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
+import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetOverviewTopCharts } from '@/api';
 
 
 export default {
@@ -122,128 +116,7 @@ export default {
         return {
             // 左侧边栏数据
             activeLeftTab: 'overview',
-            menuData: [
-                {
-                    id: 'root-1',
-                    label: '主控中心',
-                    icon: 'el-icon-monitor', // 这里可以替换为你项目用的图标类名,比如 iconfont
-                    children: [
-                        {
-                            id: 'team-1',
-                            label: '北京市交警总队',
-                            children:
-                                [
-                                    {
-                                        id: 'dist-1',
-                                        label: '通州区',
-                                        children: [
-                                            {
-                                                id: 'street-1',
-                                                label: '中仓街道',
-                                                children: [
-                                                    { id: 'node-1', label: '新华东街 - 新华南路' },
-                                                    { id: 'node-2', label: '玉带河东街 - 车站路' },
-                                                    { id: 'node-3', label: '赵登禹大街 - 新华东街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-2',
-                                                label: '新华街道',
-                                                children: [
-                                                    { id: 'node-4', label: '新华南北街交叉口' },
-                                                    { id: 'node-5', label: '通胡大街 - 紫运中路' },
-                                                    { id: 'node-6', label: '芙蓉东路 - 通胡大街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-3',
-                                                label: '北苑街道',
-                                                children: [
-                                                    { id: 'node-7', label: '北苑路口' },
-                                                    { id: 'node-8', label: '新华西街 - 北苑南路' },
-                                                    { id: 'node-9', label: '新城南街 - 新华西街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-4',
-                                                label: '玉桥街道',
-                                                children: [
-                                                    { id: 'node-10', label: '玉桥西路 - 玉桥西里中街' },
-                                                    { id: 'node-11', label: '运河西大街 - 玉桥中路' },
-                                                    { id: 'node-12', label: '梨园南街 - 运河西大街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-5',
-                                                label: '杨庄街道',
-                                                children: [
-                                                    { id: 'node-13', label: '怡乐中街 - 九棵树西路' },
-                                                    { id: 'node-14', label: '翠屏西路 - 怡乐中街' },
-                                                    { id: 'node-15', label: '杨庄路 - 新华西街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-6',
-                                                label: '九棵树街道',
-                                                children: [
-                                                    { id: 'node-16', label: '九棵树东路 - 九棵树西路' },
-                                                    { id: 'node-17', label: '云景东路 - 九棵树东路' },
-                                                    { id: 'node-18', label: '群芳南街 - 云景东路' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-7',
-                                                label: '临河里街道',
-                                                children: [
-                                                    { id: 'node-19', label: '梨园中街 - 九棵树东路' },
-                                                    { id: 'node-20', label: '临河里路 - 梨园中街' },
-                                                    { id: 'node-21', label: '万盛南街 - 临河里路' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-8',
-                                                label: '潞邑街道',
-                                                children: [
-                                                    { id: 'node-22', label: '潞苑北大街 - 潞邑西路' },
-                                                    { id: 'node-23', label: '潞苑南大街 - 潞邑三路' },
-                                                    { id: 'node-24', label: '东六环 - 潞苑北大街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-9',
-                                                label: '通运街道',
-                                                children: [
-                                                    { id: 'node-25', label: '通胡大街 - 东六环' },
-                                                    { id: 'node-26', label: '运河东大街 - 通胡大街' },
-                                                    { id: 'node-27', label: '紫运中路 - 运河东大街' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-10',
-                                                label: '潞源街道',
-                                                children: [
-                                                    { id: 'node-28', label: '宋梁路 - 运河东大街' },
-                                                    { id: 'node-29', label: '东六环 - 运河东大街' },
-                                                    { id: 'node-30', label: '潞源北街 - 宋梁路' }
-                                                ]
-                                            },
-                                            {
-                                                id: 'street-11',
-                                                label: '文景街道',
-                                                children: [
-                                                    { id: 'node-31', label: '环球大道 - 九棵树东路' },
-                                                    { id: 'node-32', label: '颐瑞东路 - 环球大道' },
-                                                    { id: 'node-33', label: '万盛南街 - 颐瑞东路' }
-                                                ]
-                                            }
-                                        ]
-                                    }
-                                ]
-
-                        }
-                    ]
-                }
-            ],
+            menuData: [],
             // 地图模式切换数据
             currentView: 'map-mode',
             viewOptions: [
@@ -260,14 +133,7 @@ export default {
                 { label: '操作', key: 'action', width: '14%' }
             ],
 
-            // 2. 模拟数据源
-            tableData: [
-                { id: 1, name: '大型活动交通安保', executor: '测试', level: '一级', status: '未开始' },
-                { id: 2, name: '道路施工路段交通引导张飞', executor: '张飞', level: '一级', status: '未开始' },
-                { id: 3, name: '酒驾醉驾专项查缉', executor: '关将', level: '二级', status: '进行中' },
-                { id: 4, name: '交通信号灯故障排查', executor: '刘备', level: '一级', status: '未开始' },
-                { id: 5, name: '应急救援通道清障', executor: '孙权', level: '一级', status: '未开始' }
-            ],
+            tableData: [],
         };
     },
     watch: {
@@ -282,7 +148,15 @@ export default {
     created() {
 
     },
-    mounted() {
+    async mounted() {
+        // 加载菜单和任务数据
+        const [menuData, taskData] = await Promise.all([
+            apiGetTongzhouMenuTree(),
+            apiGetTasks({ pageSize: 5 }),
+        ]);
+        this.menuData = menuData || [];
+        this.tableData = taskData?.list || taskData || [];
+
         // 组件挂载时检查路由
         this.checkRouteParams();
 
@@ -298,7 +172,7 @@ export default {
             console.log('父组件接收到了地图路口点击事件:', mapData, lnglat);
             // 组装模拟数据
             let nodeData = {
-                id: Math.random(1, 100),
+                id: Math.floor(Math.random()*5)+1,
                 label: mapData.road,
             }
             console.log(nodeData);
@@ -402,7 +276,7 @@ export default {
                 }
             });
         },
-        showOverviewTopDialogs() {
+        async showOverviewTopDialogs() {
             this.$refs.layout.openDialog({
                 id: 'top-chart-overview-1',
                 title: '',
@@ -457,51 +331,26 @@ export default {
                 data: nodeData
             });
         },
-        showCrossingTopDialogs() {
+        async showCrossingTopDialogs() {
+            const chartData = await apiGetCrossingTopCharts();
+            const { onlineChart, faultChart } = chartData;
             this.$refs.layout.openDialog({
                 id: 'top-chart-crossing-1',
                 title: '',
                 component: 'RingDonutChart',
-                width: 228,
-                height: 124,
-                center: false,
-                showClose: false,
-                draggable: false,
-                resizable: false,
-                position: { x: 730, y: 130 },
-                noPadding: true,
-                data: {
-                    chartData: [
-                        { name: '在线', value: 38, color: '#4DF5F8' },
-                        { name: '离线', value: 3, color: '#FFD369' }
-                    ],
-                    centerTitle: "98%",
-                    centerSubTitle: "38/41"
-                }
+                width: 228, height: 124, center: false, showClose: false,
+                draggable: false, resizable: false,
+                position: { x: 730, y: 130 }, noPadding: true,
+                data: onlineChart
             });
-
             this.$refs.layout.openDialog({
                 id: 'top-chart-crossing-2',
                 title: '',
                 component: 'RingDonutChart',
-                width: 228,
-                height: 124,
-                center: false,
-                showClose: false,
-                draggable: false,
-                resizable: false,
-                position: { x: 980, y: 130 },
-                noPadding: true,
-                data: {
-                    chartData: [
-                        { name: '通信', value: 10, color: '#4DF5F8' },
-                        { name: '检测器', value: 8, color: '#FFA033' },
-                        { name: '灯控', value: 15, color: '#FFF587' },
-                        { name: '冲突', value: 5, color: '#FF4D4F' }
-                    ],
-                    centerTitle: "98%",
-                    centerSubTitle: "38/41"
-                }
+                width: 228, height: 124, center: false, showClose: false,
+                draggable: false, resizable: false,
+                position: { x: 980, y: 130 }, noPadding: true,
+                data: faultChart
             });
         },
         // 路口列表模式下弹窗
@@ -510,19 +359,18 @@ export default {
             this.showCrossingDetailDialogs(rowData);
         },
         // 显示干线弹窗组
-        showTrunkLineDalogs(nodeData) {
+        async showTrunkLineDalogs(nodeData) {
             console.log('显示干线弹窗组', nodeData.id, nodeData.label);
-
+            const tsData = await apiGetTrafficTimeSpace();
             this.$refs.layout.openDialog({
-                id: nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                id: nodeData.id,
                 title: nodeData.label,
                 component: 'TrafficTimeSpace',
                 width: 1000,
                 height: 500,
                 center: true,
                 showClose: true,
-                // position: { x: 400, y: 450 },
-                data: makeTrafficTimeSpaceData(),
+                data: tsData,
             });
         },
         // 显示特勤弹窗组
@@ -571,9 +419,8 @@ export default {
         async openDutyDetailDialog(nodeData) {
             console.log('准备打开特勤线路详情:', nodeData);
             // 1. 获取数据
-            const panelData = await this.fetchSpecialTaskData();
-            
-            panelData.taskInfo.name = nodeData.label;
+            const panelData = await apiGetSpecialTaskMonitorData(nodeData.id);
+
             const id = 'special-task-dialog' + new Date().getTime();
             // 2. 呼出弹窗
             this.$refs.layout.openDialog({
@@ -606,101 +453,6 @@ export default {
         
         },
 
-        // 模拟从后端拉取数据
-        async fetchSpecialTaskData() {
-            // 模拟 API 请求延迟
-            await new Promise(resolve => setTimeout(resolve, 500));
-
-            // 这是后端返回的完整数据结构
-            return {
-                // 1. 头部任务信息
-                taskInfo: {
-                    name: '北京路演唱会特勤路线',
-                    time: '12:00-14:00',
-                    manager: '张飞',
-                    level: '一级',
-                    status: '进行中',
-                    statusColor: '#ff4d4f' // 红色状态灯
-                },
-                // 2. 视频流列表 (支持有源和无源)
-                videos: [
-                    { id: 1, url: testVideo1 }, // 有视频
-                    { id: 2, url: testVideo2 }, // 无视频,展示占位
-                    { id: 3, url: testVideo1 },
-                    { id: 4, url: testVideo2 } // 第4个,用于测试轮播翻页
-                ],
-                // 3. 路口控制卡片列表
-                intersections: [
-                    {
-                        id: 'INT_01',
-                        name: '京原路与北宫路交叉口1',
-                        statusColor: '#ffaa00', // 黄色状态灯
-                        stage: 3,
-                        mode: '步进',
-                        timeLeft: 30,
-                        btnText: '立即解锁',
-                        btnType: 'normal',
-                        phases: [
-                            { id: 1, icon: '↑', img: arrow1, active: false },
-                            { id: 2, icon: '↰', img: arrow2, active: false },
-                            { id: 3, icon: '↑', img: arrow3, active: true }, // 当前激活相位
-                            { id: 4, icon: '↰', img: arrow4, active: false }
-                        ],
-                        // 传给你原有的 IntersectionMapVideos 组件的数据
-                        mapData: {
-                            armsConfig: {
-                                N: { lanes: ['L', 'S', 'R'], cameraType: 1 },
-                                S: { lanes: ['L', 'S', 'R'], cameraType: 1 },
-                                E: { lanes: ['L', 'S', 'R'], cameraType: 2 },
-                                W: { lanes: ['L', 'S', 'R'], cameraType: 2 }
-                            },
-                            signals: {
-                                ns: { isGreen: true, time: 30, phaseName: '南北直行' },
-                                ew: { isGreen: false, time: 45, phaseName: '东西直行' }
-                            }
-                        },
-                        videoUrls: {
-                            nw: testVideo1,
-                            ne: testVideo2,
-                            sw: testVideo1,
-                            se: testVideo2
-                        }
-                    },
-                    // 为了演示,这里复制上面的数据作为第2、3、4个路口
-                    ...Array.from({ length: 3 }).map((_, i) => ({
-                        id: `INT_0${i + 2}`,
-                        name: `京原路与北宫路交叉口${i + 2}`,
-                        statusColor: '#00e5ff',
-                        stage: 2, mode: '系统', timeLeft: 15,
-                        btnText: '立即执行', btnType: 'primary',
-                        phases: [
-                            { id: 1, icon: '↑', img: arrow1, active: true },
-                            { id: 2, icon: '↰', img: arrow2, active: false },
-                            { id: 3, icon: '↑', img: arrow3, active: false },
-                            { id: 4, icon: '↰', img: arrow4, active: false }
-                        ],
-                        mapData: {
-                            armsConfig: {
-                                N: { lanes: ['L', 'S', 'R'], cameraType: 1 },
-                                S: { lanes: ['L', 'S', 'R'], cameraType: 1 },
-                                E: { lanes: ['L', 'S', 'R'], cameraType: 2 },
-                                W: { lanes: ['L', 'S', 'R'], cameraType: 2 }
-                            },
-                            signals: {
-                                ns: { isGreen: true, time: 30, phaseName: '南北直行' },
-                                ew: { isGreen: false, time: 45, phaseName: '东西直行' }
-                            }
-                        },
-                        videoUrls: {
-                            nw: testVideo1,
-                            ne: testVideo2,
-                            sw: testVideo2,
-                            se: testVideo1
-                        }
-                    }))
-                ]
-            };
-        },
 
     }
 }