add product stock preview
This commit is contained in:
@@ -199,11 +199,14 @@
|
||||
<t-switch
|
||||
v-model="fulfillAll"
|
||||
size="small"
|
||||
:disabled="!canFulfillImmediately"
|
||||
:disabled="!canFulfillImmediately || isStockChecking"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label-width="0">
|
||||
<div class="red">全部履约会同步启用全额收款;请先准备全部产品库存,任意一步失败,整张订单都会回滚。</div>
|
||||
<div class="stock-tips">
|
||||
<div class="tips red">全部履约会同步启用全额收款;请先准备充足的库存,任意一步失败都会下单失败。</div>
|
||||
<product-stock-preview v-if="stockPreviewList.length" :items="stockPreviewList" />
|
||||
</div>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="(paymentVerified && parsedPaymentAmount) || (receiveFullPayment && parsedSaleAmount)" label-width="0">
|
||||
<photo-scroller
|
||||
@@ -337,6 +340,7 @@ import CustomerList from "@/components/CustomerList.vue";
|
||||
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||
import ProductStockPreview from "@/components/ProductStockPreview.vue";
|
||||
import { usePackagePurchase } from "@/pages/package/usePackagePurchase";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import { useSettingStore } from "@/store/settingStore";
|
||||
@@ -351,10 +355,13 @@ import {
|
||||
formatPackageAmount,
|
||||
getPackageQuantityText,
|
||||
normalizePackageQuantity,
|
||||
parsePackageAmount
|
||||
parsePackageAmount,
|
||||
toPackageBaseQuantity
|
||||
} from "@/utils/package";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
import { buildProductStockPreview, getProductStockShortageMessage, mergeProductStockRequirements } from "@/utils/productStock";
|
||||
import type { ProductStockPreviewItem } from "@/utils/productStock";
|
||||
import { uploadPhotoList } from "@/utils/photo";
|
||||
import { getProductPhotoURL, getProductSkuSnapshotText } from "@/utils/product";
|
||||
|
||||
@@ -510,6 +517,9 @@ const paymentPhotoUrlList = computed(() => paymentPhotoItems.value.map(item => i
|
||||
const fulfillmentPhotoUrlList = computed(() => fulfillmentPhotoItems.value.map(item => item.url));
|
||||
const receiveFullPayment = ref(false);
|
||||
const fulfillAll = ref(false);
|
||||
const isStockChecking = ref(false);
|
||||
const stockPreviewList = ref<ProductStockPreviewItem[]>([]);
|
||||
let stockCheckToken = 0;
|
||||
const parsedPaymentAmount = computed(() => parsePackageAmount(paymentAmountText.value));
|
||||
const paymentVerified = computed(() => !receiveFullPayment.value
|
||||
&& 0 < paymentAmountText.value.trim().length
|
||||
@@ -553,11 +563,29 @@ const canReceiveImmediately = computed(() => userStore.hasPermissions(permission
|
||||
&& userStore.hasPermissions(permissionCodes.fundReceive.verify));
|
||||
const canFulfillImmediately = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create)
|
||||
&& userStore.hasPermissions(permissionCodes.orderFulfillment.verify)
|
||||
&& userStore.hasPermissions(permissionCodes.productStock.read)
|
||||
&& (parsedSaleAmount.value === 0 || canReceiveImmediately.value));
|
||||
|
||||
watch(fulfillAll, value => {
|
||||
if (value && parsedSaleAmount.value !== 0) {
|
||||
receiveFullPayment.value = true;
|
||||
watch(fulfillAll, async value => {
|
||||
stockCheckToken += 1;
|
||||
const token = stockCheckToken;
|
||||
if (!value) {
|
||||
isStockChecking.value = false;
|
||||
return;
|
||||
}
|
||||
isStockChecking.value = true;
|
||||
try {
|
||||
if (!await validateDirectStock() && token === stockCheckToken && fulfillAll.value) {
|
||||
fulfillAll.value = false;
|
||||
return;
|
||||
}
|
||||
if (token === stockCheckToken && fulfillAll.value && parsedSaleAmount.value !== 0) {
|
||||
receiveFullPayment.value = true;
|
||||
}
|
||||
} finally {
|
||||
if (token === stockCheckToken) {
|
||||
isStockChecking.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -603,6 +631,14 @@ function getItemSku(item: PackagePurchaseItem): ProductSku | undefined {
|
||||
return skuMap[item.skuId] || config.value?.itemList?.find(configItem => configItem.skuId === item.skuId)?.sku;
|
||||
}
|
||||
|
||||
function syncConfigSkuMap(value?: GaoPackageConfig): void {
|
||||
for (const item of value?.itemList || []) {
|
||||
if (item.skuId && item.sku) {
|
||||
skuMap[item.skuId] = item.sku;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getItemUnit(item: PackagePurchaseItem): ProductSkuUnit | undefined {
|
||||
return getItemSku(item)?.unitList?.find(unit => unit.id === item.skuUnitId);
|
||||
}
|
||||
@@ -762,10 +798,70 @@ function validateItemDecimalQuantity(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function validateDirectStock(): Promise<boolean> {
|
||||
stockPreviewList.value = [];
|
||||
const selected = packagePurchaseItemList.value.filter(item => {
|
||||
const quantity = normalizePackageQuantity(item.totalQuantity);
|
||||
return !!quantity && quantity !== "0";
|
||||
});
|
||||
const skuIdList = [...new Set(selected.map(item => item.skuId).filter(Boolean))];
|
||||
await Promise.all(skuIdList.map(async skuId => {
|
||||
if (skuMap[skuId]) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
skuMap[skuId] = await ProductAPI.skuDetail(skuId);
|
||||
} catch {
|
||||
// 统一在下方提示,避免部分 SKU 查询失败时静默跳过库存校验
|
||||
}
|
||||
}));
|
||||
for (const item of selected) {
|
||||
if (!skuMap[item.skuId]) {
|
||||
Message.warning(`${getItemName(item)} 的库存信息加载失败,请刷新后重试`);
|
||||
return false;
|
||||
}
|
||||
if (!getItemUnit(item)) {
|
||||
Message.warning(`${getItemName(item)} 的权益单位不存在,请刷新后重试`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const requirementList = mergeProductStockRequirements(selected.flatMap(item => {
|
||||
const sku = skuMap[item.skuId];
|
||||
const unit = getItemUnit(item);
|
||||
const quantity = normalizePackageQuantity(item.totalQuantity);
|
||||
const baseQuantity = quantity && unit ? toPackageBaseQuantity(quantity, unit.baseQuantityPerUnit) : undefined;
|
||||
if (!sku?.stockEnabled || !baseQuantity) {
|
||||
return [];
|
||||
}
|
||||
return [{ skuId: item.skuId, baseQuantity, productName: getItemName(item), unitName: "基础单位", stockEnabled: sku.stockEnabled }];
|
||||
}));
|
||||
if (!requirementList.length) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const stockList = await ProductAPI.stockBatchDetail({ skuIdList: requirementList.map(item => item.skuId) });
|
||||
stockPreviewList.value = buildProductStockPreview(stockList, requirementList);
|
||||
const shortageMessage = getProductStockShortageMessage(stockList, requirementList);
|
||||
if (shortageMessage) {
|
||||
Message.warning(`${shortageMessage},不能直接履约`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
stockPreviewList.value = [];
|
||||
Message.error(getErrorMessage(error, "库存查询失败,暂不能直接履约"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRecord(): Promise<void> {
|
||||
if (isSaving.value) {
|
||||
return;
|
||||
}
|
||||
if (isStockChecking.value) {
|
||||
Message.warning("库存正在预检,请稍候再提交");
|
||||
return;
|
||||
}
|
||||
if (!validatePurchaseForm() || !validateItemDecimalQuantity()) {
|
||||
return;
|
||||
}
|
||||
@@ -796,6 +892,7 @@ async function purchasePackage(): Promise<void> {
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
if (fulfillAll.value && !await validateDirectStock()) return;
|
||||
const result = await submitPurchase();
|
||||
if (!result?.salesOrderId) {
|
||||
Message.error("套餐购买成功但未返回完整订单关联,请联系管理员");
|
||||
@@ -823,8 +920,8 @@ async function loadPageData(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
config.value = await PackageAPI.configDetail(configId.value);
|
||||
syncConfigSkuMap(config.value);
|
||||
applyConfig(config.value);
|
||||
await loadConfigSkuList(config.value);
|
||||
}
|
||||
|
||||
async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
||||
@@ -832,21 +929,11 @@ async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
||||
return;
|
||||
}
|
||||
config.value = await PackageAPI.configDetail(item.id);
|
||||
await loadConfigSkuList(config.value);
|
||||
syncConfigSkuMap(config.value);
|
||||
applyConfig(config.value);
|
||||
configSelectorVisible.value = false;
|
||||
}
|
||||
|
||||
async function loadConfigSkuList(value?: GaoPackageConfig): Promise<void> {
|
||||
const skuIdList = [...new Set((value?.itemList || []).map(item => item.skuId).filter(Boolean))];
|
||||
await Promise.all(skuIdList.map(async skuId => {
|
||||
try {
|
||||
skuMap[skuId] = await ProductAPI.skuDetail(skuId);
|
||||
} catch {
|
||||
// 后端会校验最终权益单位,单个 SKU 的单位加载失败不影响套餐查看
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
navBarStore.clearRight();
|
||||
if (userStore.hasPermissions(permissionCodes.customerPackage.create)) {
|
||||
@@ -895,23 +982,8 @@ watch(config, value => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.straight-through {
|
||||
width: 100%;
|
||||
padding: .75rem;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .875rem;
|
||||
|
||||
.title {
|
||||
font-size: .9375rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tips {
|
||||
color: var(--td-text-color-secondary);
|
||||
margin: .5rem .5rem 0;
|
||||
font-size: .75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tips {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.customer-profile {
|
||||
|
||||
Reference in New Issue
Block a user