@xlui/xux-ui 0.2.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build China Cascader JSON (Advanced: 4/5 levels + Incremental + Pinyin)
4
+ * ----------------------------------------------------------------------
5
+ * 输出:Cascader 结构 [{ value, label, pinyin, abbr, aliases, search, children: [...] }]
6
+ *
7
+ * 依赖:axios, glob, pinyin
8
+ *
9
+ * CLI:
10
+ * --levels=3|4|5
11
+ * --out=china-areas.json
12
+ * --prev=previous.json # 增量基准(可选)
13
+ * --pca=./pca-code.json # 本地 pca-code.json(可选,传了就不走网络)
14
+ * --townDir=./dist/town # 4级目录(必传于 levels>=4)
15
+ * --villageDir=./dist/village # 5级目录(必传于 levels>=5)
16
+ * --excludeHKTW # 过滤港澳台
17
+ * --gzip # 额外输出 .gz
18
+ * --timeout=20000 --retries=3
19
+ */
20
+
21
+ import fs from 'node:fs'
22
+ import zlib from 'node:zlib'
23
+ import axios from 'axios'
24
+ import { glob } from 'glob'
25
+ import pinyin from 'pinyin'
26
+
27
+ // ---------------- CLI ----------------
28
+ const args = process.argv.slice(2)
29
+ const getArg = (name, def = undefined) => {
30
+ const hit = args.find(a => a === `--${name}` || a.startsWith(`--${name}=`))
31
+ if (!hit) return def
32
+ const parts = hit.split('=')
33
+ return parts.length > 1 ? parts.slice(1).join('=') : true
34
+ }
35
+
36
+ const LEVELS = Number(getArg('levels', 3))
37
+ const OUT = getArg('out', 'china-areas.full.json')
38
+ const PREV = getArg('prev')
39
+ const PCA_LOCAL = getArg('pca') // 本地 pca-code.json
40
+ const TOWN_DIR = getArg('townDir')
41
+ const VILLAGE_DIR = getArg('villageDir')
42
+ const INCLUDE_HKTW = getArg('includeHKTW', true) && !getArg('excludeHKTW', false)
43
+ const TIMEOUT = Number(getArg('timeout', 20000))
44
+ const RETRIES = Number(getArg('retries', 3))
45
+ const DO_GZIP = !!getArg('gzip', false)
46
+
47
+ const PCA_URL = 'https://raw.githubusercontent.com/modood/Administrative-divisions-of-China/master/dist/pca-code.json'
48
+
49
+ // --------------- Utils ---------------
50
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
51
+
52
+ async function httpGetJson(url, { timeout = TIMEOUT, retries = RETRIES } = {}) {
53
+ let lastErr
54
+ for (let i = 0; i <= retries; i++) {
55
+ try {
56
+ const { data } = await axios.get(url, { timeout, responseType: 'json' })
57
+ return data
58
+ } catch (err) {
59
+ lastErr = err
60
+ const delay = 500 * (i + 1)
61
+ console.warn(`⚠️ GET ${url} 失败(第 ${i + 1}/${retries + 1} 次),${delay}ms 后重试…`)
62
+ await sleep(delay)
63
+ }
64
+ }
65
+ throw lastErr
66
+ }
67
+
68
+ function readJson(file) {
69
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
70
+ }
71
+
72
+ function sortTree(arr) {
73
+ return arr
74
+ .map(n => ({
75
+ ...n,
76
+ children: Array.isArray(n.children) ? sortTree(n.children) : undefined
77
+ }))
78
+ .sort((a, b) => a.label.localeCompare(b.label, 'zh-Hans-CN'))
79
+ }
80
+
81
+ function dedupe(arr) {
82
+ const seen = new Set()
83
+ const out = []
84
+ for (const n of arr) {
85
+ const k = String(n.value)
86
+ if (!seen.has(k)) {
87
+ seen.add(k)
88
+ out.push(n)
89
+ }
90
+ }
91
+ return out
92
+ }
93
+
94
+ function filterHKTW(provinces) {
95
+ return provinces.filter(p => !/^71|^81|^82/.test(String(p.value)))
96
+ }
97
+
98
+ async function readAllJsonFiles(dir) {
99
+ const files = await glob('**/*.json', { cwd: dir, absolute: true })
100
+ const results = []
101
+ for (const fp of files) {
102
+ try {
103
+ const raw = fs.readFileSync(fp, 'utf8')
104
+ const data = JSON.parse(raw)
105
+ results.push({ file: fp, data })
106
+ } catch (e) {
107
+ console.warn(`⚠️ 读取 ${fp} 失败:${e.message}`)
108
+ }
109
+ }
110
+ return results
111
+ }
112
+
113
+ function bucketByPrefix(list, prefixLen) {
114
+ const map = new Map()
115
+ for (const item of list) {
116
+ const code = String(item.code || item.value)
117
+ const key = code.slice(0, prefixLen)
118
+ if (!map.has(key)) map.set(key, [])
119
+ map.get(key).push(item)
120
+ }
121
+ return map
122
+ }
123
+
124
+ // ------ Pinyin / Aliases Enhance ------
125
+ function toPinyinWords(han) {
126
+ const arr = pinyin(han, { style: pinyin.STYLE_NORMAL })
127
+ return arr.map(syls => syls[0] || '')
128
+ }
129
+
130
+ function buildEnhanceFields(label) {
131
+ const words = toPinyinWords(label)
132
+ const full = words.join(' ') // "bei jing shi"
133
+ const abbr = words.map(w => w[0] || '').join('') // "bjs"
134
+
135
+ const weakTerms = ['市辖区','市辖县','自治区直辖县级行政区划','省直辖县级行政区划']
136
+ const aliases = []
137
+ // 简化别名:去掉末尾常见后缀(便于检索)
138
+ const simplified = label.replace(/[市县区盟旗]$/u, '')
139
+ if (simplified && simplified !== label && !weakTerms.includes(label)) {
140
+ aliases.push(simplified)
141
+ }
142
+
143
+ const search = [label, full, abbr, ...aliases].join(' ').trim()
144
+ return { pinyin: full, abbr, aliases, search }
145
+ }
146
+
147
+ // -------- Incremental Merge ----------
148
+ function indexOldTree(oldRootArr) {
149
+ const map = new Map()
150
+ const dfs = (node) => {
151
+ map.set(String(node.value), node)
152
+ if (Array.isArray(node.children)) {
153
+ for (const c of node.children) dfs(c)
154
+ }
155
+ }
156
+ for (const n of oldRootArr || []) dfs(n)
157
+ return map
158
+ }
159
+
160
+ function isStructurallySame(newNode, oldNode) {
161
+ if (!oldNode) return false
162
+ if (newNode.label !== oldNode.label) return false
163
+
164
+ const nc = newNode.children || []
165
+ const oc = oldNode.children || []
166
+ if (nc.length !== oc.length) return false
167
+
168
+ const setNew = new Set(nc.map(n => String(n.value)))
169
+ for (const c of oc) {
170
+ if (!setNew.has(String(c.value))) return false
171
+ }
172
+ return true
173
+ }
174
+
175
+ function mergeNodeWithOld(newNode, oldIndex) {
176
+ const oldNode = oldIndex.get(String(newNode.value))
177
+ let merged = { ...newNode }
178
+
179
+ if (isStructurallySame(newNode, oldNode)) {
180
+ merged = {
181
+ ...merged,
182
+ pinyin: oldNode.pinyin,
183
+ abbr: oldNode.abbr,
184
+ aliases: oldNode.aliases,
185
+ search: oldNode.search
186
+ }
187
+ } else {
188
+ Object.assign(merged, buildEnhanceFields(merged.label))
189
+ }
190
+
191
+ if (Array.isArray(newNode.children) && newNode.children.length) {
192
+ merged.children = newNode.children.map(c => mergeNodeWithOld(c, oldIndex))
193
+ }
194
+
195
+ return merged
196
+ }
197
+
198
+ // ---------------- Main ----------------
199
+ ;(async function main() {
200
+ console.log(`\n== 构建中国级联(4/5 级 + 增量 + 拼音)==`)
201
+ console.log(`- levels: ${LEVELS}`)
202
+ console.log(`- out: ${OUT}`)
203
+ if (PREV) console.log(`- prev: ${PREV}`)
204
+ if (PCA_LOCAL) console.log(`- pca(local): ${PCA_LOCAL}`)
205
+ if (TOWN_DIR) console.log(`- townDir: ${TOWN_DIR}`)
206
+ if (VILLAGE_DIR) console.log(`- villageDir: ${VILLAGE_DIR}`)
207
+ console.log(`- includeHKTW: ${INCLUDE_HKTW}`)
208
+ console.log(`- gzip: ${DO_GZIP}\n`)
209
+
210
+ // 旧树索引
211
+ let oldIndex = new Map()
212
+ if (PREV && fs.existsSync(PREV)) {
213
+ try {
214
+ const old = readJson(PREV)
215
+ oldIndex = indexOldTree(old)
216
+ console.log(`🗂️ 载入旧文件 ${PREV},节点数:${oldIndex.size}`)
217
+ } catch (e) {
218
+ console.warn(`⚠️ 读取 prev 失败:${e.message}`)
219
+ }
220
+ }
221
+
222
+ // 1) 3级:省/市/区
223
+ let pca
224
+ if (PCA_LOCAL) {
225
+ console.log(`📖 使用本地 pca-code.json:${PCA_LOCAL}`)
226
+ pca = readJson(PCA_LOCAL)
227
+ } else {
228
+ console.log(`📥 下载省/市/区:${PCA_URL}`)
229
+ pca = await httpGetJson(PCA_URL)
230
+ }
231
+
232
+ const mapNode3 = (node) => ({
233
+ value: String(node.code),
234
+ label: String(node.name),
235
+ children: Array.isArray(node.children) ? node.children.map(mapNode3) : undefined
236
+ })
237
+
238
+ let cascader = sortTree(pca.map(mapNode3))
239
+ cascader = dedupe(cascader)
240
+ if (!INCLUDE_HKTW) cascader = filterHKTW(cascader)
241
+
242
+ // 2) 4级:乡镇(9位)
243
+ if (LEVELS >= 4) {
244
+ if (!TOWN_DIR) {
245
+ console.error('❌ levels>=4 需要 --townDir 指定乡镇目录')
246
+ process.exit(2)
247
+ }
248
+ console.log(`🧩 拼接乡镇/街道(9 位码) from ${TOWN_DIR}`)
249
+ const townFiles = await readAllJsonFiles(TOWN_DIR)
250
+
251
+ const towns = []
252
+ for (const { data } of townFiles) {
253
+ if (Array.isArray(data)) {
254
+ for (const t of data) {
255
+ const code = String(t.code ?? t.value ?? '')
256
+ const name = String(t.name ?? t.label ?? '')
257
+ if (/^\d{9}$/.test(code)) towns.push({ code, name })
258
+ }
259
+ } else if (data && typeof data === 'object') {
260
+ for (const key of Object.keys(data)) {
261
+ const arr = data[key]
262
+ if (Array.isArray(arr)) {
263
+ for (const t of arr) {
264
+ const code = String(t.code ?? t.value ?? '')
265
+ const name = String(t.name ?? t.label ?? '')
266
+ if (/^\d{9}$/.test(code)) towns.push({ code, name })
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ const townBuckets = bucketByPrefix(towns, 6)
274
+ let attached = 0
275
+ for (const prov of cascader) {
276
+ if (!prov.children) continue
277
+ for (const city of prov.children) {
278
+ if (!city.children) continue
279
+ for (const dist of city.children) {
280
+ const k6 = String(dist.value).slice(0, 6)
281
+ const arr = townBuckets.get(k6)
282
+ if (arr && arr.length) {
283
+ const children = arr.map(t => ({ value: t.code, label: t.name }))
284
+ dist.children = sortTree(dedupe([...(dist.children || []), ...children]))
285
+ attached += arr.length
286
+ }
287
+ }
288
+ }
289
+ }
290
+ console.log(`✅ 乡镇/街道已挂接:${attached} 条`)
291
+ }
292
+
293
+ // 3) 5级:村(12位)
294
+ if (LEVELS >= 5) {
295
+ if (!VILLAGE_DIR) {
296
+ console.error('❌ levels>=5 需要 --villageDir 指定村目录')
297
+ process.exit(3)
298
+ }
299
+ console.log(`🧩 拼接村/居(12 位码) from ${VILLAGE_DIR}`)
300
+ const villageFiles = await readAllJsonFiles(VILLAGE_DIR)
301
+
302
+ const villages = []
303
+ for (const { data } of villageFiles) {
304
+ if (Array.isArray(data)) {
305
+ for (const v of data) {
306
+ const code = String(v.code ?? v.value ?? '')
307
+ const name = String(v.name ?? v.label ?? '')
308
+ if (/^\d{12}$/.test(code)) villages.push({ code, name })
309
+ }
310
+ } else if (data && typeof data === 'object') {
311
+ for (const key of Object.keys(data)) {
312
+ const arr = data[key]
313
+ if (Array.isArray(arr)) {
314
+ for (const v of arr) {
315
+ const code = String(v.code ?? v.value ?? '')
316
+ const name = String(v.name ?? v.label ?? '')
317
+ if (/^\d{12}$/.test(code)) villages.push({ code, name })
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ const villageBuckets = bucketByPrefix(villages, 9)
325
+ let attached = 0
326
+
327
+ for (const prov of cascader) {
328
+ if (!prov.children) continue
329
+ for (const city of prov.children) {
330
+ if (!city.children) continue
331
+ for (const dist of city.children) {
332
+ if (!dist.children) continue
333
+ for (const town of dist.children) {
334
+ const k9 = String(town.value).slice(0, 9)
335
+ const arr = villageBuckets.get(k9)
336
+ if (arr && arr.length) {
337
+ const children = arr.map(v => ({ value: v.code, label: v.name }))
338
+ town.children = sortTree(dedupe([...(town.children || []), ...children]))
339
+ attached += arr.length
340
+ }
341
+ }
342
+ }
343
+ }
344
+ }
345
+ console.log(`✅ 村/居 已挂接:${attached} 条`)
346
+ }
347
+
348
+ // 4) 增量合并 + 拼音增强
349
+ const oldIndex = PREV && fs.existsSync(PREV) ? indexOldTree(readJson(PREV)) : new Map()
350
+ const enhanceRecursively = (node) => {
351
+ // 保底:如果没有增量参照,也要生成拼音增强
352
+ if (!('pinyin' in node)) Object.assign(node, buildEnhanceFields(node.label))
353
+ if (Array.isArray(node.children)) node.children.forEach(enhanceRecursively)
354
+ }
355
+
356
+ let merged = cascader.map(n => mergeNodeWithOld(n, oldIndex))
357
+ // 对没有 prev 的场景,确保增强字段存在
358
+ merged.forEach(enhanceRecursively)
359
+
360
+ // 5) 写出
361
+ merged = sortTree(merged)
362
+ fs.writeFileSync(OUT, JSON.stringify(merged, null, 2), 'utf8')
363
+ const sizeKB = (fs.statSync(OUT).size / 1024).toFixed(1)
364
+ console.log(`\n🎉 完成!输出 -> ${OUT}(约 ${sizeKB} KB)`)
365
+
366
+ if (DO_GZIP) {
367
+ const gz = zlib.gzipSync(fs.readFileSync(OUT))
368
+ fs.writeFileSync(`${OUT}.gz`, gz)
369
+ const gk = (gz.length / 1024).toFixed(1)
370
+ console.log(`🗜️ 同步输出 -> ${OUT}.gz(约 ${gk} KB)`)
371
+ }
372
+ console.log('')
373
+ })().catch(err => {
374
+ console.error('❌ 发生错误:', err?.message || err)
375
+ process.exit(1)
376
+ })
377
+