index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <script lang="ts" setup>
  2. import { IotDeviceApi } from '@/api/pms/device'
  3. import { useUserStore } from '@/store/modules/user'
  4. import download from '@/utils/download'
  5. import { rangeShortcuts } from '@/utils/formatTime'
  6. import { formatIotValue } from '@/utils/useSocketBus'
  7. import { useDebounceFn } from '@vueuse/core'
  8. import { dayjs, ElMessage } from 'element-plus'
  9. import { computed, ref } from 'vue'
  10. defineOptions({ name: 'MonitoringQuery' })
  11. const id = useUserStore().getUser.deptId ?? 157
  12. const deptId = id
  13. interface Query {
  14. deptId?: number
  15. deviceCode?: string
  16. time?: string[]
  17. identifier?: string
  18. }
  19. const query = ref<Query>({
  20. deviceCode: '',
  21. time: [...rangeShortcuts[0].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
  22. })
  23. const pageSize = ref(100)
  24. const historyStack = ref<string[]>([])
  25. const hasSearched = ref(false)
  26. // 数据列表类型
  27. interface ListItem {
  28. deviceName: string
  29. serialNumber: string
  30. identity: string
  31. value: string
  32. ts: string
  33. }
  34. const list = ref<ListItem[]>([])
  35. const loading = ref(false)
  36. const isConditionValid = computed(() => {
  37. const hasTime = query.value.time && query.value.time.length === 2
  38. const hasIdentity = !!query.value.deviceCode
  39. return hasTime && hasIdentity
  40. })
  41. const canGoBack = computed(() => historyStack.value.length > 0)
  42. const canGoNext = computed(() => list.value.length >= pageSize.value)
  43. const deviceOptions = ref<{ label: string; value: string; raw: any }[]>([])
  44. const optionsLoading = ref(false)
  45. const loadOptions = useDebounceFn(async function () {
  46. handleReset()
  47. try {
  48. optionsLoading.value = true
  49. const data = await IotDeviceApi.getDevice({
  50. deptId: query.value.deptId
  51. })
  52. deviceOptions.value = data.map((item: any) => {
  53. return {
  54. label: item.deviceCode + '-' + item.deviceName,
  55. raw: item,
  56. value: item.deviceCode
  57. }
  58. })
  59. } catch (error) {
  60. console.error(error)
  61. } finally {
  62. optionsLoading.value = false
  63. }
  64. }, 300)
  65. const attributeLoading = ref(false)
  66. const attributeOptions = ref<{ label: string; value: string; raw: any }[]>([])
  67. const handleNodeClick = (data: any) => {
  68. query.value.deptId = data.id
  69. loadOptions()
  70. }
  71. onMounted(() => {
  72. loadOptions()
  73. })
  74. const loadAttrOptions = useDebounceFn(async function (id: any) {
  75. attributeOptions.value = []
  76. query.value.identifier = undefined
  77. try {
  78. attributeLoading.value = true
  79. const data = await IotDeviceApi.getIotDeviceTds(Number(id))
  80. attributeOptions.value = data.map((item: any) => {
  81. return {
  82. label: item.modelName,
  83. raw: item,
  84. value: item.identifier
  85. }
  86. })
  87. } catch (error) {
  88. console.error(error)
  89. } finally {
  90. attributeLoading.value = false
  91. }
  92. }, 300)
  93. function handleDeviceChange(value: string) {
  94. const option = deviceOptions.value.find((i) => i.value === value)
  95. if (option) {
  96. loadAttrOptions(option.raw.id)
  97. }
  98. }
  99. const loadList = useDebounceFn(async function () {
  100. if (!isConditionValid.value) {
  101. // list.value = []
  102. return
  103. }
  104. loading.value = true
  105. try {
  106. const { time, ...other } = query.value
  107. const params = {
  108. ...other,
  109. // 直接使用 query 中的时间
  110. beginTime: time?.[0],
  111. endTime: time?.[1],
  112. pageSize: pageSize.value
  113. }
  114. const data = await IotDeviceApi.getMonitoringQuery(params)
  115. list.value = data.data || []
  116. } catch (error) {
  117. console.error(error)
  118. list.value = []
  119. } finally {
  120. loading.value = false
  121. }
  122. })
  123. function handleQuery() {
  124. if (!query.value.deviceCode) {
  125. ElMessage.warning('请选择设备')
  126. return
  127. }
  128. if (!query.value.time || query.value.time.length !== 2) {
  129. ElMessage.warning('请选择时间范围')
  130. return
  131. }
  132. hasSearched.value = true
  133. historyStack.value = []
  134. loadList()
  135. }
  136. function handleReset() {
  137. query.value = {
  138. deviceCode: '',
  139. time: [...rangeShortcuts[0].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
  140. }
  141. list.value = []
  142. historyStack.value = []
  143. hasSearched.value = false
  144. }
  145. function handleNext() {
  146. if (list.value.length === 0) return
  147. if (!query.value.time || query.value.time.length < 2) return
  148. const lastItem = list.value[list.value.length - 1]
  149. if (lastItem && lastItem.ts) {
  150. historyStack.value.push(query.value.time[1])
  151. query.value.time = [query.value.time[0], dayjs(lastItem.ts).format('YYYY-MM-DD HH:mm:ss')]
  152. loadList()
  153. } else {
  154. ElMessage.warning('无法获取最后一条数据的时间,无法跳转')
  155. }
  156. }
  157. function handlePrev() {
  158. if (historyStack.value.length === 0) return
  159. if (!query.value.time || query.value.time.length < 2) return
  160. const prevEndTime = historyStack.value.pop()
  161. if (prevEndTime) {
  162. query.value.time = [query.value.time[0], dayjs(prevEndTime).format('YYYY-MM-DD HH:mm:ss')]
  163. loadList()
  164. }
  165. }
  166. function handleSizeChange() {
  167. if (isConditionValid.value) {
  168. historyStack.value = []
  169. loadList()
  170. }
  171. }
  172. const exportLoading = ref(false)
  173. const handleExport = useDebounceFn(async function () {
  174. if (!query.value.deviceCode) {
  175. ElMessage.warning('请选择设备')
  176. return
  177. }
  178. if (!query.value.time || query.value.time.length !== 2) {
  179. ElMessage.warning('请选择时间范围')
  180. return
  181. }
  182. exportLoading.value = true
  183. try {
  184. const { time, ...other } = query.value
  185. const params = {
  186. ...other,
  187. beginTime: time?.[0],
  188. endTime: time?.[1]
  189. }
  190. const data = await IotDeviceApi.exportMonitoringQuery(params)
  191. download.excel(data, '监控查询.xls')
  192. } finally {
  193. exportLoading.value = false
  194. }
  195. }, 300)
  196. function formatterValue(row: ListItem) {
  197. const { value, suffix, isText } = formatIotValue(row.value)
  198. if (isText) {
  199. return value
  200. }
  201. return `${Number(value).toFixed(2)}${suffix}`
  202. }
  203. </script>
  204. <template>
  205. <div
  206. class="grid grid-cols-[15%_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
  207. >
  208. <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
  209. <DeptTreeSelect
  210. :top-id="156"
  211. :deptId="deptId"
  212. :init-select="false"
  213. :show-title="false"
  214. @node-click="handleNodeClick"
  215. />
  216. </div>
  217. <el-form
  218. size="default"
  219. class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
  220. >
  221. <div class="flex items-center gap-8">
  222. <el-form-item label="设备">
  223. <el-select
  224. :loading="optionsLoading"
  225. v-model="query.deviceCode"
  226. :options="deviceOptions"
  227. placeholder="请选择设备"
  228. class="w-60!"
  229. @change="handleDeviceChange"
  230. />
  231. </el-form-item>
  232. <el-form-item label="时间">
  233. <el-date-picker
  234. v-model="query.time"
  235. value-format="YYYY-MM-DD HH:mm:ss"
  236. type="datetimerange"
  237. start-placeholder="开始日期"
  238. end-placeholder="结束日期"
  239. :shortcuts="rangeShortcuts"
  240. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  241. class="!w-360px"
  242. />
  243. </el-form-item>
  244. <el-form-item label="属性">
  245. <el-select
  246. :loading="attributeLoading"
  247. v-model="query.identifier"
  248. :options="attributeOptions"
  249. placeholder="请选择属性"
  250. :no-data-text="!query.deviceCode ? '请先选择设备' : '暂无数据'"
  251. class="w-60!"
  252. />
  253. </el-form-item>
  254. <el-form-item>
  255. <el-button type="primary" @click="handleQuery()">
  256. <Icon icon="ep:search" class="mr-5px" /> 搜索
  257. </el-button>
  258. <el-button @click="handleReset()">
  259. <Icon icon="ep:refresh" class="mr-1" /> 重置
  260. </el-button>
  261. <el-button :loading="exportLoading" plain type="success" @click="handleExport">
  262. <Icon class="mr-5px" icon="ep:download" />
  263. 导出
  264. </el-button>
  265. </el-form-item>
  266. </div>
  267. </el-form>
  268. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4">
  269. <div class="flex-1 relative">
  270. <el-auto-resizer class="absolute">
  271. <template #default="{ width, height }">
  272. <zm-table
  273. :data="list"
  274. :loading="loading"
  275. :width="width"
  276. :height="height"
  277. :max-height="height"
  278. >
  279. <template #empty>
  280. <el-empty
  281. v-if="!hasSearched"
  282. description="请至少输入【设备名称】或【设备编码】,并选择【时间范围】进行搜索"
  283. />
  284. <el-empty v-else description="暂无数据" />
  285. </template>
  286. <zm-table-column type="index" label="序号" :width="60" align="center" />
  287. <zm-table-column
  288. prop="deviceName"
  289. title="设备名称"
  290. label="设备名称"
  291. align="center"
  292. min-width="120"
  293. />
  294. <zm-table-column
  295. prop="serialNumber"
  296. title="设备编码"
  297. label="设备编码"
  298. align="center"
  299. min-width="120"
  300. />
  301. <zm-table-column
  302. prop="identity"
  303. title="属性"
  304. label="属性"
  305. align="center"
  306. min-width="100"
  307. />
  308. <zm-table-column
  309. prop="value"
  310. title="值"
  311. label="值"
  312. align="center"
  313. min-width="100"
  314. coverFormatter
  315. :real-value="formatterValue"
  316. />
  317. <zm-table-column
  318. prop="ts"
  319. title="时间"
  320. :formatter="(row: ListItem) => dayjs(row.ts).format('YYYY-MM-DD HH:mm:ss')"
  321. label="时间"
  322. align="center"
  323. min-width="160"
  324. />
  325. </zm-table>
  326. </template>
  327. </el-auto-resizer>
  328. </div>
  329. <div
  330. class="h-[50px] flex items-center justify-end border-t border-gray-100 dark:border-gray-700 mt-2 gap-4"
  331. >
  332. <div class="text-sm text-gray-500">
  333. 每页显示:
  334. <el-select v-model="pageSize" class="!w-[80px]" size="default" @change="handleSizeChange">
  335. <el-option :value="10" label="10" />
  336. <el-option :value="20" label="20" />
  337. <el-option :value="50" label="50" />
  338. <el-option :value="100" label="100" />
  339. </el-select>
  340. </div>
  341. <div class="flex gap-2">
  342. <el-button size="default" :disabled="!canGoBack || loading" @click="handlePrev">
  343. <Icon icon="ep:arrow-left" /> 上一页
  344. </el-button>
  345. <el-button size="default" :disabled="!canGoNext || loading" @click="handleNext">
  346. 下一页 <Icon icon="ep:arrow-right" />
  347. </el-button>
  348. </div>
  349. </div>
  350. </div>
  351. </div>
  352. </template>
  353. <style scoped>
  354. :deep(.el-form-item) {
  355. margin-bottom: 0;
  356. }
  357. </style>