Ant Design Vue中如何实现省市穿梭框

发布时间:2021-12-24 09:02:41 作者:iii
来源:亿速云 阅读:295
# Ant Design Vue中如何实现省市穿梭框

## 前言

在Web应用开发中,地区选择器是常见的表单控件需求。当需要用户选择省市级联数据时,穿梭框(Transfer)组件能够提供直观的双栏选择交互体验。Ant Design Vue作为企业级UI框架,其穿梭框组件结合中国行政区划数据,可以构建出高效的省市选择器。

本文将详细介绍在Ant Design Vue中实现省市穿梭框的完整方案,涵盖以下核心内容:

1. Ant Design Vue穿梭框组件基础用法
2. 中国行政区划数据获取与处理
3. 省市级联数据结构的实现
4. 完整可复用的省市穿梭框组件封装
5. 性能优化与特殊场景处理
6. 实际应用案例与扩展思路

## 一、Ant Design Vue穿梭框组件基础

### 1.1 穿梭框组件介绍

穿梭框(Transfer)是Ant Design Vue提供的用于在两栏中移动元素的控件,常用于多项选择场景。其核心特点包括:

- 双栏布局(源列表/目标列表)
- 搜索过滤功能
- 自定义渲染支持
- 全选/反选操作

### 1.2 基础使用示例

