!205 feat(@vben/web-antd): erp 模块除去收款单和付款单功能基本完成

* fix(@vben/web-antd): erp 解决冲突
* fix(@vben/web-antd): erp 更新采购和销售退货表单,修复关联订单选择功能不统一详细可编辑的问题,统一文件名称格式
* feat(@vben/web-antd): erp-finance-account 新增结算账户管理功能,包括表单、列表及相关操作
* feat(@vben/web-antd): erp-sale-return 新增销售退货管理功能,包括退货列表、表单及相关操作
* feat(@vben/web-antd): erp-sale-out 新增销售出库管理功能,包括出库列表、表单及相关操作
* feat(@vben/web-antd): erp-sale-order 新增销售订单管理功能,包括订单列表、表单及相关操作
* feat(@vben/web-antd): erp-sale-customer 新增客户管理功能,包括客户表单、列表及相关操作
* feat(@vben/web-antd): erp-purchase-return 新增采购退货管理功能,包括表单、列表及相关操作
* feat(@vben/web-antd): erp-purchase-in 新增采购入库管理功能,包括表单、列表及相关操作
* feat(@vben/web-antd): erp-stock-check 新增库存盘点单管理功能,包括表单、列表及相关操作
* feat(@vben/web-antd): erp-stock-move 新增库存调拨单管理功能,包括表单、列表及相关操作
* feat(@vben/web-antd): erp-stock-out 新增其它出库单管理功能,包括表单、列表及相关操作
* fix(@vben/web-antd): erp-stock-in 修复提交表单时清空产品项 ID,确保请求成功不报row_xxx报错
This commit is contained in:
XuZhiqiang
2025-08-26 08:40:01 +00:00
committed by xingyu
parent 4af5931e92
commit bc2b71baa3
53 changed files with 10508 additions and 247 deletions

View File

