index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. <template>
  2. <el-row :gutter="20">
  3. <!-- 左侧部门树 -->
  4. <el-col :span="4" :xs="24">
  5. <ContentWrap class="h-1/1">
  6. <DeptTree @node-click="handleDeptNodeClick" />
  7. </ContentWrap>
  8. </el-col>
  9. <el-col :span="20" :xs="24">
  10. <ContentWrap v-loading="loading">
  11. <ContentWrap>
  12. <!-- 搜索工作栏 -->
  13. <el-form
  14. class="-mb-15px"
  15. :model="queryParams"
  16. ref="queryFormRef"
  17. :inline="true"
  18. label-width="68px"
  19. >
  20. <el-form-item
  21. :label="t('monitor.deviceName')"
  22. prop="deviceName"
  23. style="margin-left: 20px"
  24. >
  25. <el-input
  26. v-model="queryParams.deviceName"
  27. :placeholder="t('monitor.nameHolder')"
  28. clearable
  29. @keyup.enter="handleQuery"
  30. class="!w-240px"
  31. />
  32. </el-form-item>
  33. <el-form-item :label="t('monitor.deviceCode')" prop="deviceCode">
  34. <el-input
  35. v-model="queryParams.deviceCode"
  36. :placeholder="t('monitor.codeHolder')"
  37. clearable
  38. @keyup.enter="handleQuery"
  39. class="!w-240px"
  40. />
  41. </el-form-item>
  42. <el-form-item :label="t('monitor.ifInline')" prop="ifInline">
  43. <el-select
  44. v-model="queryParams.ifInline"
  45. :placeholder="t('monitor.ifInlineHolder')"
  46. clearable
  47. class="!w-240px"
  48. >
  49. <el-option
  50. v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
  51. :key="dict.value"
  52. :label="dict.label"
  53. :value="dict.value"
  54. />
  55. </el-select>
  56. </el-form-item>
  57. <el-form-item class="float-right !mr-0 !mb-0">
  58. <el-button-group>
  59. <el-button
  60. :type="viewMode === 'card' ? 'primary' : 'default'"
  61. @click="viewMode = 'card'"
  62. >
  63. <Icon icon="ep:grid" />
  64. </el-button>
  65. <el-button
  66. :type="viewMode === 'list' ? 'primary' : 'default'"
  67. @click="viewMode = 'list'"
  68. >
  69. <Icon icon="ep:list" />
  70. </el-button>
  71. </el-button-group>
  72. </el-form-item>
  73. <el-form-item>
  74. <el-button @click="handleQuery">
  75. <Icon icon="ep:search" class="mr-5px" />
  76. {{ t('monitor.search') }}
  77. </el-button>
  78. <el-button @click="resetQuery">
  79. <Icon icon="ep:refresh" class="mr-5px" />
  80. {{ t('monitor.reset') }}
  81. </el-button>
  82. <el-button
  83. type="success"
  84. plain
  85. @click="handleExport"
  86. :loading="exportLoading"
  87. v-hasPermi="['iot:device:export']"
  88. >
  89. <Icon icon="ep:download" class="mr-5px" /> 导出
  90. </el-button>
  91. </el-form-item>
  92. </el-form>
  93. </ContentWrap>
  94. <!-- 列表 -->
  95. <ContentWrap>
  96. <template v-if="viewMode === 'card'">
  97. <el-row :gutter="16">
  98. <el-col
  99. v-for="item in list"
  100. :key="item.id"
  101. :xs="24"
  102. :sm="12"
  103. :md="12"
  104. :lg="6"
  105. class="mb-4"
  106. >
  107. <el-card
  108. class="h-full transition-colors relative overflow-hidden"
  109. :body-style="{ padding: '0' }"
  110. >
  111. <!-- 添加渐变背景层 -->
  112. <div
  113. class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none"
  114. :class="[
  115. item.ifInline === 3
  116. ? 'bg-gradient-to-b from-[#eefaff] to-transparent'
  117. : 'bg-gradient-to-b from-[#fff1f1] to-transparent'
  118. ]"
  119. >
  120. </div>
  121. <div class="p-4 relative">
  122. <!-- 标题区域 -->
  123. <div class="flex items-center mb-3">
  124. <div class="mr-2.5 flex items-center">
  125. <img src="@/assets/svgs/iot/card-fill.svg" class="w-[18px] h-[18px]" />
  126. </div>
  127. <div class="text-[16px] font-600 flex-1">{{
  128. item.deviceCode + item.deviceName
  129. }}</div>
  130. <!-- 添加设备状态标签 -->
  131. <div class="inline-flex items-center">
  132. <div
  133. class="w-1 h-1 rounded-full mr-1.5"
  134. :class="
  135. item.ifInline === 3
  136. ? 'bg-[var(--el-color-success)]'
  137. : 'bg-[var(--el-color-danger)]'
  138. "
  139. >
  140. </div>
  141. <el-text
  142. class="!text-xs font-bold"
  143. :type="item.ifInline === 3 ? 'success' : 'danger'"
  144. >
  145. {{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATUS, item.ifInline) }}
  146. </el-text>
  147. </div>
  148. </div>
  149. <!-- 信息区域 -->
  150. <div class="flex items-center text-[14px]">
  151. <div class="flex-1">
  152. <div class="mb-2.5 last:mb-0">
  153. <span class="text-[#717c8e] mr-2.5">{{ t('monitor.deviceCode') }}</span>
  154. <span class="text-[#0070ff]">
  155. {{ item.deviceCode }}
  156. </span>
  157. </div>
  158. <div class="mb-2.5 last:mb-0">
  159. <span class="text-[#717c8e] mr-2.5">{{ t('monitor.category') }}</span>
  160. <span class="text-[#0070ff]">
  161. {{ item.assetClassName }}
  162. </span>
  163. </div>
  164. <div class="mb-2.5 last:mb-0">
  165. <span class="text-[#717c8e] mr-2.5">{{
  166. t('monitor.latestDataTime')
  167. }}</span>
  168. <span class="text-[#0070ff]">
  169. {{ item.lastInlineTime }}
  170. </span>
  171. </div>
  172. </div>
  173. <div class="w-[100px] h-[100px]">
  174. <img src="@/assets/imgs/iot/device.png" class="w-full h-full" />
  175. </div>
  176. </div>
  177. <!-- 分隔线 -->
  178. <el-divider class="!my-3" />
  179. <!-- 按钮 -->
  180. <div class="flex items-center px-0">
  181. <el-button
  182. class="flex-1 !px-2 !h-[32px] text-[13px]"
  183. type="warning"
  184. plain
  185. @click="
  186. openDetail(
  187. item.id,
  188. item.ifInline,
  189. item.lastInlineTime,
  190. item.deviceName,
  191. item.deviceCode,
  192. item.deptName
  193. )
  194. "
  195. >
  196. <Icon icon="ep:view" class="mr-1" />
  197. {{ t('monitor.details') }}
  198. </el-button>
  199. <!-- <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>-->
  200. </div>
  201. </div>
  202. </el-card>
  203. </el-col>
  204. </el-row>
  205. </template>
  206. <!-- 列表视图 -->
  207. <el-table
  208. v-else
  209. v-loading="loading"
  210. :data="list"
  211. :stripe="true"
  212. :show-overflow-tooltip="true"
  213. >
  214. <el-table-column :label="t('monitor.serial')" width="60" align="center">
  215. <template #default="scope">
  216. {{ scope.$index + 1 }}
  217. </template>
  218. </el-table-column>
  219. <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName">
  220. <template #default="scope">
  221. <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
  222. </template>
  223. </el-table-column>
  224. <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
  225. <el-table-column :label="t('monitor.category')" align="center" prop="assetClassName" />
  226. <el-table-column :label="t('monitor.status')" align="center" prop="deviceStatus">
  227. <template #default="scope">
  228. <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
  229. </template>
  230. </el-table-column>
  231. <el-table-column :label="t('monitor.online')" align="center" prop="ifInline">
  232. <template #default="scope">
  233. <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.ifInline" />
  234. </template>
  235. </el-table-column>
  236. <el-table-column
  237. :label="t('monitor.latestDataTime')"
  238. align="center"
  239. prop="lastInlineTime"
  240. :formatter="dateFormatter"
  241. width="180px"
  242. />
  243. <el-table-column :label="t('monitor.operation')" align="center" min-width="120px">
  244. <template #default="scope">
  245. <el-button
  246. link
  247. type="primary"
  248. @click="
  249. openDetail(
  250. scope.row.id,
  251. scope.row.ifInline,
  252. scope.row.lastInlineTime,
  253. scope.row.deviceName,
  254. scope.row.deviceCode,
  255. scope.row.deptName,
  256. )
  257. "
  258. >
  259. {{ t('monitor.check') }}
  260. </el-button>
  261. </template>
  262. </el-table-column>
  263. </el-table>
  264. <!-- 分页 -->
  265. <Pagination
  266. :total="total"
  267. v-model:page="queryParams.pageNo"
  268. v-model:limit="queryParams.pageSize"
  269. @pagination="getList"
  270. />
  271. </ContentWrap>
  272. </ContentWrap>
  273. </el-col>
  274. </el-row>
  275. </template>
  276. <script setup lang="ts">
  277. import { DICT_TYPE, getDictLabel, getStrDictOptions } from '@/utils/dict'
  278. import { dateFormatter } from '@/utils/formatTime'
  279. import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
  280. import download from '@/utils/download'
  281. import DeptTree from '@/views/system/user/DeptTree.vue'
  282. import { IotDeviceApi } from '@/api/pms/device'
  283. /** IoT 设备列表 */
  284. defineOptions({ name: 'IoTDeviceMonitor' })
  285. const message = useMessage() // 消息弹窗
  286. const { t } = useI18n() // 国际化
  287. const loading = ref(true) // 列表加载中
  288. const list = ref<DeviceVO[]>([]) // 列表的数据
  289. const total = ref(0) // 列表的总页数
  290. const queryParams = reactive({
  291. pageNo: 1,
  292. pageSize: 12,
  293. deviceCode: undefined,
  294. deviceName: undefined,
  295. brand: undefined,
  296. model: undefined,
  297. deptId: undefined,
  298. deviceStatus: undefined,
  299. assetProperty: undefined,
  300. picUrl: undefined,
  301. remark: undefined,
  302. manufacturerId: undefined,
  303. supplierId: undefined,
  304. manDate: [],
  305. nameplate: undefined,
  306. expires: undefined,
  307. plPrice: undefined,
  308. plDate: [],
  309. plYear: undefined,
  310. plStartDate: [],
  311. plMonthed: undefined,
  312. plAmounted: undefined,
  313. remainAmount: undefined,
  314. infoId: undefined,
  315. infoType: undefined,
  316. infoName: undefined,
  317. infoRemark: undefined,
  318. infoUrl: undefined,
  319. templateJson: undefined,
  320. creator: undefined,
  321. ifInline: undefined
  322. })
  323. const queryFormRef = ref() // 搜索的表单
  324. const exportLoading = ref(false) // 导出加载状态
  325. const products = ref<ProductVO[]>([]) // 产品列表
  326. const selectedIds = ref<number[]>([]) // 选中的设备编号数组
  327. const viewMode = ref<'card' | 'list'>('card') // 视图模式状态
  328. /** 查询列表 */
  329. const getList = async () => {
  330. loading.value = true
  331. try {
  332. const data = await IotDeviceApi.getIotDeviceTdPage(queryParams)
  333. list.value = data.list
  334. total.value = data.total
  335. } finally {
  336. loading.value = false
  337. }
  338. }
  339. /** 搜索按钮操作 */
  340. const handleQuery = () => {
  341. queryParams.pageNo = 1
  342. getList()
  343. }
  344. /** 重置按钮操作 */
  345. const resetQuery = () => {
  346. queryFormRef.value.resetFields()
  347. selectedIds.value = [] // 清空选择
  348. handleQuery()
  349. }
  350. // /** 添加/修改操作 */
  351. // const formRef = ref()
  352. // const openForm = (type: string, id?: number) => {
  353. // formRef.value.open(type, id)
  354. // }
  355. /** 打开详情 */
  356. const { push } = useRouter()
  357. const openDetail = (
  358. id: number,
  359. ifInline: string,
  360. time: string,
  361. name: string,
  362. code: string,
  363. dept: string
  364. ) => {
  365. if (time === null || time === undefined) {
  366. message.warning('没有数采数据')
  367. return
  368. }
  369. push({ name: 'TdDeviceDetail', params: { id, ifInline, time, name, code, dept } })
  370. }
  371. /** 导出方法 */
  372. const handleExport = async () => {
  373. try {
  374. // 导出的二次确认
  375. await message.exportConfirm()
  376. // 发起导出
  377. exportLoading.value = true
  378. const data = await DeviceApi.exportDeviceExcel(queryParams)
  379. download.excel(data, '物联网设备.xls')
  380. } catch {
  381. } finally {
  382. exportLoading.value = false
  383. }
  384. }
  385. /** 多选框选中数据 */
  386. const handleSelectionChange = (selection: DeviceVO[]) => {
  387. selectedIds.value = selection.map((item) => item.id)
  388. }
  389. //
  390. // /** 添加到分组操作 */
  391. // const groupFormRef = ref()
  392. // const openGroupForm = () => {
  393. // groupFormRef.value.open(selectedIds.value)
  394. // }
  395. //
  396. // /** 打开物模型数据 */
  397. // const openModel = (id: number) => {
  398. // push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } })
  399. // }
  400. //
  401. // /** 设备导入 */
  402. // const importFormRef = ref()
  403. // const handleImport = () => {
  404. // importFormRef.value.open()
  405. // }
  406. /** 初始化 **/
  407. onMounted(async () => {
  408. await getList()
  409. })
  410. /** 处理部门被点击 */
  411. const handleDeptNodeClick = async (row) => {
  412. queryParams.deptId = row.id
  413. await getList()
  414. }
  415. </script>