feat:【mall 商城】快递模版的迁移(antd 40% 表单部分)

This commit is contained in:
YunaiV
2025-10-12 17:20:49 +08:00
parent 7eeb52c582
commit 43dc2d4446
4 changed files with 599 additions and 463 deletions

View File

@@ -1,7 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
@@ -99,6 +99,7 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入模板名称', placeholder: '请输入模板名称',
}, },
rules: 'required', rules: 'required',
formItemClass: 'col-span-1',
}, },
{ {
fieldName: 'chargeMode', fieldName: 'chargeMode',
@@ -109,7 +110,8 @@ export function useFormSchema(): VbenFormSchema[] {
buttonStyle: 'solid', buttonStyle: 'solid',
optionType: 'button', optionType: 'button',
}, },
rules: z.number().default(CommonStatusEnum.ENABLE), rules: z.number().default(1),
formItemClass: 'col-span-1',
}, },
{ {
fieldName: 'sort', fieldName: 'sort',
@@ -120,6 +122,25 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0, min: 0,
}, },
rules: 'required', rules: 'required',
formItemClass: 'col-span-1',
},
{
fieldName: 'charges',
label: '运费设置',
formItemClass: 'col-span-3',
dependencies: {
triggerFields: ['chargeMode'],
show: () => true,
},
},
{
fieldName: 'frees',
label: '包邮设置',
formItemClass: 'col-span-3',
dependencies: {
triggerFields: [''],
show: () => true,
},
}, },
]; ];
} }

View File

