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
@@ -26,10 +26,7 @@
<div class="title">
<div class="heading">
<div class="name-gift">
<strong
class="clip-text"
v-text="item.productNameSnapshot || '未命名产品'"
/>
<strong v-text="item.productNameSnapshot || '未命名产品'"/>
<t-tag
v-if="item.gifted"
size="small"
@@ -39,17 +36,12 @@
</div>
<span class="can-fulfill" v-text="getItemDescription(item)" />
</div>
</div>
<div class="input">
<div
v-if="item.skuCodeSnapshot || item.skuSpecSnapshot?.length || item.skuNameSnapshot || item.specSummarySnapshot || item.unitSnapshot"
class="sku-tags"
>
<t-tag
v-if="item.skuCodeSnapshot"
size="small"
theme="primary"
variant="light"
:content="item.skuCodeSnapshot"
/>
<product-sku-snapshot-tags
:product-name="item.productNameSnapshot"
:snapshot="item.skuSpecSnapshot"
@@ -57,8 +49,6 @@
:unit="item.unitSnapshot"
/>
</div>
</div>
<div class="input">
<t-input
v-model.trim="quantityMap[item.id || '']"
class="quantity"
@@ -80,9 +70,9 @@
<t-button
size="extra-small"
variant="outline"
:disabled="getAvailableBaseQuantity(item) === '0'"
@click.stop="fillAllQuantity(item)"
>全部履约</t-button>
:disabled="getAvailableBaseQuantity(item) === '0' || (directVerify && isStockChecking)"
@click.stop="fillAllQuantity(item)"
>全部履约</t-button>
</div>
</div>
</div>
@@ -100,10 +90,13 @@
/>
</t-form-item>
<t-form-item v-if="canDirectVerify" label="直接核销履约" content-align="right">
<t-switch v-model="directVerify" size="small" :disabled="isSaving" />
<template #help v-if="directVerify">
<span class="red">直接核销会立即扣减实物库存;提交前会校验库存,库存不足时不能提交。</span>
</template>
<t-switch v-model="directVerify" size="small" :disabled="isSaving || isStockChecking" />
</t-form-item>
<t-form-item v-if="directVerify" label-width="0">
<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 label="备注">
<t-textarea v-model="remark" borderless autosize maxlength="500" />
@@ -135,18 +128,31 @@ import FulfillmentAPI from "@/api/FulfillmentAPI";
import ProductAPI from "@/api/ProductAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue";
import ProductStockPreview from "@/components/ProductStockPreview.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { Attachment } from "timi-web-api";
import { useUserStore } from "timi-web-api";
import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment";
import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder";
import type { ProductSku, ProductSkuUnit } from "@/types/Product";
import type { PhotoItem } from "@/types/Photo";
import type { Attachment } from "timi-web-api";
import { comparePackageQuantity, convertPackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity, toPackageBaseQuantity } from "@/utils/package";
import {
comparePackageQuantity,
convertPackageQuantity,
createPackageRequestNo,
getPackageQuantityText,
normalizePackageQuantity,
toPackageBaseQuantity
} from "@/utils/package";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
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 { getProductPhotoURL } from "@/utils/product";
@@ -170,6 +176,9 @@ const unitPickerVisible = ref(false);
const unitPickerSourceItemId = ref("");
const unitPickerDraft = ref<Array<string | number>>([]);
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 isPackage = computed(() => order.value?.orderType === "PACKAGE");
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.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> {
const salesOrderId = String(route.query.salesOrderId || "");
if (!salesOrderId) {
@@ -262,7 +290,7 @@ function getFulfillmentUnitForAvailableQuantity(item: GaoSalesOrderItem, unit?:
return getBaseUnit(item) || unit;
}
function fillAllQuantity(item: GaoSalesOrderItem): void {
async function fillAllQuantity(item: GaoSalesOrderItem): Promise<void> {
const unit = getFulfillmentUnit(item);
if (!item.id || !unit) {
Message.warning("请先选择实际履约单位");
@@ -274,6 +302,9 @@ function fillAllQuantity(item: GaoSalesOrderItem): void {
return;
}
quantityMap[item.id] = quantity;
if (directVerify.value) {
await previewDirectStock();
}
}
function getFulfillmentUnitList(item: GaoSalesOrderItem): ProductSkuUnit[] {
@@ -356,6 +387,7 @@ function buildAttachments(): Partial<Attachment>[] {
}
async function validateDirectStock(): Promise<boolean> {
stockPreviewList.value = [];
const selected = sourceItems.value.filter(item => !!item.id && quantityMap[item.id]);
const skuMap = new Map<string, ProductSku>();
for (const item of selected) {
@@ -385,13 +417,14 @@ async function validateDirectStock(): Promise<boolean> {
if (!item.skuId || !sku?.stockEnabled || !baseQuantity) {
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) {
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},不能直接核销履约`);
@@ -399,11 +432,30 @@ async function validateDirectStock(): Promise<boolean> {
}
return true;
} catch (error) {
stockPreviewList.value = [];
Message.error(getErrorMessage(error, "库存查询失败,暂不能直接核销履约"));
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 {
if (!order.value?.id) {
Message.warning("请从订单详情进入履约创建");
@@ -447,7 +499,12 @@ function validate(): boolean {
}
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();
isSaving.value = true;
try {
@@ -552,10 +609,10 @@ onUnmounted(() => navBarStore.clearRight());
min-width: 0;
display: flex;
overflow: hidden;
align-items: center;
justify-content: start;
strong {
flex: 1 1 0;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -568,6 +625,12 @@ onUnmounted(() => navBarStore.clearRight());
white-space: nowrap;
}
}
}
.input {
gap: .5rem;
display: flex;
margin-top: .5rem;
.sku-tags {
gap: .375rem;
@@ -575,12 +638,6 @@ onUnmounted(() => navBarStore.clearRight());
flex-wrap: wrap;
align-items: center;
}
}
.input {
gap: .5rem;
display: flex;
margin-top: .5rem;
.unit {
font-size: .875rem;
@@ -591,10 +648,8 @@ onUnmounted(() => navBarStore.clearRight());
}
}
.empty {
color: var(--td-text-color-secondary);
padding: 2rem 1rem;
text-align: center;
.tips {
font-size: .875rem;
}
}