refactor(goods): 移除地区选择器组件并优化首页界面

This commit is contained in:
JiaJun
2026-04-16 09:05:59 +08:00
parent dcc9395b25
commit d8073ff640
8 changed files with 12 additions and 227 deletions

View File

@@ -12,12 +12,10 @@
"dependencies": {
"@tanstack/react-query": "^5.96.2",
"clsx": "^2.1.1",
"element-china-area-data": "^6.1.0",
"i18next": "^26.0.4",
"ky": "^2.0.0",
"lucide-react": "^0.577.0",
"motion": "^12.38.0",
"province-city-china": "^8.5.8",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^17.0.2",

30
pnpm-lock.yaml generated
View File

@@ -14,9 +14,6 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
element-china-area-data:
specifier: ^6.1.0
version: 6.1.0
i18next:
specifier: ^26.0.4
version: 26.0.4(typescript@5.9.3)
@@ -29,9 +26,6 @@ importers:
motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
province-city-china:
specifier: ^8.5.8
version: 8.5.8
react:
specifier: ^19.2.4
version: 19.2.4
@@ -312,9 +306,6 @@ packages:
'@oxc-project/types@0.115.0':
resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==}
'@province-city-china/types@8.5.8':
resolution: {integrity: sha512-KZ3NyM8HsaBVcn5BRhWaOeZRhqEvm18PfB6HfRDuZfwwWhJLoTxB81mTENrBlONr2g8fy/fSbjsh44gvOj+/Lw==}
'@rolldown/binding-android-arm64@1.0.0-rc.9':
resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -733,9 +724,6 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
china-division@2.7.0:
resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
@@ -795,9 +783,6 @@ packages:
electron-to-chromium@1.5.313:
resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==}
element-china-area-data@6.1.0:
resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'}
@@ -1369,9 +1354,6 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
province-city-china@8.5.8:
resolution: {integrity: sha512-gUV5kSOWHVufemkq6lygb0ngNZ4snMcONmr3QzxHuj1MYOQPphiyjHplfmywcVGWdGQgim30RXia/mYB007eLg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2006,8 +1988,6 @@ snapshots:
'@oxc-project/types@0.115.0': {}
'@province-city-china/types@8.5.8': {}
'@rolldown/binding-android-arm64@1.0.0-rc.9':
optional: true
@@ -2363,8 +2343,6 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
china-division@2.7.0: {}
chownr@3.0.0: {}
clsx@2.1.1: {}
@@ -2403,10 +2381,6 @@ snapshots:
electron-to-chromium@1.5.313: {}
element-china-area-data@6.1.0:
dependencies:
china-division: 2.7.0
elementtree@0.1.7:
dependencies:
sax: 1.1.4
@@ -2903,10 +2877,6 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
province-city-china@8.5.8:
dependencies:
'@province-city-china/types': 8.5.8
punycode@2.3.1: {}
react-dom@19.2.4(react@19.2.4):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
[{"c":"110000","n":"北京市","p":"11"},{"c":"120000","n":"天津市","p":"12"},{"c":"130000","n":"河北省","p":"13"},{"c":"140000","n":"山西省","p":"14"},{"c":"150000","n":"内蒙古自治区","p":"15"},{"c":"210000","n":"辽宁省","p":"21"},{"c":"220000","n":"吉林省","p":"22"},{"c":"230000","n":"黑龙江省","p":"23"},{"c":"310000","n":"上海市","p":"31"},{"c":"320000","n":"江苏省","p":"32"},{"c":"330000","n":"浙江省","p":"33"},{"c":"340000","n":"安徽省","p":"34"},{"c":"350000","n":"福建省","p":"35"},{"c":"360000","n":"江西省","p":"36"},{"c":"370000","n":"山东省","p":"37"},{"c":"410000","n":"河南省","p":"41"},{"c":"420000","n":"湖北省","p":"42"},{"c":"430000","n":"湖南省","p":"43"},{"c":"440000","n":"广东省","p":"44"},{"c":"450000","n":"广西壮族自治区","p":"45"},{"c":"460000","n":"海南省","p":"46"},{"c":"500000","n":"重庆市","p":"50"},{"c":"510000","n":"四川省","p":"51"},{"c":"520000","n":"贵州省","p":"52"},{"c":"530000","n":"云南省","p":"53"},{"c":"540000","n":"西藏自治区","p":"54"},{"c":"610000","n":"陕西省","p":"61"},{"c":"620000","n":"甘肃省","p":"62"},{"c":"630000","n":"青海省","p":"63"},{"c":"640000","n":"宁夏回族自治区","p":"64"},{"c":"650000","n":"新疆维吾尔自治区","p":"65"},{"c":"710000","n":"台湾省","p":"71"},{"c":"810000","n":"香港特别行政区","p":"81"},{"c":"820000","n":"澳门特别行政区","p":"82"}]

View File

@@ -1,180 +0,0 @@
import {useEffect, useState} from 'react'
import {ChevronDown} from 'lucide-react'
type ProvinceNode = {
c: string
n: string
p: string
}
type CityNode = {
c: string
n: string
p: string
y: string
}
type AreaNode = {
c: string
n: string
p: string
y: string
a: string
}
type RegionPickerProps = {
value: string[]
onChange: (value: string[]) => void
}
type RegionDataset = {
province: ProvinceNode[]
city: CityNode[]
area: AreaNode[]
}
const DIRECT_MUNICIPALITY_NAMES = new Set(['北京市', '上海市', '天津市', '重庆市'])
function RegionSelect({
value,
placeholder,
options,
onChange,
disabled = false,
}: {
value: string
placeholder: string
options: Array<{c: string; n: string}>
onChange: (value: string) => void
disabled?: boolean
}) {
return (
<div className="relative">
<select
value={value}
onChange={(event) => onChange(event.target.value)}
disabled={disabled}
className="h-[42px] w-full appearance-none rounded-[10px] border border-white/10 bg-[#0E0B12] px-[12px] pr-[36px] text-[14px] text-white outline-none transition-colors focus:border-[#FA6A00] disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">{placeholder}</option>
{options.map((option) => (
<option key={option.c} value={option.n}>
{option.n}
</option>
))}
</select>
<div className="pointer-events-none absolute right-[12px] top-1/2 -translate-y-1/2 text-white/38">
<ChevronDown className="h-[14px] w-[14px]" aria-hidden="true" />
</div>
</div>
)
}
export function RegionPicker({value, onChange}: RegionPickerProps) {
const [regionData, setRegionData] = useState<RegionDataset | null>(null)
const [loadFailed, setLoadFailed] = useState(false)
const normalizedValue = value.filter(Boolean)
const province = normalizedValue[0] ?? ''
useEffect(() => {
let cancelled = false
const loadRegionData = async () => {
try {
const [provinceResponse, cityResponse, areaResponse] = await Promise.all([
fetch(`${import.meta.env.BASE_URL}china-data/province.min.json`),
fetch(`${import.meta.env.BASE_URL}china-data/city.min.json`),
fetch(`${import.meta.env.BASE_URL}china-data/area.min.json`),
])
if (!provinceResponse.ok || !cityResponse.ok || !areaResponse.ok) {
throw new Error('Failed to load region data')
}
const [provinceList, cityList, areaList] = await Promise.all([
provinceResponse.json() as Promise<ProvinceNode[]>,
cityResponse.json() as Promise<CityNode[]>,
areaResponse.json() as Promise<AreaNode[]>,
])
if (!cancelled) {
setRegionData({
province: provinceList,
city: cityList,
area: areaList,
})
setLoadFailed(false)
}
} catch {
if (!cancelled) {
setLoadFailed(true)
}
}
}
void loadRegionData()
return () => {
cancelled = true
}
}, [])
const provinceList = regionData?.province ?? []
const cityList = regionData?.city ?? []
const areaList = regionData?.area ?? []
const selectedProvince = provinceList.find((item) => item.n === province)
const cities = selectedProvince
? cityList.filter((item) => item.p === selectedProvince.p)
: []
const isDirectMunicipality = province ? DIRECT_MUNICIPALITY_NAMES.has(province) || (selectedProvince != null && cities.length === 0) : false
const city = isDirectMunicipality ? '' : normalizedValue[1] ?? ''
const district = isDirectMunicipality ? normalizedValue[1] ?? '' : normalizedValue[2] ?? ''
const selectedCity = cityList.find((item) => item.n === city && item.p === selectedProvince?.p)
const districts = isDirectMunicipality
? selectedProvince
? areaList.filter((item) => item.p === selectedProvince.p)
: []
: selectedCity
? areaList.filter((item) => item.p === selectedCity.p && item.y === selectedCity.y)
: []
return (
<div className={`grid grid-cols-1 gap-[10px] ${isDirectMunicipality ? 'sm:grid-cols-2' : 'sm:grid-cols-3'}`}>
<RegionSelect
value={province}
placeholder={loadFailed ? 'failed to load' : 'province'}
options={provinceList}
onChange={(nextProvince) => onChange(nextProvince ? [nextProvince] : [])}
disabled={loadFailed || !regionData}
/>
{isDirectMunicipality ? (
<RegionSelect
value={district}
placeholder="district"
options={districts}
disabled={loadFailed || !regionData || !province}
onChange={(nextDistrict) => onChange(nextDistrict ? [province, nextDistrict] : [province])}
/>
) : (
<RegionSelect
value={city}
placeholder="city"
options={cities}
disabled={loadFailed || !regionData || !province}
onChange={(nextCity) => onChange(nextCity ? [province, nextCity] : [province])}
/>
)}
{isDirectMunicipality ? null : (
<RegionSelect
value={district}
placeholder="district"
options={districts}
disabled={loadFailed || !regionData || !city}
onChange={(nextDistrict) => onChange(nextDistrict ? [province, city, nextDistrict] : [province, city])}
/>
)}
</div>
)
}

View File

@@ -1,6 +1,5 @@
export * from './GoodsCategoryList'
export * from './GoodsRedeemModal'
export * from './RegionPicker'
export * from './useAssetsRefresh'
export * from './useAssetsQuery'
export * from './useGoodsCatalog'

View File

@@ -13,6 +13,7 @@ import {
Coins,
Gauge,
History,
Sparkles,
UserRound,
} from 'lucide-react'
import type {
@@ -86,7 +87,6 @@ function HomePage() {
const {assetsInfo} = useAssetsQuery()
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
const isEnglish = normalizeLanguage(language) === 'en'
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
...category,
items: category.items.slice(0, 4),
@@ -221,16 +221,12 @@ function HomePage() {
</div>
<div className="flex min-w-0 flex-1 flex-col gap-3">
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-[minmax(0,7fr)_minmax(0,3fr)] gap-3">
<div
className="home-withdraw-card pc-hover-float h-full min-h-[120px] py-[14px] sm:py-[18px]">
<div className="flex h-full w-full flex-col justify-between gap-[18px]">
<div
className={`text-center text-white/68 ${
isEnglish
? 'text-[10px] tracking-[0.01em] sm:text-[13px] sm:tracking-[0.06em]'
: 'text-[11px] tracking-[0.04em] sm:text-[14px] sm:tracking-[0.16em]'
}`}>{t('home.availableForWithdrawal')}</div>
className="text-center text-[11px] font-medium tracking-[0.06em] text-white/88 drop-shadow-[0_1px_10px_rgba(0,0,0,0.28)] sm:text-[14px] sm:tracking-[0.16em]">{t('home.availableForWithdrawal')}</div>
<div className="flex flex-1 items-center justify-center text-center">
<div
className="min-w-0 break-all text-[clamp(1.6rem,5vw,2.6rem)] font-semibold leading-none tracking-[-0.02em] text-white">
@@ -243,13 +239,18 @@ function HomePage() {
</div>
<div
className="home-withdraw-card pc-hover-float h-full min-h-[120px] px-[12px] py-[14px] sm:px-[16px] sm:py-[18px]">
className="home-stat-card home-stat-card-claim pc-hover-float h-full min-h-[120px] px-[12px] py-[14px] sm:px-[16px] sm:py-[18px]">
<div className="flex h-full w-full flex-col justify-between gap-[18px]">
<div
className="text-center text-[12px] tracking-[0.08em] text-white/68 sm:text-[14px] sm:tracking-[0.16em]">{t('home.availablePoints')}</div>
<div className="flex items-start justify-between gap-[10px]">
<div
className="text-[11px] font-medium tracking-[0.06em] text-white/82 sm:text-[13px] sm:tracking-[0.14em]">{t('home.availablePoints')}</div>
<div className="flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-[10px] bg-[#d58a2d]/18 text-[#f5a322] sm:h-[34px] sm:w-[34px] sm:rounded-[12px]">
<Sparkles className="h-[14px] w-[14px] sm:h-[16px] sm:w-[16px]" aria-hidden="true"/>
</div>
</div>
<div className="flex flex-1 items-center justify-center text-center">
<div
className="min-w-0 break-all text-[clamp(1.6rem,5vw,2.6rem)] font-semibold leading-none tracking-[-0.02em] text-white">
className="min-w-0 break-all text-[clamp(1.2rem,4vw,1.9rem)] font-semibold leading-none tracking-[-0.02em] text-white">
{assetsInfo?.available_points || 0}
</div>
</div>