refactor(game): 优化游戏组件并实现国际化支持

- 在AppBootResourceGate组件中集成react-i18next实现资源加载文本的国际化
- 修改DesktopAnimal组件中的loading dots key以提高渲染性能
- 在DesktopControl组件中添加useRef和useEffect钩子管理定时器清理逻辑
- 将DesktopTitle组件重构为MessageBroadcast组件并增强其响应式设计
- 更新DesktopSupportModal组件中的客户服务文本为国际化格式
- 在AuthSession模块中实现本地存储数据清理时保留关键偏好设置
- 调整多个游戏组件的样式类以改进移动端适配效果
- 移除未使用的桌面提取功能相关代码文件
- 更新GitNexus索引统计数据反映最新的代码变更
This commit is contained in:
JiaJun
2026-06-04 18:01:40 +08:00
parent bfb4b76611
commit a6b34660ad
48 changed files with 3185 additions and 1216 deletions

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CenterModal } from '@/components/center-modal.tsx'
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
import { useModalStore } from '@/store'
@@ -8,6 +9,7 @@ const SUPPORT_CHAT_URL =
const IFRAME_READY_DELAY_MS = 2_000
function DesktopSupportModal() {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(true)
const readyTimerRef = useRef<number | null>(null)
const open = useModalStore((state) => state.modals.desktopSupport)
@@ -49,14 +51,18 @@ function DesktopSupportModal() {
isNormalBg={true}
onClose={handleClose}
titleAlign="left"
title={<div className="modal-title-glow text-design-30">线</div>}
title={
<div className="modal-title-glow text-design-30">
{t('commonUi.support.title')}
</div>
}
className="h-design-760 w-design-980"
>
<div className="h-full px-design-24 pb-design-40 pt-design-10">
<div className="relative h-full overflow-hidden rounded-[calc(var(--design-unit)*14)] border border-[#2A6D73] bg-[linear-gradient(180deg,rgba(5,22,31,0.98),rgba(2,10,17,0.98))] shadow-[inset_0_0_calc(var(--design-unit)*22)_rgba(88,205,218,0.13),0_0_calc(var(--design-unit)*18)_rgba(31,156,174,0.14)]">
{isLoading ? (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-[radial-gradient(circle_at_center,rgba(20,92,105,0.38),rgba(2,10,17,0.98)_58%)]">
<DataLoadingIndicator label="客服连线中" />
<DataLoadingIndicator label={t('commonUi.support.connecting')} />
</div>
) : null}
<iframe

View File

@@ -112,32 +112,32 @@ function MobileAutoSettingModal() {
open={open}
onClose={handleClose}
title={
<div className={'modal-title-glow text-design-26 uppercase'}>
<div className={'modal-title-glow text-design-16'}>
{t('game.modals.autoSetting.title')}
</div>
}
isNormalBg={true}
titleAlign="left"
className="!h-[min(calc(var(--design-unit)*500),calc(100dvh-var(--design-unit)*28))]"
className="h-design-370"
>
<div
className={
'flex h-full w-full flex-col justify-between px-design-18 pt-design-30 pb-design-60'
'flex h-full w-full flex-col justify-between px-design-10 pt-design-10 pb-design-14'
}
>
<div className={'flex w-full flex-col gap-design-26'}>
<div className={'flex items-center justify-between gap-design-30'}>
<div
className={
'w-design-300 shrink-0 text-design-22 leading-[1.1] text-[#9CF7FF]'
}
>
<div className={'flex w-full flex-col gap-design-8'}>
<div
className={
'rounded-[calc(var(--design-unit)*8)] border border-[rgba(104,214,222,0.3)] bg-[rgba(4,30,38,0.7)] px-design-8 py-design-8 shadow-[inset_0_0_calc(var(--design-unit)*8)_rgba(33,193,219,0.08)]'
}
>
<div className={'text-design-12 leading-[1.35] text-[#9CF7FF]'}>
{t('game.modals.autoSetting.rows.stopIfBalanceLowerThan')}
</div>
<div
className={
'game-setting-input-shell flex h-design-58 w-design-410 items-center justify-between pl-design-18 pr-design-10'
'mt-design-6 flex items-center gap-design-6 rounded-[calc(var(--design-unit)*8)] border border-[rgba(111,235,245,0.16)] bg-[rgba(10,57,66,0.7)] px-design-8 py-design-6'
}
>
<Input
@@ -145,29 +145,30 @@ function MobileAutoSettingModal() {
inputMode="decimal"
onChange={(event) => setBalanceLimitValue(event.target.value)}
className={
'game-setting-input h-full w-design-280 text-design-18'
'game-setting-input h-design-32 flex-1 bg-transparent px-design-6 py-design-6 text-design-12'
}
/>
<Switch
size={'sm'}
className="scale-[0.82]"
checked={balanceLimitEnabled}
onCheckedChange={setBalanceLimitEnabled}
/>
</div>
</div>
<div className={'flex items-center justify-between gap-design-30'}>
<div
className={
'w-design-300 shrink-0 text-design-22 leading-[1.1] text-[#9CF7FF]'
}
>
<div
className={
'rounded-[calc(var(--design-unit)*8)] border border-[rgba(104,214,222,0.3)] bg-[rgba(4,30,38,0.7)] px-design-8 py-design-8 shadow-[inset_0_0_calc(var(--design-unit)*8)_rgba(33,193,219,0.08)]'
}
>
<div className={'text-design-12 leading-[1.35] text-[#9CF7FF]'}>
{t('game.modals.autoSetting.rows.stopIfSingleWinExceeds')}
</div>
<div
className={
'game-setting-input-shell flex h-design-58 w-design-410 items-center justify-between pl-design-18 pr-design-10'
'mt-design-6 flex items-center gap-design-6 rounded-[calc(var(--design-unit)*8)] border border-[rgba(111,235,245,0.16)] bg-[rgba(10,57,66,0.7)] px-design-8 py-design-6'
}
>
<Input
@@ -175,29 +176,30 @@ function MobileAutoSettingModal() {
inputMode="decimal"
onChange={(event) => setSingleWinLimitValue(event.target.value)}
className={
'game-setting-input h-full w-design-280 text-design-18'
'game-setting-input h-design-32 flex-1 bg-transparent px-design-6 py-design-6 text-design-12'
}
/>
<Switch
size={'sm'}
className="scale-[0.82]"
checked={singleWinLimitEnabled}
onCheckedChange={setSingleWinLimitEnabled}
/>
</div>
</div>
<div className={'flex items-center justify-between gap-design-30'}>
<div
className={
'w-design-300 shrink-0 text-design-22 leading-[1.1] text-[#9CF7FF]'
}
>
{t('game.modals.autoSetting.rows.stopOnAnyJackpot')}
</div>
<div className={'flex w-design-410 justify-end pr-design-2'}>
<div
className={
'rounded-[calc(var(--design-unit)*8)] border border-[rgba(104,214,222,0.3)] bg-[rgba(4,30,38,0.7)] px-design-8 py-design-8 shadow-[inset_0_0_calc(var(--design-unit)*8)_rgba(33,193,219,0.08)]'
}
>
<div className={'flex items-center justify-between gap-design-8'}>
<div className={'text-design-12 leading-[1.35] text-[#9CF7FF]'}>
{t('game.modals.autoSetting.rows.stopOnAnyJackpot')}
</div>
<Switch
size={'sm'}
className="scale-[0.82]"
checked={jackpotStopEnabled}
onCheckedChange={setJackpotStopEnabled}
/>
@@ -205,7 +207,7 @@ function MobileAutoSettingModal() {
</div>
</div>
<div className={'flex w-full justify-center'}>
<div className={'flex w-full justify-center pt-design-10'}>
<SmartBackground
as="button"
src={lengthBlueBtn}
@@ -215,7 +217,7 @@ function MobileAutoSettingModal() {
type="button"
onClick={handleSubmit}
className={
'w-design-300 h-design-72 pb-design-4 flex cursor-pointer items-center justify-center text-design-24 font-bold tracking-wide text-[#E7FBFF] transition-transform hover:-translate-y-[1px] active:translate-y-0'
'flex h-design-45 w-full max-w-[calc(var(--design-unit)*160)] cursor-pointer items-center justify-center pb-design-2 !text-design-12 font-bold mt-design-5 tracking-wide text-[#E7FBFF] transition-transform hover:-translate-y-[1px] active:translate-y-0'
}
>
{t('game.modals.autoSetting.startAutoSpin')}

View File

@@ -33,7 +33,7 @@ function MobileLanguageModal() {
}
isNormalBg={true}
titleAlign="left"
className="!h-design-350"
className="!h-design-330"
>
<div className="flex h-full min-h-0 flex-col px-design-14 pt-design-5">
<div className="w-full flex flex-wrap gap-design-10 overflow-y-auto">

View File

@@ -2,28 +2,34 @@ import { X } from 'lucide-react'
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PeriodHistoryList } from '@/features/game/components/shared/period-history-list'
import { MobileGameHistory } from '@/features/game/components/mobile/mobile-game-history.tsx'
import { MobilePeriodHistoryList } from '@/features/game/components/mobile/mobile-period-history-list'
import {
DEFAULT_PERIOD_HISTORY_LIMIT,
type PeriodHistoryDisplayItem,
usePeriodHistoryVm,
} from '@/hooks/use-period-history-vm'
import { cn } from '@/lib/utils'
import { useModalStore } from '@/store'
const OVERLAY_EASE = [0.16, 1, 0.3, 1] as const
const DRAWER_TRANSITION = {
type: 'tween',
duration: 0.34,
duration: 0.3,
ease: OVERLAY_EASE,
} as const
type HistoryTab = 'period' | 'record'
interface PeriodHistoryDrawerLabels {
close: string
empty: string
failed: string
loading: string
recordTab: string
retry: string
title: string
periodTab: string
}
interface MobilePeriodHistoryDrawerViewProps {
@@ -59,6 +65,8 @@ export function MobilePeriodHistoryDrawer() {
empty: t('gameDesktop.periodHistory.empty'),
failed: t('gameDesktop.periodHistory.failed'),
loading: t('gameDesktop.periodHistory.loading'),
periodTab: t('gameDesktop.periodHistory.title'),
recordTab: t('gameDesktop.history.title'),
retry: t('gameDesktop.periodHistory.retry'),
title: t('gameDesktop.periodHistory.title'),
}}
@@ -78,104 +86,130 @@ export function MobilePeriodHistoryDrawerView({
open,
}: MobilePeriodHistoryDrawerViewProps) {
const prefersReducedMotion = useReducedMotion()
const [isDrawerAnimating, setIsDrawerAnimating] = useState(false)
const [activeTab, setActiveTab] = useState<HistoryTab>('period')
return (
<AnimatePresence>
{open && (
{open ? (
<>
<motion.button
type="button"
aria-label={labels.close}
className="fixed left-0 right-0 top-0 bottom-[calc(var(--design-unit)*150)] z-30 cursor-default bg-black/48"
className="fixed inset-0 z-30 cursor-default bg-black/58"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: prefersReducedMotion ? 0.12 : 0.26,
duration: prefersReducedMotion ? 0.12 : 0.22,
ease: OVERLAY_EASE,
}}
onClick={onClose}
/>
<motion.aside
role="dialog"
aria-modal="true"
aria-label={labels.title}
className="fixed left-0 top-design-16 bottom-[calc(var(--design-unit)*150)] z-40 flex w-design-1120 max-w-[calc(100vw-var(--design-unit)*24)] origin-left flex-col overflow-hidden rounded-r-[calc(var(--design-unit)*10)] border border-[rgba(81,230,255,0.62)] bg-[linear-gradient(180deg,rgba(6,19,32,0.98),rgba(3,12,22,0.96))] text-[#D5FBFF] shadow-[0_0_calc(var(--design-unit)*18)_rgba(39,216,255,0.28),0_0_calc(var(--design-unit)*54)_rgba(39,216,255,0.16),inset_0_0_calc(var(--design-unit)*18)_rgba(74,224,255,0.16)]"
className="fixed inset-x-0 bottom-0 z-40 flex h-[min(calc(var(--design-unit)*620),calc(100dvh-var(--design-unit)*12))] flex-col overflow-hidden rounded-t-[calc(var(--design-unit)*10)] border border-[rgba(81,230,255,0.62)] bg-[linear-gradient(180deg,rgba(6,19,32,0.98),rgba(3,12,22,0.96))] text-[#D5FBFF] shadow-[0_0_calc(var(--design-unit)*16)_rgba(39,216,255,0.22),0_0_calc(var(--design-unit)*40)_rgba(39,216,255,0.1),inset_0_0_calc(var(--design-unit)*14)_rgba(74,224,255,0.12)]"
initial={
prefersReducedMotion
? { opacity: 0 }
: { x: '-100%', opacity: 0.98 }
: { y: '100%', opacity: 0.98 }
}
animate={
prefersReducedMotion ? { opacity: 1 } : { x: 0, opacity: 1 }
prefersReducedMotion ? { opacity: 1 } : { y: 0, opacity: 1 }
}
exit={
prefersReducedMotion
? { opacity: 0 }
: { x: '-100%', opacity: 0.98 }
: { y: '100%', opacity: 0.98 }
}
transition={
prefersReducedMotion ? { duration: 0.12 } : DRAWER_TRANSITION
}
onAnimationStart={() => setIsDrawerAnimating(true)}
onAnimationComplete={() => setIsDrawerAnimating(false)}
style={
isDrawerAnimating
? { willChange: 'transform, opacity' }
: undefined
}
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-x-design-8 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(80,241,255,0.96),transparent)]"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute bottom-0 left-0 h-design-28 w-design-28 border-b-2 border-l-2 border-[#28E6FF]"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute bottom-0 right-0 h-design-28 w-design-28 border-b-2 border-r-2 border-[#28E6FF]"
/>
<div className="relative flex h-design-78 shrink-0 items-center justify-between border-b border-[rgba(80,224,255,0.38)] px-design-42">
<h2 className="text-design-28 font-bold leading-none text-white [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(156,244,255,0.42)]">
{labels.title}
</h2>
<div className="relative flex h-design-40 shrink-0 items-center justify-between gap-design-4 border-b border-[rgba(80,224,255,0.3)] px-design-8">
<div className="relative grid h-design-26 w-[calc(var(--design-unit)*170)] max-w-[calc(100%-var(--design-unit)*30)] shrink-0 grid-cols-2 overflow-hidden rounded-[calc(var(--design-unit)*7)] border border-[#2B8CA3]/24 bg-[#031B24]/68 p-[calc(var(--design-unit)*1.5)]">
{(
[
['period', labels.periodTab],
['record', labels.recordTab],
] as const
).map(([tab, label]) => {
const isActive = activeTab === tab
return (
<button
key={tab}
type="button"
onClick={() => setActiveTab(tab)}
className={cn(
'relative flex min-w-0 cursor-pointer items-center justify-center overflow-hidden rounded-[calc(var(--design-unit)*5)] px-design-3 text-[calc(var(--design-unit)*8.5)] font-semibold transition-colors duration-200',
isActive
? 'text-[#FEEEB0]'
: 'text-[#58ADAF] hover:text-[#BFEAEC]',
)}
>
{isActive ? (
<motion.span
layoutId="mobile-period-history-tab-active-bg"
aria-hidden="true"
className="absolute inset-0 rounded-[calc(var(--design-unit)*5)] bg-[linear-gradient(180deg,rgba(254,238,176,0.28)_0%,rgba(254,238,176,0.1)_100%)]"
transition={{
type: 'spring',
stiffness: 430,
damping: 36,
}}
/>
) : null}
{isActive ? (
<motion.span
layoutId="mobile-period-history-tab-active-indicator"
aria-hidden="true"
className="absolute inset-x-design-5 bottom-0 h-[calc(var(--design-unit)*1.5)] rounded-full bg-[linear-gradient(90deg,rgba(255,248,214,0.14),rgba(254,238,176,0.9),rgba(255,248,214,0.14))] shadow-[0_0_calc(var(--design-unit)*5)_rgba(254,238,176,0.24)]"
transition={{
type: 'spring',
stiffness: 430,
damping: 36,
}}
/>
) : null}
<span className="min-w-0 truncate">{label}</span>
</button>
)
})}
</div>
<button
type="button"
aria-label={labels.close}
className="flex h-design-42 w-design-42 cursor-pointer items-center justify-center text-[#C8F7FF] transition-colors duration-200 hover:text-white focus-visible:ring-2 focus-visible:ring-[#4FEAFF]"
className="flex h-design-22 w-design-22 cursor-pointer items-center justify-center text-[#C8F7FF] transition-colors duration-200 hover:text-white"
onClick={onClose}
>
<X size={32} strokeWidth={2.1} />
<X size={14} strokeWidth={2.1} />
</button>
</div>
<motion.div
className="history-scroll-hidden min-h-0 flex-1 overflow-y-auto px-design-34 py-design-26"
initial={
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 8 }
}
animate={
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
transition={
prefersReducedMotion
? { duration: 0.12 }
: { duration: 0.22, delay: 0.08, ease: OVERLAY_EASE }
}
>
<PeriodHistoryList
items={items}
isLoading={isLoading}
isError={isError}
labels={labels}
onRetry={onRetry}
/>
</motion.div>
<div className="min-h-0 flex-1 px-design-8 py-design-8">
{activeTab === 'period' ? (
<MobilePeriodHistoryList
items={items}
isLoading={isLoading}
isError={isError}
labels={labels}
onRetry={onRetry}
/>
) : (
<MobileGameHistory />
)}
</div>
</motion.aside>
</>
)}
) : null}
</AnimatePresence>
)
}

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MobileCenterModal } from '@/components/mobile-center-modal.tsx'
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
import { useModalStore } from '@/store'
@@ -8,6 +9,7 @@ const SUPPORT_CHAT_URL =
const IFRAME_READY_DELAY_MS = 2_000
function MobileSupportModal() {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(true)
const readyTimerRef = useRef<number | null>(null)
const open = useModalStore((state) => state.modals.desktopSupport)
@@ -49,7 +51,11 @@ function MobileSupportModal() {
isNormalBg={true}
onClose={handleClose}
titleAlign="left"
title={<div className="modal-title-glow text-design-16">线</div>}
title={
<div className="modal-title-glow text-design-16">
{t('commonUi.support.title')}
</div>
}
className="h-design-500"
>
<div className="h-full min-h-0 px-design-8 pb-design-10 pt-design-4">
@@ -57,7 +63,7 @@ function MobileSupportModal() {
{isLoading ? (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-[radial-gradient(circle_at_center,rgba(20,92,105,0.38),rgba(2,10,17,0.98)_58%)]">
<DataLoadingIndicator
label="客服连线中"
label={t('commonUi.support.connecting')}
className="text-design-12"
/>
</div>