下拉选择框

下拉选择框是表单中常用的选择控件,适合选项较多时使用。Vue 的 v-model 让下拉框的数据绑定变得简单,但仍有一些细节需要注意。

基本用法

单选下拉框

<div id="app">
  <select v-model="selected">
    <option value="">请选择</option>
    <option value="beijing">北京</option>
    <option value="shanghai">上海</option>
    <option value="guangzhou">广州</option>
    <option value="shenzhen">深圳</option>
  </select>
  <p>已选择:{{ selected }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    selected: ''
  }
})
</script>

动态生成选项

使用 v-for 动态生成选项:

<div id="app">
  <select v-model="selected">
    <option value="">请选择城市</option>
    <option 
      v-for="city in cities" 
      :key="city.value"
      :value="city.value"
    >
      {{ city.label }}
    </option>
  </select>
  <p>已选择:{{ selected }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    selected: '',
    cities: [
      { value: 'beijing', label: '北京' },
      { value: 'shanghai', label: '上海' },
      { value: 'guangzhou', label: '广州' },
      { value: 'shenzhen', label: '深圳' }
    ]
  }
})
</script>

绑定对象值

使用 v-bind 绑定完整的对象:

<div id="app">
  <select v-model="selectedCity">
    <option value="">请选择</option>
    <option 
      v-for="city in cities" 
      :key="city.id"
      :value="city"
    >
      {{ city.name }} - {{ city.province }}
    </option>
  </select>
  
  <div v-if="selectedCity">
    <h3>{{ selectedCity.name }}</h3>
    <p>省份:{{ selectedCity.province }}</p>
    <p>人口:{{ selectedCity.population }}</p>
  </div>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    selectedCity: null,
    cities: [
      { id: 1, name: '北京', province: '北京', population: '2171万' },
      { id: 2, name: '上海', province: '上海', population: '2487万' },
      { id: 3, name: '广州', province: '广东', population: '1867万' }
    ]
  }
})
</script>

多选下拉框

添加 multiple 属性实现多选:

<div id="app">
  <select v-model="selected" multiple>
    <option value="apple">苹果</option>
    <option value="banana">香蕉</option>
    <option value="orange">橙子</option>
    <option value="grape">葡萄</option>
  </select>
  <p>已选择:{{ selected }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    selected: []  // 多选时必须是数组
  }
})
</script>

注意

多选下拉框需要按住 Ctrl(Windows)或 Command(Mac)才能选择多个选项,用户体验不如复选框好。实际开发中,多个复选框或标签选择器更常用。

选项分组

使用 <optgroup> 对选项进行分组:

<div id="app">
  <select v-model="selected">
    <option value="">请选择城市</option>
    <optgroup label="华北地区">
      <option value="beijing">北京</option>
      <option value="tianjin">天津</option>
    </optgroup>
    <optgroup label="华东地区">
      <option value="shanghai">上海</option>
      <option value="hangzhou">杭州</option>
      <option value="nanjing">南京</option>
    </optgroup>
    <optgroup label="华南地区">
      <option value="guangzhou">广州</option>
      <option value="shenzhen">深圳</option>
    </optgroup>
  </select>
  <p>已选择:{{ selected }}</p>
</div>

动态生成分组

<div id="app">
  <select v-model="selected">
    <option value="">请选择</option>
    <optgroup 
      v-for="group in groups" 
      :key="group.label"
      :label="group.label"
    >
      <option 
        v-for="item in group.options" 
        :key="item.value"
        :value="item.value"
      >
        {{ item.label }}
      </option>
    </optgroup>
  </select>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    selected: '',
    groups: [
      {
        label: '华北地区',
        options: [
          { value: 'beijing', label: '北京' },
          { value: 'tianjin', label: '天津' }
        ]
      },
      {
        label: '华东地区',
        options: [
          { value: 'shanghai', label: '上海' },
          { value: 'hangzhou', label: '杭州' }
        ]
      }
    ]
  }
})
</script>

级联选择

省市区三级联动是典型的级联选择场景:

<div id="app">
  <select v-model="province" @change="onProvinceChange">
    <option value="">请选择省份</option>
    <option 
      v-for="p in provinces" 
      :key="p.code"
      :value="p.code"
    >
      {{ p.name }}
    </option>
  </select>
  
  <select v-model="city" @change="onCityChange" :disabled="!province">
    <option value="">请选择城市</option>
    <option 
      v-for="c in cities" 
      :key="c.code"
      :value="c.code"
    >
      {{ c.name }}
    </option>
  </select>
  
  <select v-model="district" :disabled="!city">
    <option value="">请选择区县</option>
    <option 
      v-for="d in districts" 
      :key="d.code"
      :value="d.code"
    >
      {{ d.name }}
    </option>
  </select>
  
  <p>已选择:{{ fullAddress }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    province: '',
    city: '',
    district: '',
    provinces: [
      { code: '11', name: '北京市' },
      { code: '31', name: '上海市' },
      { code: '44', name: '广东省' }
    ],
    allCities: {
      '11': [{ code: '1101', name: '北京市' }],
      '31': [{ code: '3101', name: '上海市' }],
      '44': [
        { code: '4401', name: '广州市' },
        { code: '4403', name: '深圳市' }
      ]
    },
    allDistricts: {
      '1101': [
        { code: '110101', name: '东城区' },
        { code: '110102', name: '西城区' }
      ],
      '3101': [
        { code: '310101', name: '黄浦区' },
        { code: '310104', name: '徐汇区' }
      ],
      '4401': [
        { code: '440103', name: '荔湾区' },
        { code: '440104', name: '越秀区' }
      ],
      '4403': [
        { code: '440303', name: '罗湖区' },
        { code: '440304', name: '福田区' }
      ]
    }
  },
  computed: {
    cities: function() {
      return this.province ? this.allCities[this.province] || [] : []
    },
    districts: function() {
      return this.city ? this.allDistricts[this.city] || [] : []
    },
    fullAddress: function() {
      if (!this.province) return ''
      const p = this.provinces.find(item => item.code === this.province)
      const c = this.cities.find(item => item.code === this.city)
      const d = this.districts.find(item => item.code === this.district)
      return [p && p.name, c && c.name, d && d.name].filter(Boolean).join(' ')
    }
  },
  methods: {
    onProvinceChange: function() {
      this.city = ''
      this.district = ''
    },
    onCityChange: function() {
      this.district = ''
    }
  }
})
</script>

