云
4 小时以前 fb4db4ae9b6a7f113ba049f6789e6916c0dcc276
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
<script lang="ts" setup>
import type { FormType } from '../data';
 
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
 
import { computed, ref } from 'vue';
 
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesWmProductSalesStatusEnum } from '@vben/constants';
 
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
 
import { useVbenForm } from '#/adapter/form';
import {
  checkProductSalesQuantity,
  createProductSales,
  finishProductSales,
  getProductSales,
  previewProductSalesScan,
  scanProductSales,
  shippingProductSales,
  stockProductSales,
  submitProductSales,
  updateProductSales,
} from '#/api/mes/wm/productsales';
import { $t } from '#/locales';
 
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
import PickingBags from './picking-bags.vue';
 
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductSalesApi.ProductSales>();
const scanContent = ref('');
const scanType = ref<'BAG' | 'PALLET' | 'BATCH'>('BAG');
const scanPreview = ref<MesWmProductSalesApi.ProductSalesScanResponse>();
const selectedBagIds = ref<number[]>([]);
const scanLoading = ref(false);
const scanRefreshKey = ref(0);
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() =>
  // 是否为编辑模式(可保存)
  ['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为拣货模式
const isShipping = computed(() => formType.value === 'shipping'); // 是否为填写运单模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行出库模式
const canSubmit = computed(
  () =>
    // 是否可提交
    formType.value === 'update' &&
    formData.value?.status === MesWmProductSalesStatusEnum.PREPARE,
);
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return $t('ui.actionTitle.view', ['销售出库单']);
  }
  if (formType.value === 'stock') {
    return '执行拣货';
  }
  if (formType.value === 'shipping') {
    return '填写运单';
  }
  if (formType.value === 'finish') {
    return '执行出库';
  }
  return formType.value === 'update'
    ? $t('ui.actionTitle.edit', ['销售出库单'])
    : $t('ui.actionTitle.create', ['销售出库单']);
});
 
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
});
 
/** 提交出库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid || !formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data =
        (await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
      await updateProductSales({ ...formData.value, ...data });
      originalSnapshot.value = current;
    }
    await submitProductSales(formData.value.id);
    message.success('提交成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
 
/** 先识别对象并选择袋码,回车不会直接写入拣货。 */
async function handleScan() {
  if (!formData.value?.id || !scanContent.value.trim() || scanLoading.value) {
    return;
  }
  scanLoading.value = true;
  try {
    scanPreview.value = undefined;
    selectedBagIds.value = [];
    const result = await previewProductSalesScan(scanContent.value.trim(), scanType.value, formData.value.id);
    scanPreview.value = result;
    if (result.scanType === 'BAG') selectedBagIds.value = result.bags?.filter(b => b.selectable).map(b => b.id) || [];
    if (!result.scanQuantity) message.warning('当前对象没有可拣袋码,请核对质检和库存状态');
  } finally { scanLoading.value = false; }
}
 
async function confirmScan() {
  if (!formData.value?.id || !scanPreview.value || !selectedBagIds.value.length || scanLoading.value) return;
  const source = scanPreview.value;
  const bagIds = [...selectedBagIds.value];
  scanLoading.value = true;
  try { await confirm(`确认将所选 ${bagIds.length} 袋加入拣货并关联追溯袋码?`); }
  catch { scanLoading.value = false; return; }
  try {
    const result = await scanProductSales({
      quantity: bagIds.length,
      salesId: formData.value.id,
      scanContent: source.scanContent!,
      scanType: source.scanType as 'BAG' | 'PALLET' | 'BATCH',
      bagIds,
    });
    message[result.alreadyScanned ? 'warning' : 'success'](
      result.alreadyScanned
        ? `该码已扫描,明细编号:${result.detailId ?? '-'}`
        : `已加入拣货 ${result.outboundQuantity} 袋,逐袋关联追溯码`,
    );
    scanContent.value = '';
    scanPreview.value = undefined;
    selectedBagIds.value = [];
    scanRefreshKey.value += 1;
  } finally {
    scanLoading.value = false;
  }
}
 
