Skip to content

SelectCascader 级联选择器

当一个数据集合有清晰的层级结构时,可通过级联选择器逐级查看并选择。

组件注册

js
import { FSelectCascader } from '@fesjs/fes-design';

app.use(FSelectCascader);

代码演示

基础用法

适用广泛的基础单选。

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="展开次级菜单:">
            <FRadioGroup v-model="expandTrigger">
                <FRadio value="click">click(默认)</FRadio>
                <FRadio value="hover">hover</FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="勾选策略:">
            <FRadioGroup v-model="checkStrictly" :cancelable="false">
                <FRadio value="all">all</FRadio>
                <FRadio value="child">child(默认)</FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="是否展示路径:">
            <FRadioGroup v-model="showPath">
                <FRadio :value="true"></FRadio>
                <FRadio :value="false">否(默认)</FRadio>
            </FRadioGroup>
        </FFormItem>
    </FForm>

    <FDivider />

    <FSelectCascader
        class="select-cascader"
        :data="data"
        :expandTrigger="expandTrigger"
        :checkStrictly="checkStrictly"
        :showPath="showPath"
    />
</template>

<script>
import { h, reactive, ref } from 'vue';
import { PictureOutlined, PlusCircleOutlined } from '@fesjs/fes-design/icon';

function createData(level = 1, baseKey = '', prefix = null, suffix = null) {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 2 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            label: createLabel(level),
            value: key,
            children: createData(level - 1, key, prefix, suffix),
            prefix: prefix ? () => h(PictureOutlined) : null,
            suffix: suffix ? () => h(PlusCircleOutlined) : null,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data = reactive(createData(4));
        const expandTrigger = ref('click');
        const checkStrictly = ref('child');
        const showPath = ref(false);
        return {
            data,
            expandTrigger,
            checkStrictly,
            showPath,
        };
    },
};
</script>

<style scoped>
.select-cascader {
    width: 200px;
}
</style>

可清空

包含清空按钮,可将选择器清空为初始状态。

play
<template>
    <FSelectCascader :data="data" clearable />
</template>

<script>
import { h, reactive } from 'vue';
import { PictureOutlined, PlusCircleOutlined } from '@fesjs/fes-design/icon';

function createData(level = 1, baseKey = '', prefix = null, suffix = null) {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 2 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            label: createLabel(level),
            value: key,
            children: createData(level - 1, key, prefix, suffix),
            prefix: prefix ? () => h(PictureOutlined) : null,
            suffix: suffix ? () => h(PlusCircleOutlined) : null,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data = reactive(createData(4));
        return {
            data,
        };
    },
};
</script>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
</style>

基础多选

适用性较广的基础多选,用 Tag 展示已选项。

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="父子关联:">
            <FRadioGroup v-model="cascade">
                <FRadio :value="true">是(默认)</FRadio>
                <FRadio :value="false"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem v-if="cascade" label="勾选策略:">
            <FRadioGroup v-model="checkStrictly" :cancelable="false">
                <FRadio value="all">all</FRadio>
                <FRadio value="parent">parent</FRadio>
                <FRadio value="child">child(默认)</FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="展开次级菜单:">
            <FRadioGroup v-model="expandTrigger">
                <FRadio value="click">click(默认)</FRadio>
                <FRadio value="hover">hover</FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="是否展示路径:">
            <FRadioGroup v-model="showPath">
                <FRadio :value="true"></FRadio>
                <FRadio :value="false">否(默认)</FRadio>
            </FRadioGroup>
        </FFormItem>
    </FForm>

    <FDivider />

    <FSelectCascader
        :data="data"
        multiple
        :cascade="cascade"
        :checkStrictly="checkStrictly"
        :expandTrigger="expandTrigger"
        :showPath="showPath"
        clearable
    />
</template>

<script>
import { h, reactive, ref } from 'vue';
import { PictureOutlined, PlusCircleOutlined } from '@fesjs/fes-design/icon';

