IotMainWorkOrderAdd.vue 34 KB

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