create-rd-form.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. <script setup lang="ts">
  2. import { FormInstance, FormRules } from 'element-plus'
  3. import { Close } from '@element-plus/icons-vue'
  4. import * as UserApi from '@/api/system/user'
  5. import { useUserStore } from '@/store/modules/user'
  6. import { getStrDictOptions } from '@/utils/dict'
  7. import * as DeptApi from '@/api/system/dept'
  8. import { IotRdDailyReportApi } from '@/api/pms/iotrddailyreport'
  9. import { formatT } from '@/utils/formatTime'
  10. import { defaultProps, handleTree } from '@/utils/tree'
  11. const user = useUserStore().getUser
  12. const deptId = user.deptId
  13. interface Props {
  14. visible: boolean
  15. loadList: () => void
  16. isview: 'detail' | 'create' | 'time'
  17. id?: number
  18. }
  19. const props = withDefaults(defineProps<Props>(), {
  20. visible: false,
  21. isview: 'create'
  22. })
  23. const emits = defineEmits(['update:visible'])
  24. const NON_PROD_FIELDS = [
  25. { key: 'repairTime', label: '设备故障' },
  26. { key: 'selfStopTime', label: '设备保养' },
  27. { key: 'accidentTime', label: '工程质量' },
  28. { key: 'complexityTime', label: '技术受限' },
  29. { key: 'rectificationTime', label: '生产组织' },
  30. { key: 'waitingStopTime', label: '不可抗力' },
  31. { key: 'partyaDesign', label: '甲方设计' },
  32. { key: 'partyaPrepare', label: '甲方准备' },
  33. { key: 'partyaResource', label: '甲方资源' },
  34. { key: 'relocationTime', label: '生产配合' },
  35. { key: 'winterBreakTime', label: '待命' },
  36. { key: 'otherNptTime', label: '其他非生产时间' }
  37. ]
  38. interface Form {
  39. deptId: number
  40. location: string
  41. responsiblePerson: number
  42. timeRange: string[]
  43. rdStatus: string
  44. dailyFuel: number
  45. productionStatus: string
  46. nextPlan: string
  47. externalRental: string
  48. malfunction: string
  49. otherNptReason: string
  50. createTime: number
  51. constructionBrief: string
  52. [key: (typeof NON_PROD_FIELDS)[number]['key']]: any
  53. }
  54. const original = {
  55. timeRange: ['08:00', '08:00'],
  56. ...NON_PROD_FIELDS.reduce((acc, field) => ({ ...acc, [field.key]: 0 }), {}),
  57. winterBreakTime: 24
  58. }
  59. const formRef = ref<FormInstance>()
  60. const form = ref<Partial<Form>>({ ...original })
  61. function noProductionTimeRule() {
  62. return {
  63. validator: (_rule: any, _value: any, callback: any) => {
  64. let totalTime = 0
  65. NON_PROD_FIELDS.forEach((field) => {
  66. const val = parseFloat(form.value[field.key])
  67. if (!isNaN(val)) {
  68. totalTime += val
  69. }
  70. })
  71. const fixedTotal = Number(totalTime.toFixed(2))
  72. if (fixedTotal > 24) {
  73. callback(new Error(`总时间(${fixedTotal}h)不能超过 24 小时`))
  74. } else {
  75. callback()
  76. }
  77. },
  78. trigger: 'blur'
  79. }
  80. }
  81. function handleRowValidate(key: string) {
  82. if (!formRef.value) return
  83. const propsToValidate = NON_PROD_FIELDS.map((field) => field.key)
  84. if (key === 'otherNptTime') propsToValidate.push('otherNptReason')
  85. formRef.value.validateField(propsToValidate)
  86. }
  87. const rules: FormRules = {
  88. deptId: [{ required: true, message: '请选择施工队伍', trigger: ['blur', 'change'] }],
  89. createTime: [{ required: true, message: '请选择施工日期', trigger: ['blur', 'change'] }],
  90. constructionBrief: [{ required: true, message: '请输入施工简报', trigger: ['blur', 'change'] }],
  91. timeRange: [
  92. { required: true, message: '请选择时间节点', trigger: ['blur', 'change'], type: 'array' }
  93. ],
  94. location: [{ required: true, message: '请输入施工地点', trigger: ['blur', 'change'] }],
  95. responsiblePerson: [
  96. { required: true, message: '请选择带班干部', trigger: ['blur', 'change'], type: 'array' }
  97. ],
  98. rdStatus: [{ required: true, message: '请选择施工状态', trigger: ['blur', 'change'] }],
  99. dailyFuel: [{ required: true, message: '请输入当日油耗', trigger: ['blur', 'change'] }],
  100. productionStatus: [{ required: true, message: '请输入生产状态', trigger: ['blur', 'change'] }],
  101. nextPlan: [{ required: true, message: '请输入下计划', trigger: ['blur', 'change'] }]
  102. }
  103. interface UserOptions {
  104. label: string
  105. value: number
  106. raw: any
  107. disabled: boolean
  108. }
  109. const userOptions = ref<UserOptions[]>([])
  110. const deptLoading = ref(false)
  111. const deptOptions = ref<any[]>([])
  112. const submitterNames = ref('')
  113. async function loadDept() {
  114. deptLoading.value = true
  115. try {
  116. function sortTreeBySort(treeNodes: Tree[]) {
  117. if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes
  118. const sortedNodes = [...treeNodes].sort((a, b) => {
  119. const sortA = a.sort != null ? a.sort : 999999
  120. const sortB = b.sort != null ? b.sort : 999999
  121. return sortA - sortB
  122. })
  123. sortedNodes.forEach((node) => {
  124. node.disabled = (node as any).type !== '3'
  125. if (node.children && Array.isArray(node.children)) {
  126. node.children = sortTreeBySort(node.children)
  127. }
  128. })
  129. return sortedNodes
  130. }
  131. const depts = await DeptApi.specifiedSimpleDepts(deptId)
  132. deptOptions.value = sortTreeBySort(handleTree(depts))
  133. } catch (error) {
  134. } finally {
  135. deptLoading.value = false
  136. }
  137. }
  138. async function loadUserOptions() {
  139. const res = await UserApi.selectedDeptsEmployee({ deptIds: [deptId] })
  140. userOptions.value = res.map((item: any) => ({
  141. label: item.nickname,
  142. value: item.id,
  143. raw: item
  144. }))
  145. }
  146. const loading = ref(false)
  147. const detail = ref<any>()
  148. async function load() {
  149. loading.value = true
  150. await loadUserOptions()
  151. await loadDept()
  152. submitterNames.value = user.nickname
  153. if (props.isview && props.id) {
  154. const res = await IotRdDailyReportApi.getIotRdDailyReport(props.id)
  155. detail.value = res
  156. submitterNames.value = res.submitterNames
  157. form.value = {
  158. deptId: res.deptId,
  159. location: res.location,
  160. responsiblePerson: res.responsiblePerson,
  161. timeRange: [formatT(res.startTime), formatT(res.endTime)],
  162. rdStatus: res.rdStatus,
  163. dailyFuel: res.dailyFuel,
  164. productionStatus: res.productionStatus,
  165. nextPlan: res.nextPlan,
  166. externalRental: res.externalRental,
  167. malfunction: res.malfunction,
  168. otherNptReason: res.otherNptReason,
  169. createTime: res.createTime,
  170. constructionBrief: res.constructionBrief,
  171. ...NON_PROD_FIELDS.reduce((acc, field) => ({ ...acc, [field.key]: res[field.key] }), {})
  172. }
  173. }
  174. loading.value = false
  175. }
  176. const rdStatusOptions = getStrDictOptions(DICT_TYPE.PMS_PROJECT_RD_STATUS)
  177. function handleOpenForm() {
  178. form.value = { ...original }
  179. load()
  180. emits('update:visible', true)
  181. }
  182. function handleCloseForm() {
  183. emits('update:visible', false)
  184. }
  185. defineExpose({ handleOpenForm })
  186. const formLoading = ref(false)
  187. const message = useMessage()
  188. async function submitForm() {
  189. if (!formRef.value) return
  190. try {
  191. formLoading.value = true
  192. await formRef.value.validate()
  193. const { timeRange, ...rest } = form.value
  194. const data: any = { ...rest, startTime: timeRange?.[0], endTime: timeRange?.[1] }
  195. if (props.id) {
  196. data.id = props.id
  197. }
  198. await IotRdDailyReportApi.createIotRdDailyReport(data)
  199. if (props.id) message.success('更新成功')
  200. else message.success('新增成功')
  201. handleCloseForm()
  202. props.loadList()
  203. } catch (error) {
  204. console.log('error :>> ', error)
  205. } finally {
  206. formLoading.value = false
  207. }
  208. }
  209. const disabled = computed(() => {
  210. if (props.isview !== 'create') {
  211. if (props.isview === 'time') {
  212. return !(detail.value?.contractName === null && detail.value?.taskName === null)
  213. }
  214. return true
  215. }
  216. return false
  217. })
  218. </script>
  219. <template>
  220. <el-drawer
  221. :model-value="visible"
  222. @update:model-value="emits('update:visible', $event)"
  223. header-class="mb-0!"
  224. :with-header="false"
  225. size="50%"
  226. :close-on-press-escape="false"
  227. :close-on-click-modal="false"
  228. >
  229. <div class="size-full flex flex-col gap-4">
  230. <div class="flex justify-between items-center">
  231. <div class="text-xl font-bold text-gray-900 leading-tight"> 新建日报 </div>
  232. <el-button link @click="handleCloseForm">
  233. <el-icon size="24"><Close /></el-icon>
  234. </el-button>
  235. </div>
  236. <el-form
  237. ref="formRef"
  238. size="default"
  239. label-position="top"
  240. :model="form"
  241. require-asterisk-position="right"
  242. class="flex flex-col"
  243. :rules="rules"
  244. :loading="loading"
  245. :disabled="disabled"
  246. >
  247. <div class="grid grid-cols-2 gap-x-8">
  248. <el-form-item label="施工队伍" prop="deptId">
  249. <el-tree-select
  250. v-model="form.deptId"
  251. :data="deptOptions"
  252. :props="defaultProps"
  253. check-strictly
  254. node-key="id"
  255. filterable
  256. class="w-full"
  257. placeholder="请选择施工队伍"
  258. />
  259. </el-form-item>
  260. <el-form-item label="施工日期" prop="createTime">
  261. <el-date-picker
  262. v-model="form.createTime"
  263. type="date"
  264. clearable
  265. class="w-full!"
  266. placeholder="请选择施工日期"
  267. value-format="x"
  268. />
  269. </el-form-item>
  270. <el-form-item label="施工地点" prop="location">
  271. <el-input v-model="form.location" clearable placeholder="请输入施工地点" />
  272. </el-form-item>
  273. <el-form-item label="带班干部" prop="responsiblePerson">
  274. <el-select
  275. v-model="form.responsiblePerson"
  276. multiple
  277. :options="userOptions"
  278. clearable
  279. filterable
  280. collapse-tags
  281. collapse-tags-tooltip
  282. :max-collapse-tags="5"
  283. tag-type="primary"
  284. placeholder="请选择带班干部"
  285. />
  286. </el-form-item>
  287. <el-form-item label="填报人" prop="submitterNames"> {{ submitterNames }} </el-form-item>
  288. <el-form-item label="时间节点" prop="timeRange">
  289. <el-time-picker
  290. v-model="form.timeRange"
  291. is-range
  292. range-separator="至"
  293. start-placeholder="开始时间"
  294. end-placeholder="结束时间"
  295. placeholder="选择时间范围"
  296. clearable
  297. format="HH:mm"
  298. value-format="HH:mm"
  299. />
  300. </el-form-item>
  301. <el-form-item label="施工状态" prop="rdStatus">
  302. <el-select
  303. v-model="form.rdStatus"
  304. :options="rdStatusOptions"
  305. placeholder="请选择"
  306. class="w-full"
  307. clearable
  308. />
  309. </el-form-item>
  310. <el-form-item label="当日油耗(L)" prop="dailyFuel">
  311. <el-input-number
  312. v-model="form.dailyFuel"
  313. :min="0"
  314. :controls="false"
  315. align="left"
  316. class="w-full!"
  317. placeholder="请输入当日油耗"
  318. >
  319. <template #suffix>升(L)</template>
  320. </el-input-number>
  321. </el-form-item>
  322. <el-form-item label="当日生产动态" prop="productionStatus">
  323. <el-input
  324. v-model="form.productionStatus"
  325. type="textarea"
  326. :autosize="{ minRows: 2 }"
  327. resize="none"
  328. show-word-limit
  329. :maxlength="1000"
  330. placeholder="请输入外组设备"
  331. />
  332. </el-form-item>
  333. <el-form-item label="下步工作计划" prop="nextPlan">
  334. <el-input
  335. v-model="form.nextPlan"
  336. type="textarea"
  337. :autosize="{ minRows: 2 }"
  338. resize="none"
  339. show-word-limit
  340. :maxlength="1000"
  341. placeholder="请输入下步工作计划"
  342. />
  343. </el-form-item>
  344. <el-form-item label="外租设备" prop="externalRental">
  345. <el-input
  346. v-model="form.externalRental"
  347. type="textarea"
  348. :autosize="{ minRows: 2 }"
  349. resize="none"
  350. show-word-limit
  351. :maxlength="1000"
  352. placeholder="请输入外组设备"
  353. />
  354. </el-form-item>
  355. <el-form-item label="故障情况" prop="malfunction">
  356. <el-input
  357. v-model="form.malfunction"
  358. type="textarea"
  359. :autosize="{ minRows: 2 }"
  360. show-word-limit
  361. resize="none"
  362. :maxlength="1000"
  363. placeholder="请输入故障情况"
  364. />
  365. </el-form-item>
  366. <el-form-item class="col-span-2" label="施工简报" prop="constructionBrief">
  367. <el-input
  368. v-model="form.constructionBrief"
  369. type="textarea"
  370. :autosize="{ minRows: 2 }"
  371. show-word-limit
  372. resize="none"
  373. :maxlength="1000"
  374. placeholder="请输入施工简报"
  375. />
  376. </el-form-item>
  377. </div>
  378. <el-divider class="m-0! mb-3! border-2! border-[var(--el-color-primary)]!" />
  379. <h3 class="text-lg font-bold text-gray-800 mb-6 flex items-center gap-2">
  380. <div class="w-1 h-4 bg-blue-600 rounded-full"></div>
  381. 非生产时间
  382. </h3>
  383. <div class="grid grid-cols-4 gap-x-8">
  384. <el-form-item
  385. v-for="field in NON_PROD_FIELDS"
  386. :key="field.key"
  387. :label="field.label"
  388. :prop="field.key"
  389. :rules="noProductionTimeRule()"
  390. >
  391. <el-input-number
  392. v-model="form[field.key]"
  393. :min="0"
  394. :max="24"
  395. :controls="false"
  396. class="w-full!"
  397. align="left"
  398. @blur="handleRowValidate(field.key)"
  399. :disabled="isview === 'detail'"
  400. >
  401. <template #suffix>小时(H)</template>
  402. </el-input-number>
  403. </el-form-item>
  404. <el-form-item
  405. class="col-span-4"
  406. label="其他非生产原因"
  407. prop="otherNptReason"
  408. :rules="{
  409. required: form.otherNptTime > 0,
  410. message: '请填写原因',
  411. trigger: ['blur', 'change']
  412. }"
  413. >
  414. <el-input
  415. v-model="form.otherNptReason"
  416. type="textarea"
  417. :autosize="{ minRows: 2 }"
  418. resize="none"
  419. show-word-limit
  420. :maxlength="1000"
  421. placeholder="当'其他非生产时间'大于0时必填"
  422. :disabled="isview === 'detail'"
  423. />
  424. </el-form-item>
  425. </div>
  426. </el-form>
  427. </div>
  428. <template #footer>
  429. <el-button
  430. :disabled="isview === 'detail'"
  431. size="default"
  432. type="primary"
  433. @click="submitForm"
  434. :loading="formLoading"
  435. >
  436. 确 定
  437. </el-button>
  438. <el-button size="default" @click="emits('update:visible', false)">取 消</el-button>
  439. </template>
  440. </el-drawer>
  441. </template>
  442. <style scoped>
  443. :deep(.el-form-item__label) {
  444. font-weight: 500;
  445. }
  446. </style>