function createData(level = 1, baseKey = '', prefix = null, suffix = null) {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 2 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            label: createLabel(level),
            value: key,
            children: createData(level - 1, key, prefix, suffix),
            prefix: prefix ? () => h(PictureOutlined) : null,
            suffix: suffix ? () => h(PlusCircleOutlined) : null,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data = reactive(createData(4));
        const cascade = ref(true);
        const checkStrictly = ref('child');
        const expandTrigger = ref('click');
        const showPath = ref(false);
        return {
            data,
            cascade,
            checkStrictly,
            expandTrigger,
            showPath,
        };
    },
};
</script>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
.text-tips {
    margin-top: 10px;
}
</style>

可搜索

可以利用搜索功能快速查找选项

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="是否可搜索:">
            <FRadioGroup v-model="filterable">
                <FRadio :value="false">否(默认)</FRadio>
                <FRadio :value="true"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem v-show="filterable" label="是否高亮:">
            <FRadioGroup
                v-model="filterTextHighlight"
                :options="[
                    { label: '否(默认)', value: false },
                    { label: '', value: true },
                ]"
            />
        </FFormItem>
    </FForm>

    <FDivider />

    <FForm :labelWidth="130">
        <FFormItem label="单选默认:">
            <FSelectCascader
                v-model="value1"
                class="select-cascader"
                :data="data"
                :filterable="filterable"
                :filterTextHighlight="filterTextHighlight"
            />
        </FFormItem>
        <FFormItem label="单选自定义过滤函数:">
            <FSelectCascader
                class="select-cascader"
                :data="data"
                :filterable="filterable"
                :filter="filter"
                :filterTextHighlight="filterTextHighlight"
            />
        </FFormItem>
    </FForm>

    <FDivider />

    <FForm labelPosition="top" :labelWidth="130">
        <FFormItem label="多选默认:">
            <FSelectCascader
                v-model="value2"
                class="select-cascader-multi"
                :data="data"
                :filterable="filterable"
                :multiple="true"
                :filterTextHighlight="filterTextHighlight"
                showPath
                emitPath
            />
        </FFormItem>
        <FFormItem label="多选自定义过滤函数:">
            <FSelectCascader
                class="select-cascader-multi"
                :data="data"
                :filterable="filterable"
                :multiple="true"
                :filter="filter"
                :filterTextHighlight="filterTextHighlight"
                showPath
                emitPath
            />
        </FFormItem>
    </FForm>
</template>

<script setup>
import { reactive, ref } from 'vue';

const filterTextHighlight = ref(false);

function createData(level = 4, baseKey = '') {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 10 - level }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            label: `${key}-${createLabel(level)}`,
            value: key,
            children: createData(level - 1, key),
            disabled: level === 1 && index === 0,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

const filterable = ref(true);
const value1 = ref();
const value2 = ref();

const data = reactive(createData(4));

// 默认会匹配所有节点描述,这里仅匹配叶子节点描述
const filter = (text, option) => {
    return option.label.indexOf(text) !== -1;
};
</script>

<style scoped>
.select-cascader {
    width: 200px;
}
.select-cascader-multi {
    width: 100%;
}
</style>

<style>
.fes-select-cascader-popper .fes-select-dropdown {
    min-width: 500px !important;
}
</style>

emitPath 返回节点菜单路径

适用于异步加载初始化展示等场景。

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="emitPath:">
            <FRadioGroup v-model="emitPath">
                <FRadio :value="false">否(默认)</FRadio>
                <FRadio :value="true"></FRadio>
            </FRadioGroup>
        </FFormItem>
    </FForm>

    <FDivider />

    <FForm :labelWidth="160">
        <FFormItem label="单选:">
            <FSelectCascader
                v-model="value1"
                :data="data.options"
                clearable
                :emitPath="emitPath"
            />
        </FFormItem>
        <FFormItem label="modelValue:">{{ value1 }}</FFormItem>
        <FFormItem label="多选:">
            <FSelectCascader
                v-model="value2"
                :data="data.options"
                multiple
                cascade
                clearable
                :emitPath="emitPath"
            />
        </FFormItem>
        <FFormItem label="modelValue:">{{ value2 }}</FFormItem>
    </FForm>
