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
+1 -2
View File
@@ -292,7 +292,6 @@ function selectUnit(sku: ProductSku, skuUnitId: string): void {
} }
.sku-card { .sku-card {
margin-top: 1.25rem;
padding: .75rem; padding: .75rem;
background: var(--td-bg-color-secondarycontainer); background: var(--td-bg-color-secondarycontainer);
border-radius: .875rem; border-radius: .875rem;
@@ -349,7 +348,7 @@ function selectUnit(sku: ProductSku, skuUnitId: string): void {
padding-bottom: env(safe-area-inset-bottom, 0); padding-bottom: env(safe-area-inset-bottom, 0);
.collapse { .collapse {
margin-bottom: 1rem; margin: 0;
} }
.product-header { .product-header {
+1 -1
View File
@@ -11,7 +11,7 @@
</t-cell> </t-cell>
<t-cell title="识别成功自动登记"> <t-cell title="识别成功自动登记">
<template #left-icon> <template #left-icon>
<t-icon name="arrow-right-up" /> <t-icon name="task-checked" />
</template> </template>
<template #right-icon> <template #right-icon>
<t-switch v-model="autoRegisterEnabled" /> <t-switch v-model="autoRegisterEnabled" />
+162 -17
View File
@@ -65,7 +65,7 @@
<div class="description" v-text="config.description" /> <div class="description" v-text="config.description" />
</div> </div>
</t-form-item> </t-form-item>
<t-form-item label="成交价(元)" label-width="6rem" required-mark> <t-form-item label="成交价" label-width="6rem" required-mark>
<t-input <t-input
v-model.trim="saleAmountText" v-model.trim="saleAmountText"
type="number" type="number"
@@ -170,6 +170,41 @@
/> />
</div> </div>
</t-form-item> </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-form-item label="购买备注">
<t-textarea <t-textarea
v-model.trim="note" v-model.trim="note"
@@ -195,26 +230,41 @@
:enable-scan="false" :enable-scan="false"
@change="handleCustomerChange" @change="handleCustomerChange"
/> />
<t-popup v-model="configSelectorVisible" placement="bottom"> <bottom-popup
<div class="config-selector bg-white"> v-model="configSelectorVisible"
<div class="popup-header"> title="选择套餐"
<t-button variant="text" size="small" @click="configSelectorVisible = false">关闭</t-button> height="70vh"
<h3>选择套餐</h3> right-button-text=""
<span /> @left-button-click="configSelectorVisible = false"
</div> >
<div class="config-selector">
<t-cell-group v-if="configList.length"> <t-cell-group v-if="configList.length">
<t-cell <t-cell
v-for="item in configList" v-for="item in configList"
:key="item.id" :key="item.id"
class="config-option"
:title="item.name || '未命名套餐'" :title="item.name || '未命名套餐'"
:note="`标准价:${formatPackageAmount(item.standardAmount)}`" :note="formatPackageAmount(item.standardAmount)"
arrow arrow
@click="selectConfig(item)" @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> </t-cell-group>
<div v-else class="center-tips" v-text="'暂无可购买套餐'" /> <div v-else class="center-tips" v-text="'暂无可购买套餐'" />
</div> </div>
</t-popup> </bottom-popup>
<t-popup v-model="unitPickerVisible" placement="bottom"> <t-popup v-model="unitPickerVisible" placement="bottom">
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="调整权益单位" @cancel="unitPickerVisible = false" @confirm="confirmItemUnitPicker" /> <t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="调整权益单位" @cancel="unitPickerVisible = false" @confirm="confirmItemUnitPicker" />
</t-popup> </t-popup>
@@ -252,6 +302,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web"; import { Time } from "timi-web";
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import { BottomPopup } from "timi-tdesign-mobile";
import { DialogPlugin } from "tdesign-mobile-vue";
import CustomerAPI from "@/api/CustomerAPI"; import CustomerAPI from "@/api/CustomerAPI";
import PackageAPI from "@/api/PackageAPI"; import PackageAPI from "@/api/PackageAPI";
import ProductAPI from "@/api/ProductAPI"; import ProductAPI from "@/api/ProductAPI";
@@ -265,14 +317,16 @@ import { useNavBarStore } from "@/store/navBarStore";
import { useSettingStore } from "@/store/settingStore"; import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer"; import type { Customer } from "@/types/Customer";
import type { PhotoItem } from "@/types/Photo"; 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 type { Product, ProductSku, ProductSkuUnit } from "@/types/Product";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { import {
addPackageQuantity, addPackageQuantity,
convertPackageQuantity, convertPackageQuantity,
formatPackageAmount, formatPackageAmount,
normalizePackageQuantity getPackageQuantityText,
normalizePackageQuantity,
parsePackageAmount
} from "@/utils/package"; } from "@/utils/package";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -423,15 +477,22 @@ function parseDateTime(value: string): Date {
const note = ref(""); const note = ref("");
const isSaving = ref(false); const isSaving = ref(false);
const photoItems = ref<PhotoItem[]>([]); const photoItems = ref<PhotoItem[]>([]);
const paymentPhotoItems = ref<PhotoItem[]>([]);
const fulfillmentPhotoItems = ref<PhotoItem[]>([]);
const photoUrlList = computed(() => photoItems.value.map(item => item.url)); 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[] { function buildAttachments(source: PhotoItem[] = photoItems.value): PackageAttachment[] {
return photoItems.value.flatMap(item => item.tempFileId ? [{ return source.flatMap(item => item.tempFileId ? [{
tempFileId: item.tempFileId, tempFileId: item.tempFileId,
attachType: item.attachType || "PHOTO" attachType: item.attachType || "PHOTO"
}] : []); }] : []);
} }
const receiveFullPayment = ref(false);
const fulfillAll = ref(false);
const { const {
isSaleable, isSaleable,
canPriceOverride, canPriceOverride,
@@ -446,9 +507,32 @@ const {
config, config,
selectedCustomerList, selectedCustomerList,
note, note,
receiveFullPayment,
fulfillAll,
hasPermission: permissionCode => userStore.hasPermissions(permissionCode), hasPermission: permissionCode => userStore.hasPermissions(permissionCode),
buildRegisteredAt: buildPurchasedAt, 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(() => { const unitPickerOptions = computed(() => {
@@ -456,6 +540,12 @@ const unitPickerOptions = computed(() => {
return item ? getSelectableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : []; 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 { function getItemLabel(item: PackagePurchaseItem): string {
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId); const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId);
const product = item.gifted ? giftProductMap[item.skuId] : undefined; const product = item.gifted ? giftProductMap[item.skuId] : undefined;
@@ -647,6 +737,28 @@ async function submitRecord(): Promise<void> {
if (!validatePurchaseForm() || !validateItemDecimalQuantity()) { if (!validatePurchaseForm() || !validateItemDecimalQuantity()) {
return; 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; isSaving.value = true;
try { try {
@@ -656,7 +768,7 @@ async function submitRecord(): Promise<void> {
return; return;
} }
await settingStore.playRegisterSuccessSound(); await settingStore.playRegisterSuccessSound();
Message.success("套餐购买已提交"); Message.success(fulfillAll.value ? "套餐、收款及履约已完成" : receiveFullPayment.value ? "套餐及全额收款已完成" : "套餐购买已提交");
await router.replace({ await router.replace({
name: "SalesOrderDetail", name: "SalesOrderDetail",
params: { id: result.salesOrderId }, params: { id: result.salesOrderId },
@@ -749,6 +861,25 @@ watch(config, value => {
width: 100%; 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 { .customer-profile {
margin: 1rem; margin: 1rem;
} }
@@ -881,7 +1012,21 @@ watch(config, value => {
border-radius: inherit; 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 { .selected-customer-popup {
height: 70vh; height: 70vh;
overflow: auto; overflow: auto;
+37 -1
View File
@@ -10,9 +10,13 @@ interface UsePackagePurchaseOptions {
config: Ref<GaoPackageConfig | undefined>; config: Ref<GaoPackageConfig | undefined>;
selectedCustomerList: Ref<Customer[]>; selectedCustomerList: Ref<Customer[]>;
note: Ref<string>; note: Ref<string>;
receiveFullPayment: Ref<boolean>;
fulfillAll: Ref<boolean>;
hasPermission: (permissionCode: string) => boolean; hasPermission: (permissionCode: string) => boolean;
buildRegisteredAt: () => number; buildRegisteredAt: () => number;
buildAttachments: () => PackageAttachment[]; buildAttachments: () => PackageAttachment[];
buildPaymentAttachments: () => PackageAttachment[];
buildFulfillmentAttachments: () => PackageAttachment[];
} }
/** 套餐配置直接购买流程,购买请求只依赖套餐配置 ID,不再关联购买事件。 */ /** 套餐配置直接购买流程,购买请求只依赖套餐配置 ID,不再关联购买事件。 */
@@ -91,12 +95,19 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
saleAmount, saleAmount,
purchasedAt: options.buildRegisteredAt(), purchasedAt: options.buildRegisteredAt(),
attachmentList: options.buildAttachments(), attachmentList: options.buildAttachments(),
straightThrough: options.receiveFullPayment.value || options.fulfillAll.value ? {
receiveFullPayment: options.receiveFullPayment.value,
paymentAttachmentList: options.buildPaymentAttachments(),
fulfillAll: options.fulfillAll.value,
fulfillmentAttachmentList: options.buildFulfillmentAttachments()
} : undefined,
itemList, itemList,
remark: options.note.value.trim() || null remark: options.note.value.trim() || null
}); });
} }
function validatePurchaseForm(): boolean { function validatePurchaseForm(): boolean {
const saleAmount = parsePackageAmount(saleAmountText.value);
if (!options.hasPermission(permissionCodes.customerPackage.create)) { if (!options.hasPermission(permissionCodes.customerPackage.create)) {
Message.warning("当前账号没有套餐购买权限"); Message.warning("当前账号没有套餐购买权限");
return false; return false;
@@ -113,7 +124,7 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
Message.warning("套餐当前不可购买,请检查套餐状态和有效期"); Message.warning("套餐当前不可购买,请检查套餐状态和有效期");
return false; return false;
} }
if (parsePackageAmount(saleAmountText.value) === undefined) { if (saleAmount === undefined) {
Message.warning("成交价必须是合法的元金额,最多两位小数"); Message.warning("成交价必须是合法的元金额,最多两位小数");
return false; return false;
} }
@@ -139,6 +150,31 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
Message.warning("套餐至少需要保留一项正数量权益"); Message.warning("套餐至少需要保留一项正数量权益");
return false; return false;
} }
const canReceiveImmediately = options.hasPermission(permissionCodes.fundReceive.create)
&& options.hasPermission(permissionCodes.fundReceive.verify);
const canFulfillImmediately = options.hasPermission(permissionCodes.orderFulfillment.create)
&& options.hasPermission(permissionCodes.orderFulfillment.verify)
&& (saleAmount === 0 || canReceiveImmediately);
if (options.receiveFullPayment.value && !canReceiveImmediately) {
Message.warning("当前账号没有收款登记和核销权限");
return false;
}
if (options.fulfillAll.value && !canFulfillImmediately) {
Message.warning("当前账号没有交易直通所需权限");
return false;
}
if (options.fulfillAll.value && saleAmount !== 0 && !options.receiveFullPayment.value) {
Message.warning("全部履约直通必须同时直接全额收款");
return false;
}
if (options.receiveFullPayment.value && saleAmount !== 0 && !options.buildPaymentAttachments().length) {
Message.warning("直接全额收款必须上传收款凭证照片");
return false;
}
if (options.fulfillAll.value && !options.buildFulfillmentAttachments().length) {
Message.warning("全部履约直通必须上传履约凭证照片");
return false;
}
return true; return true;
} }
+97 -3
View File
@@ -136,6 +136,41 @@
/> />
</div> </div>
</t-form-item> </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 && totalAmount" 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-form-item label="订单备注">
<t-textarea v-model="remark" class="textarea" borderless autosize maxlength="500" placeholder="记录本单需要注意的内容" /> <t-textarea v-model="remark" class="textarea" borderless autosize maxlength="500" placeholder="记录本单需要注意的内容" />
</t-form-item> </t-form-item>
@@ -169,6 +204,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { type Attachment, useUserStore } from "timi-web-api"; import { type Attachment, useUserStore } from "timi-web-api";
import { Time } from "timi-web"; import { Time } from "timi-web";
import { DialogPlugin } from "tdesign-mobile-vue";
import CustomerAPI from "@/api/CustomerAPI"; import CustomerAPI from "@/api/CustomerAPI";
import ProductAPI from "@/api/ProductAPI"; import ProductAPI from "@/api/ProductAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI"; import SalesOrderAPI from "@/api/SalesOrderAPI";
@@ -206,7 +242,11 @@ const draftTime = ref("");
const pickerVisible = ref(false); const pickerVisible = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
const photoItems = ref<PhotoItem[]>([]); const photoItems = ref<PhotoItem[]>([]);
const paymentPhotoItems = ref<PhotoItem[]>([]);
const fulfillmentPhotoItems = ref<PhotoItem[]>([]);
const photoUrlList = computed(() => photoItems.value.map(item => item.url)); 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));
const maxQuantity = Number.MAX_SAFE_INTEGER; const maxQuantity = Number.MAX_SAFE_INTEGER;
const productCache = new Map<string, Product>(); const productCache = new Map<string, Product>();
const skuCache = new Map<string, ProductSku>(); const skuCache = new Map<string, ProductSku>();
@@ -218,6 +258,28 @@ const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别。"); const scanStatus = ref("点击扫码开始识别。");
let latestDecodedText = ""; let latestDecodedText = "";
// ---------- 交易直通 ----------
const receiveFullPayment = ref(false);
const fulfillAll = ref(false);
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)
&& (totalAmount.value === 0 || canReceiveImmediately.value));
watch(fulfillAll, value => {
if (value && totalAmount.value !== 0) {
receiveFullPayment.value = true;
}
});
watch(receiveFullPayment, value => {
if (!value && totalAmount.value !== 0) {
fulfillAll.value = false;
}
});
function getSku(item: FormItem): ProductSku | undefined { function getSku(item: FormItem): ProductSku | undefined {
return skuCache.get(item.skuId || ""); return skuCache.get(item.skuId || "");
} }
@@ -376,8 +438,8 @@ function resolveTimestamp(value: string): number | undefined {
const timestamp = new Date(value.replace(" ", "T")).getTime(); const timestamp = new Date(value.replace(" ", "T")).getTime();
return Number.isFinite(timestamp) ? timestamp : undefined; return Number.isFinite(timestamp) ? timestamp : undefined;
} }
function buildAttachments(): Partial<Attachment>[] { function buildAttachments(source: PhotoItem[] = photoItems.value): Partial<Attachment>[] {
return photoItems.value.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO" }] : []); return source.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO" }] : []);
} }
function validate(): boolean { function validate(): boolean {
if (!customer.value?.id) { Message.warning("请选择客户"); return false; } if (!customer.value?.id) { Message.warning("请选择客户"); return false; }
@@ -392,10 +454,36 @@ function validate(): boolean {
keys.add(key); keys.add(key);
if (canPriceOverride.value && item.unitPriceText && parsePackageAmount(item.unitPriceText) === undefined) { Message.warning("成交单价必须是合法金额"); return false; } if (canPriceOverride.value && item.unitPriceText && parsePackageAmount(item.unitPriceText) === undefined) { Message.warning("成交单价必须是合法金额"); return false; }
} }
if (receiveFullPayment.value && !canReceiveImmediately.value) { Message.warning("当前账号没有收款登记和核销权限"); return false; }
if (fulfillAll.value && !canFulfillImmediately.value) { Message.warning("当前账号没有交易直通所需权限"); return false; }
if (fulfillAll.value && totalAmount.value !== 0 && !receiveFullPayment.value) { Message.warning("全部履约直通必须同时直接全额收款"); return false; }
if (receiveFullPayment.value && totalAmount.value && !buildAttachments(paymentPhotoItems.value).length) { Message.warning("直接全额收款必须上传收款凭证照片"); return false; }
if (fulfillAll.value && !buildAttachments(fulfillmentPhotoItems.value).length) { Message.warning("全部履约直通必须上传履约凭证照片"); return false; }
return true; return true;
} }
async function save(): Promise<void> { async function save(): Promise<void> {
if (isSaving.value || !validate()) return; if (isSaving.value || !validate()) return;
if (receiveFullPayment.value || fulfillAll.value) {
const dialog = DialogPlugin.confirm({
title: "确认直通完成交易",
content: fulfillAll.value
? "将一次性创建订单、核销全额收款,并完成全部履约和库存扣减。提交后如需更正,只能走撤销或冲正流程。"
: "将一次性创建订单并核销全额收款。提交后如需更正,只能走资金冲正流程。",
cancelBtn: "取消",
confirmBtn: "确认并提交",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await submitOrder();
}
});
return;
}
await submitOrder();
}
async function submitOrder(): Promise<void> {
if (!requestNo.value) requestNo.value = createPackageRequestNo(); if (!requestNo.value) requestNo.value = createPackageRequestNo();
isSaving.value = true; isSaving.value = true;
try { try {
@@ -413,9 +501,15 @@ async function save(): Promise<void> {
purchasedAt: resolveTimestamp(purchasedAtText.value), purchasedAt: resolveTimestamp(purchasedAtText.value),
itemList, itemList,
attachmentList: buildAttachments(), attachmentList: buildAttachments(),
straightThrough: receiveFullPayment.value || fulfillAll.value ? {
receiveFullPayment: receiveFullPayment.value,
paymentAttachmentList: buildAttachments(paymentPhotoItems.value),
fulfillAll: fulfillAll.value,
fulfillmentAttachmentList: buildAttachments(fulfillmentPhotoItems.value)
} : undefined,
remark: remark.value.trim() || null remark: remark.value.trim() || null
}); });
Message.success("销售订单已创建"); Message.success(fulfillAll.value ? "订单、收款及履约已完成" : receiveFullPayment.value ? "订单及全额收款已完成" : "销售订单已创建");
await router.replace(`/management/sales-orders/${result.id}`); await router.replace(`/management/sales-orders/${result.id}`);
} catch (error) { } catch (error) {
const message = getErrorMessage(error, "销售订单创建失败,重试会复用同一请求号"); const message = getErrorMessage(error, "销售订单创建失败,重试会复用同一请求号");
+3 -1
View File
@@ -5,7 +5,7 @@
<h1 v-text="appName" /> <h1 v-text="appName" />
<p v-text="appDescription" /> <p v-text="appDescription" />
</section> </section>
<t-cell-group title="协议" theme="card" v-entrance-transition="70"> <t-cell-group theme="card" v-entrance-transition="70">
<t-cell <t-cell
v-for="item in agreementItemList" v-for="item in agreementItemList"
:key="item.code" :key="item.code"
@@ -21,6 +21,8 @@
</t-cell-group> </t-cell-group>
<footer class="footer" v-entrance-transition="140"> <footer class="footer" v-entrance-transition="140">
<p>技术支持:夜雨</p> <p>技术支持:夜雨</p>
<p>联系技术:imyeyu@qq.com</p>
<p>短信服务支持:广州易保网络科技有限公司</p>
<p>Copyright © 2026 imyeyu.com</p> <p>Copyright © 2026 imyeyu.com</p>
</footer> </footer>
</div> </div>
+1 -1
View File
@@ -62,7 +62,7 @@ const entryGroupList: HomeEntryGroup[] = [
{ {
path: "/management/customers/create?redirect=/customers", path: "/management/customers/create?redirect=/customers",
permission: permissionCodes.customer.create, permission: permissionCodes.customer.create,
icon: "edit-1", icon: "user-add",
title: "录入" title: "录入"
}, },
{ {
+2 -1
View File
@@ -2,7 +2,7 @@ import type { Attachment, Model, PageResult, User } from "timi-web-api";
import type { Customer } from "@/types/Customer"; import type { Customer } from "@/types/Customer";
import type { OrderFulfillment } from "@/types/Fulfillment"; import type { OrderFulfillment } from "@/types/Fulfillment";
import type { ProductSku } from "@/types/Product"; import type { ProductSku } from "@/types/Product";
import type { GaoSalesOrderItem, GaoSalesOrderStatus, SalesOrder } from "@/types/SalesOrder"; import type { GaoSalesOrderItem, GaoSalesOrderStatus, SalesOrder, SalesOrderStraightThroughRequest } from "@/types/SalesOrder";
export type PackageConfigStatus = "DRAFT" | "ACTIVE" | "INACTIVE"; export type PackageConfigStatus = "DRAFT" | "ACTIVE" | "INACTIVE";
@@ -79,6 +79,7 @@ export type PackagePurchaseRequest = {
purchasedAt?: number; purchasedAt?: number;
itemList?: PackagePurchaseItem[]; itemList?: PackagePurchaseItem[];
attachmentList?: PackageAttachment[]; attachmentList?: PackageAttachment[];
straightThrough?: SalesOrderStraightThroughRequest;
remark?: string | null; remark?: string | null;
}; };
+12
View File
@@ -98,6 +98,17 @@ export type DirectProductOrderItemRequest = {
remark?: string | null; remark?: string | null;
}; };
export type SalesOrderStraightThroughRequest = {
receiveFullPayment?: boolean;
paymentOccurredAt?: number;
paymentAttachmentList?: Partial<Attachment>[];
paymentRemark?: string | null;
fulfillAll?: boolean;
deliveredAt?: number;
fulfillmentAttachmentList?: Partial<Attachment>[];
fulfillmentRemark?: string | null;
};
export type DirectProductOrderCreateRequest = { export type DirectProductOrderCreateRequest = {
requestNo: string; requestNo: string;
storeId?: string; storeId?: string;
@@ -105,6 +116,7 @@ export type DirectProductOrderCreateRequest = {
purchasedAt?: number; purchasedAt?: number;
itemList: DirectProductOrderItemRequest[]; itemList: DirectProductOrderItemRequest[];
attachmentList?: Partial<Attachment>[]; attachmentList?: Partial<Attachment>[];
straightThrough?: SalesOrderStraightThroughRequest;
remark?: string | null; remark?: string | null;
}; };