You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/mes/plan/PlanForm.vue

555 lines
19 KiB
Vue

<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item :label="t('ProductionPlan.Plan.dialogTaskLabel')" prop="taskId">
<el-select
disabled
v-model="formData.taskId"
clearable
filterable
:placeholder="t('ProductionPlan.Plan.dialogTaskPlaceholder')"
@change="handleTaskChange"
>
<el-option v-for="item in taskList" :key="item.id" :label="item.code" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogTaskDetailLabel')" prop="taskDetailId">
<el-select
disabled
v-model="formData.taskDetailId"
clearable
filterable
:placeholder="t('ProductionPlan.Plan.dialogTaskDetailPlaceholder')"
@change="handleTaskDetailChange"
>
<el-option
v-for="item in taskDetailList"
:key="item.id"
:label="`${item.productName}-${t('ProductionPlan.Plan.dialogTaskDetailUnplannedPrefix')}${Number(item.number || 0) - Number(item.planNumber || 0)}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item prop="code">
<template #label>
<span>
{{ t('ProductionPlan.Plan.dialogCodeLabel') }}
<el-tooltip :content="t('ProductionPlan.Plan.dialogCodeTooltip')" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-row :gutter="10" class="!w-full">
<el-col :xs="24" :sm="18" :md="16" :lg="14" :xl="12">
<el-input
:disabled="formData.isCode === true || formType === 'update'"
v-model="formData.code"
:placeholder="t('ProductionPlan.Plan.dialogCodePlaceholder')"
/>
</el-col>
<el-col :xs="24" :sm="6" :md="4" :lg="3" :xl="2">
<el-switch v-model="formData.isCode" :disabled="formType === 'update'" />
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogDeliveryDateLabel')">
<el-input :model-value="deliveryDateText" disabled />
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogProcessRouteLabel')" prop="processRouteId">
<el-input
:model-value="processRouteDisplayName"
disabled
:placeholder="t('ProductionPlan.Plan.dialogProcessRouteEmptyText')"
/>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogProcessRouteItemsLabel')">
<el-table
:data="formData.processRouteItems"
border
size="small"
row-key="processRouteItemId"
class="process-route-items-table"
:empty-text="t('ProductionPlan.Plan.dialogProcessRouteEmptyText')"
>
<el-table-column :label="t('ProductionPlan.Plan.routeItemSortColumn')" prop="sort" width="70" align="center" />
<el-table-column
:label="t('ProductionPlan.Plan.routeItemParameterCodeColumn')"
prop="processParameterCode"
min-width="140"
align="center"
/>
<el-table-column
:label="t('ProductionPlan.Plan.routeItemParameterNameColumn')"
prop="processParameterName"
min-width="160"
align="center"
/>
<el-table-column :label="t('ProductionPlan.Plan.routeItemRemarkColumn')" prop="remark" min-width="140" align="center" />
<el-table-column :label="t('ProductionPlan.Plan.routeItemDeviceColumn')" min-width="240" align="center">
<template #default="scope">
<el-input
:model-value="getRouteItemDeviceDisplay(scope.row)"
:placeholder="t('ProductionPlan.Plan.routeItemDevicePlaceholder')"
readonly
clearable
@click="openDeviceSelectDialog(scope.row)"
@clear="clearRouteItemDevice(scope.row)"
>
<template #append>
<el-button @click.stop="openDeviceSelectDialog(scope.row)">
{{ t('ProductionPlan.Plan.routeItemDeviceSelectButton') }}
</el-button>
</template>
</el-input>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogPlanNumberLabel')" prop="planNumber">
<el-input v-model="formData.planNumber" />
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogPlanStartLabel')" prop="planStartTime">
<el-date-picker
v-model="formData.planStartTime"
type="datetime"
value-format="x"
:placeholder="t('ProductionPlan.Plan.dialogPlanStartPlaceholder')"
class="!w-full"
@change="handlePlanStartTimeChange"
/>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogPlanEndLabel')" prop="planEndTime">
<el-date-picker
v-model="formData.planEndTime"
type="datetime"
value-format="x"
:placeholder="t('ProductionPlan.Plan.dialogPlanEndPlaceholder')"
class="!w-full"
@change="handlePlanEndTimeChange"
/>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogLatestStartLabel')" prop="latestStartTime">
<el-date-picker
v-model="formData.latestStartTime"
type="datetime"
value-format="x"
:disabled-date="latestStartDisabledDate"
:placeholder="t('ProductionPlan.Plan.dialogLatestStartPlaceholder')"
class="!w-full"
/>
</el-form-item>
<el-form-item :label="t('ProductionPlan.Plan.dialogRemarkLabel')" prop="remark">
<el-input
type="textarea"
v-model="formData.remark"
:placeholder="t('ProductionPlan.Plan.dialogRemarkPlaceholder')"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
{{ t('ProductionPlan.Plan.dialogSubmitButtonText') }}
</el-button>
<el-button @click="dialogVisible = false">
{{ t('ProductionPlan.Plan.dialogCancelButtonText') }}
</el-button>
</template>
</Dialog>
<TableSelectDialog
ref="deviceSelectDialogRef"
:title="t('ProductionPlan.Plan.selectDeviceDialogTitle')"
:columns="deviceColumns"
:fetch-api="fetchDeviceLedgerPage"
selection-type="single"
row-key="id"
@confirm="handleDeviceSelectConfirm"
/>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { PlanApi, PlanProcessRouteItemVO, PlanVO } from '@/api/mes/plan'
import { TaskApi, TaskDetailVO, TaskVO } from '@/api/mes/task'
import { DeviceLedgerApi } from '@/api/mes/deviceledger'
import { ErpProductCategoryProcessRouteItemRespVO, ProductApi } from '@/api/erp/product/product'
import TableSelectDialog from '@/components/TableSelectDialog/TableSelectDialog.vue'
defineOptions({ name: 'PlanForm' })
type PlanFormData = Partial<PlanVO> & {
isCode?: boolean
finishDate?: string | number
deliveryDate?: string | number
processRouteItems?: PlanProcessRouteItemVO[]
}
const taskList = ref<TaskVO[]>([])
const taskDetailList = ref<TaskDetailVO[]>([])
const maxUnplannedNumber = ref<number | undefined>(undefined)
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref<PlanFormData>({})
const formRef = ref()
const deviceSelectDialogRef = ref()
const activeRouteItemRow = ref<PlanProcessRouteItemVO>()
const deliveryDateText = computed(() => {
const sourceValue = formData.value.finishDate || formData.value.deliveryDate
if (!sourceValue) return ''
const value = dayjs(sourceValue)
if (!value.isValid()) return String(sourceValue)
return value.format('YYYY-MM-DD')
})
const processRouteDisplayName = computed(() => {
if (!formData.value.processRouteId) return ''
return formData.value.processRouteName || String(formData.value.processRouteId)
})
const latestStartDisabledDate = (date: Date) => {
if (!formData.value.finishDate) return false
const deliveryDate = dayjs(formData.value.finishDate)
if (!deliveryDate.isValid()) return false
return dayjs(date).startOf('day').valueOf() > deliveryDate.startOf('day').valueOf()
}
const validateLatestStartTime = (_rule: any, value: any, callback: (error?: Error) => void) => {
if (!value || !formData.value.finishDate) {
callback()
return
}
const latestStart = dayjs(value)
const deliveryDate = dayjs(formData.value.finishDate)
if (!latestStart.isValid() || !deliveryDate.isValid()) {
callback()
return
}
if (latestStart.startOf('day').valueOf() > deliveryDate.startOf('day').valueOf()) {
callback(new Error(t('ProductionPlan.Plan.validatorLatestStartAfterDelivery')))
return
}
callback()
}
const validatePlanStartTime = (_rule: any, value: any, callback: (error?: Error) => void) => {
if (!value || !formData.value.planEndTime) {
callback()
return
}
const startTime = Number(value)
const endTime = Number(formData.value.planEndTime)
if (Number.isNaN(startTime) || Number.isNaN(endTime)) {
callback()
return
}
if (startTime > endTime) {
callback(new Error(t('ProductionPlan.Plan.validatorPlanStartBeforeEnd')))
return
}
callback()
}
const validatePlanEndTime = (_rule: any, value: any, callback: (error?: Error) => void) => {
if (!value || !formData.value.planStartTime) {
callback()
return
}
const endTime = Number(value)
const startTime = Number(formData.value.planStartTime)
if (Number.isNaN(endTime) || Number.isNaN(startTime)) {
callback()
return
}
if (endTime < startTime) {
callback(new Error(t('ProductionPlan.Plan.validatorPlanEndAfterStart')))
return
}
callback()
}
const getSelectedTaskDetailUnplanned = () => {
const detail = taskDetailList.value.find(
(item: any) => Number(item.id) === Number(formData.value.taskDetailId)
) as any
if (!detail) return undefined
const total = Number(detail.number)
const planned = Number(detail.planNumber)
if (!Number.isFinite(total) || !Number.isFinite(planned)) return undefined
return Math.max(total - planned, 0)
}
const getPlanNumberLimit = () => {
const detailUnplanned = getSelectedTaskDetailUnplanned()
if (detailUnplanned !== undefined) return detailUnplanned
return maxUnplannedNumber.value
}
const validatePlanNumber = (_rule: any, value: any, callback: (error?: Error) => void) => {
if (value === undefined || value === null || value === '') {
callback()
return
}
const currentPlanNumber = Number(value)
if (!Number.isFinite(currentPlanNumber)) {
callback(new Error(t('ProductionPlan.Plan.validatorPlanNumberNumeric')))
return
}
const planNumberLimit = getPlanNumberLimit()
if (planNumberLimit !== undefined && currentPlanNumber > planNumberLimit) {
callback(new Error(t('ProductionPlan.Plan.validatorPlanNumberLimit', { limit: planNumberLimit })))
return
}
callback()
}
const formRules = reactive({
taskDetailId: [{ required: true, message: t('ProductionPlan.Plan.validatorTaskDetailRequired'), trigger: 'blur' }],
taskId: [{ required: true, message: t('ProductionPlan.Plan.validatorTaskRequired'), trigger: 'blur' }],
planNumber: [
{ required: true, message: t('ProductionPlan.Plan.validatorPlanNumberRequired'), trigger: 'blur' },
{ validator: validatePlanNumber, trigger: ['blur', 'change'] }
],
planStartTime: [
{ required: true, message: t('ProductionPlan.Plan.validatorPlanStartRequired'), trigger: 'blur' },
{ validator: validatePlanStartTime, trigger: 'change' }
],
planEndTime: [
{ required: true, message: t('ProductionPlan.Plan.validatorPlanEndRequired'), trigger: 'blur' },
{ validator: validatePlanEndTime, trigger: 'change' }
],
latestStartTime: [
{ required: true, message: t('ProductionPlan.Plan.validatorLatestStartRequired'), trigger: 'blur' },
{ validator: validateLatestStartTime, trigger: 'change' }
]
})
const deviceColumns = [
{ label: t('ProductionPlan.Plan.deviceColumnCode'), prop: 'deviceCode', minWidth: 140 },
{ label: t('ProductionPlan.Plan.deviceColumnName'), prop: 'deviceName', minWidth: 160 },
{ label: t('ProductionPlan.Plan.deviceColumnModel'), prop: 'deviceModel', minWidth: 140 },
{ label: t('ProductionPlan.Plan.deviceColumnWorkshop'), prop: 'workshopName', minWidth: 140 }
]
const fetchDeviceLedgerPage = (params: Record<string, any>) => {
return DeviceLedgerApi.getDeviceLedgerPage({
...params,
isScheduled: 1
})
}
const getRouteItemDeviceDisplay = (row: PlanProcessRouteItemVO) => {
if (!row?.deviceId) return ''
return row.deviceName || String(row.deviceId)
}
const openDeviceSelectDialog = (row: PlanProcessRouteItemVO) => {
activeRouteItemRow.value = row
const rows = row.deviceId
? [
{
id: Number(row.deviceId),
deviceName: row.deviceName
}
]
: []
deviceSelectDialogRef.value?.open(rows)
}
const handleDeviceSelectConfirm = (payload: { ids: (number | string)[]; rows: any[] }) => {
const row = payload.rows?.[0]
if (!row || !activeRouteItemRow.value) return
const deviceId = Number(row.id)
if (!Number.isFinite(deviceId)) return
activeRouteItemRow.value.deviceId = deviceId
activeRouteItemRow.value.deviceName = row.deviceName || row.name || row.code || `ID:${deviceId}`
}
const clearRouteItemDevice = (row: PlanProcessRouteItemVO) => {
row.deviceId = undefined
row.deviceName = undefined
}
const syncFinishDateByTaskDetail = () => {
const detail = taskDetailList.value.find((item: any) => Number(item.id) === Number(formData.value.taskDetailId)) as any
formData.value.finishDate = detail?.finishDate || detail?.deliveryDate || undefined
formData.value.deliveryDate = detail?.deliveryDate || formData.value.deliveryDate
}
const normalizeProcessRouteItems = (items?: Array<ErpProductCategoryProcessRouteItemRespVO | PlanProcessRouteItemVO>) => {
if (!Array.isArray(items)) return []
return items.map((item, index) => ({
id: (item as any).id,
categoryId: (item as any).categoryId,
processRouteId: (item as any).processRouteId,
processRouteItemId: (item as any).processRouteItemId || (item as any).id,
sort: item.sort || index + 1,
processParameterId: item.processParameterId,
processParameterCode: item.processParameterCode,
processParameterName: item.processParameterName,
remark: item.remark,
deviceId: (item as any).deviceId,
deviceName: (item as any).deviceName
}))
}
const loadProductProcessRoute = async (productId?: number | string) => {
formData.value.processRouteId = undefined
formData.value.processRouteName = undefined
formData.value.processRouteItems = []
if (!productId) return
const data = await ProductApi.getProductProcessRoute(productId)
formData.value.processRouteId = data?.processRouteId
formData.value.processRouteName = data?.processRouteName
formData.value.processRouteItems = normalizeProcessRouteItems(data?.processRouteItems)
}
const open = async (
type: string,
id?: number,
taskId?: number,
productId?: number,
number?: number,
taskDetailId?: string,
taskDeliveryDate?: string | number
) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
formLoading.value = true
try {
maxUnplannedNumber.value = number !== undefined ? Math.max(Number(number), 0) : undefined
formData.value.deliveryDate = taskDeliveryDate || undefined
if (productId) {
formData.value.productId = productId
}
if (!id) {
await loadProductProcessRoute(productId)
}
if (taskId) {
formData.value.taskId = taskId
taskDetailList.value = await TaskApi.getTaskDetailListByTaskId(formData.value.taskId)
if (taskDetailId) {
const taskDetails = taskDetailList.value.filter((item) => String(item.id) === String(taskDetailId))
if (taskDetails?.[0]?.id) {
formData.value.taskDetailId = taskDetails[0].id
formData.value.finishDate = (taskDetails[0] as any).finishDate || undefined
formData.value.deliveryDate = (taskDetails[0] as any).deliveryDate || taskDeliveryDate || undefined
}
}
}
if (number) {
formData.value.planNumber = number
formData.value.reyaNumber = number
}
if (!taskList.value.length) {
taskList.value = await TaskApi.getPlanTaskList()
}
if (id) {
const data = await PlanApi.getPlan(id)
formData.value = {
...data,
isCode: data.isCode ?? false,
processRouteItems: normalizeProcessRouteItems((data as any).processRouteItems)
}
}
} finally {
formLoading.value = false
}
}
defineExpose({ open })
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
try {
const data = {
...formData.value,
processRouteItems: normalizeProcessRouteItems(formData.value.processRouteItems)
} as unknown as PlanVO
if (formType.value === 'create') {
await PlanApi.createPlan(data)
message.success(t('common.createSuccess'))
} else {
await PlanApi.updatePlan(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
const resetForm = () => {
formData.value = {
id: undefined,
code: undefined,
productId: undefined,
taskDetailId: undefined,
taskId: undefined,
planNumber: 0,
reyaNumber: undefined,
finishNumber: 0,
status: undefined,
planStartTime: undefined,
planEndTime: undefined,
startTime: undefined,
endTime: undefined,
productionManagerId: undefined,
remark: undefined,
groupType: undefined,
isEnable: undefined,
isPreProduction: 0,
isCode: true,
latestStartTime: undefined,
finishDate: undefined,
deliveryDate: undefined,
processRouteId: undefined,
processRouteName: undefined,
processRouteItems: []
}
maxUnplannedNumber.value = undefined
formRef.value?.resetFields()
}
const handleTaskChange = async () => {
taskDetailList.value = await TaskApi.getTaskDetailListByTaskId(formData.value.taskId)
formData.value.taskDetailId = undefined
formData.value.finishDate = undefined
formRef.value?.validateField('planNumber')
}
const handleTaskDetailChange = async () => {
syncFinishDateByTaskDetail()
formRef.value?.validateField('planNumber')
}
const handlePlanStartTimeChange = () => {
formRef.value?.validateField(['planStartTime', 'planEndTime'])
}
const handlePlanEndTimeChange = () => {
formRef.value?.validateField(['planStartTime', 'planEndTime'])
}
</script>
<style scoped>
.process-route-items-table {
width: 100%;
}
</style>