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,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -59,6 +60,7 @@ export function AuthPage() {
|
||||
const [error, setError] = useState('')
|
||||
const [checkingAuth, setCheckingAuth] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { enableWavesBackground, setEnableWavesBackground } = useAnimation()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
@@ -100,7 +102,7 @@ export function AuthPage() {
|
||||
setError('')
|
||||
|
||||
if (!token.trim()) {
|
||||
setError('请输入 Access Token')
|
||||
setError(t('auth.tokenRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,12 +162,12 @@ export function AuthPage() {
|
||||
}
|
||||
} else {
|
||||
console.error('Token 验证失败:', data.message)
|
||||
setError(data.message || 'Token 验证失败,请检查后重试')
|
||||
setError(data.message || t('auth.verifyFailed'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Token 验证错误:', err)
|
||||
setError(
|
||||
err instanceof Error ? err.message : '连接服务器失败,请检查网络连接'
|
||||
err instanceof Error ? err.message : t('auth.connFailed')
|
||||
)
|
||||
} finally {
|
||||
setIsValidating(false)
|
||||
@@ -177,7 +179,7 @@ export function AuthPage() {
|
||||
return (
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
|
||||
{enableWavesBackground && <WavesBackground />}
|
||||
<div className="text-muted-foreground">正在检查登录状态...</div>
|
||||
<div className="text-muted-foreground">{t('auth.checkingAuth')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -193,7 +195,7 @@ export function AuthPage() {
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="absolute right-4 top-4 rounded-lg p-2 hover:bg-accent transition-colors z-10 text-foreground"
|
||||
title={actualTheme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
|
||||
title={actualTheme === 'dark' ? t('auth.switchToLight') : t('auth.switchToDark')}
|
||||
>
|
||||
{actualTheme === 'dark' ? (
|
||||
<Sun className="h-5 w-5" strokeWidth={2.5} fill="none" />
|
||||
@@ -209,9 +211,9 @@ export function AuthPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="text-2xl font-bold">欢迎使用 MaiBot</CardTitle>
|
||||
<CardTitle className="text-2xl font-bold">{t('auth.welcome')}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
请输入您的 Access Token 以继续访问系统
|
||||
{t('auth.accessDesc')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -228,7 +230,7 @@ export function AuthPage() {
|
||||
<Input
|
||||
id="token"
|
||||
type="password"
|
||||
placeholder="请输入您的 Access Token"
|
||||
placeholder={t('auth.tokenPlaceholder')}
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
className={cn('pl-10', error && 'border-red-500 focus-visible:ring-red-500')}
|
||||
@@ -252,10 +254,10 @@ export function AuthPage() {
|
||||
{isValidating ? (
|
||||
<>
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
验证中...
|
||||
{t('auth.verifyingLabel')}
|
||||
</>
|
||||
) : (
|
||||
'验证并进入'
|
||||
t('auth.verifyEnter')
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -264,17 +266,17 @@ export function AuthPage() {
|
||||
<DialogTrigger asChild>
|
||||
<button className="w-full text-center text-sm text-primary hover:text-primary/80 transition-colors underline-offset-4 hover:underline flex items-center justify-center gap-1">
|
||||
<HelpCircle className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
我没有 Token,我该去哪里获得 Token?
|
||||
{t('auth.helpLink')}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Lock className="h-5 w-5 text-primary" strokeWidth={2} fill="none" />
|
||||
如何获取 Access Token
|
||||
{t('auth.helpTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Access Token 是访问 MaiBot WebUI 的唯一凭证,请按以下方式获取
|
||||
{t('auth.helpDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -284,13 +286,13 @@ export function AuthPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<Terminal className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<h4 className="font-semibold text-sm">方式一:查看启动日志</h4>
|
||||
<h4 className="font-semibold text-sm">{t('auth.method1Title')}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
在 MaiBot 启动时,控制台会显示 WebUI Access Token。
|
||||
{t('auth.method1Desc')}
|
||||
</p>
|
||||
<div className="rounded bg-background p-2 font-mono text-xs">
|
||||
<p className="text-muted-foreground">🔑 WebUI Access Token: abc123...</p>
|
||||
<p className="text-muted-foreground">💡 请使用此 Token 登录 WebUI</p>
|
||||
<p className="text-muted-foreground">{t('auth.method1Example1')}</p>
|
||||
<p className="text-muted-foreground">{t('auth.method1Example2')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,15 +303,15 @@ export function AuthPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<h4 className="font-semibold text-sm">方式二:查看配置文件</h4>
|
||||
<h4 className="font-semibold text-sm">{t('auth.method2Title')}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Token 保存在项目根目录的配置文件中:
|
||||
{t('auth.method2Desc')}
|
||||
</p>
|
||||
<div className="rounded bg-background p-2 font-mono text-xs break-all">
|
||||
<code className="text-primary">data/webui.json</code>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
打开此文件,复制 <code className="px-1 py-0.5 bg-background rounded">access_token</code> 字段的值
|
||||
{t('auth.method2FileHint')} <code className="px-1 py-0.5 bg-background rounded">access_token</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,10 +322,10 @@ export function AuthPage() {
|
||||
<div className="flex gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
|
||||
<div className="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
|
||||
<p className="font-semibold">安全提示</p>
|
||||
<p className="font-semibold">{t('auth.securityTipTitle')}</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
||||
<li>请妥善保管您的 Token,不要泄露给他人</li>
|
||||
<li>如需重置 Token,请在登录后前往系统设置</li>
|
||||
<li>{t('auth.securityTip1')}</li>
|
||||
<li>{t('auth.securityTip2')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,30 +339,30 @@ export function AuthPage() {
|
||||
<AlertDialogTrigger asChild>
|
||||
<button className="w-full text-center text-sm text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline flex items-center justify-center gap-1">
|
||||
<Zap className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
我觉得这个界面很卡怎么办?
|
||||
{t('auth.slowLink')}
|
||||
</button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-primary" strokeWidth={2} fill="none" />
|
||||
关闭背景动画
|
||||
{t('auth.disableAnimTitle')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
背景动画可能会在低性能设备上造成卡顿。关闭动画可以显著提升界面流畅度。
|
||||
{t('auth.disableAnimDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="rounded-lg border bg-muted/50 p-4 space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
关闭动画后,背景将变为纯色,但不影响任何功能的使用。您可以随时在系统设置中重新开启动画。
|
||||
{t('auth.disableAnimDetail')}
|
||||
</p>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => setEnableWavesBackground(false)}
|
||||
>
|
||||
关闭动画
|
||||
{t('auth.disableAnimBtn')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
|
||||
import { APP_NAME, APP_VERSION } from '@/lib/version'
|
||||
@@ -6,6 +8,8 @@ import { cn } from '@/lib/utils'
|
||||
import { LibraryItem } from './LibraryItem'
|
||||
|
||||
export function AboutTab() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* GitHub 开源地址 */}
|
||||
@@ -27,10 +31,10 @@ export function AboutTab() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg sm:text-xl font-bold text-foreground mb-2">
|
||||
开源项目
|
||||
{t('settings.about.openSource')}
|
||||
</h3>
|
||||
<p className="text-sm sm:text-base text-muted-foreground mb-3">
|
||||
本项目在 GitHub 开源,欢迎 Star ⭐ 支持!
|
||||
{t('settings.about.openSourceDesc')}
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/Mai-with-u/MaiBot-Dashboard"
|
||||
@@ -55,7 +59,7 @@ export function AboutTab() {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
前往 GitHub
|
||||
{t('settings.about.visitGitHub')}
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
@@ -76,19 +80,19 @@ export function AboutTab() {
|
||||
|
||||
{/* 应用信息 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">关于 {APP_NAME}</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.aboutApp')} {APP_NAME}</h3>
|
||||
<div className="space-y-2 text-xs sm:text-sm text-muted-foreground">
|
||||
<p>版本: {APP_VERSION}</p>
|
||||
<p>麦麦(MaiBot)的现代化 Web 管理界面</p>
|
||||
<p>{t('settings.about.version')} {APP_VERSION}</p>
|
||||
<p>{t('settings.about.appDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 作者信息 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">作者</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.author')}</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">MaiBot 核心</p>
|
||||
<p className="text-sm font-medium">{t('settings.about.maimaiCore')}</p>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">Mai-with-u</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -100,10 +104,10 @@ export function AboutTab() {
|
||||
|
||||
{/* 技术栈 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">技术栈</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.techStack')}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs sm:text-sm text-muted-foreground">
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-medium text-foreground">前端框架</p>
|
||||
<p className="font-medium text-foreground">{t('settings.about.frontendFramework')}</p>
|
||||
<ul className="space-y-0.5 list-disc list-inside">
|
||||
<li>React 19.2.0</li>
|
||||
<li>TypeScript 5.7.2</li>
|
||||
@@ -112,7 +116,7 @@ export function AboutTab() {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-medium text-foreground">UI 组件</p>
|
||||
<p className="font-medium text-foreground">{t('settings.about.uiComponents')}</p>
|
||||
<ul className="space-y-0.5 list-disc list-inside">
|
||||
<li>shadcn/ui</li>
|
||||
<li>Radix UI</li>
|
||||
@@ -121,7 +125,7 @@ export function AboutTab() {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-medium text-foreground">后端</p>
|
||||
<p className="font-medium text-foreground">{t('settings.about.backend')}</p>
|
||||
<ul className="space-y-0.5 list-disc list-inside">
|
||||
<li>Python 3.12+</li>
|
||||
<li>FastAPI</li>
|
||||
@@ -130,7 +134,7 @@ export function AboutTab() {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-medium text-foreground">构建工具</p>
|
||||
<p className="font-medium text-foreground">{t('settings.about.buildTool')}</p>
|
||||
<ul className="space-y-0.5 list-disc list-inside">
|
||||
<li>Bun / npm</li>
|
||||
<li>ESLint 9.17.0</li>
|
||||
@@ -142,81 +146,81 @@ export function AboutTab() {
|
||||
|
||||
{/* 开源感谢 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">开源库感谢</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.openSourceThanks')}</h3>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground mb-3">
|
||||
本项目使用了以下优秀的开源库,感谢他们的贡献:
|
||||
{t('settings.about.openSourceThanksDesc')}
|
||||
</p>
|
||||
<ScrollArea className="h-[300px] sm:h-[400px]">
|
||||
<div className="space-y-4 pr-4">
|
||||
{/* UI 框架 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">UI 框架与组件</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.uiFrameworkGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="React" description="用户界面构建库" license="MIT" />
|
||||
<LibraryItem name="shadcn/ui" description="优雅的 React 组件库" license="MIT" />
|
||||
<LibraryItem name="Radix UI" description="无样式的可访问组件库" license="MIT" />
|
||||
<LibraryItem name="Tailwind CSS" description="实用优先的 CSS 框架" license="MIT" />
|
||||
<LibraryItem name="Lucide React" description="精美的图标库" license="ISC" />
|
||||
<LibraryItem name="React" description={t('settings.about.lib.react')} license="MIT" />
|
||||
<LibraryItem name="shadcn/ui" description={t('settings.about.lib.shadcn')} license="MIT" />
|
||||
<LibraryItem name="Radix UI" description={t('settings.about.lib.radix')} license="MIT" />
|
||||
<LibraryItem name="Tailwind CSS" description={t('settings.about.lib.tailwind')} license="MIT" />
|
||||
<LibraryItem name="Lucide React" description={t('settings.about.lib.lucide')} license="ISC" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 路由与状态 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">路由与状态管理</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.routingStateGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="TanStack Router" description="类型安全的路由库" license="MIT" />
|
||||
<LibraryItem name="Zustand" description="轻量级状态管理" license="MIT" />
|
||||
<LibraryItem name="TanStack Router" description={t('settings.about.lib.tanstackRouter')} license="MIT" />
|
||||
<LibraryItem name="Zustand" description={t('settings.about.lib.zustand')} license="MIT" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单与验证 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">表单处理</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.formGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="React Hook Form" description="高性能表单库" license="MIT" />
|
||||
<LibraryItem name="Zod" description="TypeScript 优先的 schema 验证" license="MIT" />
|
||||
<LibraryItem name="React Hook Form" description={t('settings.about.lib.reactHookForm')} license="MIT" />
|
||||
<LibraryItem name="Zod" description={t('settings.about.lib.zod')} license="MIT" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工具库 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">工具库</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.utilsGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="clsx" description="条件 className 构建工具" license="MIT" />
|
||||
<LibraryItem name="tailwind-merge" description="Tailwind 类名合并工具" license="MIT" />
|
||||
<LibraryItem name="class-variance-authority" description="组件变体管理" license="Apache-2.0" />
|
||||
<LibraryItem name="date-fns" description="现代化日期处理库" license="MIT" />
|
||||
<LibraryItem name="clsx" description={t('settings.about.lib.clsx')} license="MIT" />
|
||||
<LibraryItem name="tailwind-merge" description={t('settings.about.lib.tailwindMerge')} license="MIT" />
|
||||
<LibraryItem name="class-variance-authority" description={t('settings.about.lib.cva')} license="Apache-2.0" />
|
||||
<LibraryItem name="date-fns" description={t('settings.about.lib.dateFns')} license="MIT" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 动画 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">动画效果</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.animationGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="Framer Motion" description="React 动画库" license="MIT" />
|
||||
<LibraryItem name="vaul" description="抽屉组件动画" license="MIT" />
|
||||
<LibraryItem name="Framer Motion" description={t('settings.about.lib.framerMotion')} license="MIT" />
|
||||
<LibraryItem name="vaul" description={t('settings.about.lib.vaul')} license="MIT" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 后端相关 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">后端框架</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.backendGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="FastAPI" description="现代化 Python Web 框架" license="MIT" />
|
||||
<LibraryItem name="Uvicorn" description="ASGI 服务器" license="BSD-3-Clause" />
|
||||
<LibraryItem name="Pydantic" description="数据验证库" license="MIT" />
|
||||
<LibraryItem name="python-multipart" description="文件上传支持" license="Apache-2.0" />
|
||||
<LibraryItem name="FastAPI" description={t('settings.about.lib.fastapi')} license="MIT" />
|
||||
<LibraryItem name="Uvicorn" description={t('settings.about.lib.uvicorn')} license="BSD-3-Clause" />
|
||||
<LibraryItem name="Pydantic" description={t('settings.about.lib.pydantic')} license="MIT" />
|
||||
<LibraryItem name="python-multipart" description={t('settings.about.lib.pythonMultipart')} license="Apache-2.0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 开发工具 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-foreground">开发工具</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.about.devToolsGroup')}</p>
|
||||
<div className="grid gap-2 text-xs sm:text-sm">
|
||||
<LibraryItem name="TypeScript" description="JavaScript 的超集" license="Apache-2.0" />
|
||||
<LibraryItem name="Vite" description="下一代前端构建工具" license="MIT" />
|
||||
<LibraryItem name="ESLint" description="JavaScript 代码检查工具" license="MIT" />
|
||||
<LibraryItem name="PostCSS" description="CSS 转换工具" license="MIT" />
|
||||
<LibraryItem name="TypeScript" description={t('settings.about.lib.typescript')} license="Apache-2.0" />
|
||||
<LibraryItem name="Vite" description={t('settings.about.lib.vite')} license="MIT" />
|
||||
<LibraryItem name="ESLint" description={t('settings.about.lib.eslint')} license="MIT" />
|
||||
<LibraryItem name="PostCSS" description={t('settings.about.lib.postcss')} license="MIT" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,7 +229,7 @@ export function AboutTab() {
|
||||
|
||||
{/* 许可证 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">开源许可</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.about.openSourceLicense')}</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg bg-primary/5 border border-primary/20 p-3 sm:p-4">
|
||||
<div className="flex items-start gap-2 sm:gap-3">
|
||||
@@ -239,15 +243,13 @@ export function AboutTab() {
|
||||
MaiBot WebUI
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
本项目采用 GNU General Public License v3.0 开源许可证。
|
||||
您可以自由地使用、修改和分发本软件,但必须保持相同的开源许可。
|
||||
{t('settings.about.licenseDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
本项目依赖的所有开源库均遵循各自的开源许可证(MIT、Apache-2.0、BSD 等)。
|
||||
感谢所有开源贡献者的无私奉献。
|
||||
{t('settings.about.licenseDeps')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AlertTriangle, Download, RotateCcw, Trash2, Upload } from 'lucide-react'
|
||||
|
||||
import { useAnimation } from '@/hooks/use-animation'
|
||||
@@ -77,6 +78,7 @@ export function AppearanceTab() {
|
||||
const { theme, setTheme, themeConfig, updateThemeConfig, resolvedTheme, resetTheme } = useTheme()
|
||||
const { enableAnimations, setEnableAnimations, enableWavesBackground, setEnableWavesBackground } = useAnimation()
|
||||
const { toast } = useToast()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [localCSS, setLocalCSS] = useState(themeConfig.customCSS || '')
|
||||
const [cssWarnings, setCssWarnings] = useState<string[]>([])
|
||||
@@ -157,10 +159,10 @@ export function AppearanceTab() {
|
||||
const result = importThemeJSON(json)
|
||||
if (result.success) {
|
||||
// 导入成功后需要刷新页面使配置生效(因为 ThemeProvider 需要重新读取 localStorage)
|
||||
toast({ title: '导入成功', description: '主题配置已导入,页面将自动刷新' })
|
||||
toast({ title: t('settings.appearance.importSuccess'), description: t('settings.appearance.importSuccessDesc') })
|
||||
setTimeout(() => window.location.reload(), 1000)
|
||||
} else {
|
||||
toast({ title: '导入失败', description: result.errors.join('; '), variant: 'destructive' })
|
||||
toast({ title: t('settings.appearance.importFailed'), description: result.errors.join('; '), variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
@@ -172,7 +174,7 @@ export function AppearanceTab() {
|
||||
resetTheme()
|
||||
setLocalCSS('')
|
||||
setCssWarnings([])
|
||||
toast({ title: '重置成功', description: '主题已重置为默认值' })
|
||||
toast({ title: t('settings.appearance.resetSuccess'), description: t('settings.appearance.resetSuccessDesc') })
|
||||
}
|
||||
|
||||
const previewTokens = useMemo(() => {
|
||||
@@ -216,28 +218,28 @@ export function AppearanceTab() {
|
||||
<div className="space-y-6 sm:space-y-8">
|
||||
{/* 主题模式 */}
|
||||
<div>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">主题模式</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.themeMode')}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<ThemeOption
|
||||
value="light"
|
||||
current={theme}
|
||||
onChange={setTheme}
|
||||
label="浅色"
|
||||
description="始终使用浅色主题"
|
||||
label={t('settings.appearance.light')}
|
||||
description={t('settings.appearance.lightDesc')}
|
||||
/>
|
||||
<ThemeOption
|
||||
value="dark"
|
||||
current={theme}
|
||||
onChange={setTheme}
|
||||
label="深色"
|
||||
description="始终使用深色主题"
|
||||
label={t('settings.appearance.dark')}
|
||||
description={t('settings.appearance.darkDesc')}
|
||||
/>
|
||||
<ThemeOption
|
||||
value="system"
|
||||
current={theme}
|
||||
onChange={setTheme}
|
||||
label="跟随系统"
|
||||
description="根据系统设置自动切换"
|
||||
label={t('settings.appearance.system')}
|
||||
description={t('settings.appearance.systemDesc')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +247,7 @@ export function AppearanceTab() {
|
||||
{/* 主题色配置 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3 sm:mb-4">
|
||||
<h3 className="text-base sm:text-lg font-semibold">主题色</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">{t('settings.appearance.accentColor')}</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -254,7 +256,7 @@ export function AppearanceTab() {
|
||||
className="h-8"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||
重置默认
|
||||
{t('settings.appearance.resetDefault')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -271,8 +273,8 @@ export function AppearanceTab() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="accent-color-input" className="font-medium">主色调</Label>
|
||||
<p className="text-xs text-muted-foreground">点击色环选择或输入 HEX 值</p>
|
||||
<Label htmlFor="accent-color-input" className="font-medium">{t('settings.appearance.accentPrimary')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.appearance.accentHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,7 +292,7 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 实时色板预览 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium text-muted-foreground">实时色板预览</h4>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('settings.appearance.colorPreview')}</h4>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-3">
|
||||
<ColorTokenPreview name="primary" value={previewTokens.primary} foreground={previewTokens['primary-foreground']} />
|
||||
<ColorTokenPreview name="secondary" value={previewTokens.secondary} foreground={previewTokens['secondary-foreground']} />
|
||||
@@ -307,13 +309,13 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 样式微调 */}
|
||||
<div>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">界面样式微调</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.styleTweaks')}</h3>
|
||||
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{/* 1. 字体排版 (Typography) */}
|
||||
<AccordionItem value="typography">
|
||||
<AccordionTrigger>字体排版 (Typography)</AccordionTrigger>
|
||||
<AccordionTrigger>{t('settings.appearance.typographyGroup')}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex justify-end">
|
||||
@@ -325,12 +327,12 @@ export function AppearanceTab() {
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||
重置默认
|
||||
{t('settings.appearance.resetDefault')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>字体族 (Font Family)</Label>
|
||||
<Label>{t('settings.appearance.fontFamilyLabel')}</Label>
|
||||
<Select
|
||||
value={(() => {
|
||||
const fontFamily = getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-family-base', '')
|
||||
@@ -351,20 +353,20 @@ export function AppearanceTab() {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择字体族" />
|
||||
<SelectValue placeholder={t('settings.appearance.fontFamilyPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="system">系统默认 (System)</SelectItem>
|
||||
<SelectItem value="sans">无衬线 (Sans-serif)</SelectItem>
|
||||
<SelectItem value="serif">衬线 (Serif)</SelectItem>
|
||||
<SelectItem value="mono">等宽 (Monospace)</SelectItem>
|
||||
<SelectItem value="system">{t('settings.appearance.fontFamilySystem')}</SelectItem>
|
||||
<SelectItem value="sans">{t('settings.appearance.fontFamilySans')}</SelectItem>
|
||||
<SelectItem value="serif">{t('settings.appearance.fontFamilySerif')}</SelectItem>
|
||||
<SelectItem value="mono">{t('settings.appearance.fontFamilyMono')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<Label>基准字体大小 (Base Size)</Label>
|
||||
<Label>{t('settings.appearance.baseFontSize')}</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{parseFloat(getTokenValue(themeConfig.tokenOverrides, 'typography', 'font-size-base', '1')) * 16}px
|
||||
</span>
|
||||
@@ -384,7 +386,7 @@ export function AppearanceTab() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>行高 (Line Height)</Label>
|
||||
<Label>{t('settings.appearance.lineHeight')}</Label>
|
||||
<Select
|
||||
value={String(getTokenValue(themeConfig.tokenOverrides, 'typography', 'line-height-normal', 1.5))}
|
||||
onValueChange={(val) => {
|
||||
@@ -394,12 +396,12 @@ export function AppearanceTab() {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择行高" />
|
||||
<SelectValue placeholder={t('settings.appearance.lineHeightPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1.2">紧凑 (1.2)</SelectItem>
|
||||
<SelectItem value="1.5">正常 (1.5)</SelectItem>
|
||||
<SelectItem value="1.75">宽松 (1.75)</SelectItem>
|
||||
<SelectItem value="1.2">{t('settings.appearance.lineHeightCompact')}</SelectItem>
|
||||
<SelectItem value="1.5">{t('settings.appearance.lineHeightNormal')}</SelectItem>
|
||||
<SelectItem value="1.75">{t('settings.appearance.lineHeightLoose')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -409,7 +411,7 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 2. 视觉效果 (Visual) */}
|
||||
<AccordionItem value="visual">
|
||||
<AccordionTrigger>视觉效果 (Visual)</AccordionTrigger>
|
||||
<AccordionTrigger>{t('settings.appearance.visualGroup')}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex justify-end">
|
||||
@@ -421,13 +423,13 @@ export function AppearanceTab() {
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||
重置默认
|
||||
{t('settings.appearance.resetDefault')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<Label>圆角大小 (Radius)</Label>
|
||||
<Label>{t('settings.appearance.borderRadiusLabel')}</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{Math.round(parseFloat(getTokenValue(themeConfig.tokenOverrides, 'visual', 'radius-md', '0.375')) * 16)}px
|
||||
</span>
|
||||
@@ -447,7 +449,7 @@ export function AppearanceTab() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>阴影强度 (Shadow)</Label>
|
||||
<Label>{t('settings.appearance.shadowLabel')}</Label>
|
||||
<Select
|
||||
value={(() => {
|
||||
const shadowMd = String(getTokenValue(themeConfig.tokenOverrides, 'visual', 'shadow-md', ''))
|
||||
@@ -470,20 +472,20 @@ export function AppearanceTab() {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择阴影强度" />
|
||||
<SelectValue placeholder={t('settings.appearance.shadowPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">无阴影 (None)</SelectItem>
|
||||
<SelectItem value="sm">轻微 (Small)</SelectItem>
|
||||
<SelectItem value="md">中等 (Medium)</SelectItem>
|
||||
<SelectItem value="lg">强烈 (Large)</SelectItem>
|
||||
<SelectItem value="xl">极强 (Extra Large)</SelectItem>
|
||||
<SelectItem value="none">{t('settings.appearance.shadowNone')}</SelectItem>
|
||||
<SelectItem value="sm">{t('settings.appearance.shadowSm')}</SelectItem>
|
||||
<SelectItem value="md">{t('settings.appearance.shadowMd')}</SelectItem>
|
||||
<SelectItem value="lg">{t('settings.appearance.shadowLg')}</SelectItem>
|
||||
<SelectItem value="xl">{t('settings.appearance.shadowXl')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="blur-switch">模糊效果 (Blur)</Label>
|
||||
<Label htmlFor="blur-switch">{t('settings.appearance.blurLabel')}</Label>
|
||||
<Switch
|
||||
id="blur-switch"
|
||||
checked={getTokenValue(themeConfig.tokenOverrides, 'visual', 'blur-md', '0px') !== '0px'}
|
||||
@@ -500,7 +502,7 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 3. 布局 (Layout) */}
|
||||
<AccordionItem value="layout">
|
||||
<AccordionTrigger>布局 (Layout)</AccordionTrigger>
|
||||
<AccordionTrigger>{t('settings.appearance.layoutGroup')}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex justify-end">
|
||||
@@ -512,13 +514,13 @@ export function AppearanceTab() {
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||
重置默认
|
||||
{t('settings.appearance.resetDefault')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<Label>侧边栏宽度 (Sidebar Width)</Label>
|
||||
<Label>{t('settings.appearance.sidebarWidthLabel')}</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'sidebar-width', '16rem')}
|
||||
</span>
|
||||
@@ -539,7 +541,7 @@ export function AppearanceTab() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<Label>内容区最大宽度 (Max Width)</Label>
|
||||
<Label>{t('settings.appearance.maxContentWidth')}</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'max-content-width', '1280px')}
|
||||
</span>
|
||||
@@ -560,7 +562,7 @@ export function AppearanceTab() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<Label>基准间距 (Spacing Unit)</Label>
|
||||
<Label>{t('settings.appearance.spacingUnit')}</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{getTokenValue(themeConfig.tokenOverrides, 'layout', 'space-unit', '0.25rem')}
|
||||
</span>
|
||||
@@ -584,7 +586,7 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 4. 动画 (Animation) */}
|
||||
<AccordionItem value="animation">
|
||||
<AccordionTrigger>动画 (Animation)</AccordionTrigger>
|
||||
<AccordionTrigger>{t('settings.appearance.animationGroup')}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex justify-end">
|
||||
@@ -596,12 +598,12 @@ export function AppearanceTab() {
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||
重置默认
|
||||
{t('settings.appearance.resetDefault')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>动画速度 (Speed)</Label>
|
||||
<Label>{t('settings.appearance.animationSpeedLabel')}</Label>
|
||||
<Select
|
||||
value={(() => {
|
||||
const duration = String(getTokenValue(themeConfig.tokenOverrides, 'animation', 'anim-duration-normal', '300ms'))
|
||||
@@ -629,13 +631,13 @@ export function AppearanceTab() {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择动画速度" />
|
||||
<SelectValue placeholder={t('settings.appearance.animationSpeedPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fast">快速 (100ms)</SelectItem>
|
||||
<SelectItem value="normal">正常 (300ms)</SelectItem>
|
||||
<SelectItem value="slow">慢速 (500ms)</SelectItem>
|
||||
<SelectItem value="off">关闭 (0ms)</SelectItem>
|
||||
<SelectItem value="fast">{t('settings.appearance.animationFast')}</SelectItem>
|
||||
<SelectItem value="normal">{t('settings.appearance.animationNormal')}</SelectItem>
|
||||
<SelectItem value="slow">{t('settings.appearance.animationSlow')}</SelectItem>
|
||||
<SelectItem value="off">{t('settings.appearance.animationOff')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -645,13 +647,13 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 5. 背景设置 (Backgrounds) */}
|
||||
<AccordionItem value="backgrounds">
|
||||
<AccordionTrigger>背景设置 (Backgrounds)</AccordionTrigger>
|
||||
<AccordionTrigger>{t('settings.appearance.backgroundGroup')}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="pt-2">
|
||||
<Tabs defaultValue="page">
|
||||
<TabsList className="w-full grid grid-cols-5">
|
||||
<TabsTrigger value="page">页面</TabsTrigger>
|
||||
<TabsTrigger value="sidebar">侧边栏</TabsTrigger>
|
||||
<TabsTrigger value="page">{t('settings.appearance.bgPage')}</TabsTrigger>
|
||||
<TabsTrigger value="sidebar">{t('settings.appearance.bgSidebar')}</TabsTrigger>
|
||||
<TabsTrigger value="header">Header</TabsTrigger>
|
||||
<TabsTrigger value="card">Card</TabsTrigger>
|
||||
<TabsTrigger value="dialog">Dialog</TabsTrigger>
|
||||
@@ -662,8 +664,8 @@ export function AppearanceTab() {
|
||||
{layerId !== 'page' && (
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/30 px-4 py-3">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-sm font-medium">继承上级背景</Label>
|
||||
<p className="text-xs text-muted-foreground">开启后将使用上级层级的背景配置</p>
|
||||
<Label className="text-sm font-medium">{t('settings.appearance.inheritParentBg')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('settings.appearance.inheritParentBgDesc')}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={bgConfig[layerId]?.inherit ?? false}
|
||||
@@ -696,9 +698,9 @@ export function AppearanceTab() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3 sm:mb-4">
|
||||
<div>
|
||||
<h3 className="text-base sm:text-lg font-semibold">自定义 CSS</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">{t('settings.appearance.customCss')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
编写自定义 CSS 来进一步个性化界面。危险的 CSS(如 @import、url())将被自动过滤。
|
||||
{t('settings.appearance.cssDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -712,7 +714,7 @@ export function AppearanceTab() {
|
||||
disabled={!themeConfig.customCSS}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
清除
|
||||
{t('settings.appearance.clearCss')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -721,7 +723,7 @@ export function AppearanceTab() {
|
||||
value={localCSS}
|
||||
language="css"
|
||||
height="250px"
|
||||
placeholder={`/* 在这里输入自定义 CSS */\n\n/* 例如: */\n/* .sidebar { background: #1a1a2e; } */`}
|
||||
placeholder={t('settings.appearance.cssPlaceholder')}
|
||||
onChange={handleCSSChange}
|
||||
/>
|
||||
|
||||
@@ -729,7 +731,7 @@ export function AppearanceTab() {
|
||||
<div className="rounded-md bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 p-3">
|
||||
<div className="flex items-center gap-2 text-yellow-800 dark:text-yellow-200 text-sm font-medium mb-1">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
以下内容已被安全过滤:
|
||||
{t('settings.appearance.cssWarningTitle')}
|
||||
</div>
|
||||
<ul className="text-xs text-yellow-700 dark:text-yellow-300 space-y-0.5 ml-6 list-disc">
|
||||
{cssWarnings.map((w, i) => <li key={i}>{w}</li>)}
|
||||
@@ -741,17 +743,17 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 动效设置 */}
|
||||
<div>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">动画效果</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.animationEffect')}</h3>
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
{/* 全局动画开关 */}
|
||||
<div className="rounded-lg border bg-card p-3 sm:p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label htmlFor="animations" className="text-base font-medium cursor-pointer">
|
||||
启用动画效果
|
||||
{t('settings.appearance.enableAnimations')}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
关闭后将禁用所有过渡动画和特效,提升性能
|
||||
{t('settings.appearance.enableAnimationsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -767,10 +769,10 @@ export function AppearanceTab() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label htmlFor="waves-background" className="text-base font-medium cursor-pointer">
|
||||
登录页波浪背景
|
||||
{t('settings.appearance.loginWavesBackground')}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
关闭后登录页将使用纯色背景,适合低性能设备
|
||||
{t('settings.appearance.loginWavesBackgroundDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -785,7 +787,7 @@ export function AppearanceTab() {
|
||||
|
||||
{/* 主题导入/导出 */}
|
||||
<div>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">主题导入/导出</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.appearance.importExportTheme')}</h3>
|
||||
<div className="rounded-lg border bg-card p-3 sm:p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
{/* 导出按钮 */}
|
||||
@@ -795,7 +797,7 @@ export function AppearanceTab() {
|
||||
className="gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
导出主题
|
||||
{t('settings.appearance.exportTheme')}
|
||||
</Button>
|
||||
|
||||
{/* 导入按钮 */}
|
||||
@@ -805,7 +807,7 @@ export function AppearanceTab() {
|
||||
className="gap-2"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
导入主题
|
||||
{t('settings.appearance.importTheme')}
|
||||
</Button>
|
||||
|
||||
{/* 重置按钮 */}
|
||||
@@ -816,20 +818,20 @@ export function AppearanceTab() {
|
||||
className="gap-2"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置为默认
|
||||
{t('settings.appearance.resetTheme')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认重置主题</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.appearance.confirmResetTheme')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
这将重置所有主题设置为默认值,包括颜色、字体、布局和自定义 CSS。此操作不可撤销,确定要继续吗?
|
||||
{t('settings.appearance.confirmResetThemeDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleResetTheme}>
|
||||
确认重置
|
||||
{t('settings.appearance.confirmResetAction')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -846,7 +848,7 @@ export function AppearanceTab() {
|
||||
/>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
导出主题为 JSON 文件便于分享或备份,导入时会自动应用所有配置。
|
||||
{t('settings.appearance.exportDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AlertTriangle, Database, Download, HardDrive, RefreshCw, RotateCcw, Trash2, Upload } from 'lucide-react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -14,6 +15,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
||||
|
||||
// 其他设置标签页
|
||||
export function OtherTab() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { toast } = useToast()
|
||||
const [isResetting, setIsResetting] = useState(false)
|
||||
@@ -73,8 +75,8 @@ export function OtherTab() {
|
||||
const handleClearLogCache = () => {
|
||||
logWebSocket.clearLogs()
|
||||
toast({
|
||||
title: '日志已清除',
|
||||
description: '日志缓存已清空',
|
||||
title: t('settings.other.logCleared'),
|
||||
description: t('settings.other.logClearedDesc'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,8 +85,8 @@ export function OtherTab() {
|
||||
const result = clearLocalCache()
|
||||
refreshStorageUsage()
|
||||
toast({
|
||||
title: '缓存已清除',
|
||||
description: `已清除 ${result.clearedKeys.length} 项缓存数据`,
|
||||
title: t('settings.other.cacheCleared'),
|
||||
description: t('settings.other.cacheClearedDesc', { count: result.clearedKeys.length }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,14 +106,14 @@ export function OtherTab() {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast({
|
||||
title: '导出成功',
|
||||
description: '设置已导出为 JSON 文件',
|
||||
title: t('settings.other.exportSuccess'),
|
||||
description: t('settings.other.exportSuccessDesc'),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('导出设置失败:', error)
|
||||
toast({
|
||||
title: '导出失败',
|
||||
description: '无法导出设置',
|
||||
title: t('settings.other.exportFailed'),
|
||||
description: t('settings.other.exportFailedDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -141,29 +143,29 @@ export function OtherTab() {
|
||||
refreshStorageUsage()
|
||||
|
||||
toast({
|
||||
title: '导入成功',
|
||||
description: `成功导入 ${result.imported.length} 项设置${result.skipped.length > 0 ? `,跳过 ${result.skipped.length} 项` : ''}`,
|
||||
title: t('settings.other.importSuccess'),
|
||||
description: t('settings.other.importSuccessDesc', { imported: result.imported.length }) + (result.skipped.length > 0 ? t('settings.other.importSkippedSuffix', { skipped: result.skipped.length }) : ''),
|
||||
})
|
||||
|
||||
// 提示用户刷新页面以应用所有更改
|
||||
if (result.imported.includes('theme') || result.imported.includes('accentColor')) {
|
||||
toast({
|
||||
title: '提示',
|
||||
description: '部分设置需要刷新页面才能完全生效',
|
||||
title: t('settings.other.importRefreshHint'),
|
||||
description: t('settings.other.importRefreshHintDesc'),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: '导入失败',
|
||||
description: '没有有效的设置项可导入',
|
||||
title: t('settings.other.importFailed'),
|
||||
description: t('settings.other.importNoDataDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入设置失败:', error)
|
||||
toast({
|
||||
title: '导入失败',
|
||||
description: '文件格式无效',
|
||||
title: t('settings.other.importFailed'),
|
||||
description: t('settings.other.importInvalidDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -187,8 +189,8 @@ export function OtherTab() {
|
||||
setDataSyncInterval(DEFAULT_SETTINGS.dataSyncInterval)
|
||||
refreshStorageUsage()
|
||||
toast({
|
||||
title: '已重置',
|
||||
description: '所有设置已恢复为默认值,刷新页面以应用更改',
|
||||
title: t('settings.other.resetDone'),
|
||||
description: t('settings.other.resetDoneDesc'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,8 +207,8 @@ export function OtherTab() {
|
||||
|
||||
if (response.ok && data.success) {
|
||||
toast({
|
||||
title: '重置成功',
|
||||
description: '即将进入初次配置向导',
|
||||
title: t('settings.other.resetSuccess'),
|
||||
description: t('settings.other.clearStorageSuccess'),
|
||||
})
|
||||
|
||||
// 延迟跳转到配置向导
|
||||
@@ -215,16 +217,16 @@ export function OtherTab() {
|
||||
}, 1000)
|
||||
} else {
|
||||
toast({
|
||||
title: '重置失败',
|
||||
description: data.message || '无法重置配置状态',
|
||||
title: t('settings.other.resetFailed'),
|
||||
description: data.message || t('settings.other.clearStorageFailed'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重置配置状态错误:', error)
|
||||
toast({
|
||||
title: '重置失败',
|
||||
description: '连接服务器失败',
|
||||
title: t('settings.other.resetFailed'),
|
||||
description: t('settings.other.clearStorageFailed'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -238,7 +240,7 @@ export function OtherTab() {
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4 flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
性能与存储
|
||||
{t('settings.other.performance')}
|
||||
</h3>
|
||||
<div className="space-y-4 sm:space-y-5">
|
||||
{/* 存储使用情况 */}
|
||||
@@ -246,21 +248,21 @@ export function OtherTab() {
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
本地存储使用
|
||||
{t('settings.other.localStorage')}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" onClick={refreshStorageUsage} className="h-7 px-2">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-primary">{formatBytes(storageUsage.used)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{storageUsage.items} 个存储项</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('settings.other.storageItems', { count: storageUsage.items })}</p>
|
||||
</div>
|
||||
|
||||
{/* 日志缓存大小 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">日志缓存大小</Label>
|
||||
<span className="text-sm text-muted-foreground">{logCacheSize} 条</span>
|
||||
<Label className="text-sm font-medium">{t('settings.other.logCache')}</Label>
|
||||
<span className="text-sm text-muted-foreground">{logCacheSize} {t('settings.other.logCacheSizeUnit')}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[logCacheSize]}
|
||||
@@ -271,15 +273,15 @@ export function OtherTab() {
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
控制日志查看器最多缓存的日志条数,较大的值会占用更多内存
|
||||
{t('settings.other.logCacheSizeDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 数据刷新间隔 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">首页数据刷新间隔</Label>
|
||||
<span className="text-sm text-muted-foreground">{dataSyncInterval} 秒</span>
|
||||
<Label className="text-sm font-medium">{t('settings.other.dataSyncIntervalLabel')}</Label>
|
||||
<span className="text-sm text-muted-foreground">{dataSyncInterval} {t('settings.other.dataSyncIntervalUnit')}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[dataSyncInterval]}
|
||||
@@ -290,15 +292,15 @@ export function OtherTab() {
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
控制首页统计数据的自动刷新间隔
|
||||
{t('settings.other.dataSyncIntervalDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* WebSocket 重连间隔 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">WebSocket 重连间隔</Label>
|
||||
<span className="text-sm text-muted-foreground">{wsReconnectInterval / 1000} 秒</span>
|
||||
<Label className="text-sm font-medium">{t('settings.other.wsReconnectLabel')}</Label>
|
||||
<span className="text-sm text-muted-foreground">{wsReconnectInterval / 1000} {t('settings.other.wsReconnectUnit')}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[wsReconnectInterval]}
|
||||
@@ -309,15 +311,15 @@ export function OtherTab() {
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
日志 WebSocket 连接断开后的重连基础间隔
|
||||
{t('settings.other.wsReconnectDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* WebSocket 最大重连次数 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">WebSocket 最大重连次数</Label>
|
||||
<span className="text-sm text-muted-foreground">{wsMaxReconnectAttempts} 次</span>
|
||||
<Label className="text-sm font-medium">{t('settings.other.wsMaxReconnectLabel')}</Label>
|
||||
<span className="text-sm text-muted-foreground">{wsMaxReconnectAttempts} {t('settings.other.wsMaxReconnectUnit')}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[wsMaxReconnectAttempts]}
|
||||
@@ -328,7 +330,7 @@ export function OtherTab() {
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
连接失败后的最大重连尝试次数
|
||||
{t('settings.other.wsMaxReconnectDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -336,27 +338,26 @@ export function OtherTab() {
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onClick={handleClearLogCache} className="gap-2">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
清除日志缓存
|
||||
{t('settings.other.clearLogCacheFn')}
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
清除本地缓存
|
||||
{t('settings.other.clearLocalCache')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认清除本地缓存</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.other.confirmClearCache')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
这将清除所有本地缓存的设置和数据(不包括登录凭证)。
|
||||
您可能需要重新配置部分偏好设置。确定要继续吗?
|
||||
{t('settings.other.confirmClearCacheDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleClearLocalCache}>
|
||||
确认清除
|
||||
{t('settings.other.confirmClear')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -369,11 +370,11 @@ export function OtherTab() {
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4 flex items-center gap-2">
|
||||
<Download className="h-5 w-5" />
|
||||
导入/导出设置
|
||||
{t('settings.other.importExport')}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
导出当前的界面设置以便备份,或从之前导出的文件中恢复设置。
|
||||
{t('settings.other.importExportDesc')}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -384,7 +385,7 @@ export function OtherTab() {
|
||||
className="gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{isExporting ? '导出中...' : '导出设置'}
|
||||
{isExporting ? t('settings.other.exporting') : t('settings.other.exportSettings')}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
@@ -401,7 +402,7 @@ export function OtherTab() {
|
||||
className="gap-2"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
{isImporting ? '导入中...' : '导入设置'}
|
||||
{isImporting ? t('settings.other.importing') : t('settings.other.importSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -411,21 +412,20 @@ export function OtherTab() {
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 text-destructive hover:text-destructive">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置所有设置为默认值
|
||||
{t('settings.other.resetAllSettingsBtn')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认重置所有设置</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.other.confirmResetAll')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
这将把所有界面设置恢复为默认值,包括主题、颜色、动画等偏好设置。
|
||||
此操作不会影响您的登录状态。确定要继续吗?
|
||||
{t('settings.other.confirmResetAllDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleResetAllSettings}>
|
||||
确认重置
|
||||
{t('settings.other.resetAllSettingsConfirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -436,31 +436,31 @@ export function OtherTab() {
|
||||
|
||||
{/* 配置向导 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">配置向导</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.other.configWizard')}</h3>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
重新进行初次配置向导,可以帮助您重新设置系统的基础配置。
|
||||
{t('settings.other.configWizardDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" disabled={isResetting} className="gap-2">
|
||||
<RotateCcw className={cn('h-4 w-4', isResetting && 'animate-spin')} />
|
||||
重新进行初次配置
|
||||
{t('settings.other.rerunSetup')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认重新配置</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.other.confirmRerunSetup')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
这将带您重新进入初次配置向导。您可以重新设置系统的基础配置项。确定要继续吗?
|
||||
{t('settings.other.confirmRerunSetupDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleResetSetup}>
|
||||
确认重置
|
||||
{t('settings.other.resetAllSettingsConfirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -472,36 +472,35 @@ export function OtherTab() {
|
||||
<div className="rounded-lg border border-dashed border-yellow-500/50 bg-yellow-500/5 p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4 flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
开发者工具
|
||||
{t('settings.other.devTools')}
|
||||
</h3>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
以下功能仅供开发调试使用,可能会导致页面崩溃或异常。
|
||||
{t('settings.other.devToolsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="gap-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
触发测试错误
|
||||
{t('settings.other.triggerError')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认触发错误</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.other.confirmTriggerError')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
这将手动触发一个 React 错误,用于测试错误边界组件的显示效果。
|
||||
页面将显示错误界面,您可以通过刷新页面或点击返回首页来恢复。
|
||||
{t('settings.other.confirmTriggerErrorDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => setShouldThrowError(true)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
确认触发
|
||||
{t('settings.other.confirmTrigger')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
export function SecurityTab() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [currentToken, setCurrentToken] = useState('')
|
||||
const [newToken, setNewToken] = useState('')
|
||||
@@ -58,8 +60,8 @@ export function SecurityTab() {
|
||||
const copyToClipboard = async (text: string) => {
|
||||
if (!currentToken) {
|
||||
toast({
|
||||
title: '无法复制',
|
||||
description: 'Token 存储在安全 Cookie 中,请重新生成以获取新 Token',
|
||||
title: t('settings.security.cannotCopy'),
|
||||
description: t('settings.security.cannotCopyDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
@@ -68,14 +70,14 @@ export function SecurityTab() {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
toast({
|
||||
title: '复制成功',
|
||||
description: 'Token 已复制到剪贴板',
|
||||
title: t('settings.security.copySuccess'),
|
||||
description: t('settings.security.copySuccessDesc'),
|
||||
})
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
toast({
|
||||
title: '复制失败',
|
||||
description: '请手动复制 Token',
|
||||
title: t('settings.security.copyFailed'),
|
||||
description: t('settings.security.copyFailedDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -85,8 +87,8 @@ export function SecurityTab() {
|
||||
const handleUpdateToken = async () => {
|
||||
if (!newToken.trim()) {
|
||||
toast({
|
||||
title: '输入错误',
|
||||
description: '请输入新的 Token',
|
||||
title: t('settings.security.inputError'),
|
||||
description: t('settings.security.inputErrorDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
@@ -100,8 +102,8 @@ export function SecurityTab() {
|
||||
.join(', ')
|
||||
|
||||
toast({
|
||||
title: '格式错误',
|
||||
description: `Token 不符合要求: ${failedRules}`,
|
||||
title: t('settings.security.formatError'),
|
||||
description: t('settings.security.formatErrorDesc', { failedRules }),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
@@ -129,8 +131,8 @@ export function SecurityTab() {
|
||||
setCurrentToken(newToken.trim())
|
||||
|
||||
toast({
|
||||
title: '更新成功',
|
||||
description: 'Access Token 已更新,即将跳转到登录页',
|
||||
title: t('settings.security.updateSuccess'),
|
||||
description: t('settings.security.updateSuccessDesc'),
|
||||
})
|
||||
|
||||
// 延迟跳转到登录页
|
||||
@@ -139,16 +141,16 @@ export function SecurityTab() {
|
||||
}, 1500)
|
||||
} else {
|
||||
toast({
|
||||
title: '更新失败',
|
||||
description: data.message || '无法更新 Token',
|
||||
title: t('settings.security.updateFailed'),
|
||||
description: data.message || t('settings.security.updateFailedDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('更新 Token 错误:', err)
|
||||
toast({
|
||||
title: '更新失败',
|
||||
description: '连接服务器失败',
|
||||
title: t('settings.security.updateFailed'),
|
||||
description: t('settings.security.updateFailedConn'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -181,21 +183,21 @@ export function SecurityTab() {
|
||||
setTokenCopied(false)
|
||||
|
||||
toast({
|
||||
title: '生成成功',
|
||||
description: '新的 Access Token 已生成,请及时保存',
|
||||
title: t('settings.security.generateSuccess'),
|
||||
description: t('settings.security.generateSuccessDesc'),
|
||||
})
|
||||
} else {
|
||||
toast({
|
||||
title: '生成失败',
|
||||
description: data.message || '无法生成新 Token',
|
||||
title: t('settings.security.generateFailed'),
|
||||
description: data.message || t('settings.security.generateFailedDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('生成 Token 错误:', err)
|
||||
toast({
|
||||
title: '生成失败',
|
||||
description: '连接服务器失败',
|
||||
title: t('settings.security.generateFailed'),
|
||||
description: t('settings.security.generateFailedConn'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -209,13 +211,13 @@ export function SecurityTab() {
|
||||
await navigator.clipboard.writeText(generatedToken)
|
||||
setTokenCopied(true)
|
||||
toast({
|
||||
title: '复制成功',
|
||||
description: 'Token 已复制到剪贴板',
|
||||
title: t('settings.security.copySuccess'),
|
||||
description: t('settings.security.copySuccessDesc'),
|
||||
})
|
||||
} catch {
|
||||
toast({
|
||||
title: '复制失败',
|
||||
description: '请手动复制 Token',
|
||||
title: t('settings.security.copyFailed'),
|
||||
description: t('settings.security.copyFailedDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -251,10 +253,10 @@ export function SecurityTab() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
新的 Access Token
|
||||
{t('settings.security.dialogTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
这是您的新 Token,请立即保存。关闭此窗口后将跳转到登录页面。
|
||||
{t('settings.security.dialogDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -262,7 +264,7 @@ export function SecurityTab() {
|
||||
{/* Token 显示区域 */}
|
||||
<div className="rounded-lg border-2 border-primary/20 bg-primary/5 p-4">
|
||||
<Label className="text-xs text-muted-foreground mb-2 block">
|
||||
您的新 Token (64位安全令牌)
|
||||
{t('settings.security.dialogTokenLabel')}
|
||||
</Label>
|
||||
<div className="font-mono text-sm break-all select-all bg-background p-3 rounded border">
|
||||
{generatedToken}
|
||||
@@ -274,12 +276,12 @@ export function SecurityTab() {
|
||||
<div className="flex gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
|
||||
<p className="font-semibold">重要提示</p>
|
||||
<p className="font-semibold">{t('settings.security.important')}</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
||||
<li>此 Token 仅显示一次,关闭后无法再查看</li>
|
||||
<li>请立即复制并保存到安全的位置</li>
|
||||
<li>关闭窗口后将自动跳转到登录页面</li>
|
||||
<li>请使用新 Token 重新登录系统</li>
|
||||
<li>{t('settings.security.tip1')}</li>
|
||||
<li>{t('settings.security.tip2')}</li>
|
||||
<li>{t('settings.security.tip3')}</li>
|
||||
<li>{t('settings.security.tip4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,17 +297,17 @@ export function SecurityTab() {
|
||||
{tokenCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
已复制
|
||||
{t('settings.security.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
复制 Token
|
||||
{t('settings.security.copyToken')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
我已保存,关闭
|
||||
{t('settings.security.savedClose')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -313,10 +315,10 @@ export function SecurityTab() {
|
||||
|
||||
{/* 当前 Token */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">当前 Access Token</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.security.currentToken')}</h3>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current-token" className="text-sm">您的访问令牌</Label>
|
||||
<Label htmlFor="current-token" className="text-sm">{t('settings.security.yourToken')}</Label>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
@@ -325,7 +327,7 @@ export function SecurityTab() {
|
||||
value={currentToken || '••••••••••••••••••••••••••••••••'}
|
||||
readOnly
|
||||
className="pr-10 font-mono text-sm"
|
||||
placeholder="Token 存储在安全 Cookie 中"
|
||||
placeholder={t('settings.security.tokenStorePlaceholder')}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -333,13 +335,13 @@ export function SecurityTab() {
|
||||
setShowCurrentToken(!showCurrentToken)
|
||||
} else {
|
||||
toast({
|
||||
title: '无法查看',
|
||||
description: 'Token 存储在安全 Cookie 中,如需新 Token 请点击"重新生成"',
|
||||
title: t('settings.security.cannotView'),
|
||||
description: t('settings.security.cannotViewDesc'),
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 hover:bg-accent rounded"
|
||||
title={showCurrentToken ? '隐藏' : '显示'}
|
||||
title={showCurrentToken ? t('settings.security.hide') : t('settings.security.show')}
|
||||
>
|
||||
{showCurrentToken ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -353,7 +355,7 @@ export function SecurityTab() {
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => copyToClipboard(currentToken)}
|
||||
title="复制到剪贴板"
|
||||
title={t('settings.security.copyTip')}
|
||||
className="flex-shrink-0"
|
||||
disabled={!currentToken}
|
||||
>
|
||||
@@ -371,22 +373,21 @@ export function SecurityTab() {
|
||||
className="gap-2 flex-1 sm:flex-none"
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4', isRegenerating && 'animate-spin')} />
|
||||
<span className="hidden sm:inline">重新生成</span>
|
||||
<span className="sm:hidden">生成</span>
|
||||
<span className="hidden sm:inline">{t('settings.security.regenerate')}</span>
|
||||
<span className="sm:hidden">{t('settings.security.regenerateShort')}</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认重新生成 Token</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('settings.security.confirmRegenerate')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
这将生成一个新的 64 位安全令牌,并使当前 Token 立即失效。
|
||||
您需要使用新 Token 重新登录系统。此操作不可撤销,确定要继续吗?
|
||||
{t('settings.security.confirmRegenerateFullDesc')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t('settings.security.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={executeRegenerateToken}>
|
||||
确认生成
|
||||
{t('settings.security.confirmGenerate')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -394,7 +395,7 @@ export function SecurityTab() {
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||
请妥善保管您的 Access Token,不要泄露给他人
|
||||
{t('settings.security.safekeepTip')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,10 +403,10 @@ export function SecurityTab() {
|
||||
|
||||
{/* 更新 Token */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">自定义 Access Token</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">{t('settings.security.customToken')}</h3>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="new-token" className="text-sm">新的访问令牌</Label>
|
||||
<Label htmlFor="new-token" className="text-sm">{t('settings.security.newTokenLabel')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="new-token"
|
||||
@@ -413,12 +414,12 @@ export function SecurityTab() {
|
||||
value={newToken}
|
||||
onChange={(e) => setNewToken(e.target.value)}
|
||||
className="pr-10 font-mono text-sm"
|
||||
placeholder="输入自定义 Token"
|
||||
placeholder={t('settings.security.customTokenPlaceholder')}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowNewToken(!showNewToken)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 hover:bg-accent rounded"
|
||||
title={showNewToken ? '隐藏' : '显示'}
|
||||
title={showNewToken ? t('settings.security.hide') : t('settings.security.show')}
|
||||
>
|
||||
{showNewToken ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -431,7 +432,7 @@ export function SecurityTab() {
|
||||
{/* Token 验证规则显示 */}
|
||||
{newToken && (
|
||||
<div className="mt-3 space-y-2 p-3 rounded-lg bg-muted/50">
|
||||
<p className="text-sm font-medium text-foreground">Token 安全要求:</p>
|
||||
<p className="text-sm font-medium text-foreground">{t('settings.security.tokenReqTitle')}</p>
|
||||
<div className="space-y-1.5">
|
||||
{tokenValidation.rules.map((rule) => (
|
||||
<div key={rule.id} className="flex items-center gap-2 text-sm">
|
||||
@@ -452,7 +453,7 @@ export function SecurityTab() {
|
||||
<div className="mt-2 pt-2 border-t border-border">
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<Check className="h-4 w-4" />
|
||||
<span className="font-medium">Token 格式正确,可以使用</span>
|
||||
<span className="font-medium">{t('settings.security.tokenValid')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -464,21 +465,21 @@ export function SecurityTab() {
|
||||
disabled={isUpdating || !tokenValidation.isValid || !newToken}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isUpdating ? '更新中...' : '更新自定义 Token'}
|
||||
{isUpdating ? t('settings.security.updating') : t('settings.security.updateBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 安全提示 */}
|
||||
<div className="rounded-lg border border-yellow-200 dark:border-yellow-900 bg-yellow-50 dark:bg-yellow-950/30 p-3 sm:p-4">
|
||||
<h4 className="text-sm sm:text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">安全提示</h4>
|
||||
<h4 className="text-sm sm:text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">{t('settings.security.securityTip')}</h4>
|
||||
<ul className="text-xs sm:text-sm text-yellow-800 dark:text-yellow-300 space-y-1 list-disc list-inside">
|
||||
<li>重新生成 Token 会创建系统随机生成的 64 位安全令牌</li>
|
||||
<li>自定义 Token 必须满足所有安全要求才能使用</li>
|
||||
<li>更新 Token 后,旧的 Token 将立即失效</li>
|
||||
<li>请在安全的环境下查看和复制 Token</li>
|
||||
<li>如果怀疑 Token 泄露,请立即重新生成或更新</li>
|
||||
<li>建议使用系统生成的 Token 以获得最高安全性</li>
|
||||
<li>{t('settings.security.securityTip1')}</li>
|
||||
<li>{t('settings.security.securityTip2')}</li>
|
||||
<li>{t('settings.security.securityTip3')}</li>
|
||||
<li>{t('settings.security.securityTip4')}</li>
|
||||
<li>{t('settings.security.securityTip5')}</li>
|
||||
<li>{t('settings.security.securityTip6')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Info, Palette, Settings, Shield } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
@@ -9,13 +10,14 @@ import { OtherTab } from './OtherTab'
|
||||
import { SecurityTab } from './SecurityTab'
|
||||
|
||||
export function SettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6 p-4 sm:p-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">系统设置</h1>
|
||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">管理您的应用偏好设置</p>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">{t('settings.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">{t('settings.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,19 +26,19 @@ export function SettingsPage() {
|
||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-4 gap-0.5 sm:gap-1 h-auto p-1">
|
||||
<TabsTrigger value="appearance" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
|
||||
<Palette className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
|
||||
<span>外观</span>
|
||||
<span>{t('settings.tabs.appearance')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
|
||||
<Shield className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
|
||||
<span>安全</span>
|
||||
<span>{t('settings.tabs.security')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
|
||||
<Settings className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
|
||||
<span>其他</span>
|
||||
<span>{t('settings.tabs.other')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="about" className="gap-1 sm:gap-2 text-xs sm:text-sm px-2 sm:px-3 py-2">
|
||||
<Info className="h-3.5 w-3.5 sm:h-4 sm:w-4" strokeWidth={2} fill="none" />
|
||||
<span>关于</span>
|
||||
<span>{t('settings.tabs.about')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user