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
@@ -0,0 +1,43 @@
|
|||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
|
const [inputFile, outputFile] = process.argv.slice(2)
|
||||||
|
|
||||||
|
if (!inputFile || !outputFile) {
|
||||||
|
throw new Error('用法:node scripts/generate-native-place-data.mjs <ok_data_level4.csv> <输出文件>')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsvLine(line) {
|
||||||
|
const values = []
|
||||||
|
let value = ''
|
||||||
|
let quoted = false
|
||||||
|
|
||||||
|
for (let index = 0; index < line.length; index += 1) {
|
||||||
|
const character = line[index]
|
||||||
|
if (character === '"') {
|
||||||
|
if (quoted && line[index + 1] === '"') {
|
||||||
|
value += character
|
||||||
|
index += 1
|
||||||
|
} else {
|
||||||
|
quoted = !quoted
|
||||||
|
}
|
||||||
|
} else if (character === ',' && !quoted) {
|
||||||
|
values.push(value)
|
||||||
|
value = ''
|
||||||
|
} else {
|
||||||
|
value += character
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.push(value)
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
const csv = await readFile(resolve(inputFile), 'utf8')
|
||||||
|
const rows = csv.trim().split(/\r?\n/).slice(1).map((line) => {
|
||||||
|
const [id, pid, deep, , , , , name] = parseCsvLine(line)
|
||||||
|
return [Number(id), Number(pid), Number(deep), name]
|
||||||
|
})
|
||||||
|
|
||||||
|
const source = `// 由 AreaCity-JsSpider-StatsGov 的 ok_data_level4.csv 生成,数据版本 2025.251231.260403(2026-04-03)。\n// 上游项目采用 MIT License:https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov\nexport type NativePlaceRow = readonly [id: number, pid: number, deep: number, name: string]\n\nexport const nativePlaceRows: readonly NativePlaceRow[] = ${JSON.stringify(rows)} as const\n`
|
||||||
|
|
||||||
|
await writeFile(resolve(outputFile), source, 'utf8')
|
||||||
Vendored
+1
@@ -55,6 +55,7 @@ declare module 'vue' {
|
|||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
|
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
|
NativePlaceCascader: typeof import('./components/NativePlaceCascader.vue')['default']
|
||||||
NotificationRichTextEditor: typeof import('./components/NotificationRichTextEditor.vue')['default']
|
NotificationRichTextEditor: typeof import('./components/NotificationRichTextEditor.vue')['default']
|
||||||
PerformanceReportPanel: typeof import('./components/PerformanceReportPanel.vue')['default']
|
PerformanceReportPanel: typeof import('./components/PerformanceReportPanel.vue')['default']
|
||||||
RichMessageContent: typeof import('./components/RichMessageContent.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>
|
||||||
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