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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user