support split unit for product order
This commit is contained in:
@@ -80,7 +80,7 @@
|
||||
<t-form class="items" label-width="2.5rem">
|
||||
<div class="title">
|
||||
<strong>购买权益明细</strong>
|
||||
<span v-if="!canPriceOverride">当前账号无定制权限,明细不可修改</span>
|
||||
<span v-if="!canPriceOverride">当前账号无定制权限,仅可等值转换单位</span>
|
||||
</div>
|
||||
<t-form-item
|
||||
v-for="(item, index) in packagePurchaseItemList"
|
||||
@@ -102,7 +102,15 @@
|
||||
/>
|
||||
</template>
|
||||
<div class="content">
|
||||
<span class="name" v-text="getItemLabel(item)" />
|
||||
<div class="item-title">
|
||||
<span class="name" v-text="getItemLabel(item)" />
|
||||
<t-button
|
||||
size="extra-small"
|
||||
variant="outline"
|
||||
:content="getItemUnit(item)?.name || '选择单位'"
|
||||
@click="openItemUnitPicker(index)"
|
||||
/>
|
||||
</div>
|
||||
<t-input
|
||||
:model-value="item.totalQuantity"
|
||||
type="number"
|
||||
@@ -113,9 +121,18 @@
|
||||
@update:model-value="updateItemQuantity(index, $event)"
|
||||
>
|
||||
<template #suffix-icon>
|
||||
<span class="unit" v-text="getItemUnit(item)" />
|
||||
<span class="unit" v-text="getItemUnit(item)?.name || ''" />
|
||||
</template>
|
||||
</t-input>
|
||||
<t-button
|
||||
v-if="canPriceOverride"
|
||||
class="remove"
|
||||
size="extra-small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
content="移除"
|
||||
@click="removePurchaseItem(index)"
|
||||
/>
|
||||
</div>
|
||||
</t-form-item>
|
||||
<div v-if="!packagePurchaseItemList.length" class="center-tips" v-text="'当前套餐暂无权益明细'" />
|
||||
@@ -185,6 +202,9 @@
|
||||
<div v-else class="center-tips" v-text="'暂无可购买套餐'" />
|
||||
</div>
|
||||
</t-popup>
|
||||
<t-popup v-model="unitPickerVisible" placement="bottom">
|
||||
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="调整权益单位" @cancel="unitPickerVisible = false" @confirm="confirmItemUnitPicker" />
|
||||
</t-popup>
|
||||
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
||||
<div class="selected-customer-popup bg-white">
|
||||
<div class="popup-header">
|
||||
@@ -221,6 +241,7 @@ import { Time } from "timi-web";
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import PackageAPI from "@/api/PackageAPI";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
import CodeScanner from "@/components/CodeScanner.vue";
|
||||
import CustomerList from "@/components/CustomerList.vue";
|
||||
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||
@@ -232,8 +253,9 @@ import { useSettingStore } from "@/store/settingStore";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
import type { PhotoItem } from "@/types/Photo";
|
||||
import type { GaoPackageConfig, PackageAttachment, PackagePurchaseItem } from "@/types/Package";
|
||||
import type { ProductSku, ProductSkuUnit } from "@/types/Product";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { formatPackageAmount } from "@/utils/package";
|
||||
import { addPackageQuantity, convertPackageQuantity, formatPackageAmount, normalizePackageQuantity } from "@/utils/package";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
import { uploadPhotoList } from "@/utils/photo";
|
||||
@@ -256,6 +278,10 @@ const isCustomerPackageCreate = computed(() => route.name === "CustomerPackageCr
|
||||
const config = ref<GaoPackageConfig>();
|
||||
const configList = ref<GaoPackageConfig[]>([]);
|
||||
const configSelectorVisible = ref(false);
|
||||
const skuMap = reactive<Record<string, ProductSku>>({});
|
||||
const unitPickerVisible = ref(false);
|
||||
const unitPickerIndex = ref(-1);
|
||||
const unitPickerDraft = ref<Array<string | number>>([]);
|
||||
|
||||
// ---------- 客户 ----------
|
||||
|
||||
@@ -405,8 +431,13 @@ const {
|
||||
buildAttachments
|
||||
});
|
||||
|
||||
const unitPickerOptions = computed(() => {
|
||||
const item = packagePurchaseItemList.value[unitPickerIndex.value];
|
||||
return item ? getSelectableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
|
||||
});
|
||||
|
||||
function getItemLabel(item: PackagePurchaseItem): string {
|
||||
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId);
|
||||
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId);
|
||||
if (!configItem) {
|
||||
return "产品权益";
|
||||
}
|
||||
@@ -416,16 +447,78 @@ function getItemLabel(item: PackagePurchaseItem): string {
|
||||
}
|
||||
|
||||
function getItemName(item: PackagePurchaseItem): string {
|
||||
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId)?.productNameSnapshot || "未命名产品";
|
||||
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId)?.productNameSnapshot || "未命名产品";
|
||||
}
|
||||
|
||||
function getItemPhotoURL(item: PackagePurchaseItem): string {
|
||||
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId);
|
||||
return getProductPhotoURL(configItem?.sku || {});
|
||||
return getProductPhotoURL(getItemSku(item) || {});
|
||||
}
|
||||
|
||||
function getItemUnit(item: PackagePurchaseItem): string {
|
||||
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId)?.unitSnapshot || "";
|
||||
function getItemSku(item: PackagePurchaseItem): ProductSku | undefined {
|
||||
return skuMap[item.skuId] || config.value?.itemList?.find(configItem => configItem.skuId === item.skuId)?.sku;
|
||||
}
|
||||
|
||||
function getItemUnit(item: PackagePurchaseItem): ProductSkuUnit | undefined {
|
||||
return getItemSku(item)?.unitList?.find(unit => unit.id === item.skuUnitId);
|
||||
}
|
||||
|
||||
function getSelectableUnitList(item: PackagePurchaseItem): ProductSkuUnit[] {
|
||||
const maxBaseQuantityPerUnit = Math.max(...(config.value?.itemList || [])
|
||||
.filter(configItem => configItem.skuId === item.skuId)
|
||||
.map(configItem => Number(configItem.baseQuantityPerUnitSnapshot || 0)));
|
||||
return (getItemSku(item)?.unitList || []).filter(unit => unit.allowSale !== false && !!unit.id
|
||||
&& Number(unit.baseQuantityPerUnit) <= maxBaseQuantityPerUnit);
|
||||
}
|
||||
|
||||
function openItemUnitPicker(index: number): void {
|
||||
const item = packagePurchaseItemList.value[index];
|
||||
if (!item || !getSelectableUnitList(item).length) {
|
||||
Message.warning("当前 SKU 没有可用于套餐权益的销售单位");
|
||||
return;
|
||||
}
|
||||
unitPickerIndex.value = index;
|
||||
unitPickerDraft.value = [item.skuUnitId];
|
||||
unitPickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmItemUnitPicker(value: Array<string | number>): void {
|
||||
const index = unitPickerIndex.value;
|
||||
const item = packagePurchaseItemList.value[index];
|
||||
const skuUnitId = String(value[0] || "");
|
||||
const currentUnit = item && getItemUnit(item);
|
||||
const targetUnit = item && getSelectableUnitList(item).find(unit => unit.id === skuUnitId);
|
||||
if (!item || !currentUnit || !targetUnit || !skuUnitId || skuUnitId === item.skuUnitId) {
|
||||
unitPickerVisible.value = false;
|
||||
return;
|
||||
}
|
||||
const convertedQuantity = convertPackageQuantity(item.totalQuantity, currentUnit.baseQuantityPerUnit, targetUnit.baseQuantityPerUnit);
|
||||
if (!convertedQuantity) {
|
||||
Message.warning("调整单位后数量不能精确保留三位小数");
|
||||
return;
|
||||
}
|
||||
const duplicateIndex = packagePurchaseItemList.value.findIndex((candidate, candidateIndex) => candidateIndex !== index
|
||||
&& candidate.skuId === item.skuId && candidate.skuUnitId === skuUnitId);
|
||||
if (duplicateIndex < 0) {
|
||||
packagePurchaseItemList.value[index] = { ...item, skuUnitId, totalQuantity: convertedQuantity };
|
||||
} else {
|
||||
const duplicateItem = packagePurchaseItemList.value[duplicateIndex];
|
||||
const totalQuantity = addPackageQuantity(duplicateItem.totalQuantity, convertedQuantity);
|
||||
if (!totalQuantity) {
|
||||
Message.warning("合并同单位权益后数量不能精确保留三位小数");
|
||||
return;
|
||||
}
|
||||
packagePurchaseItemList.value = packagePurchaseItemList.value.flatMap((candidate, candidateIndex) => {
|
||||
if (candidateIndex === index) {
|
||||
return [];
|
||||
}
|
||||
if (candidateIndex === duplicateIndex) {
|
||||
return [{ ...candidate, totalQuantity, sort: Math.min(candidate.sort ?? candidateIndex, item.sort ?? index) }];
|
||||
}
|
||||
return [candidate];
|
||||
});
|
||||
}
|
||||
resetRequestNo();
|
||||
unitPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function updateItemQuantity(index: number, value: string | number): void {
|
||||
@@ -433,13 +526,33 @@ function updateItemQuantity(index: number, value: string | number): void {
|
||||
...item,
|
||||
totalQuantity: String(value)
|
||||
} : item);
|
||||
resetRequestNo();
|
||||
}
|
||||
|
||||
function removePurchaseItem(index: number): void {
|
||||
if (!canPriceOverride.value) {
|
||||
return;
|
||||
}
|
||||
packagePurchaseItemList.value = packagePurchaseItemList.value.filter((_, itemIndex) => itemIndex !== index);
|
||||
resetRequestNo();
|
||||
}
|
||||
|
||||
function validateItemDecimalQuantity(): boolean {
|
||||
for (const item of packagePurchaseItemList.value) {
|
||||
const quantity = normalizePackageQuantity(item.totalQuantity);
|
||||
if (quantity && quantity !== "0" && quantity.includes(".") && getItemSku(item)?.allowDecimalQuantity === false) {
|
||||
Message.warning("该 SKU 不允许小数数量");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function submitRecord(): Promise<void> {
|
||||
if (isSaving.value) {
|
||||
return;
|
||||
}
|
||||
if (!validatePurchaseForm()) {
|
||||
if (!validatePurchaseForm() || !validateItemDecimalQuantity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -473,6 +586,7 @@ async function loadPageData(): Promise<void> {
|
||||
}
|
||||
config.value = await PackageAPI.configDetail(configId.value);
|
||||
applyConfig(config.value);
|
||||
await loadConfigSkuList(config.value);
|
||||
}
|
||||
|
||||
async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
||||
@@ -480,9 +594,21 @@ async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
||||
return;
|
||||
}
|
||||
config.value = await PackageAPI.configDetail(item.id);
|
||||
await loadConfigSkuList(config.value);
|
||||
configSelectorVisible.value = false;
|
||||
}
|
||||
|
||||
async function loadConfigSkuList(value?: GaoPackageConfig): Promise<void> {
|
||||
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)) {
|
||||
@@ -608,11 +734,27 @@ watch(config, value => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
.content {
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
|
||||
.item-title {
|
||||
gap: .375rem;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.t-input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.remove {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user