@@ -0,0 +1,247 @@
<script lang="ts" setup>
import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate';
import type { SystemAreaApi } from '#/api/system/area';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { InputNumber, TreeSelect } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAreaTree } from '#/api/system/area';
import { useChargesColumns } from '../data';
interface Props {
items?: MallDeliveryExpressTemplateApi.TemplateCharge[];
chargeMode?: number;
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
chargeMode: 1,
});
const emit = defineEmits(['update:items']);
const tableData = ref<any[]>([]);
const areaTree = ref<SystemAreaApi.Area[]>([]);
// TODO @AI待优化
// 根据计费方式设置列标题
const columnTitle = computed(() => {
const titleMap = {
1: { startCountTitle: '首件', extraCountTitle: '续件' },
2: { startCountTitle: '首件重量(kg)', extraCountTitle: '续件重量(kg)' },
3: { startCountTitle: '首件体积(m³)', extraCountTitle: '续件体积(m³)' },
};
return titleMap[props.chargeMode] || titleMap[1];
});
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useChargesColumns(),
data: tableData.value,
minHeight: 200,
autoResize: true,
border: true,
rowConfig: {
keyField: 'seq',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 监听外部传入的数据 */
watch(
() => props.items,
async (items) => {
if (!items) {
return;
}
tableData.value = [...items];
await nextTick();
await gridApi.grid.reloadData(tableData.value);
updateColumnsTitle();
},
{
immediate: true,
},
);
/** 监听计费方式变化 */
watch(
() => props.chargeMode,
() => {
updateColumnsTitle();
},
);
/** 更新列标题 */
function updateColumnsTitle() {
const columns = useChargesColumns();
const startCountCol = columns.find((col) => col.field === 'startCount');
const extraCountCol = columns.find((col) => col.field === 'extraCount');
if (startCountCol) startCountCol.title = columnTitle.value.startCountTitle;
if (extraCountCol) extraCountCol.title = columnTitle.value.extraCountTitle;
if (gridApi.grid) {
gridApi.grid.reloadColumn(columns);
}
}
/** 处理新增 */
function handleAdd() {
const newRow = {
areaIds: [],
startCount: undefined,
startPrice: undefined,
extraCount: undefined,
extraPrice: undefined,
};
tableData.value.push(newRow);
emit('update:items', [...tableData.value]);
}
/** 处理删除 */
function handleDelete(row: any) {
const index = tableData.value.findIndex((item) => item.seq === row.seq);
if (index !== -1) {
tableData.value.splice(index, 1);
}
emit('update:items', [...tableData.value]);
}
/** 处理行数据变更 */
function handleRowChange(row: any) {
const index = tableData.value.findIndex((item) => item.seq === row.seq);
if (index === -1) {
tableData.value.push(row);
} else {
tableData.value[index] = row;
}
emit('update:items', [...tableData.value]);
}
/** 表单校验 */
function validate() {
for (let i = 0; i < tableData.value.length; i++) {
const item = tableData.value[i];
if (!item.areaIds || item.areaIds.length === 0) {
throw new Error(`运费设置第 ${i + 1} 行:区域不能为空`);
}
if (!item.startCount || item.startCount <= 0) {
throw new Error(
`运费设置第 ${i + 1} 行:${columnTitle.value.startCountTitle}必须大于 0`,
);
}
if (!item.startPrice || item.startPrice <= 0) {
throw new Error(`运费设置第 ${i + 1}运费必须大于0`);
}
if (!item.extraCount || item.extraCount <= 0) {
throw new Error(
`运费设置第 ${i + 1} 行:${columnTitle.value.extraCountTitle}必须大于 0`,
);
}
if (!item.extraPrice || item.extraPrice <= 0) {
throw new Error(`运费设置第 ${i + 1} 行:续费必须大于 0`);
}
}
}
defineExpose({
validate,
});
/** 初始化 */
onMounted(async () => {
areaTree.value = await getAreaTree();
});
</script>
<template>
<Grid class="w-full">
<template #areaIds="{ row }">
<TreeSelect
v-model:value="row.areaIds"
:tree-data="areaTree"
:field-names="{
label: 'name',
value: 'id',
children: 'children',
}"
placeholder="请选择地区"
class="w-full"
multiple
tree-checkable
:show-checked-strategy="TreeSelect.SHOW_CHILD"
:max-tag-count="5"
@change="handleRowChange(row)"
/>
</template>
<template #startCount="{ row }">
<InputNumber
v-model:value="row.startCount"
:min="1"
@change="handleRowChange(row)"
/>
</template>
<template #startPrice="{ row }">
<InputNumber
v-model:value="row.startPrice"
:min="0"
:precision="2"
@change="handleRowChange(row)"
/>
</template>
<template #extraCount="{ row }">
<InputNumber
v-model:value="row.extraCount"
:min="1"
@change="handleRowChange(row)"
/>
</template>
<template #extraPrice="{ row }">
<InputNumber
v-model:value="row.extraPrice"
:min="0"
:precision="2"
@change="handleRowChange(row)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '删除',
type: 'link',
danger: true,
popConfirm: {
title: '确认删除该区域吗?',
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
<template #bottom>
<TableAction
class="mt-2 flex justify-center"
:actions="[
{
label: '添加计费区域',
type: 'default',
onClick: handleAdd,
},
]"
/>
</template>
</Grid>
</template>

View File

