IotFailureReportForm.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. <template>
  2. <Dialog v-model="dialogVisible" :title="dialogTitle" width="1080px" class="failure-report-dialog">
  3. <template #title>
  4. <div class="dialog-header mb-6">
  5. <div class="dialog-header__icon">
  6. <Icon icon="ep:warning" :size="22" />
  7. </div>
  8. <div>
  9. <div class="dialog-title">{{ dialogTitle }}</div>
  10. <div class="dialog-subtitle">维护故障基础信息、处理状态与现场描述</div>
  11. </div>
  12. </div>
  13. </template>
  14. <el-form
  15. ref="formRef"
  16. v-loading="formLoading"
  17. :model="formData"
  18. :rules="formRules"
  19. size="default"
  20. label-width="110px"
  21. class="failure-report-form"
  22. :class="{ 'is-readonly': disabled }">
  23. <section class="info-card">
  24. <div class="info-card__header">
  25. <div class="info-card__title">
  26. <span class="info-card__marker"></span>
  27. <span>基础信息</span>
  28. </div>
  29. </div>
  30. <div class="info-card__body">
  31. <el-row :gutter="16">
  32. <el-col :xs="24" :sm="12">
  33. <el-form-item :label="t('faultForm.failureType')" prop="failureType">
  34. <el-select
  35. v-model="formData.failureType"
  36. :placeholder="t('faultForm.failureTypeHolder')"
  37. :disabled="disabled"
  38. clearable
  39. class="form-control"
  40. @change="handleFailureTypeChange">
  41. <el-option
  42. v-for="dict in getStrDictOptions(DICT_TYPE.FAILURE_TYPE)"
  43. :key="dict.label"
  44. :label="dict.label"
  45. :value="dict.value" />
  46. </el-select>
  47. </el-form-item>
  48. <el-form-item
  49. v-if="formData.failureType === 'other'"
  50. label="其他故障描述"
  51. prop="typeDesc"
  52. :rules="typeDescRule">
  53. <el-input
  54. v-model="formData.typeDesc"
  55. type="textarea"
  56. placeholder="请输入其他故障描述"
  57. :disabled="disabled"
  58. class="form-control" />
  59. </el-form-item>
  60. </el-col>
  61. <el-col :xs="24" :sm="12">
  62. <el-form-item :label="t('faultForm.device')" prop="deviceName">
  63. <el-select
  64. v-if="disabled"
  65. v-model="formData.deviceName"
  66. :disabled="disabled"
  67. :placeholder="t('faultForm.choose')"
  68. clearable
  69. class="form-control"
  70. @clear="handleClear" />
  71. <el-select
  72. v-else
  73. v-model="formData.deviceName"
  74. :placeholder="t('faultForm.choose')"
  75. clearable
  76. class="form-control"
  77. @click="openForm"
  78. @clear="handleClear" />
  79. </el-form-item>
  80. </el-col>
  81. <el-col :xs="24" :sm="12">
  82. <el-form-item :label="t('faultForm.failureTime')" prop="failureTime">
  83. <el-date-picker
  84. v-model="formData.failureTime"
  85. :disabled="disabled"
  86. type="datetime"
  87. value-format="x"
  88. :placeholder="t('faultForm.failureTimeHolder')"
  89. class="form-control" />
  90. </el-form-item>
  91. </el-col>
  92. <el-col :xs="24" :sm="12">
  93. <el-form-item :label="t('faultForm.shutDown')" prop="ifStop">
  94. <el-select
  95. v-model="formData.ifStop"
  96. :placeholder="t('faultForm.choose')"
  97. clearable
  98. :disabled="disabled"
  99. class="form-control">
  100. <el-option
  101. v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  102. :key="dict.label"
  103. :label="dict.label"
  104. :value="dict.value" />
  105. </el-select>
  106. </el-form-item>
  107. </el-col>
  108. <el-col :xs="24" :sm="12">
  109. <el-form-item :label="t('faultForm.picture')" prop="pic">
  110. <UploadImg v-model="formData.pic" height="60px" :disabled="disabled" />
  111. </el-form-item>
  112. </el-col>
  113. </el-row>
  114. </div>
  115. </section>
  116. <section class="info-card">
  117. <div class="info-card__header">
  118. <div class="info-card__title">
  119. <span class="info-card__marker"></span>
  120. <span>处理信息</span>
  121. </div>
  122. </div>
  123. <div class="info-card__body">
  124. <el-row :gutter="16">
  125. <el-col :xs="24" :sm="12">
  126. <el-form-item :label="t('faultForm.solve')" prop="ifDeal">
  127. <el-select
  128. v-model="formData.ifDeal"
  129. :placeholder="t('faultForm.choose')"
  130. clearable
  131. :disabled="disabled"
  132. class="form-control"
  133. @change="dealChange">
  134. <el-option
  135. v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  136. :key="dict.label"
  137. :label="dict.label"
  138. :value="dict.value" />
  139. </el-select>
  140. </el-form-item>
  141. </el-col>
  142. <el-col :xs="24" :sm="12">
  143. <el-form-item :label="t('faultForm.assist')" prop="needHelp">
  144. <el-select
  145. v-model="formData.needHelp"
  146. :placeholder="t('faultForm.choose')"
  147. clearable
  148. :disabled="disabled"
  149. class="form-control"
  150. @change="helpChange">
  151. <el-option
  152. v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  153. :key="dict.label"
  154. :label="dict.label"
  155. :value="dict.value" />
  156. </el-select>
  157. </el-form-item>
  158. </el-col>
  159. <el-col v-if="formData.ifDeal" :xs="24" :sm="12">
  160. <el-form-item :label="t('faultForm.dealTime')" prop="dealTime">
  161. <el-date-picker
  162. v-model="formData.dealTime"
  163. :disabled="disabled"
  164. type="datetime"
  165. value-format="x"
  166. placeholder="选择故障解决时间"
  167. class="form-control"
  168. @change="endTimeBlur" />
  169. </el-form-item>
  170. </el-col>
  171. <el-col v-if="!formData.ifDeal && approvalShow" :xs="24" :sm="12">
  172. <el-form-item
  173. :label="t('faultForm.approvalId')"
  174. prop="approvalId"
  175. :rules="!formData.ifDeal ? approvalRule : []">
  176. <el-select
  177. v-model="formData.approvalId"
  178. multiple
  179. :placeholder="t('faultForm.approvalHolder')"
  180. filterable
  181. clearable
  182. :disabled="disabled"
  183. class="form-control">
  184. <el-option
  185. v-for="ap in approver"
  186. :key="ap.id"
  187. :label="ap.nickname"
  188. :value="ap.id" />
  189. </el-select>
  190. </el-form-item>
  191. </el-col>
  192. </el-row>
  193. </div>
  194. </section>
  195. <section class="info-card">
  196. <div class="info-card__header">
  197. <div class="info-card__title">
  198. <span class="info-card__marker"></span>
  199. <span>故障描述</span>
  200. </div>
  201. </div>
  202. <div class="info-card__body">
  203. <el-row :gutter="16">
  204. <el-col :span="24">
  205. <el-form-item :label="t('faultForm.faultImpact')" prop="failureInfluence">
  206. <el-input
  207. v-model="formData.failureInfluence"
  208. type="textarea"
  209. :rows="2"
  210. :placeholder="t('faultForm.faultImpactHolder')"
  211. :disabled="disabled" />
  212. </el-form-item>
  213. </el-col>
  214. <el-col v-if="formData.ifDeal" :span="24">
  215. <el-form-item
  216. label="解决办法"
  217. prop="solution"
  218. :rules="formData.ifDeal ? solutionRule : []">
  219. <el-input
  220. v-model="formData.solution"
  221. type="textarea"
  222. :rows="2"
  223. placeholder="请输入解决办法"
  224. :disabled="disabled" />
  225. </el-form-item>
  226. </el-col>
  227. <el-col :span="24">
  228. <el-form-item :label="t('faultForm.faultDescription')" prop="description">
  229. <el-input
  230. v-model="formData.description"
  231. type="textarea"
  232. :rows="3"
  233. :disabled="disabled" />
  234. </el-form-item>
  235. </el-col>
  236. <el-col :span="24">
  237. <el-form-item :label="t('faultForm.remark')" prop="remark">
  238. <el-input
  239. v-model="formData.remark"
  240. type="textarea"
  241. :rows="2"
  242. :placeholder="t('faultForm.rHolder')"
  243. :disabled="disabled" />
  244. </el-form-item>
  245. </el-col>
  246. </el-row>
  247. </div>
  248. </section>
  249. </el-form>
  250. <template #footer>
  251. <div class="dialog-footer">
  252. <el-button @click="dialogVisible = false">{{ t('faultForm.cancel') }}</el-button>
  253. <el-button v-if="!disabled" type="primary" :disabled="formLoading" @click="submitForm">
  254. {{ t('faultForm.ok') }}
  255. </el-button>
  256. </div>
  257. </template>
  258. <DeviceList ref="deviceFormRef" @choose="deviceChoose" />
  259. </Dialog>
  260. </template>
  261. <script setup lang="ts">
  262. import { IotFailureReportApi, IotFailureReportVO } from '@/api/pms/failure'
  263. import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
  264. import DeviceList from '@/views/pms/failure/DeviceList.vue'
  265. import dayjs from 'dayjs'
  266. import { IotDeviceApi } from '@/api/pms/device'
  267. /** 故障上报 表单 */
  268. defineOptions({ name: 'IotFailureReportForm' })
  269. const { t } = useI18n() // 国际化
  270. const message = useMessage() // 消息弹窗
  271. const dialogVisible = ref(false) // 弹窗的是否展示
  272. const dialogTitle = ref('') // 弹窗的标题
  273. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  274. const formType = ref('') // 表单的类型:create - 新增;update - 修改
  275. const approvalShow = ref(true)
  276. const approver = ref([])
  277. let company = ''
  278. const formData = ref({
  279. failureType: undefined,
  280. typeDesc: undefined,
  281. approvalId: undefined,
  282. id: undefined,
  283. failureCode: undefined,
  284. failureName: undefined,
  285. deviceId: undefined,
  286. deviceName: undefined,
  287. deviceCode: undefined,
  288. status: undefined,
  289. ifStop: undefined,
  290. failureTime: undefined,
  291. dealTime: undefined,
  292. failureInfluence: undefined,
  293. failureSystem: undefined,
  294. description: undefined,
  295. pic: undefined,
  296. ifDeal: undefined,
  297. needHelp: undefined,
  298. solution: undefined,
  299. remark: undefined,
  300. deptId: undefined
  301. })
  302. const dealChange = () => {
  303. formData.value.needHelp = !formData.value.ifDeal
  304. if (!formData.value.ifDeal) {
  305. IotFailureReportApi.getApproval().then((res) => {
  306. approver.value = res
  307. })
  308. }
  309. }
  310. const helpChange = () => {
  311. formData.value.ifDeal = !formData.value.needHelp
  312. }
  313. const deviceChoose = async (row) => {
  314. formData.value.deviceId = row.id
  315. formData.value.deviceName = row.deviceName
  316. formData.value.deviceCode = row.deviceCode
  317. formData.value.deptId = row.deptId
  318. company = await IotDeviceApi.getCompanyByDevice(row.id)
  319. if ('rh' === company) {
  320. approvalShow.value = false
  321. } else {
  322. approvalShow.value = true
  323. }
  324. // deviceLabel.value = row.deviceName
  325. }
  326. const endTimeBlur = () => {
  327. if (formData.value.dealTime <= formData.value.failureTime) {
  328. message.error('故障解决时间不得早于故障时间')
  329. formData.value.dealTime = undefined
  330. }
  331. const now = dayjs()
  332. const target = dayjs(formData.value.dealTime)
  333. if (target.isAfter(now)) {
  334. message.error('故障解决时间不得晚于当前时间')
  335. formData.value.dealTime = undefined
  336. }
  337. }
  338. const handleClear = () => {
  339. formData.value.deviceId = undefined
  340. formData.value.deviceName = undefined
  341. formData.value.deptId = undefined
  342. }
  343. const handleFailureTypeChange = (value: string) => {
  344. if (value !== 'other') {
  345. formData.value.typeDesc = undefined
  346. formRef.value?.clearValidate?.('typeDesc')
  347. }
  348. }
  349. /** 添加/修改操作 */
  350. const deviceFormRef = ref()
  351. const openForm = () => {
  352. deviceFormRef.value.open()
  353. }
  354. const formRules = ref({
  355. failureType: [{ required: true, message: '故障类型不能为空', trigger: 'blur' }],
  356. deviceName: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
  357. ifDeal: [{ required: true, message: '是否解决不能为空', trigger: 'blur' }],
  358. needHelp: [{ required: true, message: '是否协助不能为空', trigger: 'blur' }],
  359. failureTime: [{ required: true, message: '故障时间不能为空', trigger: 'blur' }],
  360. failureSystem: [{ required: true, message: '故障系统不能为空', trigger: 'blur' }],
  361. dealTime: [{ required: true, message: '故障解决时间不能为空', trigger: 'blur' }]
  362. // solution:[{required: ()=> formData.value.ifDeal==='true', message: '解决办法不能为空', trigger: 'blur'}],
  363. })
  364. const solutionRule = [{ required: true, message: '解决办法不能为空', trigger: 'blur' }]
  365. const typeDescRule = [{ required: true, message: '其他故障描述不能为空', trigger: 'blur' }]
  366. const approvalRule = [{ required: true, message: '审批人不能为空', trigger: 'blur' }]
  367. const formRef = ref() // 表单 Ref
  368. const disabled = ref(false)
  369. /** 打开弹窗 */
  370. const open = async (type: string, id?: number, disable?: boolean, fallbackData?: Partial<IotFailureReportVO>) => {
  371. disabled.value = disable
  372. dialogVisible.value = true
  373. dialogTitle.value = t('action.' + type)
  374. formType.value = type
  375. resetForm()
  376. formData.value.deviceId = null
  377. formData.value.deviceName = null
  378. // 修改时,设置数据
  379. if (id) {
  380. formLoading.value = true
  381. try {
  382. formData.value = await IotFailureReportApi.getIotFailureReport(id)
  383. if (formData.value.failureType === 'other' && formData.value.typeDesc == null) {
  384. formData.value.typeDesc = fallbackData?.typeDesc ?? ''
  385. }
  386. } finally {
  387. formLoading.value = false
  388. }
  389. }
  390. }
  391. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  392. /** 提交表单 */
  393. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  394. const submitForm = async () => {
  395. // 校验表单
  396. await formRef.value.validate()
  397. // 提交请求
  398. formLoading.value = true
  399. try {
  400. // formData.value.pic = formData.value.pic.path;
  401. const data = formData.value as unknown as IotFailureReportVO
  402. if (!formData.value.ifDeal) {
  403. if (company !== 'rh') formData.value.approvalId = formData.value.approvalId.join(',')
  404. }
  405. if (formType.value === 'create') {
  406. await IotFailureReportApi.createIotFailureReport(data)
  407. message.success(t('common.createSuccess'))
  408. } else {
  409. await IotFailureReportApi.updateIotFailureReport(data)
  410. message.success(t('common.updateSuccess'))
  411. }
  412. dialogVisible.value = false
  413. // 发送操作成功的事件
  414. emit('success')
  415. } finally {
  416. formLoading.value = false
  417. }
  418. }
  419. /** 重置表单 */
  420. const resetForm = () => {
  421. formData.value = {
  422. failureType: undefined,
  423. typeDesc: undefined,
  424. id: undefined,
  425. failureCode: undefined,
  426. failureName: undefined,
  427. deviceId: undefined,
  428. status: undefined,
  429. ifStop: undefined,
  430. failureTime: undefined,
  431. failureInfluence: undefined,
  432. failureSystem: undefined,
  433. description: undefined,
  434. pic: undefined,
  435. ifDeal: undefined,
  436. needHelp: undefined,
  437. solution: undefined,
  438. remark: undefined
  439. }
  440. formRef.value?.resetFields()
  441. }
  442. </script>
  443. <style lang="scss">
  444. .failure-report-dialog {
  445. .el-dialog {
  446. overflow: hidden;
  447. border-radius: 8px;
  448. }
  449. .el-dialog__header {
  450. min-height: 56px;
  451. padding: 8px 18px;
  452. margin-right: 0 !important;
  453. background: var(--el-bg-color);
  454. border-bottom: 1px solid var(--el-border-color-lighter);
  455. }
  456. .el-dialog__body {
  457. padding: 14px 16px 0 !important;
  458. background: var(--el-bg-color);
  459. }
  460. .el-dialog__footer {
  461. padding: 10px 18px;
  462. background: var(--el-bg-color);
  463. border-top: 1px solid var(--el-border-color-lighter);
  464. }
  465. }
  466. </style>
  467. <style scoped lang="scss">
  468. .dialog-header {
  469. display: flex;
  470. gap: 10px;
  471. align-items: center;
  472. }
  473. .dialog-header__icon {
  474. display: flex;
  475. width: 36px;
  476. height: 36px;
  477. color: var(--el-color-primary);
  478. background: rgb(64 158 255 / 12%);
  479. border: 1px solid rgb(64 158 255 / 18%);
  480. border-radius: 8px;
  481. align-items: center;
  482. justify-content: center;
  483. }
  484. .dialog-title {
  485. font-size: 16px;
  486. font-weight: 600;
  487. line-height: 22px;
  488. color: var(--el-text-color-primary);
  489. }
  490. .dialog-subtitle {
  491. margin-top: 2px;
  492. font-size: 12px;
  493. line-height: 16px;
  494. color: var(--el-text-color-secondary);
  495. }
  496. .failure-report-form {
  497. display: flex;
  498. flex-direction: column;
  499. gap: 10px;
  500. margin-bottom: 16px;
  501. }
  502. .info-card {
  503. overflow: hidden;
  504. background: var(--el-bg-color);
  505. border: 1px solid var(--el-border-color-lighter);
  506. border-radius: 8px;
  507. box-shadow: 0 6px 18px rgb(31 45 61 / 4%);
  508. }
  509. .info-card__header {
  510. display: flex;
  511. min-height: 40px;
  512. padding: 0 16px;
  513. background: linear-gradient(90deg, rgb(64 158 255 / 8%) 0%, rgb(255 255 255 / 0%) 72%);
  514. border-bottom: 1px solid var(--el-border-color-lighter);
  515. align-items: center;
  516. }
  517. .info-card__title {
  518. display: flex;
  519. gap: 8px;
  520. font-size: 15px;
  521. font-weight: 600;
  522. line-height: 22px;
  523. color: var(--el-text-color-primary);
  524. align-items: center;
  525. }
  526. .info-card__marker {
  527. width: 4px;
  528. height: 16px;
  529. background: var(--el-color-primary);
  530. border-radius: 2px;
  531. }
  532. .info-card__body {
  533. padding: 12px 16px 0;
  534. }
  535. .form-control {
  536. width: 100%;
  537. }
  538. .failure-report-form :deep(.el-form-item) {
  539. margin-bottom: 18px;
  540. }
  541. .failure-report-form :deep(.el-form-item__label) {
  542. font-weight: 600;
  543. color: var(--el-text-color-regular);
  544. }
  545. .dialog-footer {
  546. display: flex;
  547. justify-content: flex-end;
  548. gap: 10px;
  549. }
  550. @media (width <= 768px) {
  551. .info-card__header {
  552. min-height: auto;
  553. padding: 10px 14px;
  554. }
  555. .info-card__body {
  556. padding: 12px 14px 0;
  557. }
  558. }
  559. </style>