返回 AiToEarn
GeneralTab.tsx
根目录 / project / aitoearn-web / src / app / layout / SettingsModal / tabs / GeneralTab.tsx
1 /**
2 * GeneralTab - 通用设置 Tab
3 */
4
5 'use client'
6
7 import { setCookie } from 'cookies-next'
8 import { Loader2 } from 'lucide-react'
9 import { useTheme } from 'next-themes'
10 import Image from 'next/image'
11 import { useRouter } from 'next/navigation'
12 import { useTransClient } from '@/app/i18n/client'
13 import { getAllLanguageOptions } from '@/app/i18n/languageConfig'
14 import { cookieName } from '@/app/i18n/settings'
15 import {
16 Select,
17 SelectContent,
18 SelectItem,
19 SelectTrigger,
20 SelectValue,
21 } from '@/components/ui/select'
22 import { useGetClientLng } from '@/hooks/useSystem'
23 import { cn } from '@/utils/className'
24 import { useState } from 'react'
25
26 import darkColorImg from '../images/darkColor.png'
27 import followSystemImg from '../images/followSystem.png'
28 // 主题图片
29 import lightColorImg from '../images/lightColor.png'
30
31 // 使用统一的语言配置
32 const languageOptions = getAllLanguageOptions()
33
34 /** 主题选项配置 */
35 const themeOptions: { value: string, labelKey: string, image: typeof lightColorImg }[] = [
36 { value: 'light', labelKey: 'general.themeLight', image: lightColorImg },
37 { value: 'dark', labelKey: 'general.themeDark', image: darkColorImg },
38 { value: 'system', labelKey: 'general.themeSystem', image: followSystemImg },
39 ]
40
41 export function GeneralTab() {
42 const { t } = useTransClient('settings')
43 const router = useRouter()
44 const lng = useGetClientLng()
45 const [isChangingLanguage, setIsChangingLanguage] = useState(false)
46
47 // 使用 next-themes 管理主题
48 const { theme, setTheme } = useTheme()
49
50 const handleLanguageChange = async (newLng: string) => {
51 if (isChangingLanguage || newLng === lng)
52 return
53
54 setIsChangingLanguage(true)
55
56 try {
57 // 关键修复:在路由跳转前同步设置 cookie,避免竞态条件
58 setCookie(cookieName, newLng, { path: '/' })
59
60 const currentPath = location.pathname
61 const pathWithoutLang = currentPath.replace(`/${lng}`, '') || '/'
62 const newPath = `/${newLng}${pathWithoutLang}`
63
64 await router.push(newPath)
65 router.refresh()
66 }
67 catch (error) {
68 console.error('Language change failed:', error)
69 setIsChangingLanguage(false)
70 }
71 }
72
73 return (
74 <div className="w-full space-y-8">
75 {/* 外观主题 */}
76 <div>
77 <h4 className="text-sm font-medium text-foreground mb-1">{t('general.theme')}</h4>
78 <p className="text-sm text-muted-foreground mb-4">{t('general.themeDesc')}</p>
79 <div className="flex flex-wrap gap-3 md:gap-4">
80 {themeOptions.map((option) => {
81 const isActive = theme === option.value
82 return (
83 <button
84 key={option.value}
85 onClick={() => setTheme(option.value)}
86 className="flex flex-col items-center gap-2 group"
87 >
88 {/* 图片容器 */}
89 <div
90 className={cn(
91 'relative w-16 h-11 md:w-20 md:h-14 rounded-lg overflow-hidden border-2 transition-all',
92 isActive
93 ? 'border-primary shadow-sm'
94 : 'border-border hover:border-muted-foreground',
95 )}
96 >
97 <Image
98 src={option.image}
99 alt={t(option.labelKey)}
100 fill
101 className="object-cover"
102 />
103 </div>
104 {/* 标签 */}
105 <span
106 className={cn(
107 'text-xs transition-colors',
108 isActive ? 'text-primary font-medium' : 'text-muted-foreground',
109 )}
110 >
111 {t(option.labelKey)}
112 </span>
113 </button>
114 )
115 })}
116 </div>
117 </div>
118
119 {/* 网站语言 */}
120 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
121 <div className="min-w-0 flex-1">
122 <h4 className="text-sm font-medium text-foreground">{t('general.language')}</h4>
123 <p className="mt-0.5 text-sm text-muted-foreground">{t('general.languageDesc')}</p>
124 </div>
125 <Select value={lng} onValueChange={handleLanguageChange} disabled={isChangingLanguage}>
126 <SelectTrigger className="h-9 w-full sm:w-[140px]">
127 {isChangingLanguage ? (
128 <div className="flex items-center gap-2">
129 <Loader2 className="h-4 w-4 animate-spin" />
130 <span className="text-muted-foreground">{t('general.changingLanguage')}</span>
131 </div>
132 ) : (
133 <SelectValue />
134 )}
135 </SelectTrigger>
136 <SelectContent>
137 {languageOptions.map(option => (
138 <SelectItem key={option.value} value={option.value}>
139 {option.label}
140 </SelectItem>
141 ))}
142 </SelectContent>
143 </Select>
144 </div>
145
146 </div>
147 )
148 }
149
149 lines Plain Text