feat(@vben/web-antd): erp-重构采购订单模块

- 更新表单组件,使用已有的 VbenForm 组件
- 优化表单字段定义,使用 useFormSchema 函数统一管理
- 重构表格组件,使用 VxeTable
- 优化数据处理逻辑,提高代码复用性和可维护性
- 调整界面布局和样式,提升用户体验
pull/181/head
nehc 2025-07-23 17:02:38 +08:00
parent 1cad71f3bf
commit 7c2c249e5d
5 changed files with 399 additions and 393 deletions

View File

@ -1,338 +0,0 @@
<script lang="ts" setup>
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import {
erpPriceInputFormatter,
erpPriceMultiply,
formatDateTime,
} from '@vben/utils';
import {
Button,
Col,
DatePicker,
Form,
Input,
InputNumber,
message,
Row,
Select,
Textarea,
} from 'ant-design-vue';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import {
createPurchaseOrder,
getPurchaseOrder,
updatePurchaseOrder,
} from '#/api/erp/purchase/order';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSimpleUserList } from '#/api/system/user';
import PurchaseOrderItemForm from './PurchaseOrderItemForm.vue';
interface Props {
type: string;
id?: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
success: [];
}>();
const [Modal, modalApi] = useVbenModal({
title: computed(() => {
if (props.type === 'create') return '添加采购订单';
if (props.type === 'update') return '编辑采购订单';
return '采购订单详情';
}),
width: '80%',
});
const formLoading = ref(false);
const formType = ref('');
const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>({
id: undefined,
no: undefined,
supplierId: undefined,
orderTime: undefined,
remark: undefined,
fileUrl: undefined,
discountPercent: 0,
discountPrice: 0,
totalPrice: 0,
accountId: undefined,
depositPrice: 0,
status: 10,
items: [],
});
const formRules = reactive({
supplierId: [{ required: true, message: '供应商不能为空', trigger: 'blur' }],
orderTime: [{ required: true, message: '订单时间不能为空', trigger: 'blur' }],
});
const formRef = ref();
const supplierList = ref<any[]>([]);
const userList = ref<any[]>([]);
const accountList = ref<any[]>([]);
const itemFormRef = ref();
/** 计算 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;
},
{ deep: true },
);
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
modalApi.open();
resetForm();
formType.value = type;
formData.value.id = id;
formLoading.value = true;
try {
//
supplierList.value = await getSupplierSimpleList();
userList.value = await getSimpleUserList();
accountList.value = await getAccountSimpleList();
//
if (id) {
const res = await getPurchaseOrder(id);
formData.value = res;
formData.value.orderTime = formatDateTime(formData.value.orderTime);
}
} finally {
formLoading.value = false;
}
//
await nextTick();
itemFormRef.value?.init(formData.value.items || []);
};
/** 提交表单 */
const submitForm = async () => {
//
await formRef.value?.validate();
await itemFormRef.value?.validate();
formLoading.value = true;
try {
const data = formData.value as any;
data.items = itemFormRef.value?.getData();
if (formType.value === 'create') {
await createPurchaseOrder(data);
message.success('新增成功');
} else {
await updatePurchaseOrder(data);
message.success('更新成功');
}
modalApi.close();
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
no: undefined,
supplierId: undefined,
orderTime: undefined,
remark: undefined,
fileUrl: undefined,
discountPercent: 0,
discountPrice: 0,
totalPrice: 0,
accountId: undefined,
depositPrice: 0,
status: 10,
items: [],
};
formRef.value?.resetFields();
};
defineExpose({ open });
</script>
<template>
<Modal>
<Form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
label-align="left"
>
<Row :gutter="16">
<Col :span="12">
<Form.Item label="供应商" name="supplierId">
<Select
v-model:value="formData.supplierId"
placeholder="请选择供应商"
allow-clear
show-search
:disabled="formType === 'detail'"
>
<Select.Option
v-for="item in supplierList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</Select.Option>
</Select>
</Form.Item>
</Col>
<Col :span="12">
<Form.Item label="订单时间" name="orderTime">
<DatePicker
v-model:value="formData.orderTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择订单时间"
:disabled="formType === 'detail'"
class="w-full"
/>
</Form.Item>
</Col>
</Row>
<Row :gutter="16">
<Col :span="12">
<Form.Item label="备注" name="remark">
<Textarea
v-model:value="formData.remark"
placeholder="请输入备注"
:disabled="formType === 'detail'"
/>
</Form.Item>
</Col>
<Col :span="12">
<Form.Item label="附件" name="fileUrl">
<Input
v-model:value="formData.fileUrl"
placeholder="请输入附件地址"
:disabled="formType === 'detail'"
/>
</Form.Item>
</Col>
</Row>
</Form>
<!-- 子表的表单 -->
<PurchaseOrderItemForm
ref="itemFormRef"
:items="formData.items"
:disabled="formType === 'detail'"
/>
<Form
:model="formData"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
label-align="left"
>
<Row :gutter="16">
<Col :span="12">
<Form.Item label="优惠率(%)">
<InputNumber
v-model:value="formData.discountPercent"
:formatter="erpPriceInputFormatter"
placeholder="请输入优惠率"
:disabled="formType === 'detail'"
class="w-full"
/>
</Form.Item>
</Col>
<Col :span="12">
<Form.Item label="付款优惠">
<InputNumber
v-model:value="formData.discountPrice"
:formatter="erpPriceInputFormatter"
placeholder="请输入付款优惠"
:disabled="true"
class="w-full"
/>
</Form.Item>
</Col>
</Row>
<Row :gutter="16">
<Col :span="12">
<Form.Item label="优惠后金额">
<InputNumber
v-model:value="formData.totalPrice"
:formatter="erpPriceInputFormatter"
placeholder="请输入优惠后金额"
:disabled="true"
class="w-full"
/>
</Form.Item>
</Col>
<Col :span="12">
<Form.Item label="结算账户">
<Select
v-model:value="formData.accountId"
placeholder="请选择结算账户"
allow-clear
:disabled="formType === 'detail'"
>
<Select.Option
v-for="item in accountList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row :gutter="16">
<Col :span="12">
<Form.Item label="支付订金">
<InputNumber
v-model:value="formData.depositPrice"
:formatter="erpPriceInputFormatter"
placeholder="请输入支付订金"
:disabled="formType === 'detail'"
class="w-full"
/>
</Form.Item>
</Col>
</Row>
</Form>
<template #footer>
<Button @click="modalApi.close()"></Button>
<Button
v-if="formType !== 'detail'"
:loading="formLoading"
type="primary"
@click="submitForm"
>
确定
</Button>
</template>
</Modal>
</template>

