index.vue 7.97 KB
<template>
  <view>
    <uni-popup ref="popup" type="bottom" :mask-click="false" :safe-area="true">
      <view class="sheet">
        <view class="sheet-header">
          <text class="cancel" @click="onCancel">取消</text>
          <text class="title">{{ title }}</text>
          <text class="ok" @click="onConfirm">确认</text>
        </view>
        <view class="sheet-body">
          <view class="columns">
            <scroll-view class="col" scroll-y>
              <view v-for="(p,i) in provinces" :key="valueOf(p,i)" :class="['option', { selected: isSelectedProvince(p) }]" @click="selectProvince(p)">
                <text class="label">{{ labelOf(p) }}</text>
              </view>
            </scroll-view>
            <scroll-view class="col" scroll-y>
              <view v-for="(c,i) in cities" :key="valueOf(c,i)" :class="['option', { selected: isSelectedCity(c) }]" @click="selectCity(c)">
                <text class="label">{{ labelOf(c) }}</text>
              </view>
            </scroll-view>
            <scroll-view class="col" scroll-y>
              <view v-for="(d,i) in districts" :key="valueOf(d,i)" :class="['option', { selected: isSelectedDistrict(d) }]" @click="selectDistrict(d)">
                <text class="label">{{ labelOf(d) }}</text>
              </view>
            </scroll-view>
          </view>
        </view>
      </view>
    </uni-popup>
  </view>
  
</template>

