feat:【antd】【crm】产品的 components 优化

This commit is contained in:
YunaiV
2025-09-28 20:00:06 +08:00
parent ee3af0293b
commit 3d2a53a6b2
13 changed files with 171 additions and 132 deletions

View File

@@ -0,0 +1,111 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
/** 产品详情列表的列定义 */
export function useDetailListColumns(
showBusinessPrice: boolean,
): VxeTableGridOptions['columns'] {
return [
{
field: 'productName',
title: '产品名称',
},
{
field: 'productNo',
title: '产品条码',
},
{
field: 'productUnit',
title: '产品单位',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
},
},
{
field: 'productPrice',
title: '产品价格(元)',
formatter: 'formatAmount2',
},
{
field: 'businessPrice',
title: '商机价格(元)',
formatter: 'formatAmount2',
visible: showBusinessPrice,
},
{
field: 'contractPrice',
title: '合同价格(元)',
formatter: 'formatAmount2',
visible: !showBusinessPrice,
},
{
field: 'count',
title: '数量',
formatter: 'formatNumber',
},
{
field: 'totalPrice',
title: '合计金额(元)',
formatter: 'formatAmount2',
},
];
}
/** 产品编辑表格的列定义 */
export function useProductEditTableColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'seq', title: '序号', minWidth: 50 },
{
field: 'productId',
title: '产品名称',
minWidth: 100,
slots: { default: 'productId' },
},
{
field: 'productNo',
title: '条码',
minWidth: 150,
},
{
field: 'productUnit',
title: '单位',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
},
},
{
field: 'productPrice',
title: '价格(元)',
minWidth: 100,
formatter: 'formatAmount2',
},
{
field: 'sellingPrice',
title: '售价(元)',
minWidth: 100,
slots: { default: 'sellingPrice' },
},
{
field: 'count',
title: '数量',
minWidth: 100,
slots: { default: 'count' },
},
{
field: 'totalPrice',
title: '合计',
minWidth: 100,
formatter: 'formatAmount2',
},
{
title: '操作',
width: 80,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,79 @@
<!-- 产品列表用于商机合同详情中展示它们关联的产品列表 -->
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmProductApi } from '#/api/crm/product';
import { ref } from 'vue';
import { erpPriceInputFormatter } from '@vben/utils';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getBusiness } from '#/api/crm/business';
import { getContract } from '#/api/crm/contract';
import { BizTypeEnum } from '#/api/crm/permission';
import { useDetailListColumns } from './data';
/** 组件入参 */
const props = defineProps<{
bizId: number;
bizType: BizTypeEnum;
}>();
/** 整单折扣 */
const discountPercent = ref(0);
/** 产品总金额 */
const totalProductPrice = ref(0);
/** 构建产品列表表格 */
const [Grid] = useVbenVxeGrid({
gridOptions: {
columns: useDetailListColumns(props.bizType === BizTypeEnum.CRM_BUSINESS),
height: 600,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_params) => {
const data =
props.bizType === BizTypeEnum.CRM_BUSINESS
? await getBusiness(props.bizId)
: await getContract(props.bizId);
discountPercent.value = data.discountPercent;
totalProductPrice.value = data.totalProductPrice;
return data.products;
},
},
},
toolbarConfig: {
refresh: true,
search: true,
},
keepSource: true,
rowConfig: {
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<CrmProductApi.Product>,
});
</script>
<template>
<div>
<Grid />
<div class="flex flex-col items-end justify-end">
<span class="ml-4 font-bold text-red-500">
{{ `产品总金额:${erpPriceInputFormatter(totalProductPrice)}` }}
</span>
<span class="font-bold text-red-500">
{{ `整单折扣:${erpPriceInputFormatter(discountPercent)}%` }}
</span>
<span class="font-bold text-red-500">
{{
`实际金额:${erpPriceInputFormatter(totalProductPrice * (1 - discountPercent / 100))}`
}}
</span>
</div>
</div>
</template>

View File

