TrunkCoordination.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. <template>
  2. <DashboardLayout ref="layout" layoutClass="special-situation-monitoring">
  3. <template #header-left>
  4. </template>
  5. <template #header-right>
  6. <!-- 日期 -->
  7. <DateTimeWidget />
  8. </template>
  9. <template #map>
  10. <!-- 路口列表 -->
  11. <div v-if="currentView === 'list-mode' && activeLeftTab === 'crossing'" class="list-mode-panel">
  12. <CrossingListPanel :onViewDetail="handleCrossingViewDetail"/>
  13. </div>
  14. <!-- 地图 -->
  15. <TongzhouTrafficMap v-else ref="trafficMapRef"
  16. :mode="activeLeftTab === 'crossing' ? '路口' : activeLeftTab === 'trunkLine' ? '干线' : activeLeftTab === 'specialDuty' ? '特勤' : ''"
  17. @map-crossing-click="handleMapCrossingClick"
  18. @map-crossing-mouseover="handleMapCrossingMouseover"
  19. @map-crossing-mouseout="handleMapCrossingMouseout"
  20. @bindTrunkMenuTree="handleTrunkMenuUpdate"
  21. :show-search="true"
  22. :search-offset-right="100"
  23. :search-offset-top="120"
  24. />
  25. </template>
  26. <template #left>
  27. <div class="left-sidebar-wrap">
  28. <div class="left-sidebar-title">干线协调</div>
  29. <div class="left-sidebar-body">
  30. <div v-if="trunkLineMenuData.length === 0" class="sidebar-loading">加载中...</div>
  31. <template v-else>
  32. <MenuItem v-for="item in trunkLineMenuData" :key="item.id" :node="item" :level="0"
  33. @node-click="handleTrunkLineClick">
  34. <template #label="{ node }">
  35. <span v-if="node.children && node.children.length > 0">{{ node.label }}</span>
  36. <span v-else>{{ node.label }} 绿波带</span>
  37. </template>
  38. </MenuItem>
  39. </template>
  40. </div>
  41. </div>
  42. </template>
  43. <template #right>
  44. <!-- 模式切换按钮组 -->
  45. <div class="mode-switch" v-if="activeLeftTab === 'crossing'">
  46. <ButtonGroup v-model="currentView" :options="viewOptions" @select="onViewSelect" />
  47. </div>
  48. </template>
  49. <template #center>
  50. <!-- 顶部常驻图表区域(替代弹窗) -->
  51. <div class="top-charts-bar" v-if="currentView !== 'list-mode'">
  52. <!-- 总览Tab -->
  53. <template v-if="activeLeftTab === 'overview'">
  54. <div class="top-chart-box overview-chart-box">
  55. <OnlineStatusTabs :deviceData="onlineStatusData" />
  56. </div>
  57. <div class="top-chart-box overview-chart-box">
  58. <DeviceStatusTabs :statusData="deviceFaultData" />
  59. </div>
  60. </template>
  61. <!-- 路口Tab -->
  62. <template v-if="activeLeftTab === 'crossing'">
  63. <div class="top-chart-box overview-chart-box">
  64. <OnlineStatusTabs :deviceData="onlineStatusData" />
  65. </div>
  66. <div class="top-chart-box overview-chart-box">
  67. <DeviceStatusTabs :statusData="deviceFaultData" />
  68. </div>
  69. </template>
  70. </div>
  71. </template>
  72. </DashboardLayout>
  73. </template>
  74. <script>
  75. import DashboardLayout from '@/layouts/DashboardLayout.vue';
  76. import DateTimeWidget from '@/components/ui/DateTimeWidget.vue';
  77. import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
  78. import MenuItem from '@/components/ui/MenuItem.vue';
  79. import ButtonGroup from '@/components/ui/ButtonGroup.vue';
  80. import CrossingListPanel from '@/components/ui/CrossingListPanel.vue';
  81. import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
  82. import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
  83. import { apiGetTongzhouMenuTree, apiGetTasks, apiGetTrafficTimeSpace, apiGetCrossingTopCharts, apiGetSpecialTaskMonitorData, apiGetCrossingDetailData, apiGetDeviceStatus, apiGetDeviceFaultStatus } from '@/api';
  84. export default {
  85. name: "TrunkCoordination",
  86. components: {
  87. DashboardLayout,
  88. DateTimeWidget,
  89. TongzhouTrafficMap,
  90. MenuItem,
  91. ButtonGroup,
  92. CrossingListPanel,
  93. OnlineStatusTabs,
  94. DeviceStatusTabs,
  95. },
  96. data() {
  97. return {
  98. // 左侧边栏数据
  99. activeLeftTab: 'trunkLine', // 默认选中干线Tab
  100. menuData: [],
  101. trunkLineMenuData: [],
  102. // 地图模式切换数据
  103. currentView: 'map-mode',
  104. viewOptions: [
  105. { label: '列表模式', value: 'list-mode' },
  106. { label: '地图模式', value: 'map-mode' },
  107. ],
  108. // 特勤表头
  109. tableColumns: [
  110. { label: '序号', key: 'id', width: '10%' },
  111. { label: '名称', key: 'name', width: '30%' },
  112. { label: '执行人', key: 'executor', width: '15%' },
  113. { label: '等级', key: 'level', width: '12%' },
  114. { label: '状态', key: 'status', width: '13%' },
  115. { label: '操作', key: 'action', width: '20%' }
  116. ],
  117. tableData: [],
  118. // 路口顶部图表数据
  119. crossingTopCharts: {},
  120. // 路口多选分屏
  121. crossingSelections: [],
  122. maxCrossingSlots: 4,
  123. // 在线状态 & 设备状态数据
  124. onlineStatusData: null,
  125. deviceFaultData: null,
  126. };
  127. },
  128. watch: {
  129. // 监听路由参数变化(解决多次从首页点击不同数据跳转过来,页面不刷新的问题)
  130. '$route.query': {
  131. handler() {
  132. this.checkRouteParams();
  133. },
  134. deep: true
  135. }
  136. },
  137. created() {
  138. },
  139. async mounted() {
  140. // 加载菜单和任务数据
  141. const [menuData, taskData, onlineData, faultData] = await Promise.all([
  142. apiGetTongzhouMenuTree(),
  143. apiGetTasks({ pageSize: 5 }),
  144. apiGetDeviceStatus(),
  145. apiGetDeviceFaultStatus(),
  146. ]);
  147. this.menuData = menuData || [];
  148. this.trunkLineMenuData = [];
  149. this.tableData = taskData?.list || taskData || [];
  150. this.onlineStatusData = onlineData || null;
  151. this.deviceFaultData = faultData || null;
  152. // 组件挂载时检查路由
  153. this.checkRouteParams();
  154. },
  155. methods: {
  156. // 处理地图鼠标滑入事件
  157. handleMapCrossingMouseover(mapData, lnglat, pixel) {
  158. console.log('父组件接收到了地图路口鼠标滑入事件:', mapData);
  159. // 组装模拟数据
  160. const scale = window.innerWidth / 1920;
  161. let nodeData = {
  162. id: mapData.id || (mapData.position[0] + mapData.position[1]),
  163. label: mapData.road,
  164. pixelX: pixel ? Math.round(pixel.x / scale) : 950,
  165. pixelY: pixel ? Math.round(pixel.y / scale) : 430,
  166. }
  167. // 离线/降级/故障:显示小弹窗提示状态
  168. const abnormalNames = ['离线', '降级', '故障'];
  169. if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
  170. const statusText = mapData.statusLabel || mapData.name;
  171. this.$refs.layout.openDialog({
  172. id: 'offline_tip_' + nodeData.id,
  173. title: nodeData.label,
  174. component: 'OfflineTip',
  175. width: 260,
  176. height: 100,
  177. center: false,
  178. showClose: false,
  179. noPadding: false,
  180. draggable: false,
  181. resizable: false,
  182. position: { x: (nodeData.pixelX || 950) + 10, y: nodeData.pixelY || 430 },
  183. data: { status: statusText, road: nodeData.label },
  184. });
  185. return;
  186. }
  187. console.log(nodeData);
  188. if (this.activeLeftTab === 'overview') { // 总览
  189. this.showOverviewDalogs(nodeData);
  190. } else if (this.activeLeftTab === 'crossing') { // 路口
  191. this.showOverviewDalogs(nodeData);
  192. }
  193. },
  194. // 处理地图鼠标滑出事件
  195. handleMapCrossingMouseout(mapData) {
  196. console.log('父组件接收到了地图路口鼠标滑出事件:', mapData);
  197. if (!mapData) return;
  198. const id = mapData.id || (mapData.position[0] + mapData.position[1]);
  199. this.$refs.layout.handleDialogClose('offline_tip_' + id);
  200. if (this.activeLeftTab === 'overview') { // 总览
  201. this.$refs.layout.handleDialogClose('crossing3_' + id);
  202. } else if (this.activeLeftTab === 'crossing') { // 路口
  203. this.$refs.layout.handleDialogClose('crossing3_' + id);
  204. }
  205. },
  206. // 处理地图点击事件
  207. handleMapCrossingClick(mapData, lnglat, pixel) {
  208. console.log('父组件接收到了地图路口点击事件:', mapData);
  209. // 离线/降级/故障状态不弹详情
  210. const abnormalNames = ['离线', '降级', '故障'];
  211. if (abnormalNames.includes(mapData.statusLabel || mapData.name)) {
  212. this.$msg({
  213. title: '提示',
  214. message: `路口「${mapData.road || mapData.id}」设备${mapData.statusLabel || mapData.name},无法查看详情`,
  215. duration: 3000,
  216. });
  217. return;
  218. }
  219. // 组装模拟数据
  220. const scale = window.innerWidth / 1920;
  221. let nodeData = {
  222. id: mapData.id || (mapData.position[0] + mapData.position[1]),
  223. label: mapData.road,
  224. // 反算为设计稿坐标(SmartDialog 内部会再乘 scale)
  225. pixelX: pixel ? Math.round(pixel.x / scale) : 950,
  226. pixelY: pixel ? Math.round(pixel.y / scale) : 430,
  227. }
  228. // 干线marker点击时,从菜单数据中匹配对应干线
  229. if (this.activeLeftTab === 'trunkLine' && mapData.id) {
  230. const matched = this.findTrunkMenuNode(mapData.id);
  231. if (matched) {
  232. nodeData.id = matched.id;
  233. nodeData.label = matched.label;
  234. nodeData.intersections = matched.intersections;
  235. nodeData.distances = matched.distances;
  236. }
  237. }
  238. console.log(nodeData);
  239. if (this.activeLeftTab === 'overview') { // 总览
  240. this.showCrossingDetailDialogs(nodeData);
  241. } else if (this.activeLeftTab === 'crossing') { // 路口
  242. this.showCrossingDalogs(nodeData);
  243. } else if (this.activeLeftTab === 'trunkLine') { // 干线
  244. this.showTrunkLineDalogs(nodeData);
  245. } else if (this.activeLeftTab === 'specialDuty') { // 特勤
  246. if (mapData.taskId) {
  247. nodeData.id = mapData.taskId;
  248. }
  249. this.showSpecialDutyDalogs(nodeData);
  250. }
  251. },
  252. // 列表模式Tab切换
  253. onListTabSelect(tabName) {
  254. if (tabName !== 'crossing') {
  255. this.currentView = 'map-mode';
  256. }
  257. this.handleTabClick(tabName);
  258. },
  259. // 模式切换
  260. onViewSelect(item) {
  261. console.log('你点击了:', item.label);
  262. this.currentView = item.value;
  263. this.$refs.layout.clearDialogs(); // 清空全部弹窗
  264. this.crossingSelections = [];
  265. // 列表模式弹窗
  266. if (this.currentView === 'list-mode') {
  267. // this.$refs.layout.openDialog({
  268. // id: 'crossing-list', // 这里的 ID 可以根据实际业务场景动态生成
  269. // title: '',
  270. // component: 'CrossingListPanel',
  271. // width: 1920,
  272. // height: 750,
  273. // center: false,
  274. // showClose: true,
  275. // noPadding: false,
  276. // enableDblclickExpand: false,
  277. // position: { x: 100, y: 150 },
  278. // data: {
  279. // onViewDetail: (rowData) => this.handleCrossingViewDetail(rowData)
  280. // }
  281. // });
  282. } else {
  283. this.loadCrossingTopCharts();
  284. }
  285. },
  286. // 处理tab点击
  287. handleTabClick(tabName) {
  288. console.log('父组件接收到了tab点击事件:', tabName);
  289. this.$refs.layout.clearDialogs(); // 清空全部弹窗
  290. this.crossingSelections = [];
  291. this.showTopChartDalogs(); // 根据当前Tab显示对应的顶部常驻图表
  292. },
  293. // 处理菜单folder标题点击:计算子区路口中心坐标,移动地图
  294. handleFolderClick(nodeData) {
  295. console.log('父组件接收到了文件夹点击事件:', nodeData);
  296. const leaves = [];
  297. const collect = (nodes) => {
  298. if (!Array.isArray(nodes)) return;
  299. for (const n of nodes) {
  300. if (n.children) collect(n.children);
  301. else if (n.lng && n.lat) leaves.push(n);
  302. }
  303. };
  304. collect(nodeData.children || []);
  305. if (leaves.length === 0 || !this.$refs.trafficMapRef) return;
  306. const avgLng = leaves.reduce((s, n) => s + n.lng, 0) / leaves.length;
  307. const avgLat = leaves.reduce((s, n) => s + n.lat, 0) / leaves.length;
  308. const zoom = leaves.length <= 6 ? 15 : 14;
  309. const map = this.$refs.trafficMapRef.map;
  310. if (map) map.setZoomAndCenter(zoom, [avgLng, avgLat], false, 500);
  311. },
  312. // 处理菜单点击
  313. handleMenuClick(nodeData) {
  314. console.log('父组件接收到了最底层路口点击事件:', nodeData);
  315. // 通过地图组件获取像素坐标(如果有经纬度的话)
  316. // if (nodeData.lng && nodeData.lat && this.$refs.trafficMapRef) {
  317. // // 地图联动
  318. // this.$refs.trafficMapRef.focusByLocation([nodeData.lng, nodeData.lat]);
  319. // const pixel = this.$refs.trafficMapRef.lngLatToPixel(nodeData.lng, nodeData.lat);
  320. // if (pixel) {
  321. // const scale = window.innerWidth / 1920;
  322. // nodeData.pixelX = Math.round(pixel.x / scale) + 20;
  323. // nodeData.pixelY = Math.round(pixel.y / scale);
  324. // }
  325. // }
  326. // 根据Tab来显示不同的弹窗内容
  327. if (this.activeLeftTab === 'overview') { // 总览
  328. // 临时逻辑,有真实接口后可以删除
  329. const index = Math.floor(Math.random() * 10);
  330. const position = localStorage.getItem(`pos${index + 1}`).split(',');
  331. // 地图联动
  332. this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
  333. this.showOverviewDalogs(nodeData);
  334. } else if (this.activeLeftTab === 'crossing') { // 路口
  335. this.showCrossingDalogs(nodeData);
  336. } else if (this.activeLeftTab === 'trunkLine') { // 干线
  337. this.showTrunkLineDalogs(nodeData);
  338. } else if (this.activeLeftTab === 'specialDuty') { // 特勤
  339. this.showSpecialDutyDalogs(nodeData);
  340. }
  341. },
  342. // 处理弹窗双击展开(通过 onExpand 回调从 Layout 传入)
  343. handleDoubleClickExpend(nodeData) {
  344. console.log('处理弹窗双击事件', nodeData);
  345. if (this.activeLeftTab === 'crossing' || this.activeLeftTab === 'overview') {
  346. this.showCrossingDetailDialogs(nodeData);
  347. }
  348. },
  349. // 显示顶部常驻图表(根据当前Tab状态)
  350. showTopChartDalogs() {
  351. if (this.activeLeftTab === 'crossing') {
  352. this.loadCrossingTopCharts();
  353. }
  354. },
  355. // 显示总览弹窗组
  356. showOverviewDalogs(nodeData) {
  357. console.log('显示总览弹窗组', nodeData.id, nodeData.label);
  358. // 路口弹窗
  359. this.$refs.layout.openDialog({
  360. id: 'crossing3_' + nodeData.id, // 这里的 ID 可以根据实际业务场景动态生成
  361. title: nodeData.label,
  362. component: 'CrossingPanel',
  363. width: 260,
  364. height: 260,
  365. center: false,
  366. showClose: true,
  367. position: { x: (nodeData.pixelX || 950) + 20, y: nodeData.pixelY || 430 },
  368. noPadding: false,
  369. data: {
  370. ...nodeData,
  371. onExpand: (data) => this.handleDoubleClickExpend(data)
  372. },
  373. onClose: () => {
  374. // this.$refs.layout.handleDialogClose('top-chart-crossing-1');
  375. // this.$refs.layout.handleDialogClose('top-chart-crossing-2');
  376. }
  377. });
  378. },
  379. async loadCrossingTopCharts() {
  380. try {
  381. this.crossingTopCharts = await apiGetCrossingTopCharts();
  382. } catch (e) { /* ignore */ }
  383. },
  384. // 显示路口弹窗组(多选分屏)
  385. async showCrossingDalogs(nodeData) {
  386. console.log('路口多选', nodeData.id, nodeData.label);
  387. // 0. 离线检查
  388. const detailData = await apiGetCrossingDetailData(nodeData.id, { iconMode: 'simple' });
  389. if (detailData?.intersectionData?.status !== '在线') {
  390. this.$msg({
  391. title: '提示',
  392. message: `路口「${nodeData.label || nodeData.id}」设备离线,无法查看详情`,
  393. duration: 3000,
  394. });
  395. return;
  396. }
  397. // 1. 已选中 → 不重复操作
  398. const existIndex = this.crossingSelections.findIndex(c => c.id === nodeData.id);
  399. if (existIndex !== -1) {
  400. return;
  401. }
  402. // 2. 已满 → 先进先出
  403. if (this.crossingSelections.length >= this.maxCrossingSlots) {
  404. this.crossingSelections.shift();
  405. }
  406. // 3. 追加选中,带上预加载数据避免重复请求
  407. this.crossingSelections.push({ ...nodeData, _preloadedData: detailData });
  408. // 4. 打开或更新弹窗
  409. this.openCrossingMultiView();
  410. },
  411. openCrossingMultiView() {
  412. this.$refs.layout.openDialog({
  413. id: 'crossing-multi-view',
  414. title: '',
  415. component: 'CrossingMultiView',
  416. width: 1400,
  417. height: 700,
  418. center: false,
  419. position: { x: 500, y: 150 },
  420. showClose: false,
  421. noPadding: true,
  422. enableDblclickExpand: false,
  423. draggable: false,
  424. data: {
  425. crossings: [...this.crossingSelections],
  426. maxSlots: this.maxCrossingSlots,
  427. onRemove: (id) => this.handleCrossingRemove(id),
  428. onReorder: (newOrder) => {
  429. this.crossingSelections = newOrder;
  430. }
  431. },
  432. onClose: () => {
  433. this.crossingSelections = [];
  434. }
  435. });
  436. },
  437. handleCrossingRemove(id) {
  438. this.crossingSelections = this.crossingSelections.filter(c => c.id !== id);
  439. if (this.crossingSelections.length === 0) {
  440. this.$refs.layout.handleDialogClose('crossing-multi-view');
  441. } else {
  442. this.openCrossingMultiView();
  443. }
  444. },
  445. // 单个路口详情弹窗(总览双击展开等场景使用)
  446. async showCrossingDetailDialogs(nodeData) {
  447. console.log('显示路口详情弹窗组', nodeData.id, nodeData.label);
  448. const detailData = await apiGetCrossingDetailData(nodeData.id, { iconMode: 'simple' });
  449. if (detailData?.intersectionData?.status !== '在线') {
  450. this.$msg({
  451. title: '提示',
  452. message: `路口「${nodeData.label || nodeData.id}」设备离线,无法查看详情`,
  453. duration: 3000,
  454. });
  455. return;
  456. }
  457. const dialogId = 'crossing_detail' + nodeData.id;
  458. this.$refs.layout.openDialog({
  459. id: dialogId,
  460. title: ' ',
  461. component: 'CrossingDetailPanel',
  462. width: 1315,
  463. height: 682,
  464. center: false,
  465. showClose: true,
  466. position: { x: 500, y: 170 },
  467. noPadding: false,
  468. enableDblclickExpand: false,
  469. data: { ...nodeData, preloadedData: detailData },
  470. headerComponent: 'CrossingDetailHeader',
  471. headerProps: {
  472. currentRoute: { ...(detailData?.currentRoute || {}), name: nodeData.label || detailData?.currentRoute?.name },
  473. intersectionData: detailData?.intersectionData || {},
  474. cycleLength: detailData?.cycleLength || 0,
  475. }
  476. });
  477. },
  478. // 路口列表模式下弹窗
  479. handleCrossingViewDetail(rowData) {
  480. console.log('显示路口列表查看', rowData);
  481. this.showCrossingDetailDialogs(rowData);
  482. },
  483. // 显示干线弹窗组
  484. // 干线菜单叶子节点点击
  485. handleTrunkLineClick(nodeData) {
  486. console.log('干线菜单点击:', nodeData);
  487. this.showTrunkLineDalogs(nodeData);
  488. },
  489. findTrunkMenuNode(markerId) {
  490. // 从 marker ID(如 trunk_1_point_3)提取干线编号(trunk_1)
  491. const trunkId = String(markerId).replace(/_point_\d+$/, '');
  492. const leaves = [];
  493. const walk = (nodes) => {
  494. if (!Array.isArray(nodes)) return;
  495. for (const n of nodes) {
  496. if (n.children && n.children.length > 0) walk(n.children);
  497. else leaves.push(n);
  498. }
  499. };
  500. walk(this.trunkLineMenuData);
  501. return leaves.find(n => n.id === trunkId) || null;
  502. },
  503. handleTrunkMenuUpdate(segments) {
  504. this.trunkLineMenuData = [{
  505. id: 'trunk_root',
  506. label: '主控中心',
  507. icon: 'icon-control',
  508. isOpen: true,
  509. children: [{
  510. id: 'trunk_beijing',
  511. label: '北京市交警总队',
  512. icon: 'icon-police',
  513. isOpen: true,
  514. children: [{
  515. id: 'trunk_tongzhou',
  516. label: '通州区',
  517. icon: 'icon-district',
  518. isOpen: true,
  519. children: (() => {
  520. const list = segments.slice(0, 6);
  521. if (list[5]) list[5] = { ...list[5], label: '张台路与湖亦路路口' };
  522. return list;
  523. })()
  524. }]
  525. }]
  526. }];
  527. },
  528. async showTrunkLineDalogs(nodeData) {
  529. console.log('显示干线弹窗组', nodeData.id, nodeData.label);
  530. // 优先使用菜单节点自带的路口和距离数据
  531. let tsData;
  532. if (nodeData.intersections && nodeData.distances) {
  533. tsData = await apiGetTrafficTimeSpace({
  534. label: nodeData.label,
  535. intersections: nodeData.intersections,
  536. distances: nodeData.distances,
  537. });
  538. } else {
  539. tsData = await apiGetTrafficTimeSpace({ label: nodeData.label });
  540. }
  541. this.$refs.layout.openDialog({
  542. id: nodeData.id,
  543. title: nodeData.label + ' 绿波带',
  544. component: 'TrafficTimeSpace',
  545. width: 1200,
  546. height: 700,
  547. center: false,
  548. position: { x: 500, y: 150 },
  549. showClose: true,
  550. noPadding: false,
  551. data: tsData,
  552. });
  553. },
  554. // 显示特勤弹窗组
  555. showSpecialDutyDalogs(nodeData) {
  556. console.log('显示特勤弹窗组', nodeData.id, nodeData.label);
  557. this.openDutyDetailDialog(nodeData);
  558. },
  559. // === 解析路由参数并执行对应操作 ===
  560. checkRouteParams() {
  561. // 统一参数接收:特勤接收 id,路口接收 intersectionName 和 plan
  562. const { tab, action, id, } = this.$route.query;
  563. if (!tab) return; // 如果没有传递 tab 参数,说明是正常访问,不处理
  564. // 1. 处理“特勤线路”跳转
  565. if (tab === 'specialDuty') {
  566. this.activeLeftTab = 'specialDuty'; // 切换到左侧【特勤】Tab
  567. this.handleTabClick('specialDuty'); // 手动触发 Tab 切换事件,更新顶部图表
  568. // 这里判断的条件改为 id
  569. if (action === 'open-dialog' && id) {
  570. this.$nextTick(() => {
  571. this.openDutyDetailDialog({id: id, label: '特勤路口'}); // 打开特勤弹窗
  572. });
  573. }
  574. }
  575. // 2. 处理“关键路口”跳转
  576. else if (tab === 'crossing') {
  577. this.activeLeftTab = 'crossing'; // 切换到左侧【路口】Tab
  578. this.handleTabClick('crossing'); // 手动触发 Tab 切换事件,更新顶部图表
  579. if (action === 'open-dialog') {
  580. this.$nextTick(() => {
  581. // 构造一个假的 nodeData 传给详情弹窗方法
  582. this.showCrossingDetailDialogs({
  583. id: 'route_' + new Date().getTime(), // 动态生成一个ID防重复
  584. label: '路口详情',
  585. });
  586. });
  587. }
  588. }
  589. },
  590. // === 特勤详情弹窗 (你需要根据实际组件名替换) ===
  591. async openDutyDetailDialog(nodeData) {
  592. console.log('准备打开特勤线路详情:', nodeData);
  593. // 1. 获取数据
  594. const panelData = await apiGetSpecialTaskMonitorData(nodeData.id);
  595. const id = 'special-task-dialog' + new Date().getTime();
  596. // 2. 呼出弹窗
  597. this.$refs.layout.openDialog({
  598. id: id,
  599. title: ' ', // 留空以隐藏默认标题,使用自定义 Header
  600. width: 1400, // 弹窗宽一点,容纳 3 列
  601. height: 700,
  602. center: false,
  603. showClose: true,
  604. noPadding: true, // 去除默认内边距,让内部组件自己控制
  605. position: {x: 200, y: 150},
  606. // 挂载主体组件和数据
  607. component: 'SpecialTaskMonitorPanel',
  608. data: { panelData: panelData },
  609. // 挂载自定义 Header 和数据
  610. headerComponent: 'TaskMonitorHeader',
  611. headerProps: {
  612. taskData: panelData.taskInfo,
  613. onStartTask: () => {
  614. console.log('点击了立即执行');
  615. panelData.taskInfo.status = '进行中';
  616. const tableRow = this.tableData.find(r => r.id === nodeData.id);
  617. if (tableRow) tableRow.status = '进行中';
  618. },
  619. onEndTask: () => {
  620. console.log('点击了立即结束');
  621. panelData.taskInfo.status = '已完成';
  622. const tableRow = this.tableData.find(r => r.id === nodeData.id);
  623. if (tableRow) tableRow.status = '已完成';
  624. }
  625. }
  626. });
  627. return panelData;
  628. },
  629. handleSpecialTaskView(row) {
  630. console.log('查看特勤线路,当前数据:', row);
  631. this.openDutyDetailDialog(row);
  632. },
  633. async handleSpecialTaskStart(row) {
  634. console.log('立即执行特勤任务:', row);
  635. const panelData = await this.openDutyDetailDialog(row);
  636. this.$msg({
  637. title: '操作确认',
  638. message: `确认立即执行任务「${row.name}」?`,
  639. duration: 0,
  640. showConfirm: true,
  641. showCancel: true,
  642. confirmText: '确认执行',
  643. noBackdrop: true,
  644. onConfirm: () => { row.status = '进行中'; panelData.taskInfo.status = '进行中'; }
  645. });
  646. },
  647. async handleSpecialTaskEnd(row) {
  648. console.log('立即结束特勤任务:', row);
  649. const panelData = await this.openDutyDetailDialog(row);
  650. this.$msg({
  651. title: '操作确认',
  652. message: `确认立即结束任务「${row.name}」?`,
  653. duration: 0,
  654. showConfirm: true,
  655. showCancel: true,
  656. confirmText: '确认结束',
  657. noBackdrop: true,
  658. onConfirm: () => { row.status = '已完成'; panelData.taskInfo.status = '已完成'; }
  659. });
  660. },
  661. async handleSpecialTaskRestart(row) {
  662. console.log('重新开始特勤任务:', row);
  663. const panelData = await this.openDutyDetailDialog(row);
  664. this.$msg({
  665. title: '操作确认',
  666. message: `确认重新开始任务「${row.name}」?`,
  667. duration: 0,
  668. showConfirm: true,
  669. showCancel: true,
  670. confirmText: '确认开始',
  671. noBackdrop: true,
  672. onConfirm: () => { row.status = '进行中'; panelData.taskInfo.status = '进行中'; }
  673. });
  674. },
  675. }
  676. }
  677. </script>
  678. <style scoped>
  679. .mode-switch {
  680. display: flex;
  681. flex-direction: row;
  682. justify-content: flex-end;
  683. }
  684. .mode-switch>div {
  685. width: 200px;
  686. }
  687. .left-sidebar-wrap {
  688. display: flex;
  689. flex-direction: column;
  690. max-width: 400px;
  691. }
  692. .left-sidebar-title {
  693. font-size: clamp(14px, 1.04vw, 20px);
  694. font-weight: bold;
  695. color: #e0e8f0;
  696. padding: 12px 0;
  697. letter-spacing: 2px;
  698. }
  699. .left-sidebar-body {
  700. background: rgba(17, 36, 70, 0.9);
  701. outline: 2px solid #3760A9;
  702. outline-offset: -2px;
  703. height: 700px;
  704. overflow: auto;
  705. }
  706. .sidebar-loading {
  707. display: flex;
  708. align-items: center;
  709. justify-content: center;
  710. height: 120px;
  711. color: #758599;
  712. font-size: 14px;
  713. }
  714. .duty-table {
  715. margin-top: 10px;
  716. }
  717. .action-btn {
  718. cursor: pointer;
  719. color: #4da8ff;
  720. margin-right: 10px;
  721. }
  722. .action-btn:hover {
  723. text-decoration: underline;
  724. }
  725. .action-start {
  726. color: #67c23a;
  727. }
  728. .action-end {
  729. color: #f56c6c;
  730. }
  731. .top-charts-bar {
  732. display: flex;
  733. justify-content: center;
  734. gap: clamp(10px, 1.04vw, 20px);
  735. pointer-events: none;
  736. }
  737. .top-chart-box {
  738. pointer-events: auto;
  739. flex-shrink: 0;
  740. background: radial-gradient(circle at 20% 0%, rgba(40,120,200,0.5) 0%, rgba(20,60,130,0.7) 70%);
  741. box-shadow: inset 0px 0px 0.625rem 0px rgba(88, 146, 255, 0.4), inset 1.25rem 0px 1.875rem -0.625rem rgba(88, 146, 255, 0.15);
  742. border: 1px solid rgba(255, 255, 255, 0.15);
  743. border-radius: clamp(6px, 0.625vw, 12px);
  744. overflow: hidden;
  745. }
  746. /* --- 总览Tab图表尺寸适配 (原 300x160) --- */
  747. .overview-chart-box {
  748. /* clamp(最小值, 理想值(1920下比例), 最大值) */
  749. width: clamp(200px, 15.625vw, 300px);
  750. height: clamp(106px, 8.333vw, 160px);
  751. }
  752. /* --- 路口Tab图表尺寸适配 (原 228x124) --- */
  753. .crossing-chart-box {
  754. width: clamp(152px, 11.875vw, 228px);
  755. height: clamp(82px, 6.458vw, 124px);
  756. }
  757. ::v-deep .list-mode-panel {
  758. position: absolute;
  759. inset: 0;
  760. padding: 150px 30px 30px 30px;
  761. box-sizing: border-box;
  762. display: flex;
  763. flex-direction: column;
  764. overflow: hidden;
  765. }
  766. .list-mode-tabs {
  767. flex-shrink: 0;
  768. max-width: 400px;
  769. }
  770. .duty-name {
  771. display: inline-block;
  772. max-width: 8em;
  773. overflow: hidden;
  774. text-overflow: ellipsis;
  775. white-space: nowrap;
  776. vertical-align: middle;
  777. }
  778. /* 针对特勤 Tab 单独剥离背景和边框 */
  779. ::v-deep .special-duty-pane {
  780. padding: 10px 20px;
  781. }
  782. </style>