Add native place cascading selector and generated data
This commit is contained in:
1 parent
2b93bc2e64
commit
0854a92cc0
6 files changed
+159
-2
No files matched your search
Vendored
+1
@@ -55,6 +55,7 @@ declare module 'vue' {
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
NativePlaceCascader: typeof import('./components/NativePlaceCascader.vue')['default']
|
||||
NotificationRichTextEditor: typeof import('./components/NotificationRichTextEditor.vue')['default']
|
||||
PerformanceReportPanel: typeof import('./components/PerformanceReportPanel.vue')['default']
|
||||
RichMessageContent: typeof import('./components/RichMessageContent.vue')['default']
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { nativePlaceRows } from '../data/nativePlaces.generated'
|
||||
|
||||
const model = defineModel<string>({ default: '' })
|
||||
|
||||
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 @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="!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="!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="!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>
|
||||
@@ -0,0 +1,5 @@
|
||||
// 由 AreaCity-JsSpider-StatsGov 的 ok_data_level4.csv 生成,数据版本 2025.251231.260403(2026-04-03)。
|
||||
// 上游项目采用 MIT License:https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov
|
||||
export type NativePlaceRow = readonly [id: number, pid: number, deep: number, name: string]
|
||||
|
||||
export const nativePlaceRows: readonly NativePlaceRow[] = [[11,0,0,"北京市"],[1101,11,1,"北京市"],[110101,1101,2,"东城区"],[110101001,110101,3,"东华门街道"],[110101002,110101,3,"景山街道"],[110101003,110101,3,"交道口街道"],[110101004,110101,3,"安定门街道"],[110101005,110101,3,"北新桥街道"],[110101006,110101,3,"东四街道"],[110101007,110101,3,"朝阳门街道"],[110101008,110101,3,"建国门街道"],[110101009,110101,3,"东直门街道"],[110101010,110101,3,"和平里街道"],[110101011,110101,3,"前门街道"],[110101012,110101,3,"崇文门外街道"],[110101013,110101,3,"东花市街道"],[110101014,110101,3,"龙潭街道"],[110101015,110101,3,"体育馆路街道"],[110101016,110101,3,"天坛街道"],[110101017,110101,3,"永定门外街道"],[110102,1101,2,"西城区"],[110102001,110102,3,"西长安街街道"],[110102003,110102,3,"新街口街道"],[110102007,110102,3,"月坛街道"],[110102009,110102,3,"展览路街道"],[110102010,110102,3,"德胜街道"],[110102011,110102,3,"金融街街道"],[110102012,110102,3,"什刹海街道"],[110102013,110102,3,"大栅栏街道"],[110102014,110102,3,"天桥街道"],[110102015,110102,3,"椿树街道"],[110102016,110102,3,"陶然亭街道"],[110102017,110102,3,"广安门内街道"],[110102018,110102,3,"牛街街道"],[110102019,110102,3,"白纸坊街道"],[110102020,110102,3,"广安门外街道"],[110105,1101,2,"朝阳区"],[110105001,110105,3,"建外街道"],[110105002,110105,3,"朝外街道"],[110105003,110105,3,"呼家楼街道"],[110105004,110105,3,"三里屯街道"],[110105005,110105,3,"左家庄街道"],[110105006,110105,3,"香河园街道"],[110105007,110105,3,"和平街街道"],[110105008,110105,3,"安贞街道"],[110105009,110105,3,"亚运村街道"],[110105010,110105,3,"小关街道"],[110105011,110105,3,"酒仙桥街道"],[110105012,110105,3,"麦子店街道"],[110105013,110105,3,"团结湖街道"],[110105014,110105,3,"六里屯街道"],[110105015,110105,3,"八里庄街道"],[110105016,110105,3,"双井街道"],[110105017,110105,3,"劲松街道"],[110105018,110105,3,"潘家园街道"],[110105019,110105,3,"垡头街道"],[110105021,110105,3,"南磨房乡"],[110105022,110105,3,"高碑店乡"],[110105023,110105,3,"将台乡"],[110105024,110105,3,"太阳宫乡"],[110105025,110105,3,"大屯街道"],[110105026,110105,3,"望京街道"],[110105027,110105,3,"小红门乡"],[110105028,110105,3,"十八里店乡"],[110105029,110105,3,"平房乡"],[110105030,110105,3,"东风乡"],[110105031,110105,3,"奥运村街道"],[110105032,110105,3,"来广营乡"],[110105033,110105,3,"常营回族乡"],[110105034,110105,3,"三间房乡"],[110105035,110105,3,"管庄乡"],[110105036,110105,3,"金盏乡"],[110105037,110105,3,"孙河乡"],[110105038,110105,3,"崔各庄乡"],[110105039,110105,3,"东坝乡"],[110105040,110105,3,"黑庄户乡"],[110105041,110105,3,"豆各庄乡"],[110105042,110105,3,"王四营乡"],[110105043,110105,3,"东湖街道"],[110105400,110105,3,"首都机场街道"],[110106,1101,2,"丰台区"],[110106001,110106,3,"右安门街道"],[110106002,110106,3,"太平桥街道"],[110106003,110106,3,"西罗园街道"],[110106004,110106,3,"大红门街道"],[110106005,110106,3,"南苑街道"],[110106006,110106,3,"东高地街道"],[110106007,110106,3,"东铁匠营街道"],[110106008,110106,3,"六里桥街道"],[110106009,110106,3,"丰台街道"],[110106010,110106,3,"新村街道"],[110106011,110106,3,"长辛店街道"],[110106012,110106,3,"云岗街道"],[110106013,110106,3,"方庄街道"],[110106014,110106,3,"宛平街道"],[110106015,110106,3,"马家堡街道"],[110106016,110106,3,"和义街道"],[110106017,110106,3,"卢沟桥街道"],[110106018,110106,3,"花乡街道"],[110106020,110106,3,"成寿寺街道"],[110106021,110106,3,"石榴庄街道"],[110106022,110106,3,"玉泉营街道"],[110106023,110106,3,"看丹街道"],[110106024,110106,3,"五里店街道"],[110106025,110106,3,"青塔街道"],[110106100,110106,3,"北宫镇"],[110106101,110106,3,"王佐镇"],[110107,1101,2,"石景山区"],[110107001,110107,3,"八宝山街道"],[110107002,110107,3,"老山街道"],[110107003,110107,3,"八角街道"],[110107004,110107,3,"古城街道"],[110107005,110107,3,"苹果园街道"],[110107006,110107,3,"金顶街街道"],[110107009,110107,3,"广宁街道"],[110107010,110107,3,"五里坨街道"],[110107011,110107,3,"鲁谷街道"],[110108,1101,2,"海淀区"],[110108001,110108,3,"万寿路街道"],[110108002,110108,3,"永定路街道"],[110108003,110108,3,"羊坊店街道"],[110108004,110108,3,"甘家口街道"],[110108005,110108,3,"八里庄街道"],[110108006,110108,3,"紫竹院街道"],[110108007,110108,3,"北下关街道"],[110108008,110108,3,"北太平庄街道"],[110108010,110108,3,"学院路街道"],[110108011,110108,3,"中关村街道"],[110108012,110108,3,"海淀街道"],[110108013,110108,3,"青龙桥街道"],[110108014,110108,3,"清华园街道"],[110108015,110108,3,"燕园街道"],[110108016,110108,3,"香山街道"],[110108017,110108,3,Line truncated
|
||||
Loaded 3 of 6 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user