|
|
@@ -0,0 +1,873 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
+import * as DeptApi from '@/api/system/dept'
|
|
|
+import { IotMaintenancePlanApi } from '@/api/pms/maintenance'
|
|
|
+import { dayjs, FormInstance, FormRules } from 'element-plus'
|
|
|
+import { IotMaintenanceBomApi } from '@/api/pms/iotmaintenancebom'
|
|
|
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
|
|
|
+import { DeviceList, List, Plan, Query } from './types'
|
|
|
+import { IotDeviceApi } from '@/api/pms/device'
|
|
|
+import MaintenancePlanForm from './maintenance-plan-form.vue'
|
|
|
+import maintenanceDeviceList from './maintenance-device-list.vue'
|
|
|
+import { useTagsViewStore } from '@/store/modules/tagsView'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const router = useRouter()
|
|
|
+const { delView } = useTagsViewStore()
|
|
|
+const message = useMessage()
|
|
|
+const { t } = useI18n()
|
|
|
+
|
|
|
+const mode = computed(() => {
|
|
|
+ if (route.name === 'IotMaintenancePlanDetail') return 'detail'
|
|
|
+ if (route.name === 'IotMainPlanEdit') return 'edit'
|
|
|
+ return 'create'
|
|
|
+})
|
|
|
+
|
|
|
+const isReadonly = computed(() => mode.value === 'detail')
|
|
|
+
|
|
|
+const id = computed(() => route.params.id as string | undefined)
|
|
|
+
|
|
|
+const initQuery = (): Query => ({
|
|
|
+ deviceIds: undefined,
|
|
|
+ planId: undefined,
|
|
|
+ bomFlag: 'b'
|
|
|
+})
|
|
|
+
|
|
|
+const queryParams = ref<Query>(initQuery())
|
|
|
+
|
|
|
+const initPlan = (): Plan => ({
|
|
|
+ name: '',
|
|
|
+ remark: '',
|
|
|
+ responsiblePerson: '',
|
|
|
+ serialNumber: ''
|
|
|
+})
|
|
|
+
|
|
|
+const list = ref<List[]>([])
|
|
|
+
|
|
|
+const { ZmTable, ZmTableColumn } = useTableComponents<List>()
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const panelLoading = ref(false)
|
|
|
+const saving = ref(false)
|
|
|
+const plan = ref<Plan>(initPlan())
|
|
|
+const deviceIds = ref<Set<number>>(new Set())
|
|
|
+const originalRowMap = ref(new Map<string, List>())
|
|
|
+const attrs = ref(
|
|
|
+ new Map<string, { timeAccumulatedAttrs: any[]; mileageAccumulatedAttrs: any[] }>()
|
|
|
+)
|
|
|
+const planRef = ref<FormInstance>()
|
|
|
+const planRules = reactive<FormRules>({
|
|
|
+ name: [{ required: true, message: '计划名称不能为空', trigger: ['blur', 'change'] }]
|
|
|
+})
|
|
|
+
|
|
|
+const getRowKey = (row: Pick<List, 'deviceId' | 'bomNodeId'>) => `${row.deviceId}-${row.bomNodeId}`
|
|
|
+
|
|
|
+const cloneRow = (row: List): List => ({
|
|
|
+ ...row
|
|
|
+})
|
|
|
+
|
|
|
+const refreshOriginalRows = () => {
|
|
|
+ const currentKeys = new Set(list.value.map(getRowKey))
|
|
|
+
|
|
|
+ list.value.forEach((row) => {
|
|
|
+ const key = getRowKey(row)
|
|
|
+ originalRowMap.value.set(key, cloneRow(row))
|
|
|
+ })
|
|
|
+
|
|
|
+ Array.from(originalRowMap.value.keys()).forEach((key) => {
|
|
|
+ if (!currentKeys.has(key)) {
|
|
|
+ originalRowMap.value.delete(key)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const resetAttrs = (rows: List[]) => {
|
|
|
+ attrs.value.clear()
|
|
|
+ rows.forEach((row: any) => {
|
|
|
+ attrs.value.set(getRowKey(row), {
|
|
|
+ timeAccumulatedAttrs: row.timeAccumulatedAttrs || [],
|
|
|
+ mileageAccumulatedAttrs: row.mileageAccumulatedAttrs || []
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const syncListMeta = () => {
|
|
|
+ deviceIds.value = new Set(list.value.map((item) => item.deviceId))
|
|
|
+ sortList()
|
|
|
+}
|
|
|
+
|
|
|
+const resetPageRows = (rows: List[]) => {
|
|
|
+ list.value = rows
|
|
|
+ resetAttrs(rows)
|
|
|
+ refreshOriginalRows()
|
|
|
+ syncListMeta()
|
|
|
+}
|
|
|
+
|
|
|
+const initPage = async () => {
|
|
|
+ panelLoading.value = true
|
|
|
+
|
|
|
+ plan.value = initPlan()
|
|
|
+ queryParams.value = initQuery()
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (mode.value === 'create') {
|
|
|
+ const deptId = useUserStore().getUser.deptId
|
|
|
+ const dept = await DeptApi.getDept(deptId)
|
|
|
+ plan.value.name = `${dept?.name || ''} - 保养计划`
|
|
|
+ plan.value.deptId = deptId
|
|
|
+
|
|
|
+ const { wsCache } = useCache()
|
|
|
+ const userInfo = wsCache.get(CACHE_KEY.USER)
|
|
|
+ plan.value.responsiblePerson = userInfo.user.id
|
|
|
+ } else if (id.value) {
|
|
|
+ const res = await IotMaintenancePlanApi.getIotMaintenancePlan(Number(id.value))
|
|
|
+ plan.value = res
|
|
|
+
|
|
|
+ queryParams.value.planId = Number(id.value)
|
|
|
+ const data = await IotMaintenanceBomApi.getMainPlanBOMs(queryParams.value)
|
|
|
+ resetPageRows(data || [])
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ panelLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const sortList = () => {
|
|
|
+ list.value = list.value.sort((a, b) => {
|
|
|
+ const aCode = a.deviceCode || ''
|
|
|
+ const bCode = b.deviceCode || ''
|
|
|
+ const aName = a.name || ''
|
|
|
+ const bName = b.name || ''
|
|
|
+
|
|
|
+ if (aCode !== bCode) {
|
|
|
+ return aCode.localeCompare(bCode)
|
|
|
+ }
|
|
|
+
|
|
|
+ return aName.localeCompare(bName)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => route.fullPath,
|
|
|
+ () => {
|
|
|
+ initPage()
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => list.value.length,
|
|
|
+ () => {
|
|
|
+ syncListMeta()
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+const runningTimeVisible = computed(() => list.value.some((item) => item.runningTimeRule === 0))
|
|
|
+const mileageVisible = computed(() => list.value.some((item) => item.mileageRule === 0))
|
|
|
+const naturalDateVisible = computed(() => list.value.some((item) => item.naturalDateRule === 0))
|
|
|
+
|
|
|
+const EMPTY_TEXT = '——'
|
|
|
+type DisplayValue = number | string
|
|
|
+
|
|
|
+const isPositiveNumber = (value: unknown): value is number => typeof value === 'number' && value > 0
|
|
|
+
|
|
|
+const toFixedNumber = (value: number, fractionDigits = 2) =>
|
|
|
+ parseFloat(value.toFixed(fractionDigits))
|
|
|
+
|
|
|
+const emptyFormatter = (row: List, keys: (keyof List)[], isTime: boolean = false) => {
|
|
|
+ const data = [...keys].reverse().find((key) => row[key] !== null && row[key] !== undefined)
|
|
|
+ const value = data ? row[data] : EMPTY_TEXT
|
|
|
+
|
|
|
+ return value !== EMPTY_TEXT && isTime ? dayjs(value as number).format('YYYY-MM-DD') : value
|
|
|
+}
|
|
|
+
|
|
|
+const positiveField = (row: List, key: keyof List) => {
|
|
|
+ const value = row[key]
|
|
|
+ return isPositiveNumber(value) ? value : null
|
|
|
+}
|
|
|
+
|
|
|
+const positiveDisplayValue = (row: List, keys: (keyof List)[]) => {
|
|
|
+ const value = emptyFormatter(row, keys)
|
|
|
+ return isPositiveNumber(value) ? value : null
|
|
|
+}
|
|
|
+
|
|
|
+const ruleValue = (
|
|
|
+ rule: List['runningTimeRule'],
|
|
|
+ values: Array<number | null>,
|
|
|
+ calculator: (...values: number[]) => DisplayValue
|
|
|
+) => {
|
|
|
+ const numericValues = values.filter(isPositiveNumber)
|
|
|
+ return rule === 0 && numericValues.length === values.length
|
|
|
+ ? calculator(...numericValues)
|
|
|
+ : EMPTY_TEXT
|
|
|
+}
|
|
|
+
|
|
|
+const nextMaintenanceH = (row: List) =>
|
|
|
+ ruleValue(
|
|
|
+ row.runningTimeRule,
|
|
|
+ [positiveField(row, 'lastRunningTime'), positiveField(row, 'nextRunningTime')],
|
|
|
+ (last, next) => last + next
|
|
|
+ )
|
|
|
+
|
|
|
+const remainH = (row: List) =>
|
|
|
+ ruleValue(
|
|
|
+ row.runningTimeRule,
|
|
|
+ [
|
|
|
+ positiveField(row, 'lastRunningTime'),
|
|
|
+ positiveField(row, 'nextRunningTime'),
|
|
|
+ positiveDisplayValue(row, ['totalRunTime', 'tempTotalRunTime'])
|
|
|
+ ],
|
|
|
+ (last, next, current) => toFixedNumber(next - (current - last))
|
|
|
+ )
|
|
|
+
|
|
|
+const nextMaintenanceKm = (row: List) =>
|
|
|
+ ruleValue(
|
|
|
+ row.mileageRule,
|
|
|
+ [positiveField(row, 'lastRunningKilometers'), positiveField(row, 'nextRunningKilometers')],
|
|
|
+ (last, next) => last + next
|
|
|
+ )
|
|
|
+
|
|
|
+const remainKm = (row: List) =>
|
|
|
+ ruleValue(
|
|
|
+ row.mileageRule,
|
|
|
+ [
|
|
|
+ positiveField(row, 'lastRunningKilometers'),
|
|
|
+ positiveField(row, 'nextRunningKilometers'),
|
|
|
+ positiveDisplayValue(row, ['totalMileage', 'tempTotalMileage'])
|
|
|
+ ],
|
|
|
+ (last, next, current) => toFixedNumber(next - (current - last))
|
|
|
+ )
|
|
|
+
|
|
|
+const nextNaturalDateValue = (row: List) => {
|
|
|
+ if (row.naturalDateRule !== 0 || !row.lastNaturalDate || !row.nextNaturalDate) return null
|
|
|
+ return dayjs(row.lastNaturalDate).add(row.nextNaturalDate, 'day')
|
|
|
+}
|
|
|
+
|
|
|
+const nextMaintenanceDate = (row: List) =>
|
|
|
+ nextNaturalDateValue(row)?.format('YYYY-MM-DD') ?? EMPTY_TEXT
|
|
|
+
|
|
|
+const remainDay = (row: List) =>
|
|
|
+ isPositiveNumber(row.nextNaturalDate)
|
|
|
+ ? (nextNaturalDateValue(row)?.diff(dayjs(), 'day') ?? EMPTY_TEXT)
|
|
|
+ : EMPTY_TEXT
|
|
|
+
|
|
|
+const hasEnabledRule = (row: List) =>
|
|
|
+ row.runningTimeRule === 0 || row.mileageRule === 0 || row.naturalDateRule === 0
|
|
|
+
|
|
|
+const requiresTimeAccumulatedAttr = (row: List) => {
|
|
|
+ const rowAttrs = attrs.value.get(getRowKey(row))
|
|
|
+ return (
|
|
|
+ row.runningTimeRule === 0 &&
|
|
|
+ (rowAttrs?.timeAccumulatedAttrs.length ?? 0) > 0 &&
|
|
|
+ (row.totalRunTime == null || isNaN(row.totalRunTime))
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const requiresMileageAccumulatedAttr = (row: List) => {
|
|
|
+ const rowAttrs = attrs.value.get(getRowKey(row))
|
|
|
+ return (
|
|
|
+ row.mileageRule === 0 &&
|
|
|
+ (rowAttrs?.mileageAccumulatedAttrs.length ?? 0) > 0 &&
|
|
|
+ (row.totalMileage == null || isNaN(row.totalMileage))
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const checkRowFilled = (row: List) => {
|
|
|
+ if (!hasEnabledRule(row)) return false
|
|
|
+
|
|
|
+ const runningTimeFilled =
|
|
|
+ row.runningTimeRule !== 0 ||
|
|
|
+ (isPositiveNumber(row.lastRunningTime) &&
|
|
|
+ isPositiveNumber(row.nextRunningTime) &&
|
|
|
+ isPositiveNumber(row.timePeriodLead) &&
|
|
|
+ (!requiresTimeAccumulatedAttr(row) || !!row.code))
|
|
|
+
|
|
|
+ const mileageFilled =
|
|
|
+ row.mileageRule !== 0 ||
|
|
|
+ (isPositiveNumber(row.lastRunningKilometers) &&
|
|
|
+ isPositiveNumber(row.nextRunningKilometers) &&
|
|
|
+ isPositiveNumber(row.kiloCycleLead) &&
|
|
|
+ (!requiresMileageAccumulatedAttr(row) || !!row.type))
|
|
|
+
|
|
|
+ const naturalDateFilled =
|
|
|
+ row.naturalDateRule !== 0 ||
|
|
|
+ (!!row.lastNaturalDate &&
|
|
|
+ isPositiveNumber(row.nextNaturalDate) &&
|
|
|
+ isPositiveNumber(row.naturalDatePeriodLead))
|
|
|
+
|
|
|
+ return runningTimeFilled && mileageFilled && naturalDateFilled
|
|
|
+}
|
|
|
+
|
|
|
+const cellClassName = ({ row, column }: { row: List; column: { type?: string } }) =>
|
|
|
+ column.type === 'index' && checkRowFilled(row) ? 'all-filled' : ''
|
|
|
+
|
|
|
+const getRowDisplayName = (row: List) => `${row.deviceCode || '-'}-${row.name || '-'}`
|
|
|
+
|
|
|
+const validateTableData = () => {
|
|
|
+ if (list.value.length === 0) {
|
|
|
+ message.error('请至少添加一条设备保养明细')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const [index, row] of list.value.entries()) {
|
|
|
+ const rowNumber = index + 1
|
|
|
+ const rowName = getRowDisplayName(row)
|
|
|
+
|
|
|
+ if (!hasEnabledRule(row)) {
|
|
|
+ message.error(`第 ${rowNumber} 行(${rowName}):保养项至少设置1个保养规则`)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (row.runningTimeRule === 0) {
|
|
|
+ if (
|
|
|
+ !isPositiveNumber(row.lastRunningTime) ||
|
|
|
+ !isPositiveNumber(row.nextRunningTime) ||
|
|
|
+ !isPositiveNumber(row.timePeriodLead)
|
|
|
+ ) {
|
|
|
+ message.error(`第 ${rowNumber} 行(${rowName}):请完整配置运行时长规则`)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (requiresTimeAccumulatedAttr(row) && !row.code) {
|
|
|
+ message.error(`第 ${rowNumber} 行(${rowName}):请选择累计运行时长参数`)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (row.mileageRule === 0) {
|
|
|
+ if (
|
|
|
+ !isPositiveNumber(row.lastRunningKilometers) ||
|
|
|
+ !isPositiveNumber(row.nextRunningKilometers) ||
|
|
|
+ !isPositiveNumber(row.kiloCycleLead)
|
|
|
+ ) {
|
|
|
+ message.error(`第 ${rowNumber} 行(${rowName}):请完整配置运行里程规则`)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (requiresMileageAccumulatedAttr(row) && !row.type) {
|
|
|
+ message.error(`第 ${rowNumber} 行(${rowName}):请选择累计运行公里数参数`)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ row.naturalDateRule === 0 &&
|
|
|
+ (!row.lastNaturalDate ||
|
|
|
+ !isPositiveNumber(row.nextNaturalDate) ||
|
|
|
+ !isPositiveNumber(row.naturalDatePeriodLead))
|
|
|
+ ) {
|
|
|
+ message.error(`第 ${rowNumber} 行(${rowName}):请完整配置自然日期规则`)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const close = () => {
|
|
|
+ delView(unref(router.currentRoute))
|
|
|
+ router.push({ name: 'IotMaintenancePlan', params: {} })
|
|
|
+}
|
|
|
+
|
|
|
+const savePlan = async () => {
|
|
|
+ if (isReadonly.value) return
|
|
|
+ if (!planRef.value) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ await planRef.value.validate()
|
|
|
+ } catch {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!validateTableData()) return
|
|
|
+
|
|
|
+ saving.value = true
|
|
|
+ try {
|
|
|
+ const data = {
|
|
|
+ mainPlan: plan.value,
|
|
|
+ mainPlanBom: list.value
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mode.value === 'create') {
|
|
|
+ await IotMaintenancePlanApi.createIotMaintenancePlan(data)
|
|
|
+ message.success(t('common.createSuccess'))
|
|
|
+ } else {
|
|
|
+ await IotMaintenancePlanApi.updatePlan(data)
|
|
|
+ message.success(t('common.updateSuccess'))
|
|
|
+ }
|
|
|
+
|
|
|
+ close()
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type RuleType = 'runningTime' | 'mileage' | 'naturalDate'
|
|
|
+type RuleField =
|
|
|
+ | 'tempTotalRunTime'
|
|
|
+ | 'lastRunningTime'
|
|
|
+ | 'nextRunningTime'
|
|
|
+ | 'timePeriodLead'
|
|
|
+ | 'tempTotalMileage'
|
|
|
+ | 'lastRunningKilometers'
|
|
|
+ | 'nextRunningKilometers'
|
|
|
+ | 'kiloCycleLead'
|
|
|
+ | 'lastNaturalDate'
|
|
|
+ | 'nextNaturalDate'
|
|
|
+ | 'naturalDatePeriodLead'
|
|
|
+ | 'code'
|
|
|
+ | 'type'
|
|
|
+
|
|
|
+const ruleFieldMap: Record<RuleType, RuleField[]> = {
|
|
|
+ runningTime: ['lastRunningTime', 'nextRunningTime', 'timePeriodLead', 'code', 'tempTotalRunTime'],
|
|
|
+ mileage: [
|
|
|
+ 'lastRunningKilometers',
|
|
|
+ 'nextRunningKilometers',
|
|
|
+ 'kiloCycleLead',
|
|
|
+ 'type',
|
|
|
+ 'tempTotalMileage'
|
|
|
+ ],
|
|
|
+ naturalDate: ['lastNaturalDate', 'nextNaturalDate', 'naturalDatePeriodLead']
|
|
|
+}
|
|
|
+
|
|
|
+const clearRuleFields = (row: List, fields: RuleField[]) => {
|
|
|
+ fields.forEach((field) => {
|
|
|
+ row[field] = null
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const restoreRuleFields = (row: List, fields: RuleField[]) => {
|
|
|
+ const originalRow = originalRowMap.value.get(getRowKey(row))
|
|
|
+ if (!originalRow) return
|
|
|
+
|
|
|
+ fields.forEach((field) => {
|
|
|
+ row[field] = originalRow[field] as any
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleRuleChange = (row: List, ruleType: RuleType, value: unknown) => {
|
|
|
+ const fields = ruleFieldMap[ruleType]
|
|
|
+
|
|
|
+ if (value === 1) {
|
|
|
+ clearRuleFields(row, fields)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ restoreRuleFields(row, fields)
|
|
|
+}
|
|
|
+
|
|
|
+const deviceList = ref()
|
|
|
+
|
|
|
+const onDeviceSelect = () => {
|
|
|
+ if (isReadonly.value) return
|
|
|
+ deviceList.value.open()
|
|
|
+}
|
|
|
+
|
|
|
+const deviceChoose = async (rows: DeviceList[]) => {
|
|
|
+ const selectIds = rows.map((item) => item.id)
|
|
|
+ const params = {
|
|
|
+ deviceIds: selectIds.join(','),
|
|
|
+ bomFlag: 'b'
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await IotDeviceApi.deviceAssociateBomList(params)
|
|
|
+
|
|
|
+ if (res.length === 0) {
|
|
|
+ message.error('选择的设备不存在待保养BOM项')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Array.isArray(res)) return
|
|
|
+
|
|
|
+ const existingKeys = new Set(list.value.map((item) => `${item.deviceId}-${item.bomNodeId}`))
|
|
|
+
|
|
|
+ const items = res
|
|
|
+ .filter((item) => !existingKeys.has(`${item.id}-${item.bomNodeId}`))
|
|
|
+ .map<List>((item) => {
|
|
|
+ const row: List = {
|
|
|
+ deviceId: item.id,
|
|
|
+ bomNodeId: item.bomNodeId,
|
|
|
+ deviceCode: item.deviceCode,
|
|
|
+ deviceName: item.deviceName,
|
|
|
+ name: item.name,
|
|
|
+ runningTimeRule: 1,
|
|
|
+ mileageRule: 1,
|
|
|
+ naturalDateRule: 1,
|
|
|
+ totalRunTime: item.totalRunTime,
|
|
|
+ tempTotalRunTime: null,
|
|
|
+ totalMileage: item.totalMileage,
|
|
|
+ tempTotalMileage: null,
|
|
|
+ lastMaintenanceDate: null,
|
|
|
+ lastRunningTime: null,
|
|
|
+ nextRunningTime: null,
|
|
|
+ lastRunningKilometers: null,
|
|
|
+ nextRunningKilometers: null,
|
|
|
+ lastNaturalDate: null,
|
|
|
+ nextNaturalDate: null,
|
|
|
+ kiloCycleLead: null,
|
|
|
+ timePeriodLead: null,
|
|
|
+ naturalDatePeriodLead: null,
|
|
|
+ code: item.code ?? null,
|
|
|
+ type: item.type ?? null
|
|
|
+ }
|
|
|
+
|
|
|
+ attrs.value.set(getRowKey(row), {
|
|
|
+ timeAccumulatedAttrs: item.timeAccumulatedAttrs || [],
|
|
|
+ mileageAccumulatedAttrs: item.mileageAccumulatedAttrs || []
|
|
|
+ })
|
|
|
+
|
|
|
+ return row
|
|
|
+ })
|
|
|
+
|
|
|
+ list.value.push(...items)
|
|
|
+ refreshOriginalRows()
|
|
|
+ syncListMeta()
|
|
|
+ } catch (error) {
|
|
|
+ message.error('获取设备关联保养BOM项失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDelete = (str: string) => {
|
|
|
+ list.value = list.value.filter((item) => `${item.deviceId}-${item.bomNodeId}` !== str)
|
|
|
+}
|
|
|
+
|
|
|
+const planForm = ref()
|
|
|
+
|
|
|
+const editPlan = (row: List) => {
|
|
|
+ if (row.runningTimeRule !== 0 && row.mileageRule !== 0 && row.naturalDateRule !== 0) {
|
|
|
+ message.error('请先设置保养规则')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const rowAttrs = attrs.value.get(getRowKey(row))
|
|
|
+ planForm.value?.open({
|
|
|
+ row,
|
|
|
+ readonly: isReadonly.value,
|
|
|
+ timeAccumulatedAttrs: rowAttrs?.timeAccumulatedAttrs || [],
|
|
|
+ mileageAccumulatedAttrs: rowAttrs?.mileageAccumulatedAttrs || []
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ v-loading="panelLoading"
|
|
|
+ class="flex flex-col gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
|
|
|
+ <section class="panel">
|
|
|
+ <div class="plan-info-accent"></div>
|
|
|
+ <el-form
|
|
|
+ :model="plan"
|
|
|
+ size="default"
|
|
|
+ :rules="planRules"
|
|
|
+ label-width="88px"
|
|
|
+ class="plan-info-form"
|
|
|
+ ref="planRef">
|
|
|
+ <el-row :gutter="24">
|
|
|
+ <el-col :xs="24" :md="13">
|
|
|
+ <el-form-item :label="t('main.planName')" prop="name">
|
|
|
+ <el-input v-model="plan.name" :disabled="isReadonly" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :md="11">
|
|
|
+ <el-form-item :label="t('main.planCode')" prop="serialNumber">
|
|
|
+ <el-input v-model="plan.serialNumber" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="24" class="mt-2">
|
|
|
+ <el-form-item :label="t('iotMaintain.remark')" prop="remark">
|
|
|
+ <el-input
|
|
|
+ v-model="plan.remark"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ :placeholder="t('iotMaintain.remarkHolder')"
|
|
|
+ :disabled="isReadonly"
|
|
|
+ resize="none"
|
|
|
+ show-word-limit
|
|
|
+ maxlength="300" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ </section>
|
|
|
+ <section class="panel content-panel flex-1!">
|
|
|
+ <div v-if="!isReadonly" class="operation">
|
|
|
+ <el-button size="default" type="success" @click="onDeviceSelect">
|
|
|
+ <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="table">
|
|
|
+ <el-auto-resizer class="absolute">
|
|
|
+ <template #default="{ width, height }">
|
|
|
+ <ZmTable
|
|
|
+ :data="list"
|
|
|
+ :loading="loading"
|
|
|
+ :width="width"
|
|
|
+ :height="height"
|
|
|
+ :highlight-current-row="false"
|
|
|
+ :cell-class-name="cellClassName">
|
|
|
+ <ZmTableColumn
|
|
|
+ type="index"
|
|
|
+ :label="t('iotDevice.serial')"
|
|
|
+ fixed="left"
|
|
|
+ hide-in-column-settings />
|
|
|
+ <ZmTableColumn
|
|
|
+ :min-width="120"
|
|
|
+ prop="deviceCode"
|
|
|
+ :label="t('iotMaintain.deviceCode')" />
|
|
|
+ <ZmTableColumn
|
|
|
+ :min-width="240"
|
|
|
+ prop="deviceName"
|
|
|
+ :label="t('iotMaintain.deviceName')" />
|
|
|
+ <ZmTableColumn :min-width="240" prop="name" :label="t('bomList.bomNode')" />
|
|
|
+ <ZmTableColumn prop="runningTimeRule" width="66" :label="t('main.runTime')">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch
|
|
|
+ v-model="row.runningTimeRule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ :disabled="isReadonly"
|
|
|
+ @change="(value) => handleRuleChange(row, 'runningTime', value)" />
|
|
|
+ </template>
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn prop="mileageRule" width="66" :label="t('main.mileage')">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch
|
|
|
+ v-model="row.mileageRule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ :disabled="isReadonly"
|
|
|
+ @change="(value) => handleRuleChange(row, 'mileage', value)" />
|
|
|
+ </template>
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn prop="naturalDateRule" width="66" :label="t('main.date')">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch
|
|
|
+ v-model="row.naturalDateRule"
|
|
|
+ :active-value="0"
|
|
|
+ :inactive-value="1"
|
|
|
+ :disabled="isReadonly"
|
|
|
+ @change="(value) => handleRuleChange(row, 'naturalDate', value)" />
|
|
|
+ </template>
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="totalRunTime"
|
|
|
+ :label="t('operationFillForm.sumTime')"
|
|
|
+ width="108"
|
|
|
+ :real-value="(row) => emptyFormatter(row, ['totalRunTime', 'tempTotalRunTime'])"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="totalMileage"
|
|
|
+ :label="t('operationFillForm.sumKil')"
|
|
|
+ width="128"
|
|
|
+ :real-value="(row) => emptyFormatter(row, ['totalMileage', 'tempTotalMileage'])"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="lastMaintenanceDate"
|
|
|
+ :label="t('mainPlan.lastMaintenanceDate')"
|
|
|
+ width="90"
|
|
|
+ :real-value="(row) => emptyFormatter(row, ['lastMaintenanceDate'], true)"
|
|
|
+ cover-formatter />
|
|
|
+
|
|
|
+ <ZmTableColumn
|
|
|
+ :visible="runningTimeVisible"
|
|
|
+ column-key="time-group"
|
|
|
+ label="保养时长"
|
|
|
+ is-parent>
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="lastRunningTime"
|
|
|
+ :label="t('mainPlan.lastMaintenanceOperationTime')"
|
|
|
+ :real-value="(row) => emptyFormatter(row, ['lastRunningTime'])"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="nextMaintenanceH"
|
|
|
+ :label="t('mainPlan.nextMaintenanceH')"
|
|
|
+ :real-value="nextMaintenanceH"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="remainH"
|
|
|
+ :label="t('mainPlan.remainH')"
|
|
|
+ :real-value="remainH"
|
|
|
+ cover-formatter />
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn
|
|
|
+ :visible="mileageVisible"
|
|
|
+ column-key="mileage-group"
|
|
|
+ label="保养里程"
|
|
|
+ is-parent>
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="lastRunningKilometers"
|
|
|
+ :label="t('mainPlan.lastMaintenanceMileage')"
|
|
|
+ :real-value="(row) => emptyFormatter(row, ['lastRunningKilometers'])"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="nextRunningKilometers"
|
|
|
+ :label="t('mainPlan.nextMaintenanceKm')"
|
|
|
+ :real-value="nextMaintenanceKm"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="remainKmometers"
|
|
|
+ :label="t('mainPlan.remainKm')"
|
|
|
+ :real-value="remainKm"
|
|
|
+ cover-formatter />
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn
|
|
|
+ :visible="naturalDateVisible"
|
|
|
+ column-key="date-group"
|
|
|
+ label="保养日期"
|
|
|
+ is-parent>
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="lastNaturalDate"
|
|
|
+ :label="t('mainPlan.lastMaintenanceNaturalDate')"
|
|
|
+ :real-value="(row) => emptyFormatter(row, ['lastNaturalDate'], true)"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="nextMaintenanceDate"
|
|
|
+ :label="t('mainPlan.nextMaintDate')"
|
|
|
+ :real-value="nextMaintenanceDate"
|
|
|
+ cover-formatter />
|
|
|
+ <ZmTableColumn
|
|
|
+ prop="remainDay"
|
|
|
+ :label="t('mainPlan.remainDay')"
|
|
|
+ :real-value="remainDay"
|
|
|
+ cover-formatter />
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn
|
|
|
+ column-key="operation"
|
|
|
+ :label="t('operationFill.operation')"
|
|
|
+ :width="140"
|
|
|
+ fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button size="default" link type="primary" @click="editPlan(row)">
|
|
|
+ <div class="i-lucide:edit-3 mr-1 translate-y-1px"></div>
|
|
|
+ {{ isReadonly ? t('form.set') : t('modelTemplate.update') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="default"
|
|
|
+ v-if="!isReadonly"
|
|
|
+ link
|
|
|
+ type="danger"
|
|
|
+ @click="handleDelete(`${row.deviceId}-${row.bomNodeId}`)">
|
|
|
+ <div class="i-lucide:x mr-1 translate-y-1px"></div>
|
|
|
+ {{ t('modelTemplate.delete') }}
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </ZmTableColumn>
|
|
|
+ </ZmTable>
|
|
|
+ </template>
|
|
|
+ </el-auto-resizer>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ <section class="panel footer-panel">
|
|
|
+ <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
|
|
|
+ <el-button v-if="!isReadonly" type="primary" :loading="saving" @click="savePlan">{{
|
|
|
+ t('iotMaintain.save')
|
|
|
+ }}</el-button>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <maintenance-device-list ref="deviceList" @choose="deviceChoose" />
|
|
|
+ <maintenance-plan-form ref="planForm" @saved="refreshOriginalRows" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.panel {
|
|
|
+ position: relative;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ padding: 18px 20px 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-accent {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ left: 0;
|
|
|
+ height: 3px;
|
|
|
+ background: linear-gradient(
|
|
|
+ 90deg,
|
|
|
+ var(--el-color-primary),
|
|
|
+ var(--el-color-success),
|
|
|
+ var(--el-color-warning)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form :deep(.el-form-item) {
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form :deep(.el-form-item__label) {
|
|
|
+ height: 32px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 32px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form :deep(.el-input__wrapper),
|
|
|
+.plan-info-form :deep(.el-textarea__inner) {
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 0 0 1px var(--el-border-color-light) inset;
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form :deep(.el-input__wrapper:hover),
|
|
|
+.plan-info-form :deep(.el-textarea__inner:hover) {
|
|
|
+ box-shadow: 0 0 0 1px var(--el-border-color) inset;
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form :deep(.el-input.is-disabled .el-input__wrapper) {
|
|
|
+ background-color: var(--el-fill-color-lighter);
|
|
|
+}
|
|
|
+
|
|
|
+.plan-info-form :deep(.el-textarea__inner) {
|
|
|
+ min-height: 76px !important;
|
|
|
+ padding-top: 8px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+.content-panel {
|
|
|
+ display: flex;
|
|
|
+ min-height: 0;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.operation {
|
|
|
+ display: flex;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.table {
|
|
|
+ position: relative;
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1 1 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.footer-panel {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 12px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.zm-table .all-filled) {
|
|
|
+ background-color: #67c23a !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.zm-table .all-filled .cell) {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+@media (width <= 768px) {
|
|
|
+ .panel {
|
|
|
+ padding: 16px 14px 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|