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-navcontent 插槽自定义选择栏和内容区域的样式。

<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

属性

属性名类型默认值说明
valueSelectedValue{}当前选中的值
confirm-textstring"\u786E\u5B9A"确认按钮文本
cancel-textstring"\u53D6\u6D88"取消按钮文本
show-footerbooleantrue是否显示底部按钮
layout"list" | "tag""tag"布局方式
visibleboolean - 是否显示
optionsFilterField[] - 选项配置

插槽

名称
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
本页包含