feat(@vben/web-antd): erp-采购订单表单逻辑

- 使用 handleValuesChange
- 优化 discountPrice 和 totalPrice 的计算逻辑
- 改进子表单的验证和数据处理
- 统一数值格式化处理
- 优化表格数据的更新方式
pull/181/head
nehc 2025-07-25 19:20:39 +08:00
parent e319888240
commit 8a7239ce24
4 changed files with 97 additions and 50 deletions

View File

@ -66,7 +66,7 @@ function handleUpdateValue(row: any) {
} else { } else {
tableData.value[index] = row; tableData.value[index] = row;
} }
emit('update:products', tableData.value); emit('update:products', [...tableData.value]);
} }
/** 表格配置 */ /** 表格配置 */

View File

@ -3,6 +3,7 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { erpCountInputFormatter, erpPriceInputFormatter } from '@vben/utils'; import { erpCountInputFormatter, erpPriceInputFormatter } from '@vben/utils';
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account'; import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product'; import { getProductSimpleList } from '#/api/erp/product/product';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier'; import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
@ -96,11 +97,11 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0, min: 0,
max: 100, max: 100,
precision: 2, precision: 2,
formatter: erpPriceInputFormatter,
style: { width: '100%' }, style: { width: '100%' },
}, },
fieldName: 'discountPercent', fieldName: 'discountPercent',
label: '优惠率(%)', label: '优惠率(%)',
rules: z.number().min(0).optional(),
}, },
{ {
component: 'InputNumber', component: 'InputNumber',
@ -146,11 +147,12 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
placeholder: '请输入支付订金', placeholder: '请输入支付订金',
precision: 2, precision: 2,
formatter: erpPriceInputFormatter,
style: { width: '100%' }, style: { width: '100%' },
min: 0,
}, },
fieldName: 'depositPrice', fieldName: 'depositPrice',
label: '支付订金', label: '支付订金',
rules: z.number().min(0).optional(),
}, },
]; ];
} }

View File

@ -16,13 +16,19 @@ import { usePurchaseOrderItemTableColumns } from '../data';
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
items: () => [], items: () => [],
disabled: false, disabled: false,
discountPercent: 0,
}); });
const emit = defineEmits(['update:items']); const emit = defineEmits([
'update:items',
'update:discount-price',
'update:total-price',
]);
interface Props { interface Props {
items?: ErpPurchaseOrderApi.PurchaseOrderItem[]; items?: ErpPurchaseOrderApi.PurchaseOrderItem[];
disabled?: boolean; disabled?: boolean;
discountPercent?: number;
} }
const tableData = ref<ErpPurchaseOrderApi.PurchaseOrderItem[]>([]); const tableData = ref<ErpPurchaseOrderApi.PurchaseOrderItem[]>([]);
@ -62,7 +68,8 @@ watch(
return; return;
} }
await nextTick(); await nextTick();
tableData.value = items; tableData.value = [...items];
await nextTick();
gridApi.grid.reloadData(tableData.value); gridApi.grid.reloadData(tableData.value);
}, },
{ {
@ -70,6 +77,30 @@ watch(
}, },
); );
/** 计算 discountPrice、totalPrice 价格 */
watch(
() => [tableData.value, props.discountPercent],
() => {
if (!tableData.value || tableData.value.length === 0) {
return;
}
const totalPrice = tableData.value.reduce(
(prev, curr) => prev + (curr.totalPrice || 0),
0,
);
const discountPrice =
props.discountPercent === null
? 0
: erpPriceMultiply(totalPrice, props.discountPercent / 100);
const finalTotalPrice = totalPrice - discountPrice;
//
emit('update:discount-price', discountPrice);
emit('update:total-price', finalTotalPrice);
},
{ deep: true },
);
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
productOptions.value = await getProductSimpleList(); productOptions.value = await getProductSimpleList();
@ -81,6 +112,11 @@ function handleAdd() {
function handleDelete(row: ErpPurchaseOrderApi.PurchaseOrderItem) { function handleDelete(row: ErpPurchaseOrderApi.PurchaseOrderItem) {
gridApi.grid.remove(row); gridApi.grid.remove(row);
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
}
emit('update:items', [...tableData.value]);
} }
async function handleProductChange(productId: any, row: any) { async function handleProductChange(productId: any, row: any) {
@ -121,7 +157,7 @@ function handleUpdateValue(row: any) {
} else { } else {
tableData.value[index] = row; tableData.value[index] = row;
} }
emit('update:items', tableData.value); emit('update:items', [...tableData.value]);
} }
const getSummaries = (): { const getSummaries = (): {
@ -131,7 +167,7 @@ const getSummaries = (): {
totalPrice: number; totalPrice: number;
totalProductPrice: number; totalProductPrice: number;
} => { } => {
const sums = { return {
productName: '合计', productName: '合计',
count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0), count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0),
totalProductPrice: tableData.value.reduce( totalProductPrice: tableData.value.reduce(
@ -147,20 +183,21 @@ const getSummaries = (): {
0, 0,
), ),
}; };
return sums;
}; };
const validate = async (): Promise<void> => { const validate = async (): Promise<void> => {
for (let i = 0; i < tableData.value.length; i++) { for (let i = 0; i < tableData.value.length; i++) {
const item = tableData.value[i]; const item = tableData.value[i];
if (!item.productId) { if (item) {
throw new Error(`${i + 1} 行:产品不能为空`); if (!item.productId) {
} throw new Error(`${i + 1} 行:产品不能为空`);
if (!item.count || item.count <= 0) { }
throw new Error(`${i + 1} 行:产品数量不能为空`); if (!item.count || item.count <= 0) {
} throw new Error(`${i + 1} 行:产品数量不能为空`);
if (!item.productPrice || item.productPrice <= 0) { }
throw new Error(`${i + 1} 行:产品单价不能为空`); if (!item.productPrice || item.productPrice <= 0) {
throw new Error(`${i + 1} 行:产品单价不能为空`);
}
} }
} }
}; };
@ -169,8 +206,10 @@ const getData = (): ErpPurchaseOrderApi.PurchaseOrderItem[] => tableData.value;
const init = ( const init = (
items: ErpPurchaseOrderApi.PurchaseOrderItem[] | undefined, items: ErpPurchaseOrderApi.PurchaseOrderItem[] | undefined,
): void => { ): void => {
tableData.value = items || []; tableData.value = items ? [...items] : [];
gridApi.grid.reloadData(tableData.value); nextTick(() => {
gridApi.grid.reloadData(tableData.value);
});
}; };
defineExpose({ validate, getData, init }); defineExpose({ validate, getData, init });
@ -232,7 +271,6 @@ defineExpose({ validate, getData, init });
</template> </template>
<template #bottom> <template #bottom>
<!-- 合计行 -->
<div class="border-border bg-muted mt-2 rounded border p-2"> <div class="border-border bg-muted mt-2 rounded border p-2">
<div class="text-muted-foreground flex justify-between text-sm"> <div class="text-muted-foreground flex justify-between text-sm">
<span class="text-foreground font-medium">合计</span> <span class="text-foreground font-medium">合计</span>

