Dropdown 下拉选择器
该组件目前处于实验性状态,需要进行更加严密的测试,API 也可能发生比较大的变动
下拉打开这个抽屉,喜欢的糖果都在里面🍬
import { DuDropdown } from 'dangoui'
使用注意
value
为嵌套对象结构,用于存储每个维度下各分组的选中值- 组件支持单维度或多维度筛选
- 支持单选和多选模式
示例
基础用法
最简单的单维度筛选
<template>
<PreviewBlock title="单维度筛选">
<DuButton @click="handleShow" full size="large">选择价格区间</DuButton>
<DuDropdown
:options="options"
v-model:value="value"
v-model:visible="visible"
@confirm="handleConfirm"
/>
<!-- 结果展示 -->
<div v-if="Object.keys(value)?.length > 0" class="result-display">
<template v-for="(items, key) in value" :key="key">
<div class="result-group">
<div class="result-label">{{options.find(item=>item.value === key).label}}:</div>
<div class="result-tags">
<template v-for="values in items" :key="groupKey">
<DuTag
size="small"
class="result-tag"
>
{{ values.label }}
</DuTag>
</template>
</div>
</div>
</template>
</div>
</PreviewBlock>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DuButton, DuTag, DuDropdown } from 'dangoui'
const value = ref({})
const visible = ref(false)
function handleShow() {
visible.value = true
}
function handleConfirm(selected) {
console.log('选中的选项:', selected)
}
const options = [
{
label: '价格区间',
value: 'price',
multiple: true,
options: [
{ label: '50元以下', value: '0-50' },
{ label: '50-100元', value: '50-100' },
{ label: '100-200元', value: '100-200' },
{ label: '200-500元', value: '200-500' },
{ label: '500元以上', value: '500+' }
]
}
]
</script>
<style scoped>
.result-display {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
font-size: 14px;
}
.result-group {
display: flex;
align-items: flex-start;
gap: 8px;
}
.result-label{
width:100px
}
.result-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.result-tag {
margin-right: 4px;
}
</style>
0
自动关闭
点击选项后自动关闭,适用于单选场景。
<template>
<PreviewBlock title="点击自动关闭">
<DuButton @click="handleShow" full size="large">选择商品类型</DuButton>
<DuDropdown
:options="options"
v-model:value="value"
v-model:visible="visible"
:show-footer="false"
@confirm="handleConfirm"
/>
<!-- 结果展示 -->
<div v-if="Object.keys(value)?.length > 0" class="result-display">
<template v-for="(items, key) in value" :key="key">
<div class="result-group">
<div class="result-label">{{options.find(item=>item.value === key).label}}:</div>
<div class="result-tags">
<template v-for="values in items" :key="groupKey">
<DuTag
size="small"
class="result-tag"
>
{{ values.label }}
</DuTag>
</template>
</div>
</div>
</template>
</div>
</PreviewBlock>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DuButton, DuTag, DuDropdown } from 'dangoui'
const value = ref({})
const visible = ref(false)
function handleShow() {
visible.value = true
}
function handleConfirm(selected) {
console.log('选中的选项:', selected)
}
const options = [
{
label: '商品类型',
multiple: false,
value: 'type',
options: [
{ label: '数码产品', value: 'digital' },
{ label: '家居用品', value: 'home' },
{ label: '服装鞋包', value: 'clothing' },
{ label: '美妆护肤', value: 'beauty' },
{ label: '食品饮料', value: 'food' }
]
}
]
</script>
<style scoped>
.result-display {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
font-size: 14px;
}
.result-group {
display: flex;
align-items: flex-start;
gap: 8px;
}
.result-label{
width:100px
}
.result-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.result-tag {
margin-right: 4px;
}
</style>
0
列表布局
适用于单选场景的列表布局。
<template>
<PreviewBlock title="列表布局">
<DuButton @click="handleShow" full size="large">选择商品类型</DuButton>
<DuDropdown
:options="options"
v-model:value="value"
v-model:visible="visible"
:show-footer="false"
layout="list"
@confirm="handleConfirm"
/>
<!-- 结果展示 -->
<div v-if="Object.keys(value)?.length > 0" class="result-display">
<template v-for="(items, key) in value" :key="key">
<div class="result-group">
<div class="result-label">{{options.find(item=>item.value === key).label}}:</div>
<div class="result-tags">
<template v-for="values in items" :key="values.value">
<DuTag
size="small"
class="result-tag"
>
{{ values.label }}
</DuTag>
</template>
</div>
</div>
</template>
</div>
</PreviewBlock>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DuButton, DuTag, DuDropdown } from 'dangoui'
const value = ref({})
const visible = ref(false)
function handleShow() {
visible.value = true
}
function handleConfirm(selected) {
console.log('选中的选项:', selected)
}
const options = [
{
label: '商品类型',
value: 'type',
multiple: false,
options: [
{ label: '数码产品', value: 'digital' },
{ label: '家居用品', value: 'home' },
{ label: '服装鞋包', value: 'clothing' },
{ label: '美妆护肤', value: 'beauty' },
{ label: '食品饮料', value: 'food' }
]
}
]
</script>
<style scoped>
.result-display {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
font-size: 14px;
}
.result-group {
display: flex;
align-items: flex-start;
gap: 8px;
}
.result-label {
width: 100px;
}
.result-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
</style>
0
多维度筛选
支持多个筛选维度的复杂场景
<template>
<PreviewBlock title="多维度多分组筛选">
<DuButton @click="handleShow" full size="large">打开筛选</DuButton>
<DuDropdown
:options="options"
v-model:value="value"
v-model:visible="visible"
@confirm="handleConfirm"
/>
<!-- 分组展示结果 -->
<div v-if="Object.keys(value).length > 0" class="complex-result">
<div class="result-header">
<h4>筛选结果</h4>
<DuButton type="text" size="small" @click="clearAll">清空全部</DuButton>
</div>
<div class="result-content">
<template v-for="(items, key) in value" :key="key">
<div v-if="hasSelectedItems(items)" class="result-option">
<div class="option-header">
<span class="option-label">{{ getOptionLabel(key) }}</span>
</div>
<!-- 处理直接options的情况 -->
<template v-if="!hasGroups(key)">
<div class="option-tags">
<DuTag
v-for="item in items"
:key="item.value"
size="small"
color="primary"
closeable
@close="() => handleTagClick(key, item)"
>
{{ item.label }}
</DuTag>
</div>
</template>
<!-- 处理groups的情况 -->
<template v-else>
<div class="groups-container">
<template v-for="(values, groupKey) in items" :key="groupKey">
<div v-if="values.length > 0" class="group-item">
<div class="group-header">
<span class="group-label">{{ getGroupLabel(key, groupKey) }}</span>
</div>
<div class="group-tags">
<DuTag
v-for="item in values"
:key="item.value"
size="small"
color="primary"
closeable
@close="() => handleTagClick(key, item)"
>
{{ item.label }}
</DuTag>
</div>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</div>
</PreviewBlock>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DuButton, DuTag,DuDropdown } from 'dangoui'
const value = ref({})
const visible = ref(false)
function hasSelectedItems(items: any) {
if(Array.isArray(items)) return items.length>0
return Object.values(items).some(group => (group as any[]).length > 0)
}
function getOptionLabel(key: string) {
return options.find(opt => opt.value === key)?.label || key
}
function hasGroups(key: string) {
const option = options.find(opt => opt.value === key)
return option && 'groups' in option
}
function getGroupLabel(optionKey: string, groupKey: string) {
const option = options.find(opt => opt.value === optionKey)
if (!option || !('groups' in option)) return groupKey
const group = option.groups.find(g => g.value === groupKey)
return group?.label || groupKey
}
function handleTagClick(optionKey: string, item: any) {
const option = options.find(opt => opt.value === optionKey)
if (!option) return
if ('options' in option) {
const currentOptions = value.value[optionKey] || []
const index = currentOptions.findIndex((opt: any) => opt.value === item.value)
if (index > -1) {
currentOptions.splice(index, 1)
if (currentOptions.length === 0) {
delete value.value[optionKey]
}
}
} else {
const groups = value.value[optionKey] || {}
Object.entries(groups).forEach(([groupKey, options]: [string, any]) => {
const index = options.findIndex((opt: any) => opt.value === item.value)
if (index > -1) {
options.splice(index, 1)
if (options.length === 0) {
delete groups[groupKey]
}
if (Object.keys(groups).length === 0) {
delete value.value[optionKey]
}
}
})
}
}
function handleShow() {
visible.value = true
}
function handleConfirm(selected: any) {
console.log('选中的选项:', selected)
}
function clearAll() {
value.value = {}
}
const options = [
{
label: '商品分类',
value: 'category',
groups: [
{
label: '食品饮料',
value: 'food_drink',
multiple: true,
options: [
{ label: '零食小吃', value: 'snacks' },
{ label: '饮料冲调', value: 'drinks' },
{ label: '生鲜果蔬', value: 'fresh' },
{ label: '粮油调味', value: 'condiment' }
]
},
{
label: '服装鞋包',
value: 'clothing_bags',
multiple: true,
options: [
{ label: '上衣', value: 'tops' },
{ label: '裤装', value: 'pants' },
{ label: '裙装', value: 'dresses' },
{ label: '箱包', value: 'bags' }
]
}
]
},
{
label: '价格区间',
value: 'price_range',
multiple: false,
options: [
{ label: '50元以下', value: '0-50' },
{ label: '50-100元', value: '50-100' },
{ label: '100-200元', value: '100-200' },
{ label: '200-500元', value: '200-500' },
{ label: '500元以上', value: '500+' }
]
},
{
label: '商品属性',
value: 'product_attrs',
groups: [
{
label: '优惠活动',
value: 'promotions',
multiple: true,
options: [
{ label: '限时特惠', value: 'time_limited' },
{ label: '满减优惠', value: 'full_reduction' },
{ label: '会员专享', value: 'vip_only' },
{ label: '赠品活动', value: 'gift' }
]
},
{
label: '配送方式',
value: 'delivery',
multiple: false,
options: [
{ label: '包邮', value: 'free_shipping' },
{ label: '极速达', value: 'fast_delivery' },
{ label: '同城配送', value: 'local_delivery' }
]
}
]
}
]
</script>
<style scoped>
.complex-result {
border: 1px solid #eee;
border-radius: 8px;
width: 100%;
background: #fff;
padding: 12px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.result-header h4 {
margin: 0;
font-size: 15px;
font-weight: 500;
}
.result-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.result-option {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-header {
display: flex;
align-items: center;
gap: 8px;
}
.option-label {
font-size: 14px;
font-weight: 500;
}
.option-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 4px 0;
}
.groups-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.group-item {
display: flex;
flex-direction: column;
gap: 6px;
padding-left: 12px;
}
.group-header {
display: flex;
align-items: center;
}
.group-label {
font-size: 13px;
}
.group-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.du-tag) {
cursor: pointer;
transition: all 0.2s;
}
:deep(.du-tag:hover) {
opacity: 0.85;
}
</style>
0
自定义样式
通过 option-nav
和 content
插槽自定义选择栏和内容区域的样式。
<template>
<PreviewBlock title="自定义样式">
<DuButton @click="handleShow" full size="large">打开筛选</DuButton>
<DuDropdown
:options="options"
v-model:value="value"
v-model:visible="visible"
@confirm="handleConfirm"
>
<!-- 自定义选择栏 -->
<template #option-nav="{ options, currentIndex, onChange }">
<div class="custom-nav">
<div
v-for="(option, index) in options"
:key="option.value"
:class="['custom-nav-item', { active: currentIndex === index }]"
@click="onChange(index)"
>
<DuIcon name="like-normal" :size="12" />
<span>{{ option.label }}</span>
</div>
</div>
</template>
<!-- 自定义内容区域 -->
<template #content="{ currentOption, currentGroups, isSelected, onSelect }">
<div class="custom-content">
<div
v-for="group in currentGroups"
:key="group.value"
class="custom-group"
>
<div v-if="currentGroups?.length > 1" class="custom-group-title">
{{ group.label }}
</div>
<div class="custom-options">
<div
v-for="option in group.options"
:key="option.value"
:class="['custom-option', { active: isSelected(option, group) }]"
@click="onSelect(option, group)"
>
<DuIcon name="wishing" :size="12" />
<span>{{ option.label }}</span>
</div>
</div>
</div>
</div>
</template>
</DuDropdown>
<!-- 结果展示 -->
<div v-if="Object.keys(value)?.length > 0" class="result-display">
<template v-for="(items, key) in value" :key="key">
<div class="result-group">
<div class="result-label">{{options.find(item=>item.value === key).label}}:</div>
<div class="result-tags">
<template v-for="values in items" :key="groupKey">
<DuTag
size="small"
class="result-tag"
>
{{ values.label }}
</DuTag>
</template>
</div>
</div>
</template>
</div>
</PreviewBlock>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { DuButton, DuTag, DuIcon, DuDropdown } from 'dangoui'
const value = ref({})
const visible = ref(false)
function handleShow() {
visible.value = true
}
function handleConfirm(selected) {
console.log('选中的选项:', selected)
}
const options = [
{
label: '商品类型',
value: 'type',
multiple: true,
options: [
{ label: '数码产品', value: 'digital' },
{ label: '家居用品', value: 'home' },
{ label: '服装鞋包', value: 'clothing' },
{ label: '美妆护肤', value: 'beauty' },
{ label: '食品饮料', value: 'food' }
]
},
{
label: '价格区间',
value: 'price',
multiple: false,
options: [
{ label: '50元以下', value: '0-50' },
{ label: '50-100元', value: '50-100' },
{ label: '100-200元', value: '100-200' },
{ label: '200-500元', value: '200-500' },
{ label: '500元以上', value: '500+' }
]
}
]
</script>
<style scoped>
/* 基础样式 */
.custom-nav,
.custom-nav-item,
.custom-option {
display: flex;
align-items: center;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 导航栏样式 */
.custom-nav {
padding: 4px;
box-shadow: inset 0 0 0 1px #eee;
}
.custom-nav-item {
flex: 1;
justify-content: center;
gap: 6px;
padding: 10px;
font-size: 14px;
color: #666;
cursor: pointer;
}
.custom-nav-item:hover:not(.active),
.custom-option:hover:not(.active) {
color: #ff4d8c;
border-color: #ff4d8c;
}
.custom-nav-item.active,
.custom-option.active {
color: #ff4d8c;
background: rgba(255, 77, 140, 0.08);
border-color: #ff4d8c;
font-weight: 600;
}
/* 内容区域样式 */
.custom-content {
padding: 16px;
}
.custom-group {
margin-bottom: 16px;
}
.custom-group-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 14px;
color: #666;
}
.custom-group-title::after {
content: '';
flex: 1;
height: 1px;
background: #eee;
}
.custom-options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.custom-option {
gap: 8px;
padding: 10px 14px;
border: 1px solid #eee;
font-size: 14px;
color: #666;
cursor: pointer;
}
/* 图标样式 */
:deep(.du-icon) {
font-size: 16px;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.custom-nav-item:hover :deep(.du-icon),
.custom-option:hover :deep(.du-icon) {
color: #ff4d8c;
transform: scale(1.1);
}
/* 结果展示样式 */
.result-display {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
font-size: 14px;
}
.result-group {
display: flex;
align-items: flex-start;
gap: 8px;
}
.result-label {
width: 100px;
}
.result-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
</style>
0
API
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | SelectedValue | {} | 当前选中的值 |
confirm-text | string | "\u786E\u5B9A" | 确认按钮文本 |
cancel-text | string | "\u53D6\u6D88" | 取消按钮文本 |
show-footer | boolean | true | 是否显示底部按钮 |
layout | "list" | "tag" | "tag" | 布局方式 |
visible | boolean | - | 是否显示 |
options | FilterField[] | - | 选项配置 |
插槽
名称 |
---|
option-nav |
content |
事件
名称 | 类型 |
---|---|
confirm | (event: "confirm", value: SelectedValue): void |
update:visible | (event: "update:visible", visible: boolean): void |
update:value | (event: "update:value", value: SelectedValue): void |