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
+42 -52
View File
@@ -46,35 +46,24 @@
<strong>购买产品</strong>
<span v-text="`${items.length} 项`" />
</div>
<div class="tips">同一产品单位不能重复,数量最多三位小数</div>
<div class="tips">同一 SKU 单位不能重复,数量最多三位小数</div>
<div class="items">
<div v-for="(item, index) in items" :key="item.clientKey" class="item">
<div class="header">
<span class="no" v-text="String(index + 1).padStart(2, '0')" />
<strong v-text="item.productName || '未选择产品'" />
<strong v-text="getItemTitle(item)" />
<t-button theme="danger" variant="text" size="extra-small" @click="removeItem(index)">删除</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="请选择产品"
selection-mode="product"
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="canPriceOverride ? '成交单价' : '单价'">
<t-input
@@ -93,7 +82,7 @@
:max="maxQuantity"
:min="getQuantityStep(item)"
:step="getQuantityStep(item)"
:integer="!getProduct(item)?.allowDecimalQuantity"
:integer="!getSku(item)?.allowDecimalQuantity"
theme="outline"
/>
</t-form-item>
@@ -181,7 +170,7 @@ import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import PhotoScroller from "@/components/PhotoScroller.vue";
import type { Customer } from "@/types/Customer";
import type { Product, ProductUnit } from "@/types/Product";
import type { Product, ProductSku, ProductSkuUnit } from "@/types/Product";
import type { PhotoItem } from "@/types/Photo";
import type { DirectProductOrderItemRequest } from "@/types/SalesOrder";
import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
@@ -213,6 +202,7 @@ const photoItems = ref<PhotoItem[]>([]);
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const maxQuantity = Number.MAX_SAFE_INTEGER;
const productCache = new Map<string, Product>();
const skuCache = new Map<string, ProductSku>();
const requestNo = ref("");
const customerDisplayText = computed(() => customer.value?.name || customer.value?.code || "选择客户");
const customerCode = ref("");
@@ -221,12 +211,27 @@ const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别。");
let latestDecodedText = "";
function getProduct(item: FormItem): Product | undefined {
return productCache.get(item.productId || "");
function getSku(item: FormItem): ProductSku | undefined {
return skuCache.get(item.skuId || "");
}
function getQuantityStep(item: FormItem): number {
return getProductQuantityStep(getProduct(item));
return getProductQuantityStep(getSku(item));
}
function getProduct(item: FormItem): Product | undefined {
const sku = getSku(item);
return productCache.get(sku?.productId || "");
}
function getUnit(item: FormItem): ProductSkuUnit | undefined {
return getSku(item)?.unitList?.find(unit => unit.id === item.skuUnitId);
}
function getItemTitle(item: FormItem): string {
const product = getProduct(item);
const sku = getSku(item);
return sku?.specSummary || sku?.name || product?.name || item.productName || "未选择产品";
}
const totalAmount = computed(() => {
@@ -264,45 +269,30 @@ function removeItem(index: number): void {
}
}
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 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 item = items.value[index];
const saleableUnits = (product.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
const productUnitId = saleableUnits.length < 2 ? saleableUnits[0]?.id || "" : value.productUnitId;
const selectionChanged = item.productId !== value.productId || item.productUnitId !== productUnitId;
item.productId = value.productId;
item.productUnitId = productUnitId;
const selectionChanged = item.skuId !== value.skuId || item.skuUnitId !== value.skuUnitId;
item.skuId = value.skuId;
item.skuUnitId = value.skuUnitId;
item.productName = product.name;
if (selectionChanged) {
item.unitPriceText = "";
}
}
function getSaleableUnits(item: FormItem): ProductUnit[] {
return (productCache.get(item.productId || "")?.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
}
function selectProductUnit(index: number, unit: ProductUnit): void {
const item = items.value[index];
const product = productCache.get(item.productId || "");
if (!product?.id || !unit.id) {
return;
}
if (item.productUnitId !== unit.id) {
item.unitPriceText = "";
}
item.productUnitId = unit.id;
item.productName = product.name;
}
function getItemUnitPrice(item: FormItem): number | undefined {
if (canPriceOverride.value && item.unitPriceText.trim()) {
return parsePackageAmount(item.unitPriceText);
}
const salePrice = productCache.get(item.productId || "")?.unitList?.find(unit => unit.id === item.productUnitId)?.salePrice;
const salePrice = getUnit(item)?.salePrice;
return salePrice === null ? undefined : salePrice;
}
function getItemUnitPricePlaceholder(item: FormItem): string {
const salePrice = productCache.get(item.productId || "")?.unitList?.find(unit => unit.id === item.productUnitId)?.salePrice;
const salePrice = getUnit(item)?.salePrice;
return salePrice === undefined || salePrice === null ? "产品价" : `产品价:${formatProductAmount(salePrice)}`;
}
function calculateItemAmount(item: FormItem): number | undefined {
@@ -388,9 +378,9 @@ function validate(): boolean {
const keys = new Set<string>();
for (const item of items.value) {
const quantity = normalizePackageQuantity(item.quantity ?? "");
const key = `${item.productId}\u0000${item.productUnitId}`;
if (!item.productId || !item.productUnitId || !quantity || quantity === "0") { Message.warning("产品单位和正数量不能为空"); return false; }
if (getProduct(item) && !getProduct(item)!.allowDecimalQuantity && quantity.includes(".")) { Message.warning("该产品不允许小数数量"); return false; }
const key = `${item.skuId}\u0000${item.skuUnitId}`;
if (!item.skuId || !item.skuUnitId || !quantity || quantity === "0") { Message.warning("SKU 单位和正数量不能为空"); return false; }
if (getSku(item) && !getSku(item)!.allowDecimalQuantity && quantity.includes(".")) { Message.warning("该 SKU 不允许小数数量"); return false; }
if (keys.has(key)) { Message.warning("产品单位不能重复"); return false; }
keys.add(key);
if (canPriceOverride.value && item.unitPriceText && parsePackageAmount(item.unitPriceText) === undefined) { Message.warning("成交单价必须是合法金额"); return false; }
@@ -403,8 +393,8 @@ async function save(): Promise<void> {
isSaving.value = true;
try {
const itemList = items.value.map((item, index) => ({
productId: item.productId!,
productUnitId: item.productUnitId!,
skuId: item.skuId!,
skuUnitId: item.skuUnitId!,
quantity: normalizePackageQuantity(item.quantity ?? "")!,
unitPrice: canPriceOverride.value && item.unitPriceText ? parsePackageAmount(item.unitPriceText) : undefined,
sort: index,