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:
attacker committed 2026-06-27 16:54:07 +08:00
1 parent 084a3be2cf
commit 2e179ef7f4
15 files changed
+860 -112

No files matched your search

+61 -14
View File
@@ -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>
+59 -13
View File
@@ -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>
+79 -71
View File
@@ -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>