IotMaintenanceSearch.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. <template>
  2. <el-row :gutter="20">
  3. <!-- 左侧部门树 -->
  4. <el-col :span="4" :xs="24">
  5. <ContentWrap class="h-1/1" v-if="treeShow">
  6. <DeptTree @node-click="handleDeptNodeClick" />
  7. </ContentWrap>
  8. </el-col>
  9. <el-col :span="contentSpan" :xs="24">
  10. <ContentWrap>
  11. <!-- 搜索工作栏 -->
  12. <el-form
  13. class="-mb-15px"
  14. :model="queryParams"
  15. ref="queryFormRef"
  16. :inline="true"
  17. label-width="68px"
  18. >
  19. <el-form-item :label="t('iotDevice.code')" prop="deviceCode" style="margin-left: 25px">
  20. <el-input
  21. v-model="queryParams.deviceCode"
  22. :placeholder="t('iotDevice.codeHolder')"
  23. clearable
  24. @keyup.enter="handleQuery"
  25. class="!w-200px"
  26. />
  27. </el-form-item>
  28. <el-form-item :label="t('iotDevice.name')" prop="deviceName">
  29. <el-input
  30. v-model="queryParams.deviceName"
  31. :placeholder="t('iotDevice.nameHolder')"
  32. clearable
  33. @keyup.enter="handleQuery"
  34. class="!w-200px"
  35. />
  36. </el-form-item>
  37. <el-form-item>
  38. <el-button @click="handleQuery"
  39. ><Icon icon="ep:search" class="mr-5px" />
  40. {{ t('file.search') }}</el-button
  41. >
  42. <el-button @click="resetQuery"
  43. ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('file.reset') }}</el-button
  44. >
  45. <el-button
  46. type="success"
  47. plain
  48. @click="handleExport"
  49. :loading="exportLoading"
  50. v-hasPermi="['rq:iot-device:export']"
  51. >
  52. <Icon icon="ep:download" class="mr-5px" /> 导出
  53. </el-button>
  54. </el-form-item>
  55. </el-form>
  56. </ContentWrap>
  57. <!-- 列表 -->
  58. <ContentWrap>
  59. <el-table v-loading="loading" :data="list" :stripe="true" style="width: 100%" @header-dragend="handleHeaderDragEnd"
  60. :show-overflow-tooltip="false" :header-cell-style="tableHeaderStyle" ref="tableRef">
  61. <el-table-column :label="t('monitor.serial')" width="60" align="center" fixed="left">
  62. <template #default="scope">
  63. {{ scope.$index + 1 }}
  64. </template>
  65. </el-table-column>
  66. <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" min-width="120"
  67. :show-overflow-tooltip="true" fixed="left"/>
  68. <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" :min-width="deviceNameMinWidth"
  69. :width="columnWidths['deviceName']" :show-overflow-tooltip="true"
  70. :max-width="deviceNameMaxWidth" fixed="left">
  71. <template #default="scope">
  72. <el-link :underline="false" type="primary" @click="handleDetail(scope.row.id)">
  73. {{ scope.row.deviceName }}
  74. </el-link>
  75. </template>
  76. </el-table-column>
  77. <el-table-column :label="t('bomList.serviceDue')" align="center" min-width="120">
  78. <template #default="scope">
  79. <template v-if="hasMaintenancePlan(scope.row.mainDistance)">
  80. <span :class="getDistanceClass(scope.row.mainDistance)">
  81. {{ scope.row.mainDistance }}
  82. </span>
  83. </template>
  84. <span v-else>无保养计划</span>
  85. </template>
  86. </el-table-column>
  87. <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" min-width="120"/>
  88. <el-table-column :label="t('monitor.status')" align="center" prop="deviceStatus" min-width="100">
  89. <template #default="scope">
  90. <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="scope.row.deviceStatus" />
  91. </template>
  92. </el-table-column>
  93. <!-- 动态分组列 - 修改后结构 -->
  94. <template v-for="group in dynamicColumns" :key="group.header">
  95. <el-table-column :label="group.header" align="center" :header-cell-class-name="`group-header-${group.groupIndex}`" min-width="auto">
  96. <!-- 循环分组内的所有类型 -->
  97. <template v-for="type in group.types" :key="type">
  98. <!-- 类型标签列 -->
  99. <el-table-column :label="getTypeLabel(type)" align="center" min-width="auto">
  100. <!-- 根据类型渲染对应字段 -->
  101. <!-- 1. 时间类型字段 -->
  102. <template v-if="type === 'time'">
  103. <el-table-column
  104. :label="t('mainPlan.lastMaintenanceOperationTime')"
  105. :width="getDynamicColumnWidth(group, type, 'lastRunningTime')"
  106. :show-overflow-tooltip="true"
  107. align="center">
  108. <template #default="scope">
  109. {{ scope.row.groupBomMap?.[group.header]?.lastRunningTime ?? '-' }}
  110. </template>
  111. </el-table-column>
  112. <el-table-column
  113. :label="t('mainPlan.nextMaintenanceH')"
  114. :width="getDynamicColumnWidth(group, type, 'nextMaintenanceH')"
  115. :show-overflow-tooltip="true"
  116. align="center">
  117. <template #default="scope">
  118. {{ (scope.row.groupBomMap?.[group.header]?.lastRunningTime || 0) +
  119. (scope.row.groupBomMap?.[group.header]?.nextRunningTime || 0) }}
  120. </template>
  121. </el-table-column>
  122. <el-table-column
  123. :label="t('operationFillForm.sumTime')"
  124. :width="getDynamicColumnWidth(group, type, 'totalRunTime')"
  125. :show-overflow-tooltip="true"
  126. align="center">
  127. <template #default="scope">
  128. {{ scope.row.groupBomMap?.[group.header]?.totalRunTime ?? '-' }}
  129. </template>
  130. </el-table-column>
  131. <el-table-column
  132. :label="t('mainPlan.remainH')"
  133. :width="getDynamicColumnWidth(group, type, 'remainH')"
  134. :show-overflow-tooltip="true"
  135. align="center">
  136. <template #default="scope">
  137. <span :class="getNewRemainingClass(scope.row.groupBomMap?.[group.header], type)">
  138. {{ getNewRemainingValue(scope.row.groupBomMap?.[group.header], type) }}
  139. </span>
  140. </template>
  141. </el-table-column>
  142. </template>
  143. <!-- 2. 日期类型字段 -->
  144. <template v-else-if="type === 'date'">
  145. <el-table-column
  146. :label="t('mainPlan.lastMaintenanceNaturalDate')"
  147. :width="getDynamicColumnWidth(group, type, 'lastNaturalDate')"
  148. :show-overflow-tooltip="true"
  149. align="center">
  150. <template #default="scope">
  151. {{ formatDate(scope.row.groupBomMap?.[group.header]?.lastNaturalDate) }}
  152. </template>
  153. </el-table-column>
  154. <el-table-column
  155. :label="t('mainPlan.nextMaintDate')"
  156. :width="getDynamicColumnWidth(group, type, 'nextMaintDate')"
  157. :show-overflow-tooltip="true"
  158. align="center">
  159. <template #default="scope">
  160. {{
  161. formatDate(
  162. (scope.row.groupBomMap?.[group.header]?.lastNaturalDate || 0) +
  163. (scope.row.groupBomMap?.[group.header]?.nextNaturalDate || 0) * 86400000
  164. )
  165. }}
  166. </template>
  167. </el-table-column>
  168. <el-table-column
  169. :label="t('main.currentDate')"
  170. :width="getDynamicColumnWidth(group, type, 'currentDate')"
  171. :show-overflow-tooltip="true"
  172. align="center">
  173. <template #default>
  174. {{ currentDateFormatted }}
  175. </template>
  176. </el-table-column>
  177. <el-table-column
  178. :label="t('mainPlan.remainDay')"
  179. :width="getDynamicColumnWidth(group, type, 'remainDay')"
  180. :show-overflow-tooltip="true"
  181. align="center">
  182. <template #default="scope">
  183. <span :class="getNewRemainingClass(scope.row.groupBomMap?.[group.header], type)">
  184. {{ getNewRemainingValue(scope.row.groupBomMap?.[group.header], type) }}
  185. </span>
  186. </template>
  187. </el-table-column>
  188. </template>
  189. <!-- 3. 里程类型字段 -->
  190. <template v-else-if="type === 'mileage'">
  191. <el-table-column
  192. :label="t('mainPlan.lastMaintenanceMileage')"
  193. :width="getDynamicColumnWidth(group, type, 'lastRunningKilometers')"
  194. :show-overflow-tooltip="true"
  195. align="center">
  196. <template #default="scope">
  197. {{ scope.row.groupBomMap?.[group.header]?.lastRunningKilometers ?? '-' }}
  198. </template>
  199. </el-table-column>
  200. <el-table-column
  201. :label="t('mainPlan.nextMaintenanceKm')"
  202. :width="getDynamicColumnWidth(group, type, 'nextMaintenanceKm')"
  203. :show-overflow-tooltip="true"
  204. align="center">
  205. <template #default="scope">
  206. {{ (scope.row.groupBomMap?.[group.header]?.lastRunningKilometers || 0) +
  207. (scope.row.groupBomMap?.[group.header]?.nextRunningKilometers || 0) }}
  208. </template>
  209. </el-table-column>
  210. <el-table-column
  211. :label="t('operationFillForm.sumKil')"
  212. :width="getDynamicColumnWidth(group, type, 'totalMileage')"
  213. :show-overflow-tooltip="true"
  214. align="center">
  215. <template #default="scope">
  216. {{ scope.row.groupBomMap?.[group.header]?.totalMileage ?? '-' }}
  217. </template>
  218. </el-table-column>
  219. <el-table-column
  220. :label="t('mainPlan.remainKm')"
  221. :width="getDynamicColumnWidth(group, type, 'remainKm')"
  222. :show-overflow-tooltip="true"
  223. align="center">
  224. <template #default="scope">
  225. <span :class="getNewRemainingClass(scope.row.groupBomMap?.[group.header], type)">
  226. {{ getNewRemainingValue(scope.row.groupBomMap?.[group.header], type) }}
  227. </span>
  228. </template>
  229. </el-table-column>
  230. </template>
  231. </el-table-column>
  232. </template>
  233. </el-table-column>
  234. </template>
  235. <el-table-column :label="t('monitor.operation')" align="center" min-width="120" fixed="right">
  236. <template #default="scope">
  237. <el-button
  238. link
  239. type="primary"
  240. @click="openBomForm(scope.row)"
  241. v-hasPermi="['rq:iot-device:query']"
  242. v-if="hasMaintenancePlan(scope.row.mainDistance)"
  243. >
  244. {{ t('monitor.details') }}
  245. </el-button>
  246. </template>
  247. </el-table-column>
  248. </el-table>
  249. <!-- 分页 -->
  250. <Pagination
  251. :total="total"
  252. v-model:page="queryParams.pageNo"
  253. v-model:limit="queryParams.pageSize"
  254. @pagination="getList"
  255. />
  256. </ContentWrap>
  257. </el-col>
  258. </el-row>
  259. <DeviceAlarmBomList ref="modelFormRef" :flag = "flag" />
  260. </template>
  261. <script setup lang="ts">
  262. import download from '@/utils/download'
  263. import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
  264. import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
  265. import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
  266. import DeptTree from '@/views/system/user/DeptTree.vue'
  267. import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
  268. import DeviceAlarmBomList from "@/views/pms/iotmainworkorder/DeviceAlarmBomList.vue";
  269. import dayjs from 'dayjs' // 引入 dayjs 库
  270. /** 保养查询 列表 保养项分组 */
  271. defineOptions({ name: 'IotMaintenanceSearch' })
  272. const message = useMessage() // 消息弹窗
  273. const { t } = useI18n() // 国际化
  274. const { push } = useRouter() // 路由跳转
  275. const modelFormRef = ref()
  276. const loading = ref(true) // 列表的加载中
  277. const list = ref<IotDeviceVO[]>([]) // 列表的数据
  278. const total = ref(0) // 列表的总页数
  279. const queryParams = reactive({
  280. pageNo: 1,
  281. pageSize: 10,
  282. deviceCode: undefined,
  283. deviceName: undefined,
  284. brand: undefined,
  285. model: undefined,
  286. deptId: undefined,
  287. deviceStatus: undefined,
  288. assetProperty: undefined,
  289. picUrl: undefined,
  290. remark: undefined,
  291. manufacturerId: undefined,
  292. supplierId: undefined,
  293. manDate: [],
  294. nameplate: undefined,
  295. expires: undefined,
  296. plPrice: undefined,
  297. plDate: [],
  298. plYear: undefined,
  299. plStartDate: [],
  300. plMonthed: undefined,
  301. plAmounted: undefined,
  302. remainAmount: undefined,
  303. infoId: undefined,
  304. infoType: undefined,
  305. infoName: undefined,
  306. infoRemark: undefined,
  307. infoUrl: undefined,
  308. templateJson: undefined,
  309. creator: undefined,
  310. setFlag: ''
  311. })
  312. const dynamicColumns = ref<any[]>([]) // 动态列配置
  313. const queryFormRef = ref() // 搜索的表单
  314. const flag = ref() // 查询保养计划或保养工单的标识
  315. const exportLoading = ref(false) // 导出的加载中
  316. const contentSpan = ref(20)
  317. const treeShow = ref(true)
  318. const currentDateFormatted = ref(dayjs().format('YYYY-MM-DD')) // 当前日期格式化
  319. const tableRef = ref(); // 表格引用
  320. const columnWidths = ref<Record<string, string>>({}); // 列宽存储
  321. // 设备名称列的最小和最大宽度
  322. const deviceNameMinWidth = 150; // 最小宽度
  323. const deviceNameMaxWidth = 400; // 最大宽度,防止过长破坏布局
  324. // 表头样式
  325. const tableHeaderStyle = () => {
  326. return {
  327. borderBottom: '2px solid #606266',
  328. borderRight: '2px solid #606266',
  329. backgroundColor: '#f5f7fa',
  330. fontWeight: 'bold',
  331. whiteSpace: 'nowrap'
  332. }
  333. }
  334. // 处理列宽拖动事件
  335. const handleHeaderDragEnd = (newWidth: number, oldWidth: number, column: any) => {
  336. if (column.property) {
  337. columnWidths.value[column.property] = `${newWidth}px`;
  338. } else if (column.label) {
  339. // 处理没有property的列(如动态列)
  340. columnWidths.value[column.label] = `${newWidth}px`;
  341. }
  342. }
  343. /** 计算文本宽度辅助函数 */
  344. const getTextWidth = (text: string, fontSize = 14): number => {
  345. const span = document.createElement('span');
  346. span.style.visibility = 'hidden';
  347. span.style.position = 'absolute';
  348. span.style.whiteSpace = 'nowrap';
  349. span.style.fontSize = `${fontSize}px`;
  350. span.innerText = text;
  351. document.body.appendChild(span);
  352. const width = span.offsetWidth;
  353. document.body.removeChild(span);
  354. return width;
  355. }
  356. /** 计算列宽主函数 */
  357. const calculateColumnWidths = () => {
  358. const MIN_WIDTH = 100; // 最小列宽
  359. const PADDING = 20; // 内边距
  360. const MAX_DEVICE_NAME_WIDTH = deviceNameMaxWidth; // 设备名称最大宽度
  361. // 静态列配置
  362. const staticColumns = [
  363. { prop: 'serial', label: t('monitor.serial') },
  364. { prop: 'deviceCode', label: t('iotDevice.code') },
  365. { prop: 'deviceName', label: t('iotDevice.name') },
  366. { prop: 'mainDistance', label: t('bomList.serviceDue') },
  367. { prop: 'deptName', label: t('iotDevice.dept') },
  368. { prop: 'deviceStatus', label: t('monitor.status') },
  369. { prop: 'operation', label: t('monitor.operation') }
  370. ];
  371. const newWidths: Record<string, string> = {};
  372. // 计算静态列宽度
  373. staticColumns.forEach(col => {
  374. const headerWidth = getTextWidth(col.label);
  375. let maxContentWidth = 0;
  376. list.value.forEach(item => {
  377. let value = '';
  378. if (col.prop === 'serial') {
  379. value = (list.value.indexOf(item) + 1).toString();
  380. } else if (col.prop === 'deviceName') {
  381. // 设备名称特殊处理,考虑链接内容
  382. value = item.deviceName || '';
  383. } else {
  384. value = item[col.prop]?.toString() || '';
  385. }
  386. const contentWidth = getTextWidth(value);
  387. if (contentWidth > maxContentWidth) maxContentWidth = contentWidth;
  388. });
  389. // 对设备名称列应用最大宽度限制
  390. if (col.prop === 'deviceName') {
  391. maxContentWidth = Math.min(maxContentWidth, MAX_DEVICE_NAME_WIDTH);
  392. }
  393. const finalWidth = Math.max(headerWidth, maxContentWidth, MIN_WIDTH) + PADDING;
  394. newWidths[col.prop] = `${finalWidth}px`;
  395. });
  396. // 计算动态列宽度
  397. dynamicColumns.value.forEach((group, groupIndex) => {
  398. group.types.forEach(type => {
  399. const typeLabel = getTypeLabel(type);
  400. const typeHeaderWidth = getTextWidth(typeLabel);
  401. // 根据类型定义子列
  402. const subColumns = [];
  403. if (type === 'time') {
  404. subColumns.push(
  405. { key: 'lastRunningTime', label: t('mainPlan.lastMaintenanceOperationTime') },
  406. { key: 'nextMaintenanceH', label: t('mainPlan.nextMaintenanceH') },
  407. { key: 'totalRunTime', label: t('operationFillForm.sumTime') },
  408. { key: 'remainH', label: t('mainPlan.remainH') }
  409. );
  410. } else if (type === 'date') {
  411. subColumns.push(
  412. { key: 'lastNaturalDate', label: t('mainPlan.lastMaintenanceNaturalDate') },
  413. { key: 'nextMaintDate', label: t('mainPlan.nextMaintDate') },
  414. { key: 'currentDate', label: t('main.currentDate') },
  415. { key: 'remainDay', label: t('mainPlan.remainDay') }
  416. );
  417. } else if (type === 'mileage') {
  418. subColumns.push(
  419. { key: 'lastRunningKilometers', label: t('mainPlan.lastMaintenanceMileage') },
  420. { key: 'nextMaintenanceKm', label: t('mainPlan.nextMaintenanceKm') },
  421. { key: 'totalMileage', label: t('operationFillForm.sumKil') },
  422. { key: 'remainKm', label: t('mainPlan.remainKm') }
  423. );
  424. }
  425. // 计算每个子列的宽度
  426. subColumns.forEach(subCol => {
  427. const fullKey = `${group.header}-${type}-${subCol.key}`;
  428. const headerWidth = getTextWidth(subCol.label);
  429. let maxContentWidth = 0;
  430. list.value.forEach(item => {
  431. const groupData = item.groupBomMap?.[group.header];
  432. let value = '';
  433. if (subCol.key === 'currentDate') {
  434. value = currentDateFormatted.value;
  435. } else if (groupData) {
  436. // 特殊处理计算字段
  437. if (subCol.key === 'nextMaintenanceH') {
  438. value = ((groupData.lastRunningTime || 0) + (groupData.nextRunningTime || 0)).toString();
  439. }
  440. else if (subCol.key === 'remainH') {
  441. value = getNewRemainingValue(groupData, 'time').toString();
  442. }
  443. else if (subCol.key === 'nextMaintDate') {
  444. const nextDate = (groupData.lastNaturalDate || 0) +
  445. (groupData.nextNaturalDate || 0) * 86400000;
  446. value = formatDate(nextDate);
  447. }
  448. else if (subCol.key === 'remainDay') {
  449. value = getNewRemainingValue(groupData, 'date').toString();
  450. }
  451. else if (subCol.key === 'nextMaintenanceKm') {
  452. value = ((groupData.lastRunningKilometers || 0) +
  453. (groupData.nextRunningKilometers || 0)).toString();
  454. }
  455. else if (subCol.key === 'remainKm') {
  456. value = getNewRemainingValue(groupData, 'mileage').toString();
  457. }
  458. // 其他字段直接取值
  459. else {
  460. value = groupData[subCol.key]?.toString() || '';
  461. }
  462. } else {
  463. value = '-'; // 无数据时显示短横线
  464. }
  465. const contentWidth = getTextWidth(value);
  466. if (contentWidth > maxContentWidth) maxContentWidth = contentWidth;
  467. });
  468. const finalWidth = Math.max(headerWidth, maxContentWidth, MIN_WIDTH) + PADDING;
  469. newWidths[fullKey] = `${finalWidth}px`;
  470. });
  471. });
  472. });
  473. columnWidths.value = newWidths;
  474. }
  475. /** 获取动态列宽 */
  476. const getDynamicColumnWidth = (group: any, type: string, key: string) => {
  477. const fullKey = `${group.header}-${type}-${key}`;
  478. return columnWidths.value[fullKey] || 'auto';
  479. }
  480. /** 查询列表 */
  481. const getList = async () => {
  482. loading.value = true
  483. try {
  484. const data = await IotMainWorkOrderApi.maintenanceSearch(queryParams)
  485. list.value = data.list.map((item: any) => {
  486. // 构建分组映射
  487. const groupBomMap: Record<string, any> = {}
  488. if (item.groupBomDistances?.length > 0) {
  489. item.groupBomDistances.forEach((group: any) => {
  490. // 提取分组名称
  491. const groupName = group.name?.split('->')[0] || group.name
  492. groupBomMap[groupName] = group
  493. })
  494. }
  495. return { ...item, groupBomMap }
  496. })
  497. total.value = data.total
  498. // 生成动态列配置
  499. generateDynamicColumns(data.list)
  500. // 计算列宽
  501. nextTick(() => {
  502. calculateColumnWidths();
  503. tableRef.value?.doLayout(); // 重新布局表格
  504. });
  505. } finally {
  506. loading.value = false
  507. }
  508. }
  509. /** 生成动态列配置 */
  510. const generateDynamicColumns = (dataList: any[]) => {
  511. // 使用Map收集分组和类型:Map<分组名称, Set<类型>>
  512. const groupTypeMap = new Map<string, Set<string>>()
  513. dataList.forEach(item => {
  514. if (item.groupBomDistances?.length > 0) {
  515. item.groupBomDistances.forEach((group: any) => {
  516. const groupName = group.name?.split('->')[0] || group.name
  517. if (!groupName) return
  518. // 确定分组类型
  519. let type = ''
  520. if (group.lastRunningTime != null && group.nextRunningTime != null) {
  521. type = 'time'
  522. } else if (group.lastNaturalDate != null && group.nextNaturalDate != null) {
  523. type = 'date'
  524. } else if (group.lastRunningKilometers != null && group.nextRunningKilometers != null) {
  525. type = 'mileage'
  526. }
  527. if (!type) return
  528. if (!groupTypeMap.has(groupName)) {
  529. groupTypeMap.set(groupName, new Set())
  530. }
  531. groupTypeMap.get(groupName)?.add(type)
  532. })
  533. }
  534. })
  535. // 转换为需要的结构并按固定顺序排序类型
  536. dynamicColumns.value = Array.from(groupTypeMap.entries()).map(([header, typeSet], index) => ({
  537. header,
  538. types: Array.from(typeSet).sort((a, b) => {
  539. // 固定顺序:time -> date -> mileage
  540. const order = { 'time': 1, 'date': 2, 'mileage': 3 }
  541. return (order[a] || 4) - (order[b] || 5)
  542. }),
  543. groupIndex: index % 4 // 循环使用4种样式
  544. }))
  545. // 计算动态列宽度
  546. nextTick(calculateColumnWidths);
  547. }
  548. /** 获取类型标签 */
  549. const getTypeLabel = (type: string) => {
  550. const labels = {
  551. 'time': t('main.maintenanceRuntime'),
  552. 'date': t('main.maintenanceDate'),
  553. 'mileage': t('main.maintenanceMileage')
  554. }
  555. return labels[type] || type
  556. }
  557. /** 格式化日期 */
  558. const formatDate = (timestamp: number | null) => {
  559. if (!timestamp) return '-'
  560. const date = new Date(timestamp)
  561. return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
  562. }
  563. /** 新的剩余值计算方法 */
  564. const getNewRemainingValue = (group: any, type: string) => {
  565. if (!group) return '-'
  566. switch (type) {
  567. case 'time':
  568. // 剩余时长 = nextRunningTime - (totalRunTime - lastRunningTime)
  569. const totalRunTime = group.totalRunTime || 0
  570. const lastRunningTime = group.lastRunningTime || 0
  571. const nextRunningTime = group.nextRunningTime || 0
  572. return (nextRunningTime - (totalRunTime - lastRunningTime)).toFixed(2)
  573. case 'mileage':
  574. // 剩余里程 = nextRunningKilometers - (totalMileage - lastRunningKilometers)
  575. const totalMileage = group.totalMileage || 0
  576. const lastRunningKilometers = group.lastRunningKilometers || 0
  577. const nextRunningKilometers = group.nextRunningKilometers || 0
  578. return (nextRunningKilometers - (totalMileage - lastRunningKilometers)).toFixed(2)
  579. case 'date':
  580. // 计算剩余天数
  581. if (!group.lastNaturalDate || !group.nextNaturalDate) return '-'
  582. try {
  583. // 上次保养日期:将时间戳转换为 Day.js 对象
  584. const lastNaturalDate = dayjs(group.lastNaturalDate)
  585. // 计算下次保养日期
  586. const nextMaintenanceDate = lastNaturalDate.add(group.nextNaturalDate, 'day')
  587. // 计算剩余天数(当前日期到下次保养日期的天数差)
  588. return nextMaintenanceDate.diff(dayjs(), 'day')
  589. } catch (e) {
  590. console.error('日期计算错误:', e)
  591. return '-'
  592. }
  593. default:
  594. return '-'
  595. }
  596. }
  597. /** 获取剩余值 */
  598. const getRemainingValue = (group: any, type: string) => {
  599. if (!group) return '-'
  600. switch (type) {
  601. case 'time':
  602. return group.nextRunningTime ?? '-'
  603. case 'date':
  604. return group.nextNaturalDate ?? '-'
  605. case 'mileage':
  606. return group.nextRunningKilometers ?? '-'
  607. default:
  608. return '-'
  609. }
  610. }
  611. /** 新的剩余值样式 */
  612. const getNewRemainingClass = (group: any, type: string) => {
  613. const value = getNewRemainingValue(group, type)
  614. if (value === '-' || value === null) return ''
  615. const numValue = typeof value === 'string' ? parseFloat(value) : value
  616. return numValue < 0 ? 'negative-distance' :
  617. numValue > 0 ? 'positive-distance' : ''
  618. }
  619. /** 获取剩余值样式 */
  620. const getRemainingClass = (group: any, type: string) => {
  621. const value = getRemainingValue(group, type)
  622. if (value === '-' || value === null) return ''
  623. const numValue = typeof value === 'string' ? parseFloat(value) : value
  624. return numValue < 0 ? 'negative-distance' :
  625. numValue > 0 ? 'positive-distance' : ''
  626. }
  627. /** 判断是否有分组数据 */
  628. const hasGroupBomDistances = (row: any) => {
  629. return row.groupBomDistances?.length > 0
  630. }
  631. /** 处理部门被点击 */
  632. const handleDeptNodeClick = async (row) => {
  633. queryParams.deptId = row.id
  634. await getList()
  635. }
  636. /** 搜索按钮操作 */
  637. const handleQuery = () => {
  638. queryParams.pageNo = 1
  639. getList()
  640. }
  641. /** 重置按钮操作 */
  642. const resetQuery = () => {
  643. queryFormRef.value.resetFields()
  644. handleQuery()
  645. }
  646. const getDistanceClass = (distance: number | string | null) => {
  647. if (distance === null || distance === undefined) return '';
  648. // 如果是数字类型,直接处理
  649. if (typeof distance === 'number') {
  650. return distance < 0 ? 'negative-distance' :
  651. distance > 0 ? 'positive-distance' : '';
  652. }
  653. // 如果是字符串,提取数字部分
  654. if (typeof distance === 'string') {
  655. // 使用正则提取数字部分(包括负号、小数点和科学计数法)
  656. const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0];
  657. // 如果提取到数字部分,转换为数值
  658. if (numericPart) {
  659. const num = parseFloat(numericPart);
  660. return num < 0 ? 'negative-distance' :
  661. num > 0 ? 'positive-distance' : '';
  662. }
  663. }
  664. return '';
  665. };
  666. // 判断是否有保养计划
  667. const hasMaintenancePlan = (mainDistance: any) => {
  668. // 检查:非null、非undefined、非空字符串
  669. return mainDistance != null && mainDistance !== '';
  670. };
  671. const handleDetail = (id: number) => {
  672. push({ name: 'DeviceDetailInfo', params: { id } })
  673. }
  674. const drawerVisible = ref<boolean>(false)
  675. const showDrawer = ref()
  676. const openBomForm = async (row) => {
  677. if (row.workOrderId) {
  678. flag.value = 'workOrder';
  679. modelFormRef.value.open(row.workOrderId, flag.value, row.id)
  680. } else if (row.planId) {
  681. flag.value = 'plan';
  682. modelFormRef.value.open(row.planId, flag.value, row.id)
  683. }
  684. }
  685. /** 导出按钮操作 */
  686. const handleExport = async () => {
  687. try {
  688. // 导出的二次确认
  689. await message.exportConfirm()
  690. // 发起导出
  691. exportLoading.value = true
  692. const data = await IotDeviceApi.exportIotDevice(queryParams)
  693. download.excel(data, '设备台账.xls')
  694. } catch {
  695. } finally {
  696. exportLoading.value = false
  697. }
  698. }
  699. const { wsCache } = useCache()
  700. /** 初始化 **/
  701. onMounted(() => {
  702. getList()
  703. })
  704. </script>
  705. <style scoped>
  706. /* 正数样式 - 淡绿色 */
  707. .positive-distance {
  708. color: #67c23a; /* element-plus 成功色 */
  709. background-color: rgba(103, 194, 58, 0.1); /* 10% 透明度的淡绿色背景 */
  710. padding: 2px 8px;
  711. border-radius: 4px;
  712. display: inline-block;
  713. }
  714. /* 负数样式 - 淡红色 */
  715. .negative-distance {
  716. color: #f56c6c; /* element-plus 危险色 */
  717. background-color: rgba(245, 108, 108, 0.1); /* 10% 透明度的淡红色背景 */
  718. padding: 2px 8px;
  719. border-radius: 4px;
  720. display: inline-block;
  721. }
  722. /* 分组表头样式 - 使用::v-deep穿透 */
  723. :deep(.group-header-0) {
  724. background-color: #f0f9ff !important; /* 淡蓝色 */
  725. border-right: 2px solid #606266 !important;
  726. }
  727. :deep(.group-header-1) {
  728. background-color: #fdf6ec !important; /* 淡橙色 */
  729. border-right: 2px solid #606266 !important;
  730. }
  731. :deep(.group-header-2) {
  732. background-color: #f0f4ff !important; /* 淡紫色 */
  733. border-right: 2px solid #606266 !important;
  734. }
  735. :deep(.group-header-3) {
  736. background-color: #e6f7ff !important; /* 淡青色 */
  737. border-right: 2px solid #606266 !important;
  738. }
  739. /* 表头边框加深 */
  740. :deep(.el-table th) {
  741. border-bottom: 2px solid #606266 !important;
  742. border-right: 2px solid #606266 !important;
  743. }
  744. /* 内容不换行 */
  745. :deep(.el-table .cell) {
  746. white-space: nowrap !important;
  747. }
  748. /* 表头文字不换行 */
  749. :deep(.el-table__header .cell) {
  750. white-space: nowrap !important;
  751. overflow: visible !important;
  752. text-overflow: unset !important;
  753. }
  754. /* 列宽自适应调整 */
  755. :deep(.el-table) {
  756. table-layout: auto !important;
  757. }
  758. /* 分组列之间的分隔线 */
  759. :deep(.el-table__header .el-table__cell) {
  760. overflow: visible !important;
  761. border-right: 2px solid #606266 !important;
  762. }
  763. /* 最后列去除右边框 */
  764. :deep(.el-table__header .el-table__cell:last-child) {
  765. border-right: none !important;
  766. }
  767. /* 设备名称列特殊处理 */
  768. :deep(.el-table__body .device-name-cell .cell) {
  769. overflow: hidden;
  770. text-overflow: ellipsis;
  771. }
  772. </style>