</template>

<script>
import { h, reactive, ref } from 'vue';
import { PictureOutlined, PlusCircleOutlined } from '@fesjs/fes-design/icon';

function createData(level = 1, baseKey = '', prefix = null, suffix = null) {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 15 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            label: createLabel(level),
            value: key,
            children: createData(level - 1, key, prefix, suffix),
            prefix: prefix ? () => h(PictureOutlined) : null,
            suffix: suffix ? () => h(PlusCircleOutlined) : null,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data = reactive({
            options: [],
        });

        const value1 = ref(['213', '213114']);
        const value2 = ref([
            ['213', '213113'],
            ['213', '213114'],
        ]);
        const emitPath = ref(true);

        setTimeout(() => {
            data.options = createData(2);
        }, 1000);

        return {
            data,
            value1,
            value2,
            emitPath,
        };
    },
};
</script>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
.text-tips {
    margin-top: 10px;
}
</style>

异步加载

若需要自动加载节点展示,则需要 emitPath 置为 true

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="单选:">
            <FSelectCascader
                v-model="value1"
                :data="data1"
                :loadData="loadData"
                valueField="id"
                labelField="name"
                childrenField="child"
                clearable
                remote
                emitPath
                showPath
                @change="handleChange"
            />
        </FFormItem>
        <FFormItem label="modelValue:">{{ value1 }}</FFormItem>
        <FFormItem label="多选:">
            <FSelectCascader
                v-model="value2"
                :data="data2"
                :loadData="loadData"
                valueField="id"
                labelField="name"
                childrenField="child"
                multiple
                cascade
                checkStrictly="parent"
                clearable
                remote
                emitPath
                @change="handleChange"
            />
        </FFormItem>
        <FFormItem label="modelValue:">{{ value2 }}</FFormItem>
    </FForm>
</template>

<script>
import { reactive, ref } from 'vue';

function createData(level = 1, baseKey = '') {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 2 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            name: createLabel(level),
            id: key,
            child: createData(level - 1, key),
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data1 = reactive(createData(2));
        const data2 = reactive([]);
        const loadData = (node) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    let children = [];
                    // 非第一级
                    if (node) {
                        children = [
                            {
                                name: `${node.name}1`,
                                id: `${node.id}-1`,
                                isLeaf:
                                    node.id.split('-').length > 1,
                            },
                            {
                                name: `${node.name}2`,
                                id: `${node.id}-2`,
                                isLeaf:
                                    node.id.split('-').length > 1,
                            },
                        ];
                    } else {
                        // 第一级
                        children = createData(2);
                    }
                    resolve(children);
                }, 2000);
            });
        };

        const handleChange = (value) => {
            console.log('[selectCascader.async] [handleChange] value:', value);
        };

        const value1 = ref([]);
        const value2 = ref([
            ['20', '2010', '2010-1', '2010-1-1'],
            ['20', '2111'],
        ]);

        // 异步设置初始值
        setTimeout(() => {
            value1.value = ['20', '2010', '2010-1', '2010-1-1'];
        }, 2000);

        return {
            loadData,
            data1,
            data2,
            handleChange,
            value1,
            value2,
        };
    },
};
</script>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
</style>

禁用状态

选择器不可用状态。

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="多选是否折叠展示:">
            <FRadioGroup v-model="collapseTags">
                <FRadio :value="false">否(默认)</FRadio>
                <FRadio :value="true"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem v-if="collapseTags" label="折叠项限制:">
            <FRadioGroup v-model="collapseTagsLimit">
                <FRadio :value="1">1(默认)</FRadio>
                <FRadio :value="2">2</FRadio>
                <FRadio :value="3">3</FRadio>
            </FRadioGroup>
        </FFormItem>
    </FForm>

    <FDivider />

    <FSpace>
        <FSelectCascader v-model="value1" :data="data" disabled />
        <FSelectCascader v-model="value2" :data="data" disabled />
    </FSpace>
    <FSpace>
        <FSelectCascader
            v-model="value3"
            :data="data"
            multiple
            disabled
            :collapseTags="collapseTags"
            :collapseTagsLimit="collapseTagsLimit"
        />
        <FSelectCascader
            v-model="value4"
            :data="data"
            multiple
            disabled
            :collapseTags="collapseTags"
            :collapseTagsLimit="collapseTagsLimit"
        />
    </FSpace>
