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

@@ -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>