add product sku

This commit is contained in:
Timi
2026-09-07 12:59:27 +08:00
parent 9841723177
commit 1d70aebd83
49 changed files with 3700 additions and 756 deletions
+10 -5
View File
@@ -30,8 +30,8 @@
<t-cell
class="item"
v-for="item in config.itemList || []"
:title="item.productNameSnapshot"
:key="item.id || `${item.productId}-${item.productUnitId}`"
:title="getItemTitle(item)"
:key="item.id || `${item.skuId}-${item.skuUnitId}`"
:note="`${item.defaultQuantity} ${item.unitSnapshot || ''}${item.remark ? `|${item.remark}` : ''}`"
>
<template #leftIcon>
@@ -50,7 +50,7 @@
</template>
<template #description>
<t-tag
:content="getProductTypeText(item.product?.productType)"
:content="item.skuCodeSnapshot || item.sku?.code || 'SKU'"
theme="primary"
variant="light"
/>
@@ -89,7 +89,7 @@ import { getErrorMessage } from "@/utils/error";
import { formatPackageAmount, getPackageConfigStatusText } from "@/utils/package";
import { Message } from "@/utils/message";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { getProductPhotoURL as getProductPhotoUrl, getProductTypeText } from "@/utils/product";
import { getProductPhotoURL as getProductPhotoUrl } from "@/utils/product";
defineOptions({ name: "PackageConfigDetail" });
@@ -113,7 +113,12 @@ function getStatusTheme(status?: PackageConfigStatus): "success" | "warning" | "
}
function getItemPhotoURL(item: PackageConfigItem): string {
return getProductPhotoUrl(item.product || {});
return getProductPhotoUrl(item.sku || {});
}
function getItemTitle(item: PackageConfigItem): string {
const skuText = item.skuNameSnapshot || item.specSummarySnapshot;
return `${item.productNameSnapshot || "未命名产品"}${skuText ? ` · ${skuText}` : ""}`;
}
async function loadData(): Promise<void> {
+48 -56
View File
@@ -34,7 +34,7 @@
>
<div class="header">
<span class="no" v-text="String(index + 1).padStart(2, '0')" />
<strong v-text="item.productNameSnapshot || '未选择产品'" />
<strong v-text="getItemTitle(item)" />
<t-button
v-if="1 < form.itemList.length"
theme="danger"
@@ -44,26 +44,15 @@
>删除</t-button>
</div>
<product-selector
:model-value="item.productId ? {productId: item.productId, productUnitId: item.productUnitId || ''} : undefined"
:model-value="item.skuId ? { skuId: item.skuId, skuUnitId: item.skuUnitId || '' } : undefined"
:selected-product="getProduct(item)"
label="产品"
placeholder="请选择产品"
required-mark
@update:model-value="applyProduct(index, $event)"
/>
<t-form-item v-if="item.productId" label="产品单位" required-mark>
<div v-if="getSaleableUnits(item).length" class="unit-list">
<t-button
v-for="unit in getSaleableUnits(item)"
:key="unit.id"
:theme="item.productUnitId === unit.id ? 'primary' : 'default'"
:variant="item.productUnitId === unit.id ? 'base' : 'outline'"
size="extra-small"
:content="unit.name"
@click="selectProductUnit(index, unit)"
/>
</div>
<span v-else class="unit-empty" v-text="'暂无可售单位'" />
<t-form-item v-if="item.skuId" label="SKU 单位" content-align="right">
<span v-text="getUnit(item)?.name || '未选择单位'" />
</t-form-item>
<t-form-item label="默认配额" content-align="right" required-mark>
<t-stepper
@@ -72,7 +61,7 @@
:max="Number.MAX_SAFE_INTEGER"
:min="getQuantityStep(item)"
:step="getQuantityStep(item)"
:integer="!getProduct(item)?.allowDecimalQuantity"
:integer="!getSku(item)?.allowDecimalQuantity"
theme="outline"
/>
</t-form-item>
@@ -122,7 +111,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, ProductUnit } from "@/types/Product";
import type { Product, ProductSku, ProductSkuUnit } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { formatPackageAmountInput, getPackageConfigStatusText, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
import { Message } from "@/utils/message";
@@ -178,13 +167,28 @@ function createItem(item?: Partial<PackageConfigItem>): PackageConfigFormItem {
}
const productCache = new Map<string, Product>();
const skuCache = new Map<string, ProductSku>();
function getProduct(item: PackageConfigFormItem): Product | undefined {
return productCache.get(item.productId || "");
const sku = getSku(item);
return productCache.get(sku?.productId || "");
}
function getSku(item: PackageConfigFormItem): ProductSku | undefined {
return skuCache.get(item.skuId || "") || item.sku;
}
function getUnit(item: PackageConfigFormItem): ProductSkuUnit | undefined {
return getSku(item)?.unitList?.find(unit => unit.id === item.skuUnitId);
}
function getItemTitle(item: PackageConfigFormItem): string {
const sku = getSku(item);
return sku?.specSummary || sku?.name || item.productNameSnapshot || "未选择产品";
}
function getQuantityStep(item: PackageConfigFormItem): number {
return getProductQuantityStep(getProduct(item));
return getProductQuantityStep(getSku(item));
}
const estimatedOriginalAmount = computed<number | undefined>(() => {
@@ -200,45 +204,29 @@ const estimatedOriginalAmount = computed<number | undefined>(() => {
});
const estimatedOriginalAmountText = computed(() => estimatedOriginalAmount.value === undefined ? "待完善" : `¥${formatProductAmount(estimatedOriginalAmount.value)}`);
async function applyProduct(index: number, value?: { productId: string; productUnitId: string }): Promise<void> {
async function applyProduct(index: number, value?: { skuId: string; skuUnitId: string }): Promise<void> {
if (!value) {
return;
}
const product = productCache.get(value.productId) || await ProductAPI.detail(value.productId);
productCache.set(value.productId, product);
const saleableUnits = (product.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
const productUnitId = saleableUnits.length < 2 ? saleableUnits[0]?.id || "" : value.productUnitId;
const unit = saleableUnits.find(item => item.id === productUnitId);
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], {
productId: value.productId,
productUnitId,
skuId: value.skuId,
skuUnitId: value.skuUnitId,
productNameSnapshot: product.name,
skuNameSnapshot: sku.name,
specSummarySnapshot: sku.specSummary,
unitSnapshot: unit?.name || "",
baseQuantityPerUnitSnapshot: unit?.baseQuantityPerUnit
});
}
function getSaleableUnits(item: PackageConfigFormItem): ProductUnit[] {
return (productCache.get(item.productId || "")?.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
}
function selectProductUnit(index: number, unit: ProductUnit): void {
const item = form.itemList[index];
const product = productCache.get(item.productId || "");
if (!product?.id || !unit.id) {
return;
}
Object.assign(item, {
productUnitId: unit.id,
productNameSnapshot: product.name,
unitSnapshot: unit.name,
baseQuantityPerUnitSnapshot: unit.baseQuantityPerUnit
});
}
function calculateOriginalAmount(item: PackageConfigFormItem): number | undefined {
const quantity = normalizePackageQuantity(item.defaultQuantity ?? "");
const salePrice = productCache.get(item.productId || "")?.unitList?.find(unit => unit.id === item.productUnitId)?.salePrice;
const salePrice = getUnit(item)?.salePrice;
if (!quantity || quantity === "0" || salePrice === undefined || salePrice === null) {
return undefined;
}
@@ -331,13 +319,13 @@ function validateForm(): boolean {
const keySet = new Set<string>();
for (const item of form.itemList) {
const quantity = normalizePackageQuantity(item.defaultQuantity ?? "");
const key = `${item.productId}\u0000${item.productUnitId}`;
if (!item.productId || !item.productUnitId || !quantity || quantity === "0") {
Message.warning("产品单位和正数量不能为空");
const key = `${item.skuId}\u0000${item.skuUnitId}`;
if (!item.skuId || !item.skuUnitId || !quantity || quantity === "0") {
Message.warning("SKU 单位和正数量不能为空");
return false;
}
if (getProduct(item) && !getProduct(item)!.allowDecimalQuantity && quantity.includes(".")) {
Message.warning("该产品不允许小数数量");
if (getSku(item) && !getSku(item)!.allowDecimalQuantity && quantity.includes(".")) {
Message.warning("该 SKU 不允许小数数量");
return false;
}
if (keySet.has(key)) {
@@ -371,8 +359,8 @@ async function save(): Promise<void> {
beginAt: form.beginAt ?? null,
endAt: form.endAt ?? null,
itemList: form.itemList.map((item, index) => ({
productId: item.productId!,
productUnitId: item.productUnitId!,
skuId: item.skuId!,
skuUnitId: item.skuUnitId!,
defaultQuantity: normalizePackageQuantity(item.defaultQuantity ?? "")!,
sort: index,
remark: item.remark?.trim() || null
@@ -408,9 +396,13 @@ async function loadData(): Promise<void> {
}
const result = await PackageAPI.configDetail(routeId.value);
const itemList = (result.itemList || []).map(createItem);
const productIdList = [...new Set(itemList.map(item => item.productId).filter((id): id is string => !!id))];
await Promise.all(productIdList.map(async productId => {
productCache.set(productId, await ProductAPI.detail(productId));
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));
}
}));
Object.assign(form, {
id: result.id || routeId.value,
+3 -2
View File
@@ -46,7 +46,7 @@
<div class="products">
<t-tag
v-for="productItem in item.itemList || []"
:key="productItem.id || `${productItem.productId}-${productItem.productUnitId}`"
:key="productItem.id || `${productItem.skuId}-${productItem.skuUnitId}`"
:content="getProductRightText(productItem)"
theme="primary"
variant="light"
@@ -193,7 +193,8 @@ function getStatusTheme(statusValue?: PackageConfigStatus): "success" | "warning
function getProductRightText(item: PackageConfigItem): string {
const quantity = getPackageQuantityText(item.defaultQuantity);
return `${item.productNameSnapshot || "未命名产品"} × ${quantity}${item.unitSnapshot ? ` ${item.unitSnapshot}` : ""}`;
const skuText = item.skuNameSnapshot || item.specSummarySnapshot;
return `${item.productNameSnapshot || "未命名产品"}${skuText ? ` · ${skuText}` : ""} × ${quantity}${item.unitSnapshot ? ` ${item.unitSnapshot}` : ""}`;
}
onMounted(async () => {
+7 -7
View File
@@ -75,7 +75,7 @@
</div>
<t-form-item
v-for="(item, index) in packagePurchaseItemList"
:key="`${index}-${item.productId}-${item.productUnitId}`"
:key="`${index}-${item.skuId}-${item.skuUnitId}`"
class="item"
>
<template #label>
@@ -374,21 +374,21 @@ const {
});
function getItemLabel(item: PackagePurchaseItem): string {
const configItem = config.value?.itemList?.find(configItem => configItem.productId === item.productId && configItem.productUnitId === item.productUnitId);
return configItem ? `${configItem.productNameSnapshot || "未命名产品"}` : "产品权益";
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId);
return configItem ? `${configItem.productNameSnapshot || "未命名产品"} · ${configItem.skuNameSnapshot || configItem.specSummarySnapshot || "默认 SKU"}` : "产品权益";
}
function getItemName(item: PackagePurchaseItem): string {
return config.value?.itemList?.find(configItem => configItem.productId === item.productId && configItem.productUnitId === item.productUnitId)?.productNameSnapshot || "未命名产品";
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId)?.productNameSnapshot || "未命名产品";
}
function getItemPhotoURL(item: PackagePurchaseItem): string {
const configItem = config.value?.itemList?.find(configItem => configItem.productId === item.productId && configItem.productUnitId === item.productUnitId);
return getProductPhotoURL(configItem?.product || {});
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId);
return getProductPhotoURL(configItem?.sku || {});
}
function getItemUnit(item: PackagePurchaseItem): string {
return config.value?.itemList?.find(configItem => configItem.productId === item.productId && configItem.productUnitId === item.productUnitId)?.unitSnapshot || "";
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId)?.unitSnapshot || "";
}
function updateItemQuantity(index: number, value: string | number): void {
+11 -6
View File
@@ -42,8 +42,8 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
}
saleAmountText.value = formatPackageAmountInput(config.standardAmount);
packagePurchaseItemList.value = (config.itemList || []).map((item, index) => ({
productId: item.productId,
productUnitId: item.productUnitId,
skuId: item.skuId,
skuUnitId: item.skuUnitId,
totalQuantity: normalizePackageQuantity(item.defaultQuantity) || "",
sort: item.sort ?? index,
remark: item.remark ?? null
@@ -79,8 +79,8 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
purchasedAt: options.buildRegisteredAt(),
attachmentList: options.buildAttachments(),
itemList: packagePurchaseItemList.value.length ? packagePurchaseItemList.value.map((item, index) => ({
productId: item.productId,
productUnitId: item.productUnitId,
skuId: item.skuId,
skuUnitId: item.skuUnitId,
totalQuantity: normalizePackageQuantity(item.totalQuantity)!,
sort: item.sort ?? index,
remark: item.remark ?? null
@@ -112,8 +112,13 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
}
for (const item of packagePurchaseItemList.value) {
const quantity = normalizePackageQuantity(item.totalQuantity);
if (!item.productId || !item.productUnitId || !quantity || quantity === "0") {
Message.warning("产品单位和正数量不能为空");
if (!item.skuId || !item.skuUnitId || !quantity || quantity === "0") {
Message.warning("SKU 单位和正数量不能为空");
return false;
}
const configItem = options.config.value.itemList?.find(source => source.skuId === item.skuId && source.skuUnitId === item.skuUnitId);
if (configItem?.sku && !configItem.sku.allowDecimalQuantity && quantity.includes(".")) {
Message.warning("该 SKU 不允许小数数量");
return false;
}
}