maintenance-plan-form.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. <script setup lang="ts">
  2. import type { FormInstance, FormRules } from 'element-plus'
  3. import { List } from './types'
  4. const { t } = useI18n()
  5. const visible = ref(false)
  6. type AccumulatedAttr = {
  7. pointName: string
  8. totalRunTime?: number | null
  9. }
  10. interface Props {
  11. row: List
  12. readonly: boolean
  13. timeAccumulatedAttrs: AccumulatedAttr[]
  14. mileageAccumulatedAttrs: AccumulatedAttr[]
  15. }
  16. const form = ref<List>({
  17. deviceId: 0,
  18. bomNodeId: '',
  19. deviceCode: '',
  20. deviceName: '',
  21. name: '',
  22. runningTimeRule: 0,
  23. mileageRule: 0,
  24. naturalDateRule: 0,
  25. totalRunTime: null,
  26. tempTotalRunTime: null,
  27. totalMileage: null,
  28. tempTotalMileage: null,
  29. lastMaintenanceDate: null,
  30. lastRunningTime: null,
  31. nextRunningTime: null,
  32. lastRunningKilometers: null,
  33. nextRunningKilometers: null,
  34. lastNaturalDate: null,
  35. nextNaturalDate: null,
  36. kiloCycleLead: null,
  37. timePeriodLead: null,
  38. naturalDatePeriodLead: null,
  39. code: null,
  40. type: null
  41. })
  42. const isReadonly = ref<boolean>(false)
  43. const sourceRow = ref<List>()
  44. const formRef = ref<FormInstance>()
  45. const timeAccumulatedAttrs = ref<AccumulatedAttr[]>([])
  46. const mileageAccumulatedAttrs = ref<AccumulatedAttr[]>([])
  47. const timeEnable = computed(() => form.value.runningTimeRule === 0)
  48. const mileageEnable = computed(() => form.value.mileageRule === 0)
  49. const naturalDateEnable = computed(() => form.value.naturalDateRule === 0)
  50. const timeAccumulatedVisible = computed(
  51. () =>
  52. timeEnable.value &&
  53. timeAccumulatedAttrs.value.length > 0 &&
  54. (form.value.totalRunTime == null || isNaN(form.value.totalRunTime))
  55. )
  56. const mileageAccumulatedVisible = computed(
  57. () =>
  58. mileageEnable.value &&
  59. mileageAccumulatedAttrs.value.length > 0 &&
  60. (form.value.totalMileage == null || isNaN(form.value.totalMileage))
  61. )
  62. const accumulatedVisible = computed(
  63. () => timeAccumulatedVisible.value || mileageAccumulatedVisible.value
  64. )
  65. type NumberFieldProp =
  66. | 'lastRunningTime'
  67. | 'nextRunningTime'
  68. | 'timePeriodLead'
  69. | 'lastRunningKilometers'
  70. | 'nextRunningKilometers'
  71. | 'kiloCycleLead'
  72. | 'nextNaturalDate'
  73. | 'naturalDatePeriodLead'
  74. type DateFieldProp = 'lastNaturalDate'
  75. type NumberFieldConfig = {
  76. label: string
  77. prop: NumberFieldProp
  78. type: 'number'
  79. precision?: number
  80. }
  81. type DateFieldConfig = {
  82. label: string
  83. prop: DateFieldProp
  84. type: 'date'
  85. placeholder?: string
  86. }
  87. type FieldConfig = NumberFieldConfig | DateFieldConfig
  88. type ConditionalFieldConfig = FieldConfig & { visible?: boolean }
  89. type SectionConfig = {
  90. key: string
  91. title: string
  92. visible: boolean
  93. fields: FieldConfig[]
  94. }
  95. const enabledRuleCount = computed(
  96. () => [timeEnable.value, mileageEnable.value, naturalDateEnable.value].filter(Boolean).length
  97. )
  98. const enabledFields = (fields: ConditionalFieldConfig[]): FieldConfig[] =>
  99. fields.filter(({ visible = true }) => visible)
  100. const ruleSections = computed<SectionConfig[]>(() => [
  101. {
  102. key: 'basic',
  103. title: t('mainPlan.basicMaintenanceRecords'),
  104. visible: enabledRuleCount.value > 0,
  105. fields: enabledFields([
  106. {
  107. label: t('mainPlan.lastMaintenanceOperationTime'),
  108. prop: 'lastRunningTime',
  109. type: 'number',
  110. precision: 1,
  111. visible: timeEnable.value
  112. },
  113. {
  114. label: t('mainPlan.lastMaintenanceMileage'),
  115. prop: 'lastRunningKilometers',
  116. type: 'number',
  117. precision: 2,
  118. visible: mileageEnable.value
  119. },
  120. {
  121. label: t('mainPlan.lastMaintenanceNaturalDate'),
  122. prop: 'lastNaturalDate',
  123. type: 'date',
  124. placeholder: '选择日期',
  125. visible: naturalDateEnable.value
  126. }
  127. ])
  128. },
  129. {
  130. key: 'time',
  131. title: t('mainPlan.RunTimeRuleConfiguration'),
  132. visible: timeEnable.value,
  133. fields: [
  134. {
  135. label: t('mainPlan.RunTimeCycle'),
  136. prop: 'nextRunningTime',
  137. type: 'number',
  138. precision: 1
  139. },
  140. {
  141. label: t('mainPlan.RunTimeCycle_Lead'),
  142. prop: 'timePeriodLead',
  143. type: 'number',
  144. precision: 1
  145. }
  146. ]
  147. },
  148. {
  149. key: 'mileage',
  150. title: t('mainPlan.operatingMileageRuleConfiguration'),
  151. visible: mileageEnable.value,
  152. fields: [
  153. {
  154. label: t('mainPlan.operatingMileageCycle'),
  155. prop: 'nextRunningKilometers',
  156. type: 'number',
  157. precision: 2
  158. },
  159. {
  160. label: t('mainPlan.OperatingMileageCycle_lead'),
  161. prop: 'kiloCycleLead',
  162. type: 'number',
  163. precision: 2
  164. }
  165. ]
  166. },
  167. {
  168. key: 'natural-date',
  169. title: t('mainPlan.NaturalDayRuleConfig'),
  170. visible: naturalDateEnable.value,
  171. fields: [
  172. {
  173. label: t('mainPlan.NaturalDailyCycle'),
  174. prop: 'nextNaturalDate',
  175. type: 'number'
  176. },
  177. {
  178. label: t('mainPlan.NaturalDailyCycle_Lead'),
  179. prop: 'naturalDatePeriodLead',
  180. type: 'number'
  181. }
  182. ]
  183. }
  184. ])
  185. const cloneRow = (row: List): List => ({
  186. ...row
  187. })
  188. const isPositiveRequiredValue = (value: unknown) => typeof value === 'number' && value > 0
  189. const positiveNumberRule = (label: string) => ({
  190. required: true,
  191. validator: (_rule: unknown, value: unknown, callback: (error?: Error) => void) => {
  192. if (isPositiveRequiredValue(value)) {
  193. callback()
  194. return
  195. }
  196. callback(new Error(`请填写${label}`))
  197. },
  198. trigger: ['blur', 'change']
  199. })
  200. const requiredRule = (message: string) => ({
  201. required: true,
  202. message,
  203. trigger: ['blur', 'change']
  204. })
  205. const formRules = computed<FormRules>(() => {
  206. const rules: FormRules = {}
  207. for (const section of ruleSections.value) {
  208. if (!section.visible) continue
  209. for (const field of section.fields) {
  210. rules[field.prop] =
  211. field.type === 'number'
  212. ? [positiveNumberRule(field.label)]
  213. : [requiredRule(`请选择${field.label}`)]
  214. }
  215. }
  216. if (timeAccumulatedVisible.value) {
  217. rules.code = [requiredRule('请选择累计运行时长')]
  218. }
  219. if (mileageAccumulatedVisible.value) {
  220. rules.type = [requiredRule('请选择累计运行公里数')]
  221. }
  222. return rules
  223. })
  224. const emit = defineEmits<{
  225. saved: []
  226. }>()
  227. const close = () => {
  228. visible.value = false
  229. }
  230. const save = async () => {
  231. if (isReadonly.value) {
  232. close()
  233. return
  234. }
  235. if (!sourceRow.value) return
  236. if (!formRef.value) return
  237. try {
  238. await formRef.value.validate()
  239. } catch {
  240. return
  241. }
  242. const data = cloneRow(form.value)
  243. if (timeAccumulatedVisible.value) {
  244. const selectedTimeAttr = timeAccumulatedAttrs.value.find((item) => item.pointName === data.code)
  245. data.tempTotalRunTime = selectedTimeAttr?.totalRunTime ?? null
  246. }
  247. if (mileageAccumulatedVisible.value) {
  248. const selectedMileageAttr = mileageAccumulatedAttrs.value.find(
  249. (item) => item.pointName === data.type
  250. )
  251. data.tempTotalMileage = selectedMileageAttr?.totalRunTime ?? null
  252. }
  253. Object.assign(sourceRow.value, data)
  254. emit('saved')
  255. close()
  256. }
  257. const open = ({
  258. row,
  259. readonly,
  260. timeAccumulatedAttrs: timeAttrs,
  261. mileageAccumulatedAttrs: mileageAttrs
  262. }: Props) => {
  263. sourceRow.value = row
  264. form.value = cloneRow(row)
  265. isReadonly.value = readonly
  266. timeAccumulatedAttrs.value = timeAttrs || []
  267. mileageAccumulatedAttrs.value = mileageAttrs || []
  268. visible.value = true
  269. nextTick(() => {
  270. formRef.value?.clearValidate()
  271. })
  272. }
  273. defineExpose({
  274. open
  275. })
  276. </script>
  277. <template>
  278. <Dialog v-model="visible" title="保养项配置" width="720px" :close-on-click-modal="false">
  279. <el-form
  280. ref="formRef"
  281. size="default"
  282. :model="form"
  283. :rules="formRules"
  284. label-position="top"
  285. class="maintenance-config-form">
  286. <div class="config-summary">
  287. <div class="summary-item">
  288. <span class="summary-label">{{ t('iotMaintain.deviceCode') }}</span>
  289. <span class="summary-value">{{ form.deviceCode || '-' }}</span>
  290. </div>
  291. <div class="summary-item">
  292. <span class="summary-label">{{ t('iotMaintain.deviceName') }}</span>
  293. <span class="summary-value">{{ form.deviceName || '-' }}</span>
  294. </div>
  295. <div class="summary-item summary-item-full">
  296. <span class="summary-label">{{ t('bomList.bomNode') }}</span>
  297. <span class="summary-value">{{ form.name || '-' }}</span>
  298. </div>
  299. </div>
  300. <el-empty v-if="enabledRuleCount === 0" :image-size="88" description="当前保养项未启用规则" />
  301. <section
  302. v-for="section in ruleSections"
  303. v-show="section.visible"
  304. :key="section.key"
  305. class="config-section">
  306. <div class="section-header">
  307. <div class="section-title">{{ section.title }}</div>
  308. </div>
  309. <el-row :gutter="16">
  310. <el-col v-for="field in section.fields" :key="field.prop" :xs="24" :sm="12">
  311. <el-form-item :label="field.label" :prop="field.prop">
  312. <el-input-number
  313. v-if="field.type === 'number'"
  314. v-model="form[field.prop]"
  315. :precision="field.precision"
  316. :min="0"
  317. :controls="false"
  318. :disabled="isReadonly"
  319. class="w-full!" />
  320. <el-date-picker
  321. v-else
  322. v-model="form[field.prop]"
  323. type="date"
  324. :placeholder="field.placeholder"
  325. value-format="x"
  326. :disabled="isReadonly"
  327. class="w-full!" />
  328. </el-form-item>
  329. </el-col>
  330. </el-row>
  331. </section>
  332. <section v-if="accumulatedVisible" class="config-section">
  333. <div class="section-header">
  334. <div class="section-title">{{ t('mainPlan.accumulatedParams') }}</div>
  335. </div>
  336. <el-row :gutter="16">
  337. <el-col v-if="timeAccumulatedVisible" :xs="24" :sm="12">
  338. <el-form-item :label="t('mainPlan.accumulatedRunTime')" prop="code">
  339. <el-select
  340. v-model="form.code"
  341. placeholder="请选择累计运行时长"
  342. clearable
  343. :disabled="isReadonly"
  344. class="w-full!">
  345. <el-option
  346. v-for="(item, index) in timeAccumulatedAttrs"
  347. :key="`time-${item.pointName}-${index}`"
  348. :label="item.pointName"
  349. :value="item.pointName" />
  350. </el-select>
  351. </el-form-item>
  352. </el-col>
  353. <el-col v-if="mileageAccumulatedVisible" :xs="24" :sm="12">
  354. <el-form-item :label="t('mainPlan.accumulatedMileage')" prop="type">
  355. <el-select
  356. v-model="form.type"
  357. placeholder="请选择累计运行公里数"
  358. clearable
  359. :disabled="isReadonly"
  360. class="w-full!">
  361. <el-option
  362. v-for="(item, index) in mileageAccumulatedAttrs"
  363. :key="`mileage-${item.pointName}-${index}`"
  364. :label="item.pointName"
  365. :value="item.pointName" />
  366. </el-select>
  367. </el-form-item>
  368. </el-col>
  369. </el-row>
  370. </section>
  371. </el-form>
  372. <template #footer>
  373. <el-button @click="close">{{ t('common.cancel') }}</el-button>
  374. <el-button v-if="!isReadonly" type="primary" @click="save">{{ t('common.save') }}</el-button>
  375. </template>
  376. </Dialog>
  377. </template>
  378. <style scoped lang="scss">
  379. .maintenance-config-form {
  380. display: flex;
  381. flex-direction: column;
  382. gap: 14px;
  383. }
  384. .config-summary {
  385. display: grid;
  386. grid-template-columns: repeat(2, minmax(0, 1fr));
  387. gap: 10px 14px;
  388. padding: 12px 14px;
  389. background: var(--el-fill-color-light);
  390. border: 1px solid var(--el-border-color-lighter);
  391. border-radius: 8px;
  392. }
  393. .summary-item {
  394. min-width: 0;
  395. }
  396. .summary-item-full {
  397. grid-column: 1 / -1;
  398. }
  399. .summary-label {
  400. display: block;
  401. margin-bottom: 4px;
  402. font-size: 12px;
  403. line-height: 1.2;
  404. color: var(--el-text-color-secondary);
  405. }
  406. .summary-value {
  407. display: block;
  408. overflow: hidden;
  409. font-size: 14px;
  410. font-weight: 500;
  411. line-height: 1.4;
  412. color: var(--el-text-color-primary);
  413. text-overflow: ellipsis;
  414. white-space: nowrap;
  415. }
  416. .config-section {
  417. padding: 14px 14px 2px;
  418. background: var(--el-bg-color);
  419. border: 1px solid var(--el-border-color-lighter);
  420. border-radius: 8px;
  421. }
  422. .section-header {
  423. display: flex;
  424. align-items: center;
  425. justify-content: space-between;
  426. margin-bottom: 12px;
  427. }
  428. .section-title {
  429. position: relative;
  430. padding-left: 10px;
  431. font-size: 14px;
  432. font-weight: 600;
  433. line-height: 1.4;
  434. color: var(--el-text-color-primary);
  435. }
  436. .section-title::before {
  437. position: absolute;
  438. top: 3px;
  439. bottom: 3px;
  440. left: 0;
  441. width: 3px;
  442. background: var(--el-color-primary);
  443. border-radius: 3px;
  444. content: '';
  445. }
  446. .form-control {
  447. width: 100% !important;
  448. }
  449. .maintenance-config-form :deep(.el-form-item) {
  450. margin-bottom: 14px;
  451. }
  452. .maintenance-config-form :deep(.el-form-item__label) {
  453. margin-bottom: 6px;
  454. font-size: 13px;
  455. line-height: 1.4;
  456. color: var(--el-text-color-regular);
  457. }
  458. .maintenance-config-form :deep(.el-input__wrapper) {
  459. border-radius: 6px;
  460. box-shadow: 0 0 0 1px var(--el-border-color-light) inset;
  461. }
  462. .maintenance-config-form :deep(.el-input__wrapper:hover) {
  463. box-shadow: 0 0 0 1px var(--el-border-color) inset;
  464. }
  465. .maintenance-config-form :deep(.el-input.is-disabled .el-input__wrapper) {
  466. background-color: var(--el-fill-color-lighter);
  467. }
  468. @media (width <= 640px) {
  469. .config-summary {
  470. grid-template-columns: 1fr;
  471. }
  472. }
  473. </style>