@@ -1,67 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate'; import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate';
import { computed, nextTick, onMounted, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { cloneDeep } from '@vben/utils'; import { cloneDeep } from '@vben/utils';
import { import { message } from 'ant-design-vue';
Cascader,
Form,
FormItem,
Input,
InputNumber,
message,
RadioGroup,
} from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenForm } from '#/adapter/form';
import { import {
createDeliveryExpressTemplate, createDeliveryExpressTemplate,
getDeliveryExpressTemplate, getDeliveryExpressTemplate,
updateDeliveryExpressTemplate, updateDeliveryExpressTemplate,
} from '#/api/mall/trade/delivery/expressTemplate'; } from '#/api/mall/trade/delivery/expressTemplate';
import { getAreaTree } from '#/api/system/area';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useChargesColumns, useFreesColumns } from '../data'; import { useFormSchema } from '../data';
import ChargeItemForm from './charge-item-form.vue';
import FreeItemForm from './free-item-form.vue';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formRef = ref(); const formData = ref<MallDeliveryExpressTemplateApi.ExpressTemplate>();
const chargesGridRef = ref(); const chargeItemFormRef = ref<InstanceType<typeof ChargeItemForm>>();
const freesGridRef = ref(); const freeItemFormRef = ref<InstanceType<typeof FreeItemForm>>();
const formData = ref<any>({
id: undefined,
name: '',
chargeMode: 1,
sort: 0,
charges: [],
frees: [],
});
const areaTree = ref([]);
const columnTitleMap = new Map();
const columnTitle = ref({
startCountTitle: '首件',
extraCountTitle: '续件',
freeCountTitle: '包邮件数',
});
const chargeModeOptions = getDictOptions(
DICT_TYPE.EXPRESS_CHARGE_MODE,
'number',
);
const cascaderProps = {
multiple: true,
checkStrictly: false,
label: 'name',
value: 'id',
children: 'children',
};
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
@@ -69,68 +31,73 @@ const getTitle = computed(() => {
: $t('ui.actionTitle.create', ['快递模板']); : $t('ui.actionTitle.create', ['快递模板']);
}); });
const formRules = { const [Form, formApi] = useVbenForm({
name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }], commonConfig: {
chargeMode: [ componentProps: {
{ required: true, message: '配送计费方式不能为空', trigger: 'change' }, class: 'w-full',
], },
sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }], labelWidth: 120,
},
wrapperClass: 'grid-cols-1',
layout: 'vertical',
schema: useFormSchema(),
showDefaultActions: false,
});
/** 更新运费设置 */
const handleUpdateCharges = (charges: any[]) => {
formData.value =
formApi.getValues() as MallDeliveryExpressTemplateApi.ExpressTemplate;
formData.value.charges = charges;
formApi.setValues({
charges,
});
}; };
// 运费设置表格 /** 更新包邮设置 */
const [ChargesGrid, chargesGridApi] = useVbenVxeGrid({ const handleUpdateFrees = (frees: any[]) => {
gridOptions: { formData.value =
columns: useChargesColumns(), formApi.getValues() as MallDeliveryExpressTemplateApi.ExpressTemplate;
data: formData.value.charges, formData.value.frees = frees;
minHeight: 200, formApi.setValues({
autoResize: true, frees,
border: true, });
rowConfig: { };
keyField: 'seq',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
// 包邮设置表格
const [FreesGrid, freesGridApi] = useVbenVxeGrid({
gridOptions: {
columns: useFreesColumns(),
data: formData.value.frees,
minHeight: 200,
autoResize: true,
border: true,
rowConfig: {
keyField: 'seq',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 创建或更新快递模板 */
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// 验证子表单
const chargeFormInstance = Array.isArray(chargeItemFormRef.value)
? chargeItemFormRef.value[0]
: chargeItemFormRef.value;
const freeFormInstance = Array.isArray(freeItemFormRef.value)
? freeItemFormRef.value[0]
: freeItemFormRef.value;
try { try {
await formRef.value?.validate(); if (chargeFormInstance) {
validateTables(); chargeFormInstance.validate();
} catch { }
if (freeFormInstance) {
freeFormInstance.validate();
}
} catch (error: any) {
message.error(error.message || '子表单验证失败');
return; return;
} }
modalApi.lock(); modalApi.lock();
// 提交表单
const data = cloneDeep(
await formApi.getValues(),
) as MallDeliveryExpressTemplateApi.ExpressTemplate;
try { try {
const data = cloneDeep(formData.value);
// 前端价格以元展示,提交到后端用分计算 // 前端价格以元展示,提交到后端用分计算
data.charges?.forEach((item: any) => { data.charges?.forEach((item: any) => {
item.startPrice = Math.round(item.startPrice * 100); item.startPrice = Math.round(item.startPrice * 100);
@@ -144,6 +111,7 @@ const [Modal, modalApi] = useVbenModal({
? updateDeliveryExpressTemplate(data) ? updateDeliveryExpressTemplate(data)
: createDeliveryExpressTemplate(data)); : createDeliveryExpressTemplate(data));
// 关闭并提示
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
@@ -153,21 +121,18 @@ const [Modal, modalApi] = useVbenModal({
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
resetFormData(); formData.value = undefined;
return; return;
} }
// 加载数据
const data = const data =
modalApi.getData<MallDeliveryExpressTemplateApi.ExpressTemplate>(); modalApi.getData<MallDeliveryExpressTemplateApi.ExpressTemplate>();
if (!data || !data.id) { if (!data || !data.id) {
resetFormData(); resetFormData();
initDefaultData(); await formApi.setValues(formData.value);
} else { } else {
await loadFormData(data.id); await loadFormData(data.id);
} }
await nextTick();
updateTables();
}, },
}); });
@@ -183,27 +148,12 @@ function resetFormData() {
}; };
} }
// 初始化默认数据
function initDefaultData() {
formData.value.charges = [
{
seq: Date.now(),
areaIds: [1],
startCount: 2,
startPrice: 5,
extraCount: 5,
extraPrice: 10,
},
];
}
// 加载表单数据 // 加载表单数据
async function loadFormData(id: number) { async function loadFormData(id: number) {
modalApi.lock(); modalApi.lock();
try { try {
const data = await getDeliveryExpressTemplate(id); const data = await getDeliveryExpressTemplate(id);
formData.value = data; formData.value = data;
columnTitle.value = columnTitleMap.get(formData.value.chargeMode);
// 前端价格以元展示 // 前端价格以元展示
formData.value.charges?.forEach((item: any, index: number) => { formData.value.charges?.forEach((item: any, index: number) => {
@@ -215,358 +165,34 @@ async function loadFormData(id: number) {
item.seq = item.seq || Date.now() + index; item.seq = item.seq || Date.now() + index;
item.freePrice = item.freePrice / 100; item.freePrice = item.freePrice / 100;
}); });
// 设置到 values
await formApi.setValues(formData.value);
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
} }
} }
// 更新表格数据
async function updateTables() {
await nextTick();
if (chargesGridApi.grid) {
await chargesGridApi.grid.reloadData(formData.value.charges || []);
}
if (freesGridApi.grid) {
await freesGridApi.grid.reloadData(formData.value.frees || []);
}
}
// 计费方式改变
function handleChargeModeChange(value: number) {
columnTitle.value = columnTitleMap.get(value);
updateChargesColumns();
}
// 更新运费表格列标题
function updateChargesColumns() {
const columns = useChargesColumns();
// 根据计费方式更新列标题
const startCountCol = columns.find((col) => col.field === 'startCount');
const extraCountCol = columns.find((col) => col.field === 'extraCount');
if (startCountCol) startCountCol.title = columnTitle.value.startCountTitle;
if (extraCountCol) extraCountCol.title = columnTitle.value.extraCountTitle;
chargesGridApi.grid.reloadColumn(columns);
}
// 更新包邮表格列标题
function updateFreesColumns() {
const columns = useFreesColumns();
const freeCountCol = columns.find((col) => col.field === 'freeCount');
if (freeCountCol) freeCountCol.title = columnTitle.value.freeCountTitle;
freesGridApi.grid.reloadColumn(columns);
}
// 添加计费区域
function addChargeArea() {
const newRow = {
seq: Date.now(),
areaIds: [],
startCount: 1,
startPrice: 1,
extraCount: 1,
extraPrice: 1,
};
formData.value.charges.push(newRow);
chargesGridApi.grid.reloadData(formData.value.charges);
}
// 删除计费区域
function deleteChargeArea(row: any) {
const index = formData.value.charges.findIndex(
(item: any) => item.seq === row.seq,
);
if (index !== -1) {
formData.value.charges.splice(index, 1);
chargesGridApi.grid.reloadData(formData.value.charges);
}
}
// 添加包邮区域
function addFreeArea() {
const newRow = {
seq: Date.now(),
areaIds: [],
freeCount: 1,
freePrice: 1,
};
formData.value.frees.push(newRow);
freesGridApi.grid.reloadData(formData.value.frees);
}
// 删除包邮区域
function deleteFreeArea(row: any) {
const index = formData.value.frees.findIndex(
(item: any) => item.seq === row.seq,
);
if (index !== -1) {
formData.value.frees.splice(index, 1);
freesGridApi.grid.reloadData(formData.value.frees);
}
}
// 处理运费区域变更
function handleChargeAreaChange(areaIds: any[], row: any) {
row.areaIds = areaIds;
}
// 处理包邮区域变更
function handleFreeAreaChange(areaIds: any[], row: any) {
row.areaIds = areaIds;
}
// 处理数值变更
function handleChargeValueChange(field: string, value: any, row: any) {
row[field] = value;
}
function handleFreeValueChange(field: string, value: any, row: any) {
row[field] = value;
}
// 表格验证
function validateTables() {
for (let i = 0; i < formData.value.charges.length; i++) {
const item = formData.value.charges[i];
if (!item.areaIds || item.areaIds.length === 0) {
throw new Error(`运费设置第 ${i + 1} 行:区域不能为空`);
}
if (!item.startCount || item.startCount <= 0) {
throw new Error(`运费设置第 ${i + 1}首件数必须大于0`);
}
if (!item.startPrice || item.startPrice <= 0) {
throw new Error(`运费设置第 ${i + 1}运费必须大于0`);
}
}
for (let i = 0; i < formData.value.frees.length; i++) {
const item = formData.value.frees[i];
if (!item.areaIds || item.areaIds.length === 0) {
throw new Error(`包邮设置第 ${i + 1} 行:区域不能为空`);
}
if (!item.freeCount || item.freeCount <= 0) {
throw new Error(`包邮设置第 ${i + 1}包邮件数必须大于0`);
}
if (!item.freePrice || item.freePrice <= 0) {
throw new Error(`包邮设置第 ${i + 1}包邮金额必须大于0`);
}
}
}
// 初始化数据
async function initData() {
// 表头标题和计费方式的映射
columnTitleMap.set(1, {
startCountTitle: '首件',
extraCountTitle: '续件',
freeCountTitle: '包邮件数',
});
columnTitleMap.set(2, {
startCountTitle: '首件重量(kg)',
extraCountTitle: '续件重量(kg)',
freeCountTitle: '包邮重量(kg)',
});
columnTitleMap.set(3, {
startCountTitle: '首件体积(m³)',
extraCountTitle: '续件体积(m³)',
freeCountTitle: '包邮体积(m³)',
});
// 加载区域数据
try {
areaTree.value = await getAreaTree();
} catch (error) {
console.error('加载区域数据失败:', error);
}
}
onMounted(() => {
initData();
});
// 监听计费方式变化
watch(
() => formData.value.chargeMode,
() => {
updateChargesColumns();
updateFreesColumns();
},
);
</script> </script>
<template> <template>
<Modal class="w-[80%]" :title="getTitle"> <Modal class="w-[80%]" :title="getTitle">
<Form <Form class="mx-3">
ref="formRef" <template #charges>
:model="formData" <ChargeItemForm
:rules="formRules" ref="chargeItemFormRef"
:label-col="{ span: 4 }" :items="formData?.charges ?? []"
:wrapper-col="{ span: 20 }" :charge-mode="formData?.chargeMode ?? 1"
class="mx-4" @update:items="handleUpdateCharges"
>
<FormItem label="模板名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入模板名称" />
</FormItem>
<FormItem label="计费方式" name="chargeMode">
<RadioGroup
v-model:value="formData.chargeMode"
:options="chargeModeOptions"
@change="handleChargeModeChange"
/> />
</FormItem> </template>
<template #frees>
<FormItem label="运费设置"> <FreeItemForm
<ChargesGrid ref="chargesGridRef"> ref="freeItemFormRef"
<template #areaIds="{ row }"> :items="formData?.frees ?? []"
<Cascader :charge-mode="formData?.chargeMode ?? 1"
v-model:value="row.areaIds" @update:items="handleUpdateFrees"
:options="areaTree"
:field-names="cascaderProps"
placeholder="请选择地区"
class="w-full"
multiple
show-checked-strategy="SHOW_CHILD"
@change="handleChargeAreaChange($event, row)"
/>
</template>
<template #startCount="{ row }">
<InputNumber
v-model:value="row.startCount"
:min="1"
class="w-full"
@change="handleChargeValueChange('startCount', $event, row)"
/>
</template>
<template #startPrice="{ row }">
<InputNumber
v-model:value="row.startPrice"
:min="0"
:precision="2"
class="w-full"
@change="handleChargeValueChange('startPrice', $event, row)"
/>
</template>
<template #extraCount="{ row }">
<InputNumber
v-model:value="row.extraCount"
:min="1"
class="w-full"
@change="handleChargeValueChange('extraCount', $event, row)"
/>
</template>
<template #extraPrice="{ row }">
<InputNumber
v-model:value="row.extraPrice"
:min="0"
:precision="2"
class="w-full"
@change="handleChargeValueChange('extraPrice', $event, row)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '删除',
type: 'link',
danger: true,
popConfirm: {
title: '确认删除该区域吗?',
confirm: deleteChargeArea.bind(null, row),
},
},
]"
/>
</template>
<template #bottom>
<TableAction
class="mt-2 flex justify-center"
:actions="[
{
label: '添加计费区域',
type: 'default',
onClick: addChargeArea,
},
]"
/>
</template>
</ChargesGrid>
</FormItem>
<FormItem label="包邮设置">
<FreesGrid ref="freesGridRef">
<template #areaIds="{ row }">
<Cascader
v-model:value="row.areaIds"
:options="areaTree"
:field-names="cascaderProps"
placeholder="请选择地区"
class="w-full"
multiple
show-checked-strategy="SHOW_CHILD"
@change="handleFreeAreaChange($event, row)"
/>
</template>
<template #freeCount="{ row }">
<InputNumber
v-model:value="row.freeCount"
:min="1"
class="w-full"
@change="handleFreeValueChange('freeCount', $event, row)"
/>
</template>
<template #freePrice="{ row }">
<InputNumber
v-model:value="row.freePrice"
:min="0"
:precision="2"
class="w-full"
@change="handleFreeValueChange('freePrice', $event, row)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '删除',
type: 'link',
danger: true,
popConfirm: {
title: '确认删除该区域吗?',
confirm: deleteFreeArea.bind(null, row),
},
},
]"
/>
</template>
<template #bottom>
<TableAction
class="mt-2 flex justify-center"
:actions="[
{
label: '添加包邮区域',
type: 'default',
onClick: addFreeArea,
},
]"
/>
</template>
</FreesGrid>
</FormItem>
<FormItem label="排序" name="sort">
<InputNumber
v-model:value="formData.sort"
:min="0"
placeholder="请输入显示顺序"
/> />
</FormItem> </template>
</Form> </Form>
</Modal> </Modal>
</template> </template>