<script>
import { selectorCityApi } from '@/api/base.js'
export default {
  name: 'CitySelector',
  props: {
    value: { type: Array, default: () => [] },
    title: { type: String, default: '目的地' }
  },
  data() {
    return {
      tree: [],
      provinces: [],
      cities: [],
      districts: [],
      selectedProvince: null,
      selectedCity: null,
      selectedDistrict: null,
      innerLabel: ''
    }
  },
  watch: {
    value(v) {
      const arr = Array.isArray(v) ? v : []
      if (!arr.length) { this.innerLabel = ''; this.selectedProvince = null; this.selectedCity = null; this.selectedDistrict = null }
      if (arr.length && this.tree && this.tree.length > 0) this.restoreSelectionFromArray(arr)
    }
  },
  methods: {
    async open() {
      if (!(this.tree && this.tree.length > 0)) {
        try {
          const res = await selectorCityApi()
          const data = (res && res.data) ? res.data : []
          this.tree = this.normalizeFlat(Array.isArray(data) ? data : [])
          this.provinces = this.tree
          if (Array.isArray(this.value) && this.value.length) this.restoreSelectionFromArray(this.value)
        } catch (e) {
          this.tree = []; this.provinces = []; this.cities = []; this.districts = []
        }
      }
      this.$refs.popup && this.$refs.popup.open()
    },
    async getLabel() {
      if (!(this.tree && this.tree.length > 0)) {
        try {
          const res = await selectorCityApi()
          const data = (res && res.data) ? res.data : []
          this.tree = this.normalizeFlat(Array.isArray(data) ? data : [])
          this.provinces = this.tree
        } catch (e) { this.tree = []; this.provinces = []; }
      }
      if (Array.isArray(this.value) && this.value.length) this.restoreSelectionFromArray(this.value)
      return this.innerLabel
    },
    onCancel() {
      this.$refs.popup && this.$refs.popup.close()
    },
    onConfirm() {
      if (!(this.selectedProvince && this.selectedCity && this.selectedDistrict)) {
        uni.showToast({ title: '请先选择省、市、区', icon: 'none' })
        return
      }
      const a = [
        String(this.selectedProvince.id),
        String(this.selectedCity.id),
        String(this.selectedDistrict.id)
      ]
      const parts = [this.selectedProvince, this.selectedCity, this.selectedDistrict].filter(Boolean).map(n => n.name)
      this.innerLabel = parts.join(' / ')
      this.$emit('input', a)
      this.$emit('update:value', a)
      this.$emit('change', { value: a, label: this.innerLabel })
      this.$refs.popup && this.$refs.popup.close()
    },
    selectProvince(p) {
      this.selectedProvince = p
      this.cities = Array.isArray(p.children) ? p.children : []
      this.selectedCity = null
      this.districts = []
      this.selectedDistrict = null
    },
    selectCity(c) {
      this.selectedCity = c
      this.districts = Array.isArray(c.children) ? c.children : []
      this.selectedDistrict = null
    },
    selectDistrict(d) {
      this.selectedDistrict = d
    },
    isSelectedProvince(p) { return this.selectedProvince && this.selectedProvince.id === p.id },
    isSelectedCity(c) { return this.selectedCity && this.selectedCity.id === c.id },
    isSelectedDistrict(d) { return this.selectedDistrict && this.selectedDistrict.id === d.id },
    valueOf(n,i) { const v = n && (n.id != null ? n.id : (n.value != null ? n.value : (n.code != null ? n.code : i))); return String(v) },
    labelOf(n) { const t = n && (n.name != null ? n.name : (n.label != null ? n.label : (n.text != null ? n.text : ''))); return String(t) },
    normalizeFlat(list) {
      const items = (list || []).map(it => {
        const id = it.id != null ? it.id : (it.fid != null ? it.fid : (it.value != null ? it.value : (it.code != null ? it.code : '')))
        const name = it.name != null ? it.name : (it.label != null ? it.label : (it.text != null ? it.text : ''))
        const parentId = it.parentId != null ? it.parentId : (it.pid != null ? it.pid : (it.parent != null ? it.parent : (it.pId != null ? it.pId : '')))
        return { id: String(id), name: String(name), parentId: parentId != null ? String(parentId) : '' }
      })
      const provinces = items.filter(x => !x.parentId)
      const citiesMap = {}
      for (const c of items.filter(x => x.parentId)) {
        const k = c.parentId
        if (!citiesMap[k]) citiesMap[k] = []
        citiesMap[k].push({ id: c.id, name: c.name })
      }
      const districtsMap = {}
      for (const d of items.filter(x => x.parentId)) {
        districtsMap[d.parentId] = districtsMap[d.parentId] || []
      }
      const provincesWithChildren = provinces.map(p => {
        const cs = citiesMap[p.id] || []
        const csWithChildren = cs.map(c => ({ id: c.id, name: c.name, children: items.filter(x => x.parentId === c.id).map(y => ({ id: y.id, name: y.name })) }))
        return { id: p.id, name: p.name, children: csWithChildren }
      })
      return provincesWithChildren
    },
    restoreSelectionFromArray(arr) {
      const [pid, cid, did] = arr.map(v => String(v || ''))
      const prov = (this.tree || []).find(n => String(n.id) === pid) || null
      this.selectedProvince = prov
      this.cities = prov && Array.isArray(prov.children) ? prov.children : []
      const city = (this.cities || []).find(n => String(n.id) === cid) || null
      this.selectedCity = city
      this.districts = city && Array.isArray(city.children) ? city.children : []
      const dist = (this.districts || []).find(n => String(n.id) === did) || null
      this.selectedDistrict = dist
      const parts = [this.selectedProvince, this.selectedCity, this.selectedDistrict].filter(Boolean).map(n => n.name)
      this.innerLabel = parts.join(' ')
    }
  }
}
</script>

<style lang="scss" scoped>
.sheet {
  width: 100%;
  max-height: 60vh;
  background: #fff;
  border-radius: 20rpx 20rpx 0 0;
  display: flex;
  flex-direction: column;
}
.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 30rpx 32rpx;
  border-bottom: 1rpx solid #f0f0f0;
}
.title { font-size: 36rpx; font-weight: 600; }
.cancel { color: rgba(0,0,0,0.6); font-size: 28rpx; }
.ok { color: $theme-primary; font-size: 28rpx; }
.sheet-body { flex: 0 0 auto; padding: 0; height: 50vh; overflow: hidden; }
.columns { display: flex; height: 50vh; }
.col { flex: 1; height: 50vh; border-right: 1rpx solid #f0f0f0; }
.col:last-child { border-right: none; }
.option { line-height: 40rpx; padding: 20rpx; text-align: center; font-size: 32rpx; }
.option .label { color: rgba(0,0,0,0.6); font-size: 32rpx; }
.option.selected { background: #f3f3f3; }
.option.selected .label { color: rgba(0,0,0,0.9); }
</style>