diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 0a71883..600d753 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -6,14 +6,14 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import zhCN from './locales/zh-CN.json'; import en from './locales/en.json'; -import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; +import { getInitialLanguage } from '@/utils/language'; i18n.use(initReactI18next).init({ resources: { 'zh-CN': { translation: zhCN }, en: { translation: en } }, - lng: localStorage.getItem(STORAGE_KEY_LANGUAGE) || 'zh-CN', + lng: getInitialLanguage(), fallbackLng: 'zh-CN', interpolation: { escapeValue: false // React 已经转义 diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 7b4bca4..140da2c 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { IconEye, IconEyeOff } from '@/components/ui/icons'; -import { useAuthStore, useNotificationStore } from '@/stores'; +import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores'; import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection'; export function LoginPage() { @@ -12,6 +12,8 @@ export function LoginPage() { const navigate = useNavigate(); const location = useLocation(); const { showNotification } = useNotificationStore(); + const language = useLanguageStore((state) => state.language); + const toggleLanguage = useLanguageStore((state) => state.toggleLanguage); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const login = useAuthStore((state) => state.login); const restoreSession = useAuthStore((state) => state.restoreSession); @@ -27,6 +29,7 @@ export function LoginPage() { const [error, setError] = useState(''); const detectedBase = useMemo(() => detectApiBaseFromLocation(), []); + const nextLanguageLabel = language === 'zh-CN' ? t('language.english') : t('language.chinese'); useEffect(() => { const init = async () => { @@ -49,10 +52,6 @@ export function LoginPage() { return ; } - const handleUseCurrent = () => { - setApiBase(detectedBase); - }; - const handleSubmit = async () => { if (!managementKey.trim()) { setError(t('login.error_required')); @@ -79,7 +78,20 @@ export function LoginPage() {
-
{t('title.login')}
+
+
{t('title.login')}
+ +
{t('login.subtitle')}
@@ -136,14 +148,9 @@ export function LoginPage() { } /> -
- - -
+ {error &&
{error}
} diff --git a/src/stores/useLanguageStore.ts b/src/stores/useLanguageStore.ts index 794a6a4..9de3bed 100644 --- a/src/stores/useLanguageStore.ts +++ b/src/stores/useLanguageStore.ts @@ -8,6 +8,7 @@ import { persist } from 'zustand/middleware'; import type { Language } from '@/types'; import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; import i18n from '@/i18n'; +import { getInitialLanguage } from '@/utils/language'; interface LanguageState { language: Language; @@ -18,7 +19,7 @@ interface LanguageState { export const useLanguageStore = create()( persist( (set, get) => ({ - language: 'zh-CN', + language: getInitialLanguage(), setLanguage: (language) => { // 切换 i18next 语言 diff --git a/src/styles/layout.scss b/src/styles/layout.scss index 2c24c04..c9a7710 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss @@ -431,6 +431,7 @@ display: flex; flex-direction: column; gap: $spacing-sm; + text-align: center; .title { font-size: 22px; @@ -443,6 +444,18 @@ } } + .login-title-row { + display: flex; + align-items: center; + justify-content: center; + gap: $spacing-sm; + flex-wrap: wrap; + } + + .login-language-btn { + white-space: nowrap; + } + .connection-box { background: var(--bg-secondary); border: 1px dashed var(--border-color); diff --git a/src/utils/language.ts b/src/utils/language.ts new file mode 100644 index 0000000..e4775cc --- /dev/null +++ b/src/utils/language.ts @@ -0,0 +1,42 @@ +import type { Language } from '@/types'; +import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; + +const parseStoredLanguage = (value: string): Language | null => { + try { + const parsed = JSON.parse(value); + const candidate = parsed?.state?.language ?? parsed?.language ?? parsed; + if (candidate === 'zh-CN' || candidate === 'en') { + return candidate; + } + } catch { + if (value === 'zh-CN' || value === 'en') { + return value; + } + } + return null; +}; + +const getStoredLanguage = (): Language | null => { + if (typeof window === 'undefined') { + return null; + } + try { + const stored = localStorage.getItem(STORAGE_KEY_LANGUAGE); + if (!stored) { + return null; + } + return parseStoredLanguage(stored); + } catch { + return null; + } +}; + +const getBrowserLanguage = (): Language => { + if (typeof navigator === 'undefined') { + return 'zh-CN'; + } + const raw = navigator.languages?.[0] || navigator.language || 'zh-CN'; + return raw.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en'; +}; + +export const getInitialLanguage = (): Language => getStoredLanguage() ?? getBrowserLanguage();