云
16 小时以前 c2e8526f8f857be5e3a8c6f2b39c5efd424de33e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
<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>