IotMainWorkOrderAdd.vue 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. <template>
  2. <ContentWrap v-loading="formLoading">
  3. <el-form
  4. ref="formRef"
  5. :model="formData"
  6. :rules="formRules"
  7. v-loading="formLoading"
  8. style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
  9. label-width="130px"
  10. >
  11. <div class="base-expandable-content">
  12. <el-row>
  13. <el-col :span="8">
  14. <el-form-item :label="t('bomList.name')" prop="name">
  15. <!-- <el-input type="text" v-model="formData.name" />-->
  16. <lang-input v-model="formData.name" />
  17. </el-form-item>
  18. </el-col>
  19. <el-col :span="8">
  20. <el-form-item :label="t('mainPlan.MaintenanceMethod')" prop="type">
  21. <el-select v-model="formData.outsourcingFlag" :placeholder="t('faultForm.choose')" clearable>
  22. <el-option
  23. v-for="dict in getIntDictOptions(DICT_TYPE.PMS_ORDER_PROCESS_MODE)"
  24. :key="dict.value"
  25. :label="dict.label"
  26. :value="dict.value"
  27. />
  28. </el-select>
  29. </el-form-item>
  30. <!--
  31. <el-form-item label="工单编号" prop="orderNumber">
  32. <el-input type="text" v-model="formData.orderNumber" disabled/>
  33. </el-form-item>
  34. -->
  35. </el-col>
  36. <el-col :span="8">
  37. <el-form-item :label="t('mainPlan.MaintenanceCost')" prop="cost">
  38. <el-input
  39. v-model="formData.cost"
  40. placeholder="根据物料消耗自动生成"
  41. disabled
  42. />
  43. </el-form-item>
  44. <!--
  45. <el-form-item label="责任人" prop="devicePersons">
  46. <el-input type="text" v-model="formData.devicePersons" disabled/>
  47. </el-form-item>
  48. <el-form-item label="责任人" prop="responsiblePerson">
  49. <el-select v-model="formData.responsiblePerson" filterable clearable style="width: 100%" disabled>
  50. <el-option
  51. v-for="item in deptUsers"
  52. :key="item.id"
  53. :label="item.nickname"
  54. :value="item.id"
  55. />
  56. </el-select>
  57. </el-form-item>
  58. -->
  59. </el-col>
  60. <el-col :span="8">
  61. <el-form-item :label="t('fault.start')" prop="actualStartTime">
  62. <el-date-picker
  63. style="width: 150%"
  64. v-model="formData.actualStartTime"
  65. type="datetime"
  66. value-format="x"
  67. :placeholder="t('fault.start')"
  68. />
  69. </el-form-item>
  70. </el-col>
  71. <el-col :span="8">
  72. <el-form-item :label="t('fault.end')" prop="actualEndTime">
  73. <el-date-picker
  74. style="width: 150%"
  75. v-model="formData.actualEndTime"
  76. type="datetime"
  77. value-format="x"
  78. :placeholder="t('fault.end')"
  79. />
  80. </el-form-item>
  81. </el-col>
  82. <el-col :span="8">
  83. <el-form-item :label="t('mainPlan.otherCost')" prop="otherCost">
  84. <el-input
  85. v-model="formData.otherCost"
  86. @input="handleInput(formData.otherCost, 'otherCost')"
  87. placeholder="其他费用"
  88. />
  89. </el-form-item>
  90. </el-col>
  91. <el-col :span="24">
  92. <el-form-item :label="t('faultForm.remark')" prop="remark">
  93. <el-input v-model="formData.remark" type="textarea" :placeholder="t('faultForm.rHolder')" />
  94. </el-form-item>
  95. </el-col>
  96. </el-row>
  97. </div>
  98. </el-form>
  99. </ContentWrap>
  100. <ContentWrap>
  101. <ContentWrap>
  102. <!-- 搜索工作栏 -->
  103. <el-form
  104. class="-mb-15px"
  105. :model="queryParams"
  106. ref="queryFormRef"
  107. :inline="true"
  108. label-width="68px"
  109. >
  110. <el-form-item>
  111. <el-button @click="openForm" type="warning">
  112. <Icon icon="ep:plus" class="mr-5px" />
  113. {{ t('maintain.added') }}</el-button>
  114. </el-form-item>
  115. </el-form>
  116. </ContentWrap>
  117. <!-- 列表 -->
  118. <ContentWrap>
  119. <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
  120. <!-- 添加序号列 -->
  121. <el-table-column
  122. type="index"
  123. :label="t('maintain.serial')"
  124. width="70"
  125. align="center"
  126. />
  127. <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" v-if="false"/>
  128. <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />
  129. <el-table-column :label="t('iotDevice.name')" align="center" prop="deviceName" />
  130. <el-table-column :label="t('operationFillForm.sumTime')" align="center" prop="totalRunTime" :formatter="erpPriceTableColumnFormatter"/>
  131. <el-table-column :label="t('operationFillForm.sumKil')" align="center" prop="totalMileage" :formatter="erpPriceTableColumnFormatter"/>
  132. <el-table-column :label="t('mainPlan.MaintItems')" align="center" prop="name" />
  133. <!--
  134. <el-table-column label="运行里程" key="mileageRule" width="80">
  135. <template #default="scope">
  136. <el-switch
  137. v-model="scope.row.mileageRule"
  138. :active-value="0"
  139. :inactive-value="1"
  140. />
  141. </template>
  142. </el-table-column>
  143. <el-table-column label="运行时间" key="runningTimeRule" width="80">
  144. <template #default="scope">
  145. <el-switch
  146. v-model="scope.row.runningTimeRule"
  147. :active-value="0"
  148. :inactive-value="1"
  149. />
  150. </template>
  151. </el-table-column>
  152. <el-table-column label="自然日期" key="naturalDateRule" width="80">
  153. <template #default="scope">
  154. <el-switch
  155. v-model="scope.row.naturalDateRule"
  156. :active-value="0"
  157. :inactive-value="1"
  158. />
  159. </template>
  160. </el-table-column>
  161. -->
  162. <el-table-column :label="t('iotMaintain.numberOfMaterials')" align="center" width="100">
  163. <!--
  164. <template #default="scope">
  165. {{ hasMaterial(scope.row.bomNodeId) ? '是' : '否' }}
  166. </template>
  167. -->
  168. <template #default="scope">
  169. {{ getMaterialCount(scope.row.bomNodeId) }}
  170. </template>
  171. </el-table-column>
  172. <el-table-column :label="t('iotMaintain.operation')" align="center" min-width="120px">
  173. <template #default="scope">
  174. <div style="display: flex; justify-content: center; align-items: center; width: 100%">
  175. <div>
  176. <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
  177. <el-button
  178. style="vertical-align: middle"
  179. link
  180. type="danger"
  181. @click="handleDelete(scope.row.deviceId+'-'+scope.row.bomNodeId)"
  182. >
  183. {{ t('iotMaintain.remove') }}
  184. </el-button>
  185. </div>
  186. <!-- 新增配置按钮
  187. <div style="margin-left: 12px">
  188. <el-button
  189. link
  190. type="primary"
  191. @click="openConfigDialog(scope.row)"
  192. >
  193. 配置
  194. </el-button>
  195. </div> -->
  196. <div style="margin-left: 12px">
  197. <el-button
  198. link
  199. type="primary"
  200. @click="openMaterialForm(scope.row)"
  201. >
  202. {{ t('iotMaintain.selectMaterials') }}
  203. </el-button>
  204. </div>
  205. <div style="margin-left: 12px">
  206. <el-button
  207. link
  208. type="primary"
  209. @click="handleView(scope.row.bomNodeId)"
  210. >
  211. {{ t('bomList.materialDetail') }}
  212. </el-button>
  213. </div>
  214. </div>
  215. </template>
  216. </el-table-column>
  217. </el-table>
  218. </ContentWrap>
  219. <!-- 选择的物料列表 -->
  220. <ContentWrap>
  221. <el-table v-loading="false" :data="materialList" :stripe="true" :show-overflow-tooltip="true" v-if="false">
  222. <el-table-column :label="t('bomList.bomNode')" align="center" prop="bomNodeId" />
  223. <el-table-column :label="t('workOrderMaterial.materialCode')" align="center" prop="materialCode" />
  224. <el-table-column :label="t('workOrderMaterial.materialName')" align="center" prop="materialName" />
  225. <el-table-column :label="t('workOrderMaterial.unit')" align="center" prop="unit" />
  226. <el-table-column :label="t('workOrderMaterial.unitPrice')" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter"/>
  227. <el-table-column :label="t('workOrderMaterial.ConsumptionQuantity')" align="center" prop="quantity" />
  228. <el-table-column :label="t('workOrderMaterial.total')" align="center" prop="totalInventoryQuantity" />
  229. </el-table>
  230. </ContentWrap>
  231. </ContentWrap>
  232. <ContentWrap>
  233. <el-form>
  234. <el-form-item style="float: right">
  235. <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('common.save')}}</el-button>
  236. <el-button @click="close">{{t('common.cancel')}}</el-button>
  237. </el-form-item>
  238. </el-form>
  239. </ContentWrap>
  240. <MainPlanDeviceList ref="deviceFormRef" @choose="deviceChoose" />
  241. <!-- 新增配置对话框 -->
  242. <el-dialog
  243. v-model="configDialog.visible"
  244. :title="`设备 ${configDialog.current?.deviceCode+'-'+configDialog.current?.name} 保养配置`"
  245. width="600px"
  246. >
  247. <!-- 使用header插槽自定义标题 -->
  248. <template #header>
  249. <span>设备 <strong>{{ configDialog.current?.deviceCode }}-{{ configDialog.current?.name }}</strong> 保养项配置</span>
  250. </template>
  251. <el-form :model="configDialog.form" label-width="200px" :rules="configFormRules" ref="configFormRef">
  252. <!-- 里程配置 -->
  253. <el-form-item
  254. v-if="configDialog.current?.mileageRule === 0"
  255. label="上次保养里程数(KM)"
  256. prop="lastRunningKilometers"
  257. >
  258. <el-input-number
  259. v-model="configDialog.form.lastRunningKilometers"
  260. :precision="2"
  261. :min="0"
  262. controls-position="right"
  263. :disabled="true"
  264. />
  265. </el-form-item>
  266. <!-- 推迟公里数 -->
  267. <el-form-item
  268. v-if="configDialog.current?.mileageRule === 0"
  269. label="推迟公里数(KM)"
  270. prop="delayKilometers"
  271. >
  272. <el-input-number
  273. v-model="configDialog.form.delayKilometers"
  274. :precision="2"
  275. :min="0"
  276. controls-position="right"
  277. />
  278. </el-form-item>
  279. <!-- 运行时间配置 -->
  280. <el-form-item
  281. v-if="configDialog.current?.runningTimeRule === 0"
  282. label="上次保养运行时间(H)"
  283. prop="lastRunningTime"
  284. >
  285. <el-input-number
  286. v-model="configDialog.form.lastRunningTime"
  287. :precision="1"
  288. :min="0"
  289. controls-position="right"
  290. :disabled="true"
  291. />
  292. </el-form-item>
  293. <!-- 推迟时长 -->
  294. <el-form-item
  295. v-if="configDialog.current?.runningTimeRule === 0"
  296. label="推迟时长(H)"
  297. prop="delayDuration"
  298. >
  299. <el-input-number
  300. v-model="configDialog.form.delayDuration"
  301. :precision="2"
  302. :min="0"
  303. controls-position="right"
  304. />
  305. </el-form-item>
  306. <!-- 自然日期配置 -->
  307. <el-form-item
  308. v-if="configDialog.current?.naturalDateRule === 0"
  309. label="上次保养自然日期(D)"
  310. prop="lastNaturalDate"
  311. >
  312. <el-date-picker
  313. v-model="configDialog.form.lastNaturalDate"
  314. type="date"
  315. placeholder="选择日期"
  316. format="YYYY-MM-DD"
  317. value-format="YYYY-MM-DD"
  318. :disabled="true"
  319. />
  320. </el-form-item>
  321. <!-- 推迟自然日期 -->
  322. <el-form-item
  323. v-if="configDialog.current?.naturalDateRule === 0"
  324. label="推迟自然日期(D)"
  325. prop="delayNaturalDate"
  326. >
  327. <el-input-number
  328. v-model="configDialog.form.delayNaturalDate"
  329. :precision="2"
  330. :min="0"
  331. controls-position="right"
  332. />
  333. </el-form-item>
  334. <!-- 保养规则周期值 + 提前量 -->
  335. <el-form-item
  336. v-if="configDialog.current?.mileageRule === 0"
  337. label="运行里程周期(KM)"
  338. prop="nextRunningKilometers"
  339. >
  340. <el-input-number
  341. v-model="configDialog.form.nextRunningKilometers"
  342. :precision="2"
  343. :min="0"
  344. controls-position="right"
  345. :disabled="true"
  346. />
  347. </el-form-item>
  348. <el-form-item
  349. v-if="configDialog.current?.mileageRule === 0"
  350. label="运行里程周期-提前量(KM)"
  351. prop="kiloCycleLead"
  352. >
  353. <el-input-number
  354. v-model="configDialog.form.kiloCycleLead"
  355. :precision="2"
  356. :min="0"
  357. controls-position="right"
  358. :disabled="true"
  359. />
  360. </el-form-item>
  361. <el-form-item
  362. v-if="configDialog.current?.runningTimeRule === 0"
  363. label="运行时间周期(H)"
  364. prop="nextRunningTime"
  365. >
  366. <el-input-number
  367. v-model="configDialog.form.nextRunningTime"
  368. :precision="1"
  369. :min="0"
  370. controls-position="right"
  371. :disabled="true"
  372. />
  373. </el-form-item>
  374. <el-form-item
  375. v-if="configDialog.current?.runningTimeRule === 0"
  376. label="运行时间周期-提前量(H)"
  377. prop="timePeriodLead"
  378. >
  379. <el-input-number
  380. v-model="configDialog.form.timePeriodLead"
  381. :precision="1"
  382. :min="0"
  383. controls-position="right"
  384. :disabled="true"
  385. />
  386. </el-form-item>
  387. <el-form-item
  388. v-if="configDialog.current?.naturalDateRule === 0"
  389. label="自然日周期(D)"
  390. prop="nextNaturalDate"
  391. >
  392. <el-input-number
  393. v-model="configDialog.form.nextNaturalDate"
  394. :min="0"
  395. controls-position="right"
  396. :disabled="true"
  397. />
  398. </el-form-item>
  399. <el-form-item
  400. v-if="configDialog.current?.naturalDateRule === 0"
  401. label="自然日周期-提前量(D)"
  402. prop="naturalDatePeriodLead"
  403. >
  404. <el-input-number
  405. v-model="configDialog.form.naturalDatePeriodLead"
  406. :min="0"
  407. controls-position="right"
  408. :disabled="true"
  409. />
  410. </el-form-item>
  411. </el-form>
  412. <template #footer>
  413. <el-button @click="configDialog.visible = false">取消</el-button>
  414. <el-button type="primary" @click="saveConfig">保存</el-button>
  415. </template>
  416. </el-dialog>
  417. <!-- 表单弹窗:添加/修改 -->
  418. <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
  419. <!-- 抽屉组件 展示已经选择的物料 并编辑物料消耗 -->
  420. <MaterialListDrawer
  421. :model-value="drawerVisible"
  422. @update:model-value="val => drawerVisible = val"
  423. :node-id="currentBomNodeId"
  424. :materials="materialList.filter(item => item.bomNodeId === currentBomNodeId)"
  425. />
  426. </template>
  427. <script setup lang="ts">
  428. import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
  429. import * as UserApi from '@/api/system/user'
  430. import { useUserStore } from '@/store/modules/user'
  431. import { ref } from 'vue'
  432. import { IotMaintenanceBomApi, IotMaintenanceBomVO } from '@/api/pms/iotmaintenancebom'
  433. import { IotMainWorkOrderBomApi, IotMainWorkOrderBomVO } from '@/api/pms/iotmainworkorderbom'
  434. import { IotMainWorkOrderBomMaterialApi, IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
  435. import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
  436. import { useTagsViewStore } from '@/store/modules/tagsView'
  437. import {CACHE_KEY, useCache} from "@/hooks/web/useCache";
  438. import MainPlanDeviceList from "@/views/pms/maintenance/MainPlanDeviceList.vue";
  439. import * as DeptApi from "@/api/system/dept";
  440. import {erpPriceTableColumnFormatter} from "@/utils";
  441. import dayjs from 'dayjs'
  442. import MaterialListDrawer from "@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue";
  443. import WorkOrderMaterial from "@/views/pms/iotmainworkorder/WorkOrderMaterial.vue";
  444. import { IotDevicePersonApi, IotDevicePersonVO } from '@/api/pms/iotdeviceperson'
  445. import {DICT_TYPE, getIntDictOptions, getStrDictOptions} from "@/utils/dict";
  446. /** 保养计划 表单 */
  447. defineOptions({ name: 'IotMainWorkOrderAdd' })
  448. const { t } = useI18n() // 国际化
  449. const message = useMessage() // 消息弹窗
  450. const { delView } = useTagsViewStore() // 视图操作
  451. const { currentRoute, push } = useRouter()
  452. const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
  453. const dept = ref() // 当前登录人所属部门对象
  454. const configFormRef = ref() // 配置弹出框对象
  455. const bomNodeId = ref() // 最新的bomNodeId
  456. const dialogTitle = ref('') // 弹窗的标题
  457. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  458. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  459. const deviceLabel = ref('') // 表单的类型:create - 新增;update - 修改
  460. const drawerVisible = ref<boolean>(false)
  461. const currentBomNodeId = ref() // 当前选中的bom节点
  462. const showDrawer = ref()
  463. const list = ref<IotMainWorkOrderBomVO[]>([]) // 保养工单bom关联列表的数据
  464. const materialList = ref<IotMainWorkOrderBomMaterialVO[]>([]) // 保养工单bom关联物料列表
  465. const deviceIds = ref<number[]>([]) // 已经选择的设备id数组
  466. const { params, name } = useRoute() // 查询参数
  467. const id = params.id
  468. const devicePersonsMap = ref<Map<number, Set<string>>>(new Map()) // 存储设备-责任人映射
  469. const formData = ref({
  470. id: undefined,
  471. deptId: undefined,
  472. name: '',
  473. orderNumber: undefined,
  474. responsiblePerson: undefined,
  475. actualStartTime: undefined,
  476. actualEndTime: undefined,
  477. cost: undefined,
  478. otherCost: undefined,
  479. outsourcingFlag: 0,
  480. remark: undefined,
  481. status: undefined,
  482. devicePersons: '',
  483. })
  484. /** 获取当前所有设备ID集合 */
  485. const getCurrentDeviceIds = (): number[] => {
  486. return [...new Set(list.value.map(item => item.deviceId))]
  487. }
  488. const formRules = reactive({
  489. name: [{ required: true, message: '工单名称不能为空', trigger: 'blur' }],
  490. responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
  491. })
  492. const formRef = ref() // 表单 Ref
  493. interface MaterialFormExpose {
  494. open: (deptId: number, bomNodeId: number, row: any, type: string) => void
  495. }
  496. const materialFormRef = ref<MaterialFormExpose>();
  497. // 新增配置相关状态
  498. const configDialog = reactive({
  499. visible: false,
  500. current: null as IotMaintenanceBomVO | null,
  501. form: {
  502. lastRunningKilometers: 0,
  503. delayKilometers: 0,
  504. lastRunningTime: 0,
  505. delayDuration: 0,
  506. lastNaturalDate: '',
  507. delayNaturalDate: 0,
  508. // 保养规则 周期
  509. nextRunningKilometers: 0,
  510. nextRunningTime: 0,
  511. nextNaturalDate: 0,
  512. // 提前量
  513. kiloCycleLead: 0,
  514. timePeriodLead: 0,
  515. naturalDatePeriodLead: 0
  516. }
  517. })
  518. // 监听物料列表变化
  519. watch(
  520. () => materialList.value,
  521. () => {
  522. calculateTotalCost()
  523. },
  524. { deep: true }
  525. )
  526. // 计算保养金额
  527. const calculateTotalCost = () => {
  528. // 物料总金额 = ∑(单价 * 消耗数量)
  529. const materialTotal = materialList.value.reduce((sum, item) => {
  530. const price = Number(item.unitPrice) || 0
  531. const quantity = Number(item.quantity) || 0
  532. return sum + (price * quantity)
  533. }, 0)
  534. // 保养 = 物料总金额
  535. formData.value.cost = (materialTotal).toFixed(2)
  536. }
  537. // 打开配置对话框
  538. const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
  539. configDialog.current = row
  540. // 处理日期初始化(核心修改)
  541. let initialDate = ''
  542. if (row.lastNaturalDate) {
  543. // 如果已有值:时间戳 -> 日期字符串
  544. initialDate = dayjs(row.lastNaturalDate).format('YYYY-MM-DD')
  545. } else {
  546. // 如果无值:设置默认值避免1970问题
  547. initialDate = ''
  548. }
  549. configDialog.form = {
  550. lastRunningKilometers: row.lastRunningKilometers || 0,
  551. delayKilometers: row.delayKilometers || 0,
  552. lastRunningTime: row.lastRunningTime || 0,
  553. delayDuration: row.delayDuration || 0,
  554. lastNaturalDate: initialDate,
  555. delayNaturalDate: row.delayNaturalDate || 0,
  556. // 保养规则 周期值
  557. nextRunningKilometers: row.nextRunningKilometers || 0,
  558. nextRunningTime: row.nextRunningTime || 0,
  559. nextNaturalDate: row.nextNaturalDate || 0,
  560. // 提前量
  561. kiloCycleLead: row.kiloCycleLead || 0,
  562. timePeriodLead: row.timePeriodLead || 0,
  563. naturalDatePeriodLead: row.naturalDatePeriodLead || 0
  564. }
  565. configDialog.visible = true
  566. }
  567. // const materialFormRef = ref()
  568. const openMaterialForm = (row: any) => {
  569. bomNodeId.value = row.bomNodeId;
  570. console.log('这是一个对象:', row.bomNodeId)
  571. const type = 'maintenance'
  572. materialFormRef.value.open(formData.value.deptId, bomNodeId.value, row, type)
  573. }
  574. // 获取指定bomNodeId的物料数量
  575. const getMaterialCount = (bomNodeId: number) => {
  576. return materialList.value.filter(item => item.bomNodeId === bomNodeId).length
  577. }
  578. const selectChoose = (selectedMaterial) => {
  579. selectedMaterial.bomNodeId = bomNodeId.value
  580. // 关联 bomNodeId
  581. const processedMaterials = selectedMaterial.map(material => ({
  582. ...material,
  583. bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
  584. }));
  585. // 避免重复添加
  586. processedMaterials.forEach(newMaterial => {
  587. // 检查是否已存在相同 bomNodeId + materialCode 的条目
  588. const isExist = materialList.value.some(item =>
  589. item.bomNodeId === bomNodeId.value &&
  590. item.materialCode === newMaterial.materialCode
  591. );
  592. if (!isExist) {
  593. materialList.value.push(newMaterial);
  594. }
  595. });
  596. }
  597. const deviceChoose = async(selectedDevices) => {
  598. const newIds = selectedDevices.map(device => device.id)
  599. deviceIds.value = [...new Set([...deviceIds.value, ...newIds])]
  600. const params = {
  601. deviceIds: deviceIds.value.join(',') // 明确传递数组参数
  602. }
  603. queryParams.deviceIds = JSON.parse(JSON.stringify(params.deviceIds))
  604. queryParams.bomFlag = 'b'
  605. // 根据选择的设备筛选出设备关系的分类BOM中与保养相关的节点项
  606. const res = await IotDeviceApi.deviceAssociateBomList(queryParams)
  607. const rawData = res || []
  608. if(rawData.length === 0){
  609. message.error('选择的设备不存在待保养BOM项')
  610. }
  611. if (!Array.isArray(rawData)) {
  612. console.error('接口返回数据结构异常:', rawData)
  613. return
  614. }
  615. // 转换数据结构(根据你的接口定义调整)
  616. const newItems = rawData.map(device => ({
  617. assetClass: device.assetClass,
  618. deviceCode: device.deviceCode,
  619. deviceName: device.deviceName,
  620. deviceStatus: device.deviceStatus,
  621. deptName: device.deptName,
  622. name: device.name,
  623. code: device.code,
  624. assetProperty: device.assetProperty,
  625. remark: null, // 初始化备注
  626. deviceId: device.id, // 移除操作需要
  627. bomNodeId: device.bomNodeId,
  628. totalRunTime: device.totalRunTime,
  629. totalMileage: device.totalMileage,
  630. nextRunningKilometers: 0,
  631. nextRunningTime: 0,
  632. nextNaturalDate: 0,
  633. lastNaturalDate: null, // 初始化为null而不是0
  634. // 保养规则 提前量
  635. kiloCycleLead: 0,
  636. timePeriodLead: 0,
  637. naturalDatePeriodLead: 0
  638. }))
  639. // 获取选择的设备相关的id数组
  640. newItems.forEach(item => {
  641. deviceIds.value.push(item.deviceId)
  642. })
  643. // 合并到现有列表(去重)
  644. newItems.forEach(item => {
  645. const exists = list.value.some(
  646. existing => (existing.deviceId === item.deviceId && existing.bomNodeId === item.bomNodeId)
  647. )
  648. if (!exists) {
  649. list.value.push(item)
  650. }
  651. })
  652. // 新增完设备后 查询现有设备-bom明细中 所有设备配置的负责人姓名 逗号分隔
  653. // await getDevicePersons();
  654. }
  655. /** 查看已经选择的物料 并编辑 */
  656. const handleView = (nodeId) => {
  657. currentBomNodeId.value = nodeId
  658. drawerVisible.value = true
  659. // showDrawer.value.openDrawer()
  660. console.log('当前bom节点:', currentBomNodeId.value)
  661. }
  662. const deviceFormRef = ref<InstanceType<typeof MainPlanDeviceList>>()
  663. const openForm = () => {
  664. deviceFormRef.value?.open();
  665. }
  666. const hasMaterial = (bomNodeId: number) => {
  667. return materialList.value.some(item => item.bomNodeId === bomNodeId)
  668. }
  669. // 保存配置
  670. const saveConfig = () => {
  671. (configFormRef.value as any).validate((valid: boolean) => {
  672. if (!valid) return
  673. if (!configDialog.current) return
  674. // 动态校验逻辑
  675. const requiredFields = []
  676. if (configDialog.current.mileageRule === 0) {
  677. requiredFields.push('nextRunningKilometers', 'kiloCycleLead')
  678. }
  679. if (configDialog.current.runningTimeRule === 0) {
  680. requiredFields.push('nextRunningTime', 'timePeriodLead')
  681. }
  682. if (configDialog.current.naturalDateRule === 0) {
  683. requiredFields.push('nextNaturalDate', 'naturalDatePeriodLead')
  684. }
  685. const missingFields = requiredFields.filter(field =>
  686. !configDialog.form[field as keyof typeof configDialog.form]
  687. )
  688. if (missingFields.length > 0) {
  689. message.error('请填写所有必填项')
  690. return
  691. }
  692. // 强制校验逻辑
  693. if (configDialog.current.naturalDateRule === 0) {
  694. if (!configDialog.form.lastNaturalDate) {
  695. message.error('必须选择 上次保养自然日期')
  696. return
  697. }
  698. // 验证日期有效性
  699. const dateValue = dayjs(configDialog.form.lastNaturalDate)
  700. if (!dateValue.isValid()) {
  701. message.error('日期格式不正确')
  702. return
  703. }
  704. }
  705. // 转换逻辑(关键修改)
  706. const finalDate = configDialog.form.lastNaturalDate
  707. ? dayjs(configDialog.form.lastNaturalDate).valueOf()
  708. : null // 改为null而不是0
  709. // 更新当前行的数据
  710. Object.assign(configDialog.current, {
  711. ...configDialog.form,
  712. lastNaturalDate: finalDate
  713. })
  714. configDialog.visible = false
  715. })
  716. }
  717. const handleInput = (value, obj) => {
  718. // 1. 过滤非法字符(只允许数字和小数点)
  719. let filtered = value.replace(/[^\d.]/g, '')
  720. // 2. 处理多个小数点的情况
  721. filtered = filtered.replace(/\.{2,}/g, '.')
  722. // 3. 限制小数点后最多两位
  723. let decimalParts = filtered.split('.')
  724. if (decimalParts.length > 1) {
  725. decimalParts = decimalParts.slice(0, 2)
  726. filtered = decimalParts.join('.')
  727. }
  728. // 4. 处理以小数点开头的情况(自动补0)
  729. if (filtered.startsWith('.')) {
  730. filtered = '0' + filtered
  731. }
  732. // 5. 更新绑定值(同时处理连续输入多个0的情况)
  733. formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
  734. }
  735. const queryParams = reactive({
  736. deviceIds: undefined,
  737. workOrderId: id,
  738. bomFlag: 'b'
  739. })
  740. const close = () => {
  741. delView(unref(currentRoute))
  742. push({ name: 'IotMainWorkOrder', params:{}})
  743. }
  744. /** 更新责任人显示 */
  745. function updateDevicePersonsDisplay() {
  746. const allNames = new Set<string>()
  747. devicePersonsMap.value.forEach(names => {
  748. names.forEach(name => allNames.add(name))
  749. })
  750. formData.value.devicePersons = Array.from(allNames).join(', ')
  751. }
  752. /**
  753. * 根据选择的设备查询所有设备关联的 责任人姓名 逗号分隔
  754. */
  755. async function getDevicePersons() {
  756. // 获取当前已经选择的设备ID集合
  757. const existDeviceIds = getCurrentDeviceIds()
  758. if (existDeviceIds.length === 0) {
  759. formData.value.devicePersons = ''
  760. return
  761. }
  762. try {
  763. // 调用接口获取数据
  764. const params = {
  765. deviceIds: existDeviceIds.join(',') // 明确传递数组参数
  766. }
  767. const res = await IotDevicePersonApi.getPersonsByDeviceIds(params)
  768. const personsData = res || []
  769. // 清空旧数据
  770. devicePersonsMap.value.clear()
  771. // 处理接口数据
  772. personsData.forEach((item: { deviceId: number; personName: string }) => {
  773. if (!devicePersonsMap.value.has(item.deviceId)) {
  774. devicePersonsMap.value.set(item.deviceId, new Set())
  775. }
  776. devicePersonsMap.value.get(item.deviceId)?.add(item.personName)
  777. })
  778. // 生成展示字符串
  779. updateDevicePersonsDisplay()
  780. } catch (error) {
  781. console.error('获取设备责任人失败:', error)
  782. }
  783. }
  784. /** 提交表单 */
  785. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  786. const submitForm = async () => {
  787. // 校验表单
  788. await formRef.value.validate()
  789. // 校验表格数据
  790. const isValid = validateTableData()
  791. if (!isValid) return
  792. // 新增:校验物料选择
  793. const hasMaterial = validateMaterialSelection();
  794. if (!hasMaterial) return;
  795. // 提交请求
  796. formLoading.value = true
  797. try {
  798. const data = {
  799. mainWorkOrder: formData.value,
  800. mainWorkOrderBom: list.value,
  801. mainWorkOrderMaterials: materialList.value
  802. }
  803. await IotMainWorkOrderApi.addWorkOrder(data)
  804. message.success(t('common.createSuccess'))
  805. close()
  806. // 发送操作成功的事件
  807. emit('success')
  808. } finally {
  809. formLoading.value = false
  810. }
  811. }
  812. // 新增表单校验规则
  813. const configFormRules = reactive({
  814. nextRunningKilometers: [{
  815. required: true,
  816. message: '里程周期必须填写',
  817. trigger: 'blur'
  818. }],
  819. kiloCycleLead: [{
  820. required: true,
  821. message: '提前量必须填写',
  822. trigger: 'blur'
  823. }],
  824. nextRunningTime: [{
  825. required: true,
  826. message: '时间周期必须填写',
  827. trigger: 'blur'
  828. }],
  829. timePeriodLead: [{
  830. required: true,
  831. message: '提前量必须填写',
  832. trigger: 'blur'
  833. }],
  834. nextNaturalDate: [{
  835. required: true,
  836. message: '自然日周期必须填写',
  837. trigger: 'blur'
  838. }],
  839. naturalDatePeriodLead: [{
  840. required: true,
  841. message: '提前量必须填写',
  842. trigger: 'blur'
  843. }]
  844. })
  845. /** 校验表格数据 */
  846. const validateTableData = (): boolean => {
  847. let isValid = true
  848. const errorMessages: string[] = []
  849. if (list.value.length === 0) {
  850. errorMessages.push('请至少添加一条设备保养明细')
  851. isValid = false
  852. // 直接返回无需后续校验
  853. message.error('请至少添加一条设备保养明细')
  854. return isValid
  855. }
  856. return isValid
  857. }
  858. /** 检查每个保养项是否都有物料 */
  859. const validateMaterialSelection = (): boolean => {
  860. let isValid = true;
  861. const errorMessages: string[] = [];
  862. // 遍历所有保养项
  863. list.value.forEach(item => {
  864. const hasMaterial = materialList.value.some(
  865. material => material.bomNodeId === item.bomNodeId
  866. );
  867. if (!hasMaterial) {
  868. isValid = false;
  869. errorMessages.push(
  870. `设备 ${item.deviceCode}-${item.deviceName} 的保养项【${item.name}】未添加物料`
  871. );
  872. }
  873. });
  874. // 显示错误信息
  875. if (!isValid) {
  876. let displayMessage;
  877. if (errorMessages.length > 3) {
  878. // 超过3条错误时显示前3条+总结
  879. const firstThree = errorMessages.slice(0, 3);
  880. displayMessage = firstThree.join('<br/>') +
  881. `<br/>...等共 ${errorMessages.length} 个保养项未添加物料`;
  882. } else {
  883. // 3条及以内直接显示全部
  884. displayMessage = errorMessages.join('<br/>');
  885. }
  886. message.error({
  887. message: displayMessage,
  888. dangerouslyUseHTMLString: true
  889. });
  890. }
  891. return isValid;
  892. };
  893. /** 重置表单 */
  894. const resetForm = () => {
  895. formData.value = {
  896. id: undefined,
  897. status: undefined,
  898. description: undefined,
  899. pic: undefined,
  900. remark: undefined,
  901. deviceName: undefined,
  902. processInstanceId: undefined,
  903. auditStatus: undefined,
  904. deptId: undefined
  905. }
  906. formRef.value?.resetFields()
  907. }
  908. onMounted(async () => {
  909. materialList.value = []
  910. const deptId = useUserStore().getUser.deptId
  911. // 查询当前登录人所属部门名称
  912. dept.value = await DeptApi.getDept(deptId)
  913. formData.value.name = dept.value.name + ' - 保养工单'
  914. deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
  915. formData.value.deptId = deptId
  916. try {
  917. formType.value = 'create'
  918. const { wsCache } = useCache()
  919. const userInfo = wsCache.get(CACHE_KEY.USER)
  920. // 手工新增保养工单 责任人为当前登录人
  921. formData.value.responsiblePerson = userInfo.user.id;
  922. } catch (error) {
  923. console.error('数据加载失败:', error)
  924. message.error('数据加载失败,请重试')
  925. }
  926. })
  927. const handleDelete = async (str: string) => {
  928. try {
  929. const [deviceIdStr, bomNodeId] = str.split('-')
  930. const deviceId = parseInt(deviceIdStr)
  931. // 删除列表项
  932. const index = list.value.findIndex((item) => (item.deviceId+'-'+item.bomNodeId) === str)
  933. if (index !== -1) {
  934. list.value.splice(index, 1)
  935. deviceIds.value = []
  936. }
  937. // 更新设备ID列表(需要检查是否还有该设备的其他项)
  938. const hasOtherItems = list.value.some(item => item.deviceId === deviceId)
  939. if (!hasOtherItems) {
  940. deviceIds.value = deviceIds.value.filter(id => id !== deviceId)
  941. devicePersonsMap.value.delete(deviceId) // 移除对应设备的责任人
  942. updateDevicePersonsDisplay() // 立即更新显示
  943. }
  944. } catch (error) {
  945. console.error('移除失败:', error)
  946. message.error('移除失败')
  947. }
  948. }
  949. </script>
  950. <style scoped>
  951. .base-expandable-content {
  952. overflow: hidden; /* 隐藏溢出的内容 */
  953. transition: max-height 0.3s ease; /* 平滑过渡效果 */
  954. }
  955. :deep(.el-input-number .el-input__inner) {
  956. text-align: left !important;
  957. padding-left: 10px; /* 保持左侧间距 */
  958. }
  959. /* 分组容器样式 */
  960. .form-group {
  961. position: relative;
  962. border: 1px solid #dcdfe6;
  963. border-radius: 4px;
  964. padding: 20px 15px 10px;
  965. margin-bottom: 18px;
  966. transition: border-color 0.2s;
  967. }
  968. /* 分组标题样式 */
  969. .group-title {
  970. position: absolute;
  971. top: -10px;
  972. left: 20px;
  973. background: white;
  974. padding: 0 8px;
  975. color: #606266;
  976. font-size: 12px;
  977. font-weight: 500;
  978. }
  979. </style>