</template>

<script>
import { h, reactive, ref } from 'vue';
import { PictureOutlined, PlusCircleOutlined } from '@fesjs/fes-design/icon';

function createData(level = 1, baseKey = '', prefix = null, suffix = null) {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 2 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            label: createLabel(level),
            value: key,
            children: createData(level - 1, key, prefix, suffix),
            prefix: prefix ? () => h(PictureOutlined) : null,
            suffix: suffix ? () => h(PlusCircleOutlined) : null,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data = reactive(createData(4));
        const value1 = ref('40');
        const value2 = ref('999999'); // 含未匹配项
        const value3 = ref(['40', '41', '4030']);
        const value4 = ref(['999999', '40', '41']); // 含未匹配项
        const collapseTags = ref(true);
        const collapseTagsLimit = ref(1);
        return {
            data,
            value1,
            value2,
            value3,
            value4,
            collapseTags,
            collapseTagsLimit,
        };
    },
};
</script>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
</style>

自定义选项及控制回填内容

play
<template>
    <FForm :labelWidth="160">
        <FFormItem label="是否展示路径:">
            <FRadioGroup v-model="showPath">
                <FRadio :value="true"></FRadio>
                <FRadio :value="false"></FRadio>
            </FRadioGroup>
        </FFormItem>
        <FFormItem label="是否多选:">
            <FRadioGroup v-model="multiple">
                <FRadio :value="true"></FRadio>
                <FRadio :value="false"></FRadio>
            </FRadioGroup>
        </FFormItem>
    </FForm>

    <FDivider />

    <FSelectCascader
        v-if="!multiple"
        :data="data"
        valueField="id"
        labelField="name"
        childrenField="child"
    >
        <template #tag="{ option }">
            <FEllipsis>
                <template v-if="showPath">
                    {{
                        option.path
                            .map((item) => `${item.value}-${item.label}`)
                            .join(' / ')
                    }}
                </template>
                <template v-else>
                    {{
                        option.value ? `${option.value} - ${option.label}` : ''
                    }}
                </template>
            </FEllipsis>
        </template>
    </FSelectCascader>
    <FSelectCascader
        v-else
        :data="data"
        multiple
        valueField="id"
        labelField="name"
        childrenField="child"
    >
        <template #tag="{ option }">
            <FTag type="info" size="small">
                <FEllipsis>
                    <template v-if="showPath">
                        {{
                            option.path
                                .map((item) => `${item.value}-${item.label}`)
                                .join(' / ')
                        }}
                    </template>
                    <template v-else>
                        {{
                            option.value
                                ? `${option.value} - ${option.label}`
                                : ''
                        }}
                    </template>
                </FEllipsis>
            </FTag>
        </template>
    </FSelectCascader>
</template>

<script>
import { h, reactive, ref } from 'vue';
import { PictureOutlined, PlusCircleOutlined } from '@fesjs/fes-design/icon';

function createData(level = 1, baseKey = '', prefix = null, suffix = null) {
    if (!level) {
        return undefined;
    }
    return Array.apply(null, { length: 2 }).map((_, index) => {
        const key = `${baseKey}${level}${index}`;
        return {
            name: createLabel(level),
            id: key,
            child: createData(level - 1, key, prefix, suffix),
            x: `${key}-${createLabel(level)}`,
            prefix: prefix ? () => h(PictureOutlined) : null,
            suffix: suffix ? () => h(PlusCircleOutlined) : null,
        };
    });
}

function createLabel(level) {
    if (level === 4) {
        return '道生一';
    }
    if (level === 3) {
        return '一生二';
    }
    if (level === 2) {
        return '二生三';
    }
    if (level === 1) {
        return '三生万物';
    }
}