搜索过滤

结合搜索框实现可搜索的下拉框:

<div id="app">
  <div class="search-select">
    <input 
      type="text" 
      v-model="keyword"
      @focus="showOptions = true"
      placeholder="搜索城市..."
    >
    <ul v-if="showOptions && filteredOptions.length > 0" class="options">
      <li 
        v-for="option in filteredOptions" 
        :key="option.value"
        @click="selectOption(option)"
      >
        {{ option.label }}
      </li>
    </ul>
    <p v-if="showOptions && filteredOptions.length === 0">无匹配结果</p>
  </div>
  
  <p>已选择:{{ selected }}</p>
</div>

<style>
.search-select {
  position: relative;
}
.options {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  border: 1px solid #ddd;
  max-height: 200px;
  overflow-y: auto;
  background: white;
  list-style: none;
  padding: 0;
  margin: 0;
}
.options li {
  padding: 8px 12px;
  cursor: pointer;
}
.options li:hover {
  background: #f5f5f5;
}
</style>

<script>
new Vue({
  el: '#app',
  data: {
    keyword: '',
    selected: '',
    showOptions: false,
    options: [
      { value: 'beijing', label: '北京' },
      { value: 'shanghai', label: '上海' },
      { value: 'guangzhou', label: '广州' },
      { value: 'shenzhen', label: '深圳' },
      { value: 'hangzhou', label: '杭州' },
      { value: 'nanjing', label: '南京' }
    ]
  },
  computed: {
    filteredOptions: function() {
      if (!this.keyword) return this.options
      return this.options.filter(option => 
        option.label.includes(this.keyword) || 
        option.value.includes(this.keyword)
      )
    }
  },
  methods: {
    selectOption: function(option) {
      this.selected = option.value
      this.keyword = option.label
      this.showOptions = false
    }
  },
  mounted: function() {
    document.addEventListener('click', (e) => {
      if (!this.$el.contains(e.target)) {
        this.showOptions = false
      }
    })
  }
})
</script>

自定义下拉组件

原生 <select> 样式难以自定义,实际开发中常用自定义组件:

<div id="app">
  <custom-select 
    v-model="selected"
    :options="options"
    placeholder="请选择城市"
  ></custom-select>
  <p>已选择:{{ selected }}</p>
</div>

<script>
Vue.component('custom-select', {
  props: ['value', 'options', 'placeholder'],
  template: `
    <div class="custom-select" @click="toggle">
      <div class="select-input">
        <span v-if="selectedOption">{{ selectedOption.label }}</span>
        <span v-else class="placeholder">{{ placeholder }}</span>
        <span class="arrow" :class="{ open: isOpen }">▼</span>
      </div>
      <ul v-if="isOpen" class="select-options">
        <li 
          v-for="option in options" 
          :key="option.value"
          :class="{ selected: option.value === value }"
          @click.stop="select(option)"
        >
          {{ option.label }}
        </li>
      </ul>
    </div>
  `,
  data: function() {
    return {
      isOpen: false
    }
  },
  computed: {
    selectedOption: function() {
      return this.options.find(opt => opt.value === this.value)
    }
  },
  methods: {
    toggle: function() {
      this.isOpen = !this.isOpen
    },
    select: function(option) {
      this.$emit('input', option.value)
      this.isOpen = false
    }
  },
  mounted: function() {
    document.addEventListener('click', (e) => {
      if (!this.$el.contains(e.target)) {
        this.isOpen = false
      }
    })
  }
})

new Vue({
  el: '#app',
  data: {
    selected: '',
    options: [
      { value: 'beijing', label: '北京' },
      { value: 'shanghai', label: '上海' },
      { value: 'guangzhou', label: '广州' },
      { value: 'shenzhen', label: '深圳' }
    ]
  }
})
</script>

<style>
.custom-select {
  position: relative;
  width: 200px;
}
.select-input {
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
}
.placeholder {
  color: #999;
}
.arrow {
  font-size: 12px;
  transition: transform 0.3s;
}
.arrow.open {
  transform: rotate(180deg);
}
.select-options {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  list-style: none;
  padding: 0;
  margin: 4px 0 0 0;
  max-height: 200px;
  overflow-y: auto;
}
.select-options li {
  padding: 8px 12px;
  cursor: pointer;
}
.select-options li:hover {
  background: #f5f5f5;
}
.select-options li.selected {
  background: #42b983;
  color: white;
}
</style>

小结

下拉选择框处理要点:

  1. 单选:绑定字符串,选中值赋给变量
  2. 多选:添加 multiple 属性,绑定数组
  3. 动态选项:使用 v-for 生成选项
  4. 对象绑定:使用 :value 绑定完整对象
  5. 级联选择:监听变化,重置下级选项