基于ant Design Vue级联组件a-cascader封装的级联组件
示例
结构
<a-form
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-form-item label="整棵树">
<grid-cascader v-model="model.unit" change-on-select @change="unitChange"></grid-cascader>
</a-form-item>
<a-form-item label="懒加载">
<grid-cascader v-model="model.unit" lazy full-path change-on-select></grid-cascader>
</a-form-item>
<a-form-item :wrapper-col="formTailLayout.wrapperCol">
<a-button type="primary" @click="print">print</a-button>
</a-form-item>
</a-form>
<a-form
:form="form"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-form-item label="所属网格">
<grid-cascader
v-decorator="[
'unit',
{ rules: [{ required: true, message: '请选择' }] },
]"
lazy
full-path
change-on-select
placeholder="请选择"
></grid-cascader>
</a-form-item>
<a-form-item :wrapper-col="formTailLayout.wrapperCol">
<a-button type="primary" @click="check">Check</a-button>
</a-form-item>
</a-form>
逻辑
import GridCascader from '../components/grid-cascader';
export default {
name: 'TestCascader',
components: {
GridCascader
},
data() {
return {
model: {
unit: '3-17',
},
form: this.$form.createForm(this, { name: 'dynamic_rule' }),
formItemLayout: {
labelCol: { span: 4 },
wrapperCol: { span: 12 },
},
formTailLayout: {
labelCol: { span: 4 },
wrapperCol: { span: 12, offset: 4 },
},
};
},
methods: {
print() {
console.log(this.model);
},
unitChange(value) {
this.form.setFieldsValue({ unit: value });
},
check() {
this.form.validateFields((err, value) => {
console.log('--- validateFields --->', err, value);
if (!err) {
console.info('success');
}
});
}
}
}
样式
.test-cascader {
padding: 300px 200px 0;
display: flex;
& > .ant-form {
flex: 1;
}
}
API
attributes
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
value | 指定选中项(最后一级) | string | - |
fullPath | 是否显示全路径 | boolean | false |
lazy | 是否懒加载 | boolean | false |
event
事件名 | 说明 | 参数 |
---|---|---|
change | 选择完成后的回调 | (value) => void |
源码
组件代码
结构
<a-cascader
v-bind="$attrs"
:value="initData"
:options="options"
:load-data="loadData"
:field-names="fieldNames"
:displayRender="displayRender"
style="width: 100%"
@change="onChange"
></a-cascader>
逻辑
import { getTreeData, getData, getPath } from './test';
import { treeFindPath, treeDataFormat } from '../utils/utils.js';
export default {
name: 'GridCascader',
model: {
prop: 'value',
event: 'change', // v-decorator 通过 change 事件接受新值
},
props: {
value: {
type: String,
// 当通过 a-from 标签包裹时:
// 这个参数是 v-decorator 给子组件传值用的
// 这里不要给默认值, 在 form 下使用会爆警告 Warning: SquareUpload `default value` can not collect, please use `option.initialValue` to set default value.
},
fullPath: {
// 是否显示全路径
type: Boolean,
default: false,
},
lazy: {
// 是否懒加载
type: Boolean,
default: false
},
},
data() {
return {
options: [],
fieldNames: { label: 'title', value: 'key', children: 'children' },
}
},
computed: {
initData() {
const path = treeFindPath(this.value, this.options, this.fieldNames);
return path.map(item => (item[this.fieldNames.value]));
}
},
watch: {
value: {
async handler(value) {
if (!value || !this.lazy) return;
const path = treeFindPath(value, this.options, this.fieldNames);
if (path.length) return;
// 数据回显 请求接口查询返回数据
this.options = await this.fetchPath(value);
},
immediate: true
}
},
async created() {
if (!this.lazy) {
this.options = await this.fetchTree();
} else if (!this.value) {
this.options = await this.fetchData('-1');
}
},
methods: {
async fetchTree() {
// 返回整棵树
const res = await getTreeData();
if (!Array.isArray(res.result)) return [];
const result = this.treeFormat(res.result);
console.log('--- fetchTree --->', JSON.parse(JSON.stringify(result)));
return result;
},
async fetchData(parentId = '-1') {
// (动态加载选项) 返回传入父节点的子节点
const res = await getData({ parentId });
if (!Array.isArray(res.result)) return [];
const result = this.treeFormat(res.result);
console.log('--- fetchData --->', JSON.parse(JSON.stringify(result)));
return result;
},
async fetchPath(treeId) {
// (数据回显) 返回选择节点的路径/每一级数组数据
if (!treeId) return [];
const res = await getPath({ treeId });
if (!Array.isArray(res.result.treeData)) return [];
const result = this.treeFormat(res.result.treeData);
console.log('--- fetchPath --->', JSON.parse(JSON.stringify(result)));
return result;
},
treeFormat(data) {
return treeDataFormat(data, (item) => ({
...item,
isLeaf: item.leaf
}));
},
loadData(selectedOptions) {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
this.fetchData(targetOption.key).then((result) => {
targetOption.children = result;
this.options = [...this.options];
}).finally(() => {
targetOption.loading = false;
});
},
displayRender({ labels }) {
if (labels.length == 0) return '';
if (this.fullPath) return labels.join(' / ');
return labels.pop();
},
onChange(value) {
this.$emit('change', value.pop());
},
},
}
工具方法
../utils/utils.js
/**
* 搜索树形结构 返回路径
* @param {String} key 搜索的key
* @param {Array} treeData 数据
* @param {Object} defaultProps 自定义 options 中 label value children 的字段
* @returns {Array}
*/
export function treeFindPath(key, treeData, defaultProps, path = []) {
if (!Array.isArray(treeData)) {
console.warn('treeData is not array');
return [];
}
const { label: labelField = 'label', value: valueField = 'value', children: childrenField = 'children' } = defaultProps || {};
for (let i = 0; i < treeData.length; i++) {
const node = treeData[i];
const children = node[childrenField];
const label = node[labelField];
const value = node[valueField];
path.push({
[labelField]: label,
[valueField]: value
});
if (value === key) {
return path;
}
if (children) {
const p = treeFindPath(key, children, defaultProps, path);
if (p.length) {
return p;
}
}
path.pop();
}
return [];
}
/**
* 树 格式化数据
* @param {Array} data 数据
* @param {Function} handle 处理函数
*/
export function treeDataFormat(data, handle) {
if (!Array.isArray(data)) {
console.warn('data is not array');
return data;
}
if (typeof(handle) !== 'function') {
console.warn('handle is not function');
return data;
}
return data.map(item => {
let children;
if (Array.isArray(item.children)) {
children = treeDataFormat(item.children, handle);
}
const newItem = handle(item);
if (children) {
newItem.children = children;
}
return newItem;
});
}
mock数据
../utils/utils.js
import { treeFindPath } from '../utils/utils';
const fieldNames = { label: 'title', value: 'key', children: 'children' };
const data = [{
key: '1',
title: '福建省',
parentId: '-1',
leaf: false,
children: [
{ key: '1-1', title: '福州市', parentId: '1', leaf: true },
{ key: '1-2', title: '厦门市', parentId: '1', leaf: true },
{ key: '1-3', title: '莆田市', parentId: '1', leaf: true },
{
key: '1-4',
title: '三明市',
parentId: '1',
leaf: false,
children: [
{ key: '1-4-1', title: '梅列区', parentId: '1-4', leaf: true },
{ key: '1-4-2', title: '三元区', parentId: '1-4', leaf: true },
{ key: '1-4-3', title: '明溪县', parentId: '1-4', leaf: true },
{ key: '1-4-4', title: '清流县', parentId: '1-4', leaf: true },
{ key: '1-4-5', title: '宁化县', parentId: '1-4', leaf: true },
{ key: '1-4-6', title: '大田县', parentId: '1-4', leaf: true },
{ key: '1-4-7', title: '尤溪县', parentId: '1-4', leaf: true },
{ key: '1-4-8', title: '沙县', parentId: '1-4', leaf: true },
{ key: '1-4-9', title: '将乐县', parentId: '1-4', leaf: true },
{ key: '1-4-10', title: '泰宁县', parentId: '1-4', leaf: true },
{ key: '1-4-11', title: '建宁县', parentId: '1-4', leaf: true },
{ key: '1-4-12', title: '永安市', parentId: '1-4', leaf: true },
]
},
{ key: '1-5', title: '泉州市', parentId: '1', leaf: true },
{ key: '1-6', title: '漳州市', parentId: '1', leaf: true },
{ key: '1-7', title: '南平市', parentId: '1', leaf: true },
{ key: '1-8', title: '龙岩市', parentId: '1', leaf: true },
{ key: '1-9', title: '宁德市', parentId: '1', leaf: true },
]
}, {
key: '2',
title: '江苏省',
parentId: '-1',
leaf: false,
children: [
{ key: '2-1', title: '南京市', parentId: '2', leaf: true },
{ key: '2-2', title: '无锡市', parentId: '2', leaf: true },
{ key: '2-3', title: '徐州市', parentId: '2', leaf: true },
{ key: '2-4', title: '常州市', parentId: '2', leaf: true },
{
key: '2-5',
title: '苏州市',
parentId: '2',
leaf: false,
children: [
{ key: '2-5-1', title: '沧浪区', parentId: '2-5', leaf: true },
{ key: '2-5-2', title: '平江区', parentId: '2-5', leaf: true },
{ key: '2-5-3', title: '金阊区', parentId: '2-5', leaf: true },
{ key: '2-5-4', title: '虎丘区', parentId: '2-5', leaf: true },
{ key: '2-5-5', title: '吴中区', parentId: '2-5', leaf: true },
{ key: '2-5-6', title: '相城区', parentId: '2-5', leaf: true },
{ key: '2-5-7', title: '姑苏区', parentId: '2-5', leaf: true },
{ key: '2-5-8', title: '常熟市', parentId: '2-5', leaf: true },
{ key: '2-5-9', title: '张家港市', parentId: '2-5', leaf: true },
{ key: '2-5-10', title: '昆山市', parentId: '2-5', leaf: true },
{ key: '2-5-11', title: '吴江区', parentId: '2-5', leaf: true },
{ key: '2-5-12', title: '太仓市', parentId: '2-5', leaf: true },
{ key: '2-5-13', title: '新区', parentId: '2-5', leaf: true },
{ key: '2-5-14', title: '园区', parentId: '2-5', leaf: true },
]
},
{ key: '2-6', title: '南通市', parentId: '2', leaf: true },
{ key: '2-7', title: '连云港市', parentId: '2', leaf: true },
{ key: '2-8', title: '淮安市', parentId: '2', leaf: true },
{ key: '2-9', title: '盐城市', parentId: '2', leaf: true },
{ key: '2-10', title: '扬州市', parentId: '2', leaf: true },
{ key: '2-11', title: '镇江市', parentId: '2', leaf: true },
{ key: '2-12', title: '泰州市', parentId: '2', leaf: true },
{ key: '2-13', title: '宿迁市', parentId: '2', leaf: true },
]
}, {
key: '3',
title: '河南省',
parentId: '-1',
leaf: false,
children: [
{ key: '3-1', title: '郑州市', parentId: '3', leaf: true },
{ key: '3-2', title: '开封市', parentId: '3', leaf: true },
{ key: '3-3', title: '洛阳市', parentId: '3', leaf: true },
{ key: '3-4', title: '平顶山市', parentId: '3', leaf: true },
{ key: '3-5', title: '安阳市', parentId: '3', leaf: true },
{ key: '3-6', title: '鹤壁市', parentId: '3', leaf: true },
{ key: '3-7', title: '新乡市', parentId: '3', leaf: true },
{ key: '3-8', title: '焦作市', parentId: '3', leaf: true },
{ key: '3-9', title: '濮阳市', parentId: '3', leaf: true },
{ key: '3-10', title: '许昌市', parentId: '3', leaf: true },
{ key: '3-11', title: '漯河市', parentId: '3', leaf: true },
{ key: '3-12', title: '三门峡市', parentId: '3', leaf: true },
{ key: '3-13', title: '南阳市', parentId: '3', leaf: true },
{ key: '3-14', title: '商丘市', parentId: '3', leaf: true },
{ key: '3-15', title: '信阳市', parentId: '3', leaf: true },
{
key: '3-16',
title: '周口市',
parentId: '3',
leaf: false,
children: [
{ key: '3-16-1', title: '川汇区', parentId: '3-16', leaf: true },
{ key: '3-16-2', title: '扶沟县', parentId: '3-16', leaf: true },
{ key: '3-16-3', title: '西华县', parentId: '3-16', leaf: true },
{ key: '3-16-4', title: '商水县', parentId: '3-16', leaf: true },
{ key: '3-16-5', title: '沈丘县', parentId: '3-16', leaf: true },
{ key: '3-16-6', title: '郸城县', parentId: '3-16', leaf: true },
{ key: '3-16-7', title: '淮阳县', parentId: '3-16', leaf: true },
{ key: '3-16-8', title: '太康县', parentId: '3-16', leaf: true },
{ key: '3-16-9', title: '鹿邑县', parentId: '3-16', leaf: true },
{ key: '3-16-10', title: '项城市', parentId: '3-16', leaf: true },
]
},
{ key: '3-17', title: '驻马店市', parentId: '3', leaf: true },
]
}
];
const arr = [{ key: '-1', children: data }];
const map = new Map();
(function () {
const func = (arr) => {
arr.forEach((item) => {
if (item.children && item.children.length) {
map.set(
item.key,
item.children.map((item) => ({
[fieldNames.label]: item[fieldNames.label],
[fieldNames.value]: item[fieldNames.value],
leaf: item.leaf,
}))
);
func(item.children);
}
});
};
func(arr);
})();
function delay(duration = 300) {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
}
/**
* 返回整棵树
* @returns {Promise<Array>}
*/
export async function getTreeData() {
await delay();
const res = {
code: 200,
message: '',
result: data,
success: true,
timestamp: Date.now()
};
return res;
}
/**
* (动态加载选项) 返回传入父节点的子节点
* @param {Object} param = { parentId: '父节点' }
* @returns {Promise<Array>}
*/
export async function getData({ parentId = '-1' }) {
await delay();
const res = {
code: 200,
message: '',
result: map.get(parentId),
success: true,
timestamp: Date.now()
};
return res;
}
/**
* (数据回显) 返回选择节点的路径/每一级数组数据
* @param {Object} param = { treeId: '选择节点' }
* @returns
*/
export async function getPath({ treeId = '' }) {
if (!treeId) throw new Error('treeId is required');
await delay();
const path = treeFindPath(treeId, data, fieldNames);
const treeData = [];
if (path.length) {
let list = map.get('-1');
treeData.push(...list);
for (let i = 0; i < path.length - 1; i++) {
const item = list.find((item) => item.key == path[i].key);
list = map.get(path[i].key);
// treeData.push(list);
item.children = list;
}
}
const res = {
code: 200,
message: '',
result: {
path,
treeData
},
success: true,
timestamp: Date.now()
};
return res;
}