/** 执行拣货:出库数量与拣货数量不一致时二次确认 */
async function handleStock() {
  if (!formData.value?.id) {
    return;
  }
  const quantityMatch = await checkProductSalesQuantity(formData.value.id);
  if (!quantityMatch) {
    try {
      await confirm('出库数量与拣货数量不一致,确认执行拣货?');
    } catch {
      return;
    }
  }
  modalApi.lock();
  try {
    await stockProductSales(formData.value.id);
    message.success('拣货成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
 
/** 填写运单 */
async function handleShipping() {
  if (!formData.value?.id) {
    return;
  }
  const values =
    (await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
  modalApi.lock();
  try {
    await shippingProductSales({
      carrier: values.carrier,
      id: formData.value.id,
      shippingNumber: values.shippingNumber,
    });
    message.success('运单信息填写成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
 
/** 执行出库 */
async function handleFinish() {
  if (!formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    await finishProductSales(formData.value.id);
    message.success('出库成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
 
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!isEditable.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // 提交表单
    const data =
      (await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
    try {
      if (formData.value?.id) {
        await updateProductSales({ ...formData.value, ...data });
        formData.value = { ...formData.value, ...data };
      } else {
        const id = await createProductSales(data);
        formData.value = {
          ...data,
          id,
          status: MesWmProductSalesStatusEnum.PREPARE,
        };
        await formApi.setFieldValue('id', id);
        await formApi.setFieldValue('status', formData.value.status);
        formType.value = 'update';
      }
      originalSnapshot.value = JSON.stringify(await formApi.getValues());
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    scanPreview.value = undefined;
    selectedBagIds.value = [];
    scanContent.value = '';
    if (!isOpen) {
      formData.value = undefined;
      originalSnapshot.value = '';
      return;
    }
    // 加载数据
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema(formType.value, formApi) });
    formApi.setDisabled(false);
    modalApi.setState({ showConfirmButton: isEditable.value });
    if (data?.id) {
      modalApi.lock();
      try {
        formData.value = await getProductSales(data.id);
        // 设置到 values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    }
    originalSnapshot.value = JSON.stringify(await formApi.getValues());
  },
});
</script>
 
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
    <!-- 非新建模式展示物料信息 -->
    <template v-if="formData?.id">
      <Divider>物料信息</Divider>
      <div class="mx-4">
        <div v-if="isStock" class="mb-3 flex items-center gap-2 px-4">
          <label for="sales-picking-type" class="sr-only">拣货扫码类型</label>
          <a-select id="sales-picking-type" v-model:value="scanType" :disabled="scanLoading" style="width:140px" @change="scanPreview = undefined; selectedBagIds = []">
            <a-select-option value="BAG">袋码</a-select-option><a-select-option value="PALLET">托盘码</a-select-option><a-select-option value="BATCH">生产批次码</a-select-option>
          </a-select>
          <a-input
            v-model:value="scanContent"
            :disabled="scanLoading"
            autofocus
            class="flex-1"
            placeholder="扫描所选类型码,回车识别"
            @press-enter="handleScan"
          />
          <Button :loading="scanLoading" type="default" @click="handleScan">
            识别码值
          </Button>
        </div>
        <section v-if="isStock && scanPreview" class="mb-4 rounded-lg border border-border p-4">
          <h3 class="font-semibold">核对拣货对象</h3><p>批次 {{scanPreview.batchCode}} · 可拣 {{scanPreview.scanQuantity}} 袋</p>
          <PickingBags v-if="scanPreview.scanType !== 'BAG'" v-model="selectedBagIds" :bags="scanPreview.bags || []" :disabled="scanLoading" />
          <p v-else>袋码 {{scanPreview.bags?.[0]?.code}} · 本次 {{selectedBagIds.length}} 袋</p>
          <div class="mt-3 flex gap-2"><Button type="primary" :loading="scanLoading" :disabled="!selectedBagIds.length" @click="confirmScan">确认加入拣货</Button><Button :disabled="scanLoading" @click="scanPreview = undefined">取消本次扫码</Button></div>
        </section>
        <LineList
          :form-type="formType"
          :notice-id="formData.noticeId"
          :refresh-key="scanRefreshKey"
          :sales-id="formData.id"
        />
      </div>
    </template>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit"
          title="确认提交该销售出库单?【提交后将不能修改】"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>
        </Popconfirm>
        <Button v-if="isStock" type="primary" :disabled="scanLoading || !!scanPreview" @click="handleStock">
          执行拣货
        </Button>
        <Popconfirm
          v-if="isShipping"
          title="确认提交运单信息?"
          @confirm="handleShipping"
        >
          <Button type="primary">确认填写</Button>
        </Popconfirm>
        <Popconfirm
          v-if="isFinish"
          title="确认执行出库?执行后将扣减库存。"
          @confirm="handleFinish"
        >
          <Button type="primary">确认出库</Button>
        </Popconfirm>
      </div>
    </template>
  </Modal>
</template>