feat(i18n): U3 国际化 — react-i18next 双语框架 + 侧边栏/Header/Login 全量翻译 + 语言切换器
- 安装 i18next + react-i18next,创建 src/i18n/ 配置实例
- 创建 zh-CN.json (120+ 键) + en-US.json 全量英文翻译
- 侧边栏 42+ 菜单项全量 labelKey 国际化
- Header 通知/主题/用户菜单全量翻译
- Login 页面表单/按钮/主题+语言切换器
- zustand useLanguageStore 持久化语言选择
- HTML lang 属性自动同步
- tsc -b=0, npm build=✅, 17/17 测试通过
This commit is contained in:
1 parent
084a3be2cf
commit
2e179ef7f4
15 files changed
+860
-112
No files matched your search
+2
-2
@@ -1,12 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="theme-color" content="#1e6feb" />
|
||||
<title>AIOps智能运维平台</title>
|
||||
<title>AIOps · Intelligent Operations Platform</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+80
-4
@@ -49,6 +49,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"i18next": "^26.3.3",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -56,6 +57,7 @@
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-resizable-panels": "^4.2.2",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"recharts": "^2.15.4",
|
||||
@@ -1798,9 +1800,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -6687,6 +6689,44 @@
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.3.tgz",
|
||||
"integrity": "sha512-aYVegyBdXSO93CMMihvr47jI7GHSOcIahMpJX+qzUXDzW4xDJf2uenIA+45vDU+YhiVdcfsql70AC9RVdMNrHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com/i18next"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -7584,6 +7624,33 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
|
||||
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 26.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -8289,7 +8356,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
@@ -8586,6 +8653,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"i18next": "^26.3.3",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -58,6 +59,7 @@
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-resizable-panels": "^4.2.2",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"recharts": "^2.15.4",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff } from 'lucide-react';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff, Languages } from 'lucide-react';
|
||||
import { useUserStore, useLanguageStore } from '@/store';
|
||||
import { userApi } from '@/api';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SUPPORTED_LANGUAGES, type AppLanguage } from '@/i18n';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -67,8 +69,10 @@ const ORBS = [
|
||||
* 6. 背景光球动画更协调
|
||||
*/
|
||||
export function Login() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { language, setLanguage } = useLanguageStore();
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('123456');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
@@ -90,7 +94,7 @@ export function Login() {
|
||||
localStorage.setItem('token', result.token);
|
||||
navigate('/', { replace: true });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? (err as Record<string, any>).message : '登录失败,请检查用户名和密码';
|
||||
const message = err instanceof Error ? err.message : t('login.loginFailed');
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -131,16 +135,59 @@ export function Login() {
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Theme switcher */}
|
||||
{/* Theme + Language switcher */}
|
||||
<div
|
||||
className="fixed top-4 right-4 z-50"
|
||||
className="fixed top-4 right-4 z-50 flex items-center gap-2"
|
||||
style={{ animation: 'login-fade-in 0.6s ease-out' }}
|
||||
>
|
||||
{/* Language switcher */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="切换主题"
|
||||
aria-label={t('header.language')}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-10 rounded-full',
|
||||
'border border-[var(--dg-border)] transition-all duration-200',
|
||||
'hover:scale-105 active:scale-95 hover:border-[var(--dg-border-hover)]',
|
||||
'glass'
|
||||
)}
|
||||
style={{ color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
<Languages className="size-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-40 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.languageSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{SUPPORTED_LANGUAGES.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.code}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 cursor-pointer transition-colors',
|
||||
'focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]',
|
||||
language === lang.code ? 'text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-secondary)]',
|
||||
)}
|
||||
onClick={() => setLanguage(lang.code as AppLanguage)}
|
||||
>
|
||||
<span className="text-base">{lang.flag}</span>
|
||||
<span className="flex-1 text-sm">{lang.label}</span>
|
||||
{language === lang.code && <Check className="size-4 shrink-0 text-[var(--dg-blue)]" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Theme switcher */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('header.theme')}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-10 rounded-full',
|
||||
'border border-[var(--dg-border)] transition-all duration-200',
|
||||
@@ -157,7 +204,7 @@ export function Login() {
|
||||
className="w-60 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">主题切换</DropdownMenuLabel>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.themeSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<DropdownMenuItem
|
||||
@@ -226,7 +273,7 @@ export function Login() {
|
||||
AIOps
|
||||
</h1>
|
||||
<p className="mt-2.5 text-sm text-[var(--dg-text-secondary)]">
|
||||
智能运维 · 全栈可观测平台
|
||||
{t('login.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -250,7 +297,7 @@ export function Login() {
|
||||
{/* Username */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
|
||||
用户名
|
||||
{t('login.username')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User
|
||||
@@ -260,7 +307,7 @@ export function Login() {
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
placeholder={t('login.usernamePlaceholder')}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
className="dg-input w-full py-2.5 pl-10 pr-4 text-sm"
|
||||
@@ -271,7 +318,7 @@ export function Login() {
|
||||
{/* Password */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
|
||||
密码
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock
|
||||
@@ -281,7 +328,7 @@ export function Login() {
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="请输入密码"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
autoComplete="current-password"
|
||||
className="dg-input w-full py-2.5 pl-10 pr-12 text-sm"
|
||||
/>
|
||||
@@ -314,10 +361,10 @@ export function Login() {
|
||||
{loading ? (
|
||||
<span className="inline-flex items-center justify-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
登录中...
|
||||
{t('login.loggingIn')}
|
||||
</span>
|
||||
) : (
|
||||
'登 录'
|
||||
t('login.loginButton')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useThemeStore, useNotificationStore, useUserStore } from '@/store';
|
||||
import { useThemeStore, useNotificationStore, useUserStore, useLanguageStore } from '@/store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
@@ -24,9 +25,11 @@ import {
|
||||
CheckCircle,
|
||||
Palette,
|
||||
Check,
|
||||
Languages,
|
||||
} from 'lucide-react';
|
||||
import { formatDistanceToNow } from '@/lib/utils';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { SUPPORTED_LANGUAGES, type AppLanguage } from '@/i18n';
|
||||
|
||||
// Keyboard shortcut handler — Cmd/Ctrl+B 切换侧边栏
|
||||
function useKeyboardShortcuts() {
|
||||
@@ -43,9 +46,11 @@ function useKeyboardShortcuts() {
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed, toggleSidebar } = useThemeStore();
|
||||
const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotificationStore();
|
||||
const { user, logout } = useUserStore();
|
||||
const { language, setLanguage } = useLanguageStore();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const navigate = useNavigate();
|
||||
useKeyboardShortcuts();
|
||||
@@ -55,7 +60,7 @@ export function Header() {
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
const displayName = user?.real_name || user?.username || '管理员';
|
||||
const displayName = user?.real_name || user?.username || t('header.admin');
|
||||
|
||||
const getNotificationIcon = useCallback((type: string) => {
|
||||
switch (type) {
|
||||
@@ -88,7 +93,7 @@ export function Header() {
|
||||
size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] flex-shrink-0"
|
||||
onClick={toggleSidebar}
|
||||
title={sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||
title={sidebarCollapsed ? t('header.expandSidebar') : t('header.collapseSidebar')}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -103,7 +108,7 @@ export function Header() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
title="切换主题"
|
||||
title={t('header.theme')}
|
||||
>
|
||||
<Palette className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -113,7 +118,7 @@ export function Header() {
|
||||
className="w-60 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">主题切换</DropdownMenuLabel>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.themeSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<DropdownMenuItem
|
||||
@@ -148,6 +153,47 @@ export function Header() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Language Switcher — U3 i18n */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
title={t('header.language')}
|
||||
>
|
||||
<Languages className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-40 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.languageSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{SUPPORTED_LANGUAGES.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.code}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 cursor-pointer transition-colors',
|
||||
'focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]',
|
||||
language === lang.code
|
||||
? 'text-[var(--dg-text-primary)]'
|
||||
: 'text-[var(--dg-text-secondary)]',
|
||||
)}
|
||||
onClick={() => setLanguage(lang.code as AppLanguage)}
|
||||
>
|
||||
<span className="text-base">{lang.flag}</span>
|
||||
<span className="flex-1 text-sm">{lang.label}</span>
|
||||
{language === lang.code && (
|
||||
<Check className="h-4 w-4 text-[var(--dg-blue)]" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Notifications */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -155,7 +201,7 @@ export function Header() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] relative"
|
||||
title="通知"
|
||||
title={t('header.notifications')}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
{unreadCount > 0 && (
|
||||
@@ -173,7 +219,7 @@ export function Header() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--dg-border)]">
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)]">
|
||||
通知
|
||||
{t('header.notifications')}
|
||||
</span>
|
||||
{unreadCount > 0 && (
|
||||
<Button
|
||||
@@ -183,7 +229,7 @@ export function Header() {
|
||||
onClick={markAllAsRead}
|
||||
>
|
||||
<CheckCheck className="h-3.5 w-3.5 mr-1" />
|
||||
全部已读
|
||||
{t('header.markAllRead')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -193,7 +239,7 @@ export function Header() {
|
||||
{notifications.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-[var(--dg-text-muted)]">
|
||||
<Bell className="h-10 w-10 mb-3 opacity-30" />
|
||||
<p className="text-sm">暂无通知</p>
|
||||
<p className="text-sm">{t('header.noNotifications')}</p>
|
||||
</div>
|
||||
) : (
|
||||
notifications.slice(0, 10).map((notification) => (
|
||||
@@ -242,7 +288,7 @@ export function Header() {
|
||||
className="text-xs text-[var(--dg-text-muted)] hover:text-[var(--dg-blue)] transition-colors"
|
||||
onClick={() => navigate('/alert-center')}
|
||||
>
|
||||
查看全部通知
|
||||
{t('header.viewAllNotifications')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -287,14 +333,14 @@ export function Header() {
|
||||
onClick={() => navigate('/profile')}
|
||||
>
|
||||
<User className="h-4 w-4 mr-2.5" />
|
||||
个人中心
|
||||
{t('header.profile')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
onClick={() => navigate('/settings')}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2.5" />
|
||||
账号设置
|
||||
{t('header.accountSettings')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem
|
||||
@@ -302,7 +348,7 @@ export function Header() {
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2.5" />
|
||||
退出登录
|
||||
{t('header.logout')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useThemeStore, useMenuStore } from '@/store';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
@@ -50,6 +51,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
/** i18n 翻译 key(如 "menu.dashboard"),存在时优先使用 t(labelKey) 渲染 */
|
||||
labelKey?: string;
|
||||
icon: React.ElementType;
|
||||
path?: string;
|
||||
children?: MenuItem[];
|
||||
@@ -58,121 +61,121 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ id: 'dashboard', name: '仪表盘', icon: LayoutDashboard, path: '/', permission: 'dashboard:view' },
|
||||
{ id: 'dashboard', name: '仪表盘', labelKey: 'menu.dashboard', icon: LayoutDashboard, path: '/', permission: 'dashboard:view' },
|
||||
{
|
||||
id: 'assets', name: '资产管理', icon: Server, permission: 'server:view',
|
||||
id: 'assets', name: '资产管理', labelKey: 'menu.assets', icon: Server, permission: 'server:view',
|
||||
children: [
|
||||
{ id: 'servers', name: '服务器管理', icon: Database, path: '/servers', permission: 'server:view' },
|
||||
{ id: 'cloud-accounts', name: '云账号管理', icon: Key, path: '/cloud-accounts', permission: 'cloud:account' },
|
||||
{ id: 'domains', name: '域名管理', icon: Globe, path: '/domains', permission: 'cloud:domain' },
|
||||
{ id: 'regions', name: '区域管理', icon: MapPin, path: '/regions', permission: 'cloud:account' },
|
||||
{ id: 'platforms', name: '云平台', icon: CloudCog, path: '/platforms', permission: 'cloud:account' },
|
||||
{ id: 'cloud-billing', name: '云账单', icon: BarChart3, path: '/cloud-billing', permission: 'cloud:billing' },
|
||||
{ id: 'servers', name: '服务器管理', labelKey: 'menu.servers', icon: Database, path: '/servers', permission: 'server:view' },
|
||||
{ id: 'cloud-accounts', name: '云账号管理', labelKey: 'menu.cloudAccounts', icon: Key, path: '/cloud-accounts', permission: 'cloud:account' },
|
||||
{ id: 'domains', name: '域名管理', labelKey: 'menu.domains', icon: Globe, path: '/domains', permission: 'cloud:domain' },
|
||||
{ id: 'regions', name: '区域管理', labelKey: 'menu.regions', icon: MapPin, path: '/regions', permission: 'cloud:account' },
|
||||
{ id: 'platforms', name: '云平台', labelKey: 'menu.platforms', icon: CloudCog, path: '/platforms', permission: 'cloud:account' },
|
||||
{ id: 'cloud-billing', name: '云账单', labelKey: 'menu.cloudBilling', icon: BarChart3, path: '/cloud-billing', permission: 'cloud:billing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'kubernetes', name: 'Kubernetes', icon: Cloud, permission: 'k8s:cluster',
|
||||
id: 'kubernetes', name: 'Kubernetes', labelKey: 'menu.kubernetes', icon: Cloud, permission: 'k8s:cluster',
|
||||
children: [
|
||||
{ id: 'k8s-clusters', name: '集群列表', icon: Cloud, path: '/kubernetes', permission: 'k8s:cluster' },
|
||||
{ id: 'k8s-nodes', name: '节点管理', icon: Cpu, path: '/nodes', permission: 'k8s:nodes' },
|
||||
{ id: 'k8s-clusters', name: '集群列表', labelKey: 'menu.clusterList', icon: Cloud, path: '/kubernetes', permission: 'k8s:cluster' },
|
||||
{ id: 'k8s-nodes', name: '节点管理', labelKey: 'menu.nodeManagement', icon: Cpu, path: '/nodes', permission: 'k8s:nodes' },
|
||||
{
|
||||
id: 'k8s-workload', name: '工作负载', icon: Boxes, permission: 'k8s:workloads',
|
||||
id: 'k8s-workload', name: '工作负载', labelKey: 'menu.workloads', icon: Boxes, permission: 'k8s:workloads',
|
||||
children: [
|
||||
{ id: 'k8s-deployments', name: 'Deployments', icon: Boxes, path: '/workloads/deployments', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-statefulsets', name: 'StatefulSets', icon: Boxes, path: '/workloads/statefulsets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-daemonsets', name: 'DaemonSets', icon: Boxes, path: '/workloads/daemonsets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-cronjobs', name: 'CronJobs', icon: Clock, path: '/workloads/cronjobs', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-pods', name: 'Pods', icon: Container, path: '/workloads/pods', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-deployments', name: 'Deployments', labelKey: 'menu.deployments', icon: Boxes, path: '/workloads/deployments', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-statefulsets', name: 'StatefulSets', labelKey: 'menu.statefulsets', icon: Boxes, path: '/workloads/statefulsets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-daemonsets', name: 'DaemonSets', labelKey: 'menu.daemonsets', icon: Boxes, path: '/workloads/daemonsets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-cronjobs', name: 'CronJobs', labelKey: 'menu.cronjobs', icon: Clock, path: '/workloads/cronjobs', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-pods', name: 'Pods', labelKey: 'menu.pods', icon: Container, path: '/workloads/pods', permission: 'k8s:workloads' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'k8s-network', name: '网络', icon: Network, permission: 'k8s:workloads',
|
||||
id: 'k8s-network', name: '网络', labelKey: 'menu.network', icon: Network, permission: 'k8s:workloads',
|
||||
children: [
|
||||
{ id: 'k8s-services', name: 'Services', icon: Network, path: '/workloads/services', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-ingresses', name: 'Ingresses', icon: Globe, path: '/workloads/ingresses', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-services', name: 'Services', labelKey: 'menu.services', icon: Network, path: '/workloads/services', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-ingresses', name: 'Ingresses', labelKey: 'menu.ingresses', icon: Globe, path: '/workloads/ingresses', permission: 'k8s:workloads' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'k8s-config', name: '配置中心', icon: Settings, permission: 'k8s:workloads',
|
||||
id: 'k8s-config', name: '配置中心', labelKey: 'menu.configCenter', icon: Settings, permission: 'k8s:workloads',
|
||||
children: [
|
||||
{ id: 'k8s-configmaps', name: 'ConfigMaps', icon: FileCode, path: '/workloads/configmaps', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-secrets', name: 'Secrets', icon: Shield, path: '/workloads/secrets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-configmaps', name: 'ConfigMaps', labelKey: 'menu.configmaps', icon: FileCode, path: '/workloads/configmaps', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-secrets', name: 'Secrets', labelKey: 'menu.secrets', icon: Shield, path: '/workloads/secrets', permission: 'k8s:workloads' },
|
||||
],
|
||||
},
|
||||
{ id: 'k8s-storage', name: '存储', icon: Database, path: '/workloads/pvcs', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-events', name: '事件', icon: Bell, path: '/workloads/events', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-topology', name: '架构拓扑', icon: Network, path: '/topology', permission: 'k8s:topology' },
|
||||
{ id: 'k8s-storage', name: '存储', labelKey: 'menu.storage', icon: Database, path: '/workloads/pvcs', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-events', name: '事件', labelKey: 'menu.events', icon: Bell, path: '/workloads/events', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-topology', name: '架构拓扑', labelKey: 'menu.topology', icon: Network, path: '/topology', permission: 'k8s:topology' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'deploy', name: '应用发布', icon: Rocket, permission: 'deploy:pipeline',
|
||||
id: 'deploy', name: '应用发布', labelKey: 'menu.deploy', icon: Rocket, permission: 'deploy:pipeline',
|
||||
children: [
|
||||
{ id: 'pipelines', name: '流水线', icon: GitBranch, path: '/pipelines', permission: 'deploy:pipeline' },
|
||||
{ id: 'deploy-credentials', name: '发布配置', icon: Key, path: '/deploy/configureTools', permission: 'deploy:release' },
|
||||
{ id: 'deploy-rbac', name: '发布权限', icon: Shield, path: '/deploy/rbac', permission: 'deploy:release' },
|
||||
{ id: 'pipelines', name: '流水线', labelKey: 'menu.pipelines', icon: GitBranch, path: '/pipelines', permission: 'deploy:pipeline' },
|
||||
{ id: 'deploy-credentials', name: '发布配置', labelKey: 'menu.deployConfig', icon: Key, path: '/deploy/configureTools', permission: 'deploy:release' },
|
||||
{ id: 'deploy-rbac', name: '发布权限', labelKey: 'menu.deployRbac', icon: Shield, path: '/deploy/rbac', permission: 'deploy:release' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'monitor', name: '监控中心', icon: Bell, permission: 'monitor:alert',
|
||||
id: 'monitor', name: '监控中心', labelKey: 'menu.monitor', icon: Bell, permission: 'monitor:alert',
|
||||
children: [
|
||||
{ id: 'grafana', name: '监控大屏', icon: Monitor, path: '/dashboards', permission: 'monitor:alert' },
|
||||
{ id: 'monitor-center', name: '资源监控', icon: Activity, path: '/monitor-center', permission: 'agent:monitor' },
|
||||
{ id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace', permission: 'monitor:alert' },
|
||||
{ id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center', permission: 'monitor:alert' },
|
||||
{ id: 'datasources', name: '数据源', icon: Database, path: '/datasources', permission: 'monitor:alert' },
|
||||
{ id: 'grafana', name: '监控大屏', labelKey: 'menu.grafana', icon: Monitor, path: '/dashboards', permission: 'monitor:alert' },
|
||||
{ id: 'monitor-center', name: '资源监控', labelKey: 'menu.resourceMonitor', icon: Activity, path: '/monitor-center', permission: 'agent:monitor' },
|
||||
{ id: 'trace', name: '链路追踪', labelKey: 'menu.trace', icon: GitBranch, path: '/trace', permission: 'monitor:alert' },
|
||||
{ id: 'alert-center', name: '告警中心', labelKey: 'menu.alertCenter', icon: Bell, path: '/alert-center', permission: 'monitor:alert' },
|
||||
{ id: 'datasources', name: '数据源', labelKey: 'menu.datasources', icon: Database, path: '/datasources', permission: 'monitor:alert' },
|
||||
],
|
||||
},
|
||||
// 日志中心:与监控中心平级,三个子菜单 // Logan.Li
|
||||
// 日志中心:与监控中心平级,四个子菜单
|
||||
{
|
||||
id: 'log-center', name: '日志中心', icon: ScrollText, permission: 'monitor:alert',
|
||||
id: 'log-center', name: '日志中心', labelKey: 'menu.logCenter', icon: ScrollText, permission: 'monitor:alert',
|
||||
children: [
|
||||
{ id: 'log-loki', name: 'Loki查询', icon: FileSearch, path: '/log-center/loki', permission: 'monitor:alert' },
|
||||
{ id: 'log-es', name: 'ES查询', icon: Database, path: '/log-center/es', permission: 'monitor:alert' },
|
||||
{ id: 'log-datasources', name: '日志库', icon: HardDrive, path: '/log-center/datasources', permission: 'monitor:alert' },
|
||||
{ id: 'log-alerts', name: '日志告警', icon: AlertTriangle, path: '/log-center/alerts', permission: 'monitor:alert' },
|
||||
{ id: 'log-loki', name: 'Loki查询', labelKey: 'menu.lokiQuery', icon: FileSearch, path: '/log-center/loki', permission: 'monitor:alert' },
|
||||
{ id: 'log-es', name: 'ES查询', labelKey: 'menu.esQuery', icon: Database, path: '/log-center/es', permission: 'monitor:alert' },
|
||||
{ id: 'log-datasources', name: '日志库', labelKey: 'menu.logLibrary', icon: HardDrive, path: '/log-center/datasources', permission: 'monitor:alert' },
|
||||
{ id: 'log-alerts', name: '日志告警', labelKey: 'menu.logAlerts', icon: AlertTriangle, path: '/log-center/alerts', permission: 'monitor:alert' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'automation', name: '自动化运维', icon: PlayCircle, permission: 'auto:cron',
|
||||
id: 'automation', name: '自动化运维', labelKey: 'menu.automation', icon: PlayCircle, permission: 'auto:cron',
|
||||
children: [
|
||||
{ id: 'cronjobs', name: '定时任务', icon: PlayCircle, path: '/cronjobs', permission: 'auto:cron' },
|
||||
{ id: 'scripts', name: '脚本库', icon: FileCode, path: '/scripts', permission: 'auto:script' },
|
||||
{ id: 'ansible', name: '远程执行', icon: Terminal, path: '/ansible', permission: 'auto:ansible' },
|
||||
{ id: 'cronjobs', name: '定时任务', labelKey: 'menu.cronJobs', icon: PlayCircle, path: '/cronjobs', permission: 'auto:cron' },
|
||||
{ id: 'scripts', name: '脚本库', labelKey: 'menu.scripts', icon: FileCode, path: '/scripts', permission: 'auto:script' },
|
||||
{ id: 'ansible', name: '远程执行', labelKey: 'menu.remoteExec', icon: Terminal, path: '/ansible', permission: 'auto:ansible' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'aiops', name: 'AIOps智能', icon: Brain, permission: 'aiops:assistant',
|
||||
id: 'aiops', name: 'AIOps智能', labelKey: 'menu.aiops', icon: Brain, permission: 'aiops:assistant',
|
||||
children: [
|
||||
{ id: 'ai-assistant', name: '智能助手', icon: Bot, path: '/ai-assistant', permission: 'aiops:assistant' },
|
||||
{ id: 'ai-alert-analyzer', name: '告警分析', icon: AlertTriangle, path: '/ai-alert-analyzer', permission: 'aiops:analysis' },
|
||||
{ id: 'ai-predictive', name: '容量预测', icon: BarChart3, path: '/ai-predictive', permission: 'aiops:forecast' },
|
||||
{ id: 'ai-agent-cli', name: 'Agent CLI', icon: Terminal, path: '/ai-agent-cli', permission: 'aiops:agent-cli' },
|
||||
{ id: 'ai-llm-config', name: '大模型设置', icon: Settings, path: '/ai-llm-config', permission: 'aiops:llm-config' },
|
||||
{ id: 'ai-assistant', name: '智能助手', labelKey: 'menu.aiAssistant', icon: Bot, path: '/ai-assistant', permission: 'aiops:assistant' },
|
||||
{ id: 'ai-alert-analyzer', name: '告警分析', labelKey: 'menu.aiAlertAnalyzer', icon: AlertTriangle, path: '/ai-alert-analyzer', permission: 'aiops:analysis' },
|
||||
{ id: 'ai-predictive', name: '容量预测', labelKey: 'menu.aiPredictive', icon: BarChart3, path: '/ai-predictive', permission: 'aiops:forecast' },
|
||||
{ id: 'ai-agent-cli', name: 'Agent CLI', labelKey: 'menu.aiAgentCli', icon: Terminal, path: '/ai-agent-cli', permission: 'aiops:agent-cli' },
|
||||
{ id: 'ai-llm-config', name: '大模型设置', labelKey: 'menu.aiLlmConfig', icon: Settings, path: '/ai-llm-config', permission: 'aiops:llm-config' },
|
||||
],
|
||||
},
|
||||
{ id: 'approval', name: '资源申请', icon: CheckSquare, permission: 'approval:apply',
|
||||
{ id: 'approval', name: '资源申请', labelKey: 'menu.approval', icon: CheckSquare, permission: 'approval:apply',
|
||||
children: [
|
||||
{ id: 'approval-resource', name: '资源申请', icon: Server, path: '/approval/resource', permission: 'approval:apply' },
|
||||
{ id: 'approval-app', name: '应用申请', icon: Rocket, path: '/approval/app', permission: 'approval:apply' },
|
||||
{ id: 'approval-permission', name: '权限审批', icon: ShieldCheck, path: '/approval/permission', permission: 'approval:apply' },
|
||||
{ id: 'approval-resource', name: '资源申请', labelKey: 'menu.approvalResource', icon: Server, path: '/approval/resource', permission: 'approval:apply' },
|
||||
{ id: 'approval-app', name: '应用申请', labelKey: 'menu.approvalApp', icon: Rocket, path: '/approval/app', permission: 'approval:apply' },
|
||||
{ id: 'approval-permission', name: '权限审批', labelKey: 'menu.approvalPermission', icon: ShieldCheck, path: '/approval/permission', permission: 'approval:apply' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'audit', name: '安全审计', icon: FileSearch, permission: 'audit:terminal',
|
||||
id: 'audit', name: '安全审计', labelKey: 'menu.audit', icon: FileSearch, permission: 'audit:terminal',
|
||||
children: [
|
||||
{ id: 'audit-terminal', name: '终端审计', icon: Terminal, path: '/audit/terminal', permission: 'audit:terminal' },
|
||||
{ id: 'audit-operations', name: '操作审计', icon: FileCode, path: '/audit/operations', permission: 'audit:operations' },
|
||||
{ id: 'audit-login', name: '登录日志', icon: LogIn, path: '/audit/login-logs', permission: 'audit:login-logs' },
|
||||
{ id: 'audit-terminal', name: '终端审计', labelKey: 'menu.auditTerminal', icon: Terminal, path: '/audit/terminal', permission: 'audit:terminal' },
|
||||
{ id: 'audit-operations', name: '操作审计', labelKey: 'menu.auditOperations', icon: FileCode, path: '/audit/operations', permission: 'audit:operations' },
|
||||
{ id: 'audit-login', name: '登录日志', labelKey: 'menu.auditLogin', icon: LogIn, path: '/audit/login-logs', permission: 'audit:login-logs' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system', name: '系统管理', icon: Settings, permission: 'system:user',
|
||||
id: 'system', name: '系统管理', labelKey: 'menu.system', icon: Settings, permission: 'system:user',
|
||||
children: [
|
||||
{ id: 'users', name: '用户管理', icon: Users, path: '/users', permission: 'system:user' },
|
||||
{ id: 'roles', name: '角色权限', icon: Shield, path: '/roles', permission: 'system:role' },
|
||||
{ id: 'credentials', name: '凭证配置', icon: Shield, path: '/credentials', permission: 'system:user' },
|
||||
{ id: 'approval-workflow', name: '审批流程', icon: GitBranch, path: '/approval-workflow', permission: 'system:role' },
|
||||
{ id: 'settings', name: '系统设置', icon: Settings, path: '/settings', permission: 'system:user' },
|
||||
{ id: 'users', name: '用户管理', labelKey: 'menu.users', icon: Users, path: '/users', permission: 'system:user' },
|
||||
{ id: 'roles', name: '角色权限', labelKey: 'menu.roles', icon: Shield, path: '/roles', permission: 'system:role' },
|
||||
{ id: 'credentials', name: '凭证配置', labelKey: 'menu.credentials', icon: Shield, path: '/credentials', permission: 'system:user' },
|
||||
{ id: 'approval-workflow', name: '审批流程', labelKey: 'menu.approvalWorkflow', icon: GitBranch, path: '/approval-workflow', permission: 'system:role' },
|
||||
{ id: 'settings', name: '系统设置', labelKey: 'menu.settings', icon: Settings, path: '/settings', permission: 'system:user' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -188,10 +191,14 @@ const menuItems: MenuItem[] = [
|
||||
*/
|
||||
function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number }) {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { openMenus, toggleMenu, activeMenu, setActiveMenu } = useMenuStore();
|
||||
const { sidebarCollapsed } = useThemeStore();
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isOpen = openMenus.includes(item.id);
|
||||
|
||||
// 优先使用 i18n 翻译 key;无 labelKey 则回退到 name
|
||||
const displayName = item.labelKey ? t(item.labelKey, { defaultValue: item.name }) : item.name;
|
||||
|
||||
// Determine if this menu item is active
|
||||
const isActive = item.path
|
||||
@@ -259,7 +266,7 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
{item.name}
|
||||
{displayName}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -282,7 +289,7 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<item.icon className={cn('h-3.5 w-3.5', (isActive || isActiveParent) && 'text-[var(--dg-blue)]')} />
|
||||
<span className="truncate">{item.name}</span>
|
||||
<span className="truncate">{displayName}</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
@@ -319,7 +326,7 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
|
||||
}
|
||||
>
|
||||
<item.icon className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{item.name}</span>
|
||||
<span className="truncate">{displayName}</span>
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
@@ -337,6 +344,7 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
|
||||
* 5. 底部添加版本信息
|
||||
*/
|
||||
export function Sidebar() {
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed, toggleSidebar } = useThemeStore();
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
@@ -377,7 +385,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="font-bold text-[var(--dg-text-primary)] text-sm tracking-tight truncate">
|
||||
AIOps平台
|
||||
{t('common.appName')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -387,7 +395,7 @@ export function Sidebar() {
|
||||
size="icon"
|
||||
className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] flex-shrink-0"
|
||||
onClick={toggleSidebar}
|
||||
title="收起侧边栏"
|
||||
title={t('header.collapseSidebar')}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import zhCN from '@/locales/zh-CN.json';
|
||||
import enUS from '@/locales/en-US.json';
|
||||
|
||||
export type AppLanguage = 'zh-CN' | 'en-US';
|
||||
|
||||
export const SUPPORTED_LANGUAGES: { code: AppLanguage; label: string; flag: string }[] = [
|
||||
{ code: 'zh-CN', label: '中文', flag: '🇨🇳' },
|
||||
{ code: 'en-US', label: 'English', flag: '🇺🇸' },
|
||||
];
|
||||
|
||||
export const DEFAULT_LANGUAGE: AppLanguage = 'zh-CN';
|
||||
|
||||
/** Read persisted language from localStorage (zustand persist key) */
|
||||
function getPersistedLanguage(): AppLanguage {
|
||||
try {
|
||||
const raw = localStorage.getItem('language-storage');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const lang = parsed?.state?.language;
|
||||
if (lang === 'zh-CN' || lang === 'en-US') return lang;
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
'zh-CN': { translation: zhCN },
|
||||
'en-US': { translation: enUS },
|
||||
},
|
||||
lng: getPersistedLanguage(),
|
||||
fallbackLng: 'zh-CN',
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Sync <html lang="..."> with current language
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.lang = lng;
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "AIOps Platform",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"search": "Search",
|
||||
"refresh": "Refresh",
|
||||
"loading": "Loading...",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"submit": "Submit",
|
||||
"reset": "Reset",
|
||||
"export": "Export",
|
||||
"import": "Import",
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"test": "Test",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"actions": "Actions",
|
||||
"status": "Status",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"type": "Type",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"noData": "No Data",
|
||||
"total": "Total",
|
||||
"detail": "Detail",
|
||||
"more": "More",
|
||||
"filter": "Filter",
|
||||
"clear": "Clear",
|
||||
"apply": "Apply",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"maintenance": "Maintenance",
|
||||
"unknown": "Unknown",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"required": "Required",
|
||||
"optional": "Optional",
|
||||
"confirmDelete": "Confirm delete? This action cannot be undone.",
|
||||
"savedSuccessfully": "Saved successfully",
|
||||
"deletedSuccessfully": "Deleted successfully",
|
||||
"operationSuccess": "Operation succeeded",
|
||||
"operationFailed": "Operation failed",
|
||||
"loadFailed": "Failed to load",
|
||||
"saveFailed": "Failed to save"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"assets": "Assets",
|
||||
"servers": "Servers",
|
||||
"cloudAccounts": "Cloud Accounts",
|
||||
"domains": "Domains",
|
||||
"regions": "Regions",
|
||||
"platforms": "Cloud Platforms",
|
||||
"cloudBilling": "Cloud Billing",
|
||||
"kubernetes": "Kubernetes",
|
||||
"clusterList": "Clusters",
|
||||
"nodeManagement": "Nodes",
|
||||
"workloads": "Workloads",
|
||||
"deployments": "Deployments",
|
||||
"statefulsets": "StatefulSets",
|
||||
"daemonsets": "DaemonSets",
|
||||
"cronjobs": "CronJobs",
|
||||
"pods": "Pods",
|
||||
"network": "Network",
|
||||
"services": "Services",
|
||||
"ingresses": "Ingresses",
|
||||
"configCenter": "Configuration",
|
||||
"configmaps": "ConfigMaps",
|
||||
"secrets": "Secrets",
|
||||
"storage": "Storage",
|
||||
"events": "Events",
|
||||
"topology": "Topology",
|
||||
"deploy": "Deployment",
|
||||
"pipelines": "Pipelines",
|
||||
"deployConfig": "Deploy Config",
|
||||
"deployRbac": "Deploy RBAC",
|
||||
"monitor": "Monitoring",
|
||||
"grafana": "Dashboards",
|
||||
"resourceMonitor": "Resource Monitor",
|
||||
"trace": "Tracing",
|
||||
"alertCenter": "Alert Center",
|
||||
"datasources": "Datasources",
|
||||
"logCenter": "Log Center",
|
||||
"lokiQuery": "Loki Query",
|
||||
"esQuery": "ES Query",
|
||||
"logLibrary": "Log Library",
|
||||
"logAlerts": "Log Alerts",
|
||||
"automation": "Automation",
|
||||
"cronJobs": "Cron Jobs",
|
||||
"scripts": "Scripts",
|
||||
"remoteExec": "Remote Exec",
|
||||
"aiops": "AIOps",
|
||||
"aiAssistant": "AI Assistant",
|
||||
"aiAlertAnalyzer": "Alert Analysis",
|
||||
"aiPredictive": "Capacity Forecast",
|
||||
"aiAgentCli": "Agent CLI",
|
||||
"aiLlmConfig": "LLM Settings",
|
||||
"approval": "Requests",
|
||||
"approvalResource": "Resource Request",
|
||||
"approvalApp": "App Request",
|
||||
"approvalPermission": "Permission Request",
|
||||
"audit": "Security Audit",
|
||||
"auditTerminal": "Terminal Audit",
|
||||
"auditOperations": "Operations Audit",
|
||||
"auditLogin": "Login Logs",
|
||||
"system": "System",
|
||||
"users": "Users",
|
||||
"roles": "Roles & Permissions",
|
||||
"credentials": "Credentials",
|
||||
"approvalWorkflow": "Approval Workflow",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"header": {
|
||||
"expandSidebar": "Expand Sidebar",
|
||||
"collapseSidebar": "Collapse Sidebar",
|
||||
"theme": "Switch Theme",
|
||||
"themeSwitch": "Theme",
|
||||
"notifications": "Notifications",
|
||||
"markAllRead": "Mark All Read",
|
||||
"noNotifications": "No Notifications",
|
||||
"viewAllNotifications": "View All Notifications",
|
||||
"profile": "Profile",
|
||||
"accountSettings": "Account Settings",
|
||||
"logout": "Logout",
|
||||
"language": "Language",
|
||||
"languageSwitch": "Language",
|
||||
"admin": "Administrator"
|
||||
},
|
||||
"login": {
|
||||
"title": "AIOps Intelligent Operations Platform",
|
||||
"subtitle": "Enterprise-grade intelligent operations management platform",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"loginButton": "Sign In",
|
||||
"loggingIn": "Signing in...",
|
||||
"loginFailed": "Login failed",
|
||||
"usernameRequired": "Please enter username",
|
||||
"passwordRequired": "Please enter password",
|
||||
"rememberMe": "Remember me",
|
||||
"welcome": "Welcome to",
|
||||
"features": {
|
||||
"k8s": "Kubernetes Full-Stack Management",
|
||||
"monitor": "Comprehensive Monitoring & Alerting",
|
||||
"aiops": "AI-Powered Operations",
|
||||
"ci": "CI/CD Automated Pipelines"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"serverStats": "Server Statistics",
|
||||
"totalServers": "Total Servers",
|
||||
"onlineServers": "Online Servers",
|
||||
"k8sClusters": "K8s Clusters",
|
||||
"runningPods": "Running Pods",
|
||||
"todayAlerts": "Today's Alerts",
|
||||
"recentAlerts": "Recent Alerts",
|
||||
"recentPipelines": "Recent Pipelines",
|
||||
"recentReleases": "Recent Releases",
|
||||
"systemOverview": "System Overview",
|
||||
"resourceUsage": "Resource Usage",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"quickActions": "Quick Actions"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "AIOps平台",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"search": "搜索",
|
||||
"refresh": "刷新",
|
||||
"loading": "加载中...",
|
||||
"success": "成功",
|
||||
"failed": "失败",
|
||||
"error": "错误",
|
||||
"warning": "警告",
|
||||
"info": "提示",
|
||||
"close": "关闭",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"submit": "提交",
|
||||
"reset": "重置",
|
||||
"export": "导出",
|
||||
"import": "导入",
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"test": "测试",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"all": "全部",
|
||||
"none": "无",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"actions": "操作",
|
||||
"status": "状态",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"type": "类型",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"noData": "暂无数据",
|
||||
"total": "总计",
|
||||
"detail": "详情",
|
||||
"more": "更多",
|
||||
"filter": "筛选",
|
||||
"clear": "清空",
|
||||
"apply": "应用",
|
||||
"approve": "通过",
|
||||
"reject": "拒绝",
|
||||
"pending": "待处理",
|
||||
"approved": "已通过",
|
||||
"rejected": "已拒绝",
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"running": "运行中",
|
||||
"stopped": "已停止",
|
||||
"maintenance": "维护中",
|
||||
"unknown": "未知",
|
||||
"active": "活跃",
|
||||
"inactive": "未激活",
|
||||
"required": "必填",
|
||||
"optional": "可选",
|
||||
"confirmDelete": "确认删除?此操作不可恢复。",
|
||||
"savedSuccessfully": "保存成功",
|
||||
"deletedSuccessfully": "删除成功",
|
||||
"operationSuccess": "操作成功",
|
||||
"operationFailed": "操作失败",
|
||||
"loadFailed": "加载失败",
|
||||
"saveFailed": "保存失败"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "仪表盘",
|
||||
"assets": "资产管理",
|
||||
"servers": "服务器管理",
|
||||
"cloudAccounts": "云账号管理",
|
||||
"domains": "域名管理",
|
||||
"regions": "区域管理",
|
||||
"platforms": "云平台",
|
||||
"cloudBilling": "云账单",
|
||||
"kubernetes": "Kubernetes",
|
||||
"clusterList": "集群列表",
|
||||
"nodeManagement": "节点管理",
|
||||
"workloads": "工作负载",
|
||||
"deployments": "Deployments",
|
||||
"statefulsets": "StatefulSets",
|
||||
"daemonsets": "DaemonSets",
|
||||
"cronjobs": "CronJobs",
|
||||
"pods": "Pods",
|
||||
"network": "网络",
|
||||
"services": "Services",
|
||||
"ingresses": "Ingresses",
|
||||
"configCenter": "配置中心",
|
||||
"configmaps": "ConfigMaps",
|
||||
"secrets": "Secrets",
|
||||
"storage": "存储",
|
||||
"events": "事件",
|
||||
"topology": "架构拓扑",
|
||||
"deploy": "应用发布",
|
||||
"pipelines": "流水线",
|
||||
"deployConfig": "发布配置",
|
||||
"deployRbac": "发布权限",
|
||||
"monitor": "监控中心",
|
||||
"grafana": "监控大屏",
|
||||
"resourceMonitor": "资源监控",
|
||||
"trace": "链路追踪",
|
||||
"alertCenter": "告警中心",
|
||||
"datasources": "数据源",
|
||||
"logCenter": "日志中心",
|
||||
"lokiQuery": "Loki查询",
|
||||
"esQuery": "ES查询",
|
||||
"logLibrary": "日志库",
|
||||
"logAlerts": "日志告警",
|
||||
"automation": "自动化运维",
|
||||
"cronJobs": "定时任务",
|
||||
"scripts": "脚本库",
|
||||
"remoteExec": "远程执行",
|
||||
"aiops": "AIOps智能",
|
||||
"aiAssistant": "智能助手",
|
||||
"aiAlertAnalyzer": "告警分析",
|
||||
"aiPredictive": "容量预测",
|
||||
"aiAgentCli": "Agent CLI",
|
||||
"aiLlmConfig": "大模型设置",
|
||||
"approval": "资源申请",
|
||||
"approvalResource": "资源申请",
|
||||
"approvalApp": "应用申请",
|
||||
"approvalPermission": "权限审批",
|
||||
"audit": "安全审计",
|
||||
"auditTerminal": "终端审计",
|
||||
"auditOperations": "操作审计",
|
||||
"auditLogin": "登录日志",
|
||||
"system": "系统管理",
|
||||
"users": "用户管理",
|
||||
"roles": "角色权限",
|
||||
"credentials": "凭证配置",
|
||||
"approvalWorkflow": "审批流程",
|
||||
"settings": "系统设置"
|
||||
},
|
||||
"header": {
|
||||
"expandSidebar": "展开侧边栏",
|
||||
"collapseSidebar": "收起侧边栏",
|
||||
"theme": "切换主题",
|
||||
"themeSwitch": "主题切换",
|
||||
"notifications": "通知",
|
||||
"markAllRead": "全部已读",
|
||||
"noNotifications": "暂无通知",
|
||||
"viewAllNotifications": "查看全部通知",
|
||||
"profile": "个人中心",
|
||||
"accountSettings": "账号设置",
|
||||
"logout": "退出登录",
|
||||
"language": "语言",
|
||||
"languageSwitch": "语言切换",
|
||||
"admin": "管理员"
|
||||
},
|
||||
"login": {
|
||||
"title": "AIOps 智能运维平台",
|
||||
"subtitle": "企业级智能运维管理平台",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"loginButton": "登 录",
|
||||
"loggingIn": "登录中...",
|
||||
"loginFailed": "登录失败",
|
||||
"usernameRequired": "请输入用户名",
|
||||
"passwordRequired": "请输入密码",
|
||||
"rememberMe": "记住我",
|
||||
"welcome": "欢迎使用",
|
||||
"features": {
|
||||
"k8s": "Kubernetes 全栈管理",
|
||||
"monitor": "全方位监控告警",
|
||||
"aiops": "AI 智能运维",
|
||||
"ci": "CI/CD 自动化流水线"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "仪表盘",
|
||||
"serverStats": "服务器统计",
|
||||
"totalServers": "服务器总数",
|
||||
"onlineServers": "在线服务器",
|
||||
"k8sClusters": "K8s集群",
|
||||
"runningPods": "运行中Pod",
|
||||
"todayAlerts": "今日告警",
|
||||
"recentAlerts": "最近告警",
|
||||
"recentPipelines": "最近流水线",
|
||||
"recentReleases": "最近发布",
|
||||
"systemOverview": "系统概览",
|
||||
"resourceUsage": "资源使用率",
|
||||
"cpuUsage": "CPU使用率",
|
||||
"memoryUsage": "内存使用率",
|
||||
"diskUsage": "磁盘使用率",
|
||||
"quickActions": "快捷操作"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
// U3: i18n — 必须在 App 之前导入以初始化翻译实例
|
||||
import './i18n'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
|
||||
@@ -14,6 +14,32 @@ export const useThemeStore = create<ThemeState>((set) => ({
|
||||
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
||||
}));
|
||||
|
||||
// 语言状态(i18n — U3 国际化)
|
||||
type AppLanguage = 'zh-CN' | 'en-US';
|
||||
|
||||
interface LanguageState {
|
||||
language: AppLanguage;
|
||||
setLanguage: (lang: AppLanguage) => void;
|
||||
}
|
||||
|
||||
export const useLanguageStore = create<LanguageState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
language: 'zh-CN' as AppLanguage,
|
||||
setLanguage: (lang) => {
|
||||
// 同步切换 i18next 实例
|
||||
import('@/i18n').then(({ default: i18n }) => {
|
||||
i18n.changeLanguage(lang);
|
||||
}).catch(() => { /* i18n not yet loaded */ });
|
||||
set({ language: lang });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'language-storage',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 用户状态
|
||||
interface UserState {
|
||||
user: User | null;
|
||||
|
||||
Reference in new issue
Block a user