玛尔挡水温监测系统
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

temMonitor.vue 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <template>
  2. <div class="temperature-monitor">
  3. <!-- 搜索区域 -->
  4. <div class="search-section">
  5. <el-card class="search-card">
  6. <template #header>
  7. <div class="card-header">
  8. <span>温度监控查询</span>
  9. </div>
  10. </template>
  11. <el-form :model="searchForm" :inline="true" class="search-form">
  12. <!-- 时间范围选择 -->
  13. <el-form-item label="时间范围">
  14. <el-date-picker v-model="searchForm.timeRange" type="daterange" range-separator="至" start-placeholder="开始时间"
  15. end-placeholder="结束时间" format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="time-picker" />
  16. </el-form-item>
  17. <!-- 设备选择 -->
  18. <el-form-item label="查询条件">
  19. <el-select v-model="searchForm.Info" placeholder="请选择查询条件" clearable filterable class="device-select">
  20. <el-option v-for="device in deviceOptions" :key="device.value" :label="device.label"
  21. :value="device.value" />
  22. </el-select>
  23. </el-form-item>
  24. <!-- 操作按钮 -->
  25. <el-form-item>
  26. <el-button type="primary" @click="handleSearch" :loading="loading">
  27. <el-icon>
  28. <Search />
  29. </el-icon>
  30. 查询
  31. </el-button>
  32. <el-button @click="handleReset">
  33. <el-icon>
  34. <Refresh />
  35. </el-icon>
  36. 重置
  37. </el-button>
  38. <el-button type="success" @click="handleExport">
  39. <el-icon>
  40. <Download />
  41. </el-icon>
  42. 导出
  43. </el-button>
  44. </el-form-item>
  45. </el-form>
  46. </el-card>
  47. </div>
  48. <!-- 结果展示区域 -->
  49. <div class="result-section">
  50. <el-card>
  51. <template #header>
  52. <div class="card-header">
  53. <span>查询结果</span>
  54. <span class="result-count">共 {{ totalCount }} 条记录</span>
  55. </div>
  56. </template>
  57. <!-- 使用el-tab分别展示数据表格和分析图表 -->
  58. <div class="result-content">
  59. <el-empty v-if="!hasData" description="暂无数据" />
  60. <div v-else>
  61. <el-tabs v-model="activeTab" type="border-card" class="result-tabs" @tab-change="handleTabChange">
  62. <!-- 数据表格标签页 -->
  63. <el-tab-pane label="数据表格" name="table">
  64. <div class="table-container">
  65. <el-table :data="tableData" border stripe style="width: 100%" :max-height="600" v-loading="loading">
  66. <!-- 序号列 -->
  67. <el-table-column type="index" label="序号" width="60" align="center" fixed="left" />
  68. <!-- 时间列 -->
  69. <el-table-column prop="datetime" label="时间" width="160" align="center" fixed="left" />
  70. <!-- 动态生成传感器列 -->
  71. <el-table-column v-for="sensor in sensorColumns" :key="sensor.sensorNo" :prop="sensor.sensorName"
  72. :label="sensor.sensorName" width="120" align="center" show-overflow-tooltip>
  73. <template #default="scope">
  74. <span>
  75. {{ scope.row[sensor.sensorName] ? Number(scope.row[sensor.sensorName]).toFixed(2) : '-' }}
  76. </span>
  77. </template>
  78. </el-table-column>
  79. </el-table>
  80. </div>
  81. <!-- 分页 -->
  82. <div class="pagination-container">
  83. <el-pagination background v-model:current-page="queryParams.pageNumber"
  84. v-model:page-size="queryParams.pageSize" :total="totalCount" layout="total, prev, pager, next"
  85. @current-change="handleCurrentChange" />
  86. </div>
  87. </el-tab-pane>
  88. <!-- 分析图表标签页 -->
  89. <el-tab-pane label="分析图表" name="chart">
  90. <div class="chart-container">
  91. <TemperatureChart ref="chartRef" :chart-data="chartData" :sensor-columns="chartSensorColumns" />
  92. </div>
  93. </el-tab-pane>
  94. </el-tabs>
  95. </div>
  96. </div>
  97. </el-card>
  98. </div>
  99. </div>
  100. </template>
  101. <script setup>
  102. import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from 'vue'
  103. import { ElMessage } from 'element-plus'
  104. import { Search, Refresh, Download } from '@element-plus/icons-vue'
  105. import { listTemperature, listByInfo, listAllByInfo } from '@/api/monitoring/temperature'
  106. import { recentOperLog } from '@/api/system/operlog'
  107. import { listChannel, channelInfoList } from '@/api/monitoring/channel'
  108. import TemperatureChart from './TemperatureChart.vue'
  109. import useUserStore from '@/store/modules/user'
  110. import { parseTime } from '@/utils/date'
  111. // 响应式数据
  112. const loading = ref(false)
  113. const totalCount = ref(0)
  114. const activeTab = ref('table')
  115. const chartRef = ref(null)
  116. const { proxy } = getCurrentInstance()
  117. const userStore = useUserStore()
  118. // 搜索表单数据
  119. const today = ref(parseTime(new Date(), '{y}-{m}-{d}'))
  120. const yesterday = ref(parseTime(new Date(new Date().setDate(new Date().getDate() - 1)), '{y}-{m}-{d}'))
  121. const searchForm = reactive({
  122. timeRange: [yesterday.value, today.value],
  123. Info: '',
  124. queryType: '',
  125. deviceId: '',
  126. minTemp: null,
  127. maxTemp: null
  128. })
  129. const queryParams = ref({
  130. pageNumber: 1,
  131. pageSize: 48
  132. })
  133. // 设备选项
  134. const deviceOptions = ref([])
  135. // 计算属性
  136. const hasData = computed(() => {
  137. return totalCount.value > 0
  138. })
  139. // 表格数据相关
  140. const tableData = ref([])
  141. const sensorColumns = ref([])
  142. // 图表数据相关
  143. const chartData = ref([])
  144. const chartSensorColumns = ref([])
  145. const allDataList = ref([])
  146. const getDeviceList = async () => {
  147. try {
  148. const deviceRes = await channelInfoList()
  149. if (deviceRes && Array.isArray(deviceRes)) {
  150. deviceOptions.value = deviceRes.map(item => ({
  151. label: item,
  152. value: item
  153. }))
  154. }
  155. }
  156. catch (error) {
  157. ElMessage.warning('监测数据已更新,请刷新当前页面')
  158. }
  159. }
  160. // 在组件挂载时调用
  161. onMounted(() => {
  162. getDeviceList()
  163. recentOperLog({ operName: userStore.name }).then(res => {
  164. if (res.operParam) {
  165. searchForm.Info = res.operParam.split('Info\', values=[')[1].split(']')[0]
  166. }
  167. })
  168. })
  169. // 处理图表数据,使用全部数据
  170. const processChartData = (dataList) => {
  171. if (!dataList || dataList.length === 0) {
  172. chartData.value = []
  173. chartSensorColumns.value = []
  174. return
  175. }
  176. // 获取所有唯一的传感器编号和对应的设备信息
  177. const sensorMap = new Map()
  178. dataList.forEach(item => {
  179. if (!sensorMap.has(item.sensorNo)) {
  180. sensorMap.set(item.sensorNo, {
  181. sensorNo: item.sensorNo,
  182. sensorName: item.channel?.info || `传感器${item.sensorNo}`
  183. })
  184. }
  185. })
  186. // 生成传感器列配置
  187. chartSensorColumns.value = Array.from(sensorMap.values()).sort((a, b) => a.sensorNo - b.sensorNo)
  188. // 按时间分组数据
  189. const timeGroups = {}
  190. dataList.forEach(item => {
  191. if (!timeGroups[item.datetime]) {
  192. timeGroups[item.datetime] = {
  193. datetime: item.datetime,
  194. ...chartSensorColumns.value.reduce((acc, sensor) => {
  195. acc[sensor.sensorName] = null
  196. return acc
  197. }, {})
  198. }
  199. }
  200. const sensorName = item.channel?.info || `传感器${item.sensorNo}`
  201. timeGroups[item.datetime][sensorName] = item.data
  202. })
  203. // 转换为数组并排序
  204. chartData.value = Object.values(timeGroups)
  205. }
  206. // 处理表格数据,使用全部数据并进行前端分页
  207. const processTableData = () => {
  208. if (!allDataList.value || allDataList.value.length === 0) {
  209. tableData.value = []
  210. sensorColumns.value = []
  211. return
  212. }
  213. // 获取所有唯一的传感器编号和对应的设备信息
  214. const sensorMap = new Map()
  215. allDataList.value.forEach(item => {
  216. if (!sensorMap.has(item.sensorNo)) {
  217. sensorMap.set(item.sensorNo, {
  218. sensorNo: item.sensorNo,
  219. sensorName: item.channel?.info || `传感器${item.sensorNo}`
  220. })
  221. }
  222. })
  223. // 生成传感器列配置
  224. sensorColumns.value = Array.from(sensorMap.values()).sort((a, b) => a.sensorNo - b.sensorNo)
  225. // 按时间分组数据
  226. const timeGroups = {}
  227. allDataList.value.forEach(item => {
  228. if (!timeGroups[item.datetime]) {
  229. timeGroups[item.datetime] = {
  230. datetime: item.datetime,
  231. ...sensorColumns.value.reduce((acc, sensor) => {
  232. acc[sensor.sensorName] = null
  233. return acc
  234. }, {})
  235. }
  236. }
  237. const sensorName = item.channel?.info || `传感器${item.sensorNo}`
  238. timeGroups[item.datetime][sensorName] = item.data
  239. })
  240. // 转换为数组并排序
  241. const allTableData = Object.values(timeGroups)
  242. // 前端分页处理
  243. const startIndex = (queryParams.value.pageNumber - 1) * queryParams.value.pageSize
  244. const endIndex = startIndex + queryParams.value.pageSize
  245. tableData.value = allTableData.slice(startIndex, endIndex)
  246. }
  247. const handleCurrentChange = (page) => {
  248. queryParams.value.pageNumber = page
  249. // 前端分页,直接重新处理表格数据
  250. processTableData()
  251. }
  252. // 查询监控数据
  253. const handleSearch = async () => {
  254. // 验证表单
  255. if (!searchForm.timeRange || searchForm.timeRange.length === 0) {
  256. ElMessage.warning('请选择时间范围')
  257. return
  258. }
  259. if (!searchForm.Info || searchForm.Info.trim() === '') {
  260. ElMessage.warning('请选择查询条件')
  261. return
  262. }
  263. // 检查时间范围是否超过31天
  264. const startDate = new Date(searchForm.timeRange[0])
  265. const endDate = new Date(searchForm.timeRange[1])
  266. const daysDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1
  267. if (daysDiff > 31) {
  268. // 使用 Element Plus 的确认对话框
  269. const { ElMessageBox } = await import('element-plus')
  270. try {
  271. await ElMessageBox.confirm(
  272. '查询超过1个月,数据量过大,可能会导致卡顿或崩溃,是否继续?',
  273. '查询确认',
  274. {
  275. confirmButtonText: '继续查询',
  276. cancelButtonText: '取消',
  277. type: 'warning',
  278. }
  279. )
  280. } catch (error) {
  281. // 用户点击取消
  282. return
  283. }
  284. }
  285. loading.value = true
  286. queryParams.value.pageNumber = 1
  287. try {
  288. let startTime = searchForm.timeRange[0] + ' 00:00:00'
  289. let endTime = searchForm.timeRange[1] + ' 23:59:59'
  290. // 使用 listAllByInfo 获取全部数据
  291. const allDataRes = await listAllByInfo({
  292. startTime: startTime,
  293. endTime: endTime,
  294. Info: searchForm.Info
  295. })
  296. if (allDataRes) {
  297. // 为图表和表格准备全部数据
  298. const allData = allDataRes.list || allDataRes
  299. totalCount.value = Number(allDataRes.total)
  300. // 存储全部数据
  301. allDataList.value = allData
  302. // 处理全部数据,生成图表数据
  303. processChartData(allData)
  304. // 处理全部数据,生成表格数据(前端分页)
  305. processTableData()
  306. ElMessage.success('查询成功')
  307. }
  308. } catch (error) {
  309. ElMessage.warning('监测数据已更新,请刷新当前页面')
  310. } finally {
  311. loading.value = false
  312. }
  313. }
  314. const handleReset = () => {
  315. searchForm.timeRange = []
  316. searchForm.Info = ''
  317. totalCount.value = 0
  318. tableData.value = []
  319. sensorColumns.value = []
  320. chartData.value = []
  321. chartSensorColumns.value = []
  322. allDataList.value = []
  323. // 重置分页参数
  324. queryParams.value.pageNumber = 1
  325. ElMessage.info('已重置搜索条件')
  326. }
  327. const handleExport = () => {
  328. if (totalCount.value === 0) {
  329. ElMessage.warning('暂无数据可导出')
  330. return
  331. }
  332. // ElMessage.success('导出功能开发中...')
  333. // 这里可以添加导出逻辑
  334. proxy.$download('monitoring/temperature/export', {
  335. startTime: searchForm.timeRange[0] + ' 00:00:00',
  336. endTime: searchForm.timeRange[1] + ' 23:59:59',
  337. Info: searchForm.Info
  338. }, `水温监测_${searchForm.timeRange[0] + "-" + searchForm.timeRange[1]}.xls`)
  339. }
  340. // 监听标签页切换
  341. const handleTabChange = (tabName) => {
  342. if (tabName === 'chart') {
  343. // 延迟触发图表重新渲染,确保DOM已更新
  344. nextTick(() => {
  345. setTimeout(() => {
  346. // 调用图表组件的resize方法
  347. if (chartRef.value) {
  348. chartRef.value.resizeChart()
  349. }
  350. }, 200)
  351. })
  352. }
  353. }
  354. </script>
  355. <style lang="scss" scoped>
  356. .temperature-monitor {
  357. padding: 20px;
  358. background-color: #f5f5f5;
  359. min-height: 100vh;
  360. .search-section {
  361. margin-bottom: 20px;
  362. .search-card {
  363. .card-header {
  364. font-weight: bold;
  365. color: #303133;
  366. }
  367. .search-form {
  368. .time-picker {
  369. width: 400px;
  370. }
  371. .query-select,
  372. .device-select {
  373. width: 200px;
  374. }
  375. .temp-input {
  376. width: 120px;
  377. }
  378. .temp-separator {
  379. margin: 0 10px;
  380. color: #909399;
  381. }
  382. }
  383. }
  384. }
  385. .result-section {
  386. .card-header {
  387. display: flex;
  388. justify-content: space-between;
  389. align-items: center;
  390. font-weight: bold;
  391. color: #303133;
  392. .result-count {
  393. font-size: 14px;
  394. color: #909399;
  395. font-weight: normal;
  396. }
  397. }
  398. .result-content {
  399. min-height: 200px;
  400. .pagination-container {
  401. margin-top: 20px;
  402. text-align: center;
  403. }
  404. // 标签页样式
  405. .result-tabs {
  406. .el-tabs__content {
  407. padding: 20px 0;
  408. }
  409. }
  410. // 表格容器样式
  411. .table-container {
  412. overflow-x: auto;
  413. width: 100%;
  414. // 自定义滚动条样式
  415. &::-webkit-scrollbar {
  416. height: 8px;
  417. }
  418. &::-webkit-scrollbar-track {
  419. background: #f1f1f1;
  420. border-radius: 4px;
  421. }
  422. &::-webkit-scrollbar-thumb {
  423. background: #c1c1c1;
  424. border-radius: 4px;
  425. &:hover {
  426. background: #a8a8a8;
  427. }
  428. }
  429. .el-table {
  430. .el-table__body-wrapper {
  431. overflow-x: auto;
  432. }
  433. }
  434. }
  435. // 图表容器样式
  436. .chart-container {
  437. width: 100%;
  438. height: 800px;
  439. min-height: 600px;
  440. display: flex;
  441. flex-direction: row;
  442. }
  443. }
  444. }
  445. }
  446. // 响应式设计
  447. @media (max-width: 768px) {
  448. .temperature-monitor {
  449. padding: 10px;
  450. .search-section {
  451. .search-card {
  452. .search-form {
  453. .time-picker {
  454. width: 100%;
  455. }
  456. .query-select,
  457. .device-select {
  458. width: 100%;
  459. }
  460. }
  461. }
  462. }
  463. }
  464. }
  465. </style>