IotMainWorkOrder.vue 33 KB

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