View File

@ -1,10 +1,132 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { erpCountInputFormatter, erpPriceInputFormatter } from '@vben/utils';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { DICT_TYPE, getDictOptions } from '#/utils';
/** 表单的配置项 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择供应商',
allowClear: true,
showSearch: true,
api: getSupplierSimpleList,
fieldNames: {
label: 'name',
value: 'id',
},
},
fieldName: 'supplierId',
label: '供应商',
rules: 'required',
},
{
component: 'DatePicker',
componentProps: {
placeholder: '选择订单时间',
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
style: { width: '100%' },
},
fieldName: 'orderTime',
label: '订单时间',
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
autoSize: { minRows: 2, maxRows: 4 },
class: 'w-full',
},
fieldName: 'remark',
label: '备注',
formItemClass: 'col-span-2',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入附件地址',
class: 'w-full',
},
fieldName: 'fileUrl',
label: '附件',
formItemClass: 'col-span-2',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入优惠率',
min: 0,
max: 100,
precision: 2,
formatter: erpPriceInputFormatter,
style: { width: '100%' },
},
fieldName: 'discountPercent',
label: '优惠率(%)',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '付款优惠',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
style: { width: '100%' },
},
fieldName: 'discountPrice',
label: '付款优惠',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '优惠后金额',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
style: { width: '100%' },
},
fieldName: 'totalPrice',
label: '优惠后金额',
},
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择结算账户',
allowClear: true,
showSearch: true,
api: getAccountSimpleList,
fieldNames: {
label: 'name',
value: 'id',
},
},
fieldName: 'accountId',
label: '结算账户',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入支付订金',
precision: 2,
formatter: erpPriceInputFormatter,
style: { width: '100%' },
},
fieldName: 'depositPrice',
label: '支付订金',
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
@ -23,14 +145,14 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择产品',
allowClear: true,
showSearch: true,
api: getProductSimpleList,
fieldNames: {
label: 'name',
value: 'id',
},
placeholder: '请选择产品',
allowClear: true,
showSearch: true,
},
},
{
@ -38,8 +160,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '订单时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
placeholder: ['开始时间', '结束时间'],
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
},
{
@ -47,14 +171,14 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '供应商',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择供应商',
allowClear: true,
showSearch: true,
api: getSupplierSimpleList,
fieldNames: {
label: 'name',
value: 'id',
},
placeholder: '请选择供应商',
allowClear: true,
showSearch: true,
},
},
{
@ -62,14 +186,14 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '创建人',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择创建人',
allowClear: true,
showSearch: true,
api: getSimpleUserList,
fieldNames: {
label: 'nickname',
value: 'id',
},
placeholder: '请选择创建人',
allowClear: true,
showSearch: true,
},
},
{
@ -82,29 +206,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
allowClear: true,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Input',
componentProps: {
placeholder: '请输入备注',
allowClear: true,
},
},
{
fieldName: 'inStatus',
label: '入库状态',
component: 'Select',
componentProps: {
options: [
{ label: '未入库', value: 0 },
{ label: '部分入库', value: 1 },
{ label: '全部入库', value: 2 },
],
placeholder: '请选择入库状态',
allowClear: true,
},
},
{
fieldName: 'returnStatus',
label: '退货状态',
@ -122,73 +223,68 @@ export function useGridFormSchema(): VbenFormSchema[] {
];
}
/** 表格列配置 */
export function useGridColumns(): VxeGridPropTypes.Columns {
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
type: 'checkbox',
width: 60,
width: 50,
fixed: 'left',
},
{
field: 'no',
title: '订单单号',
width: 260,
fixed: 'left',
minWidth: 180,
},
{
field: 'productNames',
title: '产品信息',
minWidth: 200,
showOverflow: 'tooltip',
},
{
field: 'supplierName',
title: '供应商',
minWidth: 120,
},
{
field: 'orderTime',
title: '订单时间',
width: 160,
formatter: 'formatDateTime',
minWidth: 160,
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
{
field: 'totalCount',
title: '总数量',
minWidth: 100,
formatter: 'formatNumber',
},
{
field: 'inCount',
title: '入库数量',
minWidth: 100,
formatter: 'formatNumber',
},
{
field: 'returnCount',
title: '退货数量',
minWidth: 100,
formatter: 'formatNumber',
},
{
field: 'totalProductPrice',
title: '金额合计',
formatter: 'formatAmount2',
minWidth: 120,
formatter: 'formatNumber',
},
{
field: 'totalPrice',
title: '含税金额',
formatter: 'formatAmount2',
minWidth: 120,
formatter: 'formatNumber',
},
{
field: 'depositPrice',
title: '支付订金',
formatter: 'formatAmount2',
minWidth: 120,
formatter: 'formatNumber',
},
{
field: 'status',
@ -197,7 +293,6 @@ export function useGridColumns(): VxeGridPropTypes.Columns {
name: 'CellDict',
props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
},
minWidth: 100,
},
{
title: '操作',
@ -207,3 +302,113 @@ export function useGridColumns(): VxeGridPropTypes.Columns {
},
];
}
/** 采购订单明细项表单配置 */
export function useItemFormSchema(
onProductChange?: (productId: number) => void,
): VbenFormSchema[] {
return [
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择产品',
allowClear: true,
showSearch: true,
api: getProductSimpleList,
fieldNames: {
label: 'name',
value: 'id',
},
onChange: onProductChange,
},
fieldName: 'productId',
label: '产品名称',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入数量',
min: 1,
precision: 2,
formatter: erpCountInputFormatter,
style: { width: '100%' },
},
fieldName: 'count',
label: '数量',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入单价',
min: 0,
precision: 2,
formatter: erpPriceInputFormatter,
style: { width: '100%' },
},
fieldName: 'productPrice',
label: '产品单价',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '金额',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
style: { width: '100%' },
},
fieldName: 'totalPrice',
label: '金额',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入税率',
min: 0,
max: 100,
precision: 2,
style: { width: '100%' },
},
fieldName: 'taxPercent',
label: '税率(%)',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '税额',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
style: { width: '100%' },
},
fieldName: 'taxPrice',
label: '税额',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '税额合计',
precision: 2,
formatter: erpPriceInputFormatter,
disabled: true,
style: { width: '100%' },
},
fieldName: 'totalTaxPrice',
label: '税额合计',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
autoSize: { minRows: 2, maxRows: 4 },
class: 'w-full',
},
fieldName: 'remark',
label: '备注',
formItemClass: 'col-span-2',
},
];
}

