hidden sku code

This commit is contained in:
Timi
2026-09-16 14:53:15 +08:00
parent d0969cb322
commit 443d0a6190
2 changed files with 93 additions and 39 deletions
@@ -8,7 +8,6 @@
variant="light-outline" variant="light-outline"
:content="tag.content" :content="tag.content"
/> />
<t-tag v-if="unitText" size="small" :content="unitText" />
</div> </div>
</template> </template>
@@ -26,10 +26,7 @@
<div class="title"> <div class="title">
<div class="heading"> <div class="heading">
<div class="name-gift"> <div class="name-gift">
<strong <strong v-text="item.productNameSnapshot || '未命名产品'"/>
class="clip-text"
v-text="item.productNameSnapshot || '未命名产品'"
/>
<t-tag <t-tag
v-if="item.gifted" v-if="item.gifted"
size="small" size="small"
@@ -39,17 +36,12 @@
</div> </div>
<span class="can-fulfill" v-text="getItemDescription(item)" /> <span class="can-fulfill" v-text="getItemDescription(item)" />
</div> </div>
</div>
<div class="input">
<div <div
v-if="item.skuCodeSnapshot || item.skuSpecSnapshot?.length || item.skuNameSnapshot || item.specSummarySnapshot || item.unitSnapshot" v-if="item.skuCodeSnapshot || item.skuSpecSnapshot?.length || item.skuNameSnapshot || item.specSummarySnapshot || item.unitSnapshot"
class="sku-tags" class="sku-tags"
> >
<t-tag
v-if="item.skuCodeSnapshot"
size="small"
theme="primary"
variant="light"
:content="item.skuCodeSnapshot"
/>
<product-sku-snapshot-tags <product-sku-snapshot-tags
:product-name="item.productNameSnapshot" :product-name="item.productNameSnapshot"
:snapshot="item.skuSpecSnapshot" :snapshot="item.skuSpecSnapshot"
@@ -57,8 +49,6 @@
:unit="item.unitSnapshot" :unit="item.unitSnapshot"
/> />
</div> </div>
</div>
<div class="input">
<t-input <t-input
v-model.trim="quantityMap[item.id || '']" v-model.trim="quantityMap[item.id || '']"
class="quantity" class="quantity"
@@ -80,7 +70,7 @@
<t-button <t-button
size="extra-small" size="extra-small"
variant="outline" variant="outline"
:disabled="getAvailableBaseQuantity(item) === '0'" :disabled="getAvailableBaseQuantity(item) === '0' || (directVerify && isStockChecking)"
@click.stop="fillAllQuantity(item)" @click.stop="fillAllQuantity(item)"
>全部履约</t-button> >全部履约</t-button>
</div> </div>
@@ -100,10 +90,13 @@
/> />
</t-form-item> </t-form-item>
<t-form-item v-if="canDirectVerify" label="直接核销履约" content-align="right"> <t-form-item v-if="canDirectVerify" label="直接核销履约" content-align="right">
<t-switch v-model="directVerify" size="small" :disabled="isSaving" /> <t-switch v-model="directVerify" size="small" :disabled="isSaving || isStockChecking" />
<template #help v-if="directVerify"> </t-form-item>
<span class="red">直接核销会立即扣减实物库存;提交前会校验库存,库存不足时不能提交。</span> <t-form-item v-if="directVerify" label-width="0">
</template> <div class="stock-tips">
<span class="tips red">直接核销会立即扣减实物库存</span>
<product-stock-preview v-if="stockPreviewList.length" :items="stockPreviewList" />
</div>
</t-form-item> </t-form-item>
<t-form-item label="备注"> <t-form-item label="备注">
<t-textarea v-model="remark" borderless autosize maxlength="500" /> <t-textarea v-model="remark" borderless autosize maxlength="500" />
@@ -135,18 +128,31 @@ import FulfillmentAPI from "@/api/FulfillmentAPI";
import ProductAPI from "@/api/ProductAPI"; import ProductAPI from "@/api/ProductAPI";
import PhotoScroller from "@/components/PhotoScroller.vue"; import PhotoScroller from "@/components/PhotoScroller.vue";
import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue"; import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue";
import ProductStockPreview from "@/components/ProductStockPreview.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { Attachment } from "timi-web-api";
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment"; import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment";
import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder"; import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder";
import type { ProductSku, ProductSkuUnit } from "@/types/Product"; import type { ProductSku, ProductSkuUnit } from "@/types/Product";
import type { PhotoItem } from "@/types/Photo"; import type { PhotoItem } from "@/types/Photo";
import type { Attachment } from "timi-web-api"; import {
import { comparePackageQuantity, convertPackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity, toPackageBaseQuantity } from "@/utils/package"; comparePackageQuantity,
convertPackageQuantity,
createPackageRequestNo,
getPackageQuantityText,
normalizePackageQuantity,
toPackageBaseQuantity
} from "@/utils/package";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { getProductStockShortageMessage, mergeProductStockRequirements } from "@/utils/productStock"; import type { ProductStockPreviewItem } from "@/utils/productStock";
import {
buildProductStockPreview,
getProductStockShortageMessage,
mergeProductStockRequirements
} from "@/utils/productStock";
import { uploadPhotoList } from "@/utils/photo"; import { uploadPhotoList } from "@/utils/photo";
import { getProductPhotoURL } from "@/utils/product"; import { getProductPhotoURL } from "@/utils/product";
@@ -170,6 +176,9 @@ const unitPickerVisible = ref(false);
const unitPickerSourceItemId = ref(""); const unitPickerSourceItemId = ref("");
const unitPickerDraft = ref<Array<string | number>>([]); const unitPickerDraft = ref<Array<string | number>>([]);
const directVerify = ref(false); const directVerify = ref(false);
const isStockChecking = ref(false);
const stockPreviewList = ref<ProductStockPreviewItem[]>([]);
let stockCheckToken = 0;
const photoUrlList = computed(() => photoItems.value.map(item => item.url)); const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const isPackage = computed(() => order.value?.orderType === "PACKAGE"); const isPackage = computed(() => order.value?.orderType === "PACKAGE");
const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT"); const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT");
@@ -185,6 +194,25 @@ const canDirectVerify = computed(() => userStore.hasRoles(gaoRoleCodes.storeMana
&& userStore.hasPermissions(permissionCodes.orderFulfillment.verify) && userStore.hasPermissions(permissionCodes.orderFulfillment.verify)
&& userStore.hasPermissions(permissionCodes.productStock.read)); && userStore.hasPermissions(permissionCodes.productStock.read));
watch(directVerify, async value => {
stockCheckToken += 1;
const token = stockCheckToken;
if (!value) {
isStockChecking.value = false;
return;
}
isStockChecking.value = true;
try {
if (!await validateDirectStock() && token === stockCheckToken && directVerify.value) {
directVerify.value = false;
}
} finally {
if (token === stockCheckToken) {
isStockChecking.value = false;
}
}
});
async function loadData(): Promise<void> { async function loadData(): Promise<void> {
const salesOrderId = String(route.query.salesOrderId || ""); const salesOrderId = String(route.query.salesOrderId || "");
if (!salesOrderId) { if (!salesOrderId) {
@@ -262,7 +290,7 @@ function getFulfillmentUnitForAvailableQuantity(item: GaoSalesOrderItem, unit?:
return getBaseUnit(item) || unit; return getBaseUnit(item) || unit;
} }
function fillAllQuantity(item: GaoSalesOrderItem): void { async function fillAllQuantity(item: GaoSalesOrderItem): Promise<void> {
const unit = getFulfillmentUnit(item); const unit = getFulfillmentUnit(item);
if (!item.id || !unit) { if (!item.id || !unit) {
Message.warning("请先选择实际履约单位"); Message.warning("请先选择实际履约单位");
@@ -274,6 +302,9 @@ function fillAllQuantity(item: GaoSalesOrderItem): void {
return; return;
} }
quantityMap[item.id] = quantity; quantityMap[item.id] = quantity;
if (directVerify.value) {
await previewDirectStock();
}
} }
function getFulfillmentUnitList(item: GaoSalesOrderItem): ProductSkuUnit[] { function getFulfillmentUnitList(item: GaoSalesOrderItem): ProductSkuUnit[] {
@@ -356,6 +387,7 @@ function buildAttachments(): Partial<Attachment>[] {
} }
async function validateDirectStock(): Promise<boolean> { async function validateDirectStock(): Promise<boolean> {
stockPreviewList.value = [];
const selected = sourceItems.value.filter(item => !!item.id && quantityMap[item.id]); const selected = sourceItems.value.filter(item => !!item.id && quantityMap[item.id]);
const skuMap = new Map<string, ProductSku>(); const skuMap = new Map<string, ProductSku>();
for (const item of selected) { for (const item of selected) {
@@ -385,13 +417,14 @@ async function validateDirectStock(): Promise<boolean> {
if (!item.skuId || !sku?.stockEnabled || !baseQuantity) { if (!item.skuId || !sku?.stockEnabled || !baseQuantity) {
return []; return [];
} }
return [{ skuId: item.skuId, baseQuantity, productName: item.productNameSnapshot || "未命名产品", unitName: "基础单位" }]; return [{ skuId: item.skuId, baseQuantity, productName: item.productNameSnapshot || "未命名产品", unitName: "基础单位", stockEnabled: sku.stockEnabled }];
})); }));
if (!requirementList.length) { if (!requirementList.length) {
return true; return true;
} }
try { try {
const stockList = await ProductAPI.stockBatchDetail({ skuIdList: requirementList.map(item => item.skuId) }); const stockList = await ProductAPI.stockBatchDetail({ skuIdList: requirementList.map(item => item.skuId) });
stockPreviewList.value = buildProductStockPreview(stockList, requirementList);
const shortageMessage = getProductStockShortageMessage(stockList, requirementList); const shortageMessage = getProductStockShortageMessage(stockList, requirementList);
if (shortageMessage) { if (shortageMessage) {
Message.warning(`${shortageMessage},不能直接核销履约`); Message.warning(`${shortageMessage},不能直接核销履约`);
@@ -399,11 +432,30 @@ async function validateDirectStock(): Promise<boolean> {
} }
return true; return true;
} catch (error) { } catch (error) {
stockPreviewList.value = [];
Message.error(getErrorMessage(error, "库存查询失败,暂不能直接核销履约")); Message.error(getErrorMessage(error, "库存查询失败,暂不能直接核销履约"));
return false; return false;
} }
} }
async function previewDirectStock(): Promise<void> {
if (!directVerify.value || isStockChecking.value) {
return;
}
stockCheckToken += 1;
const token = stockCheckToken;
isStockChecking.value = true;
try {
if (!await validateDirectStock() && token === stockCheckToken && directVerify.value) {
directVerify.value = false;
}
} finally {
if (token === stockCheckToken) {
isStockChecking.value = false;
}
}
}
function validate(): boolean { function validate(): boolean {
if (!order.value?.id) { if (!order.value?.id) {
Message.warning("请从订单详情进入履约创建"); Message.warning("请从订单详情进入履约创建");
@@ -447,7 +499,12 @@ function validate(): boolean {
} }
async function save(): Promise<void> { async function save(): Promise<void> {
if (isSaving.value || !validate()) return; if (isSaving.value) return;
if (isStockChecking.value) {
Message.warning("库存正在预检,请稍候再提交");
return;
}
if (!validate()) return;
if (!requestNo.value) requestNo.value = createPackageRequestNo(); if (!requestNo.value) requestNo.value = createPackageRequestNo();
isSaving.value = true; isSaving.value = true;
try { try {
@@ -552,10 +609,10 @@ onUnmounted(() => navBarStore.clearRight());
min-width: 0; min-width: 0;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
align-items: center;
justify-content: start;
strong { strong {
flex: 1 1 0;
min-width: 0;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -568,6 +625,12 @@ onUnmounted(() => navBarStore.clearRight());
white-space: nowrap; white-space: nowrap;
} }
} }
}
.input {
gap: .5rem;
display: flex;
margin-top: .5rem;
.sku-tags { .sku-tags {
gap: .375rem; gap: .375rem;
@@ -575,12 +638,6 @@ onUnmounted(() => navBarStore.clearRight());
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
} }
}
.input {
gap: .5rem;
display: flex;
margin-top: .5rem;
.unit { .unit {
font-size: .875rem; font-size: .875rem;
@@ -591,10 +648,8 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
.empty { .tips {
color: var(--td-text-color-secondary); font-size: .875rem;
padding: 2rem 1rem;
text-align: center;
} }
} }