@@ -0,0 +1,193 @@
<script lang="ts" setup>
import type { CrmBusinessApi } from '#/api/crm/business';
import type { CrmContractApi } from '#/api/crm/contract';
import type { CrmProductApi } from '#/api/crm/product';
import { nextTick, onMounted, ref, watch } from 'vue';
import { erpPriceMultiply } from '@vben/utils';
import { InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { BizTypeEnum } from '#/api/crm/permission';
import { getProductSimpleList } from '#/api/crm/product';
import { $t } from '#/locales';
import { useProductEditTableColumns } from './data';
const props = defineProps<{
bizType: BizTypeEnum;
products?:
| CrmBusinessApi.BusinessProduct[]
| CrmContractApi.ContractProduct[];
}>();
const emit = defineEmits(['update:products']);
/** 表格内部数据 */
const tableData = ref<any[]>([]);
/** 添加产品行 */
function handleAdd() {
gridApi.grid.insertAt(null, -1);
}
/** 删除产品行 */
function handleDelete(row: CrmProductApi.Product) {
gridApi.grid.remove(row);
}
/** 切换产品时同步基础信息 */
function handleProductChange(productId: any, row: any) {
const product = productOptions.value.find((p) => p.id === productId);
if (!product) {
return;
}
row.productUnit = product.unit;
row.productNo = product.no;
row.productPrice = product.price;
row.sellingPrice = product.price;
row.count = 0;
row.totalPrice = 0;
handleUpdateValue(row);
}
/** 金额变动时重新计算合计 */
function handlePriceChange(row: any) {
row.totalPrice = erpPriceMultiply(row.sellingPrice, row.count) ?? 0;
handleUpdateValue(row);
}
/** 将最新数据写回并通知父组件 */
function handleUpdateValue(row: any) {
const index = tableData.value.findIndex((item) => item.id === row.id);
if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
row.businessPrice = row.sellingPrice;
} else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
row.contractPrice = row.sellingPrice;
}
if (index === -1) {
row.id = tableData.value.length + 1;
tableData.value.push(row);
} else {
tableData.value[index] = row;
}
emit('update:products', [...tableData.value]);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
editConfig: {
trigger: 'click',
mode: 'cell',
},
columns: useProductEditTableColumns(),
data: tableData.value,
border: true,
showOverflow: true,
autoResize: true,
keepSource: true,
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 监听外部传入的列数据 */
watch(
() => props.products,
async (products) => {
if (!products) {
return;
}
await nextTick();
tableData.value = products;
if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
tableData.value.forEach((item) => {
item.sellingPrice = item.businessPrice;
});
} else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
tableData.value.forEach((item) => {
item.sellingPrice = item.contractPrice;
});
}
await gridApi.grid.reloadData(tableData.value);
},
{
immediate: true,
},
);
/** 产品下拉选项 */
const productOptions = ref<CrmProductApi.Product[]>([]);
/** 初始化 */
onMounted(async () => {
productOptions.value = await getProductSimpleList();
});
</script>
<template>
<Grid class="w-full">
<template #productId="{ row }">
<Select
v-model:value="row.productId"
:options="productOptions"
:field-names="{ label: 'name', value: 'id' }"
class="w-full"
@change="handleProductChange($event, row)"
/>
</template>
<template #sellingPrice="{ row }">
<InputNumber
v-model:value="row.sellingPrice"
:min="0.001"
:precision="2"
@change="handlePriceChange(row)"
/>
</template>
<template #count="{ row }">
<InputNumber
v-model:value="row.count"
:min="0.001"
:precision="3"
@change="handlePriceChange(row)"
/>
</template>
<template #bottom>
<TableAction
class="mt-4 flex justify-center"
:actions="[
{
label: '添加产品',
type: 'default',
onClick: handleAdd,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'link',
danger: true,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,9 @@
import { defineAsyncComponent } from 'vue';
export const ProductDetailsList = defineAsyncComponent(
() => import('./detail-list.vue'),
);
export const ProductEditTable = defineAsyncComponent(
() => import('./edit-table.vue'),
);