IotMainWorkOrderAdd.vue 32 KB

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