feat:【mall 商城】快递模版的迁移(antd 100%)

This commit is contained in:
YunaiV
2025-10-12 19:29:55 +08:00
parent 4270916ce4
commit 31a583419a
4 changed files with 141 additions and 205 deletions

View File

@@ -6,10 +6,32 @@ import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
/** 计费方式列标题映射 */
export const CHARGE_MODE_TITLE_MAP: Record<
number,
{
extraCountTitle: string;
startCountTitle: string;
}
> = {
1: { startCountTitle: '首件', extraCountTitle: '续件' },
2: { startCountTitle: '首件重量(kg)', extraCountTitle: '续件重量(kg)' },
3: { startCountTitle: '首件体积(m³)', extraCountTitle: '续件体积(m³)' },
};
/** 包邮方式列标题映射 */
export const FREE_MODE_TITLE_MAP: Record<number, { freeCountTitle: string }> = {
1: { freeCountTitle: '包邮件数' },
2: { freeCountTitle: '包邮重量(kg)' },
3: { freeCountTitle: '包邮体积(m³)' },
};
/** 运费设置表格列 */
export function useChargesColumns(): VxeTableGridOptions['columns'] {
export function useChargesColumns(
chargeMode = 1,
): VxeTableGridOptions['columns'] {
const chargeTitleMap = CHARGE_MODE_TITLE_MAP[chargeMode];
return [
{ type: 'seq', title: '序号', width: 50 },
{
field: 'areaIds',
title: '区域',
@@ -18,7 +40,7 @@ export function useChargesColumns(): VxeTableGridOptions['columns'] {
},
{
field: 'startCount',
title: '首件数',
title: chargeTitleMap?.startCountTitle,
width: 120,
slots: { default: 'startCount' },
},
@@ -30,7 +52,7 @@ export function useChargesColumns(): VxeTableGridOptions['columns'] {
},
{
field: 'extraCount',
title: '续件数',
title: chargeTitleMap?.extraCountTitle,
width: 120,
slots: { default: 'extraCount' },
},
@@ -50,9 +72,11 @@ export function useChargesColumns(): VxeTableGridOptions['columns'] {
}
/** 包邮设置表格列 */
export function useFreesColumns(): VxeTableGridOptions['columns'] {
export function useFreesColumns(
chargeMode = 1,
): VxeTableGridOptions['columns'] {
const freeTitleMap = FREE_MODE_TITLE_MAP[chargeMode];
return [
{ type: 'seq', title: '序号', width: 50 },
{
field: 'areaIds',
title: '区域',
@@ -61,7 +85,7 @@ export function useFreesColumns(): VxeTableGridOptions['columns'] {
},
{
field: 'freeCount',
title: '包邮件数',
title: freeTitleMap?.freeCountTitle,
width: 120,
slots: { default: 'freeCount' },
},
@@ -99,7 +123,6 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入模板名称',
},
rules: 'required',
formItemClass: 'col-span-1',
},
{
fieldName: 'chargeMode',
@@ -111,7 +134,6 @@ export function useFormSchema(): VbenFormSchema[] {
optionType: 'button',
},
rules: z.number().default(1),
formItemClass: 'col-span-1',
},
{
fieldName: 'sort',
@@ -122,25 +144,18 @@ export function useFormSchema(): VbenFormSchema[] {
min: 0,
},
rules: 'required',
formItemClass: 'col-span-1',
},
{
fieldName: 'charges',
label: '运费设置',
component: 'Input',
formItemClass: 'col-span-3',
dependencies: {
triggerFields: ['chargeMode'],
show: () => true,
},
},
{
fieldName: 'frees',
label: '包邮设置',
component: 'Input',
formItemClass: 'col-span-3',
dependencies: {
triggerFields: [''],
show: () => true,
},
},
];
}

View File

@@ -2,45 +2,35 @@
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 { computed, nextTick, 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';
import { CHARGE_MODE_TITLE_MAP, useChargesColumns } from '../data';
interface Props {
items?: MallDeliveryExpressTemplateApi.TemplateCharge[];
chargeMode?: number;
areaTree?: SystemAreaApi.Area[];
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
chargeMode: 1,
areaTree: () => [],
});
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 columnTitle = computed(() => CHARGE_MODE_TITLE_MAP[props.chargeMode]);
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useChargesColumns(),
columns: useChargesColumns(props.chargeMode),
data: tableData.value,
minHeight: 200,
autoResize: true,
@@ -68,7 +58,6 @@ watch(
tableData.value = [...items];
await nextTick();
await gridApi.grid.reloadData(tableData.value);
updateColumnsTitle();
},
{
immediate: true,
@@ -79,24 +68,13 @@ watch(
watch(
() => props.chargeMode,
() => {
updateColumnsTitle();
const columns = useChargesColumns(props.chargeMode);
if (gridApi.grid && columns) {
gridApi.grid.reloadColumn(columns);
}
},
);
/** 更新列标题 */
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 = {
@@ -139,7 +117,7 @@ function validate() {
}
if (!item.startCount || item.startCount <= 0) {
throw new Error(
`运费设置第 ${i + 1} 行:${columnTitle.value.startCountTitle}必须大于 0`,
`运费设置第 ${i + 1} 行:${columnTitle.value?.startCountTitle}必须大于 0`,
);
}
if (!item.startPrice || item.startPrice <= 0) {
@@ -147,7 +125,7 @@ function validate() {
}
if (!item.extraCount || item.extraCount <= 0) {
throw new Error(
`运费设置第 ${i + 1} 行:${columnTitle.value.extraCountTitle}必须大于 0`,
`运费设置第 ${i + 1} 行:${columnTitle.value?.extraCountTitle}必须大于 0`,
);
}
if (!item.extraPrice || item.extraPrice <= 0) {
@@ -159,16 +137,12 @@ function validate() {
defineExpose({
validate,
});
/** 初始化 */
onMounted(async () => {
areaTree.value = await getAreaTree();
});
</script>
<template>
<Grid class="w-full">
<template #areaIds="{ row }">
<!-- TODO 芋艿可优化使用 Cascade不过貌似 antd multiple 貌似有 bug -->
<TreeSelect
v-model:value="row.areaIds"
:tree-data="areaTree"
@@ -181,8 +155,7 @@ onMounted(async () => {
class="w-full"
multiple
tree-checkable
:show-checked-strategy="TreeSelect.SHOW_CHILD"
:max-tag-count="5"
:max-tag-count="1"
@change="handleRowChange(row)"
/>
</template>

View File

@@ -1,10 +1,11 @@
<script lang="ts" setup>
import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate';
import type { SystemAreaApi } from '#/api/system/area';
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { cloneDeep } from '@vben/utils';
import { cloneDeep, fenToYuan, yuanToFen } from '@vben/utils';
import { message } from 'ant-design-vue';
@@ -14,6 +15,7 @@ import {
getDeliveryExpressTemplate,
updateDeliveryExpressTemplate,
} from '#/api/mall/trade/delivery/expressTemplate';
import { getAreaTree } from '#/api/system/area';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
@@ -24,6 +26,7 @@ const emit = defineEmits(['success']);
const formData = ref<MallDeliveryExpressTemplateApi.ExpressTemplate>();
const chargeItemFormRef = ref<InstanceType<typeof ChargeItemForm>>();
const freeItemFormRef = ref<InstanceType<typeof FreeItemForm>>();
const areaTree = ref<SystemAreaApi.Area[]>([]);
const getTitle = computed(() => {
return formData.value?.id
@@ -38,28 +41,38 @@ const [Form, formApi] = useVbenForm({
},
labelWidth: 120,
},
wrapperClass: 'grid-cols-1',
wrapperClass: 'grid-cols-3',
layout: 'vertical',
schema: useFormSchema(),
showDefaultActions: false,
handleValuesChange: (values, changedFields) => {
// 目的:触发子表单的 columns 变化
if (changedFields.includes('chargeMode')) {
formData.value!.chargeMode = values.chargeMode;
}
},
});
/** 更新运费设置 */
const handleUpdateCharges = (charges: any[]) => {
const handleUpdateCharges = async (
charges: MallDeliveryExpressTemplateApi.TemplateCharge[],
) => {
formData.value =
formApi.getValues() as MallDeliveryExpressTemplateApi.ExpressTemplate;
await formApi.getValues<MallDeliveryExpressTemplateApi.ExpressTemplate>();
formData.value.charges = charges;
formApi.setValues({
await formApi.setValues({
charges,
});
};
/** 更新包邮设置 */
const handleUpdateFrees = (frees: any[]) => {
const handleUpdateFrees = async (
frees: MallDeliveryExpressTemplateApi.TemplateFree[],
) => {
formData.value =
formApi.getValues() as MallDeliveryExpressTemplateApi.ExpressTemplate;
await formApi.getValues<MallDeliveryExpressTemplateApi.ExpressTemplate>();
formData.value.frees = frees;
formApi.setValues({
await formApi.setValues({
frees,
});
};
@@ -71,22 +84,15 @@ const [Modal, modalApi] = useVbenModal({
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 {
if (chargeFormInstance) {
chargeFormInstance.validate();
}
if (freeFormInstance) {
freeFormInstance.validate();
}
chargeFormInstance.validate();
freeFormInstance.validate();
} catch (error: any) {
message.error(error.message || '子表单验证失败');
return;
@@ -98,19 +104,21 @@ const [Modal, modalApi] = useVbenModal({
await formApi.getValues(),
) as MallDeliveryExpressTemplateApi.ExpressTemplate;
try {
// 前端价格以元展示,提交到后端用分计算
data.charges?.forEach((item: any) => {
item.startPrice = Math.round(item.startPrice * 100);
item.extraPrice = Math.round(item.extraPrice * 100);
});
data.frees?.forEach((item: any) => {
item.freePrice = Math.round(item.freePrice * 100);
});
// 转换金额单位
data.charges?.forEach(
(item: MallDeliveryExpressTemplateApi.TemplateCharge) => {
item.startPrice = yuanToFen(item.startPrice);
item.extraPrice = yuanToFen(item.extraPrice);
},
);
data.frees?.forEach(
(item: MallDeliveryExpressTemplateApi.TemplateFree) => {
item.freePrice = yuanToFen(item.freePrice);
},
);
await (formData.value?.id
? updateDeliveryExpressTemplate(data)
: createDeliveryExpressTemplate(data));
// 关闭并提示
await modalApi.close();
emit('success');
@@ -128,68 +136,55 @@ const [Modal, modalApi] = useVbenModal({
const data =
modalApi.getData<MallDeliveryExpressTemplateApi.ExpressTemplate>();
if (!data || !data.id) {
resetFormData();
return;
}
modalApi.lock();
try {
formData.value = await getDeliveryExpressTemplate(data.id);
// 转换金额单位
formData.value.charges?.forEach(
(item: MallDeliveryExpressTemplateApi.TemplateCharge) => {
item.startPrice = Number.parseFloat(fenToYuan(item.startPrice));
item.extraPrice = Number.parseFloat(fenToYuan(item.extraPrice));
},
);
formData.value.frees?.forEach(
(item: MallDeliveryExpressTemplateApi.TemplateFree) => {
item.freePrice = Number.parseFloat(fenToYuan(item.freePrice));
},
);
// 设置到 values
await formApi.setValues(formData.value);
} else {
await loadFormData(data.id);
} finally {
modalApi.unlock();
}
},
});
// 重置表单数据
function resetFormData() {
formData.value = {
id: undefined,
name: '',
chargeMode: 1,
sort: 0,
charges: [],
frees: [],
};
}
// 加载表单数据
async function loadFormData(id: number) {
modalApi.lock();
try {
const data = await getDeliveryExpressTemplate(id);
formData.value = data;
// 前端价格以元展示
formData.value.charges?.forEach((item: any, index: number) => {
item.seq = item.seq || Date.now() + index;
item.startPrice = item.startPrice / 100;
item.extraPrice = item.extraPrice / 100;
});
formData.value.frees?.forEach((item: any, index: number) => {
item.seq = item.seq || Date.now() + index;
item.freePrice = item.freePrice / 100;
});
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
/** 初始化 */
onMounted(async () => {
areaTree.value = await getAreaTree();
});
</script>
<template>
<Modal class="w-[80%]" :title="getTitle">
<Modal class="w-1/2" :title="getTitle">
<Form class="mx-3">
<template #charges>
<ChargeItemForm
ref="chargeItemFormRef"
:items="formData?.charges ?? []"
:charge-mode="formData?.chargeMode ?? 1"
:items="formData?.charges"
:charge-mode="formData?.chargeMode"
:area-tree="areaTree"
@update:items="handleUpdateCharges"
/>
</template>
<template #frees>
<FreeItemForm
ref="freeItemFormRef"
:items="formData?.frees ?? []"
:charge-mode="formData?.chargeMode ?? 1"
:items="formData?.frees"
:charge-mode="formData?.chargeMode"
:area-tree="areaTree"
@update:items="handleUpdateFrees"
/>
</template>

View File

@@ -1,44 +1,36 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate';
import type { SystemAreaApi } from '#/api/system/area';
import { Cascader, InputNumber } from 'ant-design-vue';
import { computed, nextTick, 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 { useFreesColumns } from '../data';
import { FREE_MODE_TITLE_MAP, useFreesColumns } from '../data';
interface Props {
items?: any[];
disabled?: boolean;
items?: MallDeliveryExpressTemplateApi.TemplateFree[];
chargeMode?: number;
areaTree?: SystemAreaApi.Area[];
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
disabled: false,
chargeMode: 1,
areaTree: () => [],
});
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 columnTitle = computed(() => FREE_MODE_TITLE_MAP[props.chargeMode]);
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useFreesColumns(),
columns: useFreesColumns(props.chargeMode),
data: tableData.value,
minHeight: 200,
autoResize: true,
@@ -66,7 +58,6 @@ watch(
tableData.value = [...items];
await nextTick();
await gridApi.grid.reloadData(tableData.value);
updateColumnsTitle();
},
{
immediate: true,
@@ -77,30 +68,19 @@ watch(
watch(
() => props.chargeMode,
() => {
updateColumnsTitle();
const columns = useFreesColumns(props.chargeMode);
if (gridApi.grid && columns) {
gridApi.grid.reloadColumn(columns);
}
},
);
// 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,
freeCount: undefined,
freePrice: undefined,
};
tableData.value.push(newRow);
emit('update:items', [...tableData.value]);
@@ -115,18 +95,6 @@ function handleDelete(row: any) {
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);
@@ -147,11 +115,11 @@ function validate() {
}
if (!item.freeCount || item.freeCount <= 0) {
throw new Error(
`包邮设置第 ${i + 1} 行:${columnTitle.value.freeCountTitle}必须大于0`,
`包邮设置第 ${i + 1} 行:${columnTitle.value?.freeCountTitle}必须大于 0`,
);
}
if (!item.freePrice || item.freePrice <= 0) {
throw new Error(`包邮设置第 ${i + 1}包邮金额必须大于0`);
throw new Error(`包邮设置第 ${i + 1} 行:包邮金额必须大于 0`);
}
}
}
@@ -159,23 +127,15 @@ function validate() {
defineExpose({
validate,
});
/** 初始化 */
onMounted(async () => {
try {
areaTree.value = await getAreaTree();
} catch (error) {
console.error('加载区域数据失败:', error);
}
});
</script>
<template>
<Grid class="w-full">
<template #areaIds="{ row }">
<Cascader
<!-- TODO 芋艿可优化使用 Cascade不过貌似 antd multiple 貌似有 bug -->
<TreeSelect
v-model:value="row.areaIds"
:options="areaTree"
:tree-data="areaTree"
:field-names="{
label: 'name',
value: 'id',
@@ -184,18 +144,16 @@ onMounted(async () => {
placeholder="请选择地区"
class="w-full"
multiple
show-checked-strategy="SHOW_CHILD"
:disabled="disabled"
@change="handleAreaChange($event, row)"
tree-checkable
:max-tag-count="1"
@change="handleRowChange(row)"
/>
</template>
<template #freeCount="{ row }">
<InputNumber
v-model:value="row.freeCount"
:min="1"
class="w-full"
:disabled="disabled"
@change="handleValueChange('freeCount', $event, row)"
@change="handleRowChange(row)"
/>
</template>
<template #freePrice="{ row }">
@@ -203,14 +161,11 @@ onMounted(async () => {
v-model:value="row.freePrice"
:min="0"
:precision="2"
class="w-full"
:disabled="disabled"
@change="handleValueChange('freePrice', $event, row)"
@change="handleRowChange(row)"
/>
</template>
<template #actions="{ row }">
<TableAction
v-if="!disabled"
:actions="[
{
label: '删除',
@@ -224,10 +179,8 @@ onMounted(async () => {
]"
/>
</template>
<template #bottom>
<TableAction
v-if="!disabled"
class="mt-2 flex justify-center"
:actions="[
{