DeviceAlarmBomList.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. <template>
  2. <Dialog
  3. v-model="dialogVisible"
  4. :title="t('monitor.details')"
  5. :width="dialogWidth"
  6. class="device-alarm-detail-dialog"
  7. @close="handleClose">
  8. <template #title>
  9. <div class="dialog-title">
  10. <div class="dialog-title__icon"><Icon icon="ep:calendar" /></div>
  11. <div>
  12. <div class="dialog-title__text">{{ t('monitor.details') }}</div>
  13. <div class="dialog-title__subtitle">查看设备保养项目及各维度保养里程</div>
  14. </div>
  15. </div>
  16. </template>
  17. <div class="dialog-content">
  18. <section v-if="deviceInfo" class="device-info-section">
  19. <div class="section-heading"><span></span>设备信息</div>
  20. <div class="device-info-grid">
  21. <div class="info-item">
  22. <span class="info-label">{{ t('iotDevice.code') }}</span>
  23. <span class="info-value">{{ deviceInfo.deviceCode }}</span>
  24. </div>
  25. <div class="info-item">
  26. <span class="info-label">{{ t('iotDevice.name') }}</span>
  27. <span class="info-value">{{ deviceInfo.deviceName }}</span>
  28. </div>
  29. <div class="info-item" v-if="deviceInfo.model">
  30. <span class="info-label">{{ t('deviceForm.model') }}</span>
  31. <span class="info-value">{{ deviceInfo.model }}</span>
  32. </div>
  33. </div>
  34. </section>
  35. <section class="maintenance-section">
  36. <div class="section-heading"><span></span>保养项目</div>
  37. <div class="table-container">
  38. <ZmTable
  39. :loading="loading"
  40. :data="paginatedList"
  41. :show-border="true"
  42. settings-cache-key="pms-device-maintenance-alarm-detail">
  43. <ZmTableColumn
  44. :label="t('iotDevice.code')"
  45. align="center"
  46. prop="deviceCode"
  47. v-if="false" />
  48. <ZmTableColumn
  49. :label="t('iotDevice.name')"
  50. align="center"
  51. prop="deviceName"
  52. v-if="false" />
  53. <ZmTableColumn :label="t('mainPlan.MaintItems')" align="center" prop="name" />
  54. <ZmTableColumn
  55. :label="t('operationFillForm.sumTime')"
  56. align="center"
  57. prop="totalRunTime">
  58. <template #default="{ row }">
  59. {{ row.totalRunTime ?? row.tempTotalRunTime }}
  60. </template>
  61. </ZmTableColumn>
  62. <ZmTableColumn
  63. :label="t('operationFillForm.sumKil')"
  64. align="center"
  65. prop="totalMileage">
  66. <template #default="{ row }">
  67. {{ row.totalMileage ?? row.tempTotalMileage }}
  68. </template>
  69. </ZmTableColumn>
  70. <!-- 时间分组列 -->
  71. <ZmTableColumn v-if="showTimeColumns" label="保养时长" align="center">
  72. <ZmTableColumn
  73. :label="t('mainPlan.lastMaintenanceOperationTime')"
  74. align="center"
  75. prop="lastRunningTime" />
  76. <ZmTableColumn
  77. :label="t('mainPlan.RunTimeCycle')"
  78. align="center"
  79. prop="nextRunningTime" />
  80. <ZmTableColumn :label="t('mainPlan.nextMaintTime')" align="center" prop="timePeriod">
  81. <template #default="{ row }">
  82. <span :class="{ 'negative-value': isNegative(row.timePeriod) }">
  83. {{ row.timePeriod }}
  84. </span>
  85. </template>
  86. </ZmTableColumn>
  87. </ZmTableColumn>
  88. <!-- 里程分组列 -->
  89. <ZmTableColumn v-if="showMileageColumns" label="保养里程" align="center">
  90. <ZmTableColumn
  91. :label="t('mainPlan.lastMaintenanceMileage')"
  92. align="center"
  93. prop="lastRunningKilometers" />
  94. <ZmTableColumn
  95. :label="t('mainPlan.operatingMileageCycle')"
  96. align="center"
  97. prop="nextRunningKilometers" />
  98. <ZmTableColumn
  99. :label="t('mainPlan.nextMaintKil')"
  100. align="center"
  101. prop="kilometerCycle">
  102. <template #default="{ row }">
  103. <span :class="{ 'negative-value': isNegative(row.kilometerCycle) }">
  104. {{ row.kilometerCycle }}
  105. </span>
  106. </template>
  107. </ZmTableColumn>
  108. </ZmTableColumn>
  109. <!-- 日期分组列 -->
  110. <ZmTableColumn v-if="showNaturalDateColumns" label="保养日期" align="center">
  111. <ZmTableColumn
  112. :label="t('mainPlan.lastMaintenanceNaturalDate')"
  113. align="center"
  114. prop="lastNaturalDate">
  115. <template #default="scope">
  116. <el-date-picker
  117. v-model="scope.row.lastNaturalDate"
  118. type="date"
  119. placeholder="选择日期"
  120. format="YYYY-MM-DD"
  121. value-format="YYYY-MM-DD"
  122. style="width: 100%"
  123. :disabled="true" />
  124. </template>
  125. </ZmTableColumn>
  126. <ZmTableColumn
  127. :label="t('mainPlan.NaturalDailyCycle')"
  128. align="center"
  129. prop="nextNaturalDate" />
  130. <ZmTableColumn
  131. :label="t('mainPlan.nextMaintDate')"
  132. align="center"
  133. prop="naturalDatePeriod" />
  134. </ZmTableColumn>
  135. </ZmTable>
  136. </div>
  137. <Pagination
  138. class="mr-4"
  139. :total="total"
  140. v-model:page="queryParams.pageNo"
  141. v-model:limit="queryParams.pageSize"
  142. @pagination="handlePagination" />
  143. </section>
  144. </div>
  145. </Dialog>
  146. </template>
  147. <script setup lang="ts">
  148. import { DictDataVO } from '@/api/system/dict/dict.data'
  149. import dayjs from 'dayjs'
  150. import { IotMainWorkOrderBomApi } from '@/api/pms/iotmainworkorderbom'
  151. import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
  152. import { propTypes } from '@/utils/propTypes'
  153. import { useTableComponents } from '@/components/ZmTable/useTableComponents'
  154. const { t } = useI18n() // 国际化
  155. const { ZmTable, ZmTableColumn } = useTableComponents<IotMaintenanceBomVO>()
  156. const emit = defineEmits(['close']) // 定义 success 事件,用于操作成功后的回调
  157. const dialogVisible = ref(false) // 弹窗的是否展示
  158. const loading = ref(true) // 列表的加载中
  159. const queryFormRef = ref() // 搜索的表单
  160. const list = ref<IotMaintenanceBomVO[]>([]) // 列表的数据
  161. const total = ref(0) // 列表的总页数
  162. // 分页重置标志
  163. const shouldResetPagination = ref(false)
  164. // 添加外部传入的设备信息
  165. const externalDeviceInfo = ref(null)
  166. const dialogWidth = 'min(1200px, 92vw)'
  167. const queryParams = reactive({
  168. pageNo: 1,
  169. pageSize: 10,
  170. workOrderId: undefined,
  171. planId: undefined,
  172. deviceId: undefined
  173. })
  174. const props = defineProps({
  175. flag: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired
  176. })
  177. const selectedRow = ref(null)
  178. // 处理单选逻辑
  179. const selectRow = (row) => {
  180. selectedRow.value = selectedRow.value?.id === row.id ? null : row
  181. emit('choose', row)
  182. dialogVisible.value = false
  183. }
  184. // 分页事件处理
  185. const handlePagination = () => {
  186. console.log('分页变化,当前页:', queryParams.pageNo)
  187. }
  188. // 分页计算属性
  189. const paginatedList = computed(() => {
  190. const start = (queryParams.pageNo - 1) * queryParams.pageSize
  191. const end = start + queryParams.pageSize
  192. return list.value.slice(start, end)
  193. })
  194. const open = async (id?: number, flag?: string, deviceInfo?: any) => {
  195. // 重置分页参数
  196. queryParams.pageNo = 1
  197. queryParams.pageSize = 10
  198. list.value = [] // 清空列表避免显示旧数据
  199. total.value = 0
  200. await nextTick() // 确保DOM更新完成
  201. if (deviceInfo) {
  202. externalDeviceInfo.value = deviceInfo
  203. queryParams.deviceId = deviceInfo.deviceId // 如果需要的话
  204. }
  205. if ('workOrder' === flag) {
  206. // 加载保养工单 BOM
  207. queryParams.workOrderId = id
  208. queryParams.planId = undefined
  209. await getWorkOrderList()
  210. } else if ('plan' === flag) {
  211. queryParams.planId = id
  212. queryParams.workOrderId = undefined
  213. await getPlanList()
  214. }
  215. dialogVisible.value = true
  216. }
  217. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  218. const getWorkOrderList = async () => {
  219. loading.value = true
  220. try {
  221. const data = await IotMainWorkOrderBomApi.getWorkOrderBOMs(queryParams)
  222. // 格式化日期字段
  223. data.forEach((item) => {
  224. if (item.lastNaturalDate) {
  225. // 将时间戳转换为 YYYY-MM-DD 格式
  226. item.lastNaturalDate = dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
  227. } else {
  228. // 处理空值情况
  229. item.lastNaturalDate = ''
  230. }
  231. // 计算下次保养运行时长 H
  232. item.timePeriod = calculateTimePeriod(item)
  233. // 计算下次保养公里数 KM
  234. item.kilometerCycle = calculateKiloPeriod(item)
  235. // 计算下次保养日期
  236. item.naturalDatePeriod = calculateNextNaturalDate(item)
  237. })
  238. list.value = data
  239. total.value = data.total
  240. } finally {
  241. loading.value = false
  242. }
  243. }
  244. const getPlanList = async () => {
  245. loading.value = true
  246. try {
  247. const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams)
  248. // 格式化日期字段
  249. data.forEach((item) => {
  250. if (item.lastNaturalDate) {
  251. // 将时间戳转换为 YYYY-MM-DD 格式
  252. item.lastNaturalDate = dayjs(item.lastNaturalDate).format('YYYY-MM-DD')
  253. } else {
  254. // 处理空值情况
  255. item.lastNaturalDate = ''
  256. }
  257. // 计算下次保养运行时长 H
  258. item.timePeriod = calculateTimePeriod(item)
  259. // 计算下次保养公里数 KM
  260. item.kilometerCycle = calculateKiloPeriod(item)
  261. // 计算下次保养日期
  262. item.naturalDatePeriod = calculateNextNaturalDate(item)
  263. })
  264. list.value = data
  265. total.value = data.length
  266. } finally {
  267. loading.value = false
  268. }
  269. }
  270. // 添加设备信息计算属性
  271. const deviceInfo = computed(() => {
  272. // 优先使用外部传入的设备信息
  273. if (externalDeviceInfo.value) {
  274. return externalDeviceInfo.value
  275. }
  276. if (list.value.length > 0) {
  277. const firstRecord = list.value[0]
  278. return {
  279. deviceCode: firstRecord.deviceCode,
  280. deviceName: firstRecord.deviceName,
  281. model: firstRecord.model // 确保列表数据中也有 model
  282. }
  283. }
  284. return null
  285. })
  286. const handleClose = () => {
  287. // 重置状态避免多个弹窗出现
  288. dialogVisible.value = false
  289. loading.value = false
  290. list.value = []
  291. total.value = 0
  292. queryParams.pageNo = 1
  293. queryParams.pageSize = 10
  294. // 通知父组件弹窗已关闭
  295. emit('close')
  296. }
  297. // 计算 距离下次保养运行时长 H
  298. const calculateTimePeriod = (item: IotMaintenanceBomVO) => {
  299. if (item.runningTimeRule === 0) {
  300. const totalRunVal = item.totalRunTime ?? item.tempTotalRunTime
  301. const next = Number(item.nextRunningTime) || 0
  302. const totalRun = totalRunVal != null ? Number(totalRunVal) : 0
  303. const lastRun = Number(item.lastRunningTime) || 0
  304. const result = next - (totalRun - lastRun)
  305. return Number(result.toFixed(2))
  306. }
  307. return typeof item.timePeriod === 'number' ? Number(item.timePeriod.toFixed(2)) : item.timePeriod
  308. }
  309. // 计算 距离下次保养公里数 KM
  310. const calculateKiloPeriod = (item: IotMaintenanceBomVO) => {
  311. if (item.mileageRule === 0) {
  312. const totalRunVal = item.totalMileage ?? item.tempTotalMileage
  313. const next = Number(item.nextRunningKilometers) || 0
  314. const totalRun = totalRunVal != null ? Number(totalRunVal) : 0
  315. const lastRun = Number(item.lastRunningKilometers) || 0
  316. const result = next - (totalRun - lastRun)
  317. return Number(result.toFixed(2))
  318. }
  319. return typeof item.kilometerCycle === 'number'
  320. ? Number(item.kilometerCycle.toFixed(2))
  321. : item.kilometerCycle
  322. }
  323. // 计算下次保养日期
  324. const calculateNextNaturalDate = (item: IotMaintenanceBomVO): string => {
  325. if (item.naturalDateRule !== 0 || !item.lastNaturalDate || !item.nextNaturalDate) {
  326. return '-'
  327. }
  328. return dayjs(item.lastNaturalDate).add(item.nextNaturalDate, 'day').format('YYYY-MM-DD')
  329. }
  330. // 计算属性:控制时间相关列的显示
  331. const showTimeColumns = computed(() => {
  332. return paginatedList.value.some((item) => item.runningTimeRule === 0)
  333. })
  334. // 计算属性:控制里程相关列的显示
  335. const showMileageColumns = computed(() => {
  336. return paginatedList.value.some((item) => item.mileageRule === 0)
  337. })
  338. // 计算属性:自然日期相关列的显示
  339. const showNaturalDateColumns = computed(() => {
  340. return paginatedList.value.some((item) => item.naturalDateRule === 0)
  341. })
  342. /** 搜索按钮操作 */
  343. const handleQuery = () => {
  344. queryParams.pageNo = 1
  345. // getList()
  346. }
  347. const choose = (row: DictDataVO) => {
  348. emit('choose', row)
  349. dialogVisible.value = false
  350. }
  351. /** 重置按钮操作 */
  352. const resetQuery = () => {
  353. queryFormRef.value.resetFields()
  354. handleQuery()
  355. }
  356. // 判断是否为负数的辅助函数
  357. const isNegative = (value: any): boolean => {
  358. if (value === null || value === undefined || value === '') return false
  359. const num = Number(value)
  360. return !isNaN(num) && num < 0
  361. }
  362. </script>
  363. <style lang="scss" scoped>
  364. @media (width <= 768px) {
  365. .dialog-title__subtitle {
  366. display: none;
  367. }
  368. .device-info-grid {
  369. grid-template-columns: minmax(0, 1fr);
  370. padding: 12px;
  371. }
  372. .maintenance-section .table-container {
  373. padding-right: 12px;
  374. padding-left: 12px;
  375. }
  376. }
  377. .device-info-card {
  378. display: flex;
  379. padding: 16px;
  380. margin-bottom: 16px;
  381. background-color: #f8f9fa;
  382. border-radius: 8px;
  383. box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
  384. flex-wrap: wrap;
  385. gap: 24px; /* 项间距 */
  386. .info-item {
  387. display: flex;
  388. align-items: center;
  389. .info-label {
  390. margin-right: 8px;
  391. font-weight: 600;
  392. color: #606266;
  393. white-space: nowrap;
  394. }
  395. .info-value {
  396. min-width: 200px;
  397. padding: 4px 12px;
  398. font-weight: 500;
  399. color: #303133;
  400. background: #fff;
  401. border: 1px solid #ebeef5;
  402. border-radius: 4px;
  403. }
  404. }
  405. }
  406. /* 负数值样式 */
  407. .negative-value {
  408. font-weight: 600;
  409. color: #f56c6c;
  410. }
  411. .dialog-title {
  412. display: flex;
  413. align-items: center;
  414. min-width: 0;
  415. }
  416. .dialog-title__icon {
  417. display: flex;
  418. align-items: center;
  419. justify-content: center;
  420. width: 38px;
  421. height: 38px;
  422. margin-right: 12px;
  423. font-size: 18px;
  424. color: var(--el-color-primary);
  425. background: var(--el-color-primary-light-9);
  426. border: 1px solid var(--el-color-primary-light-7);
  427. border-radius: 8px;
  428. }
  429. .dialog-title__text {
  430. font-size: 17px;
  431. font-weight: 600;
  432. line-height: 24px;
  433. color: var(--el-text-color-primary);
  434. }
  435. .dialog-title__subtitle {
  436. margin-top: 1px;
  437. font-size: 12px;
  438. line-height: 18px;
  439. color: var(--el-text-color-secondary);
  440. }
  441. .dialog-content {
  442. display: flex;
  443. flex-direction: column;
  444. gap: 14px;
  445. }
  446. .device-info-section,
  447. .maintenance-section {
  448. overflow: hidden;
  449. border: 1px solid var(--el-border-color-lighter);
  450. border-radius: 8px;
  451. }
  452. .section-heading {
  453. display: flex;
  454. align-items: center;
  455. height: 44px;
  456. padding: 0 16px;
  457. font-size: 15px;
  458. font-weight: 600;
  459. color: var(--el-text-color-primary);
  460. background: var(--el-fill-color-extra-light);
  461. border-bottom: 1px solid var(--el-border-color-lighter);
  462. }
  463. .section-heading span {
  464. width: 4px;
  465. height: 17px;
  466. margin-right: 9px;
  467. background: var(--el-color-primary);
  468. border-radius: 2px;
  469. }
  470. .device-info-grid {
  471. display: grid;
  472. grid-template-columns: repeat(3, minmax(0, 1fr));
  473. gap: 12px;
  474. padding: 14px 16px;
  475. }
  476. .device-info-grid .info-item {
  477. display: flex;
  478. align-items: center;
  479. min-width: 0;
  480. padding: 10px 12px;
  481. background: var(--el-fill-color-extra-light);
  482. border: 1px solid var(--el-border-color-lighter);
  483. border-radius: 6px;
  484. }
  485. .device-info-grid .info-label {
  486. flex: 0 0 auto;
  487. margin-right: 10px;
  488. color: var(--el-text-color-secondary);
  489. white-space: nowrap;
  490. }
  491. .device-info-grid .info-value {
  492. min-width: 0;
  493. overflow: hidden;
  494. font-weight: 500;
  495. color: var(--el-text-color-primary);
  496. text-overflow: ellipsis;
  497. white-space: nowrap;
  498. }
  499. .maintenance-section .table-container {
  500. padding: 14px 16px 0;
  501. }
  502. /* 设备信息卡片样式 */
  503. </style>