export default {
    setup() {
        const data = reactive(createData(4));
        const showPath = ref(true);
        const multiple = ref(true);
        return {
            data,
            showPath,
            multiple,
        };
    },
};
</script>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
</style>

无数据

play
<template>
    <FSelectCascader />
</template>

<style scoped>
.fes-select-cascader {
    width: 200px;
}
</style>

SelectCascader Props

属性说明类型默认值
modelValue / v-model选中的值number / string / boolean / object-
data展示数据Array<CascaderOption>[]
appendToContainer弹窗内容是否添加到指定的 DOM 元素booleantrue
clearable是否显示清除按钮booleanfalse
disabled是否禁用booleanfalse
filterable是否支持过滤选项(remote被设定时不生效)booleanfalse
filter自定义过滤函数(pattern: string, option: TreeOption) => boolean-
filterTextHighlight过滤文本是否高亮booleanfalse
collapseTags多选时选中项是否折叠展示booleanfalse
collapseTagsLimit多选时选中项超出限制个数后才会折叠number1
tagBordered多选时,选中项展示是否有边框(disabled 为 true 时强制有边框)booleanfalse
emptyText选项为空时显示的文字,也可以使用#empty 设置string无数据
getContainer指定下拉选项挂载的 HTML 节点() => HTMLElement() => document.body
multiple是否多选booleanfalse
placeholder当没有选择内容时的提示语string-
expandedKeys(v-model)展开的节点的 key 的数组Array<string | number>[]
cascadecheckable 状态下节点选择完全受控(父子节点选中状态关联)booleanfalse
checkStrictly设置勾选策略来指定勾选回调返回的值。
多选时,all 表示回调函数值为全部选中节点;parent 表示回调函数值为父节点(当父节点下所有子节点都选中时);child 表示回调函数值为子节点。
单选时,all 表示回调函数值可为父节点;child 表示回调函数值为子节点。
stringchild
childrenField替代 CascaderOption 中的 children 字段名stringchildren
valueField替代 CascaderOption 中的 value 字段名stringvalue
labelField替代 CascaderOption 中的 label 字段名stringlabel
remote是否异步获取选项,和 loadData 配合booleanfalse
loadData异步加载数据的回调函数(node: null | CascaderOption) => Promise<CascaderOption[]>-
expandTrigger次级菜单的展开方式,可选值为click,hoverstringclick
emitPathmodelValue 是否返回选中节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值。booleanfalse
showPath是否在选择器中显示选项路径booleanfalse

SelectCascader Events

事件名称说明回调参数
change选中的值发生变化目前选中的值
visibleChange下拉框出现/隐藏时触发出现则为 true,隐藏则为 false
removeTag取消选中时调用,参数为选中项的 value (或 key) 值,仅在 multiple 模式下生效取消选中的值
blur当选择器失去焦点时触发event
focus当选择器获得焦点时触发event
clear点击清除按钮时触发event
filter过滤事件( query: String)

SelectCascader Methods

名称说明
blur取消焦点
focus获取焦点

SelectCascader Slots

名称说明参数
tag控制标签的渲染,自定义选中选项在选择框如何展示{ option: CascaderNode, handleClose: ()=> void }

CascaderOption props

属性说明类型默认值
value节点的 key,需要唯一,可使用 valueField 修改字段名string / number-
label节点的内容,可使用 labelField 修改字段名string-
children?节点的子节点CascaderOption[][]
disabled?是否禁用节点, 默认为Cascader组件的disabledboolean-
selectable?是否禁用选中节点,默认为Cascader组件的selectableboolean-
checkable?是否禁用勾选节点,默认为Cascader组件的checkableboolean-
isLeaf?节点是否是叶节点,在 remote 模式下是必须的booleanfalse
prefix?节点的前缀string / (() => VNodeChild)null
suffix?节点的后缀string / (() => VNodeChild)null

CascaderNode props

属性说明类型默认值
valueCascaderOption 中的 value 字段string / number-
labelCascaderOption 中的 label 字段string-
path节点所在的各级菜单的 valuelabel 所组成的数组Array<{ value: string | number, label: string }>[]