fix(lang): 优化语言处理和数据加载逻辑
This commit is contained in:
@@ -30,7 +30,10 @@ function App() {
|
|||||||
|
|
||||||
if (previousLanguage && previousLanguage !== normalizedLanguage) {
|
if (previousLanguage && previousLanguage !== normalizedLanguage) {
|
||||||
await queryClient.cancelQueries()
|
await queryClient.cancelQueries()
|
||||||
await queryClient.invalidateQueries({refetchType: 'active'})
|
await queryClient.invalidateQueries({
|
||||||
|
refetchType: 'active',
|
||||||
|
predicate: (query) => query.queryKey[0] !== 'auth-bootstrap',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
previousLanguageRef.current = normalizedLanguage
|
previousLanguageRef.current = normalizedLanguage
|
||||||
@@ -120,7 +123,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage/>}/>
|
<Route path="/" element={<HomePage/>}/>
|
||||||
<Route path="/goods" element={<GoodsPage/>}/>
|
<Route path="/goods" element={<GoodsPage/>}/>
|
||||||
<Route path="/record" element={<RecordPage/>}/>
|
<Route path="/record" element={<RecordPage/>}/>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||||
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressForm)
|
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressForm)
|
||||||
|
const [loadAddressesLoading, setLoadAddressesLoading] = useState(false)
|
||||||
|
|
||||||
const addressListQuery = useQuery({
|
const addressListQuery = useQuery({
|
||||||
queryKey: queryKeys.addressList(sessionId),
|
queryKey: queryKeys.addressList(sessionId),
|
||||||
@@ -100,6 +101,8 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoadAddressesLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await queryClient.fetchQuery({
|
const result = await queryClient.fetchQuery({
|
||||||
queryKey: queryKeys.addressList(sessionId),
|
queryKey: queryKeys.addressList(sessionId),
|
||||||
@@ -112,6 +115,8 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
return result
|
return result
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
|
} finally {
|
||||||
|
setLoadAddressesLoading(false)
|
||||||
}
|
}
|
||||||
}, [queryClient, sessionId])
|
}, [queryClient, sessionId])
|
||||||
|
|
||||||
@@ -177,7 +182,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
sessionId,
|
sessionId,
|
||||||
addresses,
|
addresses,
|
||||||
addressOptions,
|
addressOptions,
|
||||||
loading: addressListQuery.isPending,
|
loading: addressListQuery.isPending || loadAddressesLoading,
|
||||||
addressForm,
|
addressForm,
|
||||||
isAddressFormValid,
|
isAddressFormValid,
|
||||||
submitLoading: saveAddressMutation.isPending,
|
submitLoading: saveAddressMutation.isPending,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {type PropsWithChildren, useEffect, useRef, useState} from 'react'
|
|||||||
import {login, validateToken} from '@/api/auth.ts'
|
import {login, validateToken} from '@/api/auth.ts'
|
||||||
import {userAssets} from '@/api/user.ts'
|
import {userAssets} from '@/api/user.ts'
|
||||||
import {useTranslation} from 'react-i18next'
|
import {useTranslation} from 'react-i18next'
|
||||||
|
import {normalizeLanguage} from '@/lib/i18n'
|
||||||
import {queryKeys} from '@/lib/queryKeys.ts'
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
import {useUserStore} from '@/store/user.ts'
|
import {useUserStore} from '@/store/user.ts'
|
||||||
import type {HostContextMessage} from '@/types'
|
import type {HostContextMessage} from '@/types'
|
||||||
@@ -70,7 +71,7 @@ export function AuthGuide({children}: PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof language === 'string' && language.trim()) {
|
if (typeof language === 'string' && language.trim()) {
|
||||||
setLanguage(language.trim())
|
setLanguage(normalizeLanguage(language))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {useTranslation} from 'react-i18next'
|
|||||||
import {ChevronRight} from 'lucide-react'
|
import {ChevronRight} from 'lucide-react'
|
||||||
|
|
||||||
import Button from '@/components/button'
|
import Button from '@/components/button'
|
||||||
import {HOME_CATEGORY_META_MAP} from '@/constant'
|
import {HOME_CATEGORY_META_MAP, HOME_GOOD_TYPE_ORDER} from '@/constant'
|
||||||
import type {ProductCategory, ProductItem} from '@/types'
|
import type {ProductCategory, ProductItem} from '@/types'
|
||||||
|
|
||||||
type GoodsCategoryListProps = {
|
type GoodsCategoryListProps = {
|
||||||
@@ -56,12 +56,52 @@ export function GoodsCategoryList({
|
|||||||
}: GoodsCategoryListProps) {
|
}: GoodsCategoryListProps) {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
const resolvedEmptyText = emptyText ?? t('goods.noGoodsAvailableYet')
|
const resolvedEmptyText = emptyText ?? t('goods.noGoodsAvailableYet')
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="pb-[24px] mt-[20px]">
|
<div className="pb-[24px]">
|
||||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
{HOME_GOOD_TYPE_ORDER.map((categoryId) => {
|
||||||
{t('goods.loading')}
|
const CategoryIcon = HOME_CATEGORY_META_MAP[categoryId].icon
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<div key={categoryId} className="mt-[20px]">
|
||||||
|
<div className="mb-[10px] flex items-center justify-between gap-[12px]">
|
||||||
|
<div className="flex min-w-0 items-center gap-[10px]">
|
||||||
|
<div
|
||||||
|
className="flex h-[36px] w-[36px] items-center justify-center rounded-[12px] bg-[#FA6A00]/15 text-[#FE9F00]">
|
||||||
|
<CategoryIcon className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
|
<div className="truncate text-[15px] font-semibold text-white">
|
||||||
|
{t(HOME_CATEGORY_META_MAP[categoryId].nameKey)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showMore ? (
|
||||||
|
<div className="text-[12px] font-light text-[#FA6A00] underline">
|
||||||
|
{t('common.more')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{Array.from({length: 4}).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`${categoryId}-${index}`}
|
||||||
|
className="liquid-glass-bg flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="h-[116px] w-full bg-white/6 sm:h-[128px]"></div>
|
||||||
|
<div className="flex flex-1 flex-col items-start justify-between gap-[12px] p-[12px] sm:p-[14px]">
|
||||||
|
<div className="w-full space-y-[8px]">
|
||||||
|
<div className="h-[18px] w-[72%] rounded-full bg-white/10"></div>
|
||||||
|
<div className="h-[14px] w-[48%] rounded-full bg-white/8"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-[18px] w-[92px] rounded-full bg-[#FA6A00]/18"></div>
|
||||||
|
<div className="h-[36px] w-full rounded-[10px] bg-[#FA6A00]/20"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ky, {HTTPError, type AfterResponseHook, type Input, type KyInstance, type
|
|||||||
|
|
||||||
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
||||||
import i18n from '@/lib/i18n'
|
import i18n from '@/lib/i18n'
|
||||||
|
import {normalizeLanguage} from '@/lib/i18n'
|
||||||
import {useUserStore} from '@/store/user.ts'
|
import {useUserStore} from '@/store/user.ts'
|
||||||
import type {ValidateTokenData} from '@/types/auth.type.ts'
|
import type {ValidateTokenData} from '@/types/auth.type.ts'
|
||||||
import {objectToFormData} from './tool'
|
import {objectToFormData} from './tool'
|
||||||
@@ -67,7 +68,7 @@ export const setAccessTokenFormatter = (formatter?: TokenFormatter) => {
|
|||||||
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRequestLanguage = () => useUserStore.getState().language?.trim() || 'zh'
|
const getRequestLanguage = () => normalizeLanguage(useUserStore.getState().language)
|
||||||
|
|
||||||
const authRefreshClient = ky.create({
|
const authRefreshClient = ky.create({
|
||||||
baseUrl: API_BASE_URL,
|
baseUrl: API_BASE_URL,
|
||||||
|
|||||||
@@ -103,20 +103,6 @@ function getOrderStatus(status?: string | number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getOrderCategory(item: OrderItem) {
|
function getOrderCategory(item: OrderItem) {
|
||||||
if (item.type?.trim()) {
|
|
||||||
const normalizedType = item.type.trim().toUpperCase()
|
|
||||||
switch (normalizedType) {
|
|
||||||
case 'BONUS':
|
|
||||||
return i18n.t('record.categories.bonus')
|
|
||||||
case 'PHYSICAL':
|
|
||||||
return i18n.t('record.categories.physical')
|
|
||||||
case 'WITHDRAW':
|
|
||||||
return i18n.t('record.categories.withdraw')
|
|
||||||
default:
|
|
||||||
return toTitleCase(normalizedType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type_title) {
|
if (item.type_title) {
|
||||||
return item.type_title
|
return item.type_title
|
||||||
}
|
}
|
||||||
@@ -126,15 +112,22 @@ function getOrderCategory(item: OrderItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.type) {
|
if (item.type) {
|
||||||
switch (item.type) {
|
const normalizedType = item.type.trim().toUpperCase()
|
||||||
|
switch (normalizedType) {
|
||||||
case 'BONUS':
|
case 'BONUS':
|
||||||
return i18n.t('record.categories.bonus')
|
return i18n.t('record.categories.bonus')
|
||||||
case 'PHYSICAL':
|
case 'PHYSICAL':
|
||||||
return i18n.t('record.categories.physical')
|
return i18n.t('record.categories.physical')
|
||||||
case 'WITHDRAW':
|
case 'WITHDRAW':
|
||||||
return i18n.t('record.categories.withdraw')
|
return i18n.t('record.categories.withdraw')
|
||||||
|
case '1':
|
||||||
|
return i18n.t('record.categories.withdraw')
|
||||||
|
case '2':
|
||||||
|
return i18n.t('record.categories.physical')
|
||||||
|
case '3':
|
||||||
|
return i18n.t('record.categories.bonus')
|
||||||
default:
|
default:
|
||||||
return toTitleCase(item.type)
|
return toTitleCase(normalizedType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user