Home.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <template>
  2. <DashboardLayout>
  3. <!-- 天气 -->
  4. <template #header-left>
  5. </template>
  6. <!-- 日期 -->
  7. <template #header-right>
  8. <DateTimeWidget />
  9. </template>
  10. <!-- 地图 -->
  11. <template #map>
  12. <TongzhouTrafficMap
  13. ref="trafficMapRef"
  14. :show-search="true"
  15. />
  16. </template>
  17. <template #left>
  18. <div class="sidebar-bg">
  19. <div class="panel-list">
  20. <div class="panel-item">
  21. <PanelContainer title="在线状态">
  22. <OnlineStatusTabs :deviceData="onlineStatusData" />
  23. </PanelContainer>
  24. </div>
  25. <div class="panel-item">
  26. <PanelContainer title="控制模式">
  27. <TickDonutChart :chartData="controlInfoData" :centerTitle="controlTotal + '个'" centerSubTitle="控制信息" />
  28. </PanelContainer>
  29. </div>
  30. <div class="panel-item">
  31. <PanelContainer title="故障报警" class="alarm-panel">
  32. <SeamlessScroll :data="alarmData" :limit="3" measureSelector=".alarm-list-container">
  33. <template #default="{ list }">
  34. <AlarmMessageList :listData="list" @ignore="onAlarmIgnore" @view="onAlarmView" />
  35. </template>
  36. </SeamlessScroll>
  37. </PanelContainer>
  38. </div>
  39. </div>
  40. </div>
  41. </template>
  42. <template #right>
  43. <div class="sidebar-bg">
  44. <div class="panel-list">
  45. <div class="panel-item">
  46. <PanelContainer title="设备状态">
  47. <DeviceStatusTabs :statusData="deviceFaultData" />
  48. </PanelContainer>
  49. </div>
  50. <div class="panel-item">
  51. <PanelContainer title="勤务执行" class="table-panel">
  52. <SeamlessScroll :data="tableData" :limit="4">
  53. <template #default="{ list }">
  54. <TechTable ref="dutyTable" :columns="tableColumns" :data="list" >
  55. <template #level="{ row }">
  56. <span :title="row.level" :style="{ color: row.level === '二级' ? '#FFDF0C' : '#F00' }">
  57. {{ row.level }}
  58. </span>
  59. </template>
  60. <template #status="{ row }">
  61. <span :title="row.status" :style="{ color: row.status === '进行中' ? '#FFDF0C' : '#F00' }">
  62. {{ row.status }}
  63. </span>
  64. </template>
  65. <template #action="{ row }">
  66. <span class="action-btn" @click="handleView(row)">
  67. 查看
  68. </span>
  69. </template>
  70. </TechTable>
  71. </template>
  72. </SeamlessScroll>
  73. </PanelContainer>
  74. </div>
  75. <div class="panel-item">
  76. <PanelContainer title="关键路口" class="table-panel">
  77. <SeamlessScroll :data="keyIntersectionData" :limit="4">
  78. <template #default="{ list }">
  79. <TechTable :columns="keyIntersectionColumns" :data="list"
  80. @row-click="onIntersectionRowClick" />
  81. </template>
  82. </SeamlessScroll>
  83. </PanelContainer>
  84. </div>
  85. </div>
  86. </div>
  87. </template>
  88. <template #center>
  89. </template>
  90. </DashboardLayout>
  91. </template>
  92. <script>
  93. import DashboardLayout from '@/layouts/DashboardLayout.vue';
  94. import DateTimeWidget from '@/components/ui/DateTimeWidget.vue';
  95. import PanelContainer from '@/components/ui/PanelContainer.vue';
  96. import TickDonutChart from '@/components/ui/TickDonutChart.vue';
  97. import AlarmMessageList from '@/components/ui/AlarmMessageList.vue';
  98. import TechTable from '@/components/ui/TechTable.vue';
  99. import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
  100. import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
  101. import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
  102. import SeamlessScroll from '@/components/ui/SeamlessScroll.vue';
  103. import { apiGetControlModeStats, apiGetLatestAlarms, apiGetTasks, apiGetKeyIntersections, apiGetDeviceStatus, apiGetDeviceFaultStatus } from '@/api';
  104. export default {
  105. name: "HomePage",
  106. components: {
  107. DashboardLayout,
  108. DateTimeWidget,
  109. PanelContainer,
  110. TickDonutChart,
  111. AlarmMessageList,
  112. TechTable,
  113. TongzhouTrafficMap,
  114. OnlineStatusTabs,
  115. DeviceStatusTabs,
  116. SeamlessScroll
  117. },
  118. data() {
  119. return {
  120. dutyScrollTimer: null,
  121. currentScrollTop: 0, // 记录当前的精确滚动像素
  122. isDataDoubled: false, // 记录数据是否已经克隆翻倍
  123. scrollContainer: null, // 存放表格内部滚动容器的 DOM
  124. controlInfoData: [],
  125. alarmData: [],
  126. onlineStatusData: null,
  127. deviceFaultData: null,
  128. // 1. 表头
  129. tableColumns: [
  130. { label: '序号', key: 'id', width: '14%' },
  131. { label: '名称', key: 'name', width: '20%' },
  132. { label: '执行人', key: 'executor', width: '18%' },
  133. { label: '等级', key: 'level', width: '14%' },
  134. { label: '状态', key: 'status', width: '20%' },
  135. { label: '操作', key: 'action', width: '14%' }
  136. ],
  137. tableData: [],
  138. // 1. 表头
  139. keyIntersectionColumns: [
  140. { label: '路口', key: 'intersection', align: 'left' }, // 路口名称较长,建议左对齐更好看
  141. { label: '运营模式', key: 'mode', width: '120px' },
  142. { label: '方案号', key: 'plan', width: '80px' }
  143. ],
  144. keyIntersectionData: [],
  145. // 搜索数据
  146. currentMapSearch: 'all',
  147. mapSearchOptions: [
  148. { label: '全部', value: 'all' },
  149. { label: '选项2', value: '1' },
  150. { label: '选项3', value: '2' },
  151. ]
  152. };
  153. },
  154. computed: {
  155. controlTotal() {
  156. return this.controlInfoData.reduce((s, m) => s + (m.value || 0), 0);
  157. }
  158. },
  159. async mounted() {
  160. await this.fetchPageData();
  161. },
  162. beforeDestroy() {
  163. },
  164. methods: {
  165. async fetchPageData() {
  166. const [controlData, alarmData, taskData, keyData, onlineData, faultData] = await Promise.all([
  167. apiGetControlModeStats(),
  168. apiGetLatestAlarms(),
  169. apiGetTasks({ pageSize: 10 }),
  170. apiGetKeyIntersections(),
  171. apiGetDeviceStatus(),
  172. apiGetDeviceFaultStatus(),
  173. ]);
  174. this.controlInfoData = controlData || [];
  175. // 优先使用地图生成的告警数据(与地图点位完全匹配)
  176. const mapAlarms = localStorage.getItem('alarmListFromMap');
  177. if (mapAlarms) {
  178. try {
  179. const parsed = JSON.parse(mapAlarms);
  180. this.alarmData = parsed.map((a, i) => ({
  181. ...a,
  182. time: new Date(Date.now() - i * 180000).toLocaleTimeString(),
  183. }));
  184. } catch (e) {
  185. this.alarmData = alarmData?.list || alarmData || [];
  186. }
  187. } else {
  188. this.alarmData = alarmData?.list || alarmData || [];
  189. }
  190. this.tableData = taskData?.list || taskData || [];
  191. this.keyIntersectionData = keyData || [];
  192. this.onlineStatusData = onlineData || null;
  193. this.deviceFaultData = faultData || null;
  194. },
  195. // 处理忽略逻辑
  196. onAlarmIgnore({ item, index }) {
  197. console.log('点击了忽略:', item.title);
  198. // 真实业务中可能会调接口,这里我们可以演示本地移除:
  199. this.alarmData.splice(index, 1);
  200. },
  201. // 处理查看逻辑
  202. onAlarmView({ item, index }) {
  203. console.log('点击了查看:', item);
  204. let position;
  205. // 优先使用 item 自身携带的坐标(最可靠,不受索引偏移影响)
  206. if (item.position && item.position.length === 2) {
  207. position = [Number(item.position[0]), Number(item.position[1])];
  208. } else {
  209. // 兜底:通过索引从 localStorage 查找
  210. const actualIndex = index % 12;
  211. const positionStr = localStorage.getItem(`pos${actualIndex + 1}`);
  212. if (positionStr) {
  213. const parts = positionStr.split(',');
  214. position = [Number(parts[0]), Number(parts[1])];
  215. } else {
  216. console.warn('未找到对应的位置信息,使用默认位置');
  217. position = [116.663, 39.905];
  218. }
  219. }
  220. // 地图联动
  221. console.log(position);
  222. this.$refs.trafficMapRef.focusByLocation(position);
  223. },
  224. onIntersectionRowClick({ row, index }) {
  225. console.log(`准备跳转查看关键路口详情,当前路口:`, row.id, row.intersection);
  226. // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
  227. // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
  228. this.$router.push({
  229. path: '/surve', // 替换为真实的路由
  230. query: {
  231. tab: 'crossing', // 告诉目标页面:把 Tab 切到“路口”页
  232. action: 'open-dialog', // 告诉目标页面:直接打开弹窗
  233. id: row.id, // 传递路口 ID
  234. }
  235. });
  236. },
  237. // 处理搜索
  238. handleSearch() {
  239. console.log('搜索', this.currentMapSearch);
  240. },
  241. // 跳转逻辑修改
  242. handleView(row) {
  243. console.log('准备跳转查看特勤线路,当前数据:', row);
  244. // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
  245. // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
  246. this.$router.push({
  247. path: '/surve', // 或者使用 name: 'StatusMonitoring'
  248. query: {
  249. tab: 'specialDuty', // 告诉目标页面:把 Tab 切到“特勤线路”
  250. action: 'open-dialog', // 告诉目标页面:直接弹窗
  251. id: row.id // 传递这条特勤线路的唯一 ID,用来请求弹窗详情接口
  252. }
  253. });
  254. },
  255. }
  256. }
  257. </script>
  258. <style scoped>
  259. .sidebar-bg {
  260. height: 100%;
  261. background: linear-gradient(180deg, rgba(10, 25, 60, 0.75) 0%, rgba(5, 15, 40, 0.85) 100%);
  262. }
  263. .panel-list {
  264. display: flex;
  265. flex-direction: column;
  266. gap: 16px;
  267. height: 100%;
  268. }
  269. .panel-item {
  270. /* 可用高度 = 100vh - 80px(header) - 20px(上padding) - 60px(下padding)
  271. 3个面板 + 2个gap(16px) = 32px
  272. 每个面板 = (100vh - 192px) / 3 */
  273. height: calc((100vh - 192px) / 3);
  274. }
  275. .table-panel ::v-deep .panel-content {
  276. padding: 0;
  277. }
  278. .action-btn {
  279. color: #c4d7f0;
  280. cursor: pointer;
  281. transition: color 0.3s;
  282. user-select: none;
  283. }
  284. .action-btn:hover {
  285. color: #32F6F8;
  286. text-decoration: underline;
  287. }
  288. .map-legend-pos {
  289. position: absolute;
  290. bottom: 100px;
  291. right: 0;
  292. }
  293. .top-search-pos {
  294. position: absolute;
  295. top: 0;
  296. right: 0;
  297. display: flex;
  298. flex-direction: row;
  299. column-gap: 9px;
  300. }
  301. .table-panel ::v-deep .tech-table-wrapper {
  302. height: auto !important;
  303. max-height: none !important;
  304. overflow: visible !important;
  305. }
  306. .alarm-panel ::v-deep .alarm-list-container {
  307. height: auto !important;
  308. overflow: visible !important;
  309. padding: 0 15px !important;
  310. }
  311. .alarm-panel ::v-deep .panel-content {
  312. padding: 0;
  313. }
  314. </style>