Files
Academic-Affairs-System/web/src/components/NativePlaceCascader.vue
T
2026-08-17 19:12:51 +08:00

108 lines
3.9 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { nativePlaceRows } from '../data/nativePlaces.generated'
const model = defineModel<string>({ default: '' })
defineProps<{ disabled?: boolean }>()
type Place = {
id: number
pid: number
deep: number
name: string
}
const places = nativePlaceRows.map(([id, pid, deep, name]) => ({ id, pid, deep, name }))
const placesById = new Map<number, Place>(places.map((place) => [place.id, place]))
const childrenByParent = new Map<number, Place[]>()
for (const place of places) {
const children = childrenByParent.get(place.pid) ?? []
children.push(place)
childrenByParent.set(place.pid, children)
}
for (const children of childrenByParent.values()) {
children.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'))
}
const selectedIds = ref<number[]>([])
const syncing = ref(false)
const levelLabels = ['省份', '城市', '区县', '乡镇']
function optionsFor(level: number) {
return computed(() => {
if (level === 0) return childrenByParent.get(0) ?? []
const parentId = selectedIds.value[level - 1]
return parentId ? (childrenByParent.get(parentId) ?? []) : []
})
}
const provinceOptions = optionsFor(0)
const cityOptions = optionsFor(1)
const districtOptions = optionsFor(2)
const townOptions = optionsFor(3)
function pathFor(place: Place) {
const path = [place]
let parent = placesById.get(place.pid)
while (parent) {
path.unshift(parent)
parent = placesById.get(parent.pid)
}
return path
}
function selectedName() {
if (selectedIds.value.length !== 4) return ''
return selectedIds.value
.map((id) => placesById.get(id)?.name)
.filter((name): name is string => Boolean(name))
.join(' / ')
}
function syncFromModel(value: string) {
const normalized = value.trim()
const matched = places.find((place) =>
place.deep === 3 && pathFor(place).map((item) => item.name).join(' / ') === normalized,
)
selectedIds.value = matched ? pathFor(matched).map((place) => place.id) : []
}
function changeLevel(level: number, id?: number) {
selectedIds.value = id ? [...selectedIds.value.slice(0, level), id] : selectedIds.value.slice(0, level)
if (!syncing.value) {
model.value = selectedName()
}
}
watch(model, (value) => {
if (!syncing.value) {
syncing.value = true
syncFromModel(value)
syncing.value = false
}
}, { immediate: true })
</script>
<template>
<div class="native-place-cascader">
<el-select :model-value="selectedIds[0]" :placeholder="levelLabels[0]" filterable clearable :disabled="disabled" @update:model-value="(value) => changeLevel(0, value)">
<el-option v-for="option in provinceOptions" :key="option.id" :label="option.name" :value="option.id" />
</el-select>
<el-select :model-value="selectedIds[1]" :placeholder="levelLabels[1]" filterable clearable :disabled="disabled || !selectedIds[0]" @update:model-value="(value) => changeLevel(1, value)">
<el-option v-for="option in cityOptions" :key="option.id" :label="option.name" :value="option.id" />
</el-select>
<el-select :model-value="selectedIds[2]" :placeholder="levelLabels[2]" filterable clearable :disabled="disabled || !selectedIds[1]" @update:model-value="(value) => changeLevel(2, value)">
<el-option v-for="option in districtOptions" :key="option.id" :label="option.name" :value="option.id" />
</el-select>
<el-select :model-value="selectedIds[3]" :placeholder="levelLabels[3]" filterable clearable :disabled="disabled || !selectedIds[2]" @update:model-value="(value) => changeLevel(3, value)">
<el-option v-for="option in townOptions" :key="option.id" :label="option.name" :value="option.id" />
</el-select>
</div>
</template>
<style scoped>
.native-place-cascader { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; width: 100%; }
@media (max-width: 720px) { .native-place-cascader { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style>