feat(dashboard): add i18n support with zh/en/ja/ko locales

- Add react-i18next + i18next + i18next-browser-languagedetector
- Create i18n config (singleton import) with zh/en/ja/ko JSON locale files
- Add language switcher Globe dropdown in Header topbar
- Replace all hardcoded Chinese strings in:
  - Layout (Header, Sidebar, NavItem, Layout, constants)
  - Settings (index, AppearanceTab, SecurityTab, OtherTab, AboutTab)
  - Auth page (auth.tsx)
  - Search dialog (searchItems via useMemo + t())
  - Restart overlay (getStatusConfig accepts t param)
  - Error boundary (ErrorFallback, ErrorDetails function components)
  - HTTP warning banner
- localStorage key: maibot-locale
- Compatible with Electron
This commit is contained in:
DrSmoothl
2026-03-03 20:50:06 +08:00
parent 5cc34f24c0
commit a65a40f85f
23 changed files with 7271 additions and 473 deletions

View File

@@ -1,4 +1,5 @@
import { Component } from 'react'
import { useTranslation } from 'react-i18next'
import type { ErrorInfo, ReactNode } from 'react'
import { AlertTriangle, RefreshCw, Home, ChevronDown, ChevronUp, Copy, Check, Bug } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -65,6 +66,7 @@ function ErrorDetails({ error, errorInfo }: { error: Error; errorInfo: ErrorInfo
const [isStackOpen, setIsStackOpen] = useState(true)
const [isComponentStackOpen, setIsComponentStackOpen] = useState(false)
const [copied, setCopied] = useState(false)
const { t } = useTranslation()
const stackFrames = error.stack ? parseStackTrace(error.stack) : []
@@ -183,12 +185,12 @@ Time: ${new Date().toISOString()}
{copied ? (
<>
<Check className="mr-2 h-4 w-4 text-green-500" />
{t('errorBoundary.copiedToClipboard')}
</>
) : (
<>
<Copy className="mr-2 h-4 w-4" />
{t('errorBoundary.copyError')}
</>
)}
</Button>
@@ -204,6 +206,7 @@ function ErrorFallback({
error: Error
errorInfo: ErrorInfo | null
}) {
const { t } = useTranslation()
const handleGoHome = () => {
window.location.href = '/'
}
@@ -219,9 +222,9 @@ function ErrorFallback({
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
<AlertTriangle className="h-8 w-8 text-red-600 dark:text-red-400" />
</div>
<CardTitle className="text-2xl font-bold"></CardTitle>
<CardTitle className="text-2xl font-bold">{t('errorBoundary.title')}</CardTitle>
<CardDescription className="text-base mt-2">
{t('errorBoundary.description')}
</CardDescription>
</CardHeader>
@@ -232,17 +235,17 @@ function ErrorFallback({
<div className="flex flex-col sm:flex-row gap-2 pt-2">
<Button onClick={handleRefresh} className="flex-1">
<RefreshCw className="mr-2 h-4 w-4" />
{t('errorBoundary.refreshPage')}
</Button>
<Button onClick={handleGoHome} variant="outline" className="flex-1">
<Home className="mr-2 h-4 w-4" />
{t('errorBoundary.goHome')}
</Button>
</div>
{/* 提示信息 */}
<p className="text-xs text-center text-muted-foreground pt-2">
{t('errorBoundary.footer')}
</p>
</CardContent>
</Card>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AlertTriangle, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -7,6 +8,7 @@ import { Button } from '@/components/ui/button'
* 当用户通过 HTTP 访问时显示安全警告
*/
export function HttpWarningBanner() {
const { t } = useTranslation()
// 直接计算初始状态,避免 effect 中调用 setState
const isHttp = window.location.protocol === 'http:'
const hostname = window.location.hostname.toLowerCase()
@@ -35,11 +37,11 @@ export function HttpWarningBanner() {
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-500 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
<span className="font-semibold"></span>
使 <strong>HTTP</strong> 访 MaiBot WebUI
<span className="font-semibold">{t('httpWarning.title')}</span>
{t('httpWarning.message')}
</p>
<p className="text-xs text-amber-800 dark:text-amber-200 mt-1">
Token使 HTTPS 访使
{t('httpWarning.description')}
</p>
</div>
</div>
@@ -48,7 +50,7 @@ export function HttpWarningBanner() {
size="icon"
onClick={handleDismiss}
className="h-8 w-8 text-amber-700 hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-200 flex-shrink-0"
aria-label="关闭警告"
aria-label={t('httpWarning.dismiss')}
>
<X className="h-4 w-4" />
</Button>

View File

@@ -1,17 +1,26 @@
import { BookOpen, ChevronLeft, LogOut, Menu, Moon, PieChart, Search, Server, Sun } from 'lucide-react'
import { Link } from '@tanstack/react-router'
import { BookOpen, ChevronLeft, Globe, LogOut, Menu, Moon, PieChart, Search, Server, Sun } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BackgroundLayer } from '@/components/background-layer'
import { Button } from '@/components/ui/button'
import { Kbd } from '@/components/ui/kbd'
import { SearchDialog } from '@/components/search-dialog'
import { useEffect, useState } from 'react'
import { BackendManager } from '@/components/electron/BackendManager'
import { isElectron } from '@/lib/runtime'
import { cn } from '@/lib/utils'
import { SearchDialog } from '@/components/search-dialog'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Kbd } from '@/components/ui/kbd'
import { toggleThemeWithTransition } from '@/components/use-theme'
import { useBackground } from '@/hooks/use-background'
import { logout } from '@/lib/fetch-with-auth'
import { toggleThemeWithTransition } from '@/components/use-theme'
import { isElectron } from '@/lib/runtime'
import { cn } from '@/lib/utils'
const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const
interface HeaderProps {
sidebarOpen: boolean
@@ -26,7 +35,7 @@ interface HeaderProps {
export function Header({
sidebarOpen,
// mobileMenuOpen, // unused - kept in props for API compatibility
searchOpen,
actualTheme,
onSidebarToggle,
@@ -34,6 +43,8 @@ export function Header({
onSearchOpenChange,
onThemeChange,
}: HeaderProps) {
const { t, i18n: i18nInstance } = useTranslation()
const currentLang = i18nInstance.language || 'zh'
const headerBg = useBackground('header')
const [backendManagerOpen, setBackendManagerOpen] = useState(false)
const [activeBackendName, setActiveBackendName] = useState<string>('')
@@ -41,7 +52,7 @@ export function Header({
useEffect(() => {
if (!isElectron()) return
window.electronAPI!.getActiveBackend().then((b) => {
setActiveBackendName(b?.name ?? '未连接')
setActiveBackendName(b?.name ?? t('header.notConnected'))
})
}, [])
@@ -65,7 +76,7 @@ export function Header({
<button
onClick={onSidebarToggle}
className="hidden rounded-lg p-2 hover:bg-accent lg:block"
title={sidebarOpen ? '收起侧边栏' : '展开侧边栏'}
title={sidebarOpen ? t('header.collapseSidebar') : t('header.expandSidebar')}
>
<ChevronLeft
className={cn('h-5 w-5 transition-transform', !sidebarOpen && 'rotate-180')}
@@ -82,7 +93,7 @@ export function Header({
size="sm"
className="gap-2"
onClick={() => setBackendManagerOpen(true)}
title="切换后端连接"
title={t('header.toggleConnection')}
>
<Server className="h-4 w-4" />
<span className="hidden sm:inline text-xs text-muted-foreground truncate max-w-[100px]">
@@ -99,10 +110,10 @@ export function Header({
variant="ghost"
size="sm"
className="gap-2 bg-gradient-to-r from-pink-500/10 to-purple-500/10 hover:from-pink-500/20 hover:to-purple-500/20 border border-pink-500/20"
title="查看年度总结"
title={t('header.viewAnnualSummary')}
>
<PieChart className="h-4 w-4 text-pink-500" />
<span className="hidden sm:inline bg-gradient-to-r from-pink-500 to-purple-500 bg-clip-text text-transparent font-medium">2025 </span>
<span className="hidden sm:inline bg-gradient-to-r from-pink-500 to-purple-500 bg-clip-text text-transparent font-medium">{t('header.annualSummary')}</span>
</Button>
</Link>
@@ -112,7 +123,7 @@ export function Header({
className="relative hidden md:flex items-center w-64 h-9 pl-9 pr-16 bg-background/50 border rounded-md hover:bg-accent/50 transition-colors text-left"
>
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<span className="text-sm text-muted-foreground">...</span>
<span className="text-sm text-muted-foreground">{t('header.searchPlaceholder')}</span>
<Kbd size="sm" className="absolute right-2 top-1/2 -translate-y-1/2">
<span className="text-xs"></span>K
</Kbd>
@@ -127,12 +138,41 @@ export function Header({
size="sm"
onClick={() => window.open('https://docs.mai-mai.org', '_blank')}
className="gap-2"
title="查看麦麦文档"
title={t('header.viewDocs')}
>
<BookOpen className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="hidden sm:inline">{t('header.docs')}</span>
</Button>
{/* 语言切换 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2">
<Globe className="h-4 w-4" />
<span className="hidden sm:inline text-xs">
{t(`language.${currentLang.split('-')[0] as 'zh' | 'en' | 'ja' | 'ko'}`) ?? currentLang}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{LANGUAGE_CODES.map((code) => (
<DropdownMenuItem
key={code}
onClick={() => i18nInstance.changeLanguage(code)}
className={cn(
'cursor-pointer',
currentLang.split('-')[0] === code && 'font-semibold text-primary'
)}
>
{currentLang.split('-')[0] === code && (
<span className="mr-2"></span>
)}
{t(`language.${code}`)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* 主题切换按钮 */}
<button
onClick={(e) => {
@@ -140,7 +180,7 @@ export function Header({
toggleThemeWithTransition(newTheme, onThemeChange, e)
}}
className="rounded-lg p-2 hover:bg-accent"
title={actualTheme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
title={actualTheme === 'dark' ? t('header.switchToLight') : t('header.switchToDark')}
>
{actualTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button>
@@ -154,10 +194,10 @@ export function Header({
size="sm"
onClick={handleLogout}
className="gap-2"
title="登出系统"
title={t('header.logout')}
>
<LogOut className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="hidden sm:inline">{t('header.logoutLabel')}</span>
</Button>
</div>
</header>

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BackgroundLayer } from '@/components/background-layer'
import { BackToTop } from '@/components/back-to-top'
@@ -16,6 +17,7 @@ import { Sidebar } from './Sidebar'
import type { LayoutProps } from './types'
export function Layout({ children }: LayoutProps) {
const { t } = useTranslation()
const { checking } = useAuthGuard() // 检查认证状态
const [sidebarOpen, setSidebarOpen] = useState(true)
@@ -55,7 +57,7 @@ export function Layout({ children }: LayoutProps) {
if (checking) {
return (
<div className="flex h-screen items-center justify-center bg-background">
<div className="text-muted-foreground">...</div>
<div className="text-muted-foreground">{t('layout.verifyingLogin')}</div>
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { Link, useMatchRoute } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
@@ -13,6 +14,7 @@ interface NavItemProps {
}
export function NavItem({ item, sidebarOpen, tooltipsEnabled, onMobileMenuClose }: NavItemProps) {
const { t } = useTranslation()
const matchRoute = useMatchRoute()
const isActive = matchRoute({ to: item.path })
const Icon = item.icon
@@ -42,7 +44,7 @@ export function NavItem({ item, sidebarOpen, tooltipsEnabled, onMobileMenuClose
? 'opacity-100 max-w-[200px]'
: 'opacity-100 max-w-[200px] lg:opacity-0 lg:max-w-0 lg:overflow-hidden'
)}>
{item.label}
{t(item.label)}
</span>
</div>
</>
@@ -70,7 +72,7 @@ export function NavItem({ item, sidebarOpen, tooltipsEnabled, onMobileMenuClose
</TooltipTrigger>
{tooltipsEnabled && (
<TooltipContent side="right" className="hidden lg:block">
<p>{item.label}</p>
<p>{t(item.label)}</p>
</TooltipContent>
)}
</Tooltip>

View File

@@ -1,3 +1,5 @@
import { useTranslation } from 'react-i18next'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
import { useBackground } from '@/hooks/use-background'
@@ -20,6 +22,7 @@ export function Sidebar({
tooltipsEnabled,
onMobileMenuClose
}: SidebarProps) {
const { t } = useTranslation()
const sidebarBg = useBackground('sidebar')
return (
@@ -60,7 +63,7 @@ export function Sidebar({
!sidebarOpen && "lg:mb-1 lg:invisible"
)}>
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/60 whitespace-nowrap">
{section.title}
{t(section.title)}
</h3>
</div>

View File

@@ -4,46 +4,46 @@ import type { MenuSection } from './types'
export const menuSections: MenuSection[] = [
{
title: '概览',
title: 'sidebar.groups.overview',
items: [
{ icon: Home, label: '首页', path: '/' },
{ icon: Home, label: 'sidebar.menu.home', path: '/' },
],
},
{
title: '麦麦配置编辑',
title: 'sidebar.groups.botConfig',
items: [
{ icon: FileText, label: '麦麦主程序配置', path: '/config/bot' },
{ icon: Server, label: 'AI模型厂商配置', path: '/config/modelProvider', tourId: 'sidebar-model-provider' },
{ icon: Boxes, label: '模型管理与分配', path: '/config/model', tourId: 'sidebar-model-management' },
{ icon: Sliders, label: '麦麦适配器配置', path: '/config/adapter' },
{ icon: FileText, label: 'sidebar.menu.botMainConfig', path: '/config/bot' },
{ icon: Server, label: 'sidebar.menu.aiModelProvider', path: '/config/modelProvider', tourId: 'sidebar-model-provider' },
{ icon: Boxes, label: 'sidebar.menu.modelManagement', path: '/config/model', tourId: 'sidebar-model-management' },
{ icon: Sliders, label: 'sidebar.menu.adapterConfig', path: '/config/adapter' },
],
},
{
title: '麦麦资源管理',
title: 'sidebar.groups.botResources',
items: [
{ icon: Smile, label: '表情包管理', path: '/resource/emoji' },
{ icon: MessageSquare, label: '表达方式管理', path: '/resource/expression' },
{ icon: Hash, label: '黑话管理', path: '/resource/jargon' },
{ icon: UserCircle, label: '人物信息管理', path: '/resource/person' },
{ icon: Network, label: '知识库图谱可视化', path: '/resource/knowledge-graph' },
{ icon: Database, label: '麦麦知识库管理', path: '/resource/knowledge-base' },
{ icon: Smile, label: 'sidebar.menu.emojiManagement', path: '/resource/emoji' },
{ icon: MessageSquare, label: 'sidebar.menu.expressionManagement', path: '/resource/expression' },
{ icon: Hash, label: 'sidebar.menu.slangManagement', path: '/resource/jargon' },
{ icon: UserCircle, label: 'sidebar.menu.personInfo', path: '/resource/person' },
{ icon: Network, label: 'sidebar.menu.knowledgeGraph', path: '/resource/knowledge-graph' },
{ icon: Database, label: 'sidebar.menu.knowledgeBase', path: '/resource/knowledge-base' },
],
},
{
title: '扩展与监控',
title: 'sidebar.groups.extensionsMonitor',
items: [
{ icon: Package, label: '插件市场', path: '/plugins' },
{ icon: LayoutGrid, label: '配置模板市场', path: '/config/pack-market' },
{ icon: Sliders, label: '插件配置', path: '/plugin-config' },
{ icon: FileSearch, label: '日志查看器', path: '/logs' },
{ icon: Activity, label: '计划器&回复器监控', path: '/planner-monitor' },
{ icon: MessageSquare, label: '本地聊天室', path: '/chat' },
{ icon: Package, label: 'sidebar.menu.pluginMarket', path: '/plugins' },
{ icon: LayoutGrid, label: 'sidebar.menu.configTemplate', path: '/config/pack-market' },
{ icon: Sliders, label: 'sidebar.menu.pluginConfig', path: '/plugin-config' },
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs' },
{ icon: Activity, label: 'sidebar.menu.plannerMonitor', path: '/planner-monitor' },
{ icon: MessageSquare, label: 'sidebar.menu.localChat', path: '/chat' },
],
},
{
title: '系统',
title: 'sidebar.groups.system',
items: [
{ icon: Settings, label: '系统设置', path: '/settings' },
{ icon: Settings, label: 'sidebar.menu.settings', path: '/settings' },
],
},
]

View File

@@ -17,6 +17,7 @@
*/
import { useEffect, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
Loader2,
CheckCircle2,
@@ -70,6 +71,7 @@ const getStatusConfig = (
status: RestartStatus,
checkAttempts: number,
maxAttempts: number,
t: (key: string, opts?: Record<string, unknown>) => string,
customTitle?: string,
customDescription?: string
): StatusConfig => {
@@ -82,33 +84,33 @@ const getStatusConfig = (
},
requesting: {
icon: <Loader2 className="h-16 w-16 text-primary animate-spin" />,
title: customTitle ?? '准备重启',
description: customDescription ?? '正在发送重启请求...',
tip: '🔄 正在准备重启麦麦...',
title: customTitle ?? t('restart.preparing'),
description: customDescription ?? t('restart.preparingDesc'),
tip: t('restart.preparingTip'),
},
restarting: {
icon: <Loader2 className="h-16 w-16 text-primary animate-spin" />,
title: customTitle ?? '正在重启麦麦',
description: customDescription ?? '请稍候,麦麦正在重启中...',
tip: '🔄 配置已保存,正在重启主程序...',
title: customTitle ?? t('restart.restarting'),
description: customDescription ?? t('restart.restartingDesc'),
tip: t('restart.restartingTip'),
},
checking: {
icon: <Loader2 className="h-16 w-16 text-primary animate-spin" />,
title: '检查服务状态',
description: `等待服务恢复... (${checkAttempts}/${maxAttempts})`,
tip: '⏳ 正在等待服务恢复,请勿关闭页面...',
title: t('restart.checking'),
description: t('restart.checkingDesc', { current: checkAttempts, max: maxAttempts }),
tip: t('restart.checkingTip'),
},
success: {
icon: <CheckCircle2 className="h-16 w-16 text-green-500" />,
title: '重启成功',
description: '正在跳转到登录页面...',
tip: '✅ 配置已生效,服务运行正常',
title: t('restart.success'),
description: t('restart.successDesc'),
tip: t('restart.successTip'),
},
failed: {
icon: <AlertCircle className="h-16 w-16 text-destructive" />,
title: '重启超时',
description: '服务未能在预期时间内恢复',
tip: '⚠️ 如果长时间无响应,请尝试手动重启',
title: t('restart.failed'),
description: t('restart.failedDesc'),
tip: t('restart.failedTip'),
},
}
return configs[status]
@@ -192,6 +194,7 @@ function RestartOverlayContent({
className,
}: RestartOverlayContentProps) {
const { status, progress, elapsedTime, checkAttempts, maxAttempts } = state
const { t } = useTranslation()
// 回调处理
useEffect(() => {
@@ -206,6 +209,7 @@ function RestartOverlayContent({
status,
checkAttempts,
maxAttempts,
t,
title,
description
)
@@ -246,7 +250,7 @@ function RestartOverlayContent({
<Progress value={progress} className="h-2" />
<div className="flex justify-between text-sm text-muted-foreground">
<span>{progress}%</span>
<span>: {formatTime(elapsedTime)}</span>
<span>{t('restart.elapsed')} {formatTime(elapsedTime)}</span>
</div>
</div>
)}
@@ -265,11 +269,11 @@ function RestartOverlayContent({
className="flex-1"
>
<RefreshCw className="mr-2 h-4 w-4" />
{t('restart.refreshPage')}
</Button>
<Button onClick={onRetry} variant="secondary" className="flex-1">
<RotateCcw className="mr-2 h-4 w-4" />
{t('restart.retryCheck')}
</Button>
</div>
)}

View File

@@ -1,6 +1,8 @@
import { useState, useCallback } from 'react'
import { useState, useCallback, useMemo } from 'react'
import { Search, FileText, Server, Boxes, Smile, MessageSquare, UserCircle, FileSearch, BarChart3, Package, Settings, Home, Hash } from 'lucide-react'
import { useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
@@ -24,97 +26,98 @@ interface SearchItem {
category: string
}
const searchItems: SearchItem[] = [
{
icon: Home,
title: '首页',
description: '查看仪表板概览',
path: '/',
category: '概览',
},
{
icon: FileText,
title: '麦麦主程序配置',
description: '配置麦麦的核心设置',
path: '/config/bot',
category: '配置',
},
{
icon: Server,
title: '麦麦模型提供商配置',
description: '配置模型提供商',
path: '/config/modelProvider',
category: '配置',
},
{
icon: Boxes,
title: '麦麦模型配置',
description: '配置模型参数',
path: '/config/model',
category: '配置',
},
{
icon: Smile,
title: '表情包管理',
description: '管理麦麦的表情包',
path: '/resource/emoji',
category: '资源',
},
{
icon: MessageSquare,
title: '表达方式管理',
description: '管理麦麦的表达方式',
path: '/resource/expression',
category: '资源',
},
{
icon: UserCircle,
title: '人物信息管理',
description: '管理人物信息',
path: '/resource/person',
category: '资源',
},
{
icon: Hash,
title: '黑话管理',
description: '管理麦麦学习到的黑话和俚语',
path: '/resource/jargon',
category: '资源',
},
{
icon: BarChart3,
title: '统计信息',
description: '查看使用统计',
path: '/statistics',
category: '监控',
},
{
icon: Package,
title: '插件市场',
description: '浏览和安装插件',
path: '/plugins',
category: '扩展',
},
{
icon: FileSearch,
title: '日志查看器',
description: '查看系统日志',
path: '/logs',
category: '监控',
},
{
icon: Settings,
title: '系统设置',
description: '配置系统参数',
path: '/settings',
category: '系统',
},
]
export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
const [searchQuery, setSearchQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0)
const navigate = useNavigate()
const { t } = useTranslation()
const searchItems: SearchItem[] = useMemo(() => [
{
icon: Home,
title: t('search.items.home'),
description: t('search.items.homeDesc'),
path: '/',
category: t('search.categories.overview'),
},
{
icon: FileText,
title: t('search.items.botConfig'),
description: t('search.items.botConfigDesc'),
path: '/config/bot',
category: t('search.categories.config'),
},
{
icon: Server,
title: t('search.items.modelProvider'),
description: t('search.items.modelProviderDesc'),
path: '/config/modelProvider',
category: t('search.categories.config'),
},
{
icon: Boxes,
title: t('search.items.model'),
description: t('search.items.modelDesc'),
path: '/config/model',
category: t('search.categories.config'),
},
{
icon: Smile,
title: t('search.items.emoji'),
description: t('search.items.emojiDesc'),
path: '/resource/emoji',
category: t('search.categories.resources'),
},
{
icon: MessageSquare,
title: t('search.items.expression'),
description: t('search.items.expressionDesc'),
path: '/resource/expression',
category: t('search.categories.resources'),
},
{
icon: UserCircle,
title: t('search.items.person'),
description: t('search.items.personDesc'),
path: '/resource/person',
category: t('search.categories.resources'),
},
{
icon: Hash,
title: t('search.items.jargon'),
description: t('search.items.jargonDesc'),
path: '/resource/jargon',
category: t('search.categories.resources'),
},
{
icon: BarChart3,
title: t('search.items.statistics'),
description: t('search.items.statisticsDesc'),
path: '/statistics',
category: t('search.categories.monitor'),
},
{
icon: Package,
title: t('search.items.plugins'),
description: t('search.items.pluginsDesc'),
path: '/plugins',
category: t('search.categories.extensions'),
},
{
icon: FileSearch,
title: t('search.items.logs'),
description: t('search.items.logsDesc'),
path: '/logs',
category: t('search.categories.monitor'),
},
{
icon: Settings,
title: t('search.items.settings'),
description: t('search.items.settingsDesc'),
path: '/settings',
category: t('search.categories.system'),
},
], [t])
// 过滤搜索结果
const filteredItems = searchItems.filter(
@@ -154,7 +157,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl p-0 gap-0">
<DialogHeader className="px-4 pt-4 pb-0">
<DialogTitle className="sr-only"></DialogTitle>
<DialogTitle className="sr-only">{t('search.title')}</DialogTitle>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -164,7 +167,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
setSelectedIndex(0)
}}
onKeyDown={handleKeyDown}
placeholder="搜索页面..."
placeholder={t('search.placeholder')}
className="h-12 pl-11 text-base border-0 focus-visible:ring-0 shadow-none"
autoFocus
/>
@@ -207,7 +210,7 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground/50 mb-4" />
<p className="text-sm text-muted-foreground">
{searchQuery ? '未找到匹配的页面' : '输入关键词开始搜索'}
{searchQuery ? t('search.noResults') : t('search.startSearch')}
</p>
</div>
)}
@@ -219,15 +222,15 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded border"></kbd>
<kbd className="px-1.5 py-0.5 bg-muted rounded border"></kbd>
{t('search.navigate')}
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded border">Enter</kbd>
{t('search.select')}
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded border">Esc</kbd>
{t('search.close')}
</span>
</div>
</div>