diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index dc263dd..d4802bb 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -35,6 +35,8 @@ import { } from '@/stores'; import { configApi, versionApi } from '@/services/api'; import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; +import { LANGUAGE_LABEL_KEYS, LANGUAGE_ORDER } from '@/utils/constants'; +import type { Language } from '@/types'; const sidebarIcons: Record = { dashboard: , @@ -189,7 +191,15 @@ export function MainLayout() { const theme = useThemeStore((state) => state.theme); const cycleTheme = useThemeStore((state) => state.cycleTheme); - const toggleLanguage = useLanguageStore((state) => state.toggleLanguage); + const language = useLanguageStore((state) => state.language); + const setLanguage = useLanguageStore((state) => state.setLanguage); + + const handleLanguageChange = useCallback( + (event: React.ChangeEvent) => { + setLanguage(event.target.value as Language); + }, + [setLanguage] + ); const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); @@ -566,9 +576,23 @@ export function MainLayout() { > {headerIcons.update} - +
+ + +
+ {LANGUAGE_ORDER.map((lang) => ( + + ))} +
{t('login.subtitle')}
diff --git a/src/stores/useLanguageStore.ts b/src/stores/useLanguageStore.ts index 9de3bed..e80183e 100644 --- a/src/stores/useLanguageStore.ts +++ b/src/stores/useLanguageStore.ts @@ -6,7 +6,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { Language } from '@/types'; -import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; +import { LANGUAGE_ORDER, STORAGE_KEY_LANGUAGE } from '@/utils/constants'; import i18n from '@/i18n'; import { getInitialLanguage } from '@/utils/language'; @@ -29,8 +29,9 @@ export const useLanguageStore = create()( toggleLanguage: () => { const { language, setLanguage } = get(); - const newLanguage: Language = language === 'zh-CN' ? 'en' : 'zh-CN'; - setLanguage(newLanguage); + const currentIndex = LANGUAGE_ORDER.indexOf(language); + const nextLanguage = LANGUAGE_ORDER[(currentIndex + 1) % LANGUAGE_ORDER.length]; + setLanguage(nextLanguage); } }), { diff --git a/src/styles/layout.scss b/src/styles/layout.scss index 0ba8d9d..43feaf5 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss @@ -190,6 +190,41 @@ gap: $spacing-xs; flex-shrink: 0; + .language-select-wrapper { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--text-secondary); + + &:hover { + color: var(--text-primary); + } + } + + .language-select-icon { + display: inline-flex; + align-items: center; + justify-content: center; + } + + .language-select { + border: 1px solid var(--border-color); + border-radius: $radius-md; + padding: 10px 12px; + font-size: 14px; + background: var(--bg-primary); + color: var(--text-primary); + cursor: pointer; + height: 40px; + box-sizing: border-box; + + &:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); + } + } + svg { display: block; } diff --git a/src/types/common.ts b/src/types/common.ts index c41d23b..f78aa62 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -4,7 +4,7 @@ export type Theme = 'light' | 'dark' | 'auto'; -export type Language = 'zh-CN' | 'en'; +export type Language = 'zh-CN' | 'en' | 'ru'; export type NotificationType = 'info' | 'success' | 'warning' | 'error'; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8266a8d..a15163d 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,6 +3,8 @@ * 从原项目 src/utils/constants.js 迁移 */ +import type { Language } from '@/types'; + // 缓存过期时间(毫秒) export const CACHE_EXPIRY_MS = 30 * 1000; // 与基线保持一致,减少管理端压力 @@ -33,6 +35,15 @@ export const STORAGE_KEY_LANGUAGE = 'cli-proxy-language'; export const STORAGE_KEY_SIDEBAR = 'cli-proxy-sidebar-collapsed'; export const STORAGE_KEY_AUTH_FILES_PAGE_SIZE = 'cli-proxy-auth-files-page-size'; +// 语言配置 +export const LANGUAGE_ORDER: Language[] = ['zh-CN', 'en', 'ru']; +export const LANGUAGE_LABEL_KEYS: Record = { + 'zh-CN': 'language.chinese', + en: 'language.english', + ru: 'language.russian' +}; +export const SUPPORTED_LANGUAGES: Language[] = LANGUAGE_ORDER; + // 通知持续时间 export const NOTIFICATION_DURATION_MS = 3000; diff --git a/src/utils/language.ts b/src/utils/language.ts index e4775cc..8aa1767 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -1,16 +1,16 @@ import type { Language } from '@/types'; -import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; +import { STORAGE_KEY_LANGUAGE, SUPPORTED_LANGUAGES } 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; + if (SUPPORTED_LANGUAGES.includes(candidate as Language)) { + return candidate as Language; } } catch { - if (value === 'zh-CN' || value === 'en') { - return value; + if (SUPPORTED_LANGUAGES.includes(value as Language)) { + return value as Language; } } return null; @@ -36,7 +36,10 @@ const getBrowserLanguage = (): Language => { return 'zh-CN'; } const raw = navigator.languages?.[0] || navigator.language || 'zh-CN'; - return raw.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en'; + const lower = raw.toLowerCase(); + if (lower.startsWith('zh')) return 'zh-CN'; + if (lower.startsWith('ru')) return 'ru'; + return 'en'; }; export const getInitialLanguage = (): Language => getStoredLanguage() ?? getBrowserLanguage();