View File

@@ -0,0 +1,242 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { Cascader, InputNumber } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAreaTree } from '#/api/system/area';
import { useFreesColumns } from '../data';
interface Props {
items?: any[];
disabled?: boolean;
chargeMode?: number;
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
disabled: false,
chargeMode: 1,
});
const emit = defineEmits(['update:items']);
const tableData = ref<any[]>([]);
const areaTree = ref([]);
// 根据计费方式设置列标题
const columnTitle = computed(() => {
const titleMap = {
1: { freeCountTitle: '包邮件数' },
2: { freeCountTitle: '包邮重量(kg)' },
3: { freeCountTitle: '包邮体积(m³)' },
};
return titleMap[props.chargeMode] || titleMap[1];
});
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useFreesColumns(),
data: tableData.value,
minHeight: 200,
autoResize: true,
border: true,
rowConfig: {
keyField: 'seq',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 监听外部传入的数据 */
watch(
() => props.items,
async (items) => {
if (!items) {
return;
}
tableData.value = [...items];
await nextTick();
await gridApi.grid.reloadData(tableData.value);
updateColumnsTitle();
},
{
immediate: true,
},
);
/** 监听计费方式变化 */
watch(
() => props.chargeMode,
() => {
updateColumnsTitle();
},
);
// TODO @AI待优化。
/** 更新列标题 */
function updateColumnsTitle() {
const columns = useFreesColumns();
const freeCountCol = columns.find((col) => col.field === 'freeCount');
if (freeCountCol) freeCountCol.title = columnTitle.value.freeCountTitle;
if (gridApi.grid) {
gridApi.grid.reloadColumn(columns);
}
}
/** 处理新增 */
function handleAdd() {
const newRow = {
seq: Date.now(),
areaIds: [],
freeCount: 1,
freePrice: 1,
};
tableData.value.push(newRow);
emit('update:items', [...tableData.value]);
}
/** 处理删除 */
function handleDelete(row: any) {
const index = tableData.value.findIndex((item) => item.seq === row.seq);
if (index !== -1) {
tableData.value.splice(index, 1);
}
emit('update:items', [...tableData.value]);
}
/** 处理区域变更 */
function handleAreaChange(areaIds: any[], row: any) {
row.areaIds = areaIds;
handleRowChange(row);
}
/** 处理数值变更 */
function handleValueChange(field: string, value: any, row: any) {
row[field] = value;
handleRowChange(row);
}
/** 处理行数据变更 */
function handleRowChange(row: any) {
const index = tableData.value.findIndex((item) => item.seq === row.seq);
if (index === -1) {
tableData.value.push(row);
} else {
tableData.value[index] = row;
}
emit('update:items', [...tableData.value]);
}
/** 表单校验 */
function validate() {
for (let i = 0; i < tableData.value.length; i++) {
const item = tableData.value[i];
if (!item.areaIds || item.areaIds.length === 0) {
throw new Error(`包邮设置第 ${i + 1} 行:区域不能为空`);
}
if (!item.freeCount || item.freeCount <= 0) {
throw new Error(
`包邮设置第 ${i + 1} 行:${columnTitle.value.freeCountTitle}必须大于0`,
);
}
if (!item.freePrice || item.freePrice <= 0) {
throw new Error(`包邮设置第 ${i + 1}包邮金额必须大于0`);
}
}
}
defineExpose({
validate,
});
/** 初始化 */
onMounted(async () => {
try {
areaTree.value = await getAreaTree();
} catch (error) {
console.error('加载区域数据失败:', error);
}
});
</script>
<template>
<Grid class="w-full">
<template #areaIds="{ row }">
<Cascader
v-model:value="row.areaIds"
:options="areaTree"
:field-names="{
label: 'name',
value: 'id',
children: 'children',
}"
placeholder="请选择地区"
class="w-full"
multiple
show-checked-strategy="SHOW_CHILD"
:disabled="disabled"
@change="handleAreaChange($event, row)"
/>
</template>
<template #freeCount="{ row }">
<InputNumber
v-model:value="row.freeCount"
:min="1"
class="w-full"
:disabled="disabled"
@change="handleValueChange('freeCount', $event, row)"
/>
</template>
<template #freePrice="{ row }">
<InputNumber
v-model:value="row.freePrice"
:min="0"
:precision="2"
class="w-full"
:disabled="disabled"
@change="handleValueChange('freePrice', $event, row)"
/>
</template>
<template #actions="{ row }">
<TableAction
v-if="!disabled"
:actions="[
{
label: '删除',
type: 'link',
danger: true,
popConfirm: {
title: '确认删除该区域吗?',
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
<template #bottom>
<TableAction
v-if="!disabled"
class="mt-2 flex justify-center"
:actions="[
{
label: '添加包邮区域',
type: 'default',
onClick: handleAdd,
},
]"
/>
</template>
</Grid>
</template>