IotDeviceMainAlarm.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. <script setup lang="ts">
  2. import { useTableComponents } from '@/components/ZmTable/useTableComponents'
  3. import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
  4. import { IotMainWorkOrderApi } from '@/api/pms/iotmainworkorder'
  5. import { useUserStore } from '@/store/modules/user'
  6. import { DICT_TYPE } from '@/utils/dict'
  7. import download from '@/utils/download'
  8. import DeviceAlarmBomList from '@/views/pms/iotmainworkorder/DeviceAlarmBomList.vue'
  9. defineOptions({ name: 'IotDeviceMainAlarm' })
  10. type DeviceMainAlarmRow = IotDeviceVO & {
  11. mainDistance?: string | number | null
  12. workOrderId?: number
  13. planId?: number
  14. responsibleNames?: string
  15. }
  16. interface QueryParams extends PageParam {
  17. deptId?: number
  18. deviceCode?: string
  19. deviceName?: string
  20. brand?: string
  21. model?: string | number
  22. deviceStatus?: string
  23. assetProperty?: string
  24. picUrl?: string
  25. remark?: string
  26. manufacturerId?: number
  27. supplierId?: number
  28. manDate?: string[]
  29. nameplate?: string
  30. expires?: number
  31. plPrice?: number
  32. plDate?: string[]
  33. plYear?: number
  34. plStartDate?: string[]
  35. plMonthed?: number
  36. plAmounted?: number
  37. remainAmount?: number
  38. infoId?: number
  39. infoType?: string
  40. infoName?: string
  41. infoRemark?: string
  42. infoUrl?: string
  43. templateJson?: string
  44. creator?: string
  45. setFlag?: string
  46. }
  47. const { t } = useI18n()
  48. const { push } = useRouter()
  49. const { ZmTable, ZmTableColumn } = useTableComponents<DeviceMainAlarmRow>()
  50. const rootDeptId = 156
  51. const deptId = useUserStore().getUser.deptId || rootDeptId
  52. const initQuery: QueryParams = {
  53. pageNo: 1,
  54. pageSize: 10,
  55. deptId: undefined,
  56. deviceCode: undefined,
  57. deviceName: undefined,
  58. brand: undefined,
  59. model: undefined,
  60. deviceStatus: undefined,
  61. assetProperty: undefined,
  62. picUrl: undefined,
  63. remark: undefined,
  64. manufacturerId: undefined,
  65. supplierId: undefined,
  66. manDate: [],
  67. nameplate: undefined,
  68. expires: undefined,
  69. plPrice: undefined,
  70. plDate: [],
  71. plYear: undefined,
  72. plStartDate: [],
  73. plMonthed: undefined,
  74. plAmounted: undefined,
  75. remainAmount: undefined,
  76. infoId: undefined,
  77. infoType: undefined,
  78. infoName: undefined,
  79. infoRemark: undefined,
  80. infoUrl: undefined,
  81. templateJson: undefined,
  82. creator: undefined,
  83. setFlag: ''
  84. }
  85. const queryParams = reactive<QueryParams>({ ...initQuery })
  86. const queryFormRef = ref()
  87. const loading = ref(false)
  88. const exportLoading = ref(false)
  89. const list = ref<DeviceMainAlarmRow[]>([])
  90. const total = ref(0)
  91. const flag = ref()
  92. const modelFormRef = ref()
  93. const showRunTime = computed(() => {
  94. return list.value.some((item) => Boolean(item.totalRunTime))
  95. })
  96. const showMileage = computed(() => {
  97. return list.value.some((item) => Boolean(item.totalMileage))
  98. })
  99. const showMultiAttrs = computed(() => {
  100. const values = list.value
  101. .map((item) => item.multiAttrsTotalRuntime)
  102. .concat(list.value.map((item) => item.multiAttrsTotalMileage))
  103. .flatMap((item) => Object.values(item ?? {}))
  104. return values.some((item) => Boolean(item))
  105. })
  106. const getList = async () => {
  107. loading.value = true
  108. try {
  109. const data = await IotMainWorkOrderApi.deviceMainDistances(queryParams)
  110. list.value = data.list
  111. total.value = data.total
  112. } finally {
  113. loading.value = false
  114. }
  115. }
  116. const handleQuery = () => {
  117. queryParams.pageNo = 1
  118. getList()
  119. }
  120. const resetQuery = () => {
  121. Object.assign(queryParams, { ...initQuery })
  122. queryFormRef.value?.resetFields()
  123. handleQuery()
  124. }
  125. const handleSizeChange = (val: number) => {
  126. queryParams.pageSize = val
  127. handleQuery()
  128. }
  129. const handleCurrentChange = (val: number) => {
  130. queryParams.pageNo = val
  131. getList()
  132. }
  133. const handleDeptNodeClick = async (row: Tree) => {
  134. queryParams.deptId = row.id
  135. queryParams.pageNo = 1
  136. await getList()
  137. }
  138. const parseDistanceNumber = (distance: number | string | null | undefined) => {
  139. if (distance === null || distance === undefined || distance === '') return undefined
  140. if (typeof distance === 'number') return distance
  141. const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
  142. return numericPart ? Number(numericPart) : undefined
  143. }
  144. const getDistanceClass = (distance: number | string | null | undefined) => {
  145. const value = parseDistanceNumber(distance)
  146. if (value === undefined || value === 0) return ''
  147. return value < 0 ? 'negative-distance' : 'positive-distance'
  148. }
  149. const hasMaintenancePlan = (mainDistance: unknown) => {
  150. return mainDistance !== null && mainDistance !== undefined && mainDistance !== ''
  151. }
  152. const handleDetail = (id: number) => {
  153. push({ name: 'DeviceDetailInfo', params: { id } })
  154. }
  155. const openBomForm = async (row: DeviceMainAlarmRow) => {
  156. const deviceInfo = {
  157. deviceId: row.id,
  158. deviceCode: row.deviceCode,
  159. deviceName: row.deviceName,
  160. model: row.model
  161. }
  162. if (row.workOrderId) {
  163. flag.value = 'workOrder'
  164. modelFormRef.value?.open(row.workOrderId, flag.value, row.id)
  165. } else if (row.planId) {
  166. flag.value = 'plan'
  167. modelFormRef.value?.open(row.planId, flag.value, deviceInfo)
  168. }
  169. }
  170. const handleExport = async () => {
  171. exportLoading.value = true
  172. try {
  173. const data = await IotDeviceApi.exportIotDeviceMainAlarm(queryParams)
  174. download.excel(data, '保养查询.xls')
  175. } finally {
  176. exportLoading.value = false
  177. }
  178. }
  179. onMounted(() => {
  180. getList()
  181. })
  182. </script>
  183. <template>
  184. <div
  185. class="device-main-alarm-page grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
  186. <DeptTreeSelect
  187. :top-id="rootDeptId"
  188. :deptId="deptId"
  189. v-model="queryParams.deptId"
  190. :init-select="false"
  191. :show-title="false"
  192. request-api="getSimpleDeptList"
  193. class="device-main-alarm-tree row-span-2"
  194. @node-click="handleDeptNodeClick" />
  195. <el-form
  196. ref="queryFormRef"
  197. :model="queryParams"
  198. size="default"
  199. label-width="80px"
  200. class="device-main-alarm-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
  201. <div class="query-row">
  202. <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
  203. <el-input
  204. v-model="queryParams.deviceCode"
  205. :placeholder="t('iotDevice.codeHolder')"
  206. clearable
  207. class="query-control"
  208. @keyup.enter="handleQuery" />
  209. </el-form-item>
  210. <el-form-item :label="t('iotDevice.name')" prop="deviceName">
  211. <el-input
  212. v-model="queryParams.deviceName"
  213. :placeholder="t('iotDevice.nameHolder')"
  214. clearable
  215. class="query-control"
  216. @keyup.enter="handleQuery" />
  217. </el-form-item>
  218. </div>
  219. <el-form-item class="query-actions">
  220. <el-button type="primary" @click="handleQuery">
  221. <Icon icon="ep:search" class="mr-5px" />{{ t('file.search') }}
  222. </el-button>
  223. <el-button @click="resetQuery">
  224. <Icon icon="ep:refresh" class="mr-5px" />{{ t('file.reset') }}
  225. </el-button>
  226. <el-button
  227. type="success"
  228. plain
  229. :loading="exportLoading"
  230. @click="handleExport"
  231. v-hasPermi="['rq:iot-device:export']">
  232. <Icon icon="ep:download" class="mr-5px" />导出
  233. </el-button>
  234. </el-form-item>
  235. </el-form>
  236. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
  237. <div class="flex-1 relative min-h-0">
  238. <el-auto-resizer class="absolute">
  239. <template #default="{ width, height }">
  240. <ZmTable
  241. :data="list"
  242. :loading="loading"
  243. :width="width"
  244. :height="height"
  245. :max-height="height"
  246. show-border>
  247. <ZmTableColumn
  248. type="index"
  249. :label="t('monitor.serial')"
  250. :width="70"
  251. fixed="left"
  252. hide-in-column-settings />
  253. <ZmTableColumn prop="deviceCode" :label="t('iotDevice.code')" fixed="left" />
  254. <ZmTableColumn prop="deviceName" :label="t('iotDevice.name')" fixed="left">
  255. <template #default="{ row }">
  256. <el-link :underline="false" type="primary" @click="handleDetail(row.id)">
  257. {{ row.deviceName }}
  258. </el-link>
  259. </template>
  260. </ZmTableColumn>
  261. <ZmTableColumn :label="t('bomList.serviceDue')" min-width="140">
  262. <template #default="{ row }">
  263. <template v-if="hasMaintenancePlan(row.mainDistance)">
  264. <span :class="getDistanceClass(row.mainDistance)">
  265. {{ row.mainDistance }}
  266. </span>
  267. </template>
  268. <span v-else>无保养计划</span>
  269. </template>
  270. </ZmTableColumn>
  271. <ZmTableColumn
  272. v-if="showRunTime"
  273. prop="totalRunTime"
  274. label="累计运行时长H"
  275. min-width="140" />
  276. <ZmTableColumn
  277. v-if="showMileage"
  278. prop="totalMileage"
  279. label="累计运行里程KM"
  280. min-width="150" />
  281. <ZmTableColumn v-if="showMultiAttrs" label="多属性累计值" min-width="200">
  282. <template #default="{ row }">
  283. <template v-for="(value, key) in row.multiAttrsTotalRuntime" :key="key">
  284. <span v-if="value && key">{{ key }}: {{ value }}&nbsp;&nbsp;</span>
  285. </template>
  286. <template v-for="(value, key) in row.multiAttrsTotalMileage" :key="key">
  287. <span v-if="value && key">{{ key }}: {{ value }}&nbsp;&nbsp;</span>
  288. </template>
  289. </template>
  290. </ZmTableColumn>
  291. <ZmTableColumn prop="deptName" :label="t('iotDevice.dept')" />
  292. <ZmTableColumn prop="responsibleNames" :label="t('devicePerson.rp')" />
  293. <ZmTableColumn prop="deviceStatus" :label="t('monitor.status')">
  294. <template #default="{ row }">
  295. <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="row.deviceStatus" />
  296. </template>
  297. </ZmTableColumn>
  298. <ZmTableColumn :label="t('operationFill.status')" min-width="150">
  299. <template #default="{ row }">
  300. <span v-if="row.shouldWorkOrder && row.runningWorkOrder">
  301. {{ t('mainPlan.generatedNotExecuted') }}
  302. </span>
  303. <span v-else-if="row.shouldWorkOrder && !row.runningWorkOrder">
  304. {{ t('mainPlan.notGenerated') }}
  305. </span>
  306. <span v-else>-</span>
  307. </template>
  308. </ZmTableColumn>
  309. <ZmTableColumn :label="t('monitor.operation')" width="80" fixed="right" action>
  310. <template #default="{ row }">
  311. <el-button
  312. v-if="hasMaintenancePlan(row.mainDistance)"
  313. link
  314. type="primary"
  315. @click="openBomForm(row)"
  316. v-hasPermi="['rq:iot-device:query']">
  317. {{ t('monitor.details') }}
  318. </el-button>
  319. </template>
  320. </ZmTableColumn>
  321. </ZmTable>
  322. </template>
  323. </el-auto-resizer>
  324. </div>
  325. <div class="h-8 mt-2 flex items-center justify-end">
  326. <el-pagination
  327. v-show="total > 0"
  328. size="default"
  329. :current-page="queryParams.pageNo"
  330. :page-size="queryParams.pageSize"
  331. :background="true"
  332. :page-sizes="[10, 20, 30, 50, 100]"
  333. :total="total"
  334. layout="total, sizes, prev, pager, next, jumper"
  335. @size-change="handleSizeChange"
  336. @current-change="handleCurrentChange" />
  337. </div>
  338. </div>
  339. </div>
  340. <DeviceAlarmBomList ref="modelFormRef" :flag="flag" />
  341. </template>
  342. <style scoped>
  343. .device-main-alarm-query {
  344. display: flex;
  345. flex-wrap: wrap;
  346. align-items: center;
  347. justify-content: space-between;
  348. gap: 12px 24px;
  349. }
  350. .query-row {
  351. display: flex;
  352. flex: 1 1 auto;
  353. flex-wrap: wrap;
  354. align-items: center;
  355. gap: 12px 24px;
  356. min-width: 0;
  357. }
  358. .query-row :deep(.el-form-item__label) {
  359. white-space: nowrap;
  360. }
  361. .query-actions {
  362. flex: 0 0 auto;
  363. }
  364. .query-actions :deep(.el-form-item__content) {
  365. display: flex;
  366. flex-wrap: wrap;
  367. gap: 8px 10px;
  368. }
  369. .query-actions :deep(.el-button) {
  370. margin-left: 0;
  371. }
  372. .query-control {
  373. width: 200px;
  374. }
  375. .positive-distance {
  376. display: inline-block;
  377. padding: 2px 8px;
  378. color: #67c23a;
  379. background-color: rgb(103 194 58 / 10%);
  380. border-radius: 4px;
  381. }
  382. .negative-distance {
  383. display: inline-block;
  384. padding: 2px 8px;
  385. color: #f56c6c;
  386. background-color: rgb(245 108 108 / 10%);
  387. border-radius: 4px;
  388. }
  389. :deep(.el-form-item) {
  390. margin-bottom: 0;
  391. }
  392. @media (width >= 2200px) {
  393. .device-main-alarm-query,
  394. .query-row {
  395. flex-wrap: nowrap;
  396. }
  397. }
  398. @media (width <= 1500px) {
  399. .device-main-alarm-query,
  400. .query-row {
  401. gap: 12px 18px;
  402. }
  403. .query-control {
  404. width: 180px;
  405. }
  406. }
  407. @media (width <= 1200px) {
  408. .device-main-alarm-page {
  409. grid-template-columns: minmax(0, 1fr);
  410. grid-template-rows: auto auto minmax(480px, 1fr);
  411. height: auto;
  412. min-height: calc(
  413. 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
  414. );
  415. }
  416. :deep(.device-main-alarm-tree) {
  417. grid-row: auto !important;
  418. width: 100% !important;
  419. height: 320px !important;
  420. min-width: 0 !important;
  421. }
  422. .query-actions {
  423. width: 100%;
  424. }
  425. }
  426. @media (width <= 768px) {
  427. .device-main-alarm-query {
  428. padding: 12px;
  429. }
  430. .query-row,
  431. .query-row :deep(.el-form-item),
  432. .query-actions {
  433. width: 100%;
  434. }
  435. .query-control {
  436. width: 100%;
  437. }
  438. .query-actions :deep(.el-form-item__content) {
  439. display: grid;
  440. grid-template-columns: repeat(2, minmax(0, 1fr));
  441. gap: 8px;
  442. width: 100%;
  443. }
  444. .query-actions :deep(.el-button) {
  445. width: 100%;
  446. margin-left: 0;
  447. }
  448. }
  449. </style>