create.vue 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067
  1. <template>
  2. <view class="page repair">
  3. <view class="segmented-header">
  4. <uni-segmented-control
  5. :current="currentTab"
  6. :values="tabTitles"
  7. :style-type="styleType"
  8. :active-color="activeColor"
  9. @clickItem="onClickTabItem"
  10. />
  11. </view>
  12. <scroll-view scroll-y="true" class="segmented-content">
  13. <!-- 工单信息 -->
  14. <view class="work-order-info" v-show="currentTab === 0">
  15. <work-order-form
  16. ref="workOrderFormRef"
  17. :form-data="maintain"
  18. :form-disabled="false"
  19. @chooseDevice="onChooseDevice"
  20. @chooseLocalSearch="onChooseLocalSearch"
  21. @chooseManufacturer="onSelectedManufacturer"
  22. @uploadImg="uploadImg"
  23. @deleteImg="deleteImg"
  24. @uploadOutFiles="uploadOutFiles"
  25. @deleteOutFiles="deleteOutFiles"
  26. />
  27. </view>
  28. <!-- 维修项列表 -->
  29. <view class="work-order-bom-list" v-show="currentTab === 1">
  30. <!-- 维修项列表操作栏 -->
  31. <uni-row
  32. class="list-header flex-row align-center justify-start"
  33. :gutter="10"
  34. >
  35. <!-- 选择维修项 -->
  36. <uni-col :span="9">
  37. <button
  38. class="list-header-item flex-row align-center justify-center"
  39. type="default"
  40. @click="onChooseRepair()"
  41. >
  42. <image
  43. src="~@/static/workOrder/xuanzhewuliao.png"
  44. mode="aspectFill"
  45. ></image>
  46. <text>
  47. {{ $t("operation.select")
  48. }}{{ $t("equipmentMaintenance.maintenanceItems") }}</text
  49. >
  50. </button>
  51. </uni-col>
  52. <!-- 新增维修项 -->
  53. <uni-col :span="9">
  54. <button
  55. class="list-header-item flex-row align-center justify-center"
  56. type="default"
  57. @click="onAddRepair()"
  58. >
  59. <image
  60. src="~@/static/workOrder/xinzengwuliao.png"
  61. mode="aspectFill"
  62. ></image>
  63. <text>
  64. {{ $t("operation.add")
  65. }}{{ $t("equipmentMaintenance.maintenanceItems") }}</text
  66. >
  67. </button>
  68. </uni-col>
  69. </uni-row>
  70. <view class="list-content">
  71. <work-order-boms
  72. :boms-data="maintainBom"
  73. :current-bom="currentBomData"
  74. @onSelectBom="onSelectBom"
  75. @deleteBom="ondeleteBom"
  76. @updateStatistics="updateBomStatisticsAndCost"
  77. ></work-order-boms>
  78. </view>
  79. </view>
  80. <!--物料列表 -->
  81. <view class="material-list" v-show="currentTab === 2">
  82. <!-- 物料列表操作栏 -->
  83. <uni-row
  84. class="list-header flex-row align-center justify-start"
  85. :gutter="10"
  86. >
  87. <!-- 选择物料 不存在维修项或维修项不消耗物料时禁用 -->
  88. <uni-col :span="8">
  89. <button
  90. class="list-header-item flex-row align-center justify-center"
  91. type="default"
  92. :disabled="!currentBomData || currentBomData.rule == '1'"
  93. @click="onMaterialChoose(currentBomData)"
  94. >
  95. <image
  96. src="~@/static/workOrder/xuanzhewuliao.png"
  97. mode="aspectFill"
  98. ></image>
  99. <text>{{ t("workOrder.selectMaterial") }}</text>
  100. </button>
  101. </uni-col>
  102. <!-- 所有物料/当前维修项物料 -->
  103. <uni-col :span="8">
  104. <button
  105. class="list-header-item flex-row align-center justify-center"
  106. type="default"
  107. @click="changeMaterialList"
  108. >
  109. <image
  110. src="~@/static/workOrder/suoyoueuliao.png"
  111. mode="aspectFill"
  112. ></image>
  113. <text v-if="allMaterialShow">
  114. {{ t("workOrder.currentMaterial") }}
  115. </text>
  116. <text v-else>{{ t("workOrder.allMaterial") }}</text>
  117. </button>
  118. </uni-col>
  119. <!-- 新增物料 不存在维修项或维修项不消耗物料时禁用-->
  120. <uni-col :span="8">
  121. <button
  122. class="list-header-item flex-row align-center justify-center"
  123. type="default"
  124. :disabled="!currentBomData || currentBomData.rule == '1'"
  125. @click="addMaterial"
  126. >
  127. <image
  128. src="~@/static/workOrder/xinzengwuliao.png"
  129. mode="aspectFill"
  130. ></image>
  131. <text>{{ t("workOrder.addMaterial") }}</text>
  132. </button>
  133. </uni-col>
  134. </uni-row>
  135. <view class="list-content">
  136. <work-order-materials
  137. :materials-data="materialList"
  138. :material-disabled="false"
  139. @deleteMaterial="onDeleteMaterial"
  140. @materialChanged="updateBomStatisticsAndCost"
  141. ></work-order-materials>
  142. </view>
  143. </view>
  144. </scroll-view>
  145. <!-- 底部总览 及 操作按钮 -->
  146. <view class="segmented-footer">
  147. <uni-row
  148. class="footer-total flex-row align-center justify-between"
  149. v-if="currentTab < 2"
  150. >
  151. <uni-col
  152. :span="18"
  153. class="total-item flex-row align-center justify-around"
  154. >
  155. <!-- 共xx项 -->
  156. <view class="total-item-text">
  157. <text>{{ t("workOrder.total") }}</text>
  158. <text class="number">{{ bomNumbers.total }}</text>
  159. <text>{{ t("workOrder.item") }}</text>
  160. </view>
  161. <!-- 已完成xx项 -->
  162. <view class="total-item-text">
  163. <text>{{ t("status.finished") }}</text>
  164. <text class="number">{{ bomNumbers.maintained }}</text>
  165. <text>{{ t("workOrder.item") }}</text>
  166. </view>
  167. <!-- 未完成xx项 -->
  168. <view class="total-item-text color-red">
  169. <text>{{ t("status.unfinished") }}</text>
  170. <text class="number">{{ bomNumbers.notMaintained }}</text>
  171. <text>{{ t("workOrder.item") }}</text>
  172. </view>
  173. </uni-col>
  174. <uni-col :span="6" class="total-btn">
  175. <button class="mini-btn" type="primary" @click="submitWorkOrder">
  176. {{ t("operation.submit") }}
  177. </button>
  178. </uni-col>
  179. </uni-row>
  180. <view class="footer-btn" v-else>
  181. <button class="mini-btn" type="primary" @click="saveMartialList">
  182. {{ t("operation.save") }}{{ t("workOrder.currentMaterialList") }}
  183. </button>
  184. </view>
  185. </view>
  186. </view>
  187. <!-- 选择维修项 -->
  188. <repair-multiple ref="repairMultipleRef" @repair-submit="onRepairSubmit" />
  189. <!-- 新增维修项 -->
  190. <repair-add ref="addRef" @add-set="onAddRepairSubmit"></repair-add>
  191. <!-- 选择物料弹窗 -->
  192. <materials-choose
  193. ref="materialsChooseRef"
  194. :deviceId="addMateriaItem.id"
  195. :bomNodeId="addMateriaItem.bomNodeId"
  196. :materialItem="addMateriaItem"
  197. :noAdd="true"
  198. @material-submit="onMaterialChooseSubmit"
  199. />
  200. <!-- 全局提示信息弹窗 -->
  201. <global-message-popup
  202. class="repair-message-popup"
  203. ref="messageGlobalRef"
  204. :msgType="msgType"
  205. :messageText="messageText"
  206. />
  207. </template>
  208. <script setup>
  209. import { onLoad, onReady, onBackPress } from "@dcloudio/uni-app";
  210. import {
  211. reactive,
  212. ref,
  213. onMounted,
  214. onBeforeUnmount,
  215. nextTick,
  216. getCurrentInstance,
  217. watch,
  218. } from "vue";
  219. import dayjs from "dayjs";
  220. // --------------------------引入用户信息-------------------------------
  221. import { getDeptId, getUserId, reloginByUserId } from "@/utils/auth";
  222. import {
  223. createRepair,
  224. getRepairApplicantList,
  225. getRepairProjectManagerList,
  226. } from "@/api/repair.js";
  227. // --------------------------引用api接口-----start------------------------
  228. import {
  229. fillWorkOrder,
  230. getMaintenanceDetail,
  231. getWorkOrderBOMs,
  232. getBomMaterialsByWorkOrderId,
  233. } from "@/api/maintenance";
  234. import { uploadFile } from "@/api";
  235. // --------------------------引用api接口-----end--------------------------
  236. // --------------------------引用组件-----start---------------------------
  237. import workOrderForm from "./components/work-order-form.vue";
  238. import workOrderBoms from "./components/work-order-boms.vue";
  239. import workOrderMaterials from "./components/work-order-materials.vue";
  240. import materialsChoose from "@/components/materials/choose.vue";
  241. import repairMultiple from "@/components/repair/multiple.vue";
  242. import repairAdd from "@/components/repair/add.vue";
  243. // --------------------------引用组件-----end-----------------------------
  244. // --------------------------引用全局变量$t-------------------------------
  245. const { appContext } = getCurrentInstance();
  246. const t = appContext.config.globalProperties.$t;
  247. // ----------------------------选项卡----------------------------------
  248. // 选项卡标题
  249. const tabTitles = ref([
  250. t("workOrder.workOrderInfo"),
  251. t("workOrder.repairWorkOrderList"),
  252. t("workOrder.materialList"),
  253. ]);
  254. const currentTab = ref(0);
  255. const styleType = ref("text");
  256. const activeColor = ref("#004098");
  257. const onClickTabItem = (e) => {
  258. console.log("🚀 ~ onClickTabItem ~ e:", e)
  259. // 如果要切换的选项卡是“物料列表”,则先判断当前选中的维修项是否关闭消耗物料(rule=1),如果是,则不允许切换到“物料列表”,并提示
  260. if (!onBeforeTabChange(e.currentIndex,currentBomData.value)) {
  261. return;
  262. }
  263. currentTab.value = e.currentIndex;
  264. };
  265. // 关闭物料消耗(rule=1)时,不允许切换到物料列表tab
  266. const onBeforeTabChange = (index,bom) => {
  267. console.log("🚀 ~ onBeforeTabChange ~ index,bom:", index,bom)
  268. if (index === 2 && bom?.rule == 1) {
  269. uni.showToast({
  270. title: t("equipmentMaintenance.notConsumeMaterial"),
  271. icon: "none",
  272. });
  273. return false;
  274. }
  275. return true;
  276. };
  277. // ----------------------------提示信息弹窗----------------------------------
  278. const messageGlobalRef = ref(null);
  279. // 提示信息弹窗类型
  280. const msgType = ref("success");
  281. // 提示信息弹窗内容
  282. const messageText = ref("");
  283. // 提示信息弹窗方法
  284. const showMessage = (type, message) => {
  285. msgType.value = type;
  286. messageText.value = message;
  287. messageGlobalRef.value.openMessage();
  288. };
  289. // ----------------------------------维修项-------------------------------
  290. // 维修项统计
  291. const bomNumbers = reactive({
  292. total: 0, // 维修项总数
  293. maintained: 0, // 已维修项数
  294. notMaintained: 0, // 未维修项数
  295. });
  296. // 当前所选维修项
  297. const currentBomData = ref(null);
  298. // 切换当前维修项
  299. const onSelectBom = (bom) => {
  300. console.log("onSelectBom", bom);
  301. // 检查选择的维修项是否关闭消耗物料(rule=1),若关闭则提示且不跳转
  302. if (bom.rule == "1") {
  303. uni.showToast({
  304. title: t("equipmentMaintenance.notConsumeMaterial"),
  305. icon: "none",
  306. });
  307. return;
  308. }
  309. // 更新当前所选维修项数据
  310. currentBomData.value = bom;
  311. // 更新展示的物料列表
  312. // 更新传递给子组件的物料列表
  313. materialList.value = bom.materials || [];
  314. // 切换到物料列表tab
  315. currentTab.value = 2;
  316. // 通过点击维修项进入物料列表时,切换到"当前物料"展示
  317. allMaterialShow.value = false;
  318. };
  319. // -----------------------------表单----------------------------------
  320. const workOrderFormRef = ref(null);
  321. //维修工单表单
  322. const maintain = ref({
  323. deviceId: "",
  324. deviceName: "",
  325. type: "in", // 类型(是否委外 in 内部 out 外部)
  326. picList: [], //用于上传照片 提交时删除
  327. pics: [], // 用于存放上传后的照片地址 提交时使用
  328. outFilesList: [], // 用于上传委外相关附件 提交时删除
  329. outFiles: [], // 用于存放上传后的委外相关附件地址 提交时使用
  330. });
  331. const selectedDevices = ref({});
  332. const onChooseDevice = (data) => {
  333. console.log("onChooseDevice", data);
  334. maintain.value.deviceId = data.id;
  335. maintain.value.deptId = data.deptId;
  336. maintain.value.deviceCode = data.deviceCode;
  337. maintain.value.deviceName = data.deviceName;
  338. console.log("onChooseDevice-maintain", maintain.value);
  339. // 将选择的设备赋值给selectedDevices
  340. selectedDevices.value = data;
  341. // 如果维修类型为OA委外,将设备的model值(规格型号)赋值给maintain的model
  342. if (maintain.value.type === "out") {
  343. maintain.value.model = data.model;
  344. // 同时将设备的enableDate值(启用日期)赋值给maintain的enableDate
  345. maintain.value.enableDate = data.enableDate;
  346. } else {
  347. // 否则将model清空
  348. maintain.value.model = "";
  349. // 同时将enableDate清空
  350. maintain.value.enableDate = "";
  351. }
  352. };
  353. // 供应商选择确认
  354. const onSelectedManufacturer = (item) => {
  355. console.log("🚀 ~ onSelectedManufacturer ~ item:", item);
  356. maintain.value.supplier = item.name;
  357. };
  358. // 上传图片
  359. const uploadImg = async (event) => {
  360. console.log("🚀 ~ upload ~ event:", event);
  361. console.log("🚀 ~ upload ~ event.tempFilePaths):", event.tempFilePaths);
  362. for (const path of event.tempFilePaths) {
  363. maintain.value.pics.push((await uploadFile(path)).data);
  364. }
  365. console.log("🚀 ~ upload ~ maintain.value.pics :", maintain.value.pics);
  366. };
  367. // 删除图片
  368. const deleteImg = (event) => {
  369. console.log("🚀 ~ deleteImg ~ event:", event);
  370. maintain.value.pics.splice(event.index, 1);
  371. console.log("🚀 ~ deleteImg ~ maintain.value.pics:", maintain.value.pics);
  372. };
  373. // 上传委外相关附件
  374. const uploadOutFiles = async (event) => {
  375. console.log("🚀 ~ uploadOutFiles ~ event:", event);
  376. console.log(
  377. "🚀 ~ uploadOutFiles ~ event.tempFilePaths):",
  378. event.tempFilePaths
  379. );
  380. for (const path of event.tempFilePaths) {
  381. maintain.value.outFiles.push((await uploadFile(path)).data);
  382. }
  383. console.log(
  384. "🚀 ~ uploadOutFiles ~ maintain.value.outFiles :",
  385. maintain.value.outFiles
  386. );
  387. };
  388. // 删除委外相关附件
  389. const deleteOutFiles = (event) => {
  390. console.log("🚀 ~ deleteOutFiles ~ event:", event);
  391. maintain.value.outFiles.splice(event.index, 1);
  392. console.log(
  393. "🚀 ~ deleteOutFiles ~ maintain.value.outFiles:",
  394. maintain.value.outFiles
  395. );
  396. };
  397. // 本地搜索确认选择
  398. const onChooseLocalSearch = (propKey, item) => {
  399. console.log("🚀 ~ onChooseLocalSearch ~ propKey, item:", propKey, item);
  400. // 根据propKey设置maintain的值
  401. maintain.value[propKey] = item.value;
  402. };
  403. // 维修项列表
  404. const maintainBom = ref([]);
  405. // 选择维修项
  406. const repairMultipleRef = ref(null);
  407. const onChooseRepair = () => {
  408. // 判断是否选择设备
  409. if (!maintain.value.deviceId) {
  410. uni.showToast({
  411. title: ` ${t("operation.please")}${t("device.selectDevice")}`,
  412. icon: "none",
  413. });
  414. return;
  415. }
  416. repairMultipleRef.value.open([maintain.value.deviceId]);
  417. };
  418. // 选择维修项提交
  419. const onRepairSubmit = (data) => {
  420. console.log("onRepairSubmit", data);
  421. // 判断data数组中bomNodeId是否存在maintainBom数组中, 如果不存在, 则添加 并将materialSelected设为false
  422. data.forEach((bom) => {
  423. if (!maintainBom.value.some((item) => item.bomNodeId === bom.bomNodeId)) {
  424. maintainBom.value.push({
  425. ...bom,
  426. rule: data.rule || 0, //是否消耗物料 (0需要 1不需要)
  427. materialCount: 0, // 物料数量
  428. materials: [], // 物料明细
  429. workOrderBomOnlyKey: `${bom.deviceCode}_${bom.deviceName}_${bom.name}`, //手动拼接唯一标识
  430. materialSelected: false, //手动添加是否选择物料标识
  431. });
  432. if (bom.deviceBomMaterials && !bom.materials) {
  433. bom.materials = [];
  434. bom.deviceBomMaterials.forEach((material) => {
  435. bom.materials.push({
  436. ...material,
  437. bomName: bom.name,
  438. bomDisabled: bom.rule == 1, //是否消耗物料 (0需要 1不需要)
  439. materialName: material.materialName
  440. ? material.materialName
  441. : material.name, // 确保物料名称和物料编码不为空
  442. materialCode: material.materialCode
  443. ? material.materialCode
  444. : material.code,
  445. });
  446. });
  447. }
  448. }
  449. });
  450. console.log("onRepairSubmit-maintainBom", maintainBom.value);
  451. // 判断是否存在选择的维修项,若不存在,则将数组第一项设为当前维修项
  452. if (!currentBomData.value) {
  453. currentBomData.value = maintainBom.value[0];
  454. }
  455. // 更新维修项数量统计及保养费用
  456. updateBomStatisticsAndCost();
  457. };
  458. // 新增维修项
  459. const addRef = ref(null);
  460. // 新增维修项弹窗
  461. const onAddRepair = () => {
  462. // 判断是否选择设备
  463. if (!maintain.value.deviceId) {
  464. uni.showToast({
  465. title: ` ${t("operation.please")}${t("device.selectDevice")}`,
  466. icon: "none",
  467. });
  468. return;
  469. }
  470. const info = {
  471. id: maintain.value.deviceId,
  472. deptId: maintain.value.deptId,
  473. deviceId: maintain.value.deviceId,
  474. deviceCode: maintain.value.deviceCode,
  475. deviceName: maintain.value.deviceName,
  476. bom: maintainBom.value,
  477. };
  478. addRef.value.open(info);
  479. };
  480. // 新增维修项提交
  481. const onAddRepairSubmit = (data) => {
  482. console.log("onAddRepairSubmit", data);
  483. maintainBom.value.push({
  484. ...data,
  485. rule: data.rule || 0, //是否消耗物料
  486. materialCount: 0, // 物料数量
  487. materials: [], // 物料明细
  488. workOrderBomOnlyKey: `${data.deviceCode}_${data.deviceName}_${data.name}`, //手动拼接唯一标识
  489. materialSelected: false, //手动添加是否选择物料标识
  490. });
  491. // 判断是否存在选择的维修项,若不存在,则将数组第一项设为当前维修项
  492. if (!currentBomData.value) {
  493. currentBomData.value = maintainBom.value[0];
  494. }
  495. // 更新维修项数量统计及保养费用
  496. updateBomStatisticsAndCost();
  497. };
  498. // 删除维修项
  499. const ondeleteBom = (bom) => {
  500. console.log("🚀 ~ ondeleteBom ~ bom:", bom);
  501. // 查找maintainBom中是否存在bom
  502. const index = maintainBom.value.findIndex(
  503. (item) => item.workOrderBomOnlyKey === bom.workOrderBomOnlyKey
  504. );
  505. if (index !== -1) {
  506. maintainBom.value.splice(index, 1);
  507. if (currentBomData.value.workOrderBomOnlyKey === bom.workOrderBomOnlyKey) {
  508. currentBomData.value = maintainBom.value[0] || {};
  509. }
  510. // 删除维修项后更新物料列表
  511. changeMaterialListByShow();
  512. // 更新维修项数量统计及保养费用
  513. updateBomStatisticsAndCost();
  514. }
  515. };
  516. // 物料选择
  517. const materialsChooseRef = ref(null);
  518. const addMateriaItem = ref({});
  519. const onMaterialChoose = (item) => {
  520. console.log("onMaterialChoose", item);
  521. // 判断是否选择设备
  522. if (!maintain.value.deviceId) {
  523. uni.showToast({
  524. title: ` ${t("operation.please")}${t("device.selectDevice")}`,
  525. icon: "none",
  526. });
  527. return;
  528. }
  529. if (!item || !item.bomNodeId) {
  530. uni.showToast({
  531. title: ` ${t("operation.please")}${t("workOrder.selectConsumeMaterial")}`,
  532. icon: "none",
  533. });
  534. return;
  535. }
  536. addMateriaItem.value = item;
  537. materialsChooseRef.value.open(item);
  538. };
  539. // 选择物料弹窗提交
  540. const onMaterialChooseSubmit = (item) => {
  541. console.log("onMaterialChooseSubmit", item);
  542. /**
  543. * 1. 检查当前物料是否已存在
  544. * 维修项id(bomNodeId)、
  545. * 物料编码(materialCode)或物料名称(materialName)、
  546. * 成本中心id(costCenterId)、
  547. * 工厂id(factoryId)、
  548. * 单价(unitPrice)、
  549. * 库位id(storageLocationId)
  550. * 全部相同时视为同一物料
  551. * 2. 若已存在,则累加数量
  552. * 3. 若不存在,则新增物料
  553. */
  554. // 是否存在相同物料,若存在则累加数量,若不存在则新增
  555. const existMaterial = currentBomData.value.materials.find((material) => {
  556. const materialChooseKey = `${
  557. material.materialCode ? material.materialCode : material.materialName
  558. }_${material.costCenterId}_${material.factoryId}_${material.unitPrice}_${
  559. material.storageLocationId
  560. }`;
  561. return materialChooseKey === item.chooseKey;
  562. });
  563. if (existMaterial) {
  564. existMaterial.quantity += item.quantity;
  565. } else {
  566. currentBomData.value.materials.unshift({
  567. ...item,
  568. bomNodeId: currentBomData.value.bomNodeId, // 维修项id
  569. bomName: currentBomData.value.name, // 维修项名称
  570. isNew: false, // 是否是新添加的物料
  571. });
  572. }
  573. // 更新展示的物料列表
  574. changeMaterialListByShow();
  575. // 更新维修项数量统计及保养费用
  576. updateBomStatisticsAndCost();
  577. };
  578. // 保养工单 - 物料
  579. const materialList = ref([]);
  580. const allMaterialShow = ref(false); // 是否展示所有物料
  581. // 新添加的物料项
  582. const newMaterialItem = ref({
  583. id: null, //物料id
  584. bomNodeId: null, // 维修项id
  585. bomName: null, // 维修项名称
  586. materialCode: null, // 物料编码
  587. materialName: null, // 物料名称
  588. unit: null, // 物料单位
  589. unitPrice: 0, // 物料单价
  590. quantity: 0, // 物料消耗数量
  591. materialSource: t("workOrder.manualAdd"), // 物料来源
  592. isNew: true, // 是否是新添加的物料
  593. });
  594. // 切换展示的物料列表
  595. const changeMaterialList = () => {
  596. // 切换展示所有物料状态
  597. allMaterialShow.value = !allMaterialShow.value;
  598. // 根据展示状态切换物料列表
  599. changeMaterialListByShow();
  600. };
  601. // 返回所有物料列表
  602. const getAllMaterialList = () => {
  603. const list = maintainBom.value
  604. .map((bom) => {
  605. if (bom.materials) {
  606. return bom.materials;
  607. }
  608. })
  609. .flat();
  610. return list;
  611. };
  612. // 根据物料展示状态(allMaterialShow),切换展示的物料列表
  613. const changeMaterialListByShow = () => {
  614. // allMaterialShow 为 true 时展示所有物料列表
  615. if (allMaterialShow.value) {
  616. materialList.value = getAllMaterialList();
  617. } else {
  618. // allMaterialShow 为 false 时展示当前所选维修项的物料列表
  619. materialList.value = currentBomData.value?.materials || [];
  620. }
  621. };
  622. // 新增物料
  623. const addMaterial = () => {
  624. /**
  625. * 新增物料
  626. * 1. 不直接在materialList.value中添加新物料是因为:
  627. * 在展示所有物料时添加的新物料时再次切换到展示当前物料,新增的物料不会更新到当前维修项中
  628. * 2. 将新添加的物料项放入当前维修项的物料列表第一个
  629. */
  630. // 判断是否选择设备
  631. if (!maintain.value.deviceId) {
  632. uni.showToast({
  633. title: ` ${t("operation.please")}${t("device.selectDevice")}`,
  634. icon: "none",
  635. });
  636. return;
  637. }
  638. currentBomData.value.materials.unshift({
  639. ...newMaterialItem.value,
  640. bomNodeId: currentBomData.value.bomNodeId,
  641. bomName: currentBomData.value.name,
  642. });
  643. // 更新展示的物料列表
  644. changeMaterialListByShow();
  645. // 更新维修项数量统计及保养费用
  646. updateBomStatisticsAndCost();
  647. };
  648. // 更新维修项统计数量(已完成和未完成)
  649. const updateBomStatistics = () => {
  650. let total = maintainBom.value.length;
  651. let maintained = 0;
  652. maintainBom.value.forEach((bom) => {
  653. // 计算物料总数
  654. calculateMaterialCount(bom);
  655. // 判断是否为已维修项
  656. const isMaintained = checkIfMaintained(bom);
  657. if (isMaintained) {
  658. // 已维修项数增加
  659. maintained++;
  660. // 添加已完成标识
  661. bom.maintainedFlag = true;
  662. } else {
  663. // 添加未完成标识
  664. bom.maintainedFlag = false;
  665. }
  666. });
  667. bomNumbers.total = total;
  668. bomNumbers.maintained = maintained;
  669. bomNumbers.notMaintained = total - maintained;
  670. console.log("🚀 ~ 更新维修项统计数量 ~ bomNumbers:", bomNumbers);
  671. };
  672. /**
  673. * 检查维修项是否满足已保养条件
  674. * @param {Object} bom - 维修项数据
  675. * @returns {boolean} - 是否满足已保养条件
  676. * @description
  677. * 1. 关闭是否消耗物料(rule=1)
  678. * 2. 延保相关信息
  679. * 2.1 检查延迟保养规则是否启用
  680. * 2.2 若没有启用的规则,或启用规则但对应延迟值均不大于0
  681. * 3. 所有物料的消耗数量均大于0(如果需要消耗物料)
  682. *
  683. */
  684. const checkIfMaintained = (bom) => {
  685. // 条件1:关闭是否消耗物料(rule=1)
  686. const isNotConsumeMaterial = isBomNotConsumeMaterial(bom);
  687. // console.log("🚀 ~ 是否消耗物料:", isNotConsumeMaterial);
  688. // console.log("🚀 ~ 是否设置了延保相关信息:", hasDelaySetting);
  689. // 条件3:所有物料的消耗数量均大于0(如果需要消耗物料)
  690. const allMaterialsValid = checkAllMaterialsValid(bom);
  691. // console.log("🚀 ~ 是否所有物料的消耗数量均大于0:", allMaterialsValid);
  692. // 满足任一条件即视为已保养
  693. return isNotConsumeMaterial || allMaterialsValid;
  694. };
  695. // 维修项是否消耗物料
  696. const isBomNotConsumeMaterial = (bom) => {
  697. const isNotConsumeMaterial = bom.rule == "1";
  698. bom.isNotConsumeMaterial = isNotConsumeMaterial;
  699. // 如果存在物料列表,将isNotConsumeMaterial存入每个物料项中
  700. if (bom.materials.length > 0) {
  701. bom.materials.forEach((material) => {
  702. material.isNotConsumeMaterial = isNotConsumeMaterial;
  703. });
  704. }
  705. return isNotConsumeMaterial;
  706. };
  707. // 检查所有物料消耗数量是否均大于0
  708. const checkAllMaterialsValid = (bom) => {
  709. if (!bom.materials || bom.materials.length === 0) {
  710. return false;
  711. }
  712. return bom.materials.every((material) => {
  713. const quantity = Number(material.quantity);
  714. return !isNaN(quantity) && quantity > 0;
  715. });
  716. };
  717. // 计算维修项中所有物料的消耗数量累计
  718. const calculateMaterialCount = (bom) => {
  719. if (!bom.materials || bom.materials.length === 0) {
  720. bom.materialCount = 0;
  721. return;
  722. }
  723. const total = bom.materials.reduce((sum, material) => {
  724. const quantity = Number(material.quantity) || 0;
  725. return sum + quantity;
  726. }, 0);
  727. bom.materialCount = total;
  728. };
  729. // 计算保养费用
  730. const calculateMaintenanceCost = () => {
  731. let totalCost = 0;
  732. maintainBom.value.forEach((bom) => {
  733. // 条件1:关闭是否消耗物料(rule=1)
  734. const isNotConsumeMaterial = bom.rule == 1;
  735. // console.log("🚀计算保养费用 ~ 是否消耗物料:", isNotConsumeMaterial);
  736. // 条件2:开启是否消耗物料(rule=0)
  737. if (!isNotConsumeMaterial) {
  738. // 遍历物料计算费用
  739. bom.materials?.forEach((material) => {
  740. const quantity = Number(material.quantity) || 0;
  741. const unitPrice = Number(material.unitPrice) || 0;
  742. // 只计算消耗数量大于0的物料
  743. if (quantity > 0) {
  744. totalCost += quantity * unitPrice;
  745. }
  746. });
  747. }
  748. });
  749. // 更新工单保养费用(保留两位小数)
  750. maintain.value.maintainFee = totalCost.toFixed(2);
  751. console.log("🚀计算保养费用 ~ 保养费用:", totalCost);
  752. };
  753. // 更新维修项数量统计及保养费用
  754. const updateBomStatisticsAndCost = () => {
  755. console.log("🚀 ~ 更新维修项数量统计及保养费用 ~ bom:", maintainBom.value);
  756. // 更新维修项统计数量(已完成和未完成)
  757. updateBomStatistics();
  758. // 更新保养费用
  759. calculateMaintenanceCost();
  760. };
  761. // 删除物料
  762. const onDeleteMaterial = (data) => {
  763. materialList.value.splice(data.index, 1);
  764. // 更新当前维修项的物料数量
  765. calculateMaterialCount(currentBomData.value);
  766. // 更新维修项数量统计及保养费用
  767. updateBomStatisticsAndCost();
  768. };
  769. // 保存物料列表
  770. const saveMartialList = async () => {
  771. /**
  772. * 校验当前展示的物料列表
  773. * 1.物料列表为空时,提示请添加物料
  774. * 2.新增的物料(isNew=true)物料名称不能为空,单价不能为空,消耗数量>0
  775. * 3.已有的物料(isNew=false)单价不能为空,消耗数量>0
  776. * 记录所有不符合校验的物料维修项名称,拼接提示
  777. */
  778. console.log("保存物料列表:开始");
  779. console.log("🚀 ~ saveMartialList ~ materialList:", materialList.value);
  780. // 校验物料列表是否为空
  781. if (materialList.value.length === 0) {
  782. uni.showToast({
  783. title: t("maintenanceWorkOrder.materialEmpty"),
  784. icon: "none",
  785. });
  786. return;
  787. }
  788. const invalidBomNames = [];
  789. materialList.value.forEach((material,index) => {
  790. if (material.isNew) {
  791. if (
  792. !material.materialName ||
  793. !material.unitPrice ||
  794. !material.quantity ||
  795. material.quantity <= 0
  796. ) {
  797. invalidBomNames.push(
  798. material.bomName +
  799. ":" +
  800. (material.materialName
  801. ? material.materialName
  802. : t("workOrder.serialNumber") + "(" + (index + 1) + ")")
  803. );
  804. }
  805. } else {
  806. if (!material.unitPrice || !material.quantity || material.quantity <= 0) {
  807. invalidBomNames.push(material.bomName + ":" + material.materialName);
  808. }
  809. }
  810. });
  811. // 去重
  812. const uniqueInvalidBomNames = [...new Set(invalidBomNames)];
  813. console.log(
  814. "🚀 ~ saveMartialList ~ uniqueInvalidBomNames:",
  815. uniqueInvalidBomNames
  816. );
  817. /**
  818. * 拆分提示
  819. * 若不符合校验的物料大于3,提示前3项,其余项用...表示
  820. * 若不符合维修项的数量小于3,则全部提示
  821. */
  822. if (uniqueInvalidBomNames.length > 0) {
  823. const msg =
  824. uniqueInvalidBomNames.slice(0, 3).join(";\n") +
  825. (uniqueInvalidBomNames.length > 3 ? "..." : "\n") +
  826. t("workOrder.materialInvalid");
  827. showMessage("error", msg);
  828. return;
  829. }
  830. // 校验通过,将tab切换到保养项列表并提示保存成功
  831. const msg = t("workOrder.currentMaterialList") + t("operation.saveSuccess");
  832. showMessage("success", msg);
  833. currentTab.value = 1;
  834. console.log("保存物料列表:结束");
  835. };
  836. // ----------------------------提交工单------------------------------------
  837. /**
  838. * 校验时间规则(maintainRules未覆盖的场景)
  839. * @returns {boolean} 校验结果
  840. */
  841. const checkTimeRules = async () => {
  842. const { maintainStartTime, maintainEndTime, failureTime } = maintain.value;
  843. // 判断维修开始时间是否大于维修结束时间
  844. if (
  845. maintainStartTime &&
  846. maintainEndTime &&
  847. maintainStartTime > maintainEndTime
  848. ) {
  849. uni.showToast({ title: t("general.timeNotBeLater"), icon: "none" });
  850. return false;
  851. }
  852. // 维修开始时间不能早于故障时间
  853. if (failureTime && maintainStartTime && maintainStartTime < failureTime) {
  854. uni.showToast({ title: t("general.startTimeNotBeEarlier"), icon: "none" });
  855. return false;
  856. }
  857. // 维修结束时间不能早于故障时间
  858. if (failureTime && maintainEndTime && maintainEndTime < failureTime) {
  859. uni.showToast({ title: t("general.endTimeNotBeEarlier"), icon: "none" });
  860. return false;
  861. }
  862. return true;
  863. };
  864. const submitWorkOrder = async () => {
  865. // 校验表单信息
  866. const formIsValid = await workOrderFormRef.value.validate();
  867. console.log("🚀 ~ 保养工单信息校验结果 ~ formIsValid:", formIsValid);
  868. if (!formIsValid) return;
  869. // 4. 校验时间规则
  870. const isTimeValid = await checkTimeRules();
  871. if (!isTimeValid) return;
  872. // 5. 业务逻辑校验
  873. if (maintain.value.type === "in") {
  874. if (maintainBom.value.length === 0) {
  875. uni.showToast({
  876. title: `${t("operation.PleaseSelect")}${t(
  877. "equipmentMaintenance.maintenanceItems"
  878. )}`,
  879. icon: "none",
  880. });
  881. return;
  882. }
  883. // 校验是否存在未完成的维修项 维修类型为“内部维修”时
  884. if (bomNumbers.notMaintained > 0 && maintain.value.type == "in") {
  885. uni.showToast({
  886. title: t("equipmentMaintenance.unFinishedMaintenanceItems"),
  887. icon: "none",
  888. });
  889. return;
  890. }
  891. }
  892. // 6. 提交数据
  893. // 提取数据处理逻辑为独立函数 - 使用对象解构赋值代替 delete 操作,避免修改原始数据
  894. const processMaintainData = (maintainItems) => {
  895. const { picList, outFilesList, ...rest } = maintainItems;
  896. return rest;
  897. };
  898. // 删除maintainBom.value数组中的materialSelected和workOrderBomOnlyKey
  899. // 并且删除maintainBom.value.material数组中的chooseKey和workOrderBomOnlyKey
  900. // 形成一个新数组
  901. const orderBom = maintainBom.value.map((item) => {
  902. const newItem = Object.assign({}, item, {
  903. ifStop: item.ifStop ? true : false,
  904. });
  905. delete newItem.materialSelected;
  906. delete newItem.workOrderBomOnlyKey;
  907. delete newItem.maintainedFlag;
  908. delete newItem.isNotConsumeMaterial;
  909. newItem.materials = newItem.materials.map((materialItem) => {
  910. const newMaterialItem = {
  911. ...materialItem,
  912. };
  913. delete newMaterialItem.chooseKey;
  914. delete newMaterialItem.workOrderBomOnlyKey;
  915. delete newMaterialItem.picList;
  916. delete newMaterialItem.outFilesList;
  917. delete newMaterialItem.isNew;
  918. delete newMaterialItem.isNotConsumeMaterial;
  919. delete newMaterialItem.bomDisabled;
  920. delete newMaterialItem.bomName;
  921. return newMaterialItem;
  922. });
  923. return newItem;
  924. });
  925. console.log("orderBom", orderBom);
  926. console.log("🚀 ~ formSubmit ~ maintain.value:", maintain.value);
  927. const maintainData = processMaintainData(maintain.value);
  928. console.log("maintainData", maintainData);
  929. // 提交数据
  930. createRepair({
  931. maintain: {
  932. ...maintainData,
  933. status: maintain.value.type == "in" ? "finished" : "tx",
  934. },
  935. maintainMaterials: orderBom,
  936. })
  937. .then((res) => {
  938. console.log("createRepair", res);
  939. if (res.code == 0) {
  940. uni.showToast({
  941. title: t("operation.success"),
  942. icon: "success",
  943. });
  944. uni.navigateBack();
  945. } else {
  946. uni.showToast({
  947. title: res.msg,
  948. icon: "none",
  949. });
  950. }
  951. })
  952. .catch((err) => {
  953. console.log("createRepair-err", err);
  954. });
  955. };
  956. // --------------------------------------------监听-------------------------------
  957. // 监听设备变化
  958. watch(
  959. () => maintain.value.deviceId,
  960. (newDeviceId, oldDeviceId) => {
  961. if (newDeviceId !== oldDeviceId) {
  962. // 1. 清空维修项与物料
  963. maintainBom.value = [];
  964. // 2. 重置总费用
  965. maintain.value.maintainFee = "";
  966. // 3. 委外类型下,重置规格型号(重新关联新设备的model)与启用日期enableDate
  967. if (maintain.value.type === "out") {
  968. maintain.value.model = selectedDevices.value.model || "";
  969. maintain.value.enableDate = selectedDevices.value.enableDate || "";
  970. }
  971. // 4. 重置表单校验(设备相关字段)
  972. nextTick(() => {
  973. repairFormRef.value?.clearValidate(["maintainBom", "maintainFee"]);
  974. });
  975. }
  976. }
  977. );
  978. // 监听维修类型数据变化,触发规则更新和校验重置
  979. watch(
  980. () => maintain.value.type,
  981. (newVal, oldVal) => {
  982. console.log("维修类型变更:", oldVal, "->", newVal);
  983. // 维修类型为OA委外时,获取申请人列表和项目经理列表
  984. if (newVal == "out" && oldVal !== "out") {
  985. // 委外类型下,重置规格型号(重新关联新设备的model)与启用日期enableDate
  986. if (maintain.value.type === "out") {
  987. maintain.value.model = selectedDevices.value.model || "";
  988. maintain.value.enableDate = selectedDevices.value.enableDate || "";
  989. }
  990. } // 内部类型:清空委外特有字段
  991. else if (newVal === "in" && oldVal === "out") {
  992. const outFields = [
  993. "applyPersonId",
  994. "maintainClassify",
  995. "kmHour",
  996. "model",
  997. "enableDate",
  998. "supplier",
  999. "projectManager",
  1000. "address",
  1001. "outFiles",
  1002. "maintainItem",
  1003. ];
  1004. outFields.forEach((field) => {
  1005. if (field.includes("outFiles")) {
  1006. maintain.value[field] = [];
  1007. } else {
  1008. maintain.value[field] = "";
  1009. }
  1010. });
  1011. }
  1012. }
  1013. );
  1014. // 监听维修项数据变化,
  1015. watch(
  1016. () => maintainBom.value,
  1017. (newVal, oldVal) => {
  1018. console.log("维修项数据变更:", oldVal, "->", newVal);
  1019. },
  1020. { deep: true }
  1021. );
  1022. </script>
  1023. <style lang="scss" scoped>
  1024. @import "@/style/work-order-segmented.scss";
  1025. .page {
  1026. padding-bottom: 0;
  1027. }
  1028. </style>