gxlljc.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. <!--管线流量监测-->
  2. <template>
  3. <div class="flow-monitor">
  4. <!-- 页面头部 -->
  5. <div class="page-header">
  6. <div class="header-left">
  7. <el-icon><TrendCharts /></el-icon>
  8. <span class="title">管线流量监测</span>
  9. <div class="stats-badge">
  10. <span class="stat-item normal"><i class="dot"></i>正常 {{ normalCount }}</span>
  11. <span class="stat-item warning"><i class="dot"></i>预警 {{ warningCount }}</span>
  12. <span class="stat-item danger"><i class="dot"></i>告警 {{ dangerCount }}</span>
  13. <span class="stat-item offline"><i class="dot"></i>离线 {{ offlineCount }}</span>
  14. </div>
  15. </div>
  16. <div class="header-right">
  17. <el-input v-model="searchKeyword" placeholder="搜索测点名称/管线" clearable :prefix-icon="Search" style="width:200px" />
  18. <el-button type="primary" plain :icon="Refresh" @click="refreshData">刷新</el-button>
  19. </div>
  20. </div>
  21. <!-- 主内容区 -->
  22. <div class="main-layout">
  23. <!-- 左侧:测点列表 -->
  24. <div class="point-list-panel">
  25. <div class="list-header">
  26. <span>流量监测点</span>
  27. <span class="total-num">共 {{ filteredPoints.length }} 个测点</span>
  28. </div>
  29. <div class="filter-row">
  30. <el-select v-model="pipeType" placeholder="管线类型" clearable size="small" style="width:100%">
  31. <el-option label="高压管线" value="high" />
  32. <el-option label="中压管线" value="medium" />
  33. <el-option label="低压管线" value="low" />
  34. </el-select>
  35. </div>
  36. <div class="filter-row">
  37. <el-select v-model="statusFilter" placeholder="运行状态" clearable size="small" style="width:100%">
  38. <el-option label="正常" value="normal" />
  39. <el-option label="预警" value="warning" />
  40. <el-option label="告警" value="danger" />
  41. <el-option label="离线" value="offline" />
  42. </el-select>
  43. </div>
  44. <div class="point-list">
  45. <div
  46. v-for="point in filteredPoints"
  47. :key="point.id"
  48. class="point-item"
  49. :class="{ active: selectedPoint?.id === point.id, [point.status]: true }"
  50. @click="selectPoint(point)"
  51. >
  52. <div class="point-header">
  53. <span class="point-name">{{ point.name }}</span>
  54. <el-tag :type="statusTagType(point.status)" size="small">{{ statusLabel(point.status) }}</el-tag>
  55. </div>
  56. <div class="point-pipeline">{{ point.pipelineName }}</div>
  57. <div class="point-value">
  58. <span class="val">{{ point.latestFlow }}</span>
  59. <span class="unit">Nm³/h</span>
  60. <span class="threshold">量程: 0~{{ point.maxFlow }} Nm³/h</span>
  61. </div>
  62. <div class="point-sub" v-if="point.status !== 'offline'">
  63. <span>今日累计 <b>{{ point.todayTotal }}</b> Nm³</span>
  64. </div>
  65. <div class="point-addr">{{ point.address }}</div>
  66. <div class="point-time">{{ point.latestTime }}</div>
  67. </div>
  68. <el-empty v-if="filteredPoints.length === 0" description="暂无匹配测点" :image-size="50" />
  69. </div>
  70. </div>
  71. <!-- 右侧:详情面板 -->
  72. <div class="detail-panel">
  73. <template v-if="selectedPoint">
  74. <div class="detail-header">
  75. <div class="detail-title">
  76. <span class="point-name">{{ selectedPoint.name }}</span>
  77. <el-tag :type="statusTagType(selectedPoint.status)" size="small">{{ statusLabel(selectedPoint.status) }}</el-tag>
  78. </div>
  79. <div class="detail-meta">
  80. <span>所属管线:{{ selectedPoint.pipelineName }}</span>
  81. <span>管线类型:{{ pipeTypeLabel(selectedPoint.pipeType) }}</span>
  82. <span>计量表型号:TBQZ-{{ selectedPoint.meterModel }}</span>
  83. <span>安装位置:{{ selectedPoint.address }}</span>
  84. <span>监测单位:沅陵县燃气安全监测中心</span>
  85. </div>
  86. </div>
  87. <!-- 读数卡片 -->
  88. <div class="gauge-cards">
  89. <div class="gauge-card" :class="selectedPoint.status">
  90. <div class="gauge-label">瞬时流量</div>
  91. <div class="gauge-value">{{ selectedPoint.latestFlow }} <span class="unit">Nm³/h</span></div>
  92. <div class="gauge-sub">采集时间 {{ selectedPoint.latestTime }}</div>
  93. </div>
  94. <div class="gauge-card">
  95. <div class="gauge-label">今日累计</div>
  96. <div class="gauge-value high">{{ selectedPoint.todayTotal }} <span class="unit">Nm³</span></div>
  97. <div class="gauge-sub">当日0时起累计</div>
  98. </div>
  99. <div class="gauge-card">
  100. <div class="gauge-label">昨日累计</div>
  101. <div class="gauge-value low">{{ selectedPoint.yesterdayTotal }} <span class="unit">Nm³</span></div>
  102. <div class="gauge-sub">较昨日 {{ selectedPoint.dodChange }}</div>
  103. </div>
  104. <div class="gauge-card">
  105. <div class="gauge-label">运行状态</div>
  106. <div class="gauge-status" :class="selectedPoint.status">{{ statusLabel(selectedPoint.status) }}</div>
  107. <div class="gauge-sub">量程 0~{{ selectedPoint.maxFlow }} Nm³/h</div>
  108. </div>
  109. </div>
  110. <!-- 流量趋势图 -->
  111. <div class="chart-card">
  112. <div class="chart-header">
  113. <span>流量趋势(近10天)</span>
  114. <div class="chart-legend">
  115. <span class="leg-item"><i class="leg-dot flow"></i> 瞬时流量</span>
  116. <span class="leg-item"><i class="leg-line max"></i> 上限</span>
  117. </div>
  118. </div>
  119. <div ref="chartRef" class="chart-container"></div>
  120. </div>
  121. <!-- 历史数据表格 -->
  122. <div class="table-card">
  123. <div class="table-header">
  124. <span>历史监测记录</span>
  125. </div>
  126. <el-table :data="pagedHistory" size="small">
  127. <el-table-column prop="time" label="采集时间" width="160" />
  128. <el-table-column prop="flow" label="瞬时流量(Nm³/h)" width="140">
  129. <template #default="{ row }">
  130. <span :style="{ color: row.flow > selectedPoint.maxFlow ? '#f56c6c' : '#303133', fontWeight: row.flow > selectedPoint.maxFlow ? '600' : '400' }">
  131. {{ row.flow }}
  132. </span>
  133. </template>
  134. </el-table-column>
  135. <el-table-column prop="dailyTotal" label="日累计(Nm³)" width="130" />
  136. <el-table-column prop="temperature" label="温度(°C)" width="100" />
  137. <el-table-column prop="pressure" label="压力(kPa)" width="110" />
  138. <el-table-column prop="status" label="状态" width="100">
  139. <template #default="{ row }">
  140. <el-tag :type="row.flow > selectedPoint.maxFlow ? 'danger' : 'success'" size="small">
  141. {{ row.flow > selectedPoint.maxFlow ? '超限' : '正常' }}
  142. </el-tag>
  143. </template>
  144. </el-table-column>
  145. <el-table-column prop="collector" label="采集设备" min-width="140" />
  146. <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
  147. </el-table>
  148. <div class="history-pagination">
  149. <pagination
  150. v-model:page="historyPage"
  151. v-model:limit="historyPageSize"
  152. :total="historyData.length"
  153. :auto-scroll="false"
  154. layout="total, prev, pager, next"
  155. />
  156. </div>
  157. </div>
  158. </template>
  159. <div v-else class="empty-detail">
  160. <el-empty description="请从左侧选择流量监测点查看详情" :image-size="100" />
  161. </div>
  162. </div>
  163. </div>
  164. </div>
  165. </template>
  166. <script setup>
  167. import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
  168. import { TrendCharts, Search, Refresh } from '@element-plus/icons-vue'
  169. import * as echarts from 'echarts'
  170. const searchKeyword = ref('')
  171. const pipeType = ref('')
  172. const statusFilter = ref('')
  173. const selectedPoint = ref(null)
  174. const historyPage = ref(1)
  175. const historyPageSize = ref(10)
  176. const chartRef = ref(null)
  177. let chartInstance = null
  178. // ==================== 沅陵县管线流量测点数据 ====================
  179. const mockPoints = ref([
  180. {
  181. id: 1, name: '辰州北路流量计1#', pipelineName: '辰州北路高压段', pipeType: 'high',
  182. address: '沅陵县太常片区辰州北路门站出口计量间', lng: 110.3938, lat: 28.4695,
  183. meterModel: 'IIIC-150', latestFlow: 1850, maxFlow: 2500, status: 'normal', latestTime: '2026-06-13 15:32:18',
  184. todayTotal: '36,420', yesterdayTotal: '38,150', dodChange: '-4.5%'
  185. },
  186. {
  187. id: 2, name: '辰州中路流量计2#', pipelineName: '辰州中路高压段', pipeType: 'high',
  188. address: '沅陵县太常片区辰州中路调压站进口侧', lng: 110.3935, lat: 28.4665,
  189. meterModel: 'IIIC-150', latestFlow: 2620, maxFlow: 2500, status: 'danger', latestTime: '2026-06-13 15:31:45',
  190. todayTotal: '48,920', yesterdayTotal: '42,300', dodChange: '+15.6%'
  191. },
  192. {
  193. id: 3, name: '辰州南路流量计3#', pipelineName: '辰州南路高压段', pipeType: 'high',
  194. address: '沅陵县太常片区辰州南路滨江路口计量柜', lng: 110.3934, lat: 28.4575,
  195. meterModel: 'IIIC-100', latestFlow: 1120, maxFlow: 2000, status: 'normal', latestTime: '2026-06-13 15:32:05',
  196. todayTotal: '22,180', yesterdayTotal: '23,050', dodChange: '-3.8%'
  197. },
  198. {
  199. id: 4, name: '古城北路流量计1#', pipelineName: '古城北路中压段', pipeType: 'medium',
  200. address: '沅陵县太常片区古城北路与天宁路交叉口', lng: 110.3977, lat: 28.4741,
  201. meterModel: 'IIIC-80', latestFlow: 580, maxFlow: 800, status: 'normal', latestTime: '2026-06-13 15:30:22',
  202. todayTotal: '11,350', yesterdayTotal: '12,100', dodChange: '-6.2%'
  203. },
  204. {
  205. id: 5, name: '古城中路流量计2#', pipelineName: '古城中路中压段', pipeType: 'medium',
  206. address: '沅陵县太常片区古城中路太常安置区段', lng: 110.3970, lat: 28.4669,
  207. meterModel: 'IIIC-80', latestFlow: 820, maxFlow: 800, status: 'danger', latestTime: '2026-06-13 15:29:58',
  208. todayTotal: '16,840', yesterdayTotal: '13,500', dodChange: '+24.7%'
  209. },
  210. {
  211. id: 6, name: '古城南路流量计3#', pipelineName: '古城南路中压段', pipeType: 'medium',
  212. address: '沅陵县太常片区古城南路近建设路口', lng: 110.3963, lat: 28.4598,
  213. meterModel: 'IIIC-80', latestFlow: 420, maxFlow: 800, status: 'normal', latestTime: '2026-06-13 15:31:30',
  214. todayTotal: '8,920', yesterdayTotal: '9,450', dodChange: '-5.6%'
  215. },
  216. {
  217. id: 7, name: '迎宾西路流量计1#', pipelineName: '迎宾西路中压段', pipeType: 'medium',
  218. address: '沅陵县太常片区迎宾西路县政府对面', lng: 110.3870, lat: 28.4642,
  219. meterModel: 'IIIC-65', latestFlow: 310, maxFlow: 500, status: 'normal', latestTime: '2026-06-13 15:28:15',
  220. todayTotal: '6,180', yesterdayTotal: '6,540', dodChange: '-5.5%'
  221. },
  222. {
  223. id: 8, name: '迎宾东路流量计2#', pipelineName: '迎宾东路中压段', pipeType: 'medium',
  224. address: '沅陵县太常片区迎宾东路商贸城北侧', lng: 110.4054, lat: 28.4630,
  225. meterModel: 'IIIC-65', latestFlow: 280, maxFlow: 500, status: 'normal', latestTime: '2026-06-13 15:30:50',
  226. todayTotal: '5,650', yesterdayTotal: '5,980', dodChange: '-5.5%'
  227. },
  228. {
  229. id: 9, name: '建设路流量计1#', pipelineName: '建设路中压段', pipeType: 'medium',
  230. address: '沅陵县太常片区建设路县政府南侧计量站', lng: 110.3937, lat: 28.4582,
  231. meterModel: 'IIIC-80', latestFlow: 0, maxFlow: 800, status: 'offline', latestTime: '2026-06-13 08:15:00',
  232. todayTotal: '--', yesterdayTotal: '14,200', dodChange: '--'
  233. },
  234. {
  235. id: 10, name: '建设西路流量计2#', pipelineName: '建设西路中压段', pipeType: 'medium',
  236. address: '沅陵县太常片区建设西路工业园计量站', lng: 110.3840, lat: 28.4588,
  237. meterModel: 'IIIC-100', latestFlow: 680, maxFlow: 1000, status: 'normal', latestTime: '2026-06-13 15:29:30',
  238. todayTotal: '13,520', yesterdayTotal: '14,080', dodChange: '-4.0%'
  239. },
  240. {
  241. id: 11, name: '滨江路流量计1#', pipelineName: '滨江路段低压段', pipeType: 'low',
  242. address: '沅陵县太常片区滨江西路住宅区调压柜后', lng: 110.3820, lat: 28.4568,
  243. meterModel: 'IIIC-50', latestFlow: 95, maxFlow: 200, status: 'normal', latestTime: '2026-06-13 15:31:10',
  244. todayTotal: '1,850', yesterdayTotal: '1,920', dodChange: '-3.6%'
  245. },
  246. {
  247. id: 12, name: '滨江东路流量计2#', pipelineName: '滨江东路低压段', pipeType: 'low',
  248. address: '沅陵县太常片区滨江东路商业街调压柜后', lng: 110.4066, lat: 28.4557,
  249. meterModel: 'IIIC-50', latestFlow: 210, maxFlow: 200, status: 'danger', latestTime: '2026-06-13 15:30:40',
  250. todayTotal: '4,320', yesterdayTotal: '3,650', dodChange: '+18.4%'
  251. },
  252. {
  253. id: 13, name: '天宁西路流量计1#', pipelineName: '天宁西路低压段', pipeType: 'low',
  254. address: '沅陵县太常片区天宁西路太常卫生院旁', lng: 110.3850, lat: 28.4703,
  255. meterModel: 'IIIC-50', latestFlow: 75, maxFlow: 200, status: 'normal', latestTime: '2026-06-13 15:32:00',
  256. todayTotal: '1,520', yesterdayTotal: '1,650', dodChange: '-7.9%'
  257. },
  258. {
  259. id: 14, name: '天宁东路流量计2#', pipelineName: '天宁东路低压段', pipeType: 'low',
  260. address: '沅陵县太常片区天宁东路末端近滨江路', lng: 110.4088, lat: 28.4690,
  261. meterModel: 'IIIC-50', latestFlow: 88, maxFlow: 200, status: 'normal', latestTime: '2026-06-13 15:31:55',
  262. todayTotal: '1,760', yesterdayTotal: '1,840', dodChange: '-4.3%'
  263. },
  264. {
  265. id: 15, name: '建设东路流量计3#', pipelineName: '建设东路高压段', pipeType: 'high',
  266. address: '沅陵县太常片区建设东路工业园入口处', lng: 110.4050, lat: 28.4577,
  267. meterModel: 'IIIC-150', latestFlow: 2380, maxFlow: 2500, status: 'warning', latestTime: '2026-06-13 15:31:25',
  268. todayTotal: '42,150', yesterdayTotal: '38,600', dodChange: '+9.2%'
  269. }
  270. ])
  271. const normalCount = computed(() => mockPoints.value.filter(p => p.status === 'normal').length)
  272. const warningCount = computed(() => mockPoints.value.filter(p => p.status === 'warning').length)
  273. const dangerCount = computed(() => mockPoints.value.filter(p => p.status === 'danger').length)
  274. const offlineCount = computed(() => mockPoints.value.filter(p => p.status === 'offline').length)
  275. const filteredPoints = computed(() => {
  276. let list = mockPoints.value
  277. if (searchKeyword.value) {
  278. const kw = searchKeyword.value.toLowerCase()
  279. list = list.filter(p => p.name.toLowerCase().includes(kw) || p.pipelineName.toLowerCase().includes(kw))
  280. }
  281. if (pipeType.value) list = list.filter(p => p.pipeType === pipeType.value)
  282. if (statusFilter.value) list = list.filter(p => p.status === statusFilter.value)
  283. return list
  284. })
  285. function statusTagType(status) {
  286. const map = { normal: 'success', warning: 'warning', danger: 'danger', offline: 'info' }
  287. return map[status] || 'info'
  288. }
  289. function statusLabel(status) {
  290. const map = { normal: '正常', warning: '预警', danger: '告警', offline: '离线' }
  291. return map[status] || '未知'
  292. }
  293. function pipeTypeLabel(type) {
  294. const map = { high: '高压', medium: '中压', low: '低压' }
  295. return map[type] || type
  296. }
  297. // ==================== 近10天静态流量数据(每天4时段: 6/12/18/23时, 模拟用气高峰) ====================
  298. function generateHistory(point) {
  299. const baseFlow = point.latestFlow > 0 ? point.latestFlow : Math.round(point.maxFlow * 0.5)
  300. // 用气高峰因子: 早6点、午12点、晚18点为高峰,23点为低谷
  301. const hourFactors = { 6: 1.25, 12: 1.45, 18: 1.35, 23: 0.65 }
  302. // 每点预设的日波动序列(10天)
  303. const daySeeds = [
  304. { 6: 0, 12: 5, 18: 3, 23: -2 }, { 6: -2, 12: 2, 18: 0, 23: -4 },
  305. { 6: 3, 12: 8, 18: 5, 23: 0 }, { 6: -1, 12: 4, 18: 2, 23: -3 },
  306. { 6: 2, 12: 6, 18: 4, 23: 1 }, { 6: -3, 12: 1, 18: -1, 23: -5 },
  307. { 6: 1, 12: 7, 18: 6, 23: -1 }, { 6: 0, 12: 3, 18: 1, 23: -2 },
  308. { 6: -2, 12: 5, 18: 3, 23: -3 }, { 6: 4, 12: 9, 18: 6, 23: 0 }
  309. ]
  310. const data = []
  311. const now = new Date()
  312. let cumulative = 0
  313. const hours = [6, 12, 18, 23]
  314. for (let d = 9; d >= 0; d--) {
  315. const dayIdx = 9 - d
  316. for (const h of hours) {
  317. const date = new Date(now)
  318. date.setDate(date.getDate() - d)
  319. date.setHours(h, 0, 0)
  320. const factor = hourFactors[h] || 1
  321. const seed = (daySeeds[dayIdx] && daySeeds[dayIdx][h]) || 0
  322. const variation = baseFlow * (factor - 1) + seed * (point.maxFlow / 50)
  323. const flow = Math.round((baseFlow + variation) * 10) / 10
  324. const dailyCumulative = Math.round(Math.abs(flow) * 6)
  325. cumulative += dailyCumulative
  326. const temp = (24 + (h < 8 ? 1 : h < 14 ? 3 : h < 20 ? 4 : 0)).toFixed(1)
  327. const pressure = point.pipeType === 'high' ? (380 + h * 0.5).toFixed(1) :
  328. point.pipeType === 'medium' ? (190 + h * 0.3).toFixed(1) :
  329. (110 + h * 0.2).toFixed(1)
  330. data.push({
  331. time: date.toISOString().replace('T', ' ').substring(0, 19),
  332. flow,
  333. dailyTotal: Math.round(cumulative).toLocaleString(),
  334. temperature: temp,
  335. pressure,
  336. collector: `YLLL-${String(point.id).padStart(3, '0')}`,
  337. remark: flow > point.maxFlow ? '流量超量程上限' : ''
  338. })
  339. }
  340. }
  341. return data
  342. }
  343. const historyData = ref([])
  344. const pagedHistory = computed(() => {
  345. const start = (historyPage.value - 1) * historyPageSize.value
  346. return historyData.value.slice(start, start + historyPageSize.value)
  347. })
  348. // ==================== 选择测点 ====================
  349. function selectPoint(point) {
  350. selectedPoint.value = point
  351. historyPage.value = 1
  352. historyData.value = generateHistory(point)
  353. nextTick(() => renderChart())
  354. }
  355. // ==================== ECharts趋势图 ====================
  356. function renderChart() {
  357. if (!chartRef.value || !selectedPoint.value) return
  358. if (!chartInstance) {
  359. chartInstance = echarts.init(chartRef.value)
  360. }
  361. const point = selectedPoint.value
  362. const times = historyData.value.map(d => d.time.substring(5, 16).replace(' ', '\n'))
  363. const flows = historyData.value.map(d => d.flow)
  364. chartInstance.setOption({
  365. tooltip: {
  366. trigger: 'axis',
  367. formatter: params => {
  368. const p = params[0]
  369. const val = p.value
  370. const status = val > point.maxFlow ? '⚠ 超量程上限' : '✓ 正常'
  371. return `${p.axisValue.replace('\n', ' ')}<br/>瞬时流量: <b>${val} Nm³/h</b> ${status}`
  372. }
  373. },
  374. grid: { left: '3%', right: '5%', top: 20, bottom: 30, containLabel: true },
  375. xAxis: {
  376. type: 'category', data: times, boundaryGap: false,
  377. axisLine: { lineStyle: { color: '#ccc' } },
  378. axisTick: { show: false },
  379. axisLabel: { fontSize: 10, interval: 3 }
  380. },
  381. yAxis: {
  382. type: 'value', name: 'Nm³/h',
  383. axisLine: { show: false }, axisTick: { show: false },
  384. splitLine: { lineStyle: { color: '#f0f0f0' } },
  385. min: 0,
  386. max: Math.max(point.maxFlow + 300, ...flows) + 100
  387. },
  388. series: [
  389. {
  390. name: '瞬时流量', type: 'line', data: flows,
  391. smooth: true, symbol: 'circle', symbolSize: 4,
  392. lineStyle: { width: 2, color: '#52c41a' },
  393. itemStyle: { color: '#52c41a' },
  394. areaStyle: {
  395. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  396. { offset: 0, color: 'rgba(82,196,26,0.25)' },
  397. { offset: 1, color: 'rgba(82,196,26,0.02)' }
  398. ])
  399. },
  400. markLine: {
  401. silent: true,
  402. symbol: 'none',
  403. label: { fontSize: 11, formatter: '{b} {c} Nm³/h' },
  404. data: [
  405. { name: '量程上限', yAxis: point.maxFlow, lineStyle: { color: '#f56c6c', type: 'dashed', width: 1.5 } }
  406. ]
  407. }
  408. }
  409. ]
  410. })
  411. }
  412. function refreshData() {
  413. historyData.value = selectedPoint.value ? generateHistory(selectedPoint.value) : []
  414. nextTick(() => renderChart())
  415. }
  416. onMounted(() => {
  417. window.addEventListener('resize', () => { try { chartInstance?.resize() } catch {} })
  418. // 默认选中第一条数据
  419. if (mockPoints.value.length > 0) {
  420. selectPoint(mockPoints.value[0])
  421. }
  422. })
  423. onBeforeUnmount(() => {
  424. try { chartInstance?.dispose() } catch {}
  425. chartInstance = null
  426. })
  427. </script>
  428. <style scoped>
  429. .flow-monitor {
  430. background: #f0f2f6;
  431. height: 100vh;
  432. display: flex;
  433. flex-direction: column;
  434. overflow: hidden;
  435. }
  436. .page-header {
  437. background: #fff;
  438. padding: 12px 24px;
  439. display: flex;
  440. justify-content: space-between;
  441. align-items: center;
  442. border-bottom: 1px solid #e9ecef;
  443. flex-shrink: 0;
  444. }
  445. .header-left { display: flex; align-items: center; gap: 20px; }
  446. .header-left .title { font-size: 1.25rem; font-weight: 600; color: #1f2d3d; }
  447. .stats-badge { display: flex; gap: 20px; }
  448. .stat-item { font-size: 13px; display: flex; align-items: center; gap: 6px; }
  449. .stat-item .dot { width: 8px; height: 8px; border-radius: 50%; }
  450. .stat-item.normal { color: #67c23a; } .stat-item.normal .dot { background: #67c23a; }
  451. .stat-item.warning { color: #e6a23c; } .stat-item.warning .dot { background: #e6a23c; }
  452. .stat-item.danger { color: #f56c6c; } .stat-item.danger .dot { background: #f56c6c; }
  453. .stat-item.offline { color: #909399; } .stat-item.offline .dot { background: #909399; }
  454. .header-right { display: flex; gap: 12px; }
  455. .main-layout {
  456. display: flex;
  457. flex: 1;
  458. overflow: hidden;
  459. padding: 12px;
  460. gap: 12px;
  461. }
  462. /* 左侧测点列表 */
  463. .point-list-panel {
  464. width: 370px;
  465. background: #fff;
  466. border-radius: 12px;
  467. display: flex;
  468. flex-direction: column;
  469. overflow: hidden;
  470. flex-shrink: 0;
  471. }
  472. .list-header {
  473. padding: 14px 16px;
  474. border-bottom: 1px solid #e9ecef;
  475. display: flex;
  476. justify-content: space-between;
  477. align-items: center;
  478. font-weight: 600;
  479. }
  480. .total-num { font-size: 12px; color: #909399; font-weight: 400; }
  481. .filter-row { padding: 6px 16px; }
  482. .point-list { flex: 1; overflow-y: auto; padding: 4px 8px 8px; }
  483. .point-item {
  484. padding: 12px;
  485. margin-bottom: 6px;
  486. border-radius: 12px;
  487. background: #fafbfc;
  488. cursor: pointer;
  489. transition: all 0.2s;
  490. border: 1px solid #e9ecef;
  491. }
  492. .point-item:hover { background: #f0f5ff; }
  493. .point-item.active { background: #ecf5ff; border-color: #409eff; }
  494. .point-item.danger { border-left: 3px solid #f56c6c; }
  495. .point-item.warning { border-left: 3px solid #e6a23c; }
  496. .point-item.offline { opacity: 0.65; }
  497. .point-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  498. .point-name { font-weight: 600; font-size: 14px; flex: 1; }
  499. .point-pipeline { font-size: 12px; color: #909399; margin-bottom: 6px; }
  500. .point-value { display: flex; align-items: baseline; gap: 6px; margin-bottom: 4px; }
  501. .point-value .val { font-size: 24px; font-weight: 700; color: #1f2d3d; }
  502. .point-value .unit { font-size: 12px; color: #909399; }
  503. .point-value .threshold { font-size: 11px; color: #c0c4cc; margin-left: auto; }
  504. .point-sub { font-size: 12px; color: #606266; margin-bottom: 2px; }
  505. .point-sub b { color: #1f2d3d; }
  506. .point-addr { font-size: 11px; color: #909399; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  507. .point-time { font-size: 11px; color: #c0c4cc; margin-top: 2px; }
  508. /* 右侧详情面板 */
  509. .detail-panel {
  510. flex: 1;
  511. background: #fff;
  512. border-radius: 12px;
  513. display: flex;
  514. flex-direction: column;
  515. overflow: hidden;
  516. }
  517. .empty-detail {
  518. flex: 1;
  519. display: flex;
  520. align-items: center;
  521. justify-content: center;
  522. }
  523. .detail-header {
  524. padding: 16px 20px;
  525. border-bottom: 1px solid #e9ecef;
  526. }
  527. .detail-title { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
  528. .detail-title .point-name { font-size: 18px; font-weight: 600; }
  529. .detail-meta { display: flex; flex-wrap: wrap; gap: 0 24px; font-size: 13px; color: #909399; }
  530. /* 读数卡片 */
  531. .gauge-cards {
  532. display: grid;
  533. grid-template-columns: repeat(4, 1fr);
  534. gap: 16px;
  535. padding: 16px 20px;
  536. }
  537. .gauge-card {
  538. background: #f8f9fc;
  539. border-radius: 12px;
  540. padding: 16px;
  541. text-align: center;
  542. }
  543. .gauge-card.danger { background: #fef0f0; }
  544. .gauge-card.warning { background: #fdf6ec; }
  545. .gauge-label { font-size: 12px; color: #909399; margin-bottom: 8px; }
  546. .gauge-value { font-size: 28px; font-weight: 700; color: #1f2d3d; }
  547. .gauge-value .unit { font-size: 13px; font-weight: 400; color: #909399; }
  548. .gauge-value.high { color: #1890ff; }
  549. .gauge-value.low { color: #52c41a; }
  550. .gauge-status { font-size: 20px; font-weight: 700; }
  551. .gauge-status.normal { color: #67c23a; }
  552. .gauge-status.warning { color: #e6a23c; }
  553. .gauge-status.danger { color: #f56c6c; }
  554. .gauge-status.offline { color: #909399; }
  555. .gauge-sub { font-size: 11px; color: #c0c4cc; margin-top: 6px; }
  556. /* 趋势图 */
  557. .chart-card {
  558. margin: 0 20px 16px;
  559. padding: 16px;
  560. background: #fafbfc;
  561. border-radius: 12px;
  562. }
  563. .chart-header {
  564. display: flex;
  565. justify-content: space-between;
  566. align-items: center;
  567. margin-bottom: 12px;
  568. font-weight: 600;
  569. }
  570. .chart-legend { display: flex; gap: 16px; font-size: 12px; font-weight: 400; }
  571. .leg-item { display: flex; align-items: center; gap: 4px; }
  572. .leg-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
  573. .leg-dot.flow { background: #52c41a; }
  574. .leg-line { width: 20px; height: 2px; display: inline-block; }
  575. .leg-line.max { background: repeating-linear-gradient(90deg, #f56c6c 0, #f56c6c 4px, transparent 4px, transparent 7px); }
  576. .chart-container { width: 100%; height: 300px; }
  577. /* 历史表格 */
  578. .table-card {
  579. margin: 0 20px 16px;
  580. padding: 16px;
  581. background: #fafbfc;
  582. border-radius: 12px;
  583. flex: 1;
  584. overflow: auto;
  585. }
  586. .table-header {
  587. display: flex;
  588. justify-content: space-between;
  589. align-items: center;
  590. margin-bottom: 12px;
  591. font-weight: 600;
  592. }
  593. /* 历史记录分页:沿用若依分页组件,去掉其自带的底色与上下留白 */
  594. .history-pagination :deep(.pagination-container) {
  595. padding: 12px 0 0;
  596. background: transparent;
  597. }
  598. </style>