@@ -0,0 +1,206 @@
<script lang="ts" setup>
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { computed, nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createSaleOrder,
getSaleOrder,
updateSaleOrder,
} from '#/api/erp/sale/order';
import { useFormSchema } from '../data';
import SaleOrderItemForm from './sale-order-item-form.vue';
const emit = defineEmits(['success']);
const formData = ref<ErpSaleOrderApi.SaleOrder>();
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',
},
labelWidth: 120,
},
wrapperClass: 'grid-cols-3',
layout: 'vertical',
schema: useFormSchema(),
showDefaultActions: false,
handleValuesChange: (values, changedFields) => {
if (formData.value && changedFields.includes('discountPercent')) {
formData.value.discountPercent = values.discountPercent;
}
},
});
const handleUpdateItems = (items: ErpSaleOrderApi.SaleOrderItem[]) => {
formData.value = modalApi.getData<ErpSaleOrderApi.SaleOrder>();
if (formData.value) {
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({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
await nextTick();
const itemFormInstance = Array.isArray(itemFormRef.value)
? itemFormRef.value[0]
: itemFormRef.value;
if (itemFormInstance && typeof itemFormInstance.validate === 'function') {
try {
const isValid = await itemFormInstance.validate();
if (!isValid) {
message.error('子表单验证失败');
return;
}
} catch (error: any) {
message.error(error.message || '子表单验证失败');
return;
}
} else {
message.error('子表单验证方法不存在');
return;
}
// 验证产品清单不能为空
if (!formData.value?.items || formData.value.items.length === 0) {
message.error('产品清单不能为空,请至少添加一个产品');
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ErpSaleOrderApi.SaleOrder;
data.items = formData.value?.items?.map((item) => ({
...item,
// 解决新增销售订单报错
id: undefined,
}));
// 将文件数组转换为字符串
if (data.fileUrl && Array.isArray(data.fileUrl)) {
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
}
try {
await (formType.value === 'create'
? createSaleOrder(data)
: updateSaleOrder(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) {
return;
}
formType.value = data.type;
if (!data.id) {
// 初始化空的表单数据
formData.value = { items: [] } as unknown as ErpSaleOrderApi.SaleOrder;
await nextTick();
const itemFormInstance = Array.isArray(itemFormRef.value)
? itemFormRef.value[0]
: itemFormRef.value;
if (itemFormInstance && typeof itemFormInstance.init === 'function') {
itemFormInstance.init([]);
}
return;
}
modalApi.lock();
try {
formData.value = await getSaleOrder(data.id);
// 设置到 values
await formApi.setValues(formData.value);
// 初始化子表单
await nextTick();
const itemFormInstance = Array.isArray(itemFormRef.value)
? itemFormRef.value[0]
: itemFormRef.value;
if (itemFormInstance && typeof itemFormInstance.init === 'function') {
itemFormInstance.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"
:show-confirm-button="formType !== 'detail'"
>
<Form class="mx-3">
<template #product="slotProps">
<SaleOrderItemForm
v-bind="slotProps"
ref="itemFormRef"
class="w-full"
:items="formData?.items ?? []"
:disabled="formType === 'detail'"
:discount-percent="formData?.discountPercent ?? 0"
@update:items="handleUpdateItems"
@update:discount-price="handleUpdateDiscountPrice"
@update:total-price="handleUpdateTotalPrice"
/>
</template>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,356 @@
<script lang="ts" setup>
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { nextTick, onMounted, ref, watch } from 'vue';
import { erpPriceMultiply } from '@vben/utils';
import { Input, InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getStockCount } from '#/api/erp/stock/stock';
import { useSaleOrderItemTableColumns } from '../data';
const props = withDefaults(defineProps<Props>(), {
items: () => [],
disabled: false,
discountPercent: 0,
});
const emit = defineEmits([
'update:items',
'update:discount-price',
'update:total-price',
]);
interface Props {
items?: ErpSaleOrderApi.SaleOrderItem[];
disabled?: boolean;
discountPercent?: number;
}
const tableData = ref<ErpSaleOrderApi.SaleOrderItem[]>([]);
const productOptions = ref<any[]>([]);
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
editConfig: {
trigger: 'click',
mode: 'cell',
},
columns: useSaleOrderItemTableColumns(),
data: tableData.value,
border: true,
showOverflow: true,
autoResize: true,
minHeight: 250,
keepSource: true,
rowConfig: {
keyField: 'id',
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 监听外部传入的列数据 */
watch(
() => props.items,
async (items) => {
if (!items) {
return;
}
await nextTick();
tableData.value = [...items];
await nextTick();
gridApi.grid.reloadData(tableData.value);
},
{
immediate: true,
},
);
/** 计算 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 () => {
productOptions.value = await getProductSimpleList();
});
function handleAdd() {
const newRow = {
productId: undefined,
productName: '',
productUnitId: undefined,
productUnitName: '',
productBarCode: '',
count: 1,
productPrice: 0,
totalProductPrice: 0,
taxPercent: 0,
taxPrice: 0,
totalPrice: 0,
stockCount: 0,
remark: '',
};
tableData.value.push(newRow);
gridApi.grid.insertAt(newRow, -1);
emit('update:items', [...tableData.value]);
}
function handleDelete(row: ErpSaleOrderApi.SaleOrderItem) {
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) {
const product = productOptions.value.find((p) => p.id === productId);
if (!product) {
return;
}
const stockCount = await getStockCount(productId);
row.productId = productId;
row.productUnitId = product.unitId;
row.productBarCode = product.barCode;
row.productUnitName = product.unitName;
row.productName = product.name;
row.stockCount = stockCount || 0;
row.productPrice = product.salePrice || 0;
row.count = row.count || 1;
handlePriceChange(row);
}
function handlePriceChange(row: any) {
if (row.productPrice && row.count) {
row.totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
row.taxPrice =
erpPriceMultiply(row.totalProductPrice, (row.taxPercent || 0) / 100) ?? 0;
row.totalPrice = row.totalProductPrice + row.taxPrice;
}
handleUpdateValue(row);
}
function handleUpdateValue(row: any) {
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index === -1) {
tableData.value.push(row);
} else {
tableData.value[index] = row;
}
emit('update:items', [...tableData.value]);
}
const getSummaries = (): {
count: number;
productName: string;
taxPrice: number;
totalPrice: number;
totalProductPrice: number;
} => {
return {
productName: '合计',
count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0),
totalProductPrice: tableData.value.reduce(
(sum, item) => sum + (item.totalProductPrice || 0),
0,
),
taxPrice: tableData.value.reduce(
(sum, item) => sum + (item.taxPrice || 0),
0,
),
totalPrice: tableData.value.reduce(
(sum, item) => sum + (item.totalPrice || 0),
0,
),
};
};
const validate = async (): Promise<boolean> => {
try {
for (let i = 0; i < tableData.value.length; i++) {
const item = tableData.value[i];
if (item) {
if (!item.productId) {
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} 行:产品单价不能为空`);
}
}
}
return true;
} catch (error) {
console.error('验证失败:', error);
throw error;
}
};
const getData = (): ErpSaleOrderApi.SaleOrderItem[] => tableData.value;
const init = (items: ErpSaleOrderApi.SaleOrderItem[] | undefined): void => {
tableData.value =
items && items.length > 0
? items.map((item) => {
const newItem = { ...item };
if (newItem.productPrice && newItem.count) {
newItem.totalProductPrice =
erpPriceMultiply(newItem.productPrice, newItem.count) ?? 0;
newItem.taxPrice =
erpPriceMultiply(
newItem.totalProductPrice,
(newItem.taxPercent || 0) / 100,
) ?? 0;
newItem.totalPrice = newItem.totalProductPrice + newItem.taxPrice;
}
return newItem;
})
: [];
nextTick(() => {
gridApi.grid.reloadData(tableData.value);
});
};
defineExpose({
validate,
getData,
init,
});
</script>
<template>
<Grid class="w-full">
<template #productId="{ row }">
<Select
v-if="!disabled"
v-model:value="row.productId"
:options="productOptions"
:field-names="{ label: 'name', value: 'id' }"
style="width: 100%"
placeholder="请选择产品"
show-search
@change="handleProductChange($event, row)"
/>
<span v-else>{{ row.productName || '-' }}</span>
</template>
<template #count="{ row }">
<InputNumber
v-if="!disabled"
v-model:value="row.count"
:min="0"
:precision="2"
@change="handlePriceChange(row)"
/>
<span v-else>{{ row.count || '-' }}</span>
</template>
<template #productPrice="{ row }">
<InputNumber
v-if="!disabled"
v-model:value="row.productPrice"
:min="0"
:precision="2"
@change="handlePriceChange(row)"
/>
<span v-else>{{ row.productPrice || '-' }}</span>
</template>
<template #taxPercent="{ row }">
<InputNumber
v-if="!disabled"
v-model:value="row.taxPercent"
:min="0"
:max="100"
:precision="2"
@change="handlePriceChange(row)"
/>
<span v-else>{{ row.taxPercent || '-' }}</span>
</template>
<template #remark="{ row }">
<Input v-if="!disabled" v-model:value="row.remark" class="w-full" />
<span v-else>{{ row.remark || '-' }}</span>
</template>
<template #bottom>
<div class="border-border bg-muted mt-2 rounded border p-2">
<div class="text-muted-foreground flex justify-between text-sm">
<span class="text-foreground font-medium">合计</span>
<div class="flex space-x-4">
<span>数量{{ getSummaries().count }}</span>
<span>金额{{ getSummaries().totalProductPrice }}</span>
<span>税额{{ getSummaries().taxPrice }}</span>
<span>税额合计{{ getSummaries().totalPrice }}</span>
</div>
</div>
</div>
<TableAction
v-if="!disabled"
class="mt-4 flex justify-center"
:actions="[
{
label: '添加产品',
type: 'default',
onClick: handleAdd,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
v-if="!disabled"
:actions="[
{
label: '删除',
type: 'link',
danger: true,
popConfirm: {
title: '确认删除该产品吗?',
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>