index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. <script setup lang="ts">
  2. import { IotDeviceApi } from '@/api/pms/device'
  3. import { useTableComponents } from '@/components/ZmTable/useTableComponents'
  4. import { useUserStore } from '@/store/modules/user'
  5. import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
  6. import { useDebounceFn } from '@vueuse/core'
  7. defineOptions({ name: 'MonitoringList' })
  8. const { t } = useI18n()
  9. const id = useUserStore().getUser.deptId ?? 157
  10. const deptId = id
  11. interface Query {
  12. deptId?: number
  13. deviceName?: string
  14. deviceCode?: string
  15. ifInline?: string
  16. pageNo: number
  17. pageSize: number
  18. }
  19. const viewMode = ref('card')
  20. const query = ref<Query>({
  21. pageNo: 1,
  22. pageSize: 12,
  23. deptId: id,
  24. ifInline: '3'
  25. })
  26. interface OliDevice {
  27. id: number
  28. carId?: number
  29. deviceName: string
  30. deviceCode: string
  31. assetClassName: string
  32. deviceStatus: string
  33. ifInline: number
  34. lastInlineTime: string
  35. carOnline: string
  36. deptName: string
  37. vehicleName: string
  38. }
  39. const list = ref<OliDevice[]>([])
  40. const total = ref(0)
  41. const loading = ref(false)
  42. const loadList = useDebounceFn(async function () {
  43. loading.value = true
  44. try {
  45. const data = await IotDeviceApi.getIotDeviceOliConnectPage(query.value)
  46. // const data = await IotDeviceApi.getIotDeviceTdPage(query.value)
  47. list.value = data.list
  48. total.value = data.total
  49. } finally {
  50. loading.value = false
  51. }
  52. })
  53. function handleSizeChange(val: number) {
  54. query.value.pageSize = val
  55. handleQuery()
  56. }
  57. function handleCurrentChange(val: number) {
  58. query.value.pageNo = val
  59. loadList()
  60. }
  61. function handleQuery(setPage = true) {
  62. if (setPage) {
  63. query.value.pageNo = 1
  64. }
  65. loadList()
  66. }
  67. function resetQuery() {
  68. query.value = {
  69. pageNo: 1,
  70. pageSize: 12,
  71. deptId: id
  72. }
  73. handleQuery()
  74. }
  75. watch(
  76. [
  77. () => query.value.deptId,
  78. () => query.value.deviceName,
  79. () => query.value.deviceCode,
  80. () => query.value.ifInline
  81. ],
  82. () => {
  83. handleQuery()
  84. },
  85. { immediate: true }
  86. )
  87. const { ZmTable, ZmTableColumn } = useTableComponents<OliDevice>()
  88. const getStatusConfig = (status: number) => {
  89. switch (status) {
  90. case 3: // 在线
  91. return {
  92. bg: 'bg-gradient-to-br from-emerald-400 to-cyan-500',
  93. icon: 'ep:success-filled',
  94. label: '在线',
  95. textColor: 'text-emerald-600'
  96. }
  97. case 4: // 离线
  98. return {
  99. bg: 'bg-gradient-to-br from-slate-400 to-slate-500',
  100. icon: 'ep:circle-close-filled',
  101. label: '离线',
  102. textColor: 'text-slate-500'
  103. }
  104. case 5: // 未激活
  105. return {
  106. bg: 'bg-gradient-to-br from-blue-400 to-indigo-500',
  107. icon: 'ep:info-filled',
  108. label: '未激活',
  109. textColor: 'text-blue-500'
  110. }
  111. case 2: // 禁用
  112. return {
  113. bg: 'bg-gradient-to-br from-orange-400 to-red-500',
  114. icon: 'ep:warn-triangle-filled',
  115. label: '禁用',
  116. textColor: 'text-red-500'
  117. }
  118. default:
  119. return {
  120. bg: 'bg-gradient-to-br from-gray-300 to-gray-400',
  121. icon: 'ep:question-filled',
  122. label: '未知',
  123. textColor: 'text-gray-500'
  124. }
  125. }
  126. }
  127. const message = useMessage()
  128. const router = useRouter()
  129. const openDetail = (
  130. id: number,
  131. ifInline: number,
  132. time: string,
  133. name: string,
  134. code: string,
  135. dept: string,
  136. vehicle: string,
  137. carOnline: string
  138. ) => {
  139. if (time === null || time === undefined) {
  140. message.warning('没有数采数据')
  141. return
  142. }
  143. router.push({
  144. name: 'MonitoringDetail',
  145. query: { id, ifInline, carOnline, time, name, code, dept, vehicle }
  146. })
  147. }
  148. </script>
  149. <template>
  150. <div
  151. 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))]"
  152. >
  153. <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
  154. <DeptTreeSelect
  155. :top-id="156"
  156. :deptId="deptId"
  157. v-model="query.deptId"
  158. :init-select="false"
  159. :show-title="false"
  160. />
  161. </div>
  162. <el-form
  163. size="default"
  164. class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
  165. >
  166. <div class="flex items-center gap-8">
  167. <el-form-item :label="t('monitor.deviceName')">
  168. <el-input
  169. v-model="query.deviceName"
  170. :placeholder="t('monitor.nameHolder')"
  171. clearable
  172. @keyup.enter="handleQuery()"
  173. class="!w-240px"
  174. />
  175. </el-form-item>
  176. <el-form-item :label="t('monitor.deviceCode')">
  177. <el-input
  178. v-model="query.deviceCode"
  179. :placeholder="t('monitor.codeHolder')"
  180. clearable
  181. @keyup.enter="handleQuery()"
  182. class="!w-240px"
  183. />
  184. </el-form-item>
  185. <el-form-item :label="t('monitor.ifInline')">
  186. <el-select
  187. v-model="query.ifInline"
  188. :placeholder="t('monitor.ifInlineHolder')"
  189. clearable
  190. class="!w-240px"
  191. >
  192. <el-option
  193. v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
  194. :key="dict.value"
  195. :label="dict.label"
  196. :value="dict.value"
  197. />
  198. </el-select>
  199. </el-form-item>
  200. <el-form-item>
  201. <el-button type="primary" @click="handleQuery()">
  202. <Icon icon="ep:search" class="mr-5px" /> 搜索
  203. </el-button>
  204. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
  205. </el-form-item>
  206. </div>
  207. <el-form-item>
  208. <el-button-group>
  209. <el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
  210. <Icon icon="ep:grid" />
  211. </el-button>
  212. <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
  213. <Icon icon="ep:list" />
  214. </el-button>
  215. </el-button-group>
  216. </el-form-item>
  217. </el-form>
  218. <div
  219. class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col"
  220. :class="{ 'p-4': viewMode === 'list' }"
  221. >
  222. <div class="flex-1 relative">
  223. <el-auto-resizer class="absolute">
  224. <template #default="{ width, height }">
  225. <template v-if="list.length > 0">
  226. <zm-table
  227. v-if="viewMode === 'list'"
  228. :data="list"
  229. :loading="loading"
  230. :width="width"
  231. :max-height="height"
  232. :height="height"
  233. >
  234. <zm-table-column type="index" :label="t('monitor.serial')" :width="60" />
  235. <zm-table-column prop="deviceName" :label="t('monitor.deviceName')" />
  236. <zm-table-column prop="deviceCode" :label="t('monitor.deviceCode')" />
  237. <zm-table-column prop="assetClassName" :label="t('monitor.category')" />
  238. <zm-table-column prop="deviceStatus" :label="t('monitor.status')">
  239. <template #default="scope">
  240. <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
  241. </template>
  242. </zm-table-column>
  243. <zm-table-column prop="ifInline" :label="t('monitor.ifInline')">
  244. <template #default="scope">
  245. <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.ifInline" />
  246. </template>
  247. </zm-table-column>
  248. <zm-table-column prop="lastInlineTime" :label="t('monitor.latestDataTime')" />
  249. <zm-table-column :label="t('monitor.operation')" :width="60">
  250. <template #default="scope">
  251. <el-button
  252. link
  253. type="primary"
  254. @click="
  255. openDetail(
  256. scope.row.id,
  257. scope.row.ifInline,
  258. scope.row.lastInlineTime,
  259. scope.row.deviceName,
  260. scope.row.deviceCode,
  261. scope.row.deptName,
  262. scope.row.vehicleName,
  263. scope.row.carOnline ?? ''
  264. )
  265. "
  266. >
  267. {{ t('monitor.check') }}
  268. </el-button>
  269. </template>
  270. </zm-table-column>
  271. </zm-table>
  272. <el-scrollbar
  273. v-else
  274. :height="height"
  275. :class="width"
  276. view-class="grid grid-cols-4 grid-rows-3 gap-4 p-4"
  277. >
  278. <div
  279. v-for="item in list"
  280. :key="item.id"
  281. class="group relative flex flex-col bg-white dark:bg-[#262727] rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 hover:shadow-[0_8px_20px_rgba(0,0,0,0.1)] hover:-translate-y-1 transition-all duration-300 overflow-hidden"
  282. >
  283. <div
  284. class="h-[80px] px-4 flex items-center justify-between overflow-hidden"
  285. :class="getStatusConfig(item.ifInline).bg"
  286. >
  287. <div class="flex items-center gap-3 z-10 max-w-[80%]">
  288. <div
  289. class="bg-white/20 p-2 rounded-lg backdrop-blur-md shadow-inner shrink-0"
  290. >
  291. <Icon :icon="item.carId ? 'ep:van' : 'ep:cpu'" class="text-xl text-white" />
  292. </div>
  293. <!-- 文本区域 -->
  294. <div class="flex flex-col overflow-hidden">
  295. <el-tooltip effect="dark" :content="item.deviceName" placement="top-start">
  296. <span
  297. class="text-white font-bold text-base leading-tight"
  298. :title="item.deviceName"
  299. >
  300. {{ item.deviceName }}
  301. </span>
  302. </el-tooltip>
  303. <span class="text-white/80 text-xs font-mono truncate mt-0.5">
  304. {{ item.deviceCode }}
  305. </span>
  306. </div>
  307. </div>
  308. <div class="z-10 shrink-0">
  309. <div
  310. class="flex items-center gap-1.5 bg-black/20 backdrop-blur-md px-2.5 py-1 rounded-full text-xs font-medium text-white shadow-sm border border-white/10"
  311. >
  312. <div
  313. class="w-1.5 h-1.5 rounded-full bg-white animate-pulse"
  314. v-if="item.ifInline === 3"
  315. >
  316. </div>
  317. <Icon :icon="getStatusConfig(item.ifInline).icon" v-else />
  318. <span>{{ getStatusConfig(item.ifInline).label }}</span>
  319. </div>
  320. </div>
  321. </div>
  322. <!-- 内容区域 -->
  323. <div
  324. class="flex-1 p-4 flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300 relative z-20 bg-white dark:bg-[#262727]"
  325. >
  326. <!-- 编码行 -->
  327. <div class="flex items-center justify-between pb-2">
  328. <span class="text-gray-400 text-xs flex items-center gap-1.5">
  329. <Icon icon="ep:postcard" /> 设备编码
  330. </span>
  331. <span
  332. class="font-mono font-medium truncate max-w-[140px] select-all"
  333. :title="item.deviceCode"
  334. >
  335. {{ item.deviceCode }}
  336. </span>
  337. </div>
  338. <!-- 类别行 -->
  339. <div class="flex items-center justify-between pb-2">
  340. <span class="text-gray-400 text-xs flex items-center gap-1.5">
  341. <Icon icon="ep:price-tag" /> 设备类别
  342. </span>
  343. <el-tag
  344. size="small"
  345. type="info"
  346. effect="light"
  347. round
  348. class="!bg-gray-100 dark:!bg-gray-800 !border-gray-200 dark:!border-gray-600"
  349. >
  350. {{ item.assetClassName || '-' }}
  351. </el-tag>
  352. </div>
  353. <!-- 时间行 -->
  354. <div class="flex items-center justify-between">
  355. <span class="text-gray-400 text-xs flex items-center gap-1.5">
  356. <Icon icon="ep:clock" /> 最后上线
  357. </span>
  358. <span
  359. class="text-xs font-medium"
  360. :class="
  361. item.lastInlineTime ? 'text-gray-600 dark:text-gray-300' : 'text-gray-300'
  362. "
  363. >
  364. {{ item.lastInlineTime || '暂无记录' }}
  365. </span>
  366. </div>
  367. </div>
  368. <!-- 底部操作栏 -->
  369. <div
  370. class="px-4 py-2.5 bg-gray-50/80 dark:bg-[#1d1e1f] flex justify-between items-center group-hover:bg-blue-50/30 dark:group-hover:bg-blue-900/10 transition-colors"
  371. >
  372. <span class="text-[10px] text-gray-400"></span>
  373. <!-- <span class="text-[10px] text-gray-400">ID: {{ item.id }}</span> -->
  374. <el-button type="primary" link size="small" class="!px-0 group/btn">
  375. <span
  376. class="mr-1 group-hover/btn:underline"
  377. @click="
  378. openDetail(
  379. item.id,
  380. item.ifInline,
  381. item.lastInlineTime,
  382. item.deviceName,
  383. item.deviceCode,
  384. item.deptName,
  385. item.vehicleName,
  386. item.carOnline ?? ''
  387. )
  388. "
  389. >查看详情</span
  390. >
  391. <Icon
  392. icon="ep:arrow-right"
  393. class="group-hover/btn:translate-x-1 transition-transform"
  394. />
  395. </el-button>
  396. </div>
  397. </div>
  398. </el-scrollbar>
  399. </template>
  400. <div
  401. v-else
  402. :style="{ width: width + 'px', height: height + 'px' }"
  403. class="flex flex-col items-center justify-center text-gray-400"
  404. >
  405. <div class="i-lucide-inbox text-5xl mb-4 op-50"></div>
  406. <p class="text-sm font-medium">暂无相关数据</p>
  407. <p class="text-xs mt-1 op-60">尝试调整过滤条件或刷新页面</p>
  408. </div>
  409. </template>
  410. </el-auto-resizer>
  411. </div>
  412. <div
  413. class="h-10 mt-4 flex items-center justify-end"
  414. :class="{ 'mb-4 px-4': viewMode === 'card' }"
  415. >
  416. <el-pagination
  417. size="default"
  418. v-show="total > 0"
  419. v-model:current-page="query.pageNo"
  420. v-model:page-size="query.pageSize"
  421. :background="true"
  422. :page-sizes="[12, 20, 30, 50, 100]"
  423. :total="total"
  424. layout="total, sizes, prev, pager, next, jumper"
  425. @size-change="handleSizeChange"
  426. @current-change="handleCurrentChange"
  427. />
  428. </div>
  429. </div>
  430. </div>
  431. </template>
  432. <style scoped>
  433. :deep(.el-form-item) {
  434. margin-bottom: 0;
  435. }
  436. </style>