auto complete order

This commit is contained in:
Timi
2026-09-13 01:25:37 +08:00
parent 8d9d0c4591
commit 6cce293683
9 changed files with 317 additions and 28 deletions
+163 -18
View File
@@ -65,7 +65,7 @@
<div class="description" v-text="config.description" />
</div>
</t-form-item>
<t-form-item label="成交价(元)" label-width="6rem" required-mark>
<t-form-item label="成交价" label-width="6rem" required-mark>
<t-input
v-model.trim="saleAmountText"
type="number"
@@ -170,6 +170,41 @@
/>
</div>
</t-form-item>
<t-form-item label="已全额收款和核销" content-align="right">
<t-switch
v-model="receiveFullPayment"
size="small"
:disabled="!canReceiveImmediately"
/>
</t-form-item>
<t-form-item label="已全部产品履约和核销" content-align="right">
<t-switch
v-model="fulfillAll"
size="small"
:disabled="!canFulfillImmediately"
/>
</t-form-item>
<t-form-item label-width="0">
<div class="red">全部履约会同步启用全额收款;请先准备全部产品库存,任意一步失败,整张订单都会回滚。</div>
</t-form-item>
<t-form-item v-if="receiveFullPayment && parsedSaleAmount" label-width="0">
<photo-scroller
:model-value="paymentPhotoUrlList"
v-model:photo-items="paymentPhotoItems"
:upload="uploadPhotoList"
:edit-mode="true"
title="收款凭证照片(必要)"
/>
</t-form-item>
<t-form-item v-if="fulfillAll" label-width="0">
<photo-scroller
:model-value="fulfillmentPhotoUrlList"
v-model:photo-items="fulfillmentPhotoItems"
:upload="uploadPhotoList"
:edit-mode="true"
title="履约凭证照片(必要)"
/>
</t-form-item>
<t-form-item label="购买备注">
<t-textarea
v-model.trim="note"
@@ -195,26 +230,41 @@
:enable-scan="false"
@change="handleCustomerChange"
/>
<t-popup v-model="configSelectorVisible" placement="bottom">
<div class="config-selector bg-white">
<div class="popup-header">
<t-button variant="text" size="small" @click="configSelectorVisible = false">关闭</t-button>
<h3>选择套餐</h3>
<span />
</div>
<bottom-popup
v-model="configSelectorVisible"
title="选择套餐"
height="70vh"
right-button-text=""
@left-button-click="configSelectorVisible = false"
>
<div class="config-selector">
<t-cell-group v-if="configList.length">
<t-cell
v-for="item in configList"
:key="item.id"
class="config-option"
:title="item.name || '未命名套餐'"
:note="`标准价:${formatPackageAmount(item.standardAmount)}`"
:note="formatPackageAmount(item.standardAmount)"
arrow
@click="selectConfig(item)"
/>
>
<template #description>
<div class="products">
<t-tag
v-for="productItem in item.itemList || []"
:key="productItem.id || `${productItem.skuId}-${productItem.skuUnitId}`"
:content="getConfigProductText(productItem)"
theme="primary"
variant="light"
/>
<t-tag v-if="!item.itemList?.length" content="暂无产品权益" />
</div>
</template>
</t-cell>
</t-cell-group>
<div v-else class="center-tips" v-text="'暂无可购买套餐'" />
</div>
</t-popup>
</bottom-popup>
<t-popup v-model="unitPickerVisible" placement="bottom">
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="调整权益单位" @cancel="unitPickerVisible = false" @confirm="confirmItemUnitPicker" />
</t-popup>
@@ -252,6 +302,8 @@
<script setup lang="ts">
import { Time } from "timi-web";
import { useUserStore } from "timi-web-api";
import { BottomPopup } from "timi-tdesign-mobile";
import { DialogPlugin } from "tdesign-mobile-vue";
import CustomerAPI from "@/api/CustomerAPI";
import PackageAPI from "@/api/PackageAPI";
import ProductAPI from "@/api/ProductAPI";
@@ -265,14 +317,16 @@ import { useNavBarStore } from "@/store/navBarStore";
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 { GaoPackageConfig, PackageAttachment, PackageConfigItem, PackagePurchaseItem } from "@/types/Package";
import type { Product, ProductSku, ProductSkuUnit } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import {
addPackageQuantity,
convertPackageQuantity,
formatPackageAmount,
normalizePackageQuantity
getPackageQuantityText,
normalizePackageQuantity,
parsePackageAmount
} from "@/utils/package";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -423,15 +477,22 @@ function parseDateTime(value: string): Date {
const note = ref("");
const isSaving = ref(false);
const photoItems = ref<PhotoItem[]>([]);
const paymentPhotoItems = ref<PhotoItem[]>([]);
const fulfillmentPhotoItems = ref<PhotoItem[]>([]);
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const paymentPhotoUrlList = computed(() => paymentPhotoItems.value.map(item => item.url));
const fulfillmentPhotoUrlList = computed(() => fulfillmentPhotoItems.value.map(item => item.url));
function buildAttachments(): PackageAttachment[] {
return photoItems.value.flatMap(item => item.tempFileId ? [{
function buildAttachments(source: PhotoItem[] = photoItems.value): PackageAttachment[] {
return source.flatMap(item => item.tempFileId ? [{
tempFileId: item.tempFileId,
attachType: item.attachType || "PHOTO"
}] : []);
}
const receiveFullPayment = ref(false);
const fulfillAll = ref(false);
const {
isSaleable,
canPriceOverride,
@@ -446,9 +507,32 @@ const {
config,
selectedCustomerList,
note,
receiveFullPayment,
fulfillAll,
hasPermission: permissionCode => userStore.hasPermissions(permissionCode),
buildRegisteredAt: buildPurchasedAt,
buildAttachments
buildAttachments,
buildPaymentAttachments: () => buildAttachments(paymentPhotoItems.value),
buildFulfillmentAttachments: () => buildAttachments(fulfillmentPhotoItems.value)
});
const parsedSaleAmount = computed(() => parsePackageAmount(saleAmountText.value));
const canReceiveImmediately = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create)
&& userStore.hasPermissions(permissionCodes.fundReceive.verify));
const canFulfillImmediately = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create)
&& userStore.hasPermissions(permissionCodes.orderFulfillment.verify)
&& (parsedSaleAmount.value === 0 || canReceiveImmediately.value));
watch(fulfillAll, value => {
if (value && parsedSaleAmount.value !== 0) {
receiveFullPayment.value = true;
}
});
watch(receiveFullPayment, value => {
if (!value && parsedSaleAmount.value !== 0) {
fulfillAll.value = false;
}
});
const unitPickerOptions = computed(() => {
@@ -456,6 +540,12 @@ const unitPickerOptions = computed(() => {
return item ? getSelectableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
});
function getConfigProductText(item: PackageConfigItem): string {
const quantity = getPackageQuantityText(item.defaultQuantity);
const skuText = getProductSkuSnapshotText(item.productNameSnapshot, item.skuNameSnapshot, item.specSummarySnapshot);
return `${item.productNameSnapshot || "未命名产品"}${skuText ? ` · ${skuText}` : ""} × ${quantity}${item.unitSnapshot ? ` ${item.unitSnapshot}` : ""}`;
}
function getItemLabel(item: PackagePurchaseItem): string {
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId);
const product = item.gifted ? giftProductMap[item.skuId] : undefined;
@@ -647,6 +737,28 @@ async function submitRecord(): Promise<void> {
if (!validatePurchaseForm() || !validateItemDecimalQuantity()) {
return;
}
if (receiveFullPayment.value || fulfillAll.value) {
const dialog = DialogPlugin.confirm({
title: "确认直通完成交易",
content: fulfillAll.value
? "将一次性购买套餐、核销全额收款,并完成全部履约和库存扣减。提交后如需更正,只能走撤销或冲正流程。"
: "将一次性购买套餐并核销全额收款。提交后如需更正,只能走资金冲正流程。",
cancelBtn: "取消",
confirmBtn: "确认并提交",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await purchasePackage();
}
});
return;
}
await purchasePackage();
}
async function purchasePackage(): Promise<void> {
isSaving.value = true;
try {
@@ -656,7 +768,7 @@ async function submitRecord(): Promise<void> {
return;
}
await settingStore.playRegisterSuccessSound();
Message.success("套餐购买已提交");
Message.success(fulfillAll.value ? "套餐、收款及履约已完成" : receiveFullPayment.value ? "套餐及全额收款已完成" : "套餐购买已提交");
await router.replace({
name: "SalesOrderDetail",
params: { id: result.salesOrderId },
@@ -749,6 +861,25 @@ watch(config, value => {
width: 100%;
}
.straight-through {
width: 100%;
padding: .75rem;
background: var(--td-bg-color-secondarycontainer);
border-radius: .875rem;
.title {
font-size: .9375rem;
font-weight: 600;
}
.tips {
color: var(--td-text-color-secondary);
margin: .5rem .5rem 0;
font-size: .75rem;
line-height: 1.5;
}
}
.customer-profile {
margin: 1rem;
}
@@ -881,7 +1012,21 @@ watch(config, value => {
border-radius: inherit;
}
.config-selector,
.config-selector {
height: 100%;
overflow: auto;
.config-option {
.products {
gap: .35rem;
width: 100%;
display: flex;
flex-wrap: wrap;
margin-top: .5rem;
}
}
}
.selected-customer-popup {
height: 70vh;
overflow: auto;