refactor(goods): 移除地区选择器组件并优化首页界面
This commit is contained in:
@@ -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
30
pnpm-lock.yaml
generated
@@ -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
@@ -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"}]
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './GoodsCategoryList'
|
||||
export * from './GoodsRedeemModal'
|
||||
export * from './RegionPicker'
|
||||
export * from './useAssetsRefresh'
|
||||
export * from './useAssetsQuery'
|
||||
export * from './useGoodsCatalog'
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user