溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Ant Design Vue中如何實現省市穿梭框

發布時間:2021-12-24 09:02:41 來源:億速云 閱讀:318 作者:iii 欄目:編程語言
# 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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女