700 lines
22 KiB
Vue
700 lines
22 KiB
Vue
<template>
|
|
<div v-can="permissionCodes.orderFulfillment.create" class="fulfillment-create">
|
|
<t-cell-group title="履约来源" theme="card">
|
|
<t-cell title="销售订单" :note="order?.orderNo || order?.id || '未找到来源订单'" />
|
|
<t-cell title="客户" :note="order?.customer?.name || order?.customerId || '未选择'" />
|
|
<t-cell title="来源类型" :note="isPackage ? '套餐权益' : '直接产品'" />
|
|
</t-cell-group>
|
|
<t-form v-if="sourceItems.length" class="items form">
|
|
<t-form-item v-for="item in sourceItems" :key="item.id" class="item" label-width="0">
|
|
<div class="content">
|
|
<div class="photo">
|
|
<t-image
|
|
v-if="getProductPhotoURL(item)"
|
|
:src="getProductPhotoURL(item)"
|
|
:alt="item.productNameSnapshot || '产品图片'"
|
|
fit="cover"
|
|
class="avatar"
|
|
/>
|
|
<div
|
|
v-else
|
|
class="avatar fallback"
|
|
v-text="(item.productNameSnapshot || '产').slice(0, 1)"
|
|
/>
|
|
</div>
|
|
<div class="main">
|
|
<div class="title">
|
|
<div class="heading">
|
|
<div class="name-gift">
|
|
<strong v-text="item.productNameSnapshot || '未命名产品'"/>
|
|
<t-tag
|
|
v-if="item.gifted"
|
|
size="small"
|
|
theme="primary"
|
|
content="赠送"
|
|
/>
|
|
</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"
|
|
>
|
|
<product-sku-tags
|
|
:product-name="item.productNameSnapshot"
|
|
:snapshot="item.skuSpecSnapshot"
|
|
:fallback="item.specSummarySnapshot || item.skuNameSnapshot"
|
|
/>
|
|
</div>
|
|
<t-input
|
|
v-model.trim="quantityMap[item.id || '']"
|
|
class="quantity"
|
|
type="number"
|
|
borderless
|
|
align="right"
|
|
:disabled="!getFulfillmentUnit(item)"
|
|
placeholder="请输入履约数量"
|
|
@change="refreshStockPreview"
|
|
>
|
|
<template #suffix-icon>
|
|
<t-button
|
|
size="extra-small"
|
|
variant="outline"
|
|
:content="getFulfillmentUnit(item)?.name || '选择实际单位'"
|
|
@click.stop="openUnitPicker(item)"
|
|
/>
|
|
</template>
|
|
</t-input>
|
|
<t-button
|
|
size="extra-small"
|
|
variant="outline"
|
|
:disabled="getAvailableBaseQuantity(item) === '0' || (directVerify && isStockChecking)"
|
|
@click.stop="fillAllQuantity(item)"
|
|
>全部履约</t-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</t-form-item>
|
|
</t-form>
|
|
<div v-else class="empty" v-text="'暂无可履约明细,请返回订单详情检查'" />
|
|
<t-form class="form" label-width="7rem">
|
|
<t-form-item label="履约时间" content-align="right">
|
|
<div class="time-actions">
|
|
<t-button
|
|
theme="primary"
|
|
variant="outline"
|
|
size="extra-small"
|
|
:content="deliveredAtText || '此刻'"
|
|
@click="openDeliveredAtPicker"
|
|
/>
|
|
<t-button
|
|
v-if="deliveredAtText"
|
|
theme="primary"
|
|
variant="text"
|
|
size="extra-small"
|
|
content="此刻"
|
|
@click="resetDeliveredAt"
|
|
/>
|
|
</div>
|
|
</t-form-item>
|
|
<t-form-item v-if="canDirectVerify" label="直接核销履约" content-align="right">
|
|
<t-switch v-model="directVerify" size="small" :disabled="isSaving || isStockChecking" />
|
|
</t-form-item>
|
|
<t-form-item v-if="directVerify || stockPreviewList.length" label-width="0">
|
|
<div class="stock-tips">
|
|
<span v-if="directVerify" 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" />
|
|
</t-form-item>
|
|
<t-form-item label-width="0">
|
|
<media-selector
|
|
v-if="order"
|
|
:model-value="photoUrlList"
|
|
v-model:photo-items="photoItems"
|
|
:upload="uploadPhotoList"
|
|
:edit-mode="true"
|
|
required
|
|
title="履约凭证照片"
|
|
/>
|
|
</t-form-item>
|
|
</t-form>
|
|
<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 { Time } from "timi-web";
|
|
import MediaSelector from "@/components/mediaSelector/MediaSelector.vue";
|
|
import ProductSkuTags from "@/components/ProductSkuTags.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 {
|
|
comparePackageQuantity,
|
|
convertPackageQuantity,
|
|
createPackageRequestNo,
|
|
getPackageQuantityText,
|
|
normalizePackageQuantity,
|
|
toPackageBaseQuantity
|
|
} from "@/utils/package";
|
|
import { getErrorMessage } from "@/utils/error";
|
|
import { emitListDataChanged, listDataEventKeys } from "@/utils/listDataEvents";
|
|
import { Message } from "@/utils/message";
|
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
|
import type { ProductStockPreviewItem } from "@/utils/productStock";
|
|
import {
|
|
buildProductStockPreview,
|
|
getProductStockShortageMessage,
|
|
mergeProductStockRequirements
|
|
} from "@/utils/productStock";
|
|
import { uploadPhotoList } from "@/utils/photo";
|
|
import { getProductPhotoURL } from "@/utils/product";
|
|
|
|
defineOptions({ name: "OrderFulfillmentCreate" });
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
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);
|
|
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 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");
|
|
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 ? getFulfillmentUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
|
|
});
|
|
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || userStore.hasRoles(gaoRoleCodes.storeManager));
|
|
const canDirectVerify = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager)
|
|
&& userStore.hasPermissions(permissionCodes.orderFulfillment.create)
|
|
&& 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) {
|
|
Message.warning("请从订单详情进入履约创建");
|
|
await router.replace("/management/order-fulfillments");
|
|
return;
|
|
}
|
|
try {
|
|
order.value = await SalesOrderAPI.detail(salesOrderId);
|
|
await loadFulfillmentSkuList();
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "履约来源加载失败"));
|
|
}
|
|
}
|
|
|
|
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 = getFulfillmentUnitList(item).find(unit => unit.id === item.skuUnitId);
|
|
const defaultUnit = getFulfillmentUnitForAvailableQuantity(item, originalUnit || getFulfillmentUnitList(item)[0]);
|
|
if (defaultUnit?.id) {
|
|
fulfillmentUnitMap[item.id] = defaultUnit.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getAvailableBaseQuantity(item: GaoSalesOrderItem): string {
|
|
return getPackageQuantityText(item.availableBaseQuantity);
|
|
}
|
|
|
|
function getAvailableQuantity(item: GaoSalesOrderItem): string {
|
|
const unit = getFulfillmentUnit(item);
|
|
const quantity = convertPackageQuantity(
|
|
getAvailableBaseQuantity(item),
|
|
"1",
|
|
unit?.baseQuantityPerUnit ?? "1"
|
|
);
|
|
return getPackageQuantityText(quantity ?? item.availableBaseQuantity);
|
|
}
|
|
|
|
function getItemDescription(item: GaoSalesOrderItem): string {
|
|
return `可履约 ${getAvailableQuantity(item)} ${getFulfillmentUnit(item)?.name || getBaseUnitName(item)}`;
|
|
}
|
|
|
|
function getBaseUnitName(item: GaoSalesOrderItem): string {
|
|
return getBaseUnit(item)?.name || "基础单位";
|
|
}
|
|
|
|
function getBaseUnit(item: GaoSalesOrderItem): ProductSkuUnit | undefined {
|
|
return getFulfillmentUnitList(item).find(unit => unit.isBase);
|
|
}
|
|
|
|
function shouldDowngradeUnit(item: GaoSalesOrderItem, unit?: ProductSkuUnit): boolean {
|
|
const sku = item.skuId ? fulfillmentSkuMap[item.skuId] : undefined;
|
|
if (!unit || unit.isBase || sku?.allowDecimalQuantity !== false) {
|
|
return false;
|
|
}
|
|
return Number(getAvailableBaseQuantity(item)) < Number(unit.baseQuantityPerUnit);
|
|
}
|
|
|
|
function getFulfillmentUnitForAvailableQuantity(item: GaoSalesOrderItem, unit?: ProductSkuUnit): ProductSkuUnit | undefined {
|
|
if (!shouldDowngradeUnit(item, unit)) {
|
|
return unit;
|
|
}
|
|
return getBaseUnit(item) || unit;
|
|
}
|
|
|
|
async function fillAllQuantity(item: GaoSalesOrderItem): Promise<void> {
|
|
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;
|
|
await refreshStockPreview();
|
|
}
|
|
|
|
function getFulfillmentUnitList(item: GaoSalesOrderItem): ProductSkuUnit[] {
|
|
const unitList = (item.skuId ? fulfillmentSkuMap[item.skuId]?.unitList : [])?.filter(unit => !!unit.id) || [];
|
|
if (!item.skuUnitId) {
|
|
return unitList;
|
|
}
|
|
const originalUnit: ProductSkuUnit = {
|
|
...(unitList.find(unit => unit.id === item.skuUnitId) || {}),
|
|
id: item.skuUnitId,
|
|
skuId: item.skuId,
|
|
name: item.unitSnapshot || "成交单位",
|
|
baseQuantityPerUnit: item.baseQuantityPerUnitSnapshot ?? "1",
|
|
allowPurchase: false,
|
|
allowSale: false
|
|
};
|
|
return [originalUnit, ...unitList.filter(unit => unit.id !== item.skuUnitId)];
|
|
}
|
|
|
|
function getFulfillmentUnit(item: GaoSalesOrderItem): ProductSkuUnit | undefined {
|
|
return getFulfillmentUnitList(item).find(unit => unit.id === fulfillmentUnitMap[item.id || ""]);
|
|
}
|
|
|
|
function setFulfillmentUnit(item: GaoSalesOrderItem, unit: ProductSkuUnit): void {
|
|
if (!item.id) {
|
|
return;
|
|
}
|
|
const currentUnit = getFulfillmentUnit(item);
|
|
const currentQuantity = quantityMap[item.id];
|
|
if (currentQuantity && currentUnit?.id !== unit.id) {
|
|
quantityMap[item.id] = convertPackageQuantity(
|
|
currentQuantity,
|
|
currentUnit?.baseQuantityPerUnit ?? "1",
|
|
unit.baseQuantityPerUnit
|
|
) || "";
|
|
}
|
|
fulfillmentUnitMap[item.id] = unit.id!;
|
|
}
|
|
|
|
function openUnitPicker(item: GaoSalesOrderItem): void {
|
|
if (!item.id) {
|
|
return;
|
|
}
|
|
if (!getFulfillmentUnitList(item).length) {
|
|
Message.warning("当前 SKU 没有可用于履约的单位");
|
|
return;
|
|
}
|
|
unitPickerSourceItemId.value = item.id;
|
|
unitPickerDraft.value = [fulfillmentUnitMap[item.id] || getFulfillmentUnitList(item)[0]?.id || ""];
|
|
unitPickerVisible.value = true;
|
|
}
|
|
|
|
async function confirmUnitPicker(value: Array<string | number>): Promise<void> {
|
|
const skuUnitId = String(value[0] || "");
|
|
const item = sourceItems.value.find(sourceItem => sourceItem.id === unitPickerSourceItemId.value);
|
|
const selectedUnit = item ? getFulfillmentUnitList(item).find(unit => unit.id === skuUnitId) : undefined;
|
|
const fulfillmentUnit = item ? getFulfillmentUnitForAvailableQuantity(item, selectedUnit) : selectedUnit;
|
|
if (item?.id && fulfillmentUnit?.id) {
|
|
setFulfillmentUnit(item, fulfillmentUnit);
|
|
}
|
|
unitPickerVisible.value = false;
|
|
await refreshStockPreview();
|
|
}
|
|
|
|
function confirmTime(value: string | number): void {
|
|
deliveredAtText.value = String(value);
|
|
pickerVisible.value = false;
|
|
}
|
|
|
|
function openDeliveredAtPicker(): void {
|
|
draftTime.value = deliveredAtText.value || Time.formatDateTime(new Date());
|
|
pickerVisible.value = true;
|
|
}
|
|
|
|
function resetDeliveredAt(): void {
|
|
deliveredAtText.value = "";
|
|
draftTime.value = "";
|
|
}
|
|
|
|
function resolveTimestamp(value: string): number | undefined {
|
|
if (!value) return undefined;
|
|
const timestamp = new Date(value.replace(" ", "T")).getTime();
|
|
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
}
|
|
|
|
function buildAttachments(): Partial<Attachment>[] {
|
|
return photoItems.value.flatMap(item => item.tempFileId ? [{
|
|
tempFileId: item.tempFileId,
|
|
attachType: "PHOTO"
|
|
}] : []);
|
|
}
|
|
|
|
async function validateDirectStock(showShortageMessage = true): 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) {
|
|
if (!item.skuId) {
|
|
continue;
|
|
}
|
|
let sku = fulfillmentSkuMap[item.skuId];
|
|
if (!sku) {
|
|
try {
|
|
sku = await ProductAPI.skuDetail(item.skuId);
|
|
} catch {
|
|
// 统一在下方提示,避免部分 SKU 查询失败时静默跳过库存校验
|
|
}
|
|
}
|
|
if (!sku) {
|
|
Message.warning(`${item.productNameSnapshot || "产品"} 的库存信息加载失败,请刷新后重试`);
|
|
return false;
|
|
}
|
|
fulfillmentSkuMap[item.skuId] = sku;
|
|
skuMap.set(item.skuId, sku);
|
|
}
|
|
const requirementList = mergeProductStockRequirements(selected.flatMap(item => {
|
|
const sku = item.skuId ? skuMap.get(item.skuId) : undefined;
|
|
const unit = getFulfillmentUnit(item);
|
|
const quantity = normalizePackageQuantity(quantityMap[item.id || ""] || "");
|
|
const baseQuantity = quantity && unit ? toPackageBaseQuantity(quantity, unit.baseQuantityPerUnit) : undefined;
|
|
if (!item.skuId || !sku?.stockEnabled || !baseQuantity) {
|
|
return [];
|
|
}
|
|
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) {
|
|
if (showShortageMessage) {
|
|
Message.warning(`${shortageMessage},不能直接核销履约`);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (error) {
|
|
stockPreviewList.value = [];
|
|
Message.error(getErrorMessage(error, "库存查询失败,暂不能直接核销履约"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function previewDirectStock(force = false): Promise<void> {
|
|
if ((!directVerify.value && !force) || isStockChecking.value) {
|
|
return;
|
|
}
|
|
stockCheckToken += 1;
|
|
const token = stockCheckToken;
|
|
const showShortageMessage = directVerify.value;
|
|
isStockChecking.value = true;
|
|
try {
|
|
if (!await validateDirectStock(showShortageMessage) && token === stockCheckToken && directVerify.value) {
|
|
directVerify.value = false;
|
|
}
|
|
} finally {
|
|
if (token === stockCheckToken) {
|
|
isStockChecking.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function refreshStockPreview(): Promise<void> {
|
|
if (!directVerify.value && !stockPreviewList.value.length) {
|
|
return;
|
|
}
|
|
await previewDirectStock(!directVerify.value);
|
|
}
|
|
|
|
function validate(): boolean {
|
|
if (!order.value?.id) {
|
|
Message.warning("请从订单详情进入履约创建");
|
|
return false;
|
|
}
|
|
if (order.value.status !== "ACTIVE") {
|
|
Message.warning("当前订单不是进行中状态,不能创建履约");
|
|
return false;
|
|
}
|
|
if (!canFulfillByPayment.value) {
|
|
Message.warning("订单尚未满足履约收款条件");
|
|
return false;
|
|
}
|
|
if (!buildAttachments().length) {
|
|
Message.warning("履约必须上传至少一张照片");
|
|
return false;
|
|
}
|
|
const selected = sourceItems.value.filter(item => !!item.id && quantityMap[item.id]);
|
|
if (!selected.length) {
|
|
Message.warning("请填写至少一项履约数量");
|
|
return false;
|
|
}
|
|
for (const item of selected) {
|
|
const quantity = normalizePackageQuantity(quantityMap[item.id || ""]);
|
|
const unit = getFulfillmentUnit(item);
|
|
if (!quantity || !unit) {
|
|
Message.warning("履约数量必须是正数,最多三位小数");
|
|
return false;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function save(): Promise<void> {
|
|
if (isSaving.value) return;
|
|
if (isStockChecking.value) {
|
|
Message.warning("库存正在预检,请稍候再提交");
|
|
return;
|
|
}
|
|
if (!validate()) return;
|
|
if (!requestNo.value) requestNo.value = createPackageRequestNo();
|
|
isSaving.value = true;
|
|
try {
|
|
const itemList: GaoOrderFulfillmentItemRequest[] = sourceItems.value
|
|
.filter(item => !!item.id && quantityMap[item.id])
|
|
.map(item => ({
|
|
sourceOrderItemId: item.id!,
|
|
skuUnitId: fulfillmentUnitMap[item.id!]!,
|
|
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
|
|
}));
|
|
const request = {
|
|
salesOrderId: order.value!.id!,
|
|
requestNo: requestNo.value,
|
|
deliveredAt: resolveTimestamp(deliveredAtText.value),
|
|
itemList,
|
|
attachmentList: buildAttachments(),
|
|
remark: remark.value.trim() || null
|
|
};
|
|
if (directVerify.value && !await validateDirectStock(true)) {
|
|
directVerify.value = false;
|
|
return;
|
|
}
|
|
const result = directVerify.value
|
|
? await FulfillmentAPI.createAndVerify(request)
|
|
: await FulfillmentAPI.create(request);
|
|
emitListDataChanged(listDataEventKeys.salesOrderChanged, request.salesOrderId);
|
|
emitListDataChanged(listDataEventKeys.productStockChanged);
|
|
Message.success(directVerify.value ? "履约单创建并核销成功" : "履约单已提交");
|
|
await router.replace(`/management/order-fulfillments/${result.id}`);
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "履约单创建失败,重试会复用同一请求号"));
|
|
} finally {
|
|
isSaving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
navBarStore.setRightText("提交", "var(--td-brand-color)", save);
|
|
await loadData();
|
|
});
|
|
onUnmounted(() => navBarStore.clearRight());
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.fulfillment-create {
|
|
gap: 1rem;
|
|
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.items {
|
|
margin: 0 1rem;
|
|
background: var(--td-bg-color-container);
|
|
box-shadow: var(--td-shadow-1);
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
|
|
.item {
|
|
padding: 10px;
|
|
min-width: 0;
|
|
|
|
:deep(.t-form__controls),
|
|
:deep(.t-form__controls-content) {
|
|
min-width: 0;
|
|
}
|
|
|
|
.content {
|
|
gap: .5rem;
|
|
width: 100%;
|
|
min-width: 0;
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
|
|
.photo {
|
|
gap: .5rem;
|
|
display: flex;
|
|
|
|
.avatar {
|
|
width: 3.5rem;
|
|
height: 3.5rem;
|
|
}
|
|
}
|
|
|
|
.main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
|
|
.title {
|
|
gap: .375rem;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.heading {
|
|
gap: .5rem;
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
|
|
.name-gift {
|
|
gap: .5rem;
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
display: flex;
|
|
overflow: hidden;
|
|
align-items: center;
|
|
justify-content: start;
|
|
|
|
strong {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
}
|
|
|
|
.can-fulfill {
|
|
color: var(--td-text-color-secondary);
|
|
font-size: .75rem;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
}
|
|
|
|
.input {
|
|
gap: .5rem;
|
|
display: flex;
|
|
margin-top: .5rem;
|
|
|
|
.sku-tags {
|
|
gap: .375rem;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.unit {
|
|
font-size: .875rem;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.time-actions {
|
|
gap: .5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.tips {
|
|
font-size: .875rem;
|
|
}
|
|
|
|
}
|
|
</style>
|