<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>
|