index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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 custom-card"
  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" style="height: 38px;">
  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 v-if="!item.carId" src="@/assets/imgs/iot/device.png" class="w-full h-full" />
  175. <img v-if="item.carId" src="@/assets/imgs/iot/car.png" class="mt-4 ml-4" />
  176. </div>
  177. </div>
  178. <!-- 分隔线 -->
  179. <el-divider class="!my-3" />
  180. <!-- 按钮 -->
  181. <div class="flex items-center px-0">
  182. <el-button
  183. class="flex-1 !px-2 !h-[32px] text-[13px]"
  184. type="warning"
  185. plain
  186. @click="
  187. openDetail(
  188. item.id,
  189. item.ifInline,
  190. item.lastInlineTime,
  191. item.deviceName,
  192. item.deviceCode,
  193. item.deptName,
  194. item.vehicleName
  195. )
  196. "
  197. >
  198. <Icon icon="ep:view" class="mr-1" />
  199. {{ t('monitor.details') }}
  200. </el-button>
  201. <!-- <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>-->
  202. </div>
  203. </div>
  204. </el-card>
  205. </el-col>
  206. </el-row>
  207. </template>
  208. <!-- 列表视图 -->
  209. <el-table
  210. v-else
  211. v-loading="loading"
  212. :data="list"
  213. :stripe="true"
  214. :show-overflow-tooltip="true"
  215. >
  216. <el-table-column :label="t('monitor.serial')" width="60" align="center">
  217. <template #default="scope">
  218. {{ scope.$index + 1 }}
  219. </template>
  220. </el-table-column>
  221. <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName">
  222. <template #default="scope">
  223. <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
  224. </template>
  225. </el-table-column>
  226. <el-table-column :label="t('monitor.deviceCode')" align="center" prop="deviceCode" />
  227. <el-table-column :label="t('monitor.category')" align="center" prop="assetClassName" />
  228. <el-table-column :label="t('monitor.status')" align="center" prop="deviceStatus">
  229. <template #default="scope">
  230. <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
  231. </template>
  232. </el-table-column>
  233. <el-table-column :label="t('monitor.online')" align="center" prop="ifInline">
  234. <template #default="scope">
  235. <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="scope.row.ifInline" />
  236. </template>
  237. </el-table-column>
  238. <el-table-column
  239. :label="t('monitor.latestDataTime')"
  240. align="center"
  241. prop="lastInlineTime"
  242. :formatter="dateFormatter"
  243. width="180px"
  244. />
  245. <el-table-column :label="t('monitor.operation')" align="center" min-width="120px">
  246. <template #default="scope">
  247. <el-button
  248. link
  249. type="primary"
  250. @click="
  251. openDetail(
  252. scope.row.id,
  253. scope.row.ifInline,
  254. scope.row.lastInlineTime,
  255. scope.row.deviceName,
  256. scope.row.deviceCode,
  257. scope.row.deptName,
  258. scope.row.vehicleName
  259. )
  260. "
  261. >
  262. {{ t('monitor.check') }}
  263. </el-button>
  264. </template>
  265. </el-table-column>
  266. </el-table>
  267. <!-- 分页 -->
  268. <Pagination
  269. :total="total"
  270. v-model:page="queryParams.pageNo"
  271. v-model:limit="queryParams.pageSize"
  272. @pagination="getList"
  273. />
  274. </ContentWrap>
  275. </ContentWrap>
  276. </el-col>
  277. </el-row>
  278. </template>
  279. <script setup lang="ts">
  280. import { DICT_TYPE, getDictLabel, getStrDictOptions } from '@/utils/dict'
  281. import { dateFormatter } from '@/utils/formatTime'
  282. import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
  283. import download from '@/utils/download'
  284. import DeptTree from '@/views/system/user/DeptTree.vue'
  285. import { IotDeviceApi } from '@/api/pms/device'
  286. /** IoT 设备列表 */
  287. defineOptions({ name: 'IoTDeviceMonitor' })
  288. const message = useMessage() // 消息弹窗
  289. const { t } = useI18n() // 国际化
  290. const loading = ref(true) // 列表加载中
  291. const list = ref<DeviceVO[]>([]) // 列表的数据
  292. const total = ref(0) // 列表的总页数
  293. const queryParams = reactive({
  294. pageNo: 1,
  295. pageSize: 12,
  296. deviceCode: undefined,
  297. deviceName: undefined,
  298. brand: undefined,
  299. model: undefined,
  300. deptId: undefined,
  301. deviceStatus: undefined,
  302. assetProperty: undefined,
  303. picUrl: undefined,
  304. remark: undefined,
  305. manufacturerId: undefined,
  306. supplierId: undefined,
  307. manDate: [],
  308. nameplate: undefined,
  309. expires: undefined,
  310. plPrice: undefined,
  311. plDate: [],
  312. plYear: undefined,
  313. plStartDate: [],
  314. plMonthed: undefined,
  315. plAmounted: undefined,
  316. remainAmount: undefined,
  317. infoId: undefined,
  318. infoType: undefined,
  319. infoName: undefined,
  320. infoRemark: undefined,
  321. infoUrl: undefined,
  322. templateJson: undefined,
  323. creator: undefined,
  324. ifInline: undefined
  325. })
  326. const queryFormRef = ref() // 搜索的表单
  327. const exportLoading = ref(false) // 导出加载状态
  328. const products = ref<ProductVO[]>([]) // 产品列表
  329. const selectedIds = ref<number[]>([]) // 选中的设备编号数组
  330. const viewMode = ref<'card' | 'list'>('card') // 视图模式状态
  331. /** 查询列表 */
  332. const getList = async () => {
  333. loading.value = true
  334. try {
  335. const data = await IotDeviceApi.getIotDeviceTdPage(queryParams)
  336. list.value = data.list
  337. total.value = data.total
  338. } finally {
  339. loading.value = false
  340. }
  341. }
  342. /** 搜索按钮操作 */
  343. const handleQuery = () => {
  344. queryParams.pageNo = 1
  345. getList()
  346. }
  347. /** 重置按钮操作 */
  348. const resetQuery = () => {
  349. queryFormRef.value.resetFields()
  350. selectedIds.value = [] // 清空选择
  351. handleQuery()
  352. }
  353. // /** 添加/修改操作 */
  354. // const formRef = ref()
  355. // const openForm = (type: string, id?: number) => {
  356. // formRef.value.open(type, id)
  357. // }
  358. /** 打开详情 */
  359. const { push } = useRouter()
  360. const openDetail = (
  361. id: number,
  362. ifInline: string,
  363. time: string,
  364. name: string,
  365. code: string,
  366. dept: string,
  367. vehicle:string
  368. ) => {
  369. if (time === null || time === undefined) {
  370. message.warning('没有数采数据')
  371. return
  372. }
  373. push({ name: 'TdDeviceDetail', params: { id, ifInline, time, name, code, dept,vehicle } })
  374. }
  375. /** 导出方法 */
  376. const handleExport = async () => {
  377. try {
  378. // 导出的二次确认
  379. await message.exportConfirm()
  380. // 发起导出
  381. exportLoading.value = true
  382. const data = await DeviceApi.exportDeviceExcel(queryParams)
  383. download.excel(data, '物联网设备.xls')
  384. } catch {
  385. } finally {
  386. exportLoading.value = false
  387. }
  388. }
  389. /** 多选框选中数据 */
  390. const handleSelectionChange = (selection: DeviceVO[]) => {
  391. selectedIds.value = selection.map((item) => item.id)
  392. }
  393. //
  394. // /** 添加到分组操作 */
  395. // const groupFormRef = ref()
  396. // const openGroupForm = () => {
  397. // groupFormRef.value.open(selectedIds.value)
  398. // }
  399. //
  400. // /** 打开物模型数据 */
  401. // const openModel = (id: number) => {
  402. // push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } })
  403. // }
  404. //
  405. // /** 设备导入 */
  406. // const importFormRef = ref()
  407. // const handleImport = () => {
  408. // importFormRef.value.open()
  409. // }
  410. /** 初始化 **/
  411. onMounted(async () => {
  412. await getList()
  413. })
  414. /** 处理部门被点击 */
  415. const handleDeptNodeClick = async (row) => {
  416. queryParams.deptId = row.id
  417. await getList()
  418. }
  419. </script>
  420. <style scoped>
  421. .custom-card{
  422. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  423. transform: scale(1); /* 原始大小 */
  424. /* 关键:添加过渡动画,让变化更平滑 */
  425. transition: all 0.3s ease;
  426. }
  427. .custom-card:hover {
  428. /* 增强阴影效果,增加立体感 */
  429. box-shadow: 0 10px 20px rgba(0, 0, 0, 0.12);
  430. /* 轻微放大(1.03倍是比较合适的比例) */
  431. transform: scale(1.03);
  432. }
  433. </style>