要实现的效果:
列表:
弹框:
当选择自定义运费时才需要填写“计价方式”、“运费数据”’,选择卖家包邮时不展示“计价方式”、“运费数据”’,也不需要传递给后端
运费配送区域弹框:
当是添加状态的弹框时,已添加到列表的数据不在展示在“可配送区域”中;当是编辑状态时,将点击编辑的当前行的城市数据默认勾选,并展示在“可配送区域”中;当点击删除后,将删除的数据添加回“可配送区域”中,并展示
cityData地区编码数据格式:省市区三级数据,包含港澳台
具体实现代码:
展示数据列表页面:
<template>
<el-card>
<el-button type="primary" @click="add">新增</el-button>
<!-- 列表 -->
<el-table v-loading="false" :data="list" border style="width: 100%">
<el-table-column label="主键">
<template #default="scope">
{{ scope.row.id ?? '无数据' }}
</template>
</el-table-column>
<el-table-column label="模板名称" min-width="150px">
<template #default="scope">
{{ scope.row.name ?? '' }}
</template>
</el-table-column>
<el-table-column label="发货时间" min-width="150px">
<template #default="scope">
{{ logisitcsTempTimeTypeFilter(scope.row.timeType) }}
</template>
</el-table-column>
<el-table-column label="是否包邮" min-width="150px">
<template #default="scope">
{{ scope.row.free ? '是' : '否' }}
</template>
</el-table-column>
<el-table-column label="计价方式" min-width="150px">
<template #default="scope">
{{ scope.row.priceType === 1 ? '件数' : scope.row.priceType === 2 ? '重量' : '体积' }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="200px">
<template #default="scope">
<span>
<el-button type="primary" link @click="edit(scope.row)">编辑</el-button>
</span>
<span>
<el-popconfirm title="确认是否删除" confirm-button-text="确认" cancel-button-text="取消" width="auto" @confirm="remove(scope.row.id)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页按钮 -->
<div class="flex justify-end mt-4">
<el-pagination
v-model:page-size="pageSize"
v-model:current-page="curPage"
background
layout="sizes, prev, pager, next"
:total="total"
class="mt-4"
:page-sizes="[10, 30, 50, 80, 100]"
/>
</div>
<!-- 弹窗 -->
<modal ref="ModalRef" :list="list" @done="done"></modal>
</el-card>
</template>
<script setup lang="ts">
import useList from '/@/hooks/useList';
import { reactive, ref } from 'vue';
import { errorMessage, message } from '/@/utils/message';
import modal from './childComponents/modal.vue';
import { logisticsTemplateApi } from '/@/network/api/logisticsTemplate/index';
import { DIALOG_MODE } from '/@/hooks/useDialog/types';
import { logisitcsTempTimeTypeFilter } from '/@/utils/filter/logisitcsTemplate';
// 获取组件实例
const ModalRef = ref();
const { loading, curPage, total, list, reset, pageSize, loadData } = useList(logisticsTemplateApi.list);
// 重新请求列表数据
const done = () => {
loadData();
};
// 编辑
const edit = (data: any) => {
if (!ModalRef.value) return;
ModalRef.value.openDialog(DIALOG_MODE.EDIT, data);
};
// 新增
const add = () => {
if (!ModalRef.value) return;
ModalRef.value.openDialog(DIALOG_MODE.ADD);
};
// 删除
const remove = async (id: number) => {
try {
await logisticsTemplateApi.remove(id);
message('删除成功');
loadData();
} catch (error) {
errorMessage('删除失败');
}
};
</script>
<style lang="scss" scoped></style>
添加模板的弹窗代码( 屎山代码,还没优化(Ω_Ω) ):
<template>
<div class="">
<el-dialog v-model="visible" :title="`${modeText}物流模板`" width="80%" :before-close="customCloseDialogFn">
<el-form ref="ruleFormRef" label-position="left" label-width="120px" :rules="rules" :model="formData">
<el-form-item label="模板名称:" prop="name">
<el-input v-model="formData.name" placeholder="请填写模板名称" />
</el-form-item>
<el-form-item label="发货时间:" prop="timeType">
<el-select v-model="formData.timeType" filterable clearable cl class="w-full">
<el-option label="1天内" :value="0" />
<el-option label="2天内" :value="1" />
<el-option label="3天内" :value="2" />
<el-option label="5天内" :value="3" />
<el-option label="7天内" :value="4" />
<el-option label="10天内" :value="5" />
<el-option label="15天内" :value="6" />
<el-option label="20天内" :value="7" />
<el-option label="30天内" :value="8" />
<el-option label="60天内" :value="9" />
</el-select>
</el-form-item>
<el-form-item label="是否包邮:" prop="free">
<el-radio-group v-model="formData.free">
<el-radio :label="false">自定义运费</el-radio>
<el-radio :label="true">卖家包邮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="计价方式:" prop="priceType" v-show="!formData.free">
<el-radio-group v-model="formData.priceType">
<el-radio :label="1">按件数</el-radio>
<el-radio :label="2">按重量</el-radio>
<el-radio :label="3">按体积</el-radio>
</el-radio-group>
</el-form-item>
<!-- 运费数据表格 -->
<el-form-item label="运费数据:" prop="info" v-show="!formData.free">
<el-table v-loading="false" :data="formData.info" border style="width: 100%">
<el-table-column label="可配送区域" min-width="200px">
<template #default="scope"> {{ showCityText(scope.row.area) }} </template>
</el-table-column>
<el-table-column label="首件(个)" min-width="120px">
<template #default="scope">
{{ scope.row.first ?? '' }}
</template>
</el-table-column>
<el-table-column label="运费(元)" min-width="120px">
<template #default="scope">
{{ scope.row.firstPrice ?? '' }}
</template>
</el-table-column>
<el-table-column label="续件(个)" min-width="120px">
<template #default="scope">
{{ scope.row.continueNum ?? '' }}
</template>
</el-table-column>
<el-table-column label="续费(元)" min-width="120px">
<template #default="scope">
{{ scope.row.continuePrice ?? '' }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180px">
<template #default="scope">
<span>
<el-button type="primary" link @click="editCity(scope.row, scope.$index)">编辑</el-button>
</span>
<span>
<el-popconfirm
title="确认是否删除"
confirm-button-text="确认"
cancel-button-text="取消"
width="auto"
@confirm="remove(scope.row, scope.$index)"
>
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</span>
</template>
</el-table-column>
</el-table>
<el-button type="primary" @click="addCityFn">点击添加可配送区域和运费</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="customCloseDialogFn">关闭</el-button>
<el-button type="primary" @click="submit">{{ modeText }}</el-button>
</span>
</template>
</el-dialog>
<!-- 运费数据对话框 -->
<el-dialog v-model="dialogFormVisible" :title="`${cityDialogModel}运费数据`" width="60%" :before-close="beforeClose">
<el-form :model="form" :rules="infoRules" ref="infoRule">
<el-form-item label="可配送区域:" label-width="120px" prop="area">
<!-- 选择地理区域 -->
<el-tree
:data="cityDatas"
ref="tree"
show-checkbox
accordion
node-key="value"
v-if="dialogFormVisible"
:props="{ label: 'text', disabled: 'isDisabled' }"
:default-checked-keys="checkedList"
/>
</el-form-item>
<el-form-item label="首件(个):" label-width="120px" prop="first">
<el-input v-model="form.first" placeholder="请输入" oninput="value=value.replace(/[^\d]/g,'')" />
</el-form-item>
<el-form-item label="运费(元):" label-width="120px" prop="firstPrice">
<el-input v-model="form.firstPrice" placeholder="请输入" oninput="value=value.replace(/[^\d]/g,'')" />
</el-form-item>
<el-form-item label="续件(个):" label-width="120px" prop="continueNum">
<el-input v-model="form.continueNum" placeholder="请输入" oninput="value=value.replace(/[^\d]/g,'')" />
</el-form-item>
<el-form-item label="续费(元):" label-width="120px" prop="continuePrice">
<el-input v-model="form.continuePrice" placeholder="请输入" oninput="value=value.replace(/[^\d]/g,'')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="beforeClose">取消</el-button>
<el-button type="primary" @click="confirm"> 确认 </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref, reactive, computed, watch } from 'vue';
import rules from './FormRules';
import infoRules from './infoRules';
import useDialogFn from '/@/hooks/useDialog/useDialogWithForm';
import type { FormInstance } from 'element-plus';
import { deepClone } from '/@/utils/other';
import { DIALOG_MODE } from '/@/hooks/useDialog/types';
import { logisticsTemplateApi } from '/@/network/api/logisticsTemplate/index';
import { errorMessage, message } from '/@/utils/message';
import cityData from './cityData';
// 处理城市数据 只保留省市二级
for (let province of cityData) {
if (province.children) {
for (let city of province.children) {
delete city.children; // 删除市级数据中的 children 属性
}
}
}
// 添加parentId 为其父级的value
cityData.forEach((item) => {
item.parentId = 0;
item.children?.forEach((citem) => (citem.parentId = item.value));
});
// console.log(cityData);
// 扁平化后的数组 方便后续遍历对比使用
let flatArr: logisticsTemplateApi.cityDataType[] = [];
cityData.forEach((item: any) => {
flatArr.push({
value: item.value,
text: item.text,
parentId: item.parentId,
});
if (item.children) {
item.children.forEach((childItem: any) => {
flatArr.push(childItem);
});
}
});
// console.log(flatArr);
// 获取表单实例
const ruleFormRef = ref<FormInstance>();
const infoRule = ref<FormInstance>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
// 提取弹框对应的值和方法
const { visible, mode, closeDialog: customCloseDialog, openDialog: customOpenDialog, modeText } = useDialogFn(ruleFormRef);
/**
* 物流模板表单数据
* */
const formData = ref<logisticsTemplateApi.logisticsTemplateInfo>({
name: '',
free: false,
priceType: 1,
timeType: '',
info: [],
});
/**
* 选择配送区域的表单数据
*/
const form = ref({
area: [],
continueNum: '',
continuePrice: '',
first: '',
firstPrice: '',
});
const dialogFormVisible = ref(false);
const cityDialogModel = ref('添加');
// 关闭弹框并重新请求列表数据
const closeDialog = () => {
// 关闭并清空表单中数据
customCloseDialogFn();
emit('done');
};
const openDialog = (mode: DIALOG_MODE, data?: any) => {
customOpenDialog(mode);
nextTick(() => {
if (mode === DIALOG_MODE.EDIT && data) {
const val = deepClone(data);
formData.value = val as any;
}
});
};
// 树形控件实例
const tree = ref();
let cityDatas = ref(JSON.parse(JSON.stringify(cityData)));
// 展示在表格中的区域字符串
let cityStr: string;
// 获取传入数组项重复出现了多少次的函数
function countOccurrences(arr: any) {
const counts: any = {};
cityStr = '';
// 获取城市的parentId
let codeArr = arr.map((item: any) => item.parentId);
// console.log(codeArr);
// 遍历数组,同一省份的城市出现了多少次
codeArr.forEach(function (num: any) {
// 对于每个数字,累计其出现次数
if (counts[num]) {
counts[num]++;
} else {
counts[num] = 1;
}
});
// 遍历计数对象,打印数字和出现次数
for (let num in counts) {
// console.log(`${num}出现了${counts[num]}次。`);
// 根据地区编码前两位获取到是属于哪一个省级
if (num != '0') {
let a: any = cityData.find((item: any) => {
return item.value == num;
});
// console.log('a', a);
// 判读重复出现的次数是不是大于(因为全选会多出一个省级的对象,所以是判断大于长度)省级中市级数组的长度 是就证明是全选
if (counts[num] == a?.children?.length) {
// 全选就直接拼接省份名
cityStr = cityStr + ' ' + a?.text;
} else {
// 将属于当前省份的数据项提取出来成一个新数组
let newarr = arr.filter((item: any) => item.parentId == a.value);
// console.log('newarr', newarr);
cityStr = cityStr + ' ' + a?.text + '(';
// 遍历新数组将对应的城市名拼接起来
newarr.forEach((item: any, index: number) => {
let b = a?.children?.find((citem: any) => citem.value == item.value);
cityStr = cityStr + b?.text + '、';
});
cityStr = cityStr.substring(0, cityStr.length - 1) + ')';
}
}
}
}
const customCloseDialogFn = () => {
customCloseDialog();
cityDatas.value = JSON.parse(JSON.stringify(cityData));
};
//处理展示配送区域文字的格式
const showCityText = (arr: { code: string; name: string }[]) => {
let newArr: logisticsTemplateApi.cityDataType[] = [];
flatArr.forEach((item) => {
arr.forEach((citem) => {
if (item.value == citem.code) {
newArr.push(item);
}
});
});
countOccurrences(newArr);
return cityStr;
};
// 清空表单数据
const resetForm = () => {
form.value.continueNum = '';
form.value.continuePrice = '';
form.value.first = '';
form.value.firstPrice = '';
};
// 添加配送区域等信息到表格中
const confirm = async () => {
// 选中的数据
const checkedArr = tree.value.getCheckedNodes();
if (!checkedArr[0]) return errorMessage('请选择可配送区域');
// 转成上传需要的格式
form.value.area = checkedArr.map((item: any) => ({ code: item.value, name: item.text }));
if (!infoRule.value) return;
// 验证
const valid = await infoRule.value.validate();
if (!valid) return;
if (cityDialogModel.value === '添加') {
formData.value.info.push({ ...form.value });
} else {
formData.value.info[editIndex] = { ...form.value };
}
// 删除掉已选中的数据
checkedArr.forEach((item: any) => {
// 删除省级
cityDatas.value.forEach((citem: any, cindex: number) => {
if (item.value == citem.value) {
cityDatas.value.splice(cindex, 1);
return;
}
// 删除市级
citem.children.forEach((gitem: any, gindex: number) => {
if (item.value == gitem.value) {
citem.children.splice(gindex, 1);
}
});
});
});
// 清空表单数据
resetForm();
dialogFormVisible.value = false;
};
// 添加配送区域
const addCityFn = () => {
cityDialogModel.value = '添加';
checkedList.value = [];
dialogFormVisible.value = true;
};
// 默认选中的数组
const checkedList = ref([]);
// 保存编辑的索引
let editIndex = 0;
// 编辑前的城市数据
let beforeEditCityDatas = ref([]);
// 编辑配送区域数据
const editCity = (data: any, index: number) => {
cityDialogModel.value = '编辑';
editIndex = index;
// 将编辑前的城市数据保存起来,若没编辑就保持原来的数据
beforeEditCityDatas.value = JSON.parse(JSON.stringify(cityDatas.value));
checkedList.value = data.area.map((item: any) => item.code);
dialogFormVisible.value = true;
form.value = { ...data };
editPush(data.area);
};
//将编辑中的数据加回城市数据中
const editPush = (data: any) => {
const addCityDatas: any = [];
// 取到对应的带parentId的数据
data.forEach((item: any) => {
flatArr.forEach((citem) => {
if (item.code == citem.value) {
addCityDatas.push(citem);
}
});
});
addCityDatas.forEach((item: any) => {
if (item.parentId == 0) {
cityDatas.value.push({ ...item, children: [] });
} else {
// 判断城市数据里有没有这个父级
const hasParentId = cityDatas.value.find((hitem: any) => hitem.value === item.parentId);
if (hasParentId) {
cityDatas.value.forEach((citem: any, cindex: number) => {
if (item.parentId == citem.value) {
citem.children.push(item);
// 排序
sortCity(citem.children);
}
});
} else {
const findParent = flatArr.find((fitem: any) => fitem.value === item.parentId);
cityDatas.value.push(findParent);
cityDatas.value[cityDatas.value.length - 1].children = [];
cityDatas.value[cityDatas.value.length - 1].children.push(item);
}
}
});
sortCity(cityDatas.value);
};
// 如果编辑没有确认更改数据 恢复更新前数据
const beforeClose = () => {
dialogFormVisible.value = false;
if (cityDialogModel.value === '编辑') {
cityDatas.value = beforeEditCityDatas.value;
}
resetForm();
};
// 如果是编辑模式就将已选中的城市从展示数据中删除
watch(
() => visible.value,
(newVal) => {
if (newVal && modeText.value === '编辑') {
setTimeout(() => {
const flatInfo = formData.value.info.flat();
// console.log(flatInfo);
// 删除掉已选中的数据
flatInfo.forEach((item: any) => {
item.area.forEach((jitem: any) => {
// 删除省级
cityDatas.value.forEach((citem: any, cindex: number) => {
if (jitem.code == citem.value) {
cityDatas.value.splice(cindex, 1);
return;
}
// 删除市级
citem.children.forEach((gitem: any, gindex: number) => {
if (jitem.code == gitem.value) {
citem.children.splice(gindex, 1);
}
});
});
});
});
}, 100);
}
}
);
// 删除
const remove = (data: any, index: number) => {
formData.value.info.splice(index, 1);
editPush(data.area);
};
//排序方法 添加回来后的城市用的是push 数据回添加在最后
const sortCity = (data: any) => {
data.sort((a: any, b: any) => {
return a.value - b.value;
});
};
// 提交方法
const submit = async () => {
if (!ruleFormRef.value) return;
// 验证
const valid = await ruleFormRef.value.validate();
if (!valid) return;
let sendData: any = { info: [] };
// 判断是否包邮
if (formData.value.free) {
sendData = formData.value;
sendData.info = [];
} else if (!formData.value.free && formData.value.info.length === 0) {
return errorMessage('选择自定义运费时,需要填写运费数据信息');
} else {
sendData = formData.value;
}
if (mode.value === DIALOG_MODE.EDIT) {
// 提交
try {
await logisticsTemplateApi.update(sendData);
message('修改成功');
closeDialog();
} catch (error) {
errorMessage('修改失败');
}
}
if (mode.value === DIALOG_MODE.ADD) {
// 提交
try {
await logisticsTemplateApi.add(sendData);
message('添加成功');
closeDialog();
} catch (error) {
errorMessage('添加失败');
}
}
};
defineExpose({ visible, openDialog, closeDialog });
</script>
<style lang="scss" scoped>
:deep(.el-tree) {
display: flex;
flex-wrap: wrap;
}
:deep(.el-tree .el-tree-node__children) {
display: flex;
flex-wrap: wrap;
}
</style>