您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# 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>
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
dataSource | 数据源 | Array | [] |
targetKeys | 显示在右侧框数据的key集合 | Array | [] |
render | 每行数据渲染函数 | function(record) | - |
filterOption | 搜索过滤函数 | function(inputValue, option) | - |
showSearch | 是否显示搜索框 | boolean | false |
实现省市穿梭框需要可靠的行政区划数据源,常见选择:
推荐使用china-division
包:
npm install china-division
原始数据通常为嵌套结构,需要转换为扁平化结构供穿梭框使用:
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 // 标记为叶子节点
}))
}
考虑到行政区划数据较大,应采用缓存策略:
// 在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 })
}
}
})
创建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>
实现省份展开时动态加载对应城市:
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]
}
}
}
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 : ''
}
}
methods: {
handleChange(targetKeys, direction, moveKeys) {
this.selectedKeys = targetKeys
this.$emit('input', targetKeys)
this.$emit('change', targetKeys, direction, moveKeys)
}
},
watch: {
value(newVal) {
this.selectedKeys = newVal
}
}
<!-- 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>
a-virtual-scroll
优化// 在组件中添加防抖搜索
import { debounce } from 'lodash'
methods: {
filterOption: debounce(function(inputValue, option) {
// 过滤逻辑
}, 300)
}
watch: {
allProvinces(newVal) {
if (newVal.length > 0 && this.selectedKeys.length === 0) {
// 默认选中第一个省份
this.handleChange([newVal[0].key], 'right', [newVal[0].key])
}
}
}
props: {
value: {
type: Array,
default: () => [],
validator: value => Array.isArray(value) &&
value.every(item => typeof item === 'string')
}
}
handleChange(targetKeys) {
if (this.maxSelected && targetKeys.length > this.maxSelected) {
this.$message.warning(`最多选择${this.maxSelected}项`)
// 保持原状态
return false
}
// 正常处理
}
<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>
// 从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(', ')
}
<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>
扩展数据结构支持区县级选择:
// 数据处理
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)
]
}
}
})
}
”`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
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。