diff --git a/cloudflare-worker/package.json b/cloudflare-worker/package.json index 49017848..906b6d65 100644 --- a/cloudflare-worker/package.json +++ b/cloudflare-worker/package.json @@ -8,6 +8,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "china-division": "^2.7.0", "hono": "^4.6.0" }, "devDependencies": { diff --git a/cloudflare-worker/src/routes/geographic.ts b/cloudflare-worker/src/routes/geographic.ts index d94ee3fd..bd6a5738 100644 --- a/cloudflare-worker/src/routes/geographic.ts +++ b/cloudflare-worker/src/routes/geographic.ts @@ -1,89 +1,15 @@ import { Hono } from 'hono'; +import { + getCityOptions, + provinceOptions, +} from '../../../src/utils/chinaDivision'; const app = new Hono(); -// Province data - simplified with key/label format -const provinceData = [ - { label: '北京市', key: '110000' }, - { label: '天津市', key: '120000' }, - { label: '河北省', key: '130000' }, - { label: '山西省', key: '140000' }, - { label: '内蒙古自治区', key: '150000' }, - { label: '辽宁省', key: '210000' }, - { label: '吉林省', key: '220000' }, - { label: '黑龙江省', key: '230000' }, - { label: '上海市', key: '310000' }, - { label: '江苏省', key: '320000' }, - { label: '浙江省', key: '330000' }, - { label: '安徽省', key: '340000' }, - { label: '福建省', key: '350000' }, - { label: '江西省', key: '360000' }, - { label: '山东省', key: '370000' }, - { label: '河南省', key: '410000' }, - { label: '湖北省', key: '420000' }, - { label: '湖南省', key: '430000' }, - { label: '广东省', key: '440000' }, - { label: '广西壮族自治区', key: '450000' }, - { label: '海南省', key: '460000' }, - { label: '重庆市', key: '500000' }, - { label: '四川省', key: '510000' }, - { label: '贵州省', key: '520000' }, - { label: '云南省', key: '530000' }, - { label: '西藏自治区', key: '540000' }, - { label: '陕西省', key: '610000' }, - { label: '甘肃省', key: '620000' }, - { label: '青海省', key: '630000' }, - { label: '宁夏回族自治区', key: '640000' }, - { label: '新疆维吾尔自治区', key: '650000' }, - { label: '台湾省', key: '710000' }, - { label: '香港特别行政区', key: '810000' }, - { label: '澳门特别行政区', key: '820000' }, -]; - -// City data - moved outside handler for performance -const cityData: Record = { - '110000': [{ label: '市辖区', key: '110100' }], - '120000': [{ label: '市辖区', key: '120100' }], - '310000': [{ label: '市辖区', key: '310100' }], - '330000': [ - { label: '杭州市', key: '330100' }, - { label: '宁波市', key: '330200' }, - { label: '温州市', key: '330300' }, - { label: '嘉兴市', key: '330400' }, - { label: '湖州市', key: '330500' }, - { label: '绍兴市', key: '330600' }, - { label: '金华市', key: '330700' }, - { label: '衢州市', key: '330800' }, - { label: '舟山市', key: '330900' }, - { label: '台州市', key: '331000' }, - { label: '丽水市', key: '331100' }, - ], - '440000': [ - { label: '广州市', key: '440100' }, - { label: '韶关市', key: '440200' }, - { label: '深圳市', key: '440300' }, - { label: '珠海市', key: '440400' }, - { label: '汕头市', key: '440500' }, - { label: '佛山市', key: '440600' }, - { label: '江门市', key: '440700' }, - { label: '湛江市', key: '440800' }, - { label: '茂名市', key: '440900' }, - { label: '肇庆市', key: '441200' }, - { label: '惠州市', key: '441300' }, - { label: '梅州市', key: '441400' }, - { label: '汕尾市', key: '441500' }, - { label: '河源市', key: '441600' }, - { label: '阳江市', key: '441700' }, - { label: '清远市', key: '441800' }, - { label: '东莞市', key: '441900' }, - { label: '中山市', key: '442000' }, - ], -}; - // GET /api/geographic/province app.get('/province', (c) => { return c.json({ - data: provinceData, + data: provinceOptions, }); }); @@ -91,9 +17,8 @@ app.get('/province', (c) => { app.get('/city/:province', (c) => { const province = c.req.param('province'); - // Return data for the province, or empty array if not found return c.json({ - data: cityData[province] || [], + data: getCityOptions(province), }); }); diff --git a/package-lock.json b/package-lock.json index c39ae650..8637c9b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@tanstack/react-query": "^5.101.0", "antd": "^6.4.3", "antd-style": "^4.1.0", + "china-division": "^2.7.0", "clsx": "^2.1.1", "d3": "^7.9.0", "dayjs": "^1.11.20", @@ -32616,6 +32617,12 @@ "peerDependencies": { "zod": "^3.18.0" } + }, + "node_modules/china-division": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/china-division/-/china-division-2.7.0.tgz", + "integrity": "sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==", + "license": "MIT" } } } diff --git a/package.json b/package.json index d32240ca..de85e1df 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@tanstack/react-query": "^5.101.0", "antd": "^6.4.3", "antd-style": "^4.1.0", + "china-division": "^2.7.0", "clsx": "^2.1.1", "d3": "^7.9.0", "dayjs": "^1.11.20", diff --git a/src/pages/account/center/Center.style.ts b/src/pages/account/center/Center.style.ts index f91ff52f..7db33458 100644 --- a/src/pages/account/center/Center.style.ts +++ b/src/pages/account/center/Center.style.ts @@ -5,7 +5,12 @@ const useStyles = createStyles(({ token }) => { avatarHolder: { marginBottom: '24px', textAlign: 'center', - '& > img': { width: '104px', height: '104px', marginBottom: '20px' }, + '& > img': { + display: 'block', + width: '104px', + height: '104px', + margin: '0 auto 20px', + }, }, name: { marginBottom: '4px', diff --git a/src/pages/account/settings/_mock.ts b/src/pages/account/settings/_mock.ts index 22467423..07203490 100644 --- a/src/pages/account/settings/_mock.ts +++ b/src/pages/account/settings/_mock.ts @@ -1,19 +1,19 @@ import type { Request, Response } from 'express'; import { defaultUser } from '../../../../mock/utils'; - -const city = require('./geographic/city.json'); -const province = require('./geographic/province.json'); +import { getCityOptions, provinceOptions } from '../../../utils/chinaDivision'; function getProvince(_: Request, res: Response) { return res.json({ - data: province, + data: provinceOptions, }); } function getCity(req: Request, res: Response) { const provinceKey = req.params.province; return res.json({ - data: city[provinceKey as keyof typeof city], + data: getCityOptions( + Array.isArray(provinceKey) ? provinceKey[0] : provinceKey, + ), }); } diff --git a/src/pages/account/settings/components/base.test.tsx b/src/pages/account/settings/components/base.test.tsx index 2e0860cb..9bf35bca 100644 --- a/src/pages/account/settings/components/base.test.tsx +++ b/src/pages/account/settings/components/base.test.tsx @@ -5,15 +5,31 @@ import * as service from '../service'; import BaseView from './base'; const mocks = vi.hoisted(() => ({ + form: { + setFieldValue: vi.fn(), + }, initialValues: undefined as Record | undefined, + onValuesChange: undefined as + | ((changedValues: Record) => void) + | undefined, + dependencyProvince: { label: '浙江省', value: '330000' }, requestResults: {} as Record, })); vi.mock('@ant-design/pro-components', async () => { const React = await import('react'); - const ProForm = ({ children, initialValues }: any) => { + const ProForm = ({ + children, + formRef, + initialValues, + onValuesChange, + }: any) => { + if (formRef) { + formRef.current = mocks.form; + } mocks.initialValues = initialValues; + mocks.onValuesChange = onValuesChange; return
{children}
; }; @@ -22,15 +38,15 @@ vi.mock('@ant-design/pro-components', async () => { return { ProForm, ProFormDependency: ({ children }: any) => ( -
{children({ province: { label: '浙江省', value: '330000' } })}
+
{children({ province: mocks.dependencyProvince })}
), ProFormFieldSet: ({ children }: any) =>
{children}
, - ProFormSelect: ({ name, request }: any) => { + ProFormSelect: ({ name, params, request }: any) => { React.useEffect(() => { - request?.().then((result: any) => { + request?.(params).then((result: any) => { mocks.requestResults[name] = result; }); - }, [name, request]); + }, [name, params, request]); return
; }, ProFormText: () =>
, @@ -82,6 +98,8 @@ describe('BaseView geographic selects', () => { }, }); mocks.initialValues = undefined; + mocks.onValuesChange = undefined; + mocks.dependencyProvince = { label: '浙江省', value: '330000' }; mocks.requestResults = {}; vi.clearAllMocks(); @@ -108,10 +126,10 @@ describe('BaseView geographic selects', () => { }, }); vi.mocked(service.queryProvince).mockResolvedValue([ - { id: '330000', name: '浙江省' }, + { key: '330000', label: '浙江省' }, ]); vi.mocked(service.queryCity).mockResolvedValue([ - { id: '330100', name: '杭州市' }, + { key: '330100', label: '杭州市' }, ]); }); @@ -134,6 +152,43 @@ describe('BaseView geographic selects', () => { }); }); + it('does not set incomplete geographic initial values', async () => { + vi.mocked(service.queryCurrent).mockResolvedValue({ + data: { + address: '西湖区工专路 77 号', + avatar: '', + country: 'China', + email: 'antdesign@alipay.com', + geographic: { + province: { label: '浙江省' }, + city: { label: '杭州市' }, + }, + group: '', + name: 'Ant Design', + notice: [], + notifyCount: 0, + phone: '0752-268888888', + signature: '', + tags: [], + title: '', + unreadCount: 0, + userid: '00000001', + } as any, + }); + + render( + + + , + ); + + await waitFor(() => { + expect(mocks.initialValues?.name).toBe('Ant Design'); + expect(mocks.initialValues?.province).toBeUndefined(); + expect(mocks.initialValues?.city).toBeUndefined(); + }); + }); + it('loads cities with the selected province value', async () => { render( @@ -146,6 +201,24 @@ describe('BaseView geographic selects', () => { }); }); + it('clears city when province changes', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(mocks.onValuesChange).toBeTypeOf('function'); + }); + + mocks.onValuesChange?.({ + province: { label: '河北省', value: '130000' }, + }); + + expect(mocks.form.setFieldValue).toHaveBeenCalledWith('city', undefined); + }); + it('returns label/value option arrays for geographic selects', async () => { render( @@ -162,4 +235,47 @@ describe('BaseView geographic selects', () => { ]); }); }); + + it('supports local mock name/id geographic responses', async () => { + vi.mocked(service.queryProvince).mockResolvedValue([ + { id: '440000', name: '广东省' }, + ]); + vi.mocked(service.queryCity).mockResolvedValue([ + { id: '440300', name: '深圳市' }, + ]); + + render( + + + , + ); + + await waitFor(() => { + expect(mocks.requestResults.province).toEqual([ + { label: '广东省', value: '440000' }, + ]); + expect(mocks.requestResults.city).toEqual([ + { label: '深圳市', value: '440300' }, + ]); + }); + }); + + it('falls back to local city options when remote city data is empty', async () => { + mocks.dependencyProvince = { label: '江苏省', value: '320000' }; + vi.mocked(service.queryCity).mockResolvedValue([]); + + render( + + + , + ); + + await waitFor(() => { + expect(service.queryCity).toHaveBeenCalledWith('320000'); + expect(mocks.requestResults.city).toContainEqual({ + label: '南京市', + value: '320100', + }); + }); + }); }); diff --git a/src/pages/account/settings/components/base.tsx b/src/pages/account/settings/components/base.tsx index 020000d4..9b3c0455 100644 --- a/src/pages/account/settings/components/base.tsx +++ b/src/pages/account/settings/components/base.tsx @@ -1,4 +1,5 @@ import { UploadOutlined } from '@ant-design/icons'; +import type { ProFormInstance } from '@ant-design/pro-components'; import { ProForm, ProFormDependency, @@ -10,6 +11,8 @@ import { import { useQuery } from '@tanstack/react-query'; import { Button, Input, message, Upload } from 'antd'; import React from 'react'; +import { getCityOptions, provinceOptions } from '@/utils/chinaDivision'; +import type { GeographicItemType } from '../data'; import { queryCity, queryCurrent, queryProvince } from '../service'; import useStyles from './index.style'; @@ -28,15 +31,36 @@ const validatorPhone = ( }; const toSelectValue = (item?: { label?: string; key?: string }) => - item + item?.key ? { label: item.label, value: item.key, } : undefined; +const toSelectOptions = (items: GeographicItemType[]) => + items + .map((item) => { + const label = item.name ?? item.label; + const value = item.id ?? item.key; + + return label && value ? { label, value } : undefined; + }) + .filter((item): item is { label: string; value: string } => Boolean(item)); + +const handleFinish = async () => { + message.success('更新基本信息成功'); +}; + const BaseView: React.FC = () => { const { styles } = useStyles(); + const formRef = React.useRef(undefined); + + const handleValuesChange = (changedValues: Record) => { + if ('province' in changedValues) { + formRef.current?.setFieldValue('city', undefined); + } + }; const { data: currentUser, isLoading: loading } = useQuery({ queryKey: ['current-user'], @@ -53,17 +77,16 @@ const BaseView: React.FC = () => { } return ''; }; - const handleFinish = async () => { - message.success('更新基本信息成功'); - }; return (
{loading ? null : ( <>
{ ]} /> - + { }} name="province" request={async () => { - return queryProvince().then((data) => { - return data.map((item) => { - return { - label: item.name, - value: item.id, - }; - }); - }); + const options = toSelectOptions(await queryProvince()); + return options.length + ? options + : toSelectOptions(provinceOptions); }} /> {({ province }) => { return ( { labelInValue: true, }} disabled={!province} - request={async () => { - if (!province?.value) { + request={async (params) => { + if (!params.key) { return []; } - return queryCity(province.value).then((data) => { - return data.map((item) => { - return { - label: item.name, - value: item.id, - }; - }); - }); + const provinceKey = String(params.key); + const options = toSelectOptions( + await queryCity(provinceKey), + ); + return options.length + ? options + : toSelectOptions(getCityOptions(provinceKey)); }} /> ); diff --git a/src/pages/account/settings/data.d.ts b/src/pages/account/settings/data.d.ts index 7f20dac4..af8e5e82 100644 --- a/src/pages/account/settings/data.d.ts +++ b/src/pages/account/settings/data.d.ts @@ -4,8 +4,10 @@ export type TagType = { }; export type GeographicItemType = { - name: string; - id: string; + name?: string; + id?: string; + label?: string; + key?: string; }; export type GeographicType = { diff --git a/src/pages/account/settings/geographic/city.json b/src/pages/account/settings/geographic/city.json deleted file mode 100644 index 29783747..00000000 --- a/src/pages/account/settings/geographic/city.json +++ /dev/null @@ -1,1784 +0,0 @@ -{ - "110000": [ - { - "province": "北京市", - "name": "市辖区", - "id": "110100" - } - ], - "120000": [ - { - "province": "天津市", - "name": "市辖区", - "id": "120100" - } - ], - "130000": [ - { - "province": "河北省", - "name": "石家庄市", - "id": "130100" - }, - { - "province": "河北省", - "name": "唐山市", - "id": "130200" - }, - { - "province": "河北省", - "name": "秦皇岛市", - "id": "130300" - }, - { - "province": "河北省", - "name": "邯郸市", - "id": "130400" - }, - { - "province": "河北省", - "name": "邢台市", - "id": "130500" - }, - { - "province": "河北省", - "name": "保定市", - "id": "130600" - }, - { - "province": "河北省", - "name": "张家口市", - "id": "130700" - }, - { - "province": "河北省", - "name": "承德市", - "id": "130800" - }, - { - "province": "河北省", - "name": "沧州市", - "id": "130900" - }, - { - "province": "河北省", - "name": "廊坊市", - "id": "131000" - }, - { - "province": "河北省", - "name": "衡水市", - "id": "131100" - }, - { - "province": "河北省", - "name": "省直辖县级行政区划", - "id": "139000" - } - ], - "140000": [ - { - "province": "山西省", - "name": "太原市", - "id": "140100" - }, - { - "province": "山西省", - "name": "大同市", - "id": "140200" - }, - { - "province": "山西省", - "name": "阳泉市", - "id": "140300" - }, - { - "province": "山西省", - "name": "长治市", - "id": "140400" - }, - { - "province": "山西省", - "name": "晋城市", - "id": "140500" - }, - { - "province": "山西省", - "name": "朔州市", - "id": "140600" - }, - { - "province": "山西省", - "name": "晋中市", - "id": "140700" - }, - { - "province": "山西省", - "name": "运城市", - "id": "140800" - }, - { - "province": "山西省", - "name": "忻州市", - "id": "140900" - }, - { - "province": "山西省", - "name": "临汾市", - "id": "141000" - }, - { - "province": "山西省", - "name": "吕梁市", - "id": "141100" - } - ], - "150000": [ - { - "province": "内蒙古自治区", - "name": "呼和浩特市", - "id": "150100" - }, - { - "province": "内蒙古自治区", - "name": "包头市", - "id": "150200" - }, - { - "province": "内蒙古自治区", - "name": "乌海市", - "id": "150300" - }, - { - "province": "内蒙古自治区", - "name": "赤峰市", - "id": "150400" - }, - { - "province": "内蒙古自治区", - "name": "通辽市", - "id": "150500" - }, - { - "province": "内蒙古自治区", - "name": "鄂尔多斯市", - "id": "150600" - }, - { - "province": "内蒙古自治区", - "name": "呼伦贝尔市", - "id": "150700" - }, - { - "province": "内蒙古自治区", - "name": "巴彦淖尔市", - "id": "150800" - }, - { - "province": "内蒙古自治区", - "name": "乌兰察布市", - "id": "150900" - }, - { - "province": "内蒙古自治区", - "name": "兴安盟", - "id": "152200" - }, - { - "province": "内蒙古自治区", - "name": "锡林郭勒盟", - "id": "152500" - }, - { - "province": "内蒙古自治区", - "name": "阿拉善盟", - "id": "152900" - } - ], - "210000": [ - { - "province": "辽宁省", - "name": "沈阳市", - "id": "210100" - }, - { - "province": "辽宁省", - "name": "大连市", - "id": "210200" - }, - { - "province": "辽宁省", - "name": "鞍山市", - "id": "210300" - }, - { - "province": "辽宁省", - "name": "抚顺市", - "id": "210400" - }, - { - "province": "辽宁省", - "name": "本溪市", - "id": "210500" - }, - { - "province": "辽宁省", - "name": "丹东市", - "id": "210600" - }, - { - "province": "辽宁省", - "name": "锦州市", - "id": "210700" - }, - { - "province": "辽宁省", - "name": "营口市", - "id": "210800" - }, - { - "province": "辽宁省", - "name": "阜新市", - "id": "210900" - }, - { - "province": "辽宁省", - "name": "辽阳市", - "id": "211000" - }, - { - "province": "辽宁省", - "name": "盘锦市", - "id": "211100" - }, - { - "province": "辽宁省", - "name": "铁岭市", - "id": "211200" - }, - { - "province": "辽宁省", - "name": "朝阳市", - "id": "211300" - }, - { - "province": "辽宁省", - "name": "葫芦岛市", - "id": "211400" - } - ], - "220000": [ - { - "province": "吉林省", - "name": "长春市", - "id": "220100" - }, - { - "province": "吉林省", - "name": "吉林市", - "id": "220200" - }, - { - "province": "吉林省", - "name": "四平市", - "id": "220300" - }, - { - "province": "吉林省", - "name": "辽源市", - "id": "220400" - }, - { - "province": "吉林省", - "name": "通化市", - "id": "220500" - }, - { - "province": "吉林省", - "name": "白山市", - "id": "220600" - }, - { - "province": "吉林省", - "name": "松原市", - "id": "220700" - }, - { - "province": "吉林省", - "name": "白城市", - "id": "220800" - }, - { - "province": "吉林省", - "name": "延边朝鲜族自治州", - "id": "222400" - } - ], - "230000": [ - { - "province": "黑龙江省", - "name": "哈尔滨市", - "id": "230100" - }, - { - "province": "黑龙江省", - "name": "齐齐哈尔市", - "id": "230200" - }, - { - "province": "黑龙江省", - "name": "鸡西市", - "id": "230300" - }, - { - "province": "黑龙江省", - "name": "鹤岗市", - "id": "230400" - }, - { - "province": "黑龙江省", - "name": "双鸭山市", - "id": "230500" - }, - { - "province": "黑龙江省", - "name": "大庆市", - "id": "230600" - }, - { - "province": "黑龙江省", - "name": "伊春市", - "id": "230700" - }, - { - "province": "黑龙江省", - "name": "佳木斯市", - "id": "230800" - }, - { - "province": "黑龙江省", - "name": "七台河市", - "id": "230900" - }, - { - "province": "黑龙江省", - "name": "牡丹江市", - "id": "231000" - }, - { - "province": "黑龙江省", - "name": "黑河市", - "id": "231100" - }, - { - "province": "黑龙江省", - "name": "绥化市", - "id": "231200" - }, - { - "province": "黑龙江省", - "name": "大兴安岭地区", - "id": "232700" - } - ], - "310000": [ - { - "province": "上海市", - "name": "市辖区", - "id": "310100" - } - ], - "320000": [ - { - "province": "江苏省", - "name": "南京市", - "id": "320100" - }, - { - "province": "江苏省", - "name": "无锡市", - "id": "320200" - }, - { - "province": "江苏省", - "name": "徐州市", - "id": "320300" - }, - { - "province": "江苏省", - "name": "常州市", - "id": "320400" - }, - { - "province": "江苏省", - "name": "苏州市", - "id": "320500" - }, - { - "province": "江苏省", - "name": "南通市", - "id": "320600" - }, - { - "province": "江苏省", - "name": "连云港市", - "id": "320700" - }, - { - "province": "江苏省", - "name": "淮安市", - "id": "320800" - }, - { - "province": "江苏省", - "name": "盐城市", - "id": "320900" - }, - { - "province": "江苏省", - "name": "扬州市", - "id": "321000" - }, - { - "province": "江苏省", - "name": "镇江市", - "id": "321100" - }, - { - "province": "江苏省", - "name": "泰州市", - "id": "321200" - }, - { - "province": "江苏省", - "name": "宿迁市", - "id": "321300" - } - ], - "330000": [ - { - "province": "浙江省", - "name": "杭州市", - "id": "330100" - }, - { - "province": "浙江省", - "name": "宁波市", - "id": "330200" - }, - { - "province": "浙江省", - "name": "温州市", - "id": "330300" - }, - { - "province": "浙江省", - "name": "嘉兴市", - "id": "330400" - }, - { - "province": "浙江省", - "name": "湖州市", - "id": "330500" - }, - { - "province": "浙江省", - "name": "绍兴市", - "id": "330600" - }, - { - "province": "浙江省", - "name": "金华市", - "id": "330700" - }, - { - "province": "浙江省", - "name": "衢州市", - "id": "330800" - }, - { - "province": "浙江省", - "name": "舟山市", - "id": "330900" - }, - { - "province": "浙江省", - "name": "台州市", - "id": "331000" - }, - { - "province": "浙江省", - "name": "丽水市", - "id": "331100" - } - ], - "340000": [ - { - "province": "安徽省", - "name": "合肥市", - "id": "340100" - }, - { - "province": "安徽省", - "name": "芜湖市", - "id": "340200" - }, - { - "province": "安徽省", - "name": "蚌埠市", - "id": "340300" - }, - { - "province": "安徽省", - "name": "淮南市", - "id": "340400" - }, - { - "province": "安徽省", - "name": "马鞍山市", - "id": "340500" - }, - { - "province": "安徽省", - "name": "淮北市", - "id": "340600" - }, - { - "province": "安徽省", - "name": "铜陵市", - "id": "340700" - }, - { - "province": "安徽省", - "name": "安庆市", - "id": "340800" - }, - { - "province": "安徽省", - "name": "黄山市", - "id": "341000" - }, - { - "province": "安徽省", - "name": "滁州市", - "id": "341100" - }, - { - "province": "安徽省", - "name": "阜阳市", - "id": "341200" - }, - { - "province": "安徽省", - "name": "宿州市", - "id": "341300" - }, - { - "province": "安徽省", - "name": "六安市", - "id": "341500" - }, - { - "province": "安徽省", - "name": "亳州市", - "id": "341600" - }, - { - "province": "安徽省", - "name": "池州市", - "id": "341700" - }, - { - "province": "安徽省", - "name": "宣城市", - "id": "341800" - } - ], - "350000": [ - { - "province": "福建省", - "name": "福州市", - "id": "350100" - }, - { - "province": "福建省", - "name": "厦门市", - "id": "350200" - }, - { - "province": "福建省", - "name": "莆田市", - "id": "350300" - }, - { - "province": "福建省", - "name": "三明市", - "id": "350400" - }, - { - "province": "福建省", - "name": "泉州市", - "id": "350500" - }, - { - "province": "福建省", - "name": "漳州市", - "id": "350600" - }, - { - "province": "福建省", - "name": "南平市", - "id": "350700" - }, - { - "province": "福建省", - "name": "龙岩市", - "id": "350800" - }, - { - "province": "福建省", - "name": "宁德市", - "id": "350900" - } - ], - "360000": [ - { - "province": "江西省", - "name": "南昌市", - "id": "360100" - }, - { - "province": "江西省", - "name": "景德镇市", - "id": "360200" - }, - { - "province": "江西省", - "name": "萍乡市", - "id": "360300" - }, - { - "province": "江西省", - "name": "九江市", - "id": "360400" - }, - { - "province": "江西省", - "name": "新余市", - "id": "360500" - }, - { - "province": "江西省", - "name": "鹰潭市", - "id": "360600" - }, - { - "province": "江西省", - "name": "赣州市", - "id": "360700" - }, - { - "province": "江西省", - "name": "吉安市", - "id": "360800" - }, - { - "province": "江西省", - "name": "宜春市", - "id": "360900" - }, - { - "province": "江西省", - "name": "抚州市", - "id": "361000" - }, - { - "province": "江西省", - "name": "上饶市", - "id": "361100" - } - ], - "370000": [ - { - "province": "山东省", - "name": "济南市", - "id": "370100" - }, - { - "province": "山东省", - "name": "青岛市", - "id": "370200" - }, - { - "province": "山东省", - "name": "淄博市", - "id": "370300" - }, - { - "province": "山东省", - "name": "枣庄市", - "id": "370400" - }, - { - "province": "山东省", - "name": "东营市", - "id": "370500" - }, - { - "province": "山东省", - "name": "烟台市", - "id": "370600" - }, - { - "province": "山东省", - "name": "潍坊市", - "id": "370700" - }, - { - "province": "山东省", - "name": "济宁市", - "id": "370800" - }, - { - "province": "山东省", - "name": "泰安市", - "id": "370900" - }, - { - "province": "山东省", - "name": "威海市", - "id": "371000" - }, - { - "province": "山东省", - "name": "日照市", - "id": "371100" - }, - { - "province": "山东省", - "name": "莱芜市", - "id": "371200" - }, - { - "province": "山东省", - "name": "临沂市", - "id": "371300" - }, - { - "province": "山东省", - "name": "德州市", - "id": "371400" - }, - { - "province": "山东省", - "name": "聊城市", - "id": "371500" - }, - { - "province": "山东省", - "name": "滨州市", - "id": "371600" - }, - { - "province": "山东省", - "name": "菏泽市", - "id": "371700" - } - ], - "410000": [ - { - "province": "河南省", - "name": "郑州市", - "id": "410100" - }, - { - "province": "河南省", - "name": "开封市", - "id": "410200" - }, - { - "province": "河南省", - "name": "洛阳市", - "id": "410300" - }, - { - "province": "河南省", - "name": "平顶山市", - "id": "410400" - }, - { - "province": "河南省", - "name": "安阳市", - "id": "410500" - }, - { - "province": "河南省", - "name": "鹤壁市", - "id": "410600" - }, - { - "province": "河南省", - "name": "新乡市", - "id": "410700" - }, - { - "province": "河南省", - "name": "焦作市", - "id": "410800" - }, - { - "province": "河南省", - "name": "濮阳市", - "id": "410900" - }, - { - "province": "河南省", - "name": "许昌市", - "id": "411000" - }, - { - "province": "河南省", - "name": "漯河市", - "id": "411100" - }, - { - "province": "河南省", - "name": "三门峡市", - "id": "411200" - }, - { - "province": "河南省", - "name": "南阳市", - "id": "411300" - }, - { - "province": "河南省", - "name": "商丘市", - "id": "411400" - }, - { - "province": "河南省", - "name": "信阳市", - "id": "411500" - }, - { - "province": "河南省", - "name": "周口市", - "id": "411600" - }, - { - "province": "河南省", - "name": "驻马店市", - "id": "411700" - }, - { - "province": "河南省", - "name": "省直辖县级行政区划", - "id": "419000" - } - ], - "420000": [ - { - "province": "湖北省", - "name": "武汉市", - "id": "420100" - }, - { - "province": "湖北省", - "name": "黄石市", - "id": "420200" - }, - { - "province": "湖北省", - "name": "十堰市", - "id": "420300" - }, - { - "province": "湖北省", - "name": "宜昌市", - "id": "420500" - }, - { - "province": "湖北省", - "name": "襄阳市", - "id": "420600" - }, - { - "province": "湖北省", - "name": "鄂州市", - "id": "420700" - }, - { - "province": "湖北省", - "name": "荆门市", - "id": "420800" - }, - { - "province": "湖北省", - "name": "孝感市", - "id": "420900" - }, - { - "province": "湖北省", - "name": "荆州市", - "id": "421000" - }, - { - "province": "湖北省", - "name": "黄冈市", - "id": "421100" - }, - { - "province": "湖北省", - "name": "咸宁市", - "id": "421200" - }, - { - "province": "湖北省", - "name": "随州市", - "id": "421300" - }, - { - "province": "湖北省", - "name": "恩施土家族苗族自治州", - "id": "422800" - }, - { - "province": "湖北省", - "name": "省直辖县级行政区划", - "id": "429000" - } - ], - "430000": [ - { - "province": "湖南省", - "name": "长沙市", - "id": "430100" - }, - { - "province": "湖南省", - "name": "株洲市", - "id": "430200" - }, - { - "province": "湖南省", - "name": "湘潭市", - "id": "430300" - }, - { - "province": "湖南省", - "name": "衡阳市", - "id": "430400" - }, - { - "province": "湖南省", - "name": "邵阳市", - "id": "430500" - }, - { - "province": "湖南省", - "name": "岳阳市", - "id": "430600" - }, - { - "province": "湖南省", - "name": "常德市", - "id": "430700" - }, - { - "province": "湖南省", - "name": "张家界市", - "id": "430800" - }, - { - "province": "湖南省", - "name": "益阳市", - "id": "430900" - }, - { - "province": "湖南省", - "name": "郴州市", - "id": "431000" - }, - { - "province": "湖南省", - "name": "永州市", - "id": "431100" - }, - { - "province": "湖南省", - "name": "怀化市", - "id": "431200" - }, - { - "province": "湖南省", - "name": "娄底市", - "id": "431300" - }, - { - "province": "湖南省", - "name": "湘西土家族苗族自治州", - "id": "433100" - } - ], - "440000": [ - { - "province": "广东省", - "name": "广州市", - "id": "440100" - }, - { - "province": "广东省", - "name": "韶关市", - "id": "440200" - }, - { - "province": "广东省", - "name": "深圳市", - "id": "440300" - }, - { - "province": "广东省", - "name": "珠海市", - "id": "440400" - }, - { - "province": "广东省", - "name": "汕头市", - "id": "440500" - }, - { - "province": "广东省", - "name": "佛山市", - "id": "440600" - }, - { - "province": "广东省", - "name": "江门市", - "id": "440700" - }, - { - "province": "广东省", - "name": "湛江市", - "id": "440800" - }, - { - "province": "广东省", - "name": "茂名市", - "id": "440900" - }, - { - "province": "广东省", - "name": "肇庆市", - "id": "441200" - }, - { - "province": "广东省", - "name": "惠州市", - "id": "441300" - }, - { - "province": "广东省", - "name": "梅州市", - "id": "441400" - }, - { - "province": "广东省", - "name": "汕尾市", - "id": "441500" - }, - { - "province": "广东省", - "name": "河源市", - "id": "441600" - }, - { - "province": "广东省", - "name": "阳江市", - "id": "441700" - }, - { - "province": "广东省", - "name": "清远市", - "id": "441800" - }, - { - "province": "广东省", - "name": "东莞市", - "id": "441900" - }, - { - "province": "广东省", - "name": "中山市", - "id": "442000" - }, - { - "province": "广东省", - "name": "潮州市", - "id": "445100" - }, - { - "province": "广东省", - "name": "揭阳市", - "id": "445200" - }, - { - "province": "广东省", - "name": "云浮市", - "id": "445300" - } - ], - "450000": [ - { - "province": "广西壮族自治区", - "name": "南宁市", - "id": "450100" - }, - { - "province": "广西壮族自治区", - "name": "柳州市", - "id": "450200" - }, - { - "province": "广西壮族自治区", - "name": "桂林市", - "id": "450300" - }, - { - "province": "广西壮族自治区", - "name": "梧州市", - "id": "450400" - }, - { - "province": "广西壮族自治区", - "name": "北海市", - "id": "450500" - }, - { - "province": "广西壮族自治区", - "name": "防城港市", - "id": "450600" - }, - { - "province": "广西壮族自治区", - "name": "钦州市", - "id": "450700" - }, - { - "province": "广西壮族自治区", - "name": "贵港市", - "id": "450800" - }, - { - "province": "广西壮族自治区", - "name": "玉林市", - "id": "450900" - }, - { - "province": "广西壮族自治区", - "name": "百色市", - "id": "451000" - }, - { - "province": "广西壮族自治区", - "name": "贺州市", - "id": "451100" - }, - { - "province": "广西壮族自治区", - "name": "河池市", - "id": "451200" - }, - { - "province": "广西壮族自治区", - "name": "来宾市", - "id": "451300" - }, - { - "province": "广西壮族自治区", - "name": "崇左市", - "id": "451400" - } - ], - "460000": [ - { - "province": "海南省", - "name": "海口市", - "id": "460100" - }, - { - "province": "海南省", - "name": "三亚市", - "id": "460200" - }, - { - "province": "海南省", - "name": "三沙市", - "id": "460300" - }, - { - "province": "海南省", - "name": "儋州市", - "id": "460400" - }, - { - "province": "海南省", - "name": "省直辖县级行政区划", - "id": "469000" - } - ], - "500000": [ - { - "province": "重庆市", - "name": "市辖区", - "id": "500100" - }, - { - "province": "重庆市", - "name": "县", - "id": "500200" - } - ], - "510000": [ - { - "province": "四川省", - "name": "成都市", - "id": "510100" - }, - { - "province": "四川省", - "name": "自贡市", - "id": "510300" - }, - { - "province": "四川省", - "name": "攀枝花市", - "id": "510400" - }, - { - "province": "四川省", - "name": "泸州市", - "id": "510500" - }, - { - "province": "四川省", - "name": "德阳市", - "id": "510600" - }, - { - "province": "四川省", - "name": "绵阳市", - "id": "510700" - }, - { - "province": "四川省", - "name": "广元市", - "id": "510800" - }, - { - "province": "四川省", - "name": "遂宁市", - "id": "510900" - }, - { - "province": "四川省", - "name": "内江市", - "id": "511000" - }, - { - "province": "四川省", - "name": "乐山市", - "id": "511100" - }, - { - "province": "四川省", - "name": "南充市", - "id": "511300" - }, - { - "province": "四川省", - "name": "眉山市", - "id": "511400" - }, - { - "province": "四川省", - "name": "宜宾市", - "id": "511500" - }, - { - "province": "四川省", - "name": "广安市", - "id": "511600" - }, - { - "province": "四川省", - "name": "达州市", - "id": "511700" - }, - { - "province": "四川省", - "name": "雅安市", - "id": "511800" - }, - { - "province": "四川省", - "name": "巴中市", - "id": "511900" - }, - { - "province": "四川省", - "name": "资阳市", - "id": "512000" - }, - { - "province": "四川省", - "name": "阿坝藏族羌族自治州", - "id": "513200" - }, - { - "province": "四川省", - "name": "甘孜藏族自治州", - "id": "513300" - }, - { - "province": "四川省", - "name": "凉山彝族自治州", - "id": "513400" - } - ], - "520000": [ - { - "province": "贵州省", - "name": "贵阳市", - "id": "520100" - }, - { - "province": "贵州省", - "name": "六盘水市", - "id": "520200" - }, - { - "province": "贵州省", - "name": "遵义市", - "id": "520300" - }, - { - "province": "贵州省", - "name": "安顺市", - "id": "520400" - }, - { - "province": "贵州省", - "name": "毕节市", - "id": "520500" - }, - { - "province": "贵州省", - "name": "铜仁市", - "id": "520600" - }, - { - "province": "贵州省", - "name": "黔西南布依族苗族自治州", - "id": "522300" - }, - { - "province": "贵州省", - "name": "黔东南苗族侗族自治州", - "id": "522600" - }, - { - "province": "贵州省", - "name": "黔南布依族苗族自治州", - "id": "522700" - } - ], - "530000": [ - { - "province": "云南省", - "name": "昆明市", - "id": "530100" - }, - { - "province": "云南省", - "name": "曲靖市", - "id": "530300" - }, - { - "province": "云南省", - "name": "玉溪市", - "id": "530400" - }, - { - "province": "云南省", - "name": "保山市", - "id": "530500" - }, - { - "province": "云南省", - "name": "昭通市", - "id": "530600" - }, - { - "province": "云南省", - "name": "丽江市", - "id": "530700" - }, - { - "province": "云南省", - "name": "普洱市", - "id": "530800" - }, - { - "province": "云南省", - "name": "临沧市", - "id": "530900" - }, - { - "province": "云南省", - "name": "楚雄彝族自治州", - "id": "532300" - }, - { - "province": "云南省", - "name": "红河哈尼族彝族自治州", - "id": "532500" - }, - { - "province": "云南省", - "name": "文山壮族苗族自治州", - "id": "532600" - }, - { - "province": "云南省", - "name": "西双版纳傣族自治州", - "id": "532800" - }, - { - "province": "云南省", - "name": "大理白族自治州", - "id": "532900" - }, - { - "province": "云南省", - "name": "德宏傣族景颇族自治州", - "id": "533100" - }, - { - "province": "云南省", - "name": "怒江傈僳族自治州", - "id": "533300" - }, - { - "province": "云南省", - "name": "迪庆藏族自治州", - "id": "533400" - } - ], - "540000": [ - { - "province": "西藏自治区", - "name": "拉萨市", - "id": "540100" - }, - { - "province": "西藏自治区", - "name": "日喀则市", - "id": "540200" - }, - { - "province": "西藏自治区", - "name": "昌都市", - "id": "540300" - }, - { - "province": "西藏自治区", - "name": "林芝市", - "id": "540400" - }, - { - "province": "西藏自治区", - "name": "山南市", - "id": "540500" - }, - { - "province": "西藏自治区", - "name": "那曲地区", - "id": "542400" - }, - { - "province": "西藏自治区", - "name": "阿里地区", - "id": "542500" - } - ], - "610000": [ - { - "province": "陕西省", - "name": "西安市", - "id": "610100" - }, - { - "province": "陕西省", - "name": "铜川市", - "id": "610200" - }, - { - "province": "陕西省", - "name": "宝鸡市", - "id": "610300" - }, - { - "province": "陕西省", - "name": "咸阳市", - "id": "610400" - }, - { - "province": "陕西省", - "name": "渭南市", - "id": "610500" - }, - { - "province": "陕西省", - "name": "延安市", - "id": "610600" - }, - { - "province": "陕西省", - "name": "汉中市", - "id": "610700" - }, - { - "province": "陕西省", - "name": "榆林市", - "id": "610800" - }, - { - "province": "陕西省", - "name": "安康市", - "id": "610900" - }, - { - "province": "陕西省", - "name": "商洛市", - "id": "611000" - } - ], - "620000": [ - { - "province": "甘肃省", - "name": "兰州市", - "id": "620100" - }, - { - "province": "甘肃省", - "name": "嘉峪关市", - "id": "620200" - }, - { - "province": "甘肃省", - "name": "金昌市", - "id": "620300" - }, - { - "province": "甘肃省", - "name": "白银市", - "id": "620400" - }, - { - "province": "甘肃省", - "name": "天水市", - "id": "620500" - }, - { - "province": "甘肃省", - "name": "武威市", - "id": "620600" - }, - { - "province": "甘肃省", - "name": "张掖市", - "id": "620700" - }, - { - "province": "甘肃省", - "name": "平凉市", - "id": "620800" - }, - { - "province": "甘肃省", - "name": "酒泉市", - "id": "620900" - }, - { - "province": "甘肃省", - "name": "庆阳市", - "id": "621000" - }, - { - "province": "甘肃省", - "name": "定西市", - "id": "621100" - }, - { - "province": "甘肃省", - "name": "陇南市", - "id": "621200" - }, - { - "province": "甘肃省", - "name": "临夏回族自治州", - "id": "622900" - }, - { - "province": "甘肃省", - "name": "甘南藏族自治州", - "id": "623000" - } - ], - "630000": [ - { - "province": "青海省", - "name": "西宁市", - "id": "630100" - }, - { - "province": "青海省", - "name": "海东市", - "id": "630200" - }, - { - "province": "青海省", - "name": "海北藏族自治州", - "id": "632200" - }, - { - "province": "青海省", - "name": "黄南藏族自治州", - "id": "632300" - }, - { - "province": "青海省", - "name": "海南藏族自治州", - "id": "632500" - }, - { - "province": "青海省", - "name": "果洛藏族自治州", - "id": "632600" - }, - { - "province": "青海省", - "name": "玉树藏族自治州", - "id": "632700" - }, - { - "province": "青海省", - "name": "海西蒙古族藏族自治州", - "id": "632800" - } - ], - "640000": [ - { - "province": "宁夏回族自治区", - "name": "银川市", - "id": "640100" - }, - { - "province": "宁夏回族自治区", - "name": "石嘴山市", - "id": "640200" - }, - { - "province": "宁夏回族自治区", - "name": "吴忠市", - "id": "640300" - }, - { - "province": "宁夏回族自治区", - "name": "固原市", - "id": "640400" - }, - { - "province": "宁夏回族自治区", - "name": "中卫市", - "id": "640500" - } - ], - "650000": [ - { - "province": "新疆维吾尔自治区", - "name": "乌鲁木齐市", - "id": "650100" - }, - { - "province": "新疆维吾尔自治区", - "name": "克拉玛依市", - "id": "650200" - }, - { - "province": "新疆维吾尔自治区", - "name": "吐鲁番市", - "id": "650400" - }, - { - "province": "新疆维吾尔自治区", - "name": "哈密市", - "id": "650500" - }, - { - "province": "新疆维吾尔自治区", - "name": "昌吉回族自治州", - "id": "652300" - }, - { - "province": "新疆维吾尔自治区", - "name": "博尔塔拉蒙古自治州", - "id": "652700" - }, - { - "province": "新疆维吾尔自治区", - "name": "巴音郭楞蒙古自治州", - "id": "652800" - }, - { - "province": "新疆维吾尔自治区", - "name": "阿克苏地区", - "id": "652900" - }, - { - "province": "新疆维吾尔自治区", - "name": "克孜勒苏柯尔克孜自治州", - "id": "653000" - }, - { - "province": "新疆维吾尔自治区", - "name": "喀什地区", - "id": "653100" - }, - { - "province": "新疆维吾尔自治区", - "name": "和田地区", - "id": "653200" - }, - { - "province": "新疆维吾尔自治区", - "name": "伊犁哈萨克自治州", - "id": "654000" - }, - { - "province": "新疆维吾尔自治区", - "name": "塔城地区", - "id": "654200" - }, - { - "province": "新疆维吾尔自治区", - "name": "阿勒泰地区", - "id": "654300" - }, - { - "province": "新疆维吾尔自治区", - "name": "自治区直辖县级行政区划", - "id": "659000" - } - ] -} diff --git a/src/pages/account/settings/geographic/province.json b/src/pages/account/settings/geographic/province.json deleted file mode 100644 index 910c83f0..00000000 --- a/src/pages/account/settings/geographic/province.json +++ /dev/null @@ -1,138 +0,0 @@ -[ - { - "name": "北京市", - "id": "110000" - }, - { - "name": "天津市", - "id": "120000" - }, - { - "name": "河北省", - "id": "130000" - }, - { - "name": "山西省", - "id": "140000" - }, - { - "name": "内蒙古自治区", - "id": "150000" - }, - { - "name": "辽宁省", - "id": "210000" - }, - { - "name": "吉林省", - "id": "220000" - }, - { - "name": "黑龙江省", - "id": "230000" - }, - { - "name": "上海市", - "id": "310000" - }, - { - "name": "江苏省", - "id": "320000" - }, - { - "name": "浙江省", - "id": "330000" - }, - { - "name": "安徽省", - "id": "340000" - }, - { - "name": "福建省", - "id": "350000" - }, - { - "name": "江西省", - "id": "360000" - }, - { - "name": "山东省", - "id": "370000" - }, - { - "name": "河南省", - "id": "410000" - }, - { - "name": "湖北省", - "id": "420000" - }, - { - "name": "湖南省", - "id": "430000" - }, - { - "name": "广东省", - "id": "440000" - }, - { - "name": "广西壮族自治区", - "id": "450000" - }, - { - "name": "海南省", - "id": "460000" - }, - { - "name": "重庆市", - "id": "500000" - }, - { - "name": "四川省", - "id": "510000" - }, - { - "name": "贵州省", - "id": "520000" - }, - { - "name": "云南省", - "id": "530000" - }, - { - "name": "西藏自治区", - "id": "540000" - }, - { - "name": "陕西省", - "id": "610000" - }, - { - "name": "甘肃省", - "id": "620000" - }, - { - "name": "青海省", - "id": "630000" - }, - { - "name": "宁夏回族自治区", - "id": "640000" - }, - { - "name": "新疆维吾尔自治区", - "id": "650000" - }, - { - "name": "台湾省", - "id": "710000" - }, - { - "name": "香港特别行政区", - "id": "810000" - }, - { - "name": "澳门特别行政区", - "id": "820000" - } -] diff --git a/src/pages/account/settings/service.test.ts b/src/pages/account/settings/service.test.ts new file mode 100644 index 00000000..1063e136 --- /dev/null +++ b/src/pages/account/settings/service.test.ts @@ -0,0 +1,22 @@ +import { request } from '@umijs/max'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { queryCity } from './service'; + +vi.mock('@umijs/max', () => ({ + request: vi.fn(), +})); + +describe('account settings service', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(request).mockResolvedValue({ data: [] }); + }); + + it('encodes province when requesting city options', async () => { + await queryCity('33/000?x=1'); + + expect(request).toHaveBeenCalledWith( + '/api/geographic/city/33%2F000%3Fx%3D1', + ); + }); +}); diff --git a/src/pages/account/settings/service.ts b/src/pages/account/settings/service.ts index 067fc1d5..cf157dfc 100644 --- a/src/pages/account/settings/service.ts +++ b/src/pages/account/settings/service.ts @@ -12,7 +12,9 @@ export async function queryProvince(): Promise { export async function queryCity( province: string, ): Promise { - return request(`/api/geographic/city/${province}`).then(({ data }) => data); + return request(`/api/geographic/city/${encodeURIComponent(province)}`).then( + ({ data }) => data, + ); } export async function query() { diff --git a/src/utils/chinaDivision.test.ts b/src/utils/chinaDivision.test.ts new file mode 100644 index 00000000..9c0d14a0 --- /dev/null +++ b/src/utils/chinaDivision.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; +import { getCityOptions, provinceOptions } from './chinaDivision'; + +describe('chinaDivision geographic options', () => { + it('keeps province values compatible with existing six-digit province ids', () => { + expect(provinceOptions).toContainEqual({ + label: '浙江省', + key: '330000', + }); + expect(provinceOptions).toContainEqual({ + label: '台湾省', + key: '710000', + }); + }); + + it('returns city options for ordinary provinces', () => { + expect(getCityOptions('130000')).toContainEqual({ + label: '石家庄市', + key: '130100', + }); + expect(getCityOptions('320000')).toContainEqual({ + label: '南京市', + key: '320100', + }); + expect(getCityOptions('32')).toContainEqual({ + label: '南京市', + key: '320100', + }); + }); + + it('returns district options for municipalities', () => { + expect(getCityOptions('110000')).toContainEqual({ + label: '东城区', + key: '110101', + }); + }); + + it('returns city options for Hong Kong, Macao and Taiwan', () => { + expect(getCityOptions('710000')).toContainEqual({ + label: '台北市', + key: '710100', + }); + expect(getCityOptions('810000')).toContainEqual({ + label: '香港岛', + key: '810100', + }); + expect(getCityOptions('820000')).toContainEqual({ + label: '澳门半岛', + key: '820100', + }); + }); +}); diff --git a/src/utils/chinaDivision.ts b/src/utils/chinaDivision.ts new file mode 100644 index 00000000..80076589 --- /dev/null +++ b/src/utils/chinaDivision.ts @@ -0,0 +1,69 @@ +import hkMoTwSource from 'china-division/dist/HK-MO-TW.json'; +import pcaSource from 'china-division/dist/pca-code.json'; + +export type GeographicOption = { + label: string; + key: string; +}; + +type DivisionNode = { + code: string; + name: string; + children?: DivisionNode[]; +}; + +const directAdminCodes = new Set(['11', '12', '31', '50']); + +const specialRegions: GeographicOption[] = [ + { label: '台湾省', key: '710000' }, + { label: '香港特别行政区', key: '810000' }, + { label: '澳门特别行政区', key: '820000' }, +]; + +const toProvinceKey = (code: string) => `${code}0000`; + +const toCityKey = (code: string) => (code.length === 4 ? `${code}00` : code); + +const toOption = (item: DivisionNode): GeographicOption => ({ + label: item.name, + key: toCityKey(item.code), +}); + +export const provinceOptions: GeographicOption[] = [ + ...(pcaSource as DivisionNode[]).map((item) => ({ + label: item.name, + key: toProvinceKey(item.code), + })), + ...specialRegions, +]; + +export const getCityOptions = (provinceKey: string): GeographicOption[] => { + const provinceCode = + provinceKey.length === 2 ? provinceKey : provinceKey.slice(0, 2); + const province = (pcaSource as DivisionNode[]).find( + (item) => item.code === provinceCode, + ); + + if (province) { + const children = province.children ?? []; + const cityNodes = directAdminCodes.has(provinceCode) + ? children.flatMap((item) => item.children ?? [item]) + : children; + + return cityNodes.map(toOption); + } + + const specialRegion = specialRegions.find((item) => item.key === provinceKey); + if (!specialRegion) { + return []; + } + + return Object.keys( + (hkMoTwSource as Record>)[ + specialRegion.label + ] ?? {}, + ).map((name, index) => ({ + label: name, + key: `${provinceCode}${String(index + 1).padStart(2, '0')}00`, + })); +};