View File

@ -19,8 +19,8 @@ import {
} from '#/api/erp/purchase/order';
import { $t } from '#/locales';
import PurchaseOrderForm from './components/PurchaseOrderForm.vue';
import { useGridColumns, useGridFormSchema } from './data';
import PurchaseOrderForm from './modules/form.vue';
/** ERP 采购订单列表 */
defineOptions({ name: 'ErpPurchaseOrder' });

View File

@ -0,0 +1,139 @@
<script lang="ts" setup>
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { erpPriceMultiply, formatDateTime } from '@vben/utils';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createPurchaseOrder,
getPurchaseOrder,
updatePurchaseOrder,
} from '#/api/erp/purchase/order';
import { useFormSchema } from '../data';
import PurchaseOrderItemForm from './PurchaseOrderItemForm.vue';
const emit = defineEmits(['success']);
const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
const formType = ref('');
const itemFormRef = ref();
const getTitle = computed(() => {
if (formType.value === 'create') return '添加采购订单';
if (formType.value === 'update') return '编辑采购订单';
return '采购订单详情';
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
/** 计算 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 [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
await itemFormRef.value?.validate();
modalApi.lock();
//
const data =
(await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
data.items = itemFormRef.value?.getData();
try {
await (formType.value === 'create'
? createPurchaseOrder(data)
: updatePurchaseOrder(data));
//
await modalApi.close();
emit('success');
message.success(formType.value === 'create' ? '新增成功' : '更新成功');
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<{ id?: number; type: string }>();
if (!data || !data.id) {
return;
}
formType.value = data.type;
modalApi.lock();
try {
formData.value = await getPurchaseOrder(data.id);
formData.value.orderTime = formatDateTime(formData.value.orderTime);
// values
await formApi.setValues(formData.value);
//
itemFormRef.value?.init(formData.value.items || []);
} finally {
modalApi.unlock();
}
},
});
defineExpose({ modalApi });
</script>
<template>
<Modal
v-bind="$attrs"
:title="getTitle"
class="w-1/2"
:closable="true"
:mask-closable="true"
>
<Form />
<!-- 子表的表单 -->
<PurchaseOrderItemForm
ref="itemFormRef"
:items="formData?.items || []"
:disabled="formType === 'detail'"
/>
</Modal>
</template>