diff --git a/src/api/ProductAPI.ts b/src/api/ProductAPI.ts index a2bd317..709e405 100644 --- a/src/api/ProductAPI.ts +++ b/src/api/ProductAPI.ts @@ -9,6 +9,7 @@ import type { ProductSpec, ProductSpecValue, ProductStock, + ProductStockBatchRequest, ProductStockProduct, ProductStockChangeRequest, ProductStockManualRevokeRequest, @@ -107,6 +108,10 @@ async function stockDetail(skuId: string, storeId?: string): Promise { + return await axios.post(`${STOCK_URI}/batch-detail`, withCurrentStoreId(request)); +} + async function initialIn(request: ProductStockChangeRequest): Promise { return await axios.post(`${STOCK_URI}/initial-in`, withCurrentStoreId(request)); } @@ -161,6 +166,7 @@ export default { stockList, stockProductList, stockDetail, + stockBatchDetail, initialIn, manualIn, manualOut, diff --git a/src/components/ProductSelector.vue b/src/components/ProductSelector.vue index d28caf3..2663774 100644 --- a/src/components/ProductSelector.vue +++ b/src/components/ProductSelector.vue @@ -123,7 +123,7 @@ + + diff --git a/src/pages/package/PackageConfigEdit.vue b/src/pages/package/PackageConfigEdit.vue index 5646e27..66472b4 100644 --- a/src/pages/package/PackageConfigEdit.vue +++ b/src/pages/package/PackageConfigEdit.vue @@ -50,6 +50,7 @@ placeholder="请选择产品" required-mark @update:model-value="applyProduct(index, $event)" + @select="applyProductSelection(index, $event)" /> @@ -111,7 +112,7 @@ import { useCurrentStoreStore } from "@/store/currentStoreStore"; import { useNavBarStore } from "@/store/navBarStore"; import type { GaoPackageConfig, PackageConfigItem, PackageConfigStatus } from "@/types/Package"; import ProductAPI from "@/api/ProductAPI"; -import type { Product, ProductSku, ProductSkuUnit } from "@/types/Product"; +import type { Product, ProductSelection, ProductSku, ProductSkuUnit } from "@/types/Product"; import { getErrorMessage } from "@/utils/error"; import { formatPackageAmountInput, getPackageConfigStatusText, normalizePackageQuantity, parsePackageAmount } from "@/utils/package"; import { Message } from "@/utils/message"; @@ -204,23 +205,27 @@ const estimatedOriginalAmount = computed(() => { }); const estimatedOriginalAmountText = computed(() => estimatedOriginalAmount.value === undefined ? "待完善" : `¥${formatProductAmount(estimatedOriginalAmount.value)}`); -async function applyProduct(index: number, value?: { skuId: string; skuUnitId: string }): Promise { +function applyProduct(index: number, value?: { skuId: string; skuUnitId: string }): void { if (!value) { return; } - const sku = skuCache.get(value.skuId) || await ProductAPI.skuDetail(value.skuId); - skuCache.set(value.skuId, sku); - const product = productCache.get(sku.productId || "") || await ProductAPI.detail(sku.productId || ""); - productCache.set(sku.productId || "", product); - const unit = (sku.unitList || []).find(item => item.id === value.skuUnitId); Object.assign(form.itemList[index], { skuId: value.skuId, - skuUnitId: value.skuUnitId, - productNameSnapshot: product.name, - skuNameSnapshot: sku.name, - specSummarySnapshot: sku.specSummary, - unitSnapshot: unit?.name || "", - baseQuantityPerUnitSnapshot: unit?.baseQuantityPerUnit + skuUnitId: value.skuUnitId + }); +} + +function applyProductSelection(index: number, selection: ProductSelection): void { + skuCache.set(selection.sku.id || "", selection.sku); + productCache.set(selection.product.id || "", selection.product); + Object.assign(form.itemList[index], { + productNameSnapshot: selection.product.name, + productIdSnapshot: selection.product.id, + skuCodeSnapshot: selection.sku.code, + skuNameSnapshot: selection.sku.name, + specSummarySnapshot: selection.sku.specSummary, + unitSnapshot: selection.unit.name, + baseQuantityPerUnitSnapshot: selection.unit.baseQuantityPerUnit }); } @@ -396,13 +401,16 @@ async function loadData(): Promise { } const result = await PackageAPI.configDetail(routeId.value); const itemList = (result.itemList || []).map(createItem); - const skuIdList = [...new Set(itemList.map(item => item.skuId).filter((id): id is string => !!id))]; - await Promise.all(skuIdList.map(async skuId => { - const sku = itemList.find(item => item.skuId === skuId)?.sku || await ProductAPI.skuDetail(skuId); - skuCache.set(skuId, sku); - if (sku.productId) { - productCache.set(sku.productId, await ProductAPI.detail(sku.productId)); + for (const item of itemList) { + if (item.skuId && item.sku) { + skuCache.set(item.skuId, item.sku); } + } + const productIdList = [...new Set(itemList + .map(item => item.sku?.productId || item.productIdSnapshot) + .filter((id): id is string => !!id))]; + await Promise.all(productIdList.map(async productId => { + productCache.set(productId, await ProductAPI.detail(productId)); })); Object.assign(form, { id: result.id || routeId.value, diff --git a/src/pages/package/PackagePurchase.vue b/src/pages/package/PackagePurchase.vue index f89fc5b..ee70c22 100644 --- a/src/pages/package/PackagePurchase.vue +++ b/src/pages/package/PackagePurchase.vue @@ -199,11 +199,14 @@ -
全部履约会同步启用全额收款;请先准备全部产品库存,任意一步失败,整张订单都会回滚。
+
+
全部履约会同步启用全额收款;请先准备充足的库存,任意一步失败都会下单失败。
+ +
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([]); +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 { + 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 { if (isSaving.value) { return; } + if (isStockChecking.value) { + Message.warning("库存正在预检,请稍候再提交"); + return; + } if (!validatePurchaseForm() || !validateItemDecimalQuantity()) { return; } @@ -796,6 +892,7 @@ async function purchasePackage(): Promise { 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 { return; } config.value = await PackageAPI.configDetail(configId.value); + syncConfigSkuMap(config.value); applyConfig(config.value); - await loadConfigSkuList(config.value); } async function selectConfig(item: GaoPackageConfig): Promise { @@ -832,21 +929,11 @@ async function selectConfig(item: GaoPackageConfig): Promise { 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 { - 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 { diff --git a/src/pages/product/ProductEdit.vue b/src/pages/product/ProductEdit.vue index 6055103..50c2a64 100644 --- a/src/pages/product/ProductEdit.vue +++ b/src/pages/product/ProductEdit.vue @@ -203,7 +203,7 @@