CrossingListPanel.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <template>
  2. <div class="crossing-list-panel">
  3. <div class="header-section">
  4. <TechFilterBar :config="filterConfig" v-model="searchParams" @search="handleSearch" @reset="handleReset" />
  5. </div>
  6. <div class="table-section">
  7. <div class="tech-loading-mask" v-show="loading">
  8. <div class="spinner"></div>
  9. <span class="loading-text">正在同步路口数据...</span>
  10. </div>
  11. <TechTable :columns="tableColumns" :data="tableList" height="100%">
  12. <template #phaseStatus="{ row }">
  13. <div v-if="row._offline" class="offline-placeholder">--</div>
  14. <div v-else class="mini-chart-wrapper">
  15. <SignalTimingChart :phaseData="row.phaseData" :cycleLength="row.cycle" :currentTime="row.currentTime || 0" :showAxis="false" :showScanLine="true" :showScanLineLabel="false" :autoScan="true" />
  16. </div>
  17. </template>
  18. <template #actions="{ row }">
  19. <div class="action-buttons">
  20. <span class="btn primary" @click="handleAction('upgrade', row)">升级</span>
  21. <span class="btn primary" @click="handleAction('restart', row)">重启</span>
  22. <span class="btn primary" @click="handleAction('view', row)">查看</span>
  23. </div>
  24. </template>
  25. </TechTable>
  26. </div>
  27. <div class="footer-section">
  28. <TechPagination :total="pagination.total" :currentPage.sync="pagination.currentPage"
  29. :pageSize.sync="pagination.pageSize" @current-change="fetchData" @size-change="handleSizeChange" />
  30. </div>
  31. </div>
  32. </template>
  33. <script>
  34. // 引入四个核心组件 (请确保这里的路径与你的实际项目路径一致)
  35. import TechFilterBar from '@/components/ui/TechFilterBar.vue';
  36. import TechTable from '@/components/ui/TechTable.vue';
  37. import SignalTimingChart from '@/components/ui/SignalTimingChart.vue';
  38. import TechPagination from '@/components/ui/TechPagination.vue';
  39. import { apiGetCrossingList, apiGetDictOptions } from '@/api';
  40. export default {
  41. name: 'IntersectionManage',
  42. components: {
  43. TechFilterBar,
  44. TechTable,
  45. SignalTimingChart,
  46. TechPagination
  47. },
  48. inject: { dialogManager: { default: null } },
  49. props: {
  50. onViewDetail: {
  51. type: Function,
  52. default: null
  53. }
  54. },
  55. data() {
  56. return {
  57. loading: false, // 控制加载遮罩层的显示与隐藏
  58. // === 表单查询相关 ===
  59. searchParams: {
  60. name: '',
  61. subArea: '',
  62. status: '',
  63. timeOffset: '',
  64. isKey: ''
  65. },
  66. // 查询栏的动态配置
  67. filterConfig: [
  68. { type: 'input', label: '路口名称', key: 'name', placeholder: '请输入路口名' },
  69. {
  70. type: 'select', label: '子区', key: 'subArea',
  71. options: [{ label: '全部', value: '' }] // mounted 中从接口加载
  72. },
  73. {
  74. type: 'select', label: '状态', key: 'status',
  75. options: [
  76. { label: '全部', value: '' },
  77. { label: '在线', value: 'online' },
  78. { label: '离线', value: 'offline' }
  79. ]
  80. },
  81. {
  82. type: 'select', label: '时间偏差', key: 'timeOffset',
  83. options: [
  84. { label: '全部', value: '' },
  85. { label: '无偏差', value: 'none' },
  86. { label: '有偏差', value: 'has' }
  87. ]
  88. },
  89. {
  90. type: 'select', label: '关键路口', key: 'isKey',
  91. options: [
  92. { label: '全部', value: '' },
  93. { label: '是', value: 'yes' },
  94. { label: '否', value: 'no' }
  95. ]
  96. }
  97. ],
  98. // === 表格与分页相关 ===
  99. tableColumns: [
  100. { label: '序号', key: 'index', width: '5%' },
  101. { label: '路口名', key: 'name', width: '13%' },
  102. { label: '子区', key: 'subArea', width: '8%' },
  103. { label: 'IP地址', key: 'ip', width: '11%' },
  104. { label: '状态', key: 'status', width: '6%' },
  105. { label: '时间偏差', key: 'timeOffset', width: '8%' },
  106. { label: '周期', key: 'cycle', width: '6%' },
  107. { label: '相位状态', key: 'phaseStatus', width: '21%' }, // 图表留宽一点
  108. { label: '版本信息', key: 'version', width: '12%' },
  109. { label: '操作', key: 'actions', width: '10%' }
  110. ],
  111. tableList: [],
  112. pagination: {
  113. total: 0,
  114. currentPage: 1,
  115. pageSize: 10
  116. }
  117. };
  118. },
  119. async mounted() {
  120. // 加载子区下拉选项
  121. try {
  122. const regions = await apiGetDictOptions('regions');
  123. if (regions) {
  124. const subAreaConfig = this.filterConfig.find(c => c.key === 'subArea');
  125. if (subAreaConfig) {
  126. subAreaConfig.options = [{ label: '全部', value: '' }, ...regions];
  127. }
  128. }
  129. } catch (e) { /* ignore */ }
  130. this.fetchData();
  131. },
  132. methods: {
  133. // 接口请求
  134. async fetchData() {
  135. this.loading = true; // 开启 Loading 遮罩
  136. try {
  137. const params = {
  138. ...this.searchParams,
  139. page: this.pagination.currentPage,
  140. pageSize: this.pagination.pageSize
  141. };
  142. console.log('发起请求,参数:', params);
  143. const data = await apiGetCrossingList(params);
  144. const list = data?.list || data || [];
  145. // 离线设备:时间偏差、周期、版本信息显示"--"
  146. this.tableList = list.map(row => {
  147. if (row.status === '离线') {
  148. return { ...row, timeOffset: '--', cycle: '--', version: '--', _offline: true };
  149. }
  150. return { ...row, _offline: false };
  151. });
  152. this.pagination.total = data?.total || 0;
  153. } catch (error) {
  154. console.error('获取列表失败', error);
  155. } finally {
  156. this.loading = false; // 关闭 Loading 遮罩
  157. }
  158. },
  159. // 搜索事件
  160. handleSearch() {
  161. this.pagination.currentPage = 1;
  162. this.fetchData();
  163. },
  164. // 重置事件
  165. handleReset() {
  166. this.pagination.currentPage = 1;
  167. this.fetchData();
  168. },
  169. // 切换每页条数
  170. handleSizeChange() {
  171. this.pagination.currentPage = 1;
  172. this.fetchData();
  173. },
  174. // 统一处理操作列按钮点击
  175. handleAction(type, row) {
  176. if (type === 'upgrade') {
  177. console.log('执行升级:', row.name);
  178. if (this.dialogManager) {
  179. const dialogId = 'device-upgrade-' + row.id;
  180. this.dialogManager.openDialog({
  181. id: dialogId,
  182. title: '设备升级',
  183. component: 'DeviceUpgrade',
  184. width: 400,
  185. height: 200,
  186. center: true,
  187. enableDblclickExpand: false,
  188. noPadding: true,
  189. resizable: false,
  190. data: {
  191. nodeName: row.name,
  192. nodeId: row.id,
  193. onClose: () => this.dialogManager.closeDialog(dialogId)
  194. }
  195. });
  196. }
  197. } else if (type === 'restart') {
  198. console.log('执行重启:', row.name);
  199. if (this.dialogManager) {
  200. const dialogId = 'device-restart-' + row.id;
  201. this.dialogManager.openDialog({
  202. id: dialogId,
  203. title: '设备重启',
  204. component: 'DeviceRestart',
  205. width: 400,
  206. height: 200,
  207. center: true,
  208. enableDblclickExpand: false,
  209. noPadding: true,
  210. resizable: false,
  211. data: {
  212. nodeName: row.name,
  213. nodeId: row.id,
  214. onClose: () => this.dialogManager.closeDialog(dialogId)
  215. }
  216. });
  217. }
  218. } else if (type === 'view') {
  219. console.log('查看详情:', row.name);
  220. if (this.onViewDetail) {
  221. this.onViewDetail(row);
  222. }
  223. }
  224. }
  225. }
  226. };
  227. </script>
  228. <style scoped>
  229. /* 页面主容器:深色背景,铺满视口 */
  230. .crossing-list-panel {
  231. width: 100%;
  232. height: 100%;
  233. display: flex;
  234. flex-direction: column;
  235. box-sizing: border-box;
  236. color: #fff;
  237. overflow: hidden;
  238. }
  239. /* 顶部查询区域 */
  240. .header-section {
  241. margin-bottom: 16px;
  242. flex-shrink: 0;
  243. }
  244. /* ================= 表格主体区域与 Loading 遮罩 ================= */
  245. .table-section {
  246. flex: 1;
  247. min-height: 0;
  248. min-width: 0;
  249. position: relative;
  250. overflow: hidden;
  251. padding: 10px 0;
  252. display: flex;
  253. flex-direction: column;
  254. }
  255. /* 科技风 Loading 遮罩 */
  256. .tech-loading-mask {
  257. position: absolute;
  258. top: 0;
  259. left: 0;
  260. width: 100%;
  261. height: 100%;
  262. background-color: rgba(11, 22, 44, 0.7);
  263. /* 半透明深色底 */
  264. backdrop-filter: blur(3px);
  265. /* 增加背景模糊质感 */
  266. z-index: 50;
  267. /* 层级高于表格内容 */
  268. display: flex;
  269. flex-direction: column;
  270. justify-content: center;
  271. align-items: center;
  272. }
  273. .spinner {
  274. width: 40px;
  275. height: 40px;
  276. border: 3px solid rgba(77, 168, 255, 0.1);
  277. border-top-color: #4da8ff;
  278. /* 蓝色高亮条 */
  279. border-radius: 50%;
  280. animation: spin 1s linear infinite;
  281. margin-bottom: 16px;
  282. box-shadow: 0 0 15px rgba(77, 168, 255, 0.3);
  283. }
  284. .loading-text {
  285. color: #4da8ff;
  286. font-size: 14px;
  287. letter-spacing: 2px;
  288. animation: pulse 1.5s ease-in-out infinite;
  289. }
  290. @keyframes spin {
  291. to {
  292. transform: rotate(360deg);
  293. }
  294. }
  295. @keyframes pulse {
  296. 0%,
  297. 100% {
  298. opacity: 0.5;
  299. text-shadow: none;
  300. }
  301. 50% {
  302. opacity: 1;
  303. text-shadow: 0 0 8px rgba(77, 168, 255, 0.8);
  304. }
  305. }
  306. /* 底部分页区域 */
  307. .footer-section {
  308. margin-top: 16px;
  309. flex-shrink: 0;
  310. }
  311. /* ================= 针对插槽内容的样式 ================= */
  312. /* 离线设备占位符 */
  313. .offline-placeholder {
  314. color: rgba(255, 255, 255, 0.3);
  315. text-align: center;
  316. }
  317. /* 控制相位图表在表格内的高度 */
  318. .mini-chart-wrapper {
  319. height: 30px;
  320. width: 100%;
  321. overflow: hidden;
  322. display: block;
  323. }
  324. .mini-chart-wrapper ::v-deep .chart-container {
  325. width: 100% !important;
  326. height: 100% !important;
  327. }
  328. /* 操作列按钮布局 */
  329. .action-buttons {
  330. display: flex;
  331. justify-content: center;
  332. gap: 12px;
  333. }
  334. .action-buttons .btn {
  335. font-size: 13px;
  336. cursor: pointer;
  337. transition: all 0.2s ease;
  338. user-select: none;
  339. }
  340. /* 主操作按钮 (蓝色) */
  341. .action-buttons .btn.primary {
  342. color: #4da8ff;
  343. }
  344. .action-buttons .btn.primary:hover {
  345. color: #80c4ff;
  346. text-decoration: underline;
  347. }
  348. /* 默认操作按钮 (灰色/淡蓝色) */
  349. .action-buttons .btn.default {
  350. color: #a3b8cc;
  351. }
  352. .action-buttons .btn.default:hover {
  353. color: #ffffff;
  354. text-decoration: underline;
  355. }
  356. </style>