```html
<template>
  <a-transfer
    :data-source="dataSource"
    :target-keys="targetKeys"
    :render="item => item.title"
    @change="handleChange"
  />
</template>

<script>
export default {
  data() {
    return {
      dataSource: [
        { key: '1', title: '选项1' },
        { key: '2', title: '选项2' },
        // ...
      ],
      targetKeys: []
    }
  },
  methods: {
    handleChange(targetKeys) {
      this.targetKeys = targetKeys
    }
  }
}
</script>

1.3 关键API说明

属性 说明 类型 默认值
dataSource 数据源 Array []
targetKeys 显示在右侧框数据的key集合 Array []
render 每行数据渲染函数 function(record) -
filterOption 搜索过滤函数 function(inputValue, option) -
showSearch 是否显示搜索框 boolean false

二、中国行政区划数据处理

2.1 数据来源选择

实现省市穿梭框需要可靠的行政区划数据源,常见选择:

  1. 国家统计局数据:权威但更新不及时
  2. 高德/百度地图API:实时性好但需要联网
  3. 第三方维护的JSON数据:如china-division等npm包

推荐使用china-division包:

npm install china-division

2.2 数据结构处理

原始数据通常为嵌套结构,需要转换为扁平化结构供穿梭框使用:

import { provinces, cities } from 'china-division'

// 处理省份数据
const processProvinceData = () => {
  return provinces.map(province => ({
    key: province.code,
    title: province.name,
    isLeaf: false // 标记为非叶子节点(有下级城市)
  }))
}

// 处理城市数据
const processCityData = () => {
  return cities.map(city => ({
    key: city.code,
    title: city.name,
    pcode: city.provinceCode, // 关联父级省份
    isLeaf: true // 标记为叶子节点
  }))
}

2.3 数据缓存策略

考虑到行政区划数据较大,应采用缓存策略:

// 在Vuex中存储处理后的数据
const store = new Vuex.Store({
  state: {
    provinceData: [],
    cityData: []
  },
  mutations: {
    SET_REGION_DATA(state, { provinces, cities }) {
      state.provinceData = provinces
      state.cityData = cities
    }
  },
  actions: {
    async loadRegionData({ commit }) {
      if (this.state.provinceData.length > 0) return
      
      const provinces = processProvinceData()
      const cities = processCityData()
      commit('SET_REGION_DATA', { provinces, cities })
    }
  }
})

三、省市级联穿梭框实现

3.1 组件基础结构

创建ProvinceCityTransfer.vue组件:

<template>
  <div class="province-city-transfer">
    <a-transfer
      :data-source="formattedData"
      :target-keys="selectedKeys"
      :render="renderItem"
      :show-search="true"
      :filter-option="filterOption"
      @change="handleChange"
    />
  </div>
</template>

<script>
export default {
  name: 'ProvinceCityTransfer',
  props: {
    value: { type: Array, default: () => [] }
  },
  data() {
    return {
      allProvinces: [],
      allCities: [],
      loadedKeys: [], // 已加载的省份key
      selectedKeys: this.value
    }
  },
  computed: {
    formattedData() {
      // 合并省份和已加载城市数据
      return [...this.allProvinces, ...this.loadedCities]
    },
    loadedCities() {
      return this.allCities.filter(city => 
        this.loadedKeys.includes(city.pcode)
      )
    }
  }
}
</script>

3.2 异步加载城市数据

实现省份展开时动态加载对应城市:

methods: {
  async loadData() {
    await this.$store.dispatch('loadRegionData')
    this.allProvinces = this.$store.state.provinceData
    this.allCities = this.$store.state.cityData
  },
  
  handleExpand(expandedKeys) {
    // 找出新展开的省份key
    const newKeys = expandedKeys.filter(
      key => !this.loadedKeys.includes(key)
    )
    
    if (newKeys.length > 0) {
      this.loadedKeys = [...this.loadedKeys, ...newKeys]
    }
  }
}

3.3 自定义渲染与搜索

methods: {
  renderItem(item) {
    return (
      <span class={`transfer-item ${item.isLeaf ? 'is-city' : 'is-province'}`}>
        {item.title}
      </span>
    )
  },
  
  filterOption(inputValue, option) {
    return (
      option.title.includes(inputValue) || 
      this.getProvinceName(option.pcode).includes(inputValue)
    )
  },
  
  getProvinceName(pcode) {
    const province = this.allProvinces.find(p => p.key === pcode)
    return province ? province.title : ''
  }
}

3.4 双向数据绑定

methods: {
  handleChange(targetKeys, direction, moveKeys) {
    this.selectedKeys = targetKeys
    this.$emit('input', targetKeys)
    this.$emit('change', targetKeys, direction, moveKeys)
  }
},
watch: {
  value(newVal) {
    this.selectedKeys = newVal
  }
}

四、完整组件实现与优化

4.1 完整组件代码

<!-- ProvinceCityTransfer.vue -->
<template>
  <div class="province-city-transfer">
    <a-transfer
      :data-source="formattedData"
      :target-keys="selectedKeys"
      :render="renderItem"
      :show-search="true"
      :filter-option="filterOption"
      :list-style="listStyle"
      :titles="['待选区', '已选区']"
      :operations="['添加选择', '移除选择']"
      @change="handleChange"
      @expand="handleExpand"
    >
      <template #footer="{ direction }">
        <div class="transfer-footer">
          {{
            direction === 'left' 
              ? `共${allProvinces.length}个省份` 
              : `已选${selectedKeys.length}项`
          }}
        </div>
      </template>
    </a-transfer>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'ProvinceCityTransfer',
  props: {
    value: { type: Array, default: () => [] },
    maxSelected: { type: Number, default: 10 }
  },
  data() {
    return {
      loadedKeys: [],
      selectedKeys: [...this.value],
      listStyle: {
        width: '300px',
        height: '400px'
      }
    }
  },
  computed: {
    ...mapState(['provinceData', 'cityData']),
    allProvinces() {
      return this.provinceData || []
    },
    allCities() {
      return this.cityData || []
    },
    formattedData() {
      return [...this.allProvinces, ...this.loadedCities]
    },
    loadedCities() {
      return this.allCities.filter(city => 
        this.loadedKeys.includes(city.pcode)
      )
    }
  },
  created() {
    this.loadData()
  },
  methods: {
    async loadData() {
      await this.$store.dispatch('loadRegionData')
    },
    
    handleExpand(expandedKeys) {
      const newKeys = expandedKeys.filter(
        key => !this.loadedKeys.includes(key)
      )
      if (newKeys.length > 0) {
        this.loadedKeys = [...this.loadedKeys, ...newKeys]
      }
    },
    
    handleChange(targetKeys, direction, moveKeys) {
      if (this.maxSelected && targetKeys.length > this.maxSelected) {
        this.$message.warning(`最多只能选择${this.maxSelected}项`)
        return
      }
      
      this.selectedKeys = targetKeys
      this.$emit('input', targetKeys)
      this.$emit('change', {
        keys: targetKeys,
        direction,
        movedKeys: moveKeys,
        provinces: this.getSelectedProvinces(),
        cities: this.getSelectedCities()
      })
    },
    
    getSelectedProvinces() {
      return this.allProvinces.filter(
        p => this.selectedKeys.includes(p.key)
      )
    },
    
    getSelectedCities() {
      return this.allCities.filter(
        c => this.selectedKeys.includes(c.key)
      )
    },
    
    renderItem(item) {
      const isCity = item.isLeaf
      return (
        <span class={`transfer-item ${isCity ? 'is-city' : 'is-province'}`}>
          {isCity && (
            <span class="city-prefix">
              {this.getProvinceName(item.pcode)} - 
            </span>
          )}
          {item.title}
        </span>
      )
    },
    
    filterOption(inputValue, option) {
      if (!inputValue) return true
      const matchesTitle = option.title.includes(inputValue)
      if (option.isLeaf) {
        return (
          matchesTitle || 
          this.getProvinceName(option.pcode).includes(inputValue)
        )
      }
      return matchesTitle
    },
    
    getProvinceName(pcode) {
      const province = this.allProvinces.find(p => p.key === pcode)
      return province ? province.title : ''
    }
  },
  watch: {
    value(newVal) {
      if (JSON.stringify(newVal) !== JSON.stringify(this.selectedKeys)) {
        this.selectedKeys = [...newVal]
      }
    }
  }
}
</script>

<style scoped>
.province-city-transfer {
  margin: 20px 0;
}

.transfer-item.is-province {
  font-weight: bold;
}

.transfer-item.is-city {
  padding-left: 12px;
  color: #666;
}

.city-prefix {
  color: #999;
  margin-right: 4px;
}

.transfer-footer {
  padding: 8px;
  text-align: center;
  background: #fafafa;
  border-top: 1px solid #e8e8e8;
}
</style>

4.2 性能优化策略

  1. 虚拟滚动:对于大数据量使用a-virtual-scroll优化
  2. 按需加载:只在展开省份时加载对应城市
  3. 防抖处理:搜索输入框添加防抖
  4. 缓存策略:使用Vuex存储已处理数据
// 在组件中添加防抖搜索
import { debounce } from 'lodash'

methods: {
  filterOption: debounce(function(inputValue, option) {
    // 过滤逻辑
  }, 300)
}

4.3 边界情况处理

  1. 空数据处理
watch: {
  allProvinces(newVal) {
    if (newVal.length > 0 && this.selectedKeys.length === 0) {
      // 默认选中第一个省份
      this.handleChange([newVal[0].key], 'right', [newVal[0].key])
    }
  }
}
  1. 无效key过滤
props: {
  value: {
    type: Array,
    default: () => [],
    validator: value => Array.isArray(value) && 
      value.every(item => typeof item === 'string')
  }
}
  1. 最大选择限制
handleChange(targetKeys) {
  if (this.maxSelected && targetKeys.length > this.maxSelected) {
    this.$message.warning(`最多选择${this.maxSelected}项`)
    // 保持原状态
    return false
  }
  // 正常处理
}

五、实际应用案例

5.1 在表单中使用

<template>
  <a-form :form="form" @submit="handleSubmit">
    <a-form-item label="服务覆盖区域">
      <province-city-transfer 
        v-decorator="['regions', { rules: [{ required: true }] }]"
      />
    </a-form-item>
    <a-form-item>
      <a-button type="primary" html-type="submit">提交</a-button>
    </a-form-item>
  </a-form>
</template>

<script>
import ProvinceCityTransfer from './ProvinceCityTransfer.vue'

export default {
  components: { ProvinceCityTransfer },
  beforeCreate() {
    this.form = this.$form.createForm(this)
  },
  methods: {
    handleSubmit(e) {
      e.preventDefault()
      this.form.validateFields((err, values) => {
        if (!err) {
          console.log('提交数据:', values)
        }
      })
    }
  }
}
</script>

5.2 与服务端交互

// 从API获取已选区域
async fetchSelectedRegions() {
  const res = await api.getUserRegions()
  if (res.success) {
    this.selectedKeys = res.data.map(item => item.regionCode)
  }
},

// 提交选中区域
async submitRegions() {
  const payload = {
    regions: this.selectedKeys,
    regionNames: this.getSelectedNames()
  }
  
  const res = await api.updateUserRegions(payload)
  if (res.success) {
    this.$message.success('保存成功')
  }
},

// 获取选中区域的名称组合
getSelectedNames() {
  const provinces = this.getSelectedProvinces()
  const cities = this.getSelectedCities()
  
  return [
    ...provinces.map(p => p.title),
    ...cities.map(c => `${this.getProvinceName(c.pcode)}-${c.title}`)
  ].join(', ')
}

5.3 扩展功能:地区统计

<template>
  <div>
    <province-city-transfer v-model="selectedRegions" />
    
    <a-divider />
    
    <h3>地区统计</h3>
    <a-table 
      :columns="statsColumns" 
      :data-source="regionStats"
      :pagination="false"
    />
  </div>
</template>

<script>
const statsColumns = [
  { title: '省份', dataIndex: 'province' },
  { title: '城市数量', dataIndex: 'cityCount' },
  { title: '覆盖率', dataIndex: 'coverage' }
]

export default {
  data() {
    return {
      selectedRegions: [],
      statsColumns,
      regionStats: []
    }
  },
  watch: {
    selectedRegions: {
      handler() {
        this.calculateStats()
      },
      deep: true
    }
  },
  methods: {
    calculateStats() {
      const selectedProvinces = this.getSelectedProvinces()
      const selectedCities = this.getSelectedCities()
      
      this.regionStats = selectedProvinces.map(province => {
        const provinceCities = selectedCities.filter(
          c => c.pcode === province.key
        )
        const allCities = this.allCities.filter(
          c => c.pcode === province.key
        )
        
        return {
          key: province.key,
          province: province.title,
          cityCount: `${provinceCities.length}/${allCities.length}`,
          coverage: `${Math.round(
            (provinceCities.length / allCities.length) * 100
          )}%`
        }
      })
    }
  }
}
</script>

六、扩展与进阶

6.1 支持三级联动(区县)

扩展数据结构支持区县级选择:

// 数据处理
const processDistrictData = () => {
  return districts.map(district => ({
    key: district.code,
    title: district.name,
    ccode: district.cityCode, // 关联父级城市
    isLeaf: true
  }))
}

// 修改加载逻辑
handleExpand(expandedKeys) {
  expandedKeys.forEach(key => {
    if (!this.loadedKeys.includes(key)) {
      // 加载下级区域
      if (this.allProvinces.some(p => p.key === key)) {
        // 加载城市
        this.loadedKeys.push(key)
      } else if (this.allCities.some(c => c.key === key)) {
        // 加载区县
        this.loadedDistricts = [
          ...this.loadedDistricts,
          ...this.allDistricts.filter(d => d.ccode === key)
        ]
      }
    }
  })
}

6.2 与地图组件联动

”`javascript // 使用百度地图API示例 methods: { highlightOnMap() { const map = this.$refs.map.instance this.getSelectedCities().forEach(city => { const point = new BMap.Point(city.lng, city.lat) const marker = new BMap.Marker(point

推荐阅读:
  1. ant-design-vue 快速避坑指南(推荐)
  2. ant-design-vue如何实现表格内部字段验证功能

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

ant design vue

上一篇:ROS中的cmake指的是什么

下一篇:linux中如何删除用户组

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》