Home.vue 11 KB

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