support split unit for product order
This commit is contained in:
@@ -48,22 +48,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<t-button
|
||||
size="extra-small"
|
||||
variant="outline"
|
||||
:content="getFulfillmentUnit(item)?.name || '选择实际单位'"
|
||||
@click.stop="openUnitPicker(item)"
|
||||
/>
|
||||
<t-input
|
||||
v-model.trim="quantityMap[item.id || '']"
|
||||
class="quantity"
|
||||
type="number"
|
||||
borderless
|
||||
align="right"
|
||||
:disabled="!getFulfillmentUnit(item)"
|
||||
placeholder="请输入履约数量"
|
||||
>
|
||||
<template #suffix-icon>
|
||||
<span class="unit" v-text="item.unitSnapshot" />
|
||||
<span class="unit" v-text="getFulfillmentUnit(item)?.name || ''" />
|
||||
</template>
|
||||
</t-input>
|
||||
<t-button
|
||||
size="extra-small"
|
||||
variant="outline"
|
||||
:disabled="getAvailableQuantity(item) === '0'"
|
||||
:disabled="getAvailableBaseQuantity(item) === '0'"
|
||||
@click.stop="fillAllQuantity(item)"
|
||||
>全部履约</t-button>
|
||||
</div>
|
||||
@@ -100,24 +107,29 @@
|
||||
<t-popup v-model="pickerVisible" placement="bottom">
|
||||
<t-date-time-picker v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="履约时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
||||
</t-popup>
|
||||
<t-popup v-model="unitPickerVisible" placement="bottom">
|
||||
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="选择实际履约单位" @cancel="unitPickerVisible = false" @confirm="confirmUnitPicker" />
|
||||
</t-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||
import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
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, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
|
||||
import { comparePackageQuantity, convertPackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity, toPackageBaseQuantity } from "@/utils/package";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
import { uploadPhotoList } from "@/utils/photo";
|
||||
import { getProductPhotoURL } from "@/utils/product";
|
||||
|
||||
@@ -128,6 +140,8 @@ const navBarStore = useNavBarStore();
|
||||
const userStore = useUserStore();
|
||||
const order = ref<SalesOrder>();
|
||||
const quantityMap = reactive<Record<string, string>>({});
|
||||
const fulfillmentSkuMap = reactive<Record<string, ProductSku>>({});
|
||||
const fulfillmentUnitMap = reactive<Record<string, string>>({});
|
||||
const deliveredAtText = ref("");
|
||||
const draftTime = ref("");
|
||||
const pickerVisible = ref(false);
|
||||
@@ -135,12 +149,19 @@ const remark = ref("");
|
||||
const requestNo = ref("");
|
||||
const isSaving = ref(false);
|
||||
const photoItems = ref<PhotoItem[]>([]);
|
||||
const unitPickerVisible = ref(false);
|
||||
const unitPickerSourceItemId = ref("");
|
||||
const unitPickerDraft = ref<Array<string | number>>([]);
|
||||
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");
|
||||
const sourceItems = computed<GaoSalesOrderItem[]>(() => (order.value?.itemList || []).filter(item => item.itemType === expectedItemType.value));
|
||||
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
|
||||
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
|
||||
const sourceItems = computed<GaoSalesOrderItem[]>(() => (order.value?.itemList || []).filter(item => item.itemType === expectedItemType.value
|
||||
&& 0 < comparePackageQuantity(item.availableBaseQuantity ?? "0", "0")));
|
||||
const unitPickerOptions = computed(() => {
|
||||
const item = sourceItems.value.find(value => value.id === unitPickerSourceItemId.value);
|
||||
return item ? getSellableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
|
||||
});
|
||||
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || userStore.hasRoles(gaoRoleCodes.storeManager));
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
const salesOrderId = String(route.query.salesOrderId || "");
|
||||
@@ -151,23 +172,86 @@ async function loadData(): Promise<void> {
|
||||
}
|
||||
try {
|
||||
order.value = await SalesOrderAPI.detail(salesOrderId);
|
||||
await loadFulfillmentSkuList();
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "履约来源加载失败"));
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableQuantity(item: GaoSalesOrderItem): string {
|
||||
return getPackageQuantityText(item.availableQuantity);
|
||||
async function loadFulfillmentSkuList(): Promise<void> {
|
||||
const skuIdList = [...new Set(sourceItems.value.map(item => item.skuId).filter((skuId): skuId is string => !!skuId))];
|
||||
await Promise.all(skuIdList.map(async skuId => {
|
||||
try {
|
||||
fulfillmentSkuMap[skuId] = await ProductAPI.skuDetail(skuId);
|
||||
} catch {
|
||||
// 提交时仍由后端校验实际履约单位,单个 SKU 加载失败不影响其他明细
|
||||
}
|
||||
}));
|
||||
for (const item of sourceItems.value) {
|
||||
if (!item.id || fulfillmentUnitMap[item.id]) {
|
||||
continue;
|
||||
}
|
||||
const originalUnit = getSellableUnitList(item).find(unit => unit.id === item.skuUnitId);
|
||||
const defaultUnit = originalUnit || getSellableUnitList(item)[0];
|
||||
if (defaultUnit?.id) {
|
||||
fulfillmentUnitMap[item.id] = defaultUnit.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableBaseQuantity(item: GaoSalesOrderItem): string {
|
||||
return getPackageQuantityText(item.availableBaseQuantity);
|
||||
}
|
||||
|
||||
function getItemDescription(item: GaoSalesOrderItem): string {
|
||||
return `剩余 ${getAvailableQuantity(item)}`;
|
||||
return `可履约 ${getAvailableBaseQuantity(item)} ${getBaseUnitName(item)}`;
|
||||
}
|
||||
|
||||
function getBaseUnitName(item: GaoSalesOrderItem): string {
|
||||
return item.skuId ? fulfillmentSkuMap[item.skuId]?.unitList?.find(unit => unit.isBase)?.name || "基础单位" : "基础单位";
|
||||
}
|
||||
|
||||
function fillAllQuantity(item: GaoSalesOrderItem): void {
|
||||
if (item.id) {
|
||||
quantityMap[item.id] = getAvailableQuantity(item);
|
||||
const unit = getFulfillmentUnit(item);
|
||||
if (!item.id || !unit) {
|
||||
Message.warning("请先选择实际履约单位");
|
||||
return;
|
||||
}
|
||||
const quantity = convertPackageQuantity(getAvailableBaseQuantity(item), "1", unit.baseQuantityPerUnit);
|
||||
if (!quantity) {
|
||||
Message.warning("当前单位不能精确履约全部剩余权益,请填写可精确换算的数量");
|
||||
return;
|
||||
}
|
||||
quantityMap[item.id] = quantity;
|
||||
}
|
||||
|
||||
function getSellableUnitList(item: GaoSalesOrderItem): ProductSkuUnit[] {
|
||||
return (item.skuId ? fulfillmentSkuMap[item.skuId]?.unitList : [])?.filter(unit => unit.allowSale !== false && !!unit.id) || [];
|
||||
}
|
||||
|
||||
function getFulfillmentUnit(item: GaoSalesOrderItem): ProductSkuUnit | undefined {
|
||||
return getSellableUnitList(item).find(unit => unit.id === fulfillmentUnitMap[item.id || ""]);
|
||||
}
|
||||
|
||||
function openUnitPicker(item: GaoSalesOrderItem): void {
|
||||
if (!item.id) {
|
||||
return;
|
||||
}
|
||||
if (!getSellableUnitList(item).length) {
|
||||
Message.warning("当前 SKU 没有可用于履约的销售单位");
|
||||
return;
|
||||
}
|
||||
unitPickerSourceItemId.value = item.id;
|
||||
unitPickerDraft.value = [fulfillmentUnitMap[item.id] || getSellableUnitList(item)[0]?.id || ""];
|
||||
unitPickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmUnitPicker(value: Array<string | number>): void {
|
||||
const skuUnitId = String(value[0] || "");
|
||||
if (unitPickerSourceItemId.value && skuUnitId) {
|
||||
fulfillmentUnitMap[unitPickerSourceItemId.value] = skuUnitId;
|
||||
}
|
||||
unitPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function confirmTime(value: string | number): void {
|
||||
@@ -212,11 +296,17 @@ function validate(): boolean {
|
||||
}
|
||||
for (const item of selected) {
|
||||
const quantity = normalizePackageQuantity(quantityMap[item.id || ""]);
|
||||
if (!quantity) {
|
||||
const unit = getFulfillmentUnit(item);
|
||||
if (!quantity || !unit) {
|
||||
Message.warning("履约数量必须是正数,最多三位小数");
|
||||
return false;
|
||||
}
|
||||
if (0 < comparePackageQuantity(quantity, item.availableQuantity ?? "0")) {
|
||||
const baseQuantity = toPackageBaseQuantity(quantity, unit.baseQuantityPerUnit);
|
||||
if (!baseQuantity) {
|
||||
Message.warning("履约数量换算基础单位后最多只能保留三位小数");
|
||||
return false;
|
||||
}
|
||||
if (0 < comparePackageQuantity(baseQuantity, item.availableBaseQuantity ?? "0")) {
|
||||
Message.warning(`${item.productNameSnapshot || "产品"} 的可履约数量不足`);
|
||||
return false;
|
||||
}
|
||||
@@ -231,9 +321,10 @@ async function save(): Promise<void> {
|
||||
try {
|
||||
const itemList: GaoOrderFulfillmentItemRequest[] = sourceItems.value
|
||||
.filter(item => !!item.id && quantityMap[item.id])
|
||||
.map(item => ({
|
||||
sourceOrderItemId: item.id!,
|
||||
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
|
||||
.map(item => ({
|
||||
sourceOrderItemId: item.id!,
|
||||
skuUnitId: fulfillmentUnitMap[item.id!]!,
|
||||
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
|
||||
}));
|
||||
const result = await FulfillmentAPI.create({
|
||||
salesOrderId: order.value!.id!,
|
||||
@@ -333,6 +424,7 @@ onUnmounted(() => navBarStore.clearRight());
|
||||
.input {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
margin-top: .5rem;
|
||||
|
||||
.unit {
|
||||
font-size: .875rem;
|
||||
|
||||
Reference in New Issue
Block a user