<script setup lang="ts">
|
import type { BatchSchedulePreview, BatchScheduleRequest } from '#/api/mes/pro/task';
|
import { computed, ref, watch } from 'vue';
|
import { useVbenModal } from '@vben/common-ui';
|
import { Alert, Button, Checkbox, DatePicker, Descriptions, Table, Tag, message } from 'ant-design-vue';
|
import dayjs from 'dayjs';
|
import { executeBatchSchedule, previewBatchSchedule } from '#/api/mes/pro/task';
|
|
const emit = defineEmits(['success']);
|
const ids = ref<number[]>([]);
|
function defaultStartTime() {
|
let value = dayjs().add(5, 'minute').startOf('minute');
|
if (value.hour() >= 18) value = value.add(1, 'day').hour(9).minute(0).second(0);
|
else if (value.hour() < 9) value = value.hour(9).minute(0).second(0);
|
while (value.day() === 0 || value.day() === 6) value = value.add(1, 'day').hour(9).minute(0).second(0);
|
return value;
|
}
|
const startTime = ref(defaultStartTime());
|
const preview = ref<BatchSchedulePreview>();
|
const loading = ref(false);
|
const executing = ref(false);
|
const acceptLate = ref(false);
|
const errorText = ref('');
|
let sequence = 0;
|
let open = false;
|
const canConfirm = computed(() => !!preview.value?.canExecute && !loading.value && !executing.value
|
&& (!preview.value.warnings.length || acceptLate.value));
|
|
const [Modal, modalApi] = useVbenModal({
|
confirmText: '确认排产',
|
confirmDisabled: true,
|
onBeforeClose: () => !executing.value,
|
onConfirm: execute,
|
async onOpenChange(isOpen) {
|
open = isOpen;
|
sequence++;
|
if (!isOpen) {
|
preview.value = undefined;
|
ids.value = [];
|
loading.value = false;
|
errorText.value = '';
|
return;
|
}
|
ids.value = modalApi.getData<{ workOrderIds: number[] }>().workOrderIds;
|
startTime.value = defaultStartTime();
|
await calculate();
|
},
|
});
|
|
watch(canConfirm, (value) => modalApi.setState({ confirmDisabled: !value }));
|
watch(startTime, () => {
|
sequence++;
|
preview.value = undefined;
|
acceptLate.value = false;
|
loading.value = false;
|
errorText.value = '';
|
}, { flush: 'sync' });
|
|
function request(): BatchScheduleRequest {
|
return { workOrderIds: [...ids.value], startTime: startTime.value.format('YYYY-MM-DD HH:mm:ss') };
|
}
|
|
async function calculate() {
|
if (executing.value || !startTime.value || !ids.value.length) return;
|
const current = ++sequence;
|
preview.value = undefined;
|
acceptLate.value = false;
|
errorText.value = '';
|
loading.value = true;
|
try {
|
const result = await previewBatchSchedule(request());
|
if (open && current === sequence) preview.value = result;
|
} catch (error) {
|
if (open && current === sequence) errorText.value = error instanceof Error ? error.message : '预览失败,请稍后重试';
|
} finally {
|
if (current === sequence) loading.value = false;
|
}
|
}
|
|
async function execute() {
|
if (!canConfirm.value || !preview.value) return;
|
executing.value = true;
|
errorText.value = '';
|
modalApi.lock();
|
try {
|
const result = await executeBatchSchedule({ ...request(), previewHash: preview.value.previewHash,
|
previewTime: preview.value.previewTime, allowLate: acceptLate.value });
|
message.success(`排产成功:${ids.value.length} 个工单,生成 ${result.taskIds.length} 个任务`);
|
executing.value = false;
|
await modalApi.close();
|
emit('success');
|
} catch (error) {
|
errorText.value = error instanceof Error ? error.message : '执行结果未确认,请刷新列表并重新预览';
|
// 请求失败或结果未确认后必须重算;旧预览不能直接再次提交。
|
preview.value = undefined;
|
acceptLate.value = false;
|
emit('success'); // 刷新工单和甘特图,核对超时请求是否已经提交。
|
} finally {
|
executing.value = false;
|
modalApi.unlock();
|
}
|
}
|
|
const orderColumns = [
|
{ title: '工单编码', dataIndex: 'workOrderCode', width: 170 },
|
{ title: '生产数量', dataIndex: 'quantity', width: 100 },
|
{ title: '新增任务', dataIndex: 'taskCount', width: 90 },
|
{ title: '需求日期', dataIndex: 'requestDate', width: 120 },
|
{ title: '预计完成', dataIndex: 'endTime', width: 175 },
|
{ title: '校验结果', dataIndex: 'error', width: 300 },
|
];
|
const taskColumns = [
|
{ title: '工单', dataIndex: 'workOrderCode', width: 160 },
|
{ title: '工序', dataIndex: 'processName', width: 130 },
|
{ title: '工作站', dataIndex: 'workstationName', width: 150 },
|
{ title: '设备', dataIndex: 'machineryNames', width: 180 },
|
{ title: '本次数量', dataIndex: 'quantity', width: 100 },
|
{ title: '小时产能', dataIndex: 'hourlyCapacity', width: 100 },
|
{ title: '工时(h)', dataIndex: 'duration', width: 85 },
|
{ title: '开工时间', dataIndex: 'startTime', width: 180 },
|
{ title: '完成时间', dataIndex: 'endTime', width: 180 },
|
];
|
</script>
|
|
<template>
|
<Modal title="一键排产" class="w-[min(96vw,1200px)]">
|
<div class="space-y-5 px-1">
|
<Alert type="info" show-icon message="先校验与预览,再确认排产"
|
description="按交期优先及工序顺序安排工作站与设备;每天09:00–18:00,排除周末和已配置节假日,非工作时间自动顺延。已部分排产的工单只补齐剩余数量,最多30个工单、200个新任务。" />
|
<div class="flex flex-wrap items-end gap-3">
|
<label class="flex flex-col gap-2">
|
<span class="font-medium">最早开工时间</span>
|
<DatePicker v-model:value="startTime" show-time :allow-clear="false" :disabled="executing"
|
format="YYYY-MM-DD HH:mm:ss" :disabled-date="(date) => date.endOf('day').isBefore(dayjs())" />
|
</label>
|
<Button :loading="loading" :disabled="executing" @click="calculate">
|
{{ preview ? '重新计算预览' : '生成排产预览' }}
|
</Button>
|
<span class="text-sm text-muted-foreground">已选择 {{ ids.length }} 个工单</span>
|
</div>
|
<Alert v-if="errorText" type="error" show-icon :message="errorText" />
|
<template v-if="preview">
|
<Descriptions :column="{ xs: 1, sm: 3 }" size="small" bordered>
|
<Descriptions.Item label="工单数">{{ preview.orders.length }}</Descriptions.Item>
|
<Descriptions.Item label="新增任务数">{{ preview.tasks.length }}</Descriptions.Item>
|
<Descriptions.Item label="预览有效期">15分钟,提交时重新校验</Descriptions.Item>
|
</Descriptions>
|
<Alert v-if="!preview.canExecute" type="error" show-icon message="部分工单未通过校验,本批次不能提交"
|
description="请根据下方校验结果修正工单、设备或已有任务的配置,再重新计算预览。" />
|
<div>
|
<h3 class="mb-2 font-medium">工单校验</h3>
|
<Table :columns="orderColumns" :data-source="preview.orders" row-key="workOrderId"
|
:pagination="false" size="small" :scroll="{ x: 1055 }">
|
<template #bodyCell="{ column, record, text }">
|
<template v-if="column.dataIndex === 'error'">
|
<span v-if="record.error" class="text-destructive">{{ record.error }}</span>
|
<Tag v-else :color="record.late ? 'orange' : 'green'">{{ record.late ? '有延期风险' : '通过' }}</Tag>
|
</template>
|
<template v-else-if="column.dataIndex === 'requestDate'">{{ text ? String(text).slice(0, 10) : '未指定' }}</template>
|
</template>
|
</Table>
|
</div>
|
<div v-if="preview.tasks.length">
|
<h3 class="mb-2 font-medium">任务安排</h3>
|
<Table :columns="taskColumns" :data-source="preview.tasks"
|
:row-key="(row) => `${row.workOrderId}-${row.processId}`" :pagination="{ pageSize: 10, showSizeChanger: false }"
|
size="small" :scroll="{ x: 1365 }" />
|
</div>
|
<Alert v-if="preview.warnings.length" type="warning" show-icon message="请核对交期风险">
|
<template #description>
|
<p v-for="warning in preview.warnings" :key="warning">{{ warning }}</p>
|
<Checkbox v-model:checked="acceptLate" :disabled="executing" class="mt-2">
|
我已核对并接受以上交期延期风险
|
</Checkbox>
|
</template>
|
</Alert>
|
</template>
|
</div>
|
</Modal>
|
</template>
|