View File

@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order'; import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import { computed, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { erpPriceMultiply, formatDateTime } from '@vben/utils'; import { formatDateTime } from '@vben/utils';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@ -40,52 +40,56 @@ const [Form, formApi] = useVbenForm({
layout: 'vertical', layout: 'vertical',
schema: useFormSchema(), schema: useFormSchema(),
showDefaultActions: false, showDefaultActions: false,
handleValuesChange: (values, changedFields) => {
if (formData.value && changedFields.includes('discountPercent')) {
formData.value.discountPercent = values.discountPercent;
}
},
}); });
/** 计算 discountPrice、totalPrice 价格 */
watch(
() => [formData.value?.items, formData.value?.discountPercent],
() => {
if (!formData.value?.items) {
return;
}
// discountPrice
let totalPrice = 0;
formData.value.items.forEach((item) => {
totalPrice += item.totalPrice || 0;
});
formData.value.discountPrice = erpPriceMultiply(
totalPrice,
formData.value.discountPercent / 100,
);
formData.value.totalPrice = totalPrice - formData.value.discountPrice;
//
formApi.setValues({
discountPrice: formData.value.discountPrice,
totalPrice: formData.value.totalPrice,
});
},
{ deep: true },
);
const handleUpdateItems = (items: ErpPurchaseOrderApi.PurchaseOrderItem[]) => { const handleUpdateItems = (items: ErpPurchaseOrderApi.PurchaseOrderItem[]) => {
formData.value = modalApi.getData<ErpPurchaseOrderApi.PurchaseOrder>();
if (formData.value) { if (formData.value) {
formData.value.items = items; formData.value.items = items;
} }
}; };
const handleUpdateDiscountPrice = (discountPrice: number) => {
if (formData.value) {
formData.value.discountPrice = discountPrice;
formApi.setValues({
discountPrice: formData.value.discountPrice,
});
}
};
const handleUpdateTotalPrice = (totalPrice: number) => {
if (formData.value) {
formData.value.totalPrice = totalPrice;
formApi.setValues({
totalPrice: formData.value.totalPrice,
});
}
};
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
await itemFormRef.value?.validate(); //
if (itemFormRef.value && typeof itemFormRef.value.validate === 'function') {
await itemFormRef.value.validate();
}
modalApi.lock(); modalApi.lock();
// //
const data = const data =
(await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder; (await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
data.items = itemFormRef.value?.getData(); data.items =
itemFormRef.value && typeof itemFormRef.value.getData === 'function'
? itemFormRef.value.getData()
: [];
try { try {
await (formType.value === 'create' await (formType.value === 'create'
? createPurchaseOrder(data) ? createPurchaseOrder(data)
@ -140,9 +144,12 @@ defineExpose({ modalApi });
v-bind="slotProps" v-bind="slotProps"
ref="itemFormRef" ref="itemFormRef"
class="w-full" class="w-full"
:items="formData?.items || []" :items="formData?.items ?? []"
:disabled="formType === 'detail'" :disabled="formType === 'detail'"
:discount-percent="formData?.discountPercent ?? 0"
@update:items="handleUpdateItems" @update:items="handleUpdateItems"
@update:discount-price="handleUpdateDiscountPrice"
@update:total-price="handleUpdateTotalPrice"
/> />
</template> </template>
</Form> </Form>