index2.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. <template style="background-color: #edf4ff">
  2. <el-row :gutter="20">
  3. <!-- 左侧部门树 -->
  4. <el-col :span="4" :xs="24">
  5. <ContentWrap class="h-1/1" v-if="treeShow" style="border: 0; height: 87.5vh">
  6. <DeptTree @node-click="handleDeptNodeClick" />
  7. </ContentWrap>
  8. </el-col>
  9. <el-col :span="contentSpan" :xs="24">
  10. <!-- 统计卡片 -->
  11. <el-row :gutter="21" class="mb-4">
  12. <el-col :span="9">
  13. <div class="flex gap-2 bg-white p-5 rounded-lg">
  14. <div class="stat-card bg-blue-gradient w-[40%]">
  15. <Icon icon="ep:histogram" :size="40" />
  16. <div class="card-title">设备总数</div>
  17. <div class="card-value pt-5">{{ devicesCount }}</div>
  18. </div>
  19. <div class="stat-card bg-green-gradient w-[60%]">
  20. <div class="flex flex-col items-center h-full">
  21. <!-- <Icon icon="ep:odometer" :size="40" class="mb-2" /> -->
  22. <div class="card-title mb-2">设备状态</div>
  23. <div class="flex-1 w-full" style="height: calc(100% - 60px)">
  24. <Echart
  25. v-if="statusChartOption.series && statusChartOption.series[0].data.length > 0"
  26. :options="statusChartOption"
  27. :height="160"
  28. />
  29. <div v-else class="flex items-center justify-center h-full text-gray-500">
  30. 暂无数据
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. </div>
  36. </el-col>
  37. <el-col :span="10">
  38. <div style="background-color: #fff" class="rounded-lg">
  39. <Echart :options="myoption" :height="270" />
  40. </div>
  41. </el-col>
  42. <el-col :span="5">
  43. <div class="bg-[#fff] p-2 py-5 h-[270px] rounded-lg">
  44. <el-form ref="queryFormRef" :model="queryParams">
  45. <el-form-item label="设备编码" prop="deviceCode">
  46. <el-input
  47. style="height: 30px"
  48. @keyup.enter="handleQuery"
  49. v-model="queryParams.deviceCode"
  50. placeholder="请输入设备编码"
  51. />
  52. </el-form-item>
  53. <el-form-item label="设备名称" prop="deviceName">
  54. <el-input
  55. style="height: 30px"
  56. @keyup.enter="handleQuery"
  57. v-model="queryParams.deviceName"
  58. placeholder="请输入设备名称"
  59. />
  60. </el-form-item>
  61. <el-form-item>
  62. <el-button @click="handleQuery"
  63. ><Icon icon="ep:search" class="mr-3px" />{{
  64. t('operationFill.search')
  65. }}</el-button
  66. >
  67. </el-form-item>
  68. <el-form-item>
  69. <el-button @click="resetQuery"
  70. ><Icon icon="ep:refresh" class="mr-3px" />
  71. {{ t('operationFill.reset') }}</el-button
  72. >
  73. </el-form-item>
  74. <el-form-item>
  75. <el-button type="success" plain
  76. ><Icon icon="ep:download" class="mr-3px" /> 导出</el-button
  77. >
  78. </el-form-item>
  79. </el-form>
  80. </div>
  81. </el-col>
  82. </el-row>
  83. <!-- 列表 -->
  84. <ContentWrap style="border: 0; margin-top: 20px">
  85. <el-table
  86. v-loading="loading"
  87. :data="list"
  88. :stripe="true"
  89. :show-overflow-tooltip="true"
  90. @sort-change="handleSortChange"
  91. height="45vh"
  92. >
  93. <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
  94. <template #default="scope">
  95. {{ scope.$index + 1 }}
  96. </template>
  97. </el-table-column>
  98. <el-table-column label="设备编码" sortable align="center" prop="deviceCode" width="150" />
  99. <el-table-column
  100. :label="t('iotDevice.name')"
  101. sortable
  102. align="center"
  103. prop="deviceName"
  104. min-width="250"
  105. >
  106. <template #default="scope">
  107. <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
  108. {{ scope.row.deviceName }}
  109. </el-link>
  110. </template>
  111. </el-table-column>
  112. <el-table-column
  113. :label="t('iotDevice.dept')"
  114. align="center"
  115. prop="deptName"
  116. min-width="150"
  117. />
  118. <el-table-column
  119. :label="t('iotDevice.status')"
  120. align="center"
  121. prop="deviceStatus"
  122. min-width="90"
  123. >
  124. <template #default="scope">
  125. <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
  126. </template>
  127. </el-table-column>
  128. <el-table-column
  129. :label="t('iotDevice.assets')"
  130. align="center"
  131. prop="assetProperty"
  132. min-width="110"
  133. >
  134. <template #default="scope">
  135. <dict-tag :type="DICT_TYPE.PMS_ASSET_PROPERTY" :value="scope.row.assetProperty" />
  136. </template>
  137. </el-table-column>
  138. <el-table-column
  139. :label="t('iotDevice.assetClass')"
  140. align="center"
  141. prop="assetClassName"
  142. min-width="170"
  143. />
  144. <el-table-column label="生产厂家" align="center" prop="manufacturer" min-width="200" />
  145. <el-table-column label="生产日期" align="center" min-width="200">
  146. <template #default="scope">
  147. {{ formatDate(scope.row.manDate).substring(0, 10) }}
  148. </template>
  149. </el-table-column>
  150. <el-table-column label="投运日期" align="center" min-width="200">
  151. <template #default="scope">
  152. {{ formatDate(scope.row.enableDate).substring(0, 10) }}
  153. </template>
  154. </el-table-column>
  155. <el-table-column
  156. :label="t('deviceForm.brand')"
  157. align="center"
  158. prop="brandName"
  159. min-width="150"
  160. />
  161. <el-table-column
  162. :label="t('deviceForm.model')"
  163. align="center"
  164. prop="model"
  165. min-width="170"
  166. />
  167. <el-table-column
  168. :label="t('devicePerson.rp')"
  169. align="center"
  170. prop="chargeName"
  171. min-width="170"
  172. />
  173. <el-table-column
  174. :label="t('deviceForm.useProject')"
  175. align="center"
  176. prop="useProject"
  177. min-width="170"
  178. />
  179. <el-table-column
  180. :label="t('deviceForm.assetOwner')"
  181. align="center"
  182. prop="assetOwnership"
  183. min-width="170"
  184. />
  185. <!-- <el-table-column
  186. :label="t('operationFill.operation')"
  187. align="center"
  188. min-width="180px"
  189. fixed="right"
  190. >
  191. <template #default="scope">
  192. <el-button
  193. link
  194. type="primary"
  195. @click="openForm('update', scope.row.id)"
  196. v-hasPermi="['rq:iot-device:update']"
  197. >
  198. {{ t('iotDevice.update') }}
  199. </el-button>
  200. <el-button
  201. link
  202. type="danger"
  203. @click="handleDelete(scope.row.id)"
  204. v-hasPermi="['rq:iot-device:delete']"
  205. >
  206. {{ t('iotDevice.delete') }}
  207. </el-button>
  208. </template>
  209. </el-table-column> -->
  210. </el-table>
  211. <!-- 分页 -->
  212. <Pagination
  213. :total="total"
  214. v-model:page="queryParams.pageNo"
  215. v-model:limit="queryParams.pageSize"
  216. @pagination="getList"
  217. />
  218. </ContentWrap>
  219. </el-col>
  220. </el-row>
  221. </template>
  222. <script setup lang="ts">
  223. import download from '@/utils/download'
  224. import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
  225. import { DICT_TYPE } from '@/utils/dict'
  226. import DeptTree from '@/views/system/user/DeptTree.vue'
  227. import { buildSortingField } from '@/utils'
  228. import { useRefreshStore } from '@/store/modules/pms/refreshStore'
  229. import Echart from '@/components/Echart/src/Echart.vue'
  230. import echarts from '@/plugins/echarts'
  231. import { formatDate } from '@/utils/formatTime'
  232. /** 设备台账 列表 */
  233. defineOptions({ name: 'IotDevicePms' })
  234. const message = useMessage() // 消息弹窗
  235. const { t } = useI18n() // 国际化
  236. const { push } = useRouter() // 路由跳转
  237. const refreshStore = useRefreshStore()
  238. const loading = ref(true) // 列表的加载中
  239. const ifShow = ref(false)
  240. const list = ref<IotDeviceVO[]>([]) // 列表的数据
  241. const total = ref(0) // 列表的总页数
  242. const queryParams = reactive({
  243. pageNo: 1,
  244. pageSize: 10,
  245. deviceCode: undefined,
  246. deviceName: undefined,
  247. brand: undefined,
  248. brandName: undefined,
  249. model: undefined,
  250. deptId: undefined,
  251. deviceStatus: undefined,
  252. assetProperty: undefined,
  253. picUrl: undefined,
  254. remark: undefined,
  255. manufacturerId: undefined,
  256. supplierId: undefined,
  257. manDate: [],
  258. nameplate: undefined,
  259. expires: undefined,
  260. plPrice: undefined,
  261. plDate: [],
  262. plYear: undefined,
  263. plStartDate: [],
  264. plMonthed: undefined,
  265. plAmounted: undefined,
  266. remainAmount: undefined,
  267. infoId: undefined,
  268. infoType: undefined,
  269. infoName: undefined,
  270. infoRemark: undefined,
  271. infoUrl: undefined,
  272. templateJson: undefined,
  273. creator: undefined,
  274. sortingFields: [],
  275. assetClass: undefined,
  276. yfDeviceCode: undefined
  277. })
  278. const queryFormRef = ref(null) // 搜索的表单
  279. const exportLoading = ref(false) // 导出的加载中
  280. const contentSpan = ref(20)
  281. const treeShow = ref(true)
  282. const handleSortChange = (params: any) => {
  283. //console.log(`排序字段: ${prop}, 排序方式: ${order}`);
  284. queryParams.sortingFields = []
  285. queryParams.sortingFields = [buildSortingField(params)]
  286. getList()
  287. }
  288. const myoption = computed(() => {
  289. return {
  290. tooltip: {
  291. trigger: 'axis'
  292. },
  293. xAxis: {
  294. type: 'category',
  295. data: deviceClassify.value.map((item) => item.category),
  296. axisLabel: { color: '#000', rotate: 50 }
  297. },
  298. yAxis: {
  299. name: '分类top10',
  300. type: 'value'
  301. },
  302. series: [
  303. {
  304. data: deviceClassify.value.map((item) => item.value),
  305. type: 'bar',
  306. barWidth: 20,
  307. barCategoryGap: '30%',
  308. itemStyle: {
  309. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  310. { offset: 0, color: '#83bff6' },
  311. { offset: 0.5, color: '#188df0' },
  312. { offset: 1, color: '#188df0' }
  313. ])
  314. },
  315. emphasis: {
  316. itemStyle: {
  317. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  318. { offset: 0, color: '#2378f7' },
  319. { offset: 0.7, color: '#2378f7' },
  320. { offset: 1, color: '#83bff6' }
  321. ])
  322. }
  323. }
  324. }
  325. ]
  326. }
  327. })
  328. /** 查询列表 */
  329. const getList = async () => {
  330. loading.value = true
  331. try {
  332. const data = await IotDeviceApi.getIotDeviceList(queryParams)
  333. list.value = data.list
  334. total.value = data.total
  335. } finally {
  336. loading.value = false
  337. }
  338. }
  339. const resetQuery = () => {
  340. queryFormRef.value.resetFields()
  341. handleQuery()
  342. }
  343. /** 处理部门被点击 */
  344. const handleDeptNodeClick = async (row) => {
  345. queryParams.deptId = row.id
  346. deviceCountQueryParams.deptId = row.id
  347. await getList()
  348. await getDeviceCount()
  349. }
  350. /** 搜索按钮操作 */
  351. const handleQuery = () => {
  352. queryParams.pageNo = 1
  353. getList()
  354. }
  355. const moreQuery = (show) => {
  356. ifShow.value = show
  357. }
  358. const openForm = (type: string, id?: number, deptId?: number) => {
  359. //修改
  360. if (typeof id === 'number') {
  361. push({ name: 'DeviceDetailEdit', params: { type, id }, query: { source: 'devicerouter' } })
  362. return
  363. }
  364. // 新增
  365. if (deptId) {
  366. push({ name: 'DeviceDetailAdd', params: { type, deptId }, query: { source: 'devicerouter' } })
  367. } else {
  368. push({ name: 'DeviceDetailAddd', params: {}, query: { source: 'devicerouter' } })
  369. }
  370. }
  371. /** 删除按钮操作 */
  372. const handleDelete = async (id: number) => {
  373. try {
  374. // 删除的二次确认
  375. await message.delConfirm()
  376. // 发起删除
  377. await IotDeviceApi.deleteIotDevice(id)
  378. message.success(t('common.delSuccess'))
  379. // 刷新列表
  380. await getList()
  381. } catch {}
  382. }
  383. const handleDetail = (id: number) => {
  384. push({ name: 'DeviceDetailInfo', params: { id } })
  385. }
  386. /** 导出按钮操作 */
  387. const handleExport = async () => {
  388. try {
  389. // 导出的二次确认
  390. await message.exportConfirm()
  391. // 发起导出
  392. exportLoading.value = true
  393. const data = await IotDeviceApi.exportIotDevice(queryParams)
  394. download.excel(data, '设备台账.xls')
  395. } catch {
  396. } finally {
  397. exportLoading.value = false
  398. }
  399. }
  400. // 设备数量
  401. let devicesCount = ref(0)
  402. const deviceCountQueryParams = reactive({
  403. deptId: undefined
  404. })
  405. // 设备状态
  406. let deviceStatus = ref([])
  407. const statusChartOption = computed(() => {
  408. // 将 deviceStatus 转换为饼图需要的格式
  409. const data = deviceStatus.value.map((item) => ({
  410. value: item.value,
  411. name: item.name
  412. }))
  413. // 如果没有数据,返回空配置
  414. if (data.length === 0) {
  415. return {
  416. series: [
  417. {
  418. type: 'pie',
  419. data: []
  420. }
  421. ]
  422. }
  423. }
  424. return {
  425. tooltip: {
  426. trigger: 'item',
  427. formatter: '{a} <br/>{b}: {c} ({d}%)'
  428. },
  429. legend: {
  430. show: false
  431. },
  432. series: [
  433. {
  434. name: ' ',
  435. type: 'pie',
  436. radius: ['40%', '70%'],
  437. avoidLabelOverlap: false,
  438. itemStyle: {
  439. borderRadius: 5,
  440. borderColor: '#fff',
  441. borderWidth: 2
  442. },
  443. label: {
  444. show: true,
  445. formatter: '{b}\n{d}%',
  446. fontSize: 10
  447. },
  448. labelLine: {
  449. show: true
  450. },
  451. data: data,
  452. emphasis: {
  453. itemStyle: {
  454. shadowBlur: 10,
  455. shadowOffsetX: 0,
  456. shadowColor: 'rgba(0, 0, 0, 0.5)'
  457. }
  458. }
  459. }
  460. ]
  461. }
  462. })
  463. // 分类top10
  464. let deviceClassify = ref([])
  465. const getDeviceCount = async () => {
  466. devicesCount.value = await IotDeviceApi.getIotDeviceCount(deviceCountQueryParams)
  467. deviceStatus.value = await IotDeviceApi.getIotDeviceStatus(deviceCountQueryParams)
  468. deviceClassify.value = await IotDeviceApi.getIotDeviceClassify(deviceCountQueryParams)
  469. }
  470. /** 初始化 **/
  471. onMounted(async () => {
  472. getDeviceCount()
  473. // productClassifyList.value = handleTree(
  474. // await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
  475. // )
  476. const sort = {
  477. field: 'sortColumn',
  478. order: 'asc'
  479. }
  480. queryParams.sortingFields.push(sort)
  481. await getList()
  482. refreshStore.registerCallback('devicerouter', getList)
  483. })
  484. </script>
  485. <style scoped>
  486. .stat-card {
  487. padding: 20px;
  488. border-radius: 10px;
  489. color: white;
  490. text-align: center;
  491. font-size: 14px;
  492. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  493. transition:
  494. transform 0.3s ease,
  495. box-shadow 0.3s ease;
  496. backdrop-filter: blur(12px);
  497. height: 230px;
  498. }
  499. .stat-card::before {
  500. position: absolute;
  501. filter: blur(20px);
  502. z-index: -1;
  503. }
  504. .stat-card:hover {
  505. transform: translateY(-4px);
  506. box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
  507. }
  508. .card-title {
  509. margin: 8px 0;
  510. font-size: 16px;
  511. }
  512. .card-value {
  513. font-size: 28px;
  514. font-weight: bold;
  515. margin: 8px 0;
  516. }
  517. .card-trend {
  518. font-size: 12px;
  519. opacity: 0.9;
  520. }
  521. /* 毛玻璃渐变背景 —— 不再使用 background-image */
  522. .bg-blue-gradient {
  523. background: linear-gradient(135deg, rgba(77, 147, 255, 0.5), rgba(75, 132, 254));
  524. }
  525. .bg-green-gradient {
  526. background: linear-gradient(135deg, rgba(101, 226, 136, 0.1), #52d7a2);
  527. background-color: rgba(76, 175, 80, 0.1);
  528. }
  529. .bg-orange-gradient {
  530. background: linear-gradient(135deg, #ff9800, #f57c00);
  531. background-color: rgba(255, 152, 0, 0.5);
  532. }
  533. /* 确保内容不溢出 */
  534. :deep(.el-row) {
  535. flex-wrap: wrap;
  536. }
  537. .text-truncate {
  538. overflow: hidden;
  539. text-overflow: ellipsis;
  540. white-space: nowrap;
  541. }
  542. ::v-deep .el-table__header-wrapper {
  543. position: sticky !important;
  544. width: 100%;
  545. top: 0px;
  546. z-index: 2000;
  547. }
  548. </style>