Selaa lähdekoodia

🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)

- Ran: bun run eslint:fix && bun run lint:fix
- Inserted AGPL license header via eslint-plugin-header
- Enforced no-multiple-empty-lines and other lint rules
- Formatted code using Prettier v3 (@so1ve/prettier-config)
- No functional changes; formatting-only baseline across JS/JSX files
t0ng7u 6 kuukautta sitten
vanhempi
commit
6a87808612
100 muutettua tiedostoa jossa 5096 lisäystä ja 3460 poistoa
  1. 34 26
      web/.eslintrc.cjs
  2. 17 16
      web/index.html
  3. 1 1
      web/postcss.config.js
  4. 1 4
      web/src/App.jsx
  5. 129 93
      web/src/components/auth/LoginForm.jsx
  6. 7 1
      web/src/components/auth/OAuth2Callback.jsx
  7. 65 34
      web/src/components/auth/PasswordResetConfirm.jsx
  8. 53 26
      web/src/components/auth/PasswordResetForm.jsx
  9. 153 97
      web/src/components/auth/RegisterForm.jsx
  10. 56 42
      web/src/components/auth/TwoFAVerification.jsx
  11. 163 41
      web/src/components/common/markdown/MarkdownRenderer.jsx
  12. 10 5
      web/src/components/common/markdown/markdown.css
  13. 37 20
      web/src/components/common/modals/TwoFactorAuthModal.jsx
  14. 23 40
      web/src/components/common/ui/CardPro.jsx
  15. 42 21
      web/src/components/common/ui/CardTable.jsx
  16. 116 60
      web/src/components/common/ui/ChannelKeyDisplay.jsx
  17. 1 1
      web/src/components/common/ui/CompactModeToggle.jsx
  18. 146 116
      web/src/components/common/ui/JSONEditor.jsx
  19. 2 6
      web/src/components/common/ui/Loading.jsx
  20. 1 1
      web/src/components/common/ui/RenderUtils.jsx
  21. 192 170
      web/src/components/common/ui/ScrollableContainer.jsx
  22. 91 61
      web/src/components/common/ui/SelectableButtonGroup.jsx
  23. 43 27
      web/src/components/dashboard/AnnouncementsPanel.jsx
  24. 30 28
      web/src/components/dashboard/ApiInfoPanel.jsx
  25. 46 46
      web/src/components/dashboard/ChartsPanel.jsx
  26. 6 6
      web/src/components/dashboard/DashboardHeader.jsx
  27. 15 8
      web/src/components/dashboard/FaqPanel.jsx
  28. 28 21
      web/src/components/dashboard/StatsCards.jsx
  29. 41 24
      web/src/components/dashboard/UptimePanel.jsx
  30. 54 38
      web/src/components/dashboard/index.jsx
  31. 17 15
      web/src/components/dashboard/modals/SearchModal.jsx
  32. 164 58
      web/src/components/layout/Footer.jsx
  33. 2 6
      web/src/components/layout/HeaderBar/ActionButtons.jsx
  34. 14 14
      web/src/components/layout/HeaderBar/HeaderLogo.jsx
  35. 7 7
      web/src/components/layout/HeaderBar/LanguageSelector.jsx
  36. 11 5
      web/src/components/layout/HeaderBar/MobileMenuButton.jsx
  37. 6 15
      web/src/components/layout/HeaderBar/Navigation.jsx
  38. 11 8
      web/src/components/layout/HeaderBar/NewYearButton.jsx
  39. 5 4
      web/src/components/layout/HeaderBar/NotificationButton.jsx
  40. 9 15
      web/src/components/layout/HeaderBar/SkeletonWrapper.jsx
  41. 36 32
      web/src/components/layout/HeaderBar/ThemeToggle.jsx
  42. 63 51
      web/src/components/layout/HeaderBar/UserArea.jsx
  43. 4 4
      web/src/components/layout/HeaderBar/index.jsx
  44. 87 41
      web/src/components/layout/NoticeModal.jsx
  45. 26 6
      web/src/components/layout/PageLayout.jsx
  46. 5 2
      web/src/components/layout/SetupCheck.js
  47. 46 41
      web/src/components/layout/SiderBar.jsx
  48. 27 30
      web/src/components/playground/ChatArea.jsx
  49. 71 46
      web/src/components/playground/CodeViewer.jsx
  50. 49 47
      web/src/components/playground/ConfigManager.jsx
  51. 19 20
      web/src/components/playground/CustomInputRender.jsx
  52. 39 35
      web/src/components/playground/CustomRequestEditor.jsx
  53. 59 57
      web/src/components/playground/DebugPanel.jsx
  54. 6 10
      web/src/components/playground/FloatingButtons.jsx
  55. 51 43
      web/src/components/playground/ImageUrlInput.jsx
  56. 51 39
      web/src/components/playground/MessageActions.jsx
  57. 114 73
      web/src/components/playground/MessageContent.jsx
  58. 65 47
      web/src/components/playground/OptimizedComponents.js
  59. 105 71
      web/src/components/playground/ParameterControl.jsx
  60. 38 48
      web/src/components/playground/SettingsPanel.jsx
  61. 70 34
      web/src/components/playground/ThinkingContent.jsx
  62. 15 7
      web/src/components/playground/configStorage.js
  63. 1 1
      web/src/components/playground/index.js
  64. 232 206
      web/src/components/settings/ChannelSelectorModal.jsx
  65. 1 1
      web/src/components/settings/ChatsSetting.jsx
  66. 15 9
      web/src/components/settings/DashboardSetting.jsx
  67. 1 1
      web/src/components/settings/DrawingSetting.jsx
  68. 6 2
      web/src/components/settings/PaymentSetting.jsx
  69. 10 12
      web/src/components/settings/PersonalSetting.jsx
  70. 6 21
      web/src/components/settings/RatioSetting.jsx
  71. 35 10
      web/src/components/settings/SystemSetting.jsx
  72. 199 141
      web/src/components/settings/personal/cards/AccountManagement.jsx
  73. 112 72
      web/src/components/settings/personal/cards/ModelsList.jsx
  74. 96 63
      web/src/components/settings/personal/cards/NotificationSettings.jsx
  75. 189 129
      web/src/components/settings/personal/components/TwoFASetting.jsx
  76. 94 53
      web/src/components/settings/personal/components/UserInfoHeader.jsx
  77. 13 11
      web/src/components/settings/personal/modals/AccountDeleteModal.jsx
  78. 23 21
      web/src/components/settings/personal/modals/ChangePasswordModal.jsx
  79. 17 15
      web/src/components/settings/personal/modals/EmailBindModal.jsx
  80. 16 16
      web/src/components/settings/personal/modals/WeChatBindModal.jsx
  81. 24 22
      web/src/components/setup/SetupWizard.jsx
  82. 7 14
      web/src/components/setup/components/StepNavigation.jsx
  83. 11 11
      web/src/components/setup/components/steps/AdminStep.jsx
  84. 20 10
      web/src/components/setup/components/steps/CompleteStep.jsx
  85. 9 7
      web/src/components/setup/components/steps/DatabaseStep.jsx
  86. 4 4
      web/src/components/setup/components/steps/UsageModeStep.jsx
  87. 1 1
      web/src/components/setup/index.jsx
  88. 56 30
      web/src/components/table/channels/ChannelsActions.jsx
  89. 62 70
      web/src/components/table/channels/ChannelsColumnDefs.jsx
  90. 23 23
      web/src/components/table/channels/ChannelsFilters.jsx
  91. 11 9
      web/src/components/table/channels/ChannelsTable.jsx
  92. 18 10
      web/src/components/table/channels/ChannelsTabs.jsx
  93. 2 2
      web/src/components/table/channels/index.jsx
  94. 10 7
      web/src/components/table/channels/modals/BatchTagModal.jsx
  95. 5 10
      web/src/components/table/channels/modals/ColumnSelectorModal.jsx
  96. 369 163
      web/src/components/table/channels/modals/EditChannelModal.jsx
  97. 94 41
      web/src/components/table/channels/modals/EditTagModal.jsx
  98. 108 49
      web/src/components/table/channels/modals/ModelSelectModal.jsx
  99. 116 109
      web/src/components/table/channels/modals/ModelTestModal.jsx
  100. 185 73
      web/src/components/table/channels/modals/MultiKeyManageModal.jsx

+ 34 - 26
web/.eslintrc.cjs

@@ -1,34 +1,42 @@
 module.exports = {
   root: true,
   env: { browser: true, es2021: true, node: true },
-  parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } },
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module',
+    ecmaFeatures: { jsx: true },
+  },
   plugins: ['header', 'react-hooks'],
   overrides: [
     {
       files: ['**/*.{js,jsx}'],
       rules: {
-        'header/header': [2, 'block', [
-          '',
-          'Copyright (C) 2025 QuantumNous',
-          '',
-          'This program is free software: you can redistribute it and/or modify',
-          'it under the terms of the GNU Affero General Public License as',
-          'published by the Free Software Foundation, either version 3 of the',
-          'License, or (at your option) any later version.',
-          '',
-          'This program is distributed in the hope that it will be useful,',
-          'but WITHOUT ANY WARRANTY; without even the implied warranty of',
-          'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the',
-          'GNU Affero General Public License for more details.',
-          '',
-          'You should have received a copy of the GNU Affero General Public License',
-          'along with this program. If not, see <https://www.gnu.org/licenses/>.',
-          '',
-          'For commercial licensing, please contact support@quantumnous.com',
-          ''
-        ]],
-        'no-multiple-empty-lines': ['error', { max: 1 }]
-      }
-    }
-  ]
-}; 
+        'header/header': [
+          2,
+          'block',
+          [
+            '',
+            'Copyright (C) 2025 QuantumNous',
+            '',
+            'This program is free software: you can redistribute it and/or modify',
+            'it under the terms of the GNU Affero General Public License as',
+            'published by the Free Software Foundation, either version 3 of the',
+            'License, or (at your option) any later version.',
+            '',
+            'This program is distributed in the hope that it will be useful,',
+            'but WITHOUT ANY WARRANTY; without even the implied warranty of',
+            'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the',
+            'GNU Affero General Public License for more details.',
+            '',
+            'You should have received a copy of the GNU Affero General Public License',
+            'along with this program. If not, see <https://www.gnu.org/licenses/>.',
+            '',
+            'For commercial licensing, please contact support@quantumnous.com',
+            '',
+          ],
+        ],
+        'no-multiple-empty-lines': ['error', { max: 1 }],
+      },
+    },
+  ],
+};

+ 17 - 16
web/index.html

@@ -1,19 +1,20 @@
 <!doctype html>
 <html lang="zh">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="/logo.png" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#ffffff" />
+    <meta
+      name="description"
+      content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
+    />
+    <title>New API</title>
+  </head>
 
-<head>
-  <meta charset="utf-8" />
-  <link rel="icon" href="/logo.png" />
-  <meta name="viewport" content="width=device-width, initial-scale=1" />
-  <meta name="theme-color" content="#ffffff" />
-  <meta name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" />
-  <title>New API</title>
-</head>
-
-<body>
-  <noscript>You need to enable JavaScript to run this app.</noscript>
-  <div id="root"></div>
-  <script type="module" src="/src/index.jsx"></script>
-</body>
-
-</html>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <script type="module" src="/src/index.jsx"></script>
+  </body>
+</html>

+ 1 - 1
web/postcss.config.js

@@ -22,4 +22,4 @@ export default {
     tailwindcss: {},
     autoprefixer: {},
   },
-}
+};

+ 1 - 4
web/src/App.jsx

@@ -73,10 +73,7 @@ function App() {
             </Suspense>
           }
         />
-        <Route
-          path='/forbidden'
-          element={<Forbidden />}
-        />
+        <Route path='/forbidden' element={<Forbidden />} />
         <Route
           path='/console/models'
           element={

+ 129 - 93
web/src/components/auth/LoginForm.jsx

@@ -31,17 +31,10 @@ import {
   setUserData,
   onGitHubOAuthClicked,
   onOIDCClicked,
-  onLinuxDOOAuthClicked
+  onLinuxDOOAuthClicked,
 } from '../../helpers';
 import Turnstile from 'react-turnstile';
-import {
-  Button,
-  Card,
-  Divider,
-  Form,
-  Icon,
-  Modal,
-} from '@douyinfe/semi-ui';
+import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
 import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 import TelegramLoginButton from 'react-telegram-login';
@@ -77,7 +70,8 @@ const LoginForm = () => {
   const [emailLoginLoading, setEmailLoginLoading] = useState(false);
   const [loginLoading, setLoginLoading] = useState(false);
   const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
-  const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = useState(false);
+  const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] =
+    useState(false);
   const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
   const [showTwoFA, setShowTwoFA] = useState(false);
 
@@ -247,10 +241,7 @@ const LoginForm = () => {
   const handleOIDCClick = () => {
     setOidcLoading(true);
     try {
-      onOIDCClicked(
-        status.oidc_authorization_endpoint,
-        status.oidc_client_id
-      );
+      onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
     } finally {
       // 由于重定向,这里不会执行到,但为了完整性添加
       setTimeout(() => setOidcLoading(false), 3000);
@@ -306,73 +297,87 @@ const LoginForm = () => {
 
   const renderOAuthOptions = () => {
     return (
-      <div className="flex flex-col items-center">
-        <div className="w-full max-w-md">
-          <div className="flex items-center justify-center mb-6 gap-2">
-            <img src={logo} alt="Logo" className="h-10 rounded-full" />
-            <Title heading={3} className='!text-gray-800'>{systemName}</Title>
+      <div className='flex flex-col items-center'>
+        <div className='w-full max-w-md'>
+          <div className='flex items-center justify-center mb-6 gap-2'>
+            <img src={logo} alt='Logo' className='h-10 rounded-full' />
+            <Title heading={3} className='!text-gray-800'>
+              {systemName}
+            </Title>
           </div>
 
-          <Card className="border-0 !rounded-2xl overflow-hidden">
-            <div className="flex justify-center pt-6 pb-2">
-              <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('登 录')}</Title>
+          <Card className='border-0 !rounded-2xl overflow-hidden'>
+            <div className='flex justify-center pt-6 pb-2'>
+              <Title heading={3} className='text-gray-800 dark:text-gray-200'>
+                {t('登 录')}
+              </Title>
             </div>
-            <div className="px-2 py-8">
-              <div className="space-y-3">
+            <div className='px-2 py-8'>
+              <div className='space-y-3'>
                 {status.wechat_login && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
-                    icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
+                    icon={
+                      <Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />
+                    }
                     onClick={onWeChatLoginClicked}
                     loading={wechatLoading}
                   >
-                    <span className="ml-3">{t('使用 微信 继续')}</span>
+                    <span className='ml-3'>{t('使用 微信 继续')}</span>
                   </Button>
                 )}
 
                 {status.github_oauth && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
-                    icon={<IconGithubLogo size="large" />}
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
+                    icon={<IconGithubLogo size='large' />}
                     onClick={handleGitHubClick}
                     loading={githubLoading}
                   >
-                    <span className="ml-3">{t('使用 GitHub 继续')}</span>
+                    <span className='ml-3'>{t('使用 GitHub 继续')}</span>
                   </Button>
                 )}
 
                 {status.oidc_enabled && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
                     icon={<OIDCIcon style={{ color: '#1877F2' }} />}
                     onClick={handleOIDCClick}
                     loading={oidcLoading}
                   >
-                    <span className="ml-3">{t('使用 OIDC 继续')}</span>
+                    <span className='ml-3'>{t('使用 OIDC 继续')}</span>
                   </Button>
                 )}
 
                 {status.linuxdo_oauth && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
-                    icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
+                    icon={
+                      <LinuxDoIcon
+                        style={{
+                          color: '#E95420',
+                          width: '20px',
+                          height: '20px',
+                        }}
+                      />
+                    }
                     onClick={handleLinuxDOClick}
                     loading={linuxdoLoading}
                   >
-                    <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
+                    <span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
                   </Button>
                 )}
 
                 {status.telegram_oauth && (
-                  <div className="flex justify-center my-2">
+                  <div className='flex justify-center my-2'>
                     <TelegramLoginButton
                       dataOnauth={onTelegramLoginClicked}
                       botName={status.telegram_bot_name}
@@ -385,24 +390,24 @@ const LoginForm = () => {
                 </Divider>
 
                 <Button
-                  theme="solid"
-                  type="primary"
-                  className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
-                  icon={<IconMail size="large" />}
+                  theme='solid'
+                  type='primary'
+                  className='w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors'
+                  icon={<IconMail size='large' />}
                   onClick={handleEmailLoginClick}
                   loading={emailLoginLoading}
                 >
-                  <span className="ml-3">{t('使用 邮箱或用户名 登录')}</span>
+                  <span className='ml-3'>{t('使用 邮箱或用户名 登录')}</span>
                 </Button>
               </div>
 
               {!status.self_use_mode_enabled && (
-                <div className="mt-6 text-center text-sm">
+                <div className='mt-6 text-center text-sm'>
                   <Text>
                     {t('没有账户?')}{' '}
                     <Link
-                      to="/register"
-                      className="text-blue-600 hover:text-blue-800 font-medium"
+                      to='/register'
+                      className='text-blue-600 hover:text-blue-800 font-medium'
                     >
                       {t('注册')}
                     </Link>
@@ -418,44 +423,46 @@ const LoginForm = () => {
 
   const renderEmailLoginForm = () => {
     return (
-      <div className="flex flex-col items-center">
-        <div className="w-full max-w-md">
-          <div className="flex items-center justify-center mb-6 gap-2">
-            <img src={logo} alt="Logo" className="h-10 rounded-full" />
+      <div className='flex flex-col items-center'>
+        <div className='w-full max-w-md'>
+          <div className='flex items-center justify-center mb-6 gap-2'>
+            <img src={logo} alt='Logo' className='h-10 rounded-full' />
             <Title heading={3}>{systemName}</Title>
           </div>
 
-          <Card className="border-0 !rounded-2xl overflow-hidden">
-            <div className="flex justify-center pt-6 pb-2">
-              <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('登 录')}</Title>
+          <Card className='border-0 !rounded-2xl overflow-hidden'>
+            <div className='flex justify-center pt-6 pb-2'>
+              <Title heading={3} className='text-gray-800 dark:text-gray-200'>
+                {t('登 录')}
+              </Title>
             </div>
-            <div className="px-2 py-8">
-              <Form className="space-y-3">
+            <div className='px-2 py-8'>
+              <Form className='space-y-3'>
                 <Form.Input
-                  field="username"
+                  field='username'
                   label={t('用户名或邮箱')}
                   placeholder={t('请输入您的用户名或邮箱地址')}
-                  name="username"
+                  name='username'
                   onChange={(value) => handleChange('username', value)}
                   prefix={<IconMail />}
                 />
 
                 <Form.Input
-                  field="password"
+                  field='password'
                   label={t('密码')}
                   placeholder={t('请输入您的密码')}
-                  name="password"
-                  mode="password"
+                  name='password'
+                  mode='password'
                   onChange={(value) => handleChange('password', value)}
                   prefix={<IconLock />}
                 />
 
-                <div className="space-y-2 pt-2">
+                <div className='space-y-2 pt-2'>
                   <Button
-                    theme="solid"
-                    className="w-full !rounded-full"
-                    type="primary"
-                    htmlType="submit"
+                    theme='solid'
+                    className='w-full !rounded-full'
+                    type='primary'
+                    htmlType='submit'
                     onClick={handleSubmit}
                     loading={loginLoading}
                   >
@@ -463,9 +470,9 @@ const LoginForm = () => {
                   </Button>
 
                   <Button
-                    theme="borderless"
+                    theme='borderless'
                     type='tertiary'
-                    className="w-full !rounded-full"
+                    className='w-full !rounded-full'
                     onClick={handleResetPasswordClick}
                     loading={resetPasswordLoading}
                   >
@@ -474,17 +481,21 @@ const LoginForm = () => {
                 </div>
               </Form>
 
-              {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
+              {(status.github_oauth ||
+                status.oidc_enabled ||
+                status.wechat_login ||
+                status.linuxdo_oauth ||
+                status.telegram_oauth) && (
                 <>
                   <Divider margin='12px' align='center'>
                     {t('或')}
                   </Divider>
 
-                  <div className="mt-4 text-center">
+                  <div className='mt-4 text-center'>
                     <Button
-                      theme="outline"
-                      type="tertiary"
-                      className="w-full !rounded-full"
+                      theme='outline'
+                      type='tertiary'
+                      className='w-full !rounded-full'
                       onClick={handleOtherLoginOptionsClick}
                       loading={otherLoginOptionsLoading}
                     >
@@ -495,12 +506,12 @@ const LoginForm = () => {
               )}
 
               {!status.self_use_mode_enabled && (
-                <div className="mt-6 text-center text-sm">
+                <div className='mt-6 text-center text-sm'>
                   <Text>
                     {t('没有账户?')}{' '}
                     <Link
-                      to="/register"
-                      className="text-blue-600 hover:text-blue-800 font-medium"
+                      to='/register'
+                      className='text-blue-600 hover:text-blue-800 font-medium'
                     >
                       {t('注册')}
                     </Link>
@@ -529,21 +540,25 @@ const LoginForm = () => {
           loading: wechatCodeSubmitLoading,
         }}
       >
-        <div className="flex flex-col items-center">
-          <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
+        <div className='flex flex-col items-center'>
+          <img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
         </div>
 
-        <div className="text-center mb-4">
-          <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
+        <div className='text-center mb-4'>
+          <p>
+            {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
+          </p>
         </div>
 
         <Form>
           <Form.Input
-            field="wechat_verification_code"
+            field='wechat_verification_code'
             placeholder={t('验证码')}
             label={t('验证码')}
             value={inputs.wechat_verification_code}
-            onChange={(value) => handleChange('wechat_verification_code', value)}
+            onChange={(value) =>
+              handleChange('wechat_verification_code', value)
+            }
           />
         </Form>
       </Modal>
@@ -555,10 +570,18 @@ const LoginForm = () => {
     return (
       <Modal
         title={
-          <div className="flex items-center">
-            <div className="w-8 h-8 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mr-3">
-              <svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
-                <path fillRule="evenodd" d="M6 8a2 2 0 11-4 0 2 2 0 014 0zM8 7a1 1 0 100 2h8a1 1 0 100-2H8zM6 14a2 2 0 11-4 0 2 2 0 014 0zM8 13a1 1 0 100 2h8a1 1 0 100-2H8z" clipRule="evenodd" />
+          <div className='flex items-center'>
+            <div className='w-8 h-8 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mr-3'>
+              <svg
+                className='w-4 h-4 text-green-600 dark:text-green-400'
+                fill='currentColor'
+                viewBox='0 0 20 20'
+              >
+                <path
+                  fillRule='evenodd'
+                  d='M6 8a2 2 0 11-4 0 2 2 0 014 0zM8 7a1 1 0 100 2h8a1 1 0 100-2H8zM6 14a2 2 0 11-4 0 2 2 0 014 0zM8 13a1 1 0 100 2h8a1 1 0 100-2H8z'
+                  clipRule='evenodd'
+                />
               </svg>
             </div>
             两步验证
@@ -580,19 +603,32 @@ const LoginForm = () => {
   };
 
   return (
-    <div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+    <div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
       {/* 背景模糊晕染球 */}
-      <div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
-      <div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
-      <div className="w-full max-w-sm mt-[60px]">
-        {showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
+      <div
+        className='blur-ball blur-ball-indigo'
+        style={{ top: '-80px', right: '-80px', transform: 'none' }}
+      />
+      <div
+        className='blur-ball blur-ball-teal'
+        style={{ top: '50%', left: '-120px' }}
+      />
+      <div className='w-full max-w-sm mt-[60px]'>
+        {showEmailLogin ||
+        !(
+          status.github_oauth ||
+          status.oidc_enabled ||
+          status.wechat_login ||
+          status.linuxdo_oauth ||
+          status.telegram_oauth
+        )
           ? renderEmailLoginForm()
           : renderOAuthOptions()}
         {renderWeChatLoginModal()}
         {render2FAModal()}
 
         {turnstileEnabled && (
-          <div className="flex justify-center mt-6">
+          <div className='flex justify-center mt-6'>
             <Turnstile
               sitekey={turnstileSiteKey}
               onVerify={(token) => {

+ 7 - 1
web/src/components/auth/OAuth2Callback.jsx

@@ -20,7 +20,13 @@ For commercial licensing, please contact support@quantumnous.com
 import React, { useContext, useEffect } from 'react';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
-import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
+import {
+  API,
+  showError,
+  showSuccess,
+  updateAPI,
+  setUserData,
+} from '../../helpers';
 import { UserContext } from '../../context/User';
 import Loading from '../common/ui/Loading';
 

+ 65 - 34
web/src/components/auth/PasswordResetConfirm.jsx

@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useEffect, useState } from 'react';
-import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
+import {
+  API,
+  copy,
+  showError,
+  showNotice,
+  getLogo,
+  getSystemName,
+} from '../../helpers';
 import { useSearchParams, Link } from 'react-router-dom';
 import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
 import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
@@ -55,7 +62,7 @@ const PasswordResetConfirm = () => {
     if (formApi) {
       formApi.setValues({
         email: email || '',
-        newPassword: newPassword || ''
+        newPassword: newPassword || '',
       });
     }
   }, [searchParams, newPassword, formApi]);
@@ -97,40 +104,53 @@ const PasswordResetConfirm = () => {
   }
 
   return (
-    <div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+    <div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
       {/* 背景模糊晕染球 */}
-      <div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
-      <div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
-      <div className="w-full max-w-sm mt-[60px]">
-        <div className="flex flex-col items-center">
-          <div className="w-full max-w-md">
-            <div className="flex items-center justify-center mb-6 gap-2">
-              <img src={logo} alt="Logo" className="h-10 rounded-full" />
-              <Title heading={3} className='!text-gray-800'>{systemName}</Title>
+      <div
+        className='blur-ball blur-ball-indigo'
+        style={{ top: '-80px', right: '-80px', transform: 'none' }}
+      />
+      <div
+        className='blur-ball blur-ball-teal'
+        style={{ top: '50%', left: '-120px' }}
+      />
+      <div className='w-full max-w-sm mt-[60px]'>
+        <div className='flex flex-col items-center'>
+          <div className='w-full max-w-md'>
+            <div className='flex items-center justify-center mb-6 gap-2'>
+              <img src={logo} alt='Logo' className='h-10 rounded-full' />
+              <Title heading={3} className='!text-gray-800'>
+                {systemName}
+              </Title>
             </div>
 
-            <Card className="border-0 !rounded-2xl overflow-hidden">
-              <div className="flex justify-center pt-6 pb-2">
-                <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置确认')}</Title>
+            <Card className='border-0 !rounded-2xl overflow-hidden'>
+              <div className='flex justify-center pt-6 pb-2'>
+                <Title heading={3} className='text-gray-800 dark:text-gray-200'>
+                  {t('密码重置确认')}
+                </Title>
               </div>
-              <div className="px-2 py-8">
+              <div className='px-2 py-8'>
                 {!isValidResetLink && (
                   <Banner
-                    type="danger"
+                    type='danger'
                     description={t('无效的重置链接,请重新发起密码重置请求')}
-                    className="mb-4 !rounded-lg"
+                    className='mb-4 !rounded-lg'
                     closeIcon={null}
                   />
                 )}
                 <Form
                   getFormApi={(api) => setFormApi(api)}
-                  initValues={{ email: email || '', newPassword: newPassword || '' }}
-                  className="space-y-4"
+                  initValues={{
+                    email: email || '',
+                    newPassword: newPassword || '',
+                  }}
+                  className='space-y-4'
                 >
                   <Form.Input
-                    field="email"
+                    field='email'
                     label={t('邮箱')}
-                    name="email"
+                    name='email'
                     disabled={true}
                     prefix={<IconMail />}
                     placeholder={email ? '' : t('等待获取邮箱信息...')}
@@ -138,19 +158,21 @@ const PasswordResetConfirm = () => {
 
                   {newPassword && (
                     <Form.Input
-                      field="newPassword"
+                      field='newPassword'
                       label={t('新密码')}
-                      name="newPassword"
+                      name='newPassword'
                       disabled={true}
                       prefix={<IconLock />}
                       suffix={
                         <Button
                           icon={<IconCopy />}
-                          type="tertiary"
-                          theme="borderless"
+                          type='tertiary'
+                          theme='borderless'
                           onClick={async () => {
                             await copy(newPassword);
-                            showNotice(`${t('密码已复制到剪贴板:')} ${newPassword}`);
+                            showNotice(
+                              `${t('密码已复制到剪贴板:')} ${newPassword}`,
+                            );
                           }}
                         >
                           {t('复制')}
@@ -159,23 +181,32 @@ const PasswordResetConfirm = () => {
                     />
                   )}
 
-                  <div className="space-y-2 pt-2">
+                  <div className='space-y-2 pt-2'>
                     <Button
-                      theme="solid"
-                      className="w-full !rounded-full"
-                      type="primary"
-                      htmlType="submit"
+                      theme='solid'
+                      className='w-full !rounded-full'
+                      type='primary'
+                      htmlType='submit'
                       onClick={handleSubmit}
                       loading={loading}
-                      disabled={disableButton || newPassword || !isValidResetLink}
+                      disabled={
+                        disableButton || newPassword || !isValidResetLink
+                      }
                     >
                       {newPassword ? t('密码重置完成') : t('确认重置密码')}
                     </Button>
                   </div>
                 </Form>
 
-                <div className="mt-6 text-center text-sm">
-                  <Text><Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('返回登录')}</Link></Text>
+                <div className='mt-6 text-center text-sm'>
+                  <Text>
+                    <Link
+                      to='/login'
+                      className='text-blue-600 hover:text-blue-800 font-medium'
+                    >
+                      {t('返回登录')}
+                    </Link>
+                  </Text>
                 </div>
               </div>
             </Card>

+ 53 - 26
web/src/components/auth/PasswordResetForm.jsx

@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useEffect, useState } from 'react';
-import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../../helpers';
+import {
+  API,
+  getLogo,
+  showError,
+  showInfo,
+  showSuccess,
+  getSystemName,
+} from '../../helpers';
 import Turnstile from 'react-turnstile';
 import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
 import { IconMail } from '@douyinfe/semi-icons';
@@ -97,57 +104,77 @@ const PasswordResetForm = () => {
   }
 
   return (
-    <div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+    <div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
       {/* 背景模糊晕染球 */}
-      <div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
-      <div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
-      <div className="w-full max-w-sm mt-[60px]">
-        <div className="flex flex-col items-center">
-          <div className="w-full max-w-md">
-            <div className="flex items-center justify-center mb-6 gap-2">
-              <img src={logo} alt="Logo" className="h-10 rounded-full" />
-              <Title heading={3} className='!text-gray-800'>{systemName}</Title>
+      <div
+        className='blur-ball blur-ball-indigo'
+        style={{ top: '-80px', right: '-80px', transform: 'none' }}
+      />
+      <div
+        className='blur-ball blur-ball-teal'
+        style={{ top: '50%', left: '-120px' }}
+      />
+      <div className='w-full max-w-sm mt-[60px]'>
+        <div className='flex flex-col items-center'>
+          <div className='w-full max-w-md'>
+            <div className='flex items-center justify-center mb-6 gap-2'>
+              <img src={logo} alt='Logo' className='h-10 rounded-full' />
+              <Title heading={3} className='!text-gray-800'>
+                {systemName}
+              </Title>
             </div>
 
-            <Card className="border-0 !rounded-2xl overflow-hidden">
-              <div className="flex justify-center pt-6 pb-2">
-                <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置')}</Title>
+            <Card className='border-0 !rounded-2xl overflow-hidden'>
+              <div className='flex justify-center pt-6 pb-2'>
+                <Title heading={3} className='text-gray-800 dark:text-gray-200'>
+                  {t('密码重置')}
+                </Title>
               </div>
-              <div className="px-2 py-8">
-                <Form className="space-y-3">
+              <div className='px-2 py-8'>
+                <Form className='space-y-3'>
                   <Form.Input
-                    field="email"
+                    field='email'
                     label={t('邮箱')}
                     placeholder={t('请输入您的邮箱地址')}
-                    name="email"
+                    name='email'
                     value={email}
                     onChange={handleChange}
                     prefix={<IconMail />}
                   />
 
-                  <div className="space-y-2 pt-2">
+                  <div className='space-y-2 pt-2'>
                     <Button
-                      theme="solid"
-                      className="w-full !rounded-full"
-                      type="primary"
-                      htmlType="submit"
+                      theme='solid'
+                      className='w-full !rounded-full'
+                      type='primary'
+                      htmlType='submit'
                       onClick={handleSubmit}
                       loading={loading}
                       disabled={disableButton}
                     >
-                      {disableButton ? `${t('重试')} (${countdown})` : t('提交')}
+                      {disableButton
+                        ? `${t('重试')} (${countdown})`
+                        : t('提交')}
                     </Button>
                   </div>
                 </Form>
 
-                <div className="mt-6 text-center text-sm">
-                  <Text>{t('想起来了?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
+                <div className='mt-6 text-center text-sm'>
+                  <Text>
+                    {t('想起来了?')}{' '}
+                    <Link
+                      to='/login'
+                      className='text-blue-600 hover:text-blue-800 font-medium'
+                    >
+                      {t('登录')}
+                    </Link>
+                  </Text>
                 </div>
               </div>
             </Card>
 
             {turnstileEnabled && (
-              <div className="flex justify-center mt-6">
+              <div className='flex justify-center mt-6'>
                 <Turnstile
                   sitekey={turnstileSiteKey}
                   onVerify={(token) => {

+ 153 - 97
web/src/components/auth/RegisterForm.jsx

@@ -27,20 +27,19 @@ import {
   showSuccess,
   updateAPI,
   getSystemName,
-  setUserData
+  setUserData,
 } from '../../helpers';
 import Turnstile from 'react-turnstile';
-import {
-  Button,
-  Card,
-  Divider,
-  Form,
-  Icon,
-  Modal,
-} from '@douyinfe/semi-ui';
+import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
 import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 import Text from '@douyinfe/semi-ui/lib/es/typography/text';
-import { IconGithubLogo, IconMail, IconUser, IconLock, IconKey } from '@douyinfe/semi-icons';
+import {
+  IconGithubLogo,
+  IconMail,
+  IconUser,
+  IconLock,
+  IconKey,
+} from '@douyinfe/semi-icons';
 import {
   onGitHubOAuthClicked,
   onLinuxDOOAuthClicked,
@@ -78,7 +77,8 @@ const RegisterForm = () => {
   const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
   const [registerLoading, setRegisterLoading] = useState(false);
   const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
-  const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
+  const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] =
+    useState(false);
   const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
   const [disableButton, setDisableButton] = useState(false);
   const [countdown, setCountdown] = useState(30);
@@ -236,10 +236,7 @@ const RegisterForm = () => {
   const handleOIDCClick = () => {
     setOidcLoading(true);
     try {
-      onOIDCClicked(
-        status.oidc_authorization_endpoint,
-        status.oidc_client_id
-      );
+      onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
     } finally {
       setTimeout(() => setOidcLoading(false), 3000);
     }
@@ -303,73 +300,87 @@ const RegisterForm = () => {
 
   const renderOAuthOptions = () => {
     return (
-      <div className="flex flex-col items-center">
-        <div className="w-full max-w-md">
-          <div className="flex items-center justify-center mb-6 gap-2">
-            <img src={logo} alt="Logo" className="h-10 rounded-full" />
-            <Title heading={3} className='!text-gray-800'>{systemName}</Title>
+      <div className='flex flex-col items-center'>
+        <div className='w-full max-w-md'>
+          <div className='flex items-center justify-center mb-6 gap-2'>
+            <img src={logo} alt='Logo' className='h-10 rounded-full' />
+            <Title heading={3} className='!text-gray-800'>
+              {systemName}
+            </Title>
           </div>
 
-          <Card className="border-0 !rounded-2xl overflow-hidden">
-            <div className="flex justify-center pt-6 pb-2">
-              <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
+          <Card className='border-0 !rounded-2xl overflow-hidden'>
+            <div className='flex justify-center pt-6 pb-2'>
+              <Title heading={3} className='text-gray-800 dark:text-gray-200'>
+                {t('注 册')}
+              </Title>
             </div>
-            <div className="px-2 py-8">
-              <div className="space-y-3">
+            <div className='px-2 py-8'>
+              <div className='space-y-3'>
                 {status.wechat_login && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
-                    icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
+                    icon={
+                      <Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />
+                    }
                     onClick={onWeChatLoginClicked}
                     loading={wechatLoading}
                   >
-                    <span className="ml-3">{t('使用 微信 继续')}</span>
+                    <span className='ml-3'>{t('使用 微信 继续')}</span>
                   </Button>
                 )}
 
                 {status.github_oauth && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
-                    icon={<IconGithubLogo size="large" />}
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
+                    icon={<IconGithubLogo size='large' />}
                     onClick={handleGitHubClick}
                     loading={githubLoading}
                   >
-                    <span className="ml-3">{t('使用 GitHub 继续')}</span>
+                    <span className='ml-3'>{t('使用 GitHub 继续')}</span>
                   </Button>
                 )}
 
                 {status.oidc_enabled && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
                     icon={<OIDCIcon style={{ color: '#1877F2' }} />}
                     onClick={handleOIDCClick}
                     loading={oidcLoading}
                   >
-                    <span className="ml-3">{t('使用 OIDC 继续')}</span>
+                    <span className='ml-3'>{t('使用 OIDC 继续')}</span>
                   </Button>
                 )}
 
                 {status.linuxdo_oauth && (
                   <Button
                     theme='outline'
-                    className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
-                    type="tertiary"
-                    icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
+                    className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
+                    type='tertiary'
+                    icon={
+                      <LinuxDoIcon
+                        style={{
+                          color: '#E95420',
+                          width: '20px',
+                          height: '20px',
+                        }}
+                      />
+                    }
                     onClick={handleLinuxDOClick}
                     loading={linuxdoLoading}
                   >
-                    <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
+                    <span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
                   </Button>
                 )}
 
                 {status.telegram_oauth && (
-                  <div className="flex justify-center my-2">
+                  <div className='flex justify-center my-2'>
                     <TelegramLoginButton
                       dataOnauth={onTelegramLoginClicked}
                       botName={status.telegram_bot_name}
@@ -382,19 +393,27 @@ const RegisterForm = () => {
                 </Divider>
 
                 <Button
-                  theme="solid"
-                  type="primary"
-                  className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
-                  icon={<IconMail size="large" />}
+                  theme='solid'
+                  type='primary'
+                  className='w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors'
+                  icon={<IconMail size='large' />}
                   onClick={handleEmailRegisterClick}
                   loading={emailRegisterLoading}
                 >
-                  <span className="ml-3">{t('使用 用户名 注册')}</span>
+                  <span className='ml-3'>{t('使用 用户名 注册')}</span>
                 </Button>
               </div>
 
-              <div className="mt-6 text-center text-sm">
-                <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
+              <div className='mt-6 text-center text-sm'>
+                <Text>
+                  {t('已有账户?')}{' '}
+                  <Link
+                    to='/login'
+                    className='text-blue-600 hover:text-blue-800 font-medium'
+                  >
+                    {t('登录')}
+                  </Link>
+                </Text>
               </div>
             </div>
           </Card>
@@ -405,44 +424,48 @@ const RegisterForm = () => {
 
   const renderEmailRegisterForm = () => {
     return (
-      <div className="flex flex-col items-center">
-        <div className="w-full max-w-md">
-          <div className="flex items-center justify-center mb-6 gap-2">
-            <img src={logo} alt="Logo" className="h-10 rounded-full" />
-            <Title heading={3} className='!text-gray-800'>{systemName}</Title>
+      <div className='flex flex-col items-center'>
+        <div className='w-full max-w-md'>
+          <div className='flex items-center justify-center mb-6 gap-2'>
+            <img src={logo} alt='Logo' className='h-10 rounded-full' />
+            <Title heading={3} className='!text-gray-800'>
+              {systemName}
+            </Title>
           </div>
 
-          <Card className="border-0 !rounded-2xl overflow-hidden">
-            <div className="flex justify-center pt-6 pb-2">
-              <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
+          <Card className='border-0 !rounded-2xl overflow-hidden'>
+            <div className='flex justify-center pt-6 pb-2'>
+              <Title heading={3} className='text-gray-800 dark:text-gray-200'>
+                {t('注 册')}
+              </Title>
             </div>
-            <div className="px-2 py-8">
-              <Form className="space-y-3">
+            <div className='px-2 py-8'>
+              <Form className='space-y-3'>
                 <Form.Input
-                  field="username"
+                  field='username'
                   label={t('用户名')}
                   placeholder={t('请输入用户名')}
-                  name="username"
+                  name='username'
                   onChange={(value) => handleChange('username', value)}
                   prefix={<IconUser />}
                 />
 
                 <Form.Input
-                  field="password"
+                  field='password'
                   label={t('密码')}
                   placeholder={t('输入密码,最短 8 位,最长 20 位')}
-                  name="password"
-                  mode="password"
+                  name='password'
+                  mode='password'
                   onChange={(value) => handleChange('password', value)}
                   prefix={<IconLock />}
                 />
 
                 <Form.Input
-                  field="password2"
+                  field='password2'
                   label={t('确认密码')}
                   placeholder={t('确认密码')}
-                  name="password2"
-                  mode="password"
+                  name='password2'
+                  mode='password'
                   onChange={(value) => handleChange('password2', value)}
                   prefix={<IconLock />}
                 />
@@ -450,11 +473,11 @@ const RegisterForm = () => {
                 {showEmailVerification && (
                   <>
                     <Form.Input
-                      field="email"
+                      field='email'
                       label={t('邮箱')}
                       placeholder={t('输入邮箱地址')}
-                      name="email"
-                      type="email"
+                      name='email'
+                      type='email'
                       onChange={(value) => handleChange('email', value)}
                       prefix={<IconMail />}
                       suffix={
@@ -463,27 +486,31 @@ const RegisterForm = () => {
                           loading={verificationCodeLoading}
                           disabled={disableButton || verificationCodeLoading}
                         >
-                          {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
+                          {disableButton
+                            ? `${t('重新发送')} (${countdown})`
+                            : t('获取验证码')}
                         </Button>
                       }
                     />
                     <Form.Input
-                      field="verification_code"
+                      field='verification_code'
                       label={t('验证码')}
                       placeholder={t('输入验证码')}
-                      name="verification_code"
-                      onChange={(value) => handleChange('verification_code', value)}
+                      name='verification_code'
+                      onChange={(value) =>
+                        handleChange('verification_code', value)
+                      }
                       prefix={<IconKey />}
                     />
                   </>
                 )}
 
-                <div className="space-y-2 pt-2">
+                <div className='space-y-2 pt-2'>
                   <Button
-                    theme="solid"
-                    className="w-full !rounded-full"
-                    type="primary"
-                    htmlType="submit"
+                    theme='solid'
+                    className='w-full !rounded-full'
+                    type='primary'
+                    htmlType='submit'
                     onClick={handleSubmit}
                     loading={registerLoading}
                   >
@@ -492,17 +519,21 @@ const RegisterForm = () => {
                 </div>
               </Form>
 
-              {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
+              {(status.github_oauth ||
+                status.oidc_enabled ||
+                status.wechat_login ||
+                status.linuxdo_oauth ||
+                status.telegram_oauth) && (
                 <>
                   <Divider margin='12px' align='center'>
                     {t('或')}
                   </Divider>
 
-                  <div className="mt-4 text-center">
+                  <div className='mt-4 text-center'>
                     <Button
-                      theme="outline"
-                      type="tertiary"
-                      className="w-full !rounded-full"
+                      theme='outline'
+                      type='tertiary'
+                      className='w-full !rounded-full'
                       onClick={handleOtherRegisterOptionsClick}
                       loading={otherRegisterOptionsLoading}
                     >
@@ -512,8 +543,16 @@ const RegisterForm = () => {
                 </>
               )}
 
-              <div className="mt-6 text-center text-sm">
-                <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
+              <div className='mt-6 text-center text-sm'>
+                <Text>
+                  {t('已有账户?')}{' '}
+                  <Link
+                    to='/login'
+                    className='text-blue-600 hover:text-blue-800 font-medium'
+                  >
+                    {t('登录')}
+                  </Link>
+                </Text>
               </div>
             </div>
           </Card>
@@ -536,21 +575,25 @@ const RegisterForm = () => {
           loading: wechatCodeSubmitLoading,
         }}
       >
-        <div className="flex flex-col items-center">
-          <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
+        <div className='flex flex-col items-center'>
+          <img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
         </div>
 
-        <div className="text-center mb-4">
-          <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
+        <div className='text-center mb-4'>
+          <p>
+            {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
+          </p>
         </div>
 
         <Form>
           <Form.Input
-            field="wechat_verification_code"
+            field='wechat_verification_code'
             placeholder={t('验证码')}
             label={t('验证码')}
             value={inputs.wechat_verification_code}
-            onChange={(value) => handleChange('wechat_verification_code', value)}
+            onChange={(value) =>
+              handleChange('wechat_verification_code', value)
+            }
           />
         </Form>
       </Modal>
@@ -558,18 +601,31 @@ const RegisterForm = () => {
   };
 
   return (
-    <div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+    <div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
       {/* 背景模糊晕染球 */}
-      <div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
-      <div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
-      <div className="w-full max-w-sm mt-[60px]">
-        {showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
+      <div
+        className='blur-ball blur-ball-indigo'
+        style={{ top: '-80px', right: '-80px', transform: 'none' }}
+      />
+      <div
+        className='blur-ball blur-ball-teal'
+        style={{ top: '50%', left: '-120px' }}
+      />
+      <div className='w-full max-w-sm mt-[60px]'>
+        {showEmailRegister ||
+        !(
+          status.github_oauth ||
+          status.oidc_enabled ||
+          status.wechat_login ||
+          status.linuxdo_oauth ||
+          status.telegram_oauth
+        )
           ? renderEmailRegisterForm()
           : renderOAuthOptions()}
         {renderWeChatLoginModal()}
 
         {turnstileEnabled && (
-          <div className="flex justify-center mt-6">
+          <div className='flex justify-center mt-6'>
             <Turnstile
               sitekey={turnstileSiteKey}
               onVerify={(token) => {

+ 56 - 42
web/src/components/auth/TwoFAVerification.jsx

@@ -17,7 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 import { API, showError, showSuccess } from '../../helpers';
-import { Button, Card, Divider, Form, Input, Typography } from '@douyinfe/semi-ui';
+import {
+  Button,
+  Card,
+  Divider,
+  Form,
+  Input,
+  Typography,
+} from '@douyinfe/semi-ui';
 import React, { useState } from 'react';
 
 const { Title, Text, Paragraph } = Typography;
@@ -44,7 +51,7 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
     setLoading(true);
     try {
       const res = await API.post('/api/user/login/2fa', {
-        code: verificationCode
+        code: verificationCode,
       });
 
       if (res.data.success) {
@@ -72,30 +79,30 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
 
   if (isModal) {
     return (
-      <div className="space-y-4">
-        <Paragraph className="text-gray-600 dark:text-gray-300">
+      <div className='space-y-4'>
+        <Paragraph className='text-gray-600 dark:text-gray-300'>
           请输入认证器应用显示的验证码完成登录
         </Paragraph>
 
         <Form onSubmit={handleSubmit}>
           <Form.Input
-            field="code"
-            label={useBackupCode ? "备用码" : "验证码"}
-            placeholder={useBackupCode ? "请输入8位备用码" : "请输入6位验证码"}
+            field='code'
+            label={useBackupCode ? '备用码' : '验证码'}
+            placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
             value={verificationCode}
             onChange={setVerificationCode}
             onKeyPress={handleKeyPress}
-            size="large"
+            size='large'
             style={{ marginBottom: 16 }}
             autoFocus
           />
 
           <Button
-            htmlType="submit"
-            type="primary"
+            htmlType='submit'
+            type='primary'
             loading={loading}
             block
-            size="large"
+            size='large'
             style={{ marginBottom: 16 }}
           >
             验证并登录
@@ -106,8 +113,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
 
         <div style={{ textAlign: 'center' }}>
           <Button
-            theme="borderless"
-            type="tertiary"
+            theme='borderless'
+            type='tertiary'
             onClick={() => {
               setUseBackupCode(!useBackupCode);
               setVerificationCode('');
@@ -119,8 +126,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
 
           {onBack && (
             <Button
-              theme="borderless"
-              type="tertiary"
+              theme='borderless'
+              type='tertiary'
               onClick={onBack}
               style={{ color: '#1890ff', padding: 0 }}
             >
@@ -129,15 +136,14 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
           )}
         </div>
 
-        <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
-          <Text size="small" type="secondary">
+        <div className='bg-gray-50 dark:bg-gray-800 rounded-lg p-3'>
+          <Text size='small' type='secondary'>
             <strong>提示:</strong>
             <br />
             • 验证码每30秒更新一次
             <br />
             • 如果无法获取验证码,请使用备用码
-            <br />
-            • 每个备用码只能使用一次
+            <br />• 每个备用码只能使用一次
           </Text>
         </div>
       </div>
@@ -145,39 +151,41 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
   }
 
   return (
-    <div style={{
-      display: 'flex',
-      justifyContent: 'center',
-      alignItems: 'center',
-      minHeight: '60vh'
-    }}>
+    <div
+      style={{
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+        minHeight: '60vh',
+      }}
+    >
       <Card style={{ width: 400, padding: 24 }}>
         <div style={{ textAlign: 'center', marginBottom: 24 }}>
           <Title heading={3}>两步验证</Title>
-          <Paragraph type="secondary">
+          <Paragraph type='secondary'>
             请输入认证器应用显示的验证码完成登录
           </Paragraph>
         </div>
 
         <Form onSubmit={handleSubmit}>
           <Form.Input
-            field="code"
-            label={useBackupCode ? "备用码" : "验证码"}
-            placeholder={useBackupCode ? "请输入8位备用码" : "请输入6位验证码"}
+            field='code'
+            label={useBackupCode ? '备用码' : '验证码'}
+            placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
             value={verificationCode}
             onChange={setVerificationCode}
             onKeyPress={handleKeyPress}
-            size="large"
+            size='large'
             style={{ marginBottom: 16 }}
             autoFocus
           />
 
           <Button
-            htmlType="submit"
-            type="primary"
+            htmlType='submit'
+            type='primary'
             loading={loading}
             block
-            size="large"
+            size='large'
             style={{ marginBottom: 16 }}
           >
             验证并登录
@@ -188,8 +196,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
 
         <div style={{ textAlign: 'center' }}>
           <Button
-            theme="borderless"
-            type="tertiary"
+            theme='borderless'
+            type='tertiary'
             onClick={() => {
               setUseBackupCode(!useBackupCode);
               setVerificationCode('');
@@ -201,8 +209,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
 
           {onBack && (
             <Button
-              theme="borderless"
-              type="tertiary"
+              theme='borderless'
+              type='tertiary'
               onClick={onBack}
               style={{ color: '#1890ff', padding: 0 }}
             >
@@ -211,15 +219,21 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
           )}
         </div>
 
-        <div style={{ marginTop: 24, padding: 16, background: '#f6f8fa', borderRadius: 6 }}>
-          <Text size="small" type="secondary">
+        <div
+          style={{
+            marginTop: 24,
+            padding: 16,
+            background: '#f6f8fa',
+            borderRadius: 6,
+          }}
+        >
+          <Text size='small' type='secondary'>
             <strong>提示:</strong>
             <br />
             • 验证码每30秒更新一次
             <br />
             • 如果无法获取验证码,请使用备用码
-            <br />
-            • 每个备用码只能使用一次
+            <br />• 每个备用码只能使用一次
           </Text>
         </div>
       </Card>
@@ -227,4 +241,4 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
   );
 };
 
-export default TwoFAVerification;
+export default TwoFAVerification;

+ 163 - 41
web/src/components/common/markdown/MarkdownRenderer.jsx

@@ -160,7 +160,7 @@ export function PreCode(props) {
         }}
       >
         <div
-          className="copy-code-button"
+          className='copy-code-button'
           style={{
             position: 'absolute',
             top: '8px',
@@ -174,14 +174,15 @@ export function PreCode(props) {
         >
           <Tooltip content={t('复制代码')}>
             <Button
-              size="small"
-              theme="borderless"
+              size='small'
+              theme='borderless'
               icon={<IconCopy />}
               onClick={(e) => {
                 e.preventDefault();
                 e.stopPropagation();
                 if (ref.current) {
-                  const code = ref.current.querySelector('code')?.innerText ?? '';
+                  const code =
+                    ref.current.querySelector('code')?.innerText ?? '';
                   copy(code).then((success) => {
                     if (success) {
                       Toast.success(t('代码已复制到剪贴板'));
@@ -217,7 +218,13 @@ export function PreCode(props) {
             backgroundColor: 'var(--semi-color-bg-1)',
           }}
         >
-          <div style={{ marginBottom: '8px', fontSize: '12px', color: 'var(--semi-color-text-2)' }}>
+          <div
+            style={{
+              marginBottom: '8px',
+              fontSize: '12px',
+              color: 'var(--semi-color-text-2)',
+            }}
+          >
             HTML预览:
           </div>
           <div dangerouslySetInnerHTML={{ __html: htmlCode }} />
@@ -258,7 +265,7 @@ function CustomCode(props) {
             justifyContent: 'center',
           }}
         >
-          <Button size="small" onClick={toggleCollapsed} theme="solid">
+          <Button size='small' onClick={toggleCollapsed} theme='solid'>
             {t('显示更多')}
           </Button>
         </div>
@@ -367,7 +374,16 @@ function _MarkdownContent(props) {
       components={{
         pre: PreCode,
         code: CustomCode,
-        p: (pProps) => <p {...pProps} dir="auto" style={{ lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
+        p: (pProps) => (
+          <p
+            {...pProps}
+            dir='auto'
+            style={{
+              lineHeight: '1.6',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
         a: (aProps) => {
           const href = aProps.href || '';
           if (/\.(aac|mp3|opus|wav)$/.test(href)) {
@@ -379,13 +395,16 @@ function _MarkdownContent(props) {
           }
           if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
             return (
-              <video controls style={{ width: '100%', maxWidth: '100%', margin: '12px 0' }}>
+              <video
+                controls
+                style={{ width: '100%', maxWidth: '100%', margin: '12px 0' }}
+              >
                 <source src={href} />
               </video>
             );
           }
           const isInternal = /^\/#/i.test(href);
-          const target = isInternal ? '_self' : aProps.target ?? '_blank';
+          const target = isInternal ? '_self' : (aProps.target ?? '_blank');
           return (
             <a
               {...aProps}
@@ -403,20 +422,84 @@ function _MarkdownContent(props) {
             />
           );
         },
-        h1: (props) => <h1 {...props} style={{ fontSize: '24px', fontWeight: 'bold', margin: '20px 0 12px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
-        h2: (props) => <h2 {...props} style={{ fontSize: '20px', fontWeight: 'bold', margin: '18px 0 10px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
-        h3: (props) => <h3 {...props} style={{ fontSize: '18px', fontWeight: 'bold', margin: '16px 0 8px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
-        h4: (props) => <h4 {...props} style={{ fontSize: '16px', fontWeight: 'bold', margin: '14px 0 6px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
-        h5: (props) => <h5 {...props} style={{ fontSize: '14px', fontWeight: 'bold', margin: '12px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
-        h6: (props) => <h6 {...props} style={{ fontSize: '13px', fontWeight: 'bold', margin: '10px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
+        h1: (props) => (
+          <h1
+            {...props}
+            style={{
+              fontSize: '24px',
+              fontWeight: 'bold',
+              margin: '20px 0 12px 0',
+              color: isUserMessage ? 'white' : 'var(--semi-color-text-0)',
+            }}
+          />
+        ),
+        h2: (props) => (
+          <h2
+            {...props}
+            style={{
+              fontSize: '20px',
+              fontWeight: 'bold',
+              margin: '18px 0 10px 0',
+              color: isUserMessage ? 'white' : 'var(--semi-color-text-0)',
+            }}
+          />
+        ),
+        h3: (props) => (
+          <h3
+            {...props}
+            style={{
+              fontSize: '18px',
+              fontWeight: 'bold',
+              margin: '16px 0 8px 0',
+              color: isUserMessage ? 'white' : 'var(--semi-color-text-0)',
+            }}
+          />
+        ),
+        h4: (props) => (
+          <h4
+            {...props}
+            style={{
+              fontSize: '16px',
+              fontWeight: 'bold',
+              margin: '14px 0 6px 0',
+              color: isUserMessage ? 'white' : 'var(--semi-color-text-0)',
+            }}
+          />
+        ),
+        h5: (props) => (
+          <h5
+            {...props}
+            style={{
+              fontSize: '14px',
+              fontWeight: 'bold',
+              margin: '12px 0 4px 0',
+              color: isUserMessage ? 'white' : 'var(--semi-color-text-0)',
+            }}
+          />
+        ),
+        h6: (props) => (
+          <h6
+            {...props}
+            style={{
+              fontSize: '13px',
+              fontWeight: 'bold',
+              margin: '10px 0 4px 0',
+              color: isUserMessage ? 'white' : 'var(--semi-color-text-0)',
+            }}
+          />
+        ),
         blockquote: (props) => (
           <blockquote
             {...props}
             style={{
-              borderLeft: isUserMessage ? '4px solid rgba(255, 255, 255, 0.5)' : '4px solid var(--semi-color-primary)',
+              borderLeft: isUserMessage
+                ? '4px solid rgba(255, 255, 255, 0.5)'
+                : '4px solid var(--semi-color-primary)',
               paddingLeft: '16px',
               margin: '12px 0',
-              backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.1)' : 'var(--semi-color-fill-0)',
+              backgroundColor: isUserMessage
+                ? 'rgba(255, 255, 255, 0.1)'
+                : 'var(--semi-color-fill-0)',
               padding: '8px 16px',
               borderRadius: '0 4px 4px 0',
               fontStyle: 'italic',
@@ -424,9 +507,36 @@ function _MarkdownContent(props) {
             }}
           />
         ),
-        ul: (props) => <ul {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
-        ol: (props) => <ol {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
-        li: (props) => <li {...props} style={{ margin: '4px 0', lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
+        ul: (props) => (
+          <ul
+            {...props}
+            style={{
+              margin: '8px 0',
+              paddingLeft: '20px',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
+        ol: (props) => (
+          <ol
+            {...props}
+            style={{
+              margin: '8px 0',
+              paddingLeft: '20px',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
+        li: (props) => (
+          <li
+            {...props}
+            style={{
+              margin: '4px 0',
+              lineHeight: '1.6',
+              color: isUserMessage ? 'white' : 'inherit',
+            }}
+          />
+        ),
         table: (props) => (
           <div style={{ overflow: 'auto', margin: '12px 0' }}>
             <table
@@ -434,7 +544,9 @@ function _MarkdownContent(props) {
               style={{
                 width: '100%',
                 borderCollapse: 'collapse',
-                border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
+                border: isUserMessage
+                  ? '1px solid rgba(255, 255, 255, 0.3)'
+                  : '1px solid var(--semi-color-border)',
                 borderRadius: '6px',
                 overflow: 'hidden',
               }}
@@ -446,8 +558,12 @@ function _MarkdownContent(props) {
             {...props}
             style={{
               padding: '8px 12px',
-              backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.2)' : 'var(--semi-color-fill-1)',
-              border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
+              backgroundColor: isUserMessage
+                ? 'rgba(255, 255, 255, 0.2)'
+                : 'var(--semi-color-fill-1)',
+              border: isUserMessage
+                ? '1px solid rgba(255, 255, 255, 0.3)'
+                : '1px solid var(--semi-color-border)',
               fontWeight: 'bold',
               textAlign: 'left',
               color: isUserMessage ? 'white' : 'inherit',
@@ -459,7 +575,9 @@ function _MarkdownContent(props) {
             {...props}
             style={{
               padding: '8px 12px',
-              border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
+              border: isUserMessage
+                ? '1px solid rgba(255, 255, 255, 0.3)'
+                : '1px solid var(--semi-color-border)',
               color: isUserMessage ? 'white' : 'inherit',
             }}
           />
@@ -496,25 +614,29 @@ export function MarkdownRenderer(props) {
         color: 'var(--semi-color-text-0)',
         ...style,
       }}
-      dir="auto"
+      dir='auto'
       {...otherProps}
     >
       {loading ? (
-        <div style={{
-          display: 'flex',
-          alignItems: 'center',
-          gap: '8px',
-          padding: '16px',
-          color: 'var(--semi-color-text-2)',
-        }}>
-          <div style={{
-            width: '16px',
-            height: '16px',
-            border: '2px solid var(--semi-color-border)',
-            borderTop: '2px solid var(--semi-color-primary)',
-            borderRadius: '50%',
-            animation: 'spin 1s linear infinite',
-          }} />
+        <div
+          style={{
+            display: 'flex',
+            alignItems: 'center',
+            gap: '8px',
+            padding: '16px',
+            color: 'var(--semi-color-text-2)',
+          }}
+        >
+          <div
+            style={{
+              width: '16px',
+              height: '16px',
+              border: '2px solid var(--semi-color-border)',
+              borderTop: '2px solid var(--semi-color-primary)',
+              borderRadius: '50%',
+              animation: 'spin 1s linear infinite',
+            }}
+          />
           正在渲染...
         </div>
       ) : (
@@ -529,4 +651,4 @@ export function MarkdownRenderer(props) {
   );
 }
 
-export default MarkdownRenderer; 
+export default MarkdownRenderer;

+ 10 - 5
web/src/components/common/markdown/markdown.css

@@ -59,12 +59,12 @@
 }
 
 .user-message a {
-  color: #87CEEB !important;
+  color: #87ceeb !important;
   /* 浅蓝色链接 */
 }
 
 .user-message a:hover {
-  color: #B0E0E6 !important;
+  color: #b0e0e6 !important;
   /* hover时更浅的蓝色 */
 }
 
@@ -298,7 +298,12 @@ pre:hover .copy-code-button {
 .markdown-body hr {
   border: none;
   height: 1px;
-  background: linear-gradient(to right, transparent, var(--semi-color-border), transparent);
+  background: linear-gradient(
+    to right,
+    transparent,
+    var(--semi-color-border),
+    transparent
+  );
   margin: 24px 0;
 }
 
@@ -332,7 +337,7 @@ pre:hover .copy-code-button {
 }
 
 /* 任务列表样式 */
-.markdown-body input[type="checkbox"] {
+.markdown-body input[type='checkbox'] {
   margin-right: 8px;
   transform: scale(1.1);
 }
@@ -441,4 +446,4 @@ pre:hover .copy-code-button {
 .animate-fade-in {
   animation: fade-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
   will-change: opacity, transform;
-}
+}

+ 37 - 20
web/src/components/common/modals/TwoFactorAuthModal.jsx

@@ -43,7 +43,7 @@ const TwoFactorAuthModal = ({
   onCancel,
   title,
   description,
-  placeholder
+  placeholder,
 }) => {
   const { t } = useTranslation();
 
@@ -56,10 +56,18 @@ const TwoFactorAuthModal = ({
   return (
     <Modal
       title={
-        <div className="flex items-center">
-          <div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3">
-            <svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
-              <path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
+        <div className='flex items-center'>
+          <div className='w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3'>
+            <svg
+              className='w-4 h-4 text-blue-600 dark:text-blue-400'
+              fill='currentColor'
+              viewBox='0 0 20 20'
+            >
+              <path
+                fillRule='evenodd'
+                d='M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
+                clipRule='evenodd'
+              />
             </svg>
           </div>
           {title || t('安全验证')}
@@ -69,11 +77,9 @@ const TwoFactorAuthModal = ({
       onCancel={onCancel}
       footer={
         <>
-          <Button onClick={onCancel}>
-            {t('取消')}
-          </Button>
+          <Button onClick={onCancel}>{t('取消')}</Button>
           <Button
-            type="primary"
+            type='primary'
             loading={loading}
             disabled={!code || loading}
             onClick={onVerify}
@@ -85,18 +91,29 @@ const TwoFactorAuthModal = ({
       width={500}
       style={{ maxWidth: '90vw' }}
     >
-      <div className="space-y-6">
+      <div className='space-y-6'>
         {/* 安全提示 */}
-        <div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-4">
-          <div className="flex items-start">
-            <svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
-              <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
+        <div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-4'>
+          <div className='flex items-start'>
+            <svg
+              className='w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0'
+              fill='currentColor'
+              viewBox='0 0 20 20'
+            >
+              <path
+                fillRule='evenodd'
+                d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
+                clipRule='evenodd'
+              />
             </svg>
             <div>
-              <Typography.Text strong className="text-blue-800 dark:text-blue-200">
+              <Typography.Text
+                strong
+                className='text-blue-800 dark:text-blue-200'
+              >
                 {t('安全验证')}
               </Typography.Text>
-              <Typography.Text className="block text-blue-700 dark:text-blue-300 text-sm mt-1">
+              <Typography.Text className='block text-blue-700 dark:text-blue-300 text-sm mt-1'>
                 {description || t('为了保护账户安全,请验证您的两步验证码。')}
               </Typography.Text>
             </div>
@@ -105,19 +122,19 @@ const TwoFactorAuthModal = ({
 
         {/* 验证码输入 */}
         <div>
-          <Typography.Text strong className="block mb-2">
+          <Typography.Text strong className='block mb-2'>
             {t('验证身份')}
           </Typography.Text>
           <Input
             placeholder={placeholder || t('请输入认证器验证码或备用码')}
             value={code}
             onChange={onCodeChange}
-            size="large"
+            size='large'
             maxLength={8}
             onKeyDown={handleKeyDown}
             autoFocus
           />
-          <Typography.Text type="tertiary" size="small" className="mt-2 block">
+          <Typography.Text type='tertiary' size='small' className='mt-2 block'>
             {t('支持6位TOTP验证码或8位备用码')}
           </Typography.Text>
         </div>
@@ -126,4 +143,4 @@ const TwoFactorAuthModal = ({
   );
 };
 
-export default TwoFactorAuthModal;
+export default TwoFactorAuthModal;

+ 23 - 40
web/src/components/common/ui/CardPro.jsx

@@ -27,15 +27,15 @@ const { Text } = Typography;
 
 /**
  * CardPro 高级卡片组件
- * 
+ *
  * 布局分为6个区域:
  * 1. 统计信息区域 (statsArea)
- * 2. 描述信息区域 (descriptionArea) 
+ * 2. 描述信息区域 (descriptionArea)
  * 3. 类型切换/标签区域 (tabsArea)
  * 4. 操作按钮区域 (actionsArea)
  * 5. 搜索表单区域 (searchArea)
  * 6. 分页区域 (paginationArea) - 固定在卡片底部
- * 
+ *
  * 支持三种布局类型:
  * - type1: 操作型 (如TokensTable) - 描述信息 + 操作按钮 + 搜索表单
  * - type2: 查询型 (如LogsTable) - 统计信息 + 搜索表单
@@ -71,47 +71,38 @@ const CardPro = ({
   const hasMobileHideableContent = actionsArea || searchArea;
 
   const renderHeader = () => {
-    const hasContent = statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
+    const hasContent =
+      statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
     if (!hasContent) return null;
 
     return (
-      <div className="flex flex-col w-full">
+      <div className='flex flex-col w-full'>
         {/* 统计信息区域 - 用于type2 */}
-        {type === 'type2' && statsArea && (
-          <>
-            {statsArea}
-          </>
-        )}
+        {type === 'type2' && statsArea && <>{statsArea}</>}
 
         {/* 描述信息区域 - 用于type1和type3 */}
         {(type === 'type1' || type === 'type3') && descriptionArea && (
-          <>
-            {descriptionArea}
-          </>
+          <>{descriptionArea}</>
         )}
 
         {/* 第一个分隔线 - 在描述信息或统计信息后面 */}
         {((type === 'type1' || type === 'type3') && descriptionArea) ||
-          (type === 'type2' && statsArea) ? (
-          <Divider margin="12px" />
+        (type === 'type2' && statsArea) ? (
+          <Divider margin='12px' />
         ) : null}
 
         {/* 类型切换/标签区域 - 主要用于type3 */}
-        {type === 'type3' && tabsArea && (
-          <>
-            {tabsArea}
-          </>
-        )}
+        {type === 'type3' && tabsArea && <>{tabsArea}</>}
 
         {/* 移动端操作切换按钮 */}
         {isMobile && hasMobileHideableContent && (
           <>
-            <div className="w-full mb-2">
+            <div className='w-full mb-2'>
               <Button
                 onClick={toggleMobileActions}
                 icon={showMobileActions ? <IconEyeClosed /> : <IconEyeOpened />}
-                type="tertiary"
-                size="small"
+                type='tertiary'
+                size='small'
                 theme='outline'
                 block
               >
@@ -126,32 +117,24 @@ const CardPro = ({
           className={`flex flex-col gap-2 ${isMobile && !showMobileActions ? 'hidden' : ''}`}
         >
           {/* 操作按钮区域 - 用于type1和type3 */}
-          {(type === 'type1' || type === 'type3') && actionsArea && (
-            Array.isArray(actionsArea) ? (
+          {(type === 'type1' || type === 'type3') &&
+            actionsArea &&
+            (Array.isArray(actionsArea) ? (
               actionsArea.map((area, idx) => (
                 <React.Fragment key={idx}>
                   {idx !== 0 && <Divider />}
-                  <div className="w-full">
-                    {area}
-                  </div>
+                  <div className='w-full'>{area}</div>
                 </React.Fragment>
               ))
             ) : (
-              <div className="w-full">
-                {actionsArea}
-              </div>
-            )
-          )}
+              <div className='w-full'>{actionsArea}</div>
+            ))}
 
           {/* 当同时存在操作区和搜索区时,插入分隔线 */}
-          {(actionsArea && searchArea) && <Divider />}
+          {actionsArea && searchArea && <Divider />}
 
           {/* 搜索表单区域 - 所有类型都可能有 */}
-          {searchArea && (
-            <div className="w-full">
-              {searchArea}
-            </div>
-          )}
+          {searchArea && <div className='w-full'>{searchArea}</div>}
         </div>
       </div>
     );
@@ -214,4 +197,4 @@ CardPro.propTypes = {
   t: PropTypes.func,
 };
 
-export default CardPro; 
+export default CardPro;

+ 42 - 21
web/src/components/common/ui/CardTable.jsx

@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React, { useState, useEffect, useRef } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Table, Card, Skeleton, Pagination, Empty, Button, Collapsible } from '@douyinfe/semi-ui';
+import {
+  Table,
+  Card,
+  Skeleton,
+  Pagination,
+  Empty,
+  Button,
+  Collapsible,
+} from '@douyinfe/semi-ui';
 import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
 import PropTypes from 'prop-types';
 import { useIsMobile } from '../../../hooks/common/useIsMobile';
@@ -27,7 +35,7 @@ import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTi
 
 /**
  * CardTable 响应式表格组件
- * 
+ *
  * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
  * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
  */
@@ -75,18 +83,22 @@ const CardTable = ({
 
     const renderSkeletonCard = (key) => {
       const placeholder = (
-        <div className="p-2">
+        <div className='p-2'>
           {visibleCols.map((col, idx) => {
             if (!col.title) {
               return (
-                <div key={idx} className="mt-2 flex justify-end">
+                <div key={idx} className='mt-2 flex justify-end'>
                   <Skeleton.Title active style={{ width: 100, height: 24 }} />
                 </div>
               );
             }
 
             return (
-              <div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed" style={{ borderColor: 'var(--semi-color-border)' }}>
+              <div
+                key={idx}
+                className='flex justify-between items-center py-1 border-b last:border-b-0 border-dashed'
+                style={{ borderColor: 'var(--semi-color-border)' }}
+              >
                 <Skeleton.Title active style={{ width: 80, height: 14 }} />
                 <Skeleton.Title
                   active
@@ -103,14 +115,14 @@ const CardTable = ({
       );
 
       return (
-        <Card key={key} className="!rounded-2xl shadow-sm">
+        <Card key={key} className='!rounded-2xl shadow-sm'>
           <Skeleton loading={true} active placeholder={placeholder}></Skeleton>
         </Card>
       );
     };
 
     return (
-      <div className="flex flex-col gap-2">
+      <div className='flex flex-col gap-2'>
         {[1, 2, 3].map((i) => renderSkeletonCard(i))}
       </div>
     );
@@ -127,9 +139,12 @@ const CardTable = ({
       (!tableProps.rowExpandable || tableProps.rowExpandable(record));
 
     return (
-      <Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
+      <Card key={rowKeyVal} className='!rounded-2xl shadow-sm'>
         {columns.map((col, colIdx) => {
-          if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
+          if (
+            tableProps?.visibleColumns &&
+            !tableProps.visibleColumns[col.key]
+          ) {
             return null;
           }
 
@@ -140,7 +155,7 @@ const CardTable = ({
 
           if (!title) {
             return (
-              <div key={col.key || colIdx} className="mt-2 flex justify-end">
+              <div key={col.key || colIdx} className='mt-2 flex justify-end'>
                 {cellContent}
               </div>
             );
@@ -149,14 +164,16 @@ const CardTable = ({
           return (
             <div
               key={col.key || colIdx}
-              className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed"
+              className='flex justify-between items-start py-1 border-b last:border-b-0 border-dashed'
               style={{ borderColor: 'var(--semi-color-border)' }}
             >
-              <span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
+              <span className='font-medium text-gray-600 mr-2 whitespace-nowrap select-none'>
                 {title}
               </span>
-              <div className="flex-1 break-all flex justify-end items-center gap-1">
-                {cellContent !== undefined && cellContent !== null ? cellContent : '-'}
+              <div className='flex-1 break-all flex justify-end items-center gap-1'>
+                {cellContent !== undefined && cellContent !== null
+                  ? cellContent
+                  : '-'}
               </div>
             </div>
           );
@@ -177,7 +194,7 @@ const CardTable = ({
               {showDetails ? t('收起') : t('详情')}
             </Button>
             <Collapsible isOpen={showDetails} keepDOM>
-              <div className="pt-2">
+              <div className='pt-2'>
                 {tableProps.expandedRowRender(record, index)}
               </div>
             </Collapsible>
@@ -190,19 +207,23 @@ const CardTable = ({
   if (isEmpty) {
     if (tableProps.empty) return tableProps.empty;
     return (
-      <div className="flex justify-center p-4">
-        <Empty description="No Data" />
+      <div className='flex justify-center p-4'>
+        <Empty description='No Data' />
       </div>
     );
   }
 
   return (
-    <div className="flex flex-col gap-2">
+    <div className='flex flex-col gap-2'>
       {dataSource.map((record, index) => (
-        <MobileRowCard key={getRowKey(record, index)} record={record} index={index} />
+        <MobileRowCard
+          key={getRowKey(record, index)}
+          record={record}
+          index={index}
+        />
       ))}
       {!hidePagination && tableProps.pagination && dataSource.length > 0 && (
-        <div className="mt-2 flex justify-center">
+        <div className='mt-2 flex justify-center'>
           <Pagination {...tableProps.pagination} />
         </div>
       )}
@@ -218,4 +239,4 @@ CardTable.propTypes = {
   hidePagination: PropTypes.bool,
 };
 
-export default CardTable; 
+export default CardTable;

+ 116 - 60
web/src/components/common/ui/ChannelKeyDisplay.jsx

@@ -30,9 +30,9 @@ import { copy, showSuccess } from '../../../helpers';
  */
 const parseChannelKeys = (keyData, t) => {
   if (!keyData) return [];
-  
+
   const trimmed = keyData.trim();
-  
+
   // 检查是否是JSON数组格式(如Vertex AI)
   if (trimmed.startsWith('[')) {
     try {
@@ -40,9 +40,10 @@ const parseChannelKeys = (keyData, t) => {
       if (Array.isArray(parsed)) {
         return parsed.map((item, index) => ({
           id: index,
-          content: typeof item === 'string' ? item : JSON.stringify(item, null, 2),
+          content:
+            typeof item === 'string' ? item : JSON.stringify(item, null, 2),
           type: typeof item === 'string' ? 'text' : 'json',
-          label: `${t('密钥')} ${index + 1}`
+          label: `${t('密钥')} ${index + 1}`,
         }));
       }
     } catch (e) {
@@ -50,25 +51,27 @@ const parseChannelKeys = (keyData, t) => {
       console.warn('Failed to parse JSON keys:', e);
     }
   }
-  
+
   // 检查是否是多行密钥(按换行符分割)
-  const lines = trimmed.split('\n').filter(line => line.trim());
+  const lines = trimmed.split('\n').filter((line) => line.trim());
   if (lines.length > 1) {
     return lines.map((line, index) => ({
       id: index,
       content: line.trim(),
       type: 'text',
-      label: `${t('密钥')} ${index + 1}`
+      label: `${t('密钥')} ${index + 1}`,
     }));
   }
-  
+
   // 单个密钥
-  return [{
-    id: 0,
-    content: trimmed,
-    type: trimmed.startsWith('{') ? 'json' : 'text',
-    label: t('密钥')
-  }];
+  return [
+    {
+      id: 0,
+      content: trimmed,
+      type: trimmed.startsWith('{') ? 'json' : 'text',
+      label: t('密钥'),
+    },
+  ];
 };
 
 /**
@@ -85,7 +88,7 @@ const ChannelKeyDisplay = ({
   showSuccessIcon = true,
   successText,
   showWarning = true,
-  warningText
+  warningText,
 }) => {
   const { t } = useTranslation();
 
@@ -103,34 +106,42 @@ const ChannelKeyDisplay = ({
   };
 
   return (
-    <div className="space-y-4">
+    <div className='space-y-4'>
       {/* 成功状态 */}
       {showSuccessIcon && (
-        <div className="flex items-center gap-2">
-          <svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
-            <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
+        <div className='flex items-center gap-2'>
+          <svg
+            className='w-5 h-5 text-green-600'
+            fill='currentColor'
+            viewBox='0 0 20 20'
+          >
+            <path
+              fillRule='evenodd'
+              d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
+              clipRule='evenodd'
+            />
           </svg>
-          <Typography.Text strong className="text-green-700">
+          <Typography.Text strong className='text-green-700'>
             {successText || t('验证成功')}
           </Typography.Text>
         </div>
       )}
 
       {/* 密钥内容 */}
-      <div className="space-y-3">
-        <div className="flex items-center justify-between">
+      <div className='space-y-3'>
+        <div className='flex items-center justify-between'>
           <Typography.Text strong>
             {isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')}
           </Typography.Text>
           {isMultipleKeys && (
-            <div className="flex items-center gap-2">
-              <Typography.Text type="tertiary" size="small">
+            <div className='flex items-center gap-2'>
+              <Typography.Text type='tertiary' size='small'>
                 {t('共 {{count}} 个密钥', { count: parsedKeys.length })}
               </Typography.Text>
               <Button
-                size="small"
-                type="primary"
-                theme="outline"
+                size='small'
+                type='primary'
+                theme='outline'
                 onClick={handleCopyAll}
               >
                 {t('复制全部')}
@@ -138,27 +149,40 @@ const ChannelKeyDisplay = ({
             </div>
           )}
         </div>
-        
-        <div className="space-y-3 max-h-80 overflow-auto">
+
+        <div className='space-y-3 max-h-80 overflow-auto'>
           {parsedKeys.map((keyItem) => (
-            <Card key={keyItem.id} className="!rounded-lg !border !border-gray-200 dark:!border-gray-700">
-              <div className="space-y-2">
-                <div className="flex items-center justify-between">
-                  <Typography.Text strong size="small" className="text-gray-700 dark:text-gray-300">
+            <Card
+              key={keyItem.id}
+              className='!rounded-lg !border !border-gray-200 dark:!border-gray-700'
+            >
+              <div className='space-y-2'>
+                <div className='flex items-center justify-between'>
+                  <Typography.Text
+                    strong
+                    size='small'
+                    className='text-gray-700 dark:text-gray-300'
+                  >
                     {keyItem.label}
                   </Typography.Text>
-                  <div className="flex items-center gap-2">
+                  <div className='flex items-center gap-2'>
                     {keyItem.type === 'json' && (
-                      <Tag size="small" color="blue">{t('JSON')}</Tag>
+                      <Tag size='small' color='blue'>
+                        {t('JSON')}
+                      </Tag>
                     )}
                     <Button
-                      size="small"
-                      type="primary"
-                      theme="outline"
+                      size='small'
+                      type='primary'
+                      theme='outline'
                       icon={
-                        <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
-                          <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
-                          <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
+                        <svg
+                          className='w-3 h-3'
+                          fill='currentColor'
+                          viewBox='0 0 20 20'
+                        >
+                          <path d='M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z' />
+                          <path d='M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z' />
                         </svg>
                       }
                       onClick={() => handleCopyKey(keyItem.content)}
@@ -167,18 +191,22 @@ const ChannelKeyDisplay = ({
                     </Button>
                   </div>
                 </div>
-                
-                <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 max-h-40 overflow-auto">
+
+                <div className='bg-gray-50 dark:bg-gray-800 rounded-lg p-3 max-h-40 overflow-auto'>
                   <Typography.Text
                     code
-                    className="text-xs font-mono break-all whitespace-pre-wrap text-gray-800 dark:text-gray-200"
+                    className='text-xs font-mono break-all whitespace-pre-wrap text-gray-800 dark:text-gray-200'
                   >
                     {keyItem.content}
                   </Typography.Text>
                 </div>
-                
+
                 {keyItem.type === 'json' && (
-                  <Typography.Text type="tertiary" size="small" className="block">
+                  <Typography.Text
+                    type='tertiary'
+                    size='small'
+                    className='block'
+                  >
                     {t('JSON格式密钥,请确保格式正确')}
                   </Typography.Text>
                 )}
@@ -186,14 +214,28 @@ const ChannelKeyDisplay = ({
             </Card>
           ))}
         </div>
-        
+
         {isMultipleKeys && (
-          <div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
-            <Typography.Text type="tertiary" size="small" className="text-blue-700 dark:text-blue-300">
-              <svg className="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
-                <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
+          <div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-3'>
+            <Typography.Text
+              type='tertiary'
+              size='small'
+              className='text-blue-700 dark:text-blue-300'
+            >
+              <svg
+                className='w-4 h-4 inline mr-1'
+                fill='currentColor'
+                viewBox='0 0 20 20'
+              >
+                <path
+                  fillRule='evenodd'
+                  d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
+                  clipRule='evenodd'
+                />
               </svg>
-              {t('检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。')}
+              {t(
+                '检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。',
+              )}
             </Typography.Text>
           </div>
         )}
@@ -201,17 +243,31 @@ const ChannelKeyDisplay = ({
 
       {/* 安全警告 */}
       {showWarning && (
-        <div className="bg-yellow-50 dark:bg-yellow-900 rounded-lg p-4">
-          <div className="flex items-start">
-            <svg className="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
-              <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
+        <div className='bg-yellow-50 dark:bg-yellow-900 rounded-lg p-4'>
+          <div className='flex items-start'>
+            <svg
+              className='w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 mr-3 flex-shrink-0'
+              fill='currentColor'
+              viewBox='0 0 20 20'
+            >
+              <path
+                fillRule='evenodd'
+                d='M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z'
+                clipRule='evenodd'
+              />
             </svg>
             <div>
-              <Typography.Text strong className="text-yellow-800 dark:text-yellow-200">
+              <Typography.Text
+                strong
+                className='text-yellow-800 dark:text-yellow-200'
+              >
                 {t('安全提醒')}
               </Typography.Text>
-              <Typography.Text className="block text-yellow-700 dark:text-yellow-300 text-sm mt-1">
-                {warningText || t('请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。')}
+              <Typography.Text className='block text-yellow-700 dark:text-yellow-300 text-sm mt-1'>
+                {warningText ||
+                  t(
+                    '请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。',
+                  )}
               </Typography.Text>
             </div>
           </div>
@@ -221,4 +277,4 @@ const ChannelKeyDisplay = ({
   );
 };
 
-export default ChannelKeyDisplay;
+export default ChannelKeyDisplay;

+ 1 - 1
web/src/components/common/ui/CompactModeToggle.jsx

@@ -65,4 +65,4 @@ CompactModeToggle.propTypes = {
   className: PropTypes.string,
 };
 
-export default CompactModeToggle; 
+export default CompactModeToggle;

+ 146 - 116
web/src/components/common/ui/JSONEditor.jsx

@@ -36,11 +36,7 @@ import {
   Divider,
   Tooltip,
 } from '@douyinfe/semi-ui';
-import {
-  IconPlus,
-  IconDelete,
-  IconAlertTriangle,
-} from '@douyinfe/semi-icons';
+import { IconPlus, IconDelete, IconAlertTriangle } from '@douyinfe/semi-icons';
 
 const { Text } = Typography;
 
@@ -88,7 +84,7 @@ const JSONEditor = ({
   // 将键值对数组转换为对象(重复键时后面的会覆盖前面的)
   const keyValueArrayToObject = useCallback((arr) => {
     const result = {};
-    arr.forEach(item => {
+    arr.forEach((item) => {
       if (item.key) {
         result[item.key] = item.value;
       }
@@ -115,7 +111,8 @@ const JSONEditor = ({
   // 手动模式下的本地文本缓冲
   const [manualText, setManualText] = useState(() => {
     if (typeof value === 'string') return value;
-    if (value && typeof value === 'object') return JSON.stringify(value, null, 2);
+    if (value && typeof value === 'object')
+      return JSON.stringify(value, null, 2);
     return '';
   });
 
@@ -140,7 +137,7 @@ const JSONEditor = ({
     const keyCount = {};
     const duplicates = new Set();
 
-    keyValuePairs.forEach(pair => {
+    keyValuePairs.forEach((pair) => {
       if (pair.key) {
         keyCount[pair.key] = (keyCount[pair.key] || 0) + 1;
         if (keyCount[pair.key] > 1) {
@@ -178,51 +175,65 @@ const JSONEditor = ({
   useEffect(() => {
     if (editMode !== 'manual') {
       if (typeof value === 'string') setManualText(value);
-      else if (value && typeof value === 'object') setManualText(JSON.stringify(value, null, 2));
+      else if (value && typeof value === 'object')
+        setManualText(JSON.stringify(value, null, 2));
       else setManualText('');
     }
   }, [value, editMode]);
 
   // 处理可视化编辑的数据变化
-  const handleVisualChange = useCallback((newPairs) => {
-    setKeyValuePairs(newPairs);
-    const jsonObject = keyValueArrayToObject(newPairs);
-    const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2);
+  const handleVisualChange = useCallback(
+    (newPairs) => {
+      setKeyValuePairs(newPairs);
+      const jsonObject = keyValueArrayToObject(newPairs);
+      const jsonString =
+        Object.keys(jsonObject).length === 0
+          ? ''
+          : JSON.stringify(jsonObject, null, 2);
 
-    setJsonError('');
+      setJsonError('');
 
-    // 通过formApi设置值
-    if (formApi && field) {
-      formApi.setValue(field, jsonString);
-    }
+      // 通过formApi设置值
+      if (formApi && field) {
+        formApi.setValue(field, jsonString);
+      }
 
-    onChange?.(jsonString);
-  }, [onChange, formApi, field, keyValueArrayToObject]);
+      onChange?.(jsonString);
+    },
+    [onChange, formApi, field, keyValueArrayToObject],
+  );
 
   // 处理手动编辑的数据变化
-  const handleManualChange = useCallback((newValue) => {
-    setManualText(newValue);
-    if (newValue && newValue.trim()) {
-      try {
-        const parsed = JSON.parse(newValue);
-        setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
+  const handleManualChange = useCallback(
+    (newValue) => {
+      setManualText(newValue);
+      if (newValue && newValue.trim()) {
+        try {
+          const parsed = JSON.parse(newValue);
+          setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
+          setJsonError('');
+          onChange?.(newValue);
+        } catch (error) {
+          setJsonError(error.message);
+        }
+      } else {
+        setKeyValuePairs([]);
         setJsonError('');
-        onChange?.(newValue);
-      } catch (error) {
-        setJsonError(error.message);
+        onChange?.('');
       }
-    } else {
-      setKeyValuePairs([]);
-      setJsonError('');
-      onChange?.('');
-    }
-  }, [onChange, objectToKeyValueArray, keyValuePairs]);
+    },
+    [onChange, objectToKeyValueArray, keyValuePairs],
+  );
 
   // 切换编辑模式
   const toggleEditMode = useCallback(() => {
     if (editMode === 'visual') {
       const jsonObject = keyValueArrayToObject(keyValuePairs);
-      setManualText(Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2));
+      setManualText(
+        Object.keys(jsonObject).length === 0
+          ? ''
+          : JSON.stringify(jsonObject, null, 2),
+      );
       setEditMode('manual');
     } else {
       try {
@@ -242,12 +253,19 @@ const JSONEditor = ({
         return;
       }
     }
-  }, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]);
+  }, [
+    editMode,
+    value,
+    manualText,
+    keyValuePairs,
+    keyValueArrayToObject,
+    objectToKeyValueArray,
+  ]);
 
   // 添加键值对
   const addKeyValue = useCallback(() => {
     const newPairs = [...keyValuePairs];
-    const existingKeys = newPairs.map(p => p.key);
+    const existingKeys = newPairs.map((p) => p.key);
     let counter = 1;
     let newKey = `field_${counter}`;
     while (existingKeys.includes(newKey)) {
@@ -257,32 +275,41 @@ const JSONEditor = ({
     newPairs.push({
       id: generateUniqueId(),
       key: newKey,
-      value: ''
+      value: '',
     });
     handleVisualChange(newPairs);
   }, [keyValuePairs, handleVisualChange]);
 
   // 删除键值对
-  const removeKeyValue = useCallback((id) => {
-    const newPairs = keyValuePairs.filter(pair => pair.id !== id);
-    handleVisualChange(newPairs);
-  }, [keyValuePairs, handleVisualChange]);
+  const removeKeyValue = useCallback(
+    (id) => {
+      const newPairs = keyValuePairs.filter((pair) => pair.id !== id);
+      handleVisualChange(newPairs);
+    },
+    [keyValuePairs, handleVisualChange],
+  );
 
   // 更新键名
-  const updateKey = useCallback((id, newKey) => {
-    const newPairs = keyValuePairs.map(pair =>
-      pair.id === id ? { ...pair, key: newKey } : pair
-    );
-    handleVisualChange(newPairs);
-  }, [keyValuePairs, handleVisualChange]);
+  const updateKey = useCallback(
+    (id, newKey) => {
+      const newPairs = keyValuePairs.map((pair) =>
+        pair.id === id ? { ...pair, key: newKey } : pair,
+      );
+      handleVisualChange(newPairs);
+    },
+    [keyValuePairs, handleVisualChange],
+  );
 
   // 更新值
-  const updateValue = useCallback((id, newValue) => {
-    const newPairs = keyValuePairs.map(pair =>
-      pair.id === id ? { ...pair, value: newValue } : pair
-    );
-    handleVisualChange(newPairs);
-  }, [keyValuePairs, handleVisualChange]);
+  const updateValue = useCallback(
+    (id, newValue) => {
+      const newPairs = keyValuePairs.map((pair) =>
+        pair.id === id ? { ...pair, value: newValue } : pair,
+      );
+      handleVisualChange(newPairs);
+    },
+    [keyValuePairs, handleVisualChange],
+  );
 
   // 填入模板
   const fillTemplate = useCallback(() => {
@@ -298,7 +325,14 @@ const JSONEditor = ({
       onChange?.(templateString);
       setJsonError('');
     }
-  }, [template, onChange, formApi, field, objectToKeyValueArray, keyValuePairs]);
+  }, [
+    template,
+    onChange,
+    formApi,
+    field,
+    objectToKeyValueArray,
+    keyValuePairs,
+  ]);
 
   // 渲染值输入控件(支持嵌套)
   const renderValueInput = (pairId, value) => {
@@ -306,12 +340,12 @@ const JSONEditor = ({
 
     if (valueType === 'boolean') {
       return (
-        <div className="flex items-center">
+        <div className='flex items-center'>
           <Switch
             checked={value}
             onChange={(newValue) => updateValue(pairId, newValue)}
           />
-          <Text type="tertiary" className="ml-2">
+          <Text type='tertiary' className='ml-2'>
             {value ? t('true') : t('false')}
           </Text>
         </div>
@@ -373,29 +407,29 @@ const JSONEditor = ({
   // 渲染键值对编辑器
   const renderKeyValueEditor = () => {
     return (
-      <div className="space-y-1">
+      <div className='space-y-1'>
         {/* 重复键警告 */}
         {duplicateKeys.size > 0 && (
           <Banner
-            type="warning"
+            type='warning'
             icon={<IconAlertTriangle />}
             description={
               <div>
                 <Text strong>{t('存在重复的键名:')}</Text>
                 <Text>{Array.from(duplicateKeys).join(', ')}</Text>
                 <br />
-                <Text type="tertiary" size="small">
+                <Text type='tertiary' size='small'>
                   {t('注意:JSON中重复的键只会保留最后一个同名键的值')}
                 </Text>
               </div>
             }
-            className="mb-3"
+            className='mb-3'
           />
         )}
 
         {keyValuePairs.length === 0 && (
-          <div className="text-center py-6 px-4">
-            <Text type="tertiary" className="text-gray-500 text-sm">
+          <div className='text-center py-6 px-4'>
+            <Text type='tertiary' className='text-gray-500 text-sm'>
               {t('暂无数据,点击下方按钮添加键值对')}
             </Text>
           </div>
@@ -403,13 +437,14 @@ const JSONEditor = ({
 
         {keyValuePairs.map((pair, index) => {
           const isDuplicate = duplicateKeys.has(pair.key);
-          const isLastDuplicate = isDuplicate &&
-            keyValuePairs.slice(index + 1).every(p => p.key !== pair.key);
+          const isLastDuplicate =
+            isDuplicate &&
+            keyValuePairs.slice(index + 1).every((p) => p.key !== pair.key);
 
           return (
-            <Row key={pair.id} gutter={8} align="middle">
+            <Row key={pair.id} gutter={8} align='middle'>
               <Col span={6}>
-                <div className="relative">
+                <div className='relative'>
                   <Input
                     placeholder={t('键名')}
                     value={pair.key}
@@ -425,24 +460,22 @@ const JSONEditor = ({
                       }
                     >
                       <IconAlertTriangle
-                        className="absolute right-2 top-1/2 transform -translate-y-1/2"
+                        className='absolute right-2 top-1/2 transform -translate-y-1/2'
                         style={{
                           color: isLastDuplicate ? '#ff7d00' : '#faad14',
-                          fontSize: '14px'
+                          fontSize: '14px',
                         }}
                       />
                     </Tooltip>
                   )}
                 </div>
               </Col>
-              <Col span={16}>
-                {renderValueInput(pair.id, pair.value)}
-              </Col>
+              <Col span={16}>{renderValueInput(pair.id, pair.value)}</Col>
               <Col span={2}>
                 <Button
                   icon={<IconDelete />}
-                  type="danger"
-                  theme="borderless"
+                  type='danger'
+                  theme='borderless'
                   onClick={() => removeKeyValue(pair.id)}
                   style={{ width: '100%' }}
                 />
@@ -451,11 +484,11 @@ const JSONEditor = ({
           );
         })}
 
-        <div className="mt-2 flex justify-center">
+        <div className='mt-2 flex justify-center'>
           <Button
             icon={<IconPlus />}
-            type="primary"
-            theme="outline"
+            type='primary'
+            theme='outline'
             onClick={addKeyValue}
           >
             {t('添加键值对')}
@@ -467,27 +500,27 @@ const JSONEditor = ({
 
   // 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
   const renderRegionEditor = () => {
-    const defaultPair = keyValuePairs.find(pair => pair.key === 'default');
-    const modelPairs = keyValuePairs.filter(pair => pair.key !== 'default');
+    const defaultPair = keyValuePairs.find((pair) => pair.key === 'default');
+    const modelPairs = keyValuePairs.filter((pair) => pair.key !== 'default');
 
     return (
-      <div className="space-y-2">
+      <div className='space-y-2'>
         {/* 重复键警告 */}
         {duplicateKeys.size > 0 && (
           <Banner
-            type="warning"
+            type='warning'
             icon={<IconAlertTriangle />}
             description={
               <div>
                 <Text strong>{t('存在重复的键名:')}</Text>
                 <Text>{Array.from(duplicateKeys).join(', ')}</Text>
                 <br />
-                <Text type="tertiary" size="small">
+                <Text type='tertiary' size='small'>
                   {t('注意:JSON中重复的键只会保留最后一个同名键的值')}
                 </Text>
               </div>
             }
-            className="mb-3"
+            className='mb-3'
           />
         )}
 
@@ -500,11 +533,14 @@ const JSONEditor = ({
               if (defaultPair) {
                 updateValue(defaultPair.id, value);
               } else {
-                const newPairs = [...keyValuePairs, {
-                  id: generateUniqueId(),
-                  key: 'default',
-                  value: value
-                }];
+                const newPairs = [
+                  ...keyValuePairs,
+                  {
+                    id: generateUniqueId(),
+                    key: 'default',
+                    value: value,
+                  },
+                ];
                 handleVisualChange(newPairs);
               }
             }}
@@ -517,9 +553,9 @@ const JSONEditor = ({
             {modelPairs.map((pair) => {
               const isDuplicate = duplicateKeys.has(pair.key);
               return (
-                <Row key={pair.id} gutter={8} align="middle" className="mb-2">
+                <Row key={pair.id} gutter={8} align='middle' className='mb-2'>
                   <Col span={10}>
-                    <div className="relative">
+                    <div className='relative'>
                       <Input
                         placeholder={t('模型名称')}
                         value={pair.key}
@@ -529,7 +565,7 @@ const JSONEditor = ({
                       {isDuplicate && (
                         <Tooltip content={t('重复的键名')}>
                           <IconAlertTriangle
-                            className="absolute right-2 top-1/2 transform -translate-y-1/2"
+                            className='absolute right-2 top-1/2 transform -translate-y-1/2'
                             style={{ color: '#faad14', fontSize: '14px' }}
                           />
                         </Tooltip>
@@ -546,8 +582,8 @@ const JSONEditor = ({
                   <Col span={2}>
                     <Button
                       icon={<IconDelete />}
-                      type="danger"
-                      theme="borderless"
+                      type='danger'
+                      theme='borderless'
                       onClick={() => removeKeyValue(pair.id)}
                       style={{ width: '100%' }}
                     />
@@ -556,12 +592,12 @@ const JSONEditor = ({
               );
             })}
 
-            <div className="mt-2 flex justify-center">
+            <div className='mt-2 flex justify-center'>
               <Button
                 icon={<IconPlus />}
                 onClick={addKeyValue}
-                type="primary"
-                theme="outline"
+                type='primary'
+                theme='outline'
               >
                 {t('添加模型区域')}
               </Button>
@@ -590,9 +626,9 @@ const JSONEditor = ({
     <Form.Slot label={label}>
       <Card
         header={
-          <div className="flex justify-between items-center">
+          <div className='flex justify-between items-center'>
             <Tabs
-              type="slash"
+              type='slash'
               activeKey={editMode}
               onChange={(key) => {
                 if (key === 'manual' && editMode === 'visual') {
@@ -602,16 +638,12 @@ const JSONEditor = ({
                 }
               }}
             >
-              <TabPane tab={t('可视化')} itemKey="visual" />
-              <TabPane tab={t('手动编辑')} itemKey="manual" />
+              <TabPane tab={t('可视化')} itemKey='visual' />
+              <TabPane tab={t('手动编辑')} itemKey='manual' />
             </Tabs>
 
             {template && templateLabel && (
-              <Button
-                type="tertiary"
-                onClick={fillTemplate}
-                size="small"
-              >
+              <Button type='tertiary' onClick={fillTemplate} size='small'>
                 {templateLabel}
               </Button>
             )}
@@ -619,14 +651,14 @@ const JSONEditor = ({
         }
         headerStyle={{ padding: '12px 16px' }}
         bodyStyle={{ padding: '16px' }}
-        className="!rounded-2xl"
+        className='!rounded-2xl'
       >
         {/* JSON错误提示 */}
         {hasJsonError && (
           <Banner
-            type="danger"
+            type='danger'
             description={`JSON 格式错误: ${jsonError}`}
-            className="mb-3"
+            className='mb-3'
           />
         )}
 
@@ -668,17 +700,15 @@ const JSONEditor = ({
         {/* 额外文本显示在卡片底部 */}
         {extraText && (
           <Divider margin='12px' align='center'>
-            <Text type="tertiary" size="small">{extraText}</Text>
+            <Text type='tertiary' size='small'>
+              {extraText}
+            </Text>
           </Divider>
         )}
-        {extraFooter && (
-          <div className="mt-1">
-            {extraFooter}
-          </div>
-        )}
+        {extraFooter && <div className='mt-1'>{extraFooter}</div>}
       </Card>
     </Form.Slot>
   );
 };
 
-export default JSONEditor;
+export default JSONEditor;

+ 2 - 6
web/src/components/common/ui/Loading.jsx

@@ -21,13 +21,9 @@ import React from 'react';
 import { Spin } from '@douyinfe/semi-ui';
 
 const Loading = ({ size = 'small' }) => {
-
   return (
-    <div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
-      <Spin
-        size={size}
-        spinning={true}
-      />
+    <div className='fixed inset-0 w-screen h-screen flex items-center justify-center'>
+      <Spin size={size} spinning={true} />
     </div>
   );
 };

+ 1 - 1
web/src/components/common/ui/RenderUtils.jsx

@@ -57,4 +57,4 @@ export const renderDescription = (text, maxWidth = 200) => {
       {text || '-'}
     </Text>
   );
-};
+};

+ 192 - 170
web/src/components/common/ui/ScrollableContainer.jsx

@@ -24,197 +24,219 @@ import React, {
   useCallback,
   useMemo,
   useImperativeHandle,
-  forwardRef
+  forwardRef,
 } from 'react';
 
 /**
  * ScrollableContainer 可滚动容器组件
- * 
+ *
  * 提供自动检测滚动状态和显示渐变指示器的功能
  * 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器
- * 
+ *
  */
-const ScrollableContainer = forwardRef(({
-  children,
-  maxHeight = '24rem',
-  className = '',
-  contentClassName = '',
-  fadeIndicatorClassName = '',
-  checkInterval = 100,
-  scrollThreshold = 5,
-  debounceDelay = 16, // ~60fps
-  onScroll,
-  onScrollStateChange,
-  ...props
-}, ref) => {
-  const scrollRef = useRef(null);
-  const containerRef = useRef(null);
-  const debounceTimerRef = useRef(null);
-  const resizeObserverRef = useRef(null);
-  const onScrollStateChangeRef = useRef(onScrollStateChange);
-  const onScrollRef = useRef(onScroll);
-
-  const [showScrollHint, setShowScrollHint] = useState(false);
-
-  useEffect(() => {
-    onScrollStateChangeRef.current = onScrollStateChange;
-  }, [onScrollStateChange]);
-
-  useEffect(() => {
-    onScrollRef.current = onScroll;
-  }, [onScroll]);
-
-  const debounce = useCallback((func, delay) => {
-    return (...args) => {
-      if (debounceTimerRef.current) {
-        clearTimeout(debounceTimerRef.current);
-      }
-      debounceTimerRef.current = setTimeout(() => func(...args), delay);
-    };
-  }, []);
-
-  const checkScrollable = useCallback(() => {
-    if (!scrollRef.current) return;
-
-    const element = scrollRef.current;
-    const isScrollable = element.scrollHeight > element.clientHeight;
-    const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold;
-    const shouldShowHint = isScrollable && !isAtBottom;
-
-    setShowScrollHint(shouldShowHint);
-
-    if (onScrollStateChangeRef.current) {
-      onScrollStateChangeRef.current({
-        isScrollable,
-        isAtBottom,
-        showScrollHint: shouldShowHint,
-        scrollTop: element.scrollTop,
-        scrollHeight: element.scrollHeight,
-        clientHeight: element.clientHeight
-      });
-    }
-  }, [scrollThreshold]);
-
-  const debouncedCheckScrollable = useMemo(() =>
-    debounce(checkScrollable, debounceDelay),
-    [debounce, checkScrollable, debounceDelay]
-  );
-
-  const handleScroll = useCallback((e) => {
-    debouncedCheckScrollable();
-    if (onScrollRef.current) {
-      onScrollRef.current(e);
-    }
-  }, [debouncedCheckScrollable]);
-
-  useImperativeHandle(ref, () => ({
-    checkScrollable: () => {
-      checkScrollable();
-    },
-    scrollToTop: () => {
-      if (scrollRef.current) {
-        scrollRef.current.scrollTop = 0;
-      }
-    },
-    scrollToBottom: () => {
-      if (scrollRef.current) {
-        scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
-      }
+const ScrollableContainer = forwardRef(
+  (
+    {
+      children,
+      maxHeight = '24rem',
+      className = '',
+      contentClassName = '',
+      fadeIndicatorClassName = '',
+      checkInterval = 100,
+      scrollThreshold = 5,
+      debounceDelay = 16, // ~60fps
+      onScroll,
+      onScrollStateChange,
+      ...props
     },
-    getScrollInfo: () => {
-      if (!scrollRef.current) return null;
-      const element = scrollRef.current;
-      return {
-        scrollTop: element.scrollTop,
-        scrollHeight: element.scrollHeight,
-        clientHeight: element.clientHeight,
-        isScrollable: element.scrollHeight > element.clientHeight,
-        isAtBottom: element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold
+    ref,
+  ) => {
+    const scrollRef = useRef(null);
+    const containerRef = useRef(null);
+    const debounceTimerRef = useRef(null);
+    const resizeObserverRef = useRef(null);
+    const onScrollStateChangeRef = useRef(onScrollStateChange);
+    const onScrollRef = useRef(onScroll);
+
+    const [showScrollHint, setShowScrollHint] = useState(false);
+
+    useEffect(() => {
+      onScrollStateChangeRef.current = onScrollStateChange;
+    }, [onScrollStateChange]);
+
+    useEffect(() => {
+      onScrollRef.current = onScroll;
+    }, [onScroll]);
+
+    const debounce = useCallback((func, delay) => {
+      return (...args) => {
+        if (debounceTimerRef.current) {
+          clearTimeout(debounceTimerRef.current);
+        }
+        debounceTimerRef.current = setTimeout(() => func(...args), delay);
       };
-    }
-  }), [checkScrollable, scrollThreshold]);
-
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      checkScrollable();
-    }, checkInterval);
-    return () => clearTimeout(timer);
-  }, [checkScrollable, checkInterval]);
-
-  useEffect(() => {
-    if (!scrollRef.current) return;
-
-    if (typeof ResizeObserver === 'undefined') {
-      if (typeof MutationObserver !== 'undefined') {
-        const observer = new MutationObserver(() => {
-          debouncedCheckScrollable();
-        });
+    }, []);
 
-        observer.observe(scrollRef.current, {
-          childList: true,
-          subtree: true,
-          attributes: true,
-          characterData: true
-        });
+    const checkScrollable = useCallback(() => {
+      if (!scrollRef.current) return;
 
-        return () => observer.disconnect();
+      const element = scrollRef.current;
+      const isScrollable = element.scrollHeight > element.clientHeight;
+      const isAtBottom =
+        element.scrollTop + element.clientHeight >=
+        element.scrollHeight - scrollThreshold;
+      const shouldShowHint = isScrollable && !isAtBottom;
+
+      setShowScrollHint(shouldShowHint);
+
+      if (onScrollStateChangeRef.current) {
+        onScrollStateChangeRef.current({
+          isScrollable,
+          isAtBottom,
+          showScrollHint: shouldShowHint,
+          scrollTop: element.scrollTop,
+          scrollHeight: element.scrollHeight,
+          clientHeight: element.clientHeight,
+        });
       }
-      return;
-    }
+    }, [scrollThreshold]);
+
+    const debouncedCheckScrollable = useMemo(
+      () => debounce(checkScrollable, debounceDelay),
+      [debounce, checkScrollable, debounceDelay],
+    );
 
-    resizeObserverRef.current = new ResizeObserver((entries) => {
-      for (const entry of entries) {
+    const handleScroll = useCallback(
+      (e) => {
         debouncedCheckScrollable();
+        if (onScrollRef.current) {
+          onScrollRef.current(e);
+        }
+      },
+      [debouncedCheckScrollable],
+    );
+
+    useImperativeHandle(
+      ref,
+      () => ({
+        checkScrollable: () => {
+          checkScrollable();
+        },
+        scrollToTop: () => {
+          if (scrollRef.current) {
+            scrollRef.current.scrollTop = 0;
+          }
+        },
+        scrollToBottom: () => {
+          if (scrollRef.current) {
+            scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+          }
+        },
+        getScrollInfo: () => {
+          if (!scrollRef.current) return null;
+          const element = scrollRef.current;
+          return {
+            scrollTop: element.scrollTop,
+            scrollHeight: element.scrollHeight,
+            clientHeight: element.clientHeight,
+            isScrollable: element.scrollHeight > element.clientHeight,
+            isAtBottom:
+              element.scrollTop + element.clientHeight >=
+              element.scrollHeight - scrollThreshold,
+          };
+        },
+      }),
+      [checkScrollable, scrollThreshold],
+    );
+
+    useEffect(() => {
+      const timer = setTimeout(() => {
+        checkScrollable();
+      }, checkInterval);
+      return () => clearTimeout(timer);
+    }, [checkScrollable, checkInterval]);
+
+    useEffect(() => {
+      if (!scrollRef.current) return;
+
+      if (typeof ResizeObserver === 'undefined') {
+        if (typeof MutationObserver !== 'undefined') {
+          const observer = new MutationObserver(() => {
+            debouncedCheckScrollable();
+          });
+
+          observer.observe(scrollRef.current, {
+            childList: true,
+            subtree: true,
+            attributes: true,
+            characterData: true,
+          });
+
+          return () => observer.disconnect();
+        }
+        return;
       }
-    });
 
-    resizeObserverRef.current.observe(scrollRef.current);
+      resizeObserverRef.current = new ResizeObserver((entries) => {
+        for (const entry of entries) {
+          debouncedCheckScrollable();
+        }
+      });
 
-    return () => {
-      if (resizeObserverRef.current) {
-        resizeObserverRef.current.disconnect();
-      }
-    };
-  }, [debouncedCheckScrollable]);
+      resizeObserverRef.current.observe(scrollRef.current);
 
-  useEffect(() => {
-    return () => {
-      if (debounceTimerRef.current) {
-        clearTimeout(debounceTimerRef.current);
-      }
-    };
-  }, []);
-
-  const containerStyle = useMemo(() => ({
-    maxHeight
-  }), [maxHeight]);
-
-  const fadeIndicatorStyle = useMemo(() => ({
-    opacity: showScrollHint ? 1 : 0
-  }), [showScrollHint]);
-
-  return (
-    <div
-      ref={containerRef}
-      className={`card-content-container ${className}`}
-      {...props}
-    >
+      return () => {
+        if (resizeObserverRef.current) {
+          resizeObserverRef.current.disconnect();
+        }
+      };
+    }, [debouncedCheckScrollable]);
+
+    useEffect(() => {
+      return () => {
+        if (debounceTimerRef.current) {
+          clearTimeout(debounceTimerRef.current);
+        }
+      };
+    }, []);
+
+    const containerStyle = useMemo(
+      () => ({
+        maxHeight,
+      }),
+      [maxHeight],
+    );
+
+    const fadeIndicatorStyle = useMemo(
+      () => ({
+        opacity: showScrollHint ? 1 : 0,
+      }),
+      [showScrollHint],
+    );
+
+    return (
       <div
-        ref={scrollRef}
-        className={`overflow-y-auto card-content-scroll ${contentClassName}`}
-        style={containerStyle}
-        onScroll={handleScroll}
+        ref={containerRef}
+        className={`card-content-container ${className}`}
+        {...props}
       >
-        {children}
+        <div
+          ref={scrollRef}
+          className={`overflow-y-auto card-content-scroll ${contentClassName}`}
+          style={containerStyle}
+          onScroll={handleScroll}
+        >
+          {children}
+        </div>
+        <div
+          className={`card-content-fade-indicator ${fadeIndicatorClassName}`}
+          style={fadeIndicatorStyle}
+        />
       </div>
-      <div
-        className={`card-content-fade-indicator ${fadeIndicatorClassName}`}
-        style={fadeIndicatorStyle}
-      />
-    </div>
-  );
-});
+    );
+  },
+);
 
 ScrollableContainer.displayName = 'ScrollableContainer';
 
-export default ScrollableContainer; 
+export default ScrollableContainer;

+ 91 - 61
web/src/components/common/ui/SelectableButtonGroup.jsx

@@ -20,7 +20,17 @@ For commercial licensing, please contact support@quantumnous.com
 import React, { useState, useRef, useEffect } from 'react';
 import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
 import { useContainerWidth } from '../../../hooks/common/useContainerWidth';
-import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton, Tooltip } from '@douyinfe/semi-ui';
+import {
+  Divider,
+  Button,
+  Tag,
+  Row,
+  Col,
+  Collapsible,
+  Checkbox,
+  Skeleton,
+  Tooltip,
+} from '@douyinfe/semi-ui';
 import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
 
 /**
@@ -47,7 +57,7 @@ const SelectableButtonGroup = ({
   collapsible = true,
   collapseHeight = 200,
   withCheckbox = false,
-  loading = false
+  loading = false,
 }) => {
   const [isOpen, setIsOpen] = useState(false);
   const [skeletonCount] = useState(12);
@@ -64,15 +74,13 @@ const SelectableButtonGroup = ({
     }, [text, containerWidth]);
 
     const textElement = (
-      <span ref={textRef} className="sbg-ellipsis">
+      <span ref={textRef} className='sbg-ellipsis'>
         {text}
       </span>
     );
 
     return isOverflowing ? (
-      <Tooltip content={text}>
-        {textElement}
-      </Tooltip>
+      <Tooltip content={text}>{textElement}</Tooltip>
     ) : (
       textElement
     );
@@ -80,10 +88,10 @@ const SelectableButtonGroup = ({
 
   // 基于容器宽度计算响应式列数和标签显示策略
   const getResponsiveConfig = () => {
-    if (containerWidth <= 280) return { columns: 1, showTags: true };   // 极窄:1列+标签
-    if (containerWidth <= 380) return { columns: 2, showTags: true };   // 窄屏:2列+标签  
-    if (containerWidth <= 460) return { columns: 3, showTags: false };  // 中等:3列不加标签
-    return { columns: 3, showTags: true };                              // 最宽:3列+标签
+    if (containerWidth <= 280) return { columns: 1, showTags: true }; // 极窄:1列+标签
+    if (containerWidth <= 380) return { columns: 2, showTags: true }; // 窄屏:2列+标签
+    if (containerWidth <= 460) return { columns: 3, showTags: false }; // 中等:3列不加标签
+    return { columns: 3, showTags: true }; // 最宽:3列+标签
   };
 
   const { columns: perRow, showTags: shouldShowTags } = getResponsiveConfig();
@@ -102,9 +110,9 @@ const SelectableButtonGroup = ({
   const maskStyle = isOpen
     ? {}
     : {
-      WebkitMaskImage:
-        'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)',
-    };
+        WebkitMaskImage:
+          'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)',
+      };
 
   const toggle = () => {
     setIsOpen(!isOpen);
@@ -127,25 +135,23 @@ const SelectableButtonGroup = ({
   };
 
   const renderSkeletonButtons = () => {
-
     const placeholder = (
       <Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
         {Array.from({ length: skeletonCount }).map((_, index) => (
-          <Col
-            span={getColSpan()}
-            key={index}
-          >
-            <div style={{
-              width: '100%',
-              height: '32px',
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'flex-start',
-              border: '1px solid var(--semi-color-border)',
-              borderRadius: 'var(--semi-border-radius-medium)',
-              padding: '0 12px',
-              gap: '6px'
-            }}>
+          <Col span={getColSpan()} key={index}>
+            <div
+              style={{
+                width: '100%',
+                height: '32px',
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'flex-start',
+                border: '1px solid var(--semi-color-border)',
+                borderRadius: 'var(--semi-border-radius-medium)',
+                padding: '0 12px',
+                gap: '6px',
+              }}
+            >
               {withCheckbox && (
                 <Skeleton.Title active style={{ width: 14, height: 14 }} />
               )}
@@ -153,7 +159,7 @@ const SelectableButtonGroup = ({
                 active
                 style={{
                   width: `${60 + (index % 3) * 20}px`,
-                  height: 14
+                  height: 14,
                 }}
               />
             </div>
@@ -167,26 +173,29 @@ const SelectableButtonGroup = ({
     );
   };
 
-  const contentElement = showSkeleton ? renderSkeletonButtons() : (
+  const contentElement = showSkeleton ? (
+    renderSkeletonButtons()
+  ) : (
     <Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
       {items.map((item) => {
-        const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0);
+        const isDisabled =
+          item.disabled ||
+          (typeof item.tagCount === 'number' && item.tagCount === 0);
         const isActive = Array.isArray(activeValue)
           ? activeValue.includes(item.value)
           : activeValue === item.value;
 
         if (withCheckbox) {
           return (
-            <Col
-              span={getColSpan()}
-              key={item.value}
-            >
+            <Col span={getColSpan()} key={item.value}>
               <Button
-                onClick={() => { /* disabled */ }}
+                onClick={() => {
+                  /* disabled */
+                }}
                 theme={isActive ? 'light' : 'outline'}
                 type={isActive ? 'primary' : 'tertiary'}
                 disabled={isDisabled}
-                className="sbg-button"
+                className='sbg-button'
                 icon={
                   <Checkbox
                     checked={isActive}
@@ -197,11 +206,18 @@ const SelectableButtonGroup = ({
                 }
                 style={{ width: '100%', cursor: 'default' }}
               >
-                <div className="sbg-content">
-                  {item.icon && (<span className="sbg-icon">{item.icon}</span>)}
+                <div className='sbg-content'>
+                  {item.icon && <span className='sbg-icon'>{item.icon}</span>}
                   <ConditionalTooltipText text={item.label} />
                   {item.tagCount !== undefined && shouldShowTags && (
-                    <Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
+                    <Tag
+                      className='sbg-tag'
+                      color='white'
+                      shape='circle'
+                      size='small'
+                    >
+                      {item.tagCount}
+                    </Tag>
                   )}
                 </div>
               </Button>
@@ -210,23 +226,27 @@ const SelectableButtonGroup = ({
         }
 
         return (
-          <Col
-            span={getColSpan()}
-            key={item.value}
-          >
+          <Col span={getColSpan()} key={item.value}>
             <Button
               onClick={() => onChange(item.value)}
               theme={isActive ? 'light' : 'outline'}
               type={isActive ? 'primary' : 'tertiary'}
               disabled={isDisabled}
-              className="sbg-button"
+              className='sbg-button'
               style={{ width: '100%' }}
             >
-              <div className="sbg-content">
-                {item.icon && (<span className="sbg-icon">{item.icon}</span>)}
+              <div className='sbg-content'>
+                {item.icon && <span className='sbg-icon'>{item.icon}</span>}
                 <ConditionalTooltipText text={item.label} />
                 {item.tagCount !== undefined && shouldShowTags && (
-                  <Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
+                  <Tag
+                    className='sbg-tag'
+                    color='white'
+                    shape='circle'
+                    size='small'
+                  >
+                    {item.tagCount}
+                  </Tag>
                 )}
               </div>
             </Button>
@@ -237,9 +257,12 @@ const SelectableButtonGroup = ({
   );
 
   return (
-    <div className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`} ref={containerRef}>
+    <div
+      className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`}
+      ref={containerRef}
+    >
       {title && (
-        <Divider margin="12px" align="left">
+        <Divider margin='12px' align='left'>
           {showSkeleton ? (
             <Skeleton.Title active style={{ width: 80, height: 14 }} />
           ) : (
@@ -249,23 +272,30 @@ const SelectableButtonGroup = ({
       )}
       {needCollapse && !showSkeleton ? (
         <div style={{ position: 'relative' }}>
-          <Collapsible isOpen={isOpen} collapseHeight={collapseHeight} style={{ ...maskStyle }}>
+          <Collapsible
+            isOpen={isOpen}
+            collapseHeight={collapseHeight}
+            style={{ ...maskStyle }}
+          >
             {contentElement}
           </Collapsible>
           {isOpen ? null : (
             <div onClick={toggle} style={{ ...linkStyle }}>
-              <IconChevronDown size="small" />
+              <IconChevronDown size='small' />
               <span>{t('展开更多')}</span>
             </div>
           )}
           {isOpen && (
-            <div onClick={toggle} style={{
-              ...linkStyle,
-              position: 'static',
-              marginTop: 8,
-              bottom: 'auto'
-            }}>
-              <IconChevronUp size="small" />
+            <div
+              onClick={toggle}
+              style={{
+                ...linkStyle,
+                position: 'static',
+                marginTop: 8,
+                bottom: 'auto',
+              }}
+            >
+              <IconChevronUp size='small' />
               <span>{t('收起')}</span>
             </div>
           )}
@@ -277,4 +307,4 @@ const SelectableButtonGroup = ({
   );
 };
 
-export default SelectableButtonGroup; 
+export default SelectableButtonGroup;

+ 43 - 27
web/src/components/dashboard/AnnouncementsPanel.jsx

@@ -21,7 +21,10 @@ import React from 'react';
 import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui';
 import { Bell } from 'lucide-react';
 import { marked } from 'marked';
-import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
+import {
+  IllustrationConstruction,
+  IllustrationConstructionDark,
+} from '@douyinfe/semi-illustrations';
 import ScrollableContainer from '../common/ui/ScrollableContainer';
 
 const AnnouncementsPanel = ({
@@ -29,36 +32,43 @@ const AnnouncementsPanel = ({
   announcementLegendData,
   CARD_PROPS,
   ILLUSTRATION_SIZE,
-  t
+  t,
 }) => {
   return (
     <Card
       {...CARD_PROPS}
-      className="shadow-sm !rounded-2xl lg:col-span-2"
+      className='shadow-sm !rounded-2xl lg:col-span-2'
       title={
-        <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
-          <div className="flex items-center gap-2">
+        <div className='flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full'>
+          <div className='flex items-center gap-2'>
             <Bell size={16} />
             {t('系统公告')}
-            <Tag color="white" shape="circle">
+            <Tag color='white' shape='circle'>
               {t('显示最新20条')}
             </Tag>
           </div>
           {/* 图例 */}
-          <div className="flex flex-wrap gap-3 text-xs">
+          <div className='flex flex-wrap gap-3 text-xs'>
             {announcementLegendData.map((legend, index) => (
-              <div key={index} className="flex items-center gap-1">
+              <div key={index} className='flex items-center gap-1'>
                 <div
-                  className="w-2 h-2 rounded-full"
+                  className='w-2 h-2 rounded-full'
                   style={{
-                    backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
-                      legend.color === 'blue' ? '#3b82f6' :
-                        legend.color === 'green' ? '#10b981' :
-                          legend.color === 'orange' ? '#f59e0b' :
-                            legend.color === 'red' ? '#ef4444' : '#8b9aa7'
+                    backgroundColor:
+                      legend.color === 'grey'
+                        ? '#8b9aa7'
+                        : legend.color === 'blue'
+                          ? '#3b82f6'
+                          : legend.color === 'green'
+                            ? '#10b981'
+                            : legend.color === 'orange'
+                              ? '#f59e0b'
+                              : legend.color === 'red'
+                                ? '#ef4444'
+                                : '#8b9aa7',
                   }}
                 />
-                <span className="text-gray-600">{legend.label}</span>
+                <span className='text-gray-600'>{legend.label}</span>
               </div>
             ))}
           </div>
@@ -66,9 +76,9 @@ const AnnouncementsPanel = ({
       }
       bodyStyle={{ padding: 0 }}
     >
-      <ScrollableContainer maxHeight="24rem">
+      <ScrollableContainer maxHeight='24rem'>
         {announcementData.length > 0 ? (
-          <Timeline mode="left">
+          <Timeline mode='left'>
             {announcementData.map((item, idx) => {
               const htmlExtra = item.extra ? marked.parse(item.extra) : '';
               return (
@@ -76,16 +86,20 @@ const AnnouncementsPanel = ({
                   key={idx}
                   type={item.type || 'default'}
                   time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
-                  extra={item.extra ? (
-                    <div
-                      className="text-xs text-gray-500"
-                      dangerouslySetInnerHTML={{ __html: htmlExtra }}
-                    />
-                  ) : null}
+                  extra={
+                    item.extra ? (
+                      <div
+                        className='text-xs text-gray-500'
+                        dangerouslySetInnerHTML={{ __html: htmlExtra }}
+                      />
+                    ) : null
+                  }
                 >
                   <div>
                     <div
-                      dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
+                      dangerouslySetInnerHTML={{
+                        __html: marked.parse(item.content || ''),
+                      }}
                     />
                   </div>
                 </Timeline.Item>
@@ -93,10 +107,12 @@ const AnnouncementsPanel = ({
             })}
           </Timeline>
         ) : (
-          <div className="flex justify-center items-center py-8">
+          <div className='flex justify-center items-center py-8'>
             <Empty
               image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
-              darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+              darkModeImage={
+                <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
+              }
               title={t('暂无系统公告')}
               description={t('请联系管理员在系统设置中配置公告信息')}
             />
@@ -107,4 +123,4 @@ const AnnouncementsPanel = ({
   );
 };
 
-export default AnnouncementsPanel; 
+export default AnnouncementsPanel;

+ 30 - 28
web/src/components/dashboard/ApiInfoPanel.jsx

@@ -20,7 +20,10 @@ For commercial licensing, please contact support@quantumnous.com
 import React from 'react';
 import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
 import { Server, Gauge, ExternalLink } from 'lucide-react';
-import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
+import {
+  IllustrationConstruction,
+  IllustrationConstructionDark,
+} from '@douyinfe/semi-illustrations';
 import ScrollableContainer from '../common/ui/ScrollableContainer';
 
 const ApiInfoPanel = ({
@@ -30,12 +33,12 @@ const ApiInfoPanel = ({
   CARD_PROPS,
   FLEX_CENTER_GAP2,
   ILLUSTRATION_SIZE,
-  t
+  t,
 }) => {
   return (
     <Card
       {...CARD_PROPS}
-      className="bg-gray-50 border-0 !rounded-2xl"
+      className='bg-gray-50 border-0 !rounded-2xl'
       title={
         <div className={FLEX_CENTER_GAP2}>
           <Server size={16} />
@@ -44,66 +47,65 @@ const ApiInfoPanel = ({
       }
       bodyStyle={{ padding: 0 }}
     >
-      <ScrollableContainer maxHeight="24rem">
+      <ScrollableContainer maxHeight='24rem'>
         {apiInfoData.length > 0 ? (
           apiInfoData.map((api) => (
             <React.Fragment key={api.id}>
-              <div className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
-                <div className="flex-shrink-0 mr-3">
-                  <Avatar
-                    size="extra-small"
-                    color={api.color}
-                  >
+              <div className='flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer'>
+                <div className='flex-shrink-0 mr-3'>
+                  <Avatar size='extra-small' color={api.color}>
                     {api.route.substring(0, 2)}
                   </Avatar>
                 </div>
-                <div className="flex-1">
-                  <div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
-                    <span className="text-sm font-medium text-gray-900 !font-bold break-all">
+                <div className='flex-1'>
+                  <div className='flex flex-wrap items-center justify-between mb-1 w-full gap-2'>
+                    <span className='text-sm font-medium text-gray-900 !font-bold break-all'>
                       {api.route}
                     </span>
-                    <div className="flex items-center gap-1 mt-1 lg:mt-0">
+                    <div className='flex items-center gap-1 mt-1 lg:mt-0'>
                       <Tag
                         prefixIcon={<Gauge size={12} />}
-                        size="small"
-                        color="white"
+                        size='small'
+                        color='white'
                         shape='circle'
                         onClick={() => handleSpeedTest(api.url)}
-                        className="cursor-pointer hover:opacity-80 text-xs"
+                        className='cursor-pointer hover:opacity-80 text-xs'
                       >
                         {t('测速')}
                       </Tag>
                       <Tag
                         prefixIcon={<ExternalLink size={12} />}
-                        size="small"
-                        color="white"
+                        size='small'
+                        color='white'
                         shape='circle'
-                        onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
-                        className="cursor-pointer hover:opacity-80 text-xs"
+                        onClick={() =>
+                          window.open(api.url, '_blank', 'noopener,noreferrer')
+                        }
+                        className='cursor-pointer hover:opacity-80 text-xs'
                       >
                         {t('跳转')}
                       </Tag>
                     </div>
                   </div>
                   <div
-                    className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
+                    className='!text-semi-color-primary break-all cursor-pointer hover:underline mb-1'
                     onClick={() => handleCopyUrl(api.url)}
                   >
                     {api.url}
                   </div>
-                  <div className="text-gray-500">
-                    {api.description}
-                  </div>
+                  <div className='text-gray-500'>{api.description}</div>
                 </div>
               </div>
               <Divider />
             </React.Fragment>
           ))
         ) : (
-          <div className="flex justify-center items-center py-8">
+          <div className='flex justify-center items-center py-8'>
             <Empty
               image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
-              darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+              darkModeImage={
+                <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
+              }
               title={t('暂无API信息')}
               description={t('请联系管理员在系统设置中配置API信息')}
             />
@@ -114,4 +116,4 @@ const ApiInfoPanel = ({
   );
 };
 
-export default ApiInfoPanel; 
+export default ApiInfoPanel;

+ 46 - 46
web/src/components/dashboard/ChartsPanel.jsx

@@ -23,7 +23,7 @@ import { PieChart } from 'lucide-react';
 import {
   IconHistogram,
   IconPulse,
-  IconPieChart2Stroked
+  IconPieChart2Stroked,
 } from '@douyinfe/semi-icons';
 import { VChart } from '@visactor/react-vchart';
 
@@ -38,80 +38,80 @@ const ChartsPanel = ({
   CHART_CONFIG,
   FLEX_CENTER_GAP2,
   hasApiInfoPanel,
-  t
+  t,
 }) => {
   return (
     <Card
       {...CARD_PROPS}
       className={`!rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
       title={
-        <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
+        <div className='flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3'>
           <div className={FLEX_CENTER_GAP2}>
             <PieChart size={16} />
             {t('模型数据分析')}
           </div>
           <Tabs
-            type="button"
+            type='button'
             activeKey={activeChartTab}
             onChange={setActiveChartTab}
           >
-            <TabPane tab={
-              <span>
-                <IconHistogram />
-                {t('消耗分布')}
-              </span>
-            } itemKey="1" />
-            <TabPane tab={
-              <span>
-                <IconPulse />
-                {t('消耗趋势')}
-              </span>
-            } itemKey="2" />
-            <TabPane tab={
-              <span>
-                <IconPieChart2Stroked />
-                {t('调用次数分布')}
-              </span>
-            } itemKey="3" />
-            <TabPane tab={
-              <span>
-                <IconHistogram />
-                {t('调用次数排行')}
-              </span>
-            } itemKey="4" />
+            <TabPane
+              tab={
+                <span>
+                  <IconHistogram />
+                  {t('消耗分布')}
+                </span>
+              }
+              itemKey='1'
+            />
+            <TabPane
+              tab={
+                <span>
+                  <IconPulse />
+                  {t('消耗趋势')}
+                </span>
+              }
+              itemKey='2'
+            />
+            <TabPane
+              tab={
+                <span>
+                  <IconPieChart2Stroked />
+                  {t('调用次数分布')}
+                </span>
+              }
+              itemKey='3'
+            />
+            <TabPane
+              tab={
+                <span>
+                  <IconHistogram />
+                  {t('调用次数排行')}
+                </span>
+              }
+              itemKey='4'
+            />
           </Tabs>
         </div>
       }
       bodyStyle={{ padding: 0 }}
     >
-      <div className="h-96 p-2">
+      <div className='h-96 p-2'>
         {activeChartTab === '1' && (
-          <VChart
-            spec={spec_line}
-            option={CHART_CONFIG}
-          />
+          <VChart spec={spec_line} option={CHART_CONFIG} />
         )}
         {activeChartTab === '2' && (
-          <VChart
-            spec={spec_model_line}
-            option={CHART_CONFIG}
-          />
+          <VChart spec={spec_model_line} option={CHART_CONFIG} />
         )}
         {activeChartTab === '3' && (
-          <VChart
-            spec={spec_pie}
-            option={CHART_CONFIG}
-          />
+          <VChart spec={spec_pie} option={CHART_CONFIG} />
         )}
         {activeChartTab === '4' && (
-          <VChart
-            spec={spec_rank_bar}
-            option={CHART_CONFIG}
-          />
+          <VChart spec={spec_rank_bar} option={CHART_CONFIG} />
         )}
       </div>
     </Card>
   );
 };
 
-export default ChartsPanel; 
+export default ChartsPanel;

+ 6 - 6
web/src/components/dashboard/DashboardHeader.jsx

@@ -27,19 +27,19 @@ const DashboardHeader = ({
   showSearchModal,
   refresh,
   loading,
-  t
+  t,
 }) => {
-  const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full";
+  const ICON_BUTTON_CLASS = 'text-white hover:bg-opacity-80 !rounded-full';
 
   return (
-    <div className="flex items-center justify-between mb-4">
+    <div className='flex items-center justify-between mb-4'>
       <h2
-        className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out"
+        className='text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out'
         style={{ opacity: greetingVisible ? 1 : 0 }}
       >
         {getGreeting}
       </h2>
-      <div className="flex gap-3">
+      <div className='flex gap-3'>
         <Button
           type='tertiary'
           icon={<Search size={16} />}
@@ -58,4 +58,4 @@ const DashboardHeader = ({
   );
 };
 
-export default DashboardHeader; 
+export default DashboardHeader;

+ 15 - 8
web/src/components/dashboard/FaqPanel.jsx

@@ -22,7 +22,10 @@ import { Card, Collapse, Empty } from '@douyinfe/semi-ui';
 import { HelpCircle } from 'lucide-react';
 import { IconPlus, IconMinus } from '@douyinfe/semi-icons';
 import { marked } from 'marked';
-import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
+import {
+  IllustrationConstruction,
+  IllustrationConstructionDark,
+} from '@douyinfe/semi-illustrations';
 import ScrollableContainer from '../common/ui/ScrollableContainer';
 
 const FaqPanel = ({
@@ -30,12 +33,12 @@ const FaqPanel = ({
   CARD_PROPS,
   FLEX_CENTER_GAP2,
   ILLUSTRATION_SIZE,
-  t
+  t,
 }) => {
   return (
     <Card
       {...CARD_PROPS}
-      className="shadow-sm !rounded-2xl lg:col-span-1"
+      className='shadow-sm !rounded-2xl lg:col-span-1'
       title={
         <div className={FLEX_CENTER_GAP2}>
           <HelpCircle size={16} />
@@ -44,7 +47,7 @@ const FaqPanel = ({
       }
       bodyStyle={{ padding: 0 }}
     >
-      <ScrollableContainer maxHeight="24rem">
+      <ScrollableContainer maxHeight='24rem'>
         {faqData.length > 0 ? (
           <Collapse
             accordion
@@ -58,16 +61,20 @@ const FaqPanel = ({
                 itemKey={index.toString()}
               >
                 <div
-                  dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
+                  dangerouslySetInnerHTML={{
+                    __html: marked.parse(item.answer || ''),
+                  }}
                 />
               </Collapse.Panel>
             ))}
           </Collapse>
         ) : (
-          <div className="flex justify-center items-center py-8">
+          <div className='flex justify-center items-center py-8'>
             <Empty
               image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
-              darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+              darkModeImage={
+                <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
+              }
               title={t('暂无常见问答')}
               description={t('请联系管理员在系统设置中配置常见问答')}
             />
@@ -78,4 +85,4 @@ const FaqPanel = ({
   );
 };
 
-export default FaqPanel; 
+export default FaqPanel;

+ 28 - 21
web/src/components/dashboard/StatsCards.jsx

@@ -28,13 +28,13 @@ const StatsCards = ({
   loading,
   getTrendSpec,
   CARD_PROPS,
-  CHART_CONFIG
+  CHART_CONFIG,
 }) => {
   const navigate = useNavigate();
   const { t } = useTranslation();
   return (
-    <div className="mb-4">
-      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+    <div className='mb-4'>
+      <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
         {groupedStatsData.map((group, idx) => (
           <Card
             key={idx}
@@ -42,24 +42,24 @@ const StatsCards = ({
             className={`${group.color} border-0 !rounded-2xl w-full`}
             title={group.title}
           >
-            <div className="space-y-4">
+            <div className='space-y-4'>
               {group.items.map((item, itemIdx) => (
                 <div
                   key={itemIdx}
-                  className="flex items-center justify-between cursor-pointer"
+                  className='flex items-center justify-between cursor-pointer'
                   onClick={item.onClick}
                 >
-                  <div className="flex items-center">
+                  <div className='flex items-center'>
                     <Avatar
-                      className="mr-3"
-                      size="small"
+                      className='mr-3'
+                      size='small'
                       color={item.avatarColor}
                     >
                       {item.icon}
                     </Avatar>
                     <div>
-                      <div className="text-xs text-gray-500">{item.title}</div>
-                      <div className="text-lg font-semibold">
+                      <div className='text-xs text-gray-500'>{item.title}</div>
+                      <div className='text-lg font-semibold'>
                         <Skeleton
                           loading={loading}
                           active
@@ -67,7 +67,11 @@ const StatsCards = ({
                             <Skeleton.Paragraph
                               active
                               rows={1}
-                              style={{ width: '65px', height: '24px', marginTop: '4px' }}
+                              style={{
+                                width: '65px',
+                                height: '24px',
+                                marginTop: '4px',
+                              }}
                             />
                           }
                         >
@@ -78,9 +82,9 @@ const StatsCards = ({
                   </div>
                   {item.title === t('当前余额') ? (
                     <Tag
-                      color="white"
+                      color='white'
                       shape='circle'
-                      size="large"
+                      size='large'
                       onClick={(e) => {
                         e.stopPropagation();
                         navigate('/console/topup');
@@ -88,13 +92,16 @@ const StatsCards = ({
                     >
                       {t('充值')}
                     </Tag>
-                  ) : (loading || (item.trendData && item.trendData.length > 0)) && (
-                    <div className="w-24 h-10">
-                      <VChart
-                        spec={getTrendSpec(item.trendData, item.trendColor)}
-                        option={CHART_CONFIG}
-                      />
-                    </div>
+                  ) : (
+                    (loading ||
+                      (item.trendData && item.trendData.length > 0)) && (
+                      <div className='w-24 h-10'>
+                        <VChart
+                          spec={getTrendSpec(item.trendData, item.trendColor)}
+                          option={CHART_CONFIG}
+                        />
+                      </div>
+                    )
                   )}
                 </div>
               ))}
@@ -106,4 +113,4 @@ const StatsCards = ({
   );
 };
 
-export default StatsCards; 
+export default StatsCards;

+ 41 - 24
web/src/components/dashboard/UptimePanel.jsx

@@ -18,9 +18,20 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import { Card, Button, Spin, Tabs, TabPane, Tag, Empty } from '@douyinfe/semi-ui';
+import {
+  Card,
+  Button,
+  Spin,
+  Tabs,
+  TabPane,
+  Tag,
+  Empty,
+} from '@douyinfe/semi-ui';
 import { Gauge, RefreshCw } from 'lucide-react';
-import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
+import {
+  IllustrationConstruction,
+  IllustrationConstructionDark,
+} from '@douyinfe/semi-illustrations';
 import ScrollableContainer from '../common/ui/ScrollableContainer';
 
 const UptimePanel = ({
@@ -33,15 +44,15 @@ const UptimePanel = ({
   renderMonitorList,
   CARD_PROPS,
   ILLUSTRATION_SIZE,
-  t
+  t,
 }) => {
   return (
     <Card
       {...CARD_PROPS}
-      className="shadow-sm !rounded-2xl lg:col-span-1"
+      className='shadow-sm !rounded-2xl lg:col-span-1'
       title={
-        <div className="flex items-center justify-between w-full gap-2">
-          <div className="flex items-center gap-2">
+        <div className='flex items-center justify-between w-full gap-2'>
+          <div className='flex items-center gap-2'>
             <Gauge size={16} />
             {t('服务可用性')}
           </div>
@@ -49,39 +60,43 @@ const UptimePanel = ({
             icon={<RefreshCw size={14} />}
             onClick={loadUptimeData}
             loading={uptimeLoading}
-            size="small"
-            theme="borderless"
+            size='small'
+            theme='borderless'
             type='tertiary'
-            className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
+            className='text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full'
           />
         </div>
       }
       bodyStyle={{ padding: 0 }}
     >
       {/* 内容区域 */}
-      <div className="relative">
+      <div className='relative'>
         <Spin spinning={uptimeLoading}>
           {uptimeData.length > 0 ? (
             uptimeData.length === 1 ? (
-              <ScrollableContainer maxHeight="24rem">
+              <ScrollableContainer maxHeight='24rem'>
                 {renderMonitorList(uptimeData[0].monitors)}
               </ScrollableContainer>
             ) : (
               <Tabs
-                type="card"
+                type='card'
                 collapsible
                 activeKey={activeUptimeTab}
                 onChange={setActiveUptimeTab}
-                size="small"
+                size='small'
               >
                 {uptimeData.map((group, groupIdx) => (
                   <TabPane
                     tab={
-                      <span className="flex items-center gap-2">
+                      <span className='flex items-center gap-2'>
                         <Gauge size={14} />
                         {group.categoryName}
                         <Tag
-                          color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
+                          color={
+                            activeUptimeTab === group.categoryName
+                              ? 'red'
+                              : 'grey'
+                          }
                           size='small'
                           shape='circle'
                         >
@@ -92,7 +107,7 @@ const UptimePanel = ({
                     itemKey={group.categoryName}
                     key={groupIdx}
                   >
-                    <ScrollableContainer maxHeight="21.5rem">
+                    <ScrollableContainer maxHeight='21.5rem'>
                       {renderMonitorList(group.monitors)}
                     </ScrollableContainer>
                   </TabPane>
@@ -100,10 +115,12 @@ const UptimePanel = ({
               </Tabs>
             )
           ) : (
-            <div className="flex justify-center items-center py-8">
+            <div className='flex justify-center items-center py-8'>
               <Empty
                 image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
-                darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
+                darkModeImage={
+                  <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
+                }
                 title={t('暂无监控数据')}
                 description={t('请联系管理员在系统设置中配置Uptime')}
               />
@@ -114,15 +131,15 @@ const UptimePanel = ({
 
       {/* 图例 */}
       {uptimeData.length > 0 && (
-        <div className="p-3 bg-gray-50 rounded-b-2xl">
-          <div className="flex flex-wrap gap-3 text-xs justify-center">
+        <div className='p-3 bg-gray-50 rounded-b-2xl'>
+          <div className='flex flex-wrap gap-3 text-xs justify-center'>
             {uptimeLegendData.map((legend, index) => (
-              <div key={index} className="flex items-center gap-1">
+              <div key={index} className='flex items-center gap-1'>
                 <div
-                  className="w-2 h-2 rounded-full"
+                  className='w-2 h-2 rounded-full'
                   style={{ backgroundColor: legend.color }}
                 />
-                <span className="text-gray-600">{legend.label}</span>
+                <span className='text-gray-600'>{legend.label}</span>
               </div>
             ))}
           </div>
@@ -132,4 +149,4 @@ const UptimePanel = ({
   );
 };
 
-export default UptimePanel; 
+export default UptimePanel;

+ 54 - 38
web/src/components/dashboard/index.jsx

@@ -41,7 +41,7 @@ import {
   FLEX_CENTER_GAP2,
   ILLUSTRATION_SIZE,
   ANNOUNCEMENT_LEGEND_DATA,
-  UPTIME_STATUS_MAP
+  UPTIME_STATUS_MAP,
 } from '../../constants/dashboard.constants';
 import {
   getTrendSpec,
@@ -49,7 +49,7 @@ import {
   handleSpeedTest,
   getUptimeStatusColor,
   getUptimeStatusText,
-  renderMonitorList
+  renderMonitorList,
 } from '../../helpers/dashboard';
 
 const Dashboard = () => {
@@ -70,7 +70,7 @@ const Dashboard = () => {
     dashboardData.setPieData,
     dashboardData.setLineData,
     dashboardData.setModelColors,
-    dashboardData.t
+    dashboardData.t,
   );
 
   // ========== 统计数据 ==========
@@ -82,12 +82,12 @@ const Dashboard = () => {
     dashboardData.trendData,
     dashboardData.performanceMetrics,
     dashboardData.navigate,
-    dashboardData.t
+    dashboardData.t,
   );
 
   // ========== 数据处理 ==========
   const initChart = async () => {
-    await dashboardData.loadQuotaData().then(data => {
+    await dashboardData.loadQuotaData().then((data) => {
       if (data && data.length > 0) {
         dashboardCharts.updateChartData(data);
       }
@@ -108,25 +108,30 @@ const Dashboard = () => {
 
   // ========== 数据准备 ==========
   const apiInfoData = statusState?.status?.api_info || [];
-  const announcementData = (statusState?.status?.announcements || []).map(item => {
-    const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
-    const absoluteTime = pubDate && !isNaN(pubDate.getTime())
-      ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
-      : (item?.publishDate || '');
-    const relativeTime = getRelativeTime(item.publishDate);
-    return ({
-      ...item,
-      time: absoluteTime,
-      relative: relativeTime
-    });
-  });
+  const announcementData = (statusState?.status?.announcements || []).map(
+    (item) => {
+      const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
+      const absoluteTime =
+        pubDate && !isNaN(pubDate.getTime())
+          ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
+          : item?.publishDate || '';
+      const relativeTime = getRelativeTime(item.publishDate);
+      return {
+        ...item,
+        time: absoluteTime,
+        relative: relativeTime,
+      };
+    },
+  );
   const faqData = statusState?.status?.faq || [];
 
-  const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(([status, info]) => ({
-    status: Number(status),
-    color: info.color,
-    label: dashboardData.t(info.label)
-  }));
+  const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(
+    ([status, info]) => ({
+      status: Number(status),
+      color: info.color,
+      label: dashboardData.t(info.label),
+    }),
+  );
 
   // ========== Effects ==========
   useEffect(() => {
@@ -134,7 +139,7 @@ const Dashboard = () => {
   }, []);
 
   return (
-    <div className="h-full">
+    <div className='h-full'>
       <DashboardHeader
         getGreeting={dashboardData.getGreeting}
         greetingVisible={dashboardData.greetingVisible}
@@ -166,8 +171,10 @@ const Dashboard = () => {
       />
 
       {/* API信息和图表面板 */}
-      <div className="mb-4">
-        <div className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
+      <div className='mb-4'>
+        <div
+          className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}
+        >
           <ChartsPanel
             activeChartTab={dashboardData.activeChartTab}
             setActiveChartTab={dashboardData.setActiveChartTab}
@@ -198,16 +205,18 @@ const Dashboard = () => {
 
       {/* 系统公告和常见问答卡片 */}
       {dashboardData.hasInfoPanels && (
-        <div className="mb-4">
-          <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
+        <div className='mb-4'>
+          <div className='grid grid-cols-1 lg:grid-cols-4 gap-4'>
             {/* 公告卡片 */}
             {dashboardData.announcementsEnabled && (
               <AnnouncementsPanel
                 announcementData={announcementData}
-                announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(item => ({
-                  ...item,
-                  label: dashboardData.t(item.label)
-                }))}
+                announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(
+                  (item) => ({
+                    ...item,
+                    label: dashboardData.t(item.label),
+                  }),
+                )}
                 CARD_PROPS={CARD_PROPS}
                 ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
                 t={dashboardData.t}
@@ -234,12 +243,19 @@ const Dashboard = () => {
                 setActiveUptimeTab={dashboardData.setActiveUptimeTab}
                 loadUptimeData={dashboardData.loadUptimeData}
                 uptimeLegendData={uptimeLegendData}
-                renderMonitorList={(monitors) => renderMonitorList(
-                  monitors,
-                  (status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
-                  (status) => getUptimeStatusText(status, UPTIME_STATUS_MAP, dashboardData.t),
-                  dashboardData.t
-                )}
+                renderMonitorList={(monitors) =>
+                  renderMonitorList(
+                    monitors,
+                    (status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
+                    (status) =>
+                      getUptimeStatusText(
+                        status,
+                        UPTIME_STATUS_MAP,
+                        dashboardData.t,
+                      ),
+                    dashboardData.t,
+                  )
+                }
                 CARD_PROPS={CARD_PROPS}
                 ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
                 t={dashboardData.t}
@@ -252,4 +268,4 @@ const Dashboard = () => {
   );
 };
 
-export default Dashboard; 
+export default Dashboard;

+ 17 - 15
web/src/components/dashboard/modals/SearchModal.jsx

@@ -30,12 +30,12 @@ const SearchModal = ({
   dataExportDefaultTime,
   timeOptions,
   handleInputChange,
-  t
+  t,
 }) => {
   const formRef = useRef();
 
   const FORM_FIELD_PROPS = {
-    className: "w-full mb-2 !rounded-lg",
+    className: 'w-full mb-2 !rounded-lg',
   };
 
   const createFormField = (Component, props) => (
@@ -54,7 +54,7 @@ const SearchModal = ({
       size={isMobile ? 'full-width' : 'small'}
       centered
     >
-      <Form ref={formRef} layout='vertical' className="w-full">
+      <Form ref={formRef} layout='vertical' className='w-full'>
         {createFormField(Form.DatePicker, {
           field: 'start_timestamp',
           label: t('起始时间'),
@@ -62,7 +62,7 @@ const SearchModal = ({
           value: start_timestamp,
           type: 'dateTime',
           name: 'start_timestamp',
-          onChange: (value) => handleInputChange(value, 'start_timestamp')
+          onChange: (value) => handleInputChange(value, 'start_timestamp'),
         })}
 
         {createFormField(Form.DatePicker, {
@@ -72,7 +72,7 @@ const SearchModal = ({
           value: end_timestamp,
           type: 'dateTime',
           name: 'end_timestamp',
-          onChange: (value) => handleInputChange(value, 'end_timestamp')
+          onChange: (value) => handleInputChange(value, 'end_timestamp'),
         })}
 
         {createFormField(Form.Select, {
@@ -82,20 +82,22 @@ const SearchModal = ({
           placeholder: t('时间粒度'),
           name: 'data_export_default_time',
           optionList: timeOptions,
-          onChange: (value) => handleInputChange(value, 'data_export_default_time')
+          onChange: (value) =>
+            handleInputChange(value, 'data_export_default_time'),
         })}
 
-        {isAdminUser && createFormField(Form.Input, {
-          field: 'username',
-          label: t('用户名称'),
-          value: username,
-          placeholder: t('可选值'),
-          name: 'username',
-          onChange: (value) => handleInputChange(value, 'username')
-        })}
+        {isAdminUser &&
+          createFormField(Form.Input, {
+            field: 'username',
+            label: t('用户名称'),
+            value: username,
+            placeholder: t('可选值'),
+            name: 'username',
+            onChange: (value) => handleInputChange(value, 'username'),
+          })}
       </Form>
     </Modal>
   );
 };
 
-export default SearchModal; 
+export default SearchModal;

+ 164 - 58
web/src/components/layout/Footer.jsx

@@ -40,85 +40,191 @@ const FooterBar = () => {
 
   const currentYear = new Date().getFullYear();
 
-  const customFooter = useMemo(() => (
-    <footer className="relative h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
-      <div className="absolute hidden md:block top-[204px] left-[-100px] w-[151px] h-[151px] rounded-full bg-[#FFD166]"></div>
-      <div className="absolute md:hidden bottom-[20px] left-[-50px] w-[80px] h-[80px] rounded-full bg-[#FFD166] opacity-60"></div>
-
-      {isDemoSiteMode && (
-        <div className="flex flex-col md:flex-row justify-between w-full max-w-[1110px] mb-10 gap-8">
-          <div className="flex-shrink-0">
-            <img
-              src={logo}
-              alt={systemName}
-              className="w-16 h-16 rounded-full bg-gray-800 p-1.5 object-contain"
-            />
-          </div>
+  const customFooter = useMemo(
+    () => (
+      <footer className='relative h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden'>
+        <div className='absolute hidden md:block top-[204px] left-[-100px] w-[151px] h-[151px] rounded-full bg-[#FFD166]'></div>
+        <div className='absolute md:hidden bottom-[20px] left-[-50px] w-[80px] h-[80px] rounded-full bg-[#FFD166] opacity-60'></div>
+
+        {isDemoSiteMode && (
+          <div className='flex flex-col md:flex-row justify-between w-full max-w-[1110px] mb-10 gap-8'>
+            <div className='flex-shrink-0'>
+              <img
+                src={logo}
+                alt={systemName}
+                className='w-16 h-16 rounded-full bg-gray-800 p-1.5 object-contain'
+              />
+            </div>
 
-          <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 w-full">
-            <div className="text-left">
-              <p className="!text-semi-color-text-0 font-semibold mb-5">{t('关于我们')}</p>
-              <div className="flex flex-col gap-4">
-                <a href="https://docs.newapi.pro/wiki/project-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('关于项目')}</a>
-                <a href="https://docs.newapi.pro/support/community-interaction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('联系我们')}</a>
-                <a href="https://docs.newapi.pro/wiki/features-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('功能特性')}</a>
+            <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 w-full'>
+              <div className='text-left'>
+                <p className='!text-semi-color-text-0 font-semibold mb-5'>
+                  {t('关于我们')}
+                </p>
+                <div className='flex flex-col gap-4'>
+                  <a
+                    href='https://docs.newapi.pro/wiki/project-introduction/'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    {t('关于项目')}
+                  </a>
+                  <a
+                    href='https://docs.newapi.pro/support/community-interaction/'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    {t('联系我们')}
+                  </a>
+                  <a
+                    href='https://docs.newapi.pro/wiki/features-introduction/'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    {t('功能特性')}
+                  </a>
+                </div>
               </div>
-            </div>
 
-            <div className="text-left">
-              <p className="!text-semi-color-text-0 font-semibold mb-5">{t('文档')}</p>
-              <div className="flex flex-col gap-4">
-                <a href="https://docs.newapi.pro/getting-started/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('快速开始')}</a>
-                <a href="https://docs.newapi.pro/installation/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('安装指南')}</a>
-                <a href="https://docs.newapi.pro/api/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('API 文档')}</a>
+              <div className='text-left'>
+                <p className='!text-semi-color-text-0 font-semibold mb-5'>
+                  {t('文档')}
+                </p>
+                <div className='flex flex-col gap-4'>
+                  <a
+                    href='https://docs.newapi.pro/getting-started/'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    {t('快速开始')}
+                  </a>
+                  <a
+                    href='https://docs.newapi.pro/installation/'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    {t('安装指南')}
+                  </a>
+                  <a
+                    href='https://docs.newapi.pro/api/'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    {t('API 文档')}
+                  </a>
+                </div>
               </div>
-            </div>
 
-            <div className="text-left">
-              <p className="!text-semi-color-text-0 font-semibold mb-5">{t('相关项目')}</p>
-              <div className="flex flex-col gap-4">
-                <a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">One API</a>
-                <a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">Midjourney-Proxy</a>
-                <a href="https://github.com/Deeptrain-Community/chatnio" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">chatnio</a>
-                <a href="https://github.com/Calcium-Ion/neko-api-key-tool" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">neko-api-key-tool</a>
+              <div className='text-left'>
+                <p className='!text-semi-color-text-0 font-semibold mb-5'>
+                  {t('相关项目')}
+                </p>
+                <div className='flex flex-col gap-4'>
+                  <a
+                    href='https://github.com/songquanpeng/one-api'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    One API
+                  </a>
+                  <a
+                    href='https://github.com/novicezk/midjourney-proxy'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    Midjourney-Proxy
+                  </a>
+                  <a
+                    href='https://github.com/Deeptrain-Community/chatnio'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    chatnio
+                  </a>
+                  <a
+                    href='https://github.com/Calcium-Ion/neko-api-key-tool'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    neko-api-key-tool
+                  </a>
+                </div>
               </div>
-            </div>
 
-            <div className="text-left">
-              <p className="!text-semi-color-text-0 font-semibold mb-5">{t('基于New API的项目')}</p>
-              <div className="flex flex-col gap-4">
-                <a href="https://github.com/Calcium-Ion/new-api-horizon" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">new-api-horizon</a>
-                {/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">VoAPI</a> */}
+              <div className='text-left'>
+                <p className='!text-semi-color-text-0 font-semibold mb-5'>
+                  {t('基于New API的项目')}
+                </p>
+                <div className='flex flex-col gap-4'>
+                  <a
+                    href='https://github.com/Calcium-Ion/new-api-horizon'
+                    target='_blank'
+                    rel='noopener noreferrer'
+                    className='!text-semi-color-text-1'
+                  >
+                    new-api-horizon
+                  </a>
+                  {/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">VoAPI</a> */}
+                </div>
               </div>
             </div>
           </div>
-        </div>
-      )}
+        )}
 
-      <div className="flex flex-col md:flex-row items-center justify-between w-full max-w-[1110px] gap-6">
-        <div className="flex flex-wrap items-center gap-2">
-          <Typography.Text className="text-sm !text-semi-color-text-1">© {currentYear} {systemName}. {t('版权所有')}</Typography.Text>
-        </div>
+        <div className='flex flex-col md:flex-row items-center justify-between w-full max-w-[1110px] gap-6'>
+          <div className='flex flex-wrap items-center gap-2'>
+            <Typography.Text className='text-sm !text-semi-color-text-1'>
+              © {currentYear} {systemName}. {t('版权所有')}
+            </Typography.Text>
+          </div>
 
-        <div className="text-sm">
-          <span className="!text-semi-color-text-1">{t('设计与开发由')} </span>
-          <a href="https://github.com/QuantumNous/new-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-primary font-medium">New API</a>
-          <span className="!text-semi-color-text-1"> & </span>
-          <a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-primary font-medium">One API</a>
+          <div className='text-sm'>
+            <span className='!text-semi-color-text-1'>
+              {t('设计与开发由')}{' '}
+            </span>
+            <a
+              href='https://github.com/QuantumNous/new-api'
+              target='_blank'
+              rel='noopener noreferrer'
+              className='!text-semi-color-primary font-medium'
+            >
+              New API
+            </a>
+            <span className='!text-semi-color-text-1'> & </span>
+            <a
+              href='https://github.com/songquanpeng/one-api'
+              target='_blank'
+              rel='noopener noreferrer'
+              className='!text-semi-color-primary font-medium'
+            >
+              One API
+            </a>
+          </div>
         </div>
-      </div>
-    </footer>
-  ), [logo, systemName, t, currentYear, isDemoSiteMode]);
+      </footer>
+    ),
+    [logo, systemName, t, currentYear, isDemoSiteMode],
+  );
 
   useEffect(() => {
     loadFooter();
   }, []);
 
   return (
-    <div className="w-full">
+    <div className='w-full'>
       {footer ? (
         <div
-          className="custom-footer"
+          className='custom-footer'
           dangerouslySetInnerHTML={{ __html: footer }}
         ></div>
       ) : (

+ 2 - 6
web/src/components/layout/HeaderBar/ActionButtons.jsx

@@ -41,7 +41,7 @@ const ActionButtons = ({
   t,
 }) => {
   return (
-    <div className="flex items-center gap-2 md:gap-3">
+    <div className='flex items-center gap-2 md:gap-3'>
       <NewYearButton isNewYear={isNewYear} />
 
       <NotificationButton
@@ -50,11 +50,7 @@ const ActionButtons = ({
         t={t}
       />
 
-      <ThemeToggle
-        theme={theme}
-        onThemeToggle={onThemeToggle}
-        t={t}
-      />
+      <ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
 
       <LanguageSelector
         currentLang={currentLang}

+ 14 - 14
web/src/components/layout/HeaderBar/HeaderLogo.jsx

@@ -38,35 +38,35 @@ const HeaderLogo = ({
   }
 
   return (
-    <Link to="/" className="group flex items-center gap-2">
-      <div className="relative w-8 h-8 md:w-8 md:h-8">
-        <SkeletonWrapper
-          loading={isLoading || !logoLoaded}
-          type="image"
-        />
+    <Link to='/' className='group flex items-center gap-2'>
+      <div className='relative w-8 h-8 md:w-8 md:h-8'>
+        <SkeletonWrapper loading={isLoading || !logoLoaded} type='image' />
         <img
           src={logo}
-          alt="logo"
-          className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${(!isLoading && logoLoaded) ? 'opacity-100' : 'opacity-0'}`}
+          alt='logo'
+          className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${!isLoading && logoLoaded ? 'opacity-100' : 'opacity-0'}`}
         />
       </div>
-      <div className="hidden md:flex items-center gap-2">
-        <div className="flex items-center gap-2">
+      <div className='hidden md:flex items-center gap-2'>
+        <div className='flex items-center gap-2'>
           <SkeletonWrapper
             loading={isLoading}
-            type="title"
+            type='title'
             width={120}
             height={24}
           >
-            <Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
+            <Typography.Title
+              heading={4}
+              className='!text-lg !font-semibold !mb-0'
+            >
               {systemName}
             </Typography.Title>
           </SkeletonWrapper>
           {(isSelfUseMode || isDemoSiteMode) && !isLoading && (
             <Tag
               color={isSelfUseMode ? 'purple' : 'blue'}
-              className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
-              size="small"
+              className='text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm'
+              size='small'
               shape='circle'
             >
               {isSelfUseMode ? t('自用模式') : t('演示站点')}

+ 7 - 7
web/src/components/layout/HeaderBar/LanguageSelector.jsx

@@ -25,21 +25,21 @@ import { CN, GB } from 'country-flag-icons/react/3x2';
 const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
   return (
     <Dropdown
-      position="bottomRight"
+      position='bottomRight'
       render={
-        <Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
+        <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
           <Dropdown.Item
             onClick={() => onLanguageChange('zh')}
             className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
           >
-            <CN title="中文" className="!w-5 !h-auto" />
+            <CN title='中文' className='!w-5 !h-auto' />
             <span>中文</span>
           </Dropdown.Item>
           <Dropdown.Item
             onClick={() => onLanguageChange('en')}
             className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'en' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
           >
-            <GB title="English" className="!w-5 !h-auto" />
+            <GB title='English' className='!w-5 !h-auto' />
             <span>English</span>
           </Dropdown.Item>
         </Dropdown.Menu>
@@ -48,9 +48,9 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
       <Button
         icon={<Languages size={18} />}
         aria-label={t('切换语言')}
-        theme="borderless"
-        type="tertiary"
-        className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
+        theme='borderless'
+        type='tertiary'
+        className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
       />
     </Dropdown>
   );

+ 11 - 5
web/src/components/layout/HeaderBar/MobileMenuButton.jsx

@@ -36,13 +36,19 @@ const MobileMenuButton = ({
   return (
     <Button
       icon={
-        (isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />
+        (isMobile ? drawerOpen : collapsed) ? (
+          <IconClose className='text-lg' />
+        ) : (
+          <IconMenu className='text-lg' />
+        )
+      }
+      aria-label={
+        (isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')
       }
-      aria-label={(isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')}
       onClick={onToggle}
-      theme="borderless"
-      type="tertiary"
-      className="!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700"
+      theme='borderless'
+      type='tertiary'
+      className='!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700'
     />
   );
 };

+ 6 - 15
web/src/components/layout/HeaderBar/Navigation.jsx

@@ -21,21 +21,16 @@ import React from 'react';
 import { Link } from 'react-router-dom';
 import SkeletonWrapper from './SkeletonWrapper';
 
-const Navigation = ({
-  mainNavLinks,
-  isMobile,
-  isLoading,
-  userState
-}) => {
+const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => {
   const renderNavLinks = () => {
-    const baseClasses = 'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';
+    const baseClasses =
+      'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';
     const hoverClasses = 'hover:text-semi-color-primary';
     const spacingClasses = isMobile ? 'p-1' : 'p-2';
 
     const commonLinkClasses = `${baseClasses} ${spacingClasses} ${hoverClasses}`;
 
     return mainNavLinks.map((link) => {
-
       const linkContent = <span>{link.text}</span>;
 
       if (link.isExternal) {
@@ -58,11 +53,7 @@ const Navigation = ({
       }
 
       return (
-        <Link
-          key={link.itemKey}
-          to={targetPath}
-          className={commonLinkClasses}
-        >
+        <Link key={link.itemKey} to={targetPath} className={commonLinkClasses}>
           {linkContent}
         </Link>
       );
@@ -70,10 +61,10 @@ const Navigation = ({
   };
 
   return (
-    <nav className="flex flex-1 items-center gap-1 lg:gap-2 mx-2 md:mx-4 overflow-x-auto whitespace-nowrap scrollbar-hide">
+    <nav className='flex flex-1 items-center gap-1 lg:gap-2 mx-2 md:mx-4 overflow-x-auto whitespace-nowrap scrollbar-hide'>
       <SkeletonWrapper
         loading={isLoading}
-        type="navigation"
+        type='navigation'
         count={4}
         width={60}
         height={16}

+ 11 - 8
web/src/components/layout/HeaderBar/NewYearButton.jsx

@@ -36,21 +36,24 @@ const NewYearButton = ({ isNewYear }) => {
 
   return (
     <Dropdown
-      position="bottomRight"
+      position='bottomRight'
       render={
-        <Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
-          <Dropdown.Item onClick={handleNewYearClick} className="!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-gray-600">
+        <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
+          <Dropdown.Item
+            onClick={handleNewYearClick}
+            className='!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-gray-600'
+          >
             Happy New Year!!! 🎉
           </Dropdown.Item>
         </Dropdown.Menu>
       }
     >
       <Button
-        theme="borderless"
-        type="tertiary"
-        icon={<span className="text-xl">🎉</span>}
-        aria-label="New Year"
-        className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full"
+        theme='borderless'
+        type='tertiary'
+        icon={<span className='text-xl'>🎉</span>}
+        aria-label='New Year'
+        className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full'
       />
     </Dropdown>
   );

+ 5 - 4
web/src/components/layout/HeaderBar/NotificationButton.jsx

@@ -26,14 +26,15 @@ const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
     icon: <Bell size={18} />,
     'aria-label': t('系统公告'),
     onClick: onNoticeOpen,
-    theme: "borderless",
-    type: "tertiary",
-    className: "!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2",
+    theme: 'borderless',
+    type: 'tertiary',
+    className:
+      '!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2',
   };
 
   if (unreadCount > 0) {
     return (
-      <Badge count={unreadCount} type="danger" overflowCount={99}>
+      <Badge count={unreadCount} type='danger' overflowCount={99}>
         <Button {...buttonProps} />
       </Badge>
     );

+ 9 - 15
web/src/components/layout/HeaderBar/SkeletonWrapper.jsx

@@ -62,13 +62,17 @@ const SkeletonWrapper = ({
   // 用户区域骨架屏 (头像 + 文本)
   const renderUserAreaSkeleton = () => {
     return (
-      <div className={`flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 ${className}`}>
+      <div
+        className={`flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 ${className}`}
+      >
         <Skeleton
           loading={true}
           active
-          placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
+          placeholder={
+            <Skeleton.Avatar active size='extra-small' className='shadow-sm' />
+          }
         />
-        <div className="ml-1.5 mr-1">
+        <div className='ml-1.5 mr-1'>
           <Skeleton
             loading={true}
             active
@@ -107,12 +111,7 @@ const SkeletonWrapper = ({
       <Skeleton
         loading={true}
         active
-        placeholder={
-          <Skeleton.Title
-            active
-            style={{ width, height: 24 }}
-          />
-        }
+        placeholder={<Skeleton.Title active style={{ width, height: 24 }} />}
       />
     );
   };
@@ -124,12 +123,7 @@ const SkeletonWrapper = ({
         <Skeleton
           loading={true}
           active
-          placeholder={
-            <Skeleton.Title
-              active
-              style={{ width, height }}
-            />
-          }
+          placeholder={<Skeleton.Title active style={{ width, height }} />}
         />
       </div>
     );

+ 36 - 32
web/src/components/layout/HeaderBar/ThemeToggle.jsx

@@ -25,29 +25,32 @@ import { useActualTheme } from '../../../context/Theme';
 const ThemeToggle = ({ theme, onThemeToggle, t }) => {
   const actualTheme = useActualTheme();
 
-  const themeOptions = useMemo(() => ([
-    {
-      key: 'light',
-      icon: <Sun size={18} />,
-      buttonIcon: <Sun size={18} />,
-      label: t('浅色模式'),
-      description: t('始终使用浅色主题')
-    },
-    {
-      key: 'dark',
-      icon: <Moon size={18} />,
-      buttonIcon: <Moon size={18} />,
-      label: t('深色模式'),
-      description: t('始终使用深色主题')
-    },
-    {
-      key: 'auto',
-      icon: <Monitor size={18} />,
-      buttonIcon: <Monitor size={18} />,
-      label: t('自动模式'),
-      description: t('跟随系统主题设置')
-    }
-  ]), [t]);
+  const themeOptions = useMemo(
+    () => [
+      {
+        key: 'light',
+        icon: <Sun size={18} />,
+        buttonIcon: <Sun size={18} />,
+        label: t('浅色模式'),
+        description: t('始终使用浅色主题'),
+      },
+      {
+        key: 'dark',
+        icon: <Moon size={18} />,
+        buttonIcon: <Moon size={18} />,
+        label: t('深色模式'),
+        description: t('始终使用深色主题'),
+      },
+      {
+        key: 'auto',
+        icon: <Monitor size={18} />,
+        buttonIcon: <Monitor size={18} />,
+        label: t('自动模式'),
+        description: t('跟随系统主题设置'),
+      },
+    ],
+    [t],
+  );
 
   const getItemClassName = (isSelected) =>
     isSelected
@@ -55,13 +58,13 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
       : 'hover:!bg-semi-color-fill-1';
 
   const currentButtonIcon = useMemo(() => {
-    const currentOption = themeOptions.find(option => option.key === theme);
+    const currentOption = themeOptions.find((option) => option.key === theme);
     return currentOption?.buttonIcon || themeOptions[2].buttonIcon;
   }, [theme, themeOptions]);
 
   return (
     <Dropdown
-      position="bottomRight"
+      position='bottomRight'
       render={
         <Dropdown.Menu>
           {themeOptions.map((option) => (
@@ -71,9 +74,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
               onClick={() => onThemeToggle(option.key)}
               className={getItemClassName(theme === option.key)}
             >
-              <div className="flex flex-col">
+              <div className='flex flex-col'>
                 <span>{option.label}</span>
-                <span className="text-xs text-semi-color-text-2">
+                <span className='text-xs text-semi-color-text-2'>
                   {option.description}
                 </span>
               </div>
@@ -83,8 +86,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
           {theme === 'auto' && (
             <>
               <Dropdown.Divider />
-              <div className="px-3 py-2 text-xs text-semi-color-text-2">
-                {t('当前跟随系统')}:{actualTheme === 'dark' ? t('深色') : t('浅色')}
+              <div className='px-3 py-2 text-xs text-semi-color-text-2'>
+                {t('当前跟随系统')}:
+                {actualTheme === 'dark' ? t('深色') : t('浅色')}
               </div>
             </>
           )}
@@ -94,9 +98,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
       <Button
         icon={currentButtonIcon}
         aria-label={t('切换主题')}
-        theme="borderless"
-        type="tertiary"
-        className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1"
+        theme='borderless'
+        type='tertiary'
+        className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
       />
     </Dropdown>
   );

+ 63 - 51
web/src/components/layout/HeaderBar/UserArea.jsx

@@ -19,12 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React from 'react';
 import { Link } from 'react-router-dom';
-import {
-  Avatar,
-  Button,
-  Dropdown,
-  Typography,
-} from '@douyinfe/semi-ui';
+import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
 import { ChevronDown } from 'lucide-react';
 import {
   IconExit,
@@ -48,7 +43,7 @@ const UserArea = ({
     return (
       <SkeletonWrapper
         loading={true}
-        type="userArea"
+        type='userArea'
         width={50}
         isMobile={isMobile}
       />
@@ -58,17 +53,20 @@ const UserArea = ({
   if (userState.user) {
     return (
       <Dropdown
-        position="bottomRight"
+        position='bottomRight'
         render={
-          <Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
+          <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
             <Dropdown.Item
               onClick={() => {
                 navigate('/console/personal');
               }}
-              className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
+              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
             >
-              <div className="flex items-center gap-2">
-                <IconUserSetting size="small" className="text-gray-500 dark:text-gray-400" />
+              <div className='flex items-center gap-2'>
+                <IconUserSetting
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
                 <span>{t('个人设置')}</span>
               </div>
             </Dropdown.Item>
@@ -76,10 +74,13 @@ const UserArea = ({
               onClick={() => {
                 navigate('/console/token');
               }}
-              className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
+              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
             >
-              <div className="flex items-center gap-2">
-                <IconKey size="small" className="text-gray-500 dark:text-gray-400" />
+              <div className='flex items-center gap-2'>
+                <IconKey
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
                 <span>{t('令牌管理')}</span>
               </div>
             </Dropdown.Item>
@@ -87,16 +88,25 @@ const UserArea = ({
               onClick={() => {
                 navigate('/console/topup');
               }}
-              className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
+              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
             >
-              <div className="flex items-center gap-2">
-                <IconCreditCard size="small" className="text-gray-500 dark:text-gray-400" />
+              <div className='flex items-center gap-2'>
+                <IconCreditCard
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
                 <span>{t('钱包管理')}</span>
               </div>
             </Dropdown.Item>
-            <Dropdown.Item onClick={logout} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white">
-              <div className="flex items-center gap-2">
-                <IconExit size="small" className="text-gray-500 dark:text-gray-400" />
+            <Dropdown.Item
+              onClick={logout}
+              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white'
+            >
+              <div className='flex items-center gap-2'>
+                <IconExit
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
                 <span>{t('退出')}</span>
               </div>
             </Dropdown.Item>
@@ -104,74 +114,76 @@ const UserArea = ({
         }
       >
         <Button
-          theme="borderless"
-          type="tertiary"
-          className="flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
+          theme='borderless'
+          type='tertiary'
+          className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
         >
           <Avatar
-            size="extra-small"
+            size='extra-small'
             color={stringToColor(userState.user.username)}
-            className="mr-1"
+            className='mr-1'
           >
             {userState.user.username[0].toUpperCase()}
           </Avatar>
-          <span className="hidden md:inline">
-            <Typography.Text className="!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1">
+          <span className='hidden md:inline'>
+            <Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
               {userState.user.username}
             </Typography.Text>
           </span>
-          <ChevronDown size={14} className="text-xs text-semi-color-text-2 dark:text-gray-400" />
+          <ChevronDown
+            size={14}
+            className='text-xs text-semi-color-text-2 dark:text-gray-400'
+          />
         </Button>
       </Dropdown>
     );
   } else {
     const showRegisterButton = !isSelfUseMode;
 
-    const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5";
+    const commonSizingAndLayoutClass =
+      'flex items-center justify-center !py-[10px] !px-1.5';
 
-    const loginButtonSpecificStyling = "!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors";
+    const loginButtonSpecificStyling =
+      '!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors';
     let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`;
 
     let registerButtonClasses = `${commonSizingAndLayoutClass}`;
 
-    const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5";
-    const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
+    const loginButtonTextSpanClass =
+      '!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5';
+    const registerButtonTextSpanClass = '!text-xs !text-white !p-1.5';
 
     if (showRegisterButton) {
       if (isMobile) {
-        loginButtonClasses += " !rounded-full";
+        loginButtonClasses += ' !rounded-full';
       } else {
-        loginButtonClasses += " !rounded-l-full !rounded-r-none";
+        loginButtonClasses += ' !rounded-l-full !rounded-r-none';
       }
-      registerButtonClasses += " !rounded-r-full !rounded-l-none";
+      registerButtonClasses += ' !rounded-r-full !rounded-l-none';
     } else {
-      loginButtonClasses += " !rounded-full";
+      loginButtonClasses += ' !rounded-full';
     }
 
     return (
-      <div className="flex items-center">
-        <Link to="/login" className="flex">
+      <div className='flex items-center'>
+        <Link to='/login' className='flex'>
           <Button
-            theme="borderless"
-            type="tertiary"
+            theme='borderless'
+            type='tertiary'
             className={loginButtonClasses}
           >
-            <span className={loginButtonTextSpanClass}>
-              {t('登录')}
-            </span>
+            <span className={loginButtonTextSpanClass}>{t('登录')}</span>
           </Button>
         </Link>
         {showRegisterButton && (
-          <div className="hidden md:block">
-            <Link to="/register" className="flex -ml-px">
+          <div className='hidden md:block'>
+            <Link to='/register' className='flex -ml-px'>
               <Button
-                theme="solid"
-                type="primary"
+                theme='solid'
+                type='primary'
                 className={registerButtonClasses}
               >
-                <span className={registerButtonTextSpanClass}>
-                  {t('注册')}
-                </span>
+                <span className={registerButtonTextSpanClass}>{t('注册')}</span>
               </Button>
             </Link>
           </div>

+ 4 - 4
web/src/components/layout/HeaderBar/index.jsx

@@ -63,7 +63,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
   const { mainNavLinks } = useNavigation(t, docsLink);
 
   return (
-    <header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
+    <header className='text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg'>
       <NoticeModal
         visible={noticeVisible}
         onClose={handleNoticeClose}
@@ -72,9 +72,9 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
         unreadKeys={getUnreadKeys()}
       />
 
-      <div className="w-full px-2">
-        <div className="flex items-center justify-between h-16">
-          <div className="flex items-center">
+      <div className='w-full px-2'>
+        <div className='flex items-center justify-between h-16'>
+          <div className='flex items-center'>
             <MobileMenuButton
               isConsoleRoute={isConsoleRoute}
               isMobile={isMobile}

+ 87 - 41
web/src/components/layout/NoticeModal.jsx

@@ -18,15 +18,31 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useEffect, useState, useContext, useMemo } from 'react';
-import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
+import {
+  Button,
+  Modal,
+  Empty,
+  Tabs,
+  TabPane,
+  Timeline,
+} from '@douyinfe/semi-ui';
 import { useTranslation } from 'react-i18next';
 import { API, showError, getRelativeTime } from '../../helpers';
 import { marked } from 'marked';
-import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
+import {
+  IllustrationNoContent,
+  IllustrationNoContentDark,
+} from '@douyinfe/semi-illustrations';
 import { StatusContext } from '../../context/Status';
 import { Bell, Megaphone } from 'lucide-react';
 
-const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadKeys = [] }) => {
+const NoticeModal = ({
+  visible,
+  onClose,
+  isMobile,
+  defaultTab = 'inApp',
+  unreadKeys = [],
+}) => {
   const { t } = useTranslation();
   const [noticeContent, setNoticeContent] = useState('');
   const [loading, setLoading] = useState(false);
@@ -38,23 +54,25 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
 
   const unreadSet = useMemo(() => new Set(unreadKeys), [unreadKeys]);
 
-  const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
+  const getKeyForItem = (item) =>
+    `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
 
   const processedAnnouncements = useMemo(() => {
-    return (announcements || []).slice(0, 20).map(item => {
+    return (announcements || []).slice(0, 20).map((item) => {
       const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
-      const absoluteTime = pubDate && !isNaN(pubDate.getTime())
-        ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
-        : (item?.publishDate || '');
-      return ({
+      const absoluteTime =
+        pubDate && !isNaN(pubDate.getTime())
+          ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
+          : item?.publishDate || '';
+      return {
         key: getKeyForItem(item),
         type: item.type || 'default',
         time: absoluteTime,
         content: item.content,
         extra: item.extra,
         relative: getRelativeTime(item.publishDate),
-        isUnread: unreadSet.has(getKeyForItem(item))
-      });
+        isUnread: unreadSet.has(getKeyForItem(item)),
+      };
     });
   }, [announcements, unreadSet]);
 
@@ -100,15 +118,23 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
 
   const renderMarkdownNotice = () => {
     if (loading) {
-      return <div className="py-12"><Empty description={t('加载中...')} /></div>;
+      return (
+        <div className='py-12'>
+          <Empty description={t('加载中...')} />
+        </div>
+      );
     }
 
     if (!noticeContent) {
       return (
-        <div className="py-12">
+        <div className='py-12'>
           <Empty
-            image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
-            darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
+            image={
+              <IllustrationNoContent style={{ width: 150, height: 150 }} />
+            }
+            darkModeImage={
+              <IllustrationNoContentDark style={{ width: 150, height: 150 }} />
+            }
             description={t('暂无公告')}
           />
         </div>
@@ -118,7 +144,7 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
     return (
       <div
         dangerouslySetInnerHTML={{ __html: noticeContent }}
-        className="notice-content-scroll max-h-[55vh] overflow-y-auto pr-2"
+        className='notice-content-scroll max-h-[55vh] overflow-y-auto pr-2'
       />
     );
   };
@@ -126,10 +152,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
   const renderAnnouncementTimeline = () => {
     if (processedAnnouncements.length === 0) {
       return (
-        <div className="py-12">
+        <div className='py-12'>
           <Empty
-            image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
-            darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
+            image={
+              <IllustrationNoContent style={{ width: 150, height: 150 }} />
+            }
+            darkModeImage={
+              <IllustrationNoContentDark style={{ width: 150, height: 150 }} />
+            }
             description={t('暂无系统公告')}
           />
         </div>
@@ -137,8 +167,8 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
     }
 
     return (
-      <div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
-        <Timeline mode="left">
+      <div className='max-h-[55vh] overflow-y-auto pr-2 card-content-scroll'>
+        <Timeline mode='left'>
           {processedAnnouncements.map((item, idx) => {
             const htmlContent = marked.parse(item.content || '');
             const htmlExtra = item.extra ? marked.parse(item.extra) : '';
@@ -147,12 +177,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
                 key={idx}
                 type={item.type}
                 time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
-                extra={item.extra ? (
-                  <div
-                    className="text-xs text-gray-500"
-                    dangerouslySetInnerHTML={{ __html: htmlExtra }}
-                  />
-                ) : null}
+                extra={
+                  item.extra ? (
+                    <div
+                      className='text-xs text-gray-500'
+                      dangerouslySetInnerHTML={{ __html: htmlExtra }}
+                    />
+                  ) : null
+                }
                 className={item.isUnread ? '' : ''}
               >
                 <div>
@@ -179,26 +211,40 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
   return (
     <Modal
       title={
-        <div className="flex items-center justify-between w-full">
+        <div className='flex items-center justify-between w-full'>
           <span>{t('系统公告')}</span>
-          <Tabs
-            activeKey={activeTab}
-            onChange={setActiveTab}
-            type='button'
-          >
-            <TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' />
-            <TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' />
+          <Tabs activeKey={activeTab} onChange={setActiveTab} type='button'>
+            <TabPane
+              tab={
+                <span className='flex items-center gap-1'>
+                  <Bell size={14} /> {t('通知')}
+                </span>
+              }
+              itemKey='inApp'
+            />
+            <TabPane
+              tab={
+                <span className='flex items-center gap-1'>
+                  <Megaphone size={14} /> {t('系统公告')}
+                </span>
+              }
+              itemKey='system'
+            />
           </Tabs>
         </div>
       }
       visible={visible}
       onCancel={onClose}
-      footer={(
-        <div className="flex justify-end">
-          <Button type='secondary' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
-          <Button type="primary" onClick={onClose}>{t('关闭公告')}</Button>
+      footer={
+        <div className='flex justify-end'>
+          <Button type='secondary' onClick={handleCloseTodayNotice}>
+            {t('今日关闭')}
+          </Button>
+          <Button type='primary' onClick={onClose}>
+            {t('关闭公告')}
+          </Button>
         </div>
-      )}
+      }
       size={isMobile ? 'full-width' : 'large'}
     >
       {renderBody()}
@@ -206,4 +252,4 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
   );
 };
 
-export default NoticeModal; 
+export default NoticeModal;

+ 26 - 6
web/src/components/layout/PageLayout.jsx

@@ -27,7 +27,13 @@ import React, { useContext, useEffect, useState } from 'react';
 import { useIsMobile } from '../../hooks/common/useIsMobile';
 import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
 import { useTranslation } from 'react-i18next';
-import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers';
+import {
+  API,
+  getLogo,
+  getSystemName,
+  showError,
+  setStatusData,
+} from '../../helpers';
 import { UserContext } from '../../context/User';
 import { StatusContext } from '../../context/Status';
 import { useLocation } from 'react-router-dom';
@@ -42,9 +48,12 @@ const PageLayout = () => {
   const { i18n } = useTranslation();
   const location = useLocation();
 
-  const shouldHideFooter = location.pathname.startsWith('/console') || location.pathname === '/pricing';
+  const shouldHideFooter =
+    location.pathname.startsWith('/console') ||
+    location.pathname === '/pricing';
 
-  const shouldInnerPadding = location.pathname.includes('/console') &&
+  const shouldInnerPadding =
+    location.pathname.includes('/console') &&
     !location.pathname.startsWith('/console/chat') &&
     location.pathname !== '/console/playground';
 
@@ -120,7 +129,10 @@ const PageLayout = () => {
           zIndex: 100,
         }}
       >
-        <HeaderBar onMobileMenuToggle={() => setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
+        <HeaderBar
+          onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
+          drawerOpen={drawerOpen}
+        />
       </Header>
       <Layout
         style={{
@@ -142,12 +154,20 @@ const PageLayout = () => {
               width: 'var(--sidebar-current-width)',
             }}
           >
-            <SiderBar onNavigate={() => { if (isMobile) setDrawerOpen(false); }} />
+            <SiderBar
+              onNavigate={() => {
+                if (isMobile) setDrawerOpen(false);
+              }}
+            />
           </Sider>
         )}
         <Layout
           style={{
-            marginLeft: isMobile ? '0' : showSider ? 'var(--sidebar-current-width)' : '0',
+            marginLeft: isMobile
+              ? '0'
+              : showSider
+                ? 'var(--sidebar-current-width)'
+                : '0',
             flex: '1 1 auto',
             display: 'flex',
             flexDirection: 'column',

+ 5 - 2
web/src/components/layout/SetupCheck.js

@@ -26,7 +26,10 @@ const SetupCheck = ({ children }) => {
   const location = useLocation();
 
   useEffect(() => {
-    if (statusState?.status?.setup === false && location.pathname !== '/setup') {
+    if (
+      statusState?.status?.setup === false &&
+      location.pathname !== '/setup'
+    ) {
       window.location.href = '/setup';
     }
   }, [statusState?.status?.setup, location.pathname]);
@@ -34,4 +37,4 @@ const SetupCheck = ({ children }) => {
   return children;
 };
 
-export default SetupCheck; 
+export default SetupCheck;

+ 46 - 41
web/src/components/layout/SiderBar.jsx

@@ -23,17 +23,9 @@ import { useTranslation } from 'react-i18next';
 import { getLucideIcon } from '../../helpers/render';
 import { ChevronLeft } from 'lucide-react';
 import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
-import {
-  isAdmin,
-  isRoot,
-  showError
-} from '../../helpers';
-
-import {
-  Nav,
-  Divider,
-  Button,
-} from '@douyinfe/semi-ui';
+import { isAdmin, isRoot, showError } from '../../helpers';
+
+import { Nav, Divider, Button } from '@douyinfe/semi-ui';
 
 const routerMap = {
   home: '/',
@@ -54,7 +46,7 @@ const routerMap = {
   personal: '/console/personal',
 };
 
-const SiderBar = ({ onNavigate = () => { } }) => {
+const SiderBar = ({ onNavigate = () => {} }) => {
   const { t } = useTranslation();
   const [collapsed, toggleCollapsed] = useSidebarCollapsed();
 
@@ -275,14 +267,17 @@ const SiderBar = ({ onNavigate = () => { } }) => {
         key={item.itemKey}
         itemKey={item.itemKey}
         text={
-          <div className="flex items-center">
-            <span className="truncate font-medium text-sm" style={{ color: textColor }}>
+          <div className='flex items-center'>
+            <span
+              className='truncate font-medium text-sm'
+              style={{ color: textColor }}
+            >
               {item.text}
             </span>
           </div>
         }
         icon={
-          <div className="sidebar-icon-container flex-shrink-0">
+          <div className='sidebar-icon-container flex-shrink-0'>
             {getLucideIcon(item.itemKey, isSelected)}
           </div>
         }
@@ -302,14 +297,17 @@ const SiderBar = ({ onNavigate = () => { } }) => {
           key={item.itemKey}
           itemKey={item.itemKey}
           text={
-            <div className="flex items-center">
-              <span className="truncate font-medium text-sm" style={{ color: textColor }}>
+            <div className='flex items-center'>
+              <span
+                className='truncate font-medium text-sm'
+                style={{ color: textColor }}
+              >
                 {item.text}
               </span>
             </div>
           }
           icon={
-            <div className="sidebar-icon-container flex-shrink-0">
+            <div className='sidebar-icon-container flex-shrink-0'>
               {getLucideIcon(item.itemKey, isSelected)}
             </div>
           }
@@ -323,7 +321,10 @@ const SiderBar = ({ onNavigate = () => { } }) => {
                 key={subItem.itemKey}
                 itemKey={subItem.itemKey}
                 text={
-                  <span className="truncate font-medium text-sm" style={{ color: subTextColor }}>
+                  <span
+                    className='truncate font-medium text-sm'
+                    style={{ color: subTextColor }}
+                  >
                     {subItem.text}
                   </span>
                 }
@@ -339,18 +340,18 @@ const SiderBar = ({ onNavigate = () => { } }) => {
 
   return (
     <div
-      className="sidebar-container"
+      className='sidebar-container'
       style={{ width: 'var(--sidebar-current-width)' }}
     >
       <Nav
-        className="sidebar-nav"
+        className='sidebar-nav'
         defaultIsCollapsed={collapsed}
         isCollapsed={collapsed}
         onCollapseChange={toggleCollapsed}
         selectedKeys={selectedKeys}
-        itemStyle="sidebar-nav-item"
-        hoverStyle="sidebar-nav-item:hover"
-        selectedStyle="sidebar-nav-item-selected"
+        itemStyle='sidebar-nav-item'
+        hoverStyle='sidebar-nav-item:hover'
+        selectedStyle='sidebar-nav-item-selected'
         renderWrapper={({ itemElement, props }) => {
           const to = routerMapState[props.itemKey] || routerMap[props.itemKey];
 
@@ -381,27 +382,25 @@ const SiderBar = ({ onNavigate = () => { } }) => {
         }}
       >
         {/* 聊天区域 */}
-        <div className="sidebar-section">
-          {!collapsed && (
-            <div className="sidebar-group-label">{t('聊天')}</div>
-          )}
+        <div className='sidebar-section'>
+          {!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>}
           {chatMenuItems.map((item) => renderSubItem(item))}
         </div>
 
         {/* 控制台区域 */}
-        <Divider className="sidebar-divider" />
+        <Divider className='sidebar-divider' />
         <div>
           {!collapsed && (
-            <div className="sidebar-group-label">{t('控制台')}</div>
+            <div className='sidebar-group-label'>{t('控制台')}</div>
           )}
           {workspaceItems.map((item) => renderNavItem(item))}
         </div>
 
         {/* 个人中心区域 */}
-        <Divider className="sidebar-divider" />
+        <Divider className='sidebar-divider' />
         <div>
           {!collapsed && (
-            <div className="sidebar-group-label">{t('个人中心')}</div>
+            <div className='sidebar-group-label'>{t('个人中心')}</div>
           )}
           {financeItems.map((item) => renderNavItem(item))}
         </div>
@@ -409,10 +408,10 @@ const SiderBar = ({ onNavigate = () => { } }) => {
         {/* 管理员区域 - 只在管理员时显示 */}
         {isAdmin() && (
           <>
-            <Divider className="sidebar-divider" />
+            <Divider className='sidebar-divider' />
             <div>
               {!collapsed && (
-                <div className="sidebar-group-label">{t('管理员')}</div>
+                <div className='sidebar-group-label'>{t('管理员')}</div>
               )}
               {adminItems.map((item) => renderNavItem(item))}
             </div>
@@ -421,22 +420,28 @@ const SiderBar = ({ onNavigate = () => { } }) => {
       </Nav>
 
       {/* 底部折叠按钮 */}
-      <div className="sidebar-collapse-button">
+      <div className='sidebar-collapse-button'>
         <Button
-          theme="outline"
-          type="tertiary"
-          size="small"
+          theme='outline'
+          type='tertiary'
+          size='small'
           icon={
             <ChevronLeft
               size={16}
               strokeWidth={2.5}
-              color="var(--semi-color-text-2)"
-              style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
+              color='var(--semi-color-text-2)'
+              style={{
+                transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)',
+              }}
             />
           }
           onClick={toggleCollapsed}
           icononly={collapsed}
-          style={collapsed ? { padding: '4px', width: '100%' } : { padding: '4px 12px', width: '100%' }}
+          style={
+            collapsed
+              ? { padding: '4px', width: '100%' }
+              : { padding: '4px 12px', width: '100%' }
+          }
         >
           {!collapsed ? t('收起侧边栏') : null}
         </Button>

+ 27 - 30
web/src/components/playground/ChatArea.jsx

@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import {
-  Card,
-  Chat,
-  Typography,
-  Button,
-} from '@douyinfe/semi-ui';
-import {
-  MessageSquare,
-  Eye,
-  EyeOff,
-} from 'lucide-react';
+import { Card, Chat, Typography, Button } from '@douyinfe/semi-ui';
+import { MessageSquare, Eye, EyeOff } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import CustomInputRender from './CustomInputRender';
 
@@ -57,37 +48,43 @@ const ChatArea = ({
 
   return (
     <Card
-      className="h-full"
+      className='h-full'
       bordered={false}
-      bodyStyle={{ padding: 0, height: 'calc(100vh - 66px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
+      bodyStyle={{
+        padding: 0,
+        height: 'calc(100vh - 66px)',
+        display: 'flex',
+        flexDirection: 'column',
+        overflow: 'hidden',
+      }}
     >
       {/* 聊天头部 */}
       {styleState.isMobile ? (
-        <div className="pt-4"></div>
+        <div className='pt-4'></div>
       ) : (
-        <div className="px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl">
-          <div className="flex items-center justify-between">
-            <div className="flex items-center gap-3">
-              <div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center">
-                <MessageSquare size={20} className="text-white" />
+        <div className='px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl'>
+          <div className='flex items-center justify-between'>
+            <div className='flex items-center gap-3'>
+              <div className='w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center'>
+                <MessageSquare size={20} className='text-white' />
               </div>
               <div>
-                <Typography.Title heading={5} className="!text-white mb-0">
+                <Typography.Title heading={5} className='!text-white mb-0'>
                   {t('AI 对话')}
                 </Typography.Title>
-                <Typography.Text className="!text-white/80 text-sm hidden sm:inline">
+                <Typography.Text className='!text-white/80 text-sm hidden sm:inline'>
                   {inputs.model || t('选择模型开始对话')}
                 </Typography.Text>
               </div>
             </div>
-            <div className="flex items-center gap-2">
+            <div className='flex items-center gap-2'>
               <Button
                 icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
                 onClick={onToggleDebugPanel}
-                theme="borderless"
-                type="primary"
-                size="small"
-                className="!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10"
+                theme='borderless'
+                type='primary'
+                size='small'
+                className='!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10'
               >
                 {showDebugPanel ? t('隐藏调试') : t('显示调试')}
               </Button>
@@ -97,7 +94,7 @@ const ChatArea = ({
       )}
 
       {/* 聊天内容区域 */}
-      <div className="flex-1 overflow-hidden">
+      <div className='flex-1 overflow-hidden'>
         <Chat
           ref={chatRef}
           chatBoxRenderConfig={{
@@ -110,7 +107,7 @@ const ChatArea = ({
           style={{
             height: '100%',
             maxWidth: '100%',
-            overflow: 'hidden'
+            overflow: 'hidden',
           }}
           chats={message}
           onMessageSend={onMessageSend}
@@ -121,7 +118,7 @@ const ChatArea = ({
           showStopGenerate
           onStopGenerator={onStopGenerator}
           onClear={onClearMessages}
-          className="h-full"
+          className='h-full'
           placeholder={t('请输入您的问题...')}
         />
       </div>
@@ -129,4 +126,4 @@ const ChatArea = ({
   );
 };
 
-export default ChatArea; 
+export default ChatArea;

+ 71 - 46
web/src/components/playground/CodeViewer.jsx

@@ -102,15 +102,17 @@ const highlightJson = (str) => {
         color = '#569cd6';
       }
       return `<span style="color: ${color}">${match}</span>`;
-    }
+    },
   );
 };
 
 const isJsonLike = (content, language) => {
   if (language === 'json') return true;
   const trimmed = content.trim();
-  return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
-    (trimmed.startsWith('[') && trimmed.endsWith(']'));
+  return (
+    (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
+    (trimmed.startsWith('[') && trimmed.endsWith(']'))
+  );
 };
 
 const formatContent = (content) => {
@@ -148,7 +150,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
   const contentMetrics = useMemo(() => {
     const length = formattedContent.length;
     const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH;
-    const isVeryLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
+    const isVeryLarge =
+      length >
+      PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH *
+        PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
     return { length, isLarge, isVeryLarge };
   }, [formattedContent.length]);
 
@@ -156,8 +161,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
     if (!contentMetrics.isLarge || isExpanded) {
       return formattedContent;
     }
-    return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
-      '\n\n// ... 内容被截断以提升性能 ...';
+    return (
+      formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
+      '\n\n// ... 内容被截断以提升性能 ...'
+    );
   }, [formattedContent, contentMetrics.isLarge, isExpanded]);
 
   const highlightedContent = useMemo(() => {
@@ -174,9 +181,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
 
   const handleCopy = useCallback(async () => {
     try {
-      const textToCopy = typeof content === 'object' && content !== null
-        ? JSON.stringify(content, null, 2)
-        : content;
+      const textToCopy =
+        typeof content === 'object' && content !== null
+          ? JSON.stringify(content, null, 2)
+          : content;
 
       const success = await copy(textToCopy);
       setCopied(true);
@@ -205,11 +213,12 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
   }, [isExpanded, contentMetrics.isVeryLarge]);
 
   if (!content) {
-    const placeholderText = {
-      preview: t('正在构造请求体预览...'),
-      request: t('暂无请求数据'),
-      response: t('暂无响应数据')
-    }[title] || t('暂无数据');
+    const placeholderText =
+      {
+        preview: t('正在构造请求体预览...'),
+        request: t('暂无请求数据'),
+        response: t('暂无响应数据'),
+      }[title] || t('暂无数据');
 
     return (
       <div style={codeThemeStyles.noContent}>
@@ -222,7 +231,7 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
   const contentPadding = contentMetrics.isLarge ? '52px' : '16px';
 
   return (
-    <div style={codeThemeStyles.container} className="h-full">
+    <div style={codeThemeStyles.container} className='h-full'>
       {/* 性能警告 */}
       {contentMetrics.isLarge && (
         <div style={codeThemeStyles.performanceWarning}>
@@ -250,8 +259,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
           <Button
             icon={<Copy size={14} />}
             onClick={handleCopy}
-            size="small"
-            theme="borderless"
+            size='small'
+            theme='borderless'
             style={{
               backgroundColor: 'transparent',
               border: 'none',
@@ -268,25 +277,29 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
           ...codeThemeStyles.content,
           paddingTop: contentPadding,
         }}
-        className="model-settings-scroll"
+        className='model-settings-scroll'
       >
         {isProcessing ? (
-          <div style={{
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            height: '200px',
-            color: '#888'
-          }}>
-            <div style={{
-              width: '20px',
-              height: '20px',
-              border: '2px solid #444',
-              borderTop: '2px solid #888',
-              borderRadius: '50%',
-              animation: 'spin 1s linear infinite',
-              marginRight: '8px'
-            }} />
+          <div
+            style={{
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              height: '200px',
+              color: '#888',
+            }}
+          >
+            <div
+              style={{
+                width: '20px',
+                height: '20px',
+                border: '2px solid #444',
+                borderTop: '2px solid #888',
+                borderRadius: '50%',
+                animation: 'spin 1s linear infinite',
+                marginRight: '8px',
+              }}
+            />
             {t('正在处理大内容...')}
           </div>
         ) : (
@@ -296,18 +309,22 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
 
       {/* 展开/收起按钮 */}
       {contentMetrics.isLarge && !isProcessing && (
-        <div style={{
-          ...codeThemeStyles.actionButton,
-          bottom: '12px',
-          left: '50%',
-          transform: 'translateX(-50%)',
-        }}>
+        <div
+          style={{
+            ...codeThemeStyles.actionButton,
+            bottom: '12px',
+            left: '50%',
+            transform: 'translateX(-50%)',
+          }}
+        >
           <Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}>
             <Button
-              icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
+              icon={
+                isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />
+              }
               onClick={handleToggleExpand}
-              size="small"
-              theme="borderless"
+              size='small'
+              theme='borderless'
               style={{
                 backgroundColor: 'transparent',
                 border: 'none',
@@ -317,8 +334,16 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
             >
               {isExpanded ? t('收起') : t('展开')}
               {!isExpanded && (
-                <span style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}>
-                  (+{Math.round((contentMetrics.length - PERFORMANCE_CONFIG.PREVIEW_LENGTH) / 1000)}K)
+                <span
+                  style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}
+                >
+                  (+
+                  {Math.round(
+                    (contentMetrics.length -
+                      PERFORMANCE_CONFIG.PREVIEW_LENGTH) /
+                      1000,
+                  )}
+                  K)
                 </span>
               )}
             </Button>
@@ -329,4 +354,4 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
   );
 };
 
-export default CodeViewer; 
+export default CodeViewer;

+ 49 - 47
web/src/components/playground/ConfigManager.jsx

@@ -18,21 +18,16 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useRef } from 'react';
-import {
-  Button,
-  Typography,
-  Toast,
-  Modal,
-  Dropdown,
-} from '@douyinfe/semi-ui';
-import {
-  Download,
-  Upload,
-  RotateCcw,
-  Settings2,
-} from 'lucide-react';
+import { Button, Typography, Toast, Modal, Dropdown } from '@douyinfe/semi-ui';
+import { Download, Upload, RotateCcw, Settings2 } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
-import { exportConfig, importConfig, clearConfig, hasStoredConfig, getConfigTimestamp } from './configStorage';
+import {
+  exportConfig,
+  importConfig,
+  clearConfig,
+  hasStoredConfig,
+  getConfigTimestamp,
+} from './configStorage';
 
 const ConfigManager = ({
   currentConfig,
@@ -51,7 +46,10 @@ const ConfigManager = ({
         ...currentConfig,
         timestamp: new Date().toISOString(),
       };
-      localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp));
+      localStorage.setItem(
+        'playground_config',
+        JSON.stringify(configWithTimestamp),
+      );
 
       exportConfig(currentConfig, messages);
       Toast.success({
@@ -104,7 +102,9 @@ const ConfigManager = ({
   const handleReset = () => {
     Modal.confirm({
       title: t('重置配置'),
-      content: t('将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?'),
+      content: t(
+        '将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?',
+      ),
       okText: t('确定重置'),
       cancelText: t('取消'),
       okButtonProps: {
@@ -114,7 +114,9 @@ const ConfigManager = ({
         // 询问是否同时重置消息
         Modal.confirm({
           title: t('重置选项'),
-          content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'),
+          content: t(
+            '是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。',
+          ),
           okText: t('同时重置消息'),
           cancelText: t('仅重置配置'),
           okButtonProps: {
@@ -159,7 +161,7 @@ const ConfigManager = ({
       name: 'export',
       onClick: handleExport,
       children: (
-        <div className="flex items-center gap-2">
+        <div className='flex items-center gap-2'>
           <Download size={14} />
           {t('导出配置')}
         </div>
@@ -170,7 +172,7 @@ const ConfigManager = ({
       name: 'import',
       onClick: handleImportClick,
       children: (
-        <div className="flex items-center gap-2">
+        <div className='flex items-center gap-2'>
           <Upload size={14} />
           {t('导入配置')}
         </div>
@@ -184,7 +186,7 @@ const ConfigManager = ({
       name: 'reset',
       onClick: handleReset,
       children: (
-        <div className="flex items-center gap-2 text-red-600">
+        <div className='flex items-center gap-2 text-red-600'>
           <RotateCcw size={14} />
           {t('重置配置')}
         </div>
@@ -197,24 +199,24 @@ const ConfigManager = ({
     return (
       <>
         <Dropdown
-          trigger="click"
-          position="bottomLeft"
+          trigger='click'
+          position='bottomLeft'
           showTick
           menu={dropdownItems}
         >
           <Button
             icon={<Settings2 size={14} />}
-            theme="borderless"
-            type="tertiary"
-            size="small"
-            className="!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50"
+            theme='borderless'
+            type='tertiary'
+            size='small'
+            className='!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50'
           />
         </Dropdown>
 
         <input
           ref={fileInputRef}
-          type="file"
-          accept=".json"
+          type='file'
+          accept='.json'
           onChange={handleFileChange}
           style={{ display: 'none' }}
         />
@@ -224,42 +226,42 @@ const ConfigManager = ({
 
   // 桌面端显示紧凑的按钮组
   return (
-    <div className="space-y-3">
+    <div className='space-y-3'>
       {/* 配置状态信息和重置按钮 */}
-      <div className="flex items-center justify-between">
-        <Typography.Text className="text-xs text-gray-500">
+      <div className='flex items-center justify-between'>
+        <Typography.Text className='text-xs text-gray-500'>
           {getConfigStatus()}
         </Typography.Text>
         <Button
           icon={<RotateCcw size={12} />}
-          size="small"
-          theme="borderless"
-          type="danger"
+          size='small'
+          theme='borderless'
+          type='danger'
           onClick={handleReset}
-          className="!rounded-full !text-xs !px-2"
+          className='!rounded-full !text-xs !px-2'
         />
       </div>
 
       {/* 导出和导入按钮 */}
-      <div className="flex gap-2">
+      <div className='flex gap-2'>
         <Button
           icon={<Download size={12} />}
-          size="small"
-          theme="solid"
-          type="primary"
+          size='small'
+          theme='solid'
+          type='primary'
           onClick={handleExport}
-          className="!rounded-lg flex-1 !text-xs !h-7"
+          className='!rounded-lg flex-1 !text-xs !h-7'
         >
           {t('导出')}
         </Button>
 
         <Button
           icon={<Upload size={12} />}
-          size="small"
-          theme="outline"
-          type="primary"
+          size='small'
+          theme='outline'
+          type='primary'
           onClick={handleImportClick}
-          className="!rounded-lg flex-1 !text-xs !h-7"
+          className='!rounded-lg flex-1 !text-xs !h-7'
         >
           {t('导入')}
         </Button>
@@ -267,8 +269,8 @@ const ConfigManager = ({
 
       <input
         ref={fileInputRef}
-        type="file"
-        accept=".json"
+        type='file'
+        accept='.json'
         onChange={handleFileChange}
         style={{ display: 'none' }}
       />
@@ -276,4 +278,4 @@ const ConfigManager = ({
   );
 };
 
-export default ConfigManager; 
+export default ConfigManager;

+ 19 - 20
web/src/components/playground/CustomInputRender.jsx

@@ -21,23 +21,24 @@ import React from 'react';
 
 const CustomInputRender = (props) => {
   const { detailProps } = props;
-  const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
+  const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
+    detailProps;
 
   // 清空按钮
   const styledClearNode = clearContextNode
     ? React.cloneElement(clearContextNode, {
-      className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`,
-      style: {
-        ...clearContextNode.props.style,
-        width: '32px',
-        height: '32px',
-        minWidth: '32px',
-        padding: 0,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-      }
-    })
+        className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`,
+        style: {
+          ...clearContextNode.props.style,
+          width: '32px',
+          height: '32px',
+          minWidth: '32px',
+          padding: 0,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+        },
+      })
     : null;
 
   // 发送按钮
@@ -52,21 +53,19 @@ const CustomInputRender = (props) => {
       display: 'flex',
       alignItems: 'center',
       justifyContent: 'center',
-    }
+    },
   });
 
   return (
-    <div className="p-2 sm:p-4">
+    <div className='p-2 sm:p-4'>
       <div
-        className="flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-xl sm:rounded-2xl shadow-sm hover:shadow-md transition-shadow"
+        className='flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-xl sm:rounded-2xl shadow-sm hover:shadow-md transition-shadow'
         style={{ border: '1px solid var(--semi-color-border)' }}
         onClick={onClick}
       >
         {/* 清空对话按钮 - 左边 */}
         {styledClearNode}
-        <div className="flex-1">
-          {inputNode}
-        </div>
+        <div className='flex-1'>{inputNode}</div>
         {/* 发送按钮 - 右边 */}
         {styledSendNode}
       </div>
@@ -74,4 +73,4 @@ const CustomInputRender = (props) => {
   );
 };
 
-export default CustomInputRender; 
+export default CustomInputRender;

+ 39 - 35
web/src/components/playground/CustomRequestEditor.jsx

@@ -25,13 +25,7 @@ import {
   Switch,
   Banner,
 } from '@douyinfe/semi-ui';
-import {
-  Code,
-  Edit,
-  Check,
-  X,
-  AlertTriangle,
-} from 'lucide-react';
+import { Code, Edit, Check, X, AlertTriangle } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 
 const CustomRequestEditor = ({
@@ -48,12 +42,22 @@ const CustomRequestEditor = ({
 
   // 当切换到自定义模式时,用默认payload初始化
   useEffect(() => {
-    if (customRequestMode && (!customRequestBody || customRequestBody.trim() === '')) {
-      const defaultJson = defaultPayload ? JSON.stringify(defaultPayload, null, 2) : '';
+    if (
+      customRequestMode &&
+      (!customRequestBody || customRequestBody.trim() === '')
+    ) {
+      const defaultJson = defaultPayload
+        ? JSON.stringify(defaultPayload, null, 2)
+        : '';
       setLocalValue(defaultJson);
       onCustomRequestBodyChange(defaultJson);
     }
-  }, [customRequestMode, defaultPayload, customRequestBody, onCustomRequestBodyChange]);
+  }, [
+    customRequestMode,
+    defaultPayload,
+    customRequestBody,
+    onCustomRequestBodyChange,
+  ]);
 
   // 同步外部传入的customRequestBody到本地状态
   useEffect(() => {
@@ -113,21 +117,21 @@ const CustomRequestEditor = ({
   };
 
   return (
-    <div className="space-y-4">
+    <div className='space-y-4'>
       {/* 自定义模式开关 */}
-      <div className="flex items-center justify-between">
-        <div className="flex items-center gap-2">
-          <Code size={16} className="text-gray-500" />
-          <Typography.Text strong className="text-sm">
+      <div className='flex items-center justify-between'>
+        <div className='flex items-center gap-2'>
+          <Code size={16} className='text-gray-500' />
+          <Typography.Text strong className='text-sm'>
             自定义请求体模式
           </Typography.Text>
         </div>
         <Switch
           checked={customRequestMode}
           onChange={handleModeToggle}
-          checkedText="开"
-          uncheckedText="关"
-          size="small"
+          checkedText='开'
+          uncheckedText='关'
+          size='small'
         />
       </div>
 
@@ -135,43 +139,43 @@ const CustomRequestEditor = ({
         <>
           {/* 提示信息 */}
           <Banner
-            type="warning"
-            description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
+            type='warning'
+            description='启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。'
             icon={<AlertTriangle size={16} />}
-            className="!rounded-lg"
+            className='!rounded-lg'
             closeIcon={null}
           />
 
           {/* JSON编辑器 */}
           <div>
-            <div className="flex items-center justify-between mb-2">
-              <Typography.Text strong className="text-sm">
+            <div className='flex items-center justify-between mb-2'>
+              <Typography.Text strong className='text-sm'>
                 请求体 JSON
               </Typography.Text>
-              <div className="flex items-center gap-2">
+              <div className='flex items-center gap-2'>
                 {isValid ? (
-                  <div className="flex items-center gap-1 text-green-600">
+                  <div className='flex items-center gap-1 text-green-600'>
                     <Check size={14} />
-                    <Typography.Text className="text-xs">
+                    <Typography.Text className='text-xs'>
                       格式正确
                     </Typography.Text>
                   </div>
                 ) : (
-                  <div className="flex items-center gap-1 text-red-600">
+                  <div className='flex items-center gap-1 text-red-600'>
                     <X size={14} />
-                    <Typography.Text className="text-xs">
+                    <Typography.Text className='text-xs'>
                       格式错误
                     </Typography.Text>
                   </div>
                 )}
                 <Button
-                  theme="borderless"
-                  type="tertiary"
-                  size="small"
+                  theme='borderless'
+                  type='tertiary'
+                  size='small'
                   icon={<Edit size={14} />}
                   onClick={formatJson}
                   disabled={!isValid}
-                  className="!rounded-lg"
+                  className='!rounded-lg'
                 >
                   格式化
                 </Button>
@@ -191,12 +195,12 @@ const CustomRequestEditor = ({
             />
 
             {!isValid && errorMessage && (
-              <Typography.Text type="danger" className="text-xs mt-1 block">
+              <Typography.Text type='danger' className='text-xs mt-1 block'>
                 {errorMessage}
               </Typography.Text>
             )}
 
-            <Typography.Text className="text-xs text-gray-500 mt-2 block">
+            <Typography.Text className='text-xs text-gray-500 mt-2 block'>
               请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。
             </Typography.Text>
           </div>
@@ -206,4 +210,4 @@ const CustomRequestEditor = ({
   );
 };
 
-export default CustomRequestEditor; 
+export default CustomRequestEditor;

+ 59 - 57
web/src/components/playground/DebugPanel.jsx

@@ -26,14 +26,7 @@ import {
   Button,
   Dropdown,
 } from '@douyinfe/semi-ui';
-import {
-  Code,
-  Zap,
-  Clock,
-  X,
-  Eye,
-  Send,
-} from 'lucide-react';
+import { Code, Zap, Clock, X, Eye, Send } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import CodeViewer from './CodeViewer';
 
@@ -76,7 +69,7 @@ const DebugPanel = ({
       <Dropdown
         render={
           <Dropdown.Menu>
-            {items.map(item => {
+            {items.map((item) => {
               return (
                 <Dropdown.Item
                   key={item.itemKey}
@@ -104,21 +97,21 @@ const DebugPanel = ({
 
   return (
     <Card
-      className="h-full flex flex-col"
+      className='h-full flex flex-col'
       bordered={false}
       bodyStyle={{
         padding: styleState.isMobile ? '16px' : '24px',
         height: '100%',
         display: 'flex',
-        flexDirection: 'column'
+        flexDirection: 'column',
       }}
     >
-      <div className="flex items-center justify-between mb-6 flex-shrink-0">
-        <div className="flex items-center">
-          <div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3">
-            <Code size={20} className="text-white" />
+      <div className='flex items-center justify-between mb-6 flex-shrink-0'>
+        <div className='flex items-center'>
+          <div className='w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3'>
+            <Code size={20} className='text-white' />
           </div>
-          <Typography.Title heading={5} className="mb-0">
+          <Typography.Title heading={5} className='mb-0'>
             {t('调试信息')}
           </Typography.Title>
         </div>
@@ -127,75 +120,84 @@ const DebugPanel = ({
           <Button
             icon={<X size={16} />}
             onClick={onCloseDebugPanel}
-            theme="borderless"
-            type="tertiary"
-            size="small"
-            className="!rounded-lg"
+            theme='borderless'
+            type='tertiary'
+            size='small'
+            className='!rounded-lg'
           />
         )}
       </div>
 
-      <div className="flex-1 overflow-hidden debug-panel">
+      <div className='flex-1 overflow-hidden debug-panel'>
         <Tabs
           renderArrow={renderArrow}
-          type="card"
+          type='card'
           collapsible
-          className="h-full"
+          className='h-full'
           style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
           activeKey={activeKey}
           onChange={handleTabChange}
         >
-          <TabPane tab={
-            <div className="flex items-center gap-2">
-              <Eye size={16} />
-              {t('预览请求体')}
-              {customRequestMode && (
-                <span className="px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full">
-                  自定义
-                </span>
-              )}
-            </div>
-          } itemKey="preview">
+          <TabPane
+            tab={
+              <div className='flex items-center gap-2'>
+                <Eye size={16} />
+                {t('预览请求体')}
+                {customRequestMode && (
+                  <span className='px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full'>
+                    自定义
+                  </span>
+                )}
+              </div>
+            }
+            itemKey='preview'
+          >
             <CodeViewer
               content={debugData.previewRequest}
-              title="preview"
-              language="json"
+              title='preview'
+              language='json'
             />
           </TabPane>
 
-          <TabPane tab={
-            <div className="flex items-center gap-2">
-              <Send size={16} />
-              {t('实际请求体')}
-            </div>
-          } itemKey="request">
+          <TabPane
+            tab={
+              <div className='flex items-center gap-2'>
+                <Send size={16} />
+                {t('实际请求体')}
+              </div>
+            }
+            itemKey='request'
+          >
             <CodeViewer
               content={debugData.request}
-              title="request"
-              language="json"
+              title='request'
+              language='json'
             />
           </TabPane>
 
-          <TabPane tab={
-            <div className="flex items-center gap-2">
-              <Zap size={16} />
-              {t('响应')}
-            </div>
-          } itemKey="response">
+          <TabPane
+            tab={
+              <div className='flex items-center gap-2'>
+                <Zap size={16} />
+                {t('响应')}
+              </div>
+            }
+            itemKey='response'
+          >
             <CodeViewer
               content={debugData.response}
-              title="response"
-              language="json"
+              title='response'
+              language='json'
             />
           </TabPane>
         </Tabs>
       </div>
 
-      <div className="flex items-center justify-between mt-4 pt-4 flex-shrink-0">
+      <div className='flex items-center justify-between mt-4 pt-4 flex-shrink-0'>
         {(debugData.timestamp || debugData.previewTimestamp) && (
-          <div className="flex items-center gap-2">
-            <Clock size={14} className="text-gray-500" />
-            <Typography.Text className="text-xs text-gray-500">
+          <div className='flex items-center gap-2'>
+            <Clock size={14} className='text-gray-500' />
+            <Typography.Text className='text-xs text-gray-500'>
               {activeKey === 'preview' && debugData.previewTimestamp
                 ? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}`
                 : debugData.timestamp
@@ -209,4 +211,4 @@ const DebugPanel = ({
   );
 };
 
-export default DebugPanel; 
+export default DebugPanel;

+ 6 - 10
web/src/components/playground/FloatingButtons.jsx

@@ -19,11 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React from 'react';
 import { Button } from '@douyinfe/semi-ui';
-import {
-  Settings,
-  Eye,
-  EyeOff,
-} from 'lucide-react';
+import { Settings, Eye, EyeOff } from 'lucide-react';
 
 const FloatingButtons = ({
   styleState,
@@ -55,7 +51,7 @@ const FloatingButtons = ({
           onClick={onToggleSettings}
           theme='solid'
           type='primary'
-          className="lg:hidden"
+          className='lg:hidden'
         />
       )}
 
@@ -64,8 +60,8 @@ const FloatingButtons = ({
         <Button
           icon={showDebugPanel ? <EyeOff size={18} /> : <Eye size={18} />}
           onClick={onToggleDebugPanel}
-          theme="solid"
-          type={showDebugPanel ? "danger" : "primary"}
+          theme='solid'
+          type={showDebugPanel ? 'danger' : 'primary'}
           style={{
             position: 'fixed',
             right: 16,
@@ -80,11 +76,11 @@ const FloatingButtons = ({
               ? 'linear-gradient(to right, #e11d48, #be123c)'
               : 'linear-gradient(to right, #4f46e5, #6366f1)',
           }}
-          className="lg:hidden"
+          className='lg:hidden'
         />
       )}
     </>
   );
 };
 
-export default FloatingButtons; 
+export default FloatingButtons;

+ 51 - 43
web/src/components/playground/ImageUrlInput.jsx

@@ -18,21 +18,17 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import {
-  Input,
-  Typography,
-  Button,
-  Switch,
-} from '@douyinfe/semi-ui';
+import { Input, Typography, Button, Switch } from '@douyinfe/semi-ui';
 import { IconFile } from '@douyinfe/semi-icons';
-import {
-  FileText,
-  Plus,
-  X,
-  Image,
-} from 'lucide-react';
+import { FileText, Plus, X, Image } from 'lucide-react';
 
-const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnabledChange, disabled = false }) => {
+const ImageUrlInput = ({
+  imageUrls,
+  imageEnabled,
+  onImageUrlsChange,
+  onImageEnabledChange,
+  disabled = false,
+}) => {
   const handleAddImageUrl = () => {
     const newUrls = [...imageUrls, ''];
     onImageUrlsChange(newUrls);
@@ -51,75 +47,87 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
 
   return (
     <div className={disabled ? 'opacity-50' : ''}>
-      <div className="flex items-center justify-between mb-2">
-        <div className="flex items-center gap-2">
-          <Image size={16} className={imageEnabled && !disabled ? "text-blue-500" : "text-gray-400"} />
-          <Typography.Text strong className="text-sm">
+      <div className='flex items-center justify-between mb-2'>
+        <div className='flex items-center gap-2'>
+          <Image
+            size={16}
+            className={
+              imageEnabled && !disabled ? 'text-blue-500' : 'text-gray-400'
+            }
+          />
+          <Typography.Text strong className='text-sm'>
             图片地址
           </Typography.Text>
           {disabled && (
-            <Typography.Text className="text-xs text-orange-600">
+            <Typography.Text className='text-xs text-orange-600'>
               (已在自定义模式中忽略)
             </Typography.Text>
           )}
         </div>
-        <div className="flex items-center gap-2">
+        <div className='flex items-center gap-2'>
           <Switch
             checked={imageEnabled}
             onChange={onImageEnabledChange}
-            checkedText="启用"
-            uncheckedText="停用"
-            size="small"
-            className="flex-shrink-0"
+            checkedText='启用'
+            uncheckedText='停用'
+            size='small'
+            className='flex-shrink-0'
             disabled={disabled}
           />
           <Button
             icon={<Plus size={14} />}
-            size="small"
-            theme="solid"
-            type="primary"
+            size='small'
+            theme='solid'
+            type='primary'
             onClick={handleAddImageUrl}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={!imageEnabled || disabled}
           />
         </div>
       </div>
 
       {!imageEnabled ? (
-        <Typography.Text className="text-xs text-gray-500 mb-2 block">
-          {disabled ? '图片功能在自定义请求体模式下不可用' : '启用后可添加图片URL进行多模态对话'}
+        <Typography.Text className='text-xs text-gray-500 mb-2 block'>
+          {disabled
+            ? '图片功能在自定义请求体模式下不可用'
+            : '启用后可添加图片URL进行多模态对话'}
         </Typography.Text>
       ) : imageUrls.length === 0 ? (
-        <Typography.Text className="text-xs text-gray-500 mb-2 block">
-          {disabled ? '图片功能在自定义请求体模式下不可用' : '点击 + 按钮添加图片URL进行多模态对话'}
+        <Typography.Text className='text-xs text-gray-500 mb-2 block'>
+          {disabled
+            ? '图片功能在自定义请求体模式下不可用'
+            : '点击 + 按钮添加图片URL进行多模态对话'}
         </Typography.Text>
       ) : (
-        <Typography.Text className="text-xs text-gray-500 mb-2 block">
-          已添加 {imageUrls.length} 张图片{disabled ? ' (自定义模式下不可用)' : ''}
+        <Typography.Text className='text-xs text-gray-500 mb-2 block'>
+          已添加 {imageUrls.length} 张图片
+          {disabled ? ' (自定义模式下不可用)' : ''}
         </Typography.Text>
       )}
 
-      <div className={`space-y-2 max-h-32 overflow-y-auto image-list-scroll ${!imageEnabled || disabled ? 'opacity-50' : ''}`}>
+      <div
+        className={`space-y-2 max-h-32 overflow-y-auto image-list-scroll ${!imageEnabled || disabled ? 'opacity-50' : ''}`}
+      >
         {imageUrls.map((url, index) => (
-          <div key={index} className="flex items-center gap-2">
-            <div className="flex-1">
+          <div key={index} className='flex items-center gap-2'>
+            <div className='flex-1'>
               <Input
                 placeholder={`https://example.com/image${index + 1}.jpg`}
                 value={url}
                 onChange={(value) => handleUpdateImageUrl(index, value)}
-                className="!rounded-lg"
-                size="small"
+                className='!rounded-lg'
+                size='small'
                 prefix={<IconFile size='small' />}
                 disabled={!imageEnabled || disabled}
               />
             </div>
             <Button
               icon={<X size={12} />}
-              size="small"
-              theme="borderless"
-              type="danger"
+              size='small'
+              theme='borderless'
+              type='danger'
               onClick={() => handleRemoveImageUrl(index)}
-              className="!rounded-full !w-6 !h-6 !p-0 !min-w-0 !text-red-500 hover:!bg-red-50 flex-shrink-0"
+              className='!rounded-full !w-6 !h-6 !p-0 !min-w-0 !text-red-500 hover:!bg-red-50 flex-shrink-0'
               disabled={!imageEnabled || disabled}
             />
           </div>
@@ -129,4 +137,4 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
   );
 };
 
-export default ImageUrlInput; 
+export default ImageUrlInput;

+ 51 - 39
web/src/components/playground/MessageActions.jsx

@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import {
-  Button,
-  Tooltip,
-} from '@douyinfe/semi-ui';
-import {
-  RefreshCw,
-  Copy,
-  Trash2,
-  UserCheck,
-  Edit,
-} from 'lucide-react';
+import { Button, Tooltip } from '@douyinfe/semi-ui';
+import { RefreshCw, Copy, Trash2, UserCheck, Edit } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 
 const MessageActions = ({
@@ -40,23 +31,32 @@ const MessageActions = ({
   onRoleToggle,
   onMessageEdit,
   isAnyMessageGenerating = false,
-  isEditing = false
+  isEditing = false,
 }) => {
   const { t } = useTranslation();
 
-  const isLoading = message.status === 'loading' || message.status === 'incomplete';
+  const isLoading =
+    message.status === 'loading' || message.status === 'incomplete';
   const shouldDisableActions = isAnyMessageGenerating || isEditing;
-  const canToggleRole = message.role === 'assistant' || message.role === 'system';
-  const canEdit = !isLoading && message.content && typeof onMessageEdit === 'function' && !isEditing;
+  const canToggleRole =
+    message.role === 'assistant' || message.role === 'system';
+  const canEdit =
+    !isLoading &&
+    message.content &&
+    typeof onMessageEdit === 'function' &&
+    !isEditing;
 
   return (
-    <div className="flex items-center gap-0.5">
+    <div className='flex items-center gap-0.5'>
       {!isLoading && (
-        <Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')} position="top">
+        <Tooltip
+          content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')}
+          position='top'
+        >
           <Button
-            theme="borderless"
-            type="tertiary"
-            size="small"
+            theme='borderless'
+            type='tertiary'
+            size='small'
             icon={<RefreshCw size={styleState.isMobile ? 12 : 14} />}
             onClick={() => !shouldDisableActions && onMessageReset(message)}
             disabled={shouldDisableActions}
@@ -67,11 +67,11 @@ const MessageActions = ({
       )}
 
       {message.content && (
-        <Tooltip content={t('复制')} position="top">
+        <Tooltip content={t('复制')} position='top'>
           <Button
-            theme="borderless"
-            type="tertiary"
-            size="small"
+            theme='borderless'
+            type='tertiary'
+            size='small'
             icon={<Copy size={styleState.isMobile ? 12 : 14} />}
             onClick={() => onMessageCopy(message)}
             className={`!rounded-full !text-gray-400 hover:!text-green-600 hover:!bg-green-50 ${styleState.isMobile ? '!w-6 !h-6' : '!w-7 !h-7'} !p-0 transition-all`}
@@ -81,11 +81,14 @@ const MessageActions = ({
       )}
 
       {canEdit && (
-        <Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')} position="top">
+        <Tooltip
+          content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')}
+          position='top'
+        >
           <Button
-            theme="borderless"
-            type="tertiary"
-            size="small"
+            theme='borderless'
+            type='tertiary'
+            size='small'
             icon={<Edit size={styleState.isMobile ? 12 : 14} />}
             onClick={() => !shouldDisableActions && onMessageEdit(message)}
             disabled={shouldDisableActions}
@@ -104,27 +107,36 @@ const MessageActions = ({
                 ? t('切换为System角色')
                 : t('切换为Assistant角色')
           }
-          position="top"
+          position='top'
         >
           <Button
-            theme="borderless"
-            type="tertiary"
-            size="small"
+            theme='borderless'
+            type='tertiary'
+            size='small'
             icon={<UserCheck size={styleState.isMobile ? 12 : 14} />}
-            onClick={() => !shouldDisableActions && onRoleToggle && onRoleToggle(message)}
+            onClick={() =>
+              !shouldDisableActions && onRoleToggle && onRoleToggle(message)
+            }
             disabled={shouldDisableActions}
             className={`!rounded-full ${shouldDisableActions ? '!text-gray-300 !cursor-not-allowed' : message.role === 'system' ? '!text-purple-500 hover:!text-purple-700 hover:!bg-purple-50' : '!text-gray-400 hover:!text-purple-600 hover:!bg-purple-50'} ${styleState.isMobile ? '!w-6 !h-6' : '!w-7 !h-7'} !p-0 transition-all`}
-            aria-label={message.role === 'assistant' ? t('切换为System角色') : t('切换为Assistant角色')}
+            aria-label={
+              message.role === 'assistant'
+                ? t('切换为System角色')
+                : t('切换为Assistant角色')
+            }
           />
         </Tooltip>
       )}
 
       {!isLoading && (
-        <Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')} position="top">
+        <Tooltip
+          content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')}
+          position='top'
+        >
           <Button
-            theme="borderless"
-            type="tertiary"
-            size="small"
+            theme='borderless'
+            type='tertiary'
+            size='small'
             icon={<Trash2 size={styleState.isMobile ? 12 : 14} />}
             onClick={() => !shouldDisableActions && onMessageDelete(message)}
             disabled={shouldDisableActions}
@@ -137,4 +149,4 @@ const MessageActions = ({
   );
 };
 
-export default MessageActions; 
+export default MessageActions;

+ 114 - 73
web/src/components/playground/MessageContent.jsx

@@ -18,18 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useRef, useEffect } from 'react';
-import {
-  Typography,
-  TextArea,
-  Button,
-} from '@douyinfe/semi-ui';
+import { Typography, TextArea, Button } from '@douyinfe/semi-ui';
 import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
 import ThinkingContent from './ThinkingContent';
-import {
-  Loader2,
-  Check,
-  X,
-} from 'lucide-react';
+import { Loader2, Check, X } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 
 const MessageContent = ({
@@ -41,13 +33,14 @@ const MessageContent = ({
   onEditSave,
   onEditCancel,
   editValue,
-  onEditValueChange
+  onEditValueChange,
 }) => {
   const { t } = useTranslation();
   const previousContentLengthRef = useRef(0);
   const lastContentRef = useRef('');
 
-  const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
+  const isThinkingStatus =
+    message.status === 'loading' || message.status === 'incomplete';
 
   useEffect(() => {
     if (!isThinkingStatus) {
@@ -60,10 +53,11 @@ const MessageContent = ({
     let errorText;
 
     if (Array.isArray(message.content)) {
-      const textContent = message.content.find(item => item.type === 'text');
-      errorText = textContent && textContent.text && typeof textContent.text === 'string'
-        ? textContent.text
-        : t('请求发生错误');
+      const textContent = message.content.find((item) => item.type === 'text');
+      errorText =
+        textContent && textContent.text && typeof textContent.text === 'string'
+          ? textContent.text
+          : t('请求发生错误');
     } else if (typeof message.content === 'string') {
       errorText = message.content;
     } else {
@@ -72,21 +66,21 @@ const MessageContent = ({
 
     return (
       <div className={`${className}`}>
-        <Typography.Text className="text-white">
-          {errorText}
-        </Typography.Text>
+        <Typography.Text className='text-white'>{errorText}</Typography.Text>
       </div>
     );
   }
 
   let currentExtractedThinkingContent = null;
-  let currentDisplayableFinalContent = "";
+  let currentDisplayableFinalContent = '';
   let thinkingSource = null;
 
   const getTextContent = (content) => {
     if (Array.isArray(content)) {
-      const textItem = content.find(item => item.type === 'text');
-      return textItem && textItem.text && typeof textItem.text === 'string' ? textItem.text : '';
+      const textItem = content.find((item) => item.type === 'text');
+      return textItem && textItem.text && typeof textItem.text === 'string'
+        ? textItem.text
+        : '';
     } else if (typeof content === 'string') {
       return content;
     }
@@ -97,7 +91,7 @@ const MessageContent = ({
 
   if (message.role === 'assistant') {
     let baseContentForDisplay = getTextContent(message.content);
-    let combinedThinkingContent = "";
+    let combinedThinkingContent = '';
 
     if (message.reasoningContent) {
       combinedThinkingContent = message.reasoningContent;
@@ -112,7 +106,9 @@ const MessageContent = ({
       let lastIndex = 0;
 
       while ((match = thinkTagRegex.exec(baseContentForDisplay)) !== null) {
-        replyParts.push(baseContentForDisplay.substring(lastIndex, match.index));
+        replyParts.push(
+          baseContentForDisplay.substring(lastIndex, match.index),
+        );
         thoughtsFromPairedTags.push(match[1]);
         lastIndex = match.index + match[0].length;
       }
@@ -125,7 +121,9 @@ const MessageContent = ({
         } else {
           combinedThinkingContent = pairedThoughtsStr;
         }
-        thinkingSource = thinkingSource ? thinkingSource + ' & <think> tags' : '<think> tags';
+        thinkingSource = thinkingSource
+          ? thinkingSource + ' & <think> tags'
+          : '<think> tags';
       }
 
       baseContentForDisplay = replyParts.join('');
@@ -134,37 +132,55 @@ const MessageContent = ({
     if (isThinkingStatus) {
       const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>');
       if (lastOpenThinkIndex !== -1) {
-        const fragmentAfterLastOpen = baseContentForDisplay.substring(lastOpenThinkIndex);
+        const fragmentAfterLastOpen =
+          baseContentForDisplay.substring(lastOpenThinkIndex);
         if (!fragmentAfterLastOpen.includes('</think>')) {
-          const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
+          const unclosedThought = fragmentAfterLastOpen
+            .substring('<think>'.length)
+            .trim();
           if (unclosedThought) {
             if (combinedThinkingContent) {
               combinedThinkingContent += '\n\n---\n\n' + unclosedThought;
             } else {
               combinedThinkingContent = unclosedThought;
             }
-            thinkingSource = thinkingSource ? thinkingSource + ' + streaming <think>' : 'streaming <think>';
+            thinkingSource = thinkingSource
+              ? thinkingSource + ' + streaming <think>'
+              : 'streaming <think>';
           }
-          baseContentForDisplay = baseContentForDisplay.substring(0, lastOpenThinkIndex);
+          baseContentForDisplay = baseContentForDisplay.substring(
+            0,
+            lastOpenThinkIndex,
+          );
         }
       }
     }
 
     currentExtractedThinkingContent = combinedThinkingContent || null;
-    currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
+    currentDisplayableFinalContent = baseContentForDisplay
+      .replace(/<\/?think>/g, '')
+      .trim();
   }
 
   const finalExtractedThinkingContent = currentExtractedThinkingContent;
   const finalDisplayableFinalContent = currentDisplayableFinalContent;
 
-  if (message.role === 'assistant' &&
+  if (
+    message.role === 'assistant' &&
     isThinkingStatus &&
     !finalExtractedThinkingContent &&
-    (!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
+    (!finalDisplayableFinalContent ||
+      finalDisplayableFinalContent.trim() === '')
+  ) {
     return (
-      <div className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50`}>
-        <div className="w-5 h-5 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
-          <Loader2 className="animate-spin text-white" size={styleState.isMobile ? 16 : 20} />
+      <div
+        className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50`}
+      >
+        <div className='w-5 h-5 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg'>
+          <Loader2
+            className='animate-spin text-white'
+            size={styleState.isMobile ? 16 : 20}
+          />
         </div>
       </div>
     );
@@ -173,12 +189,17 @@ const MessageContent = ({
   return (
     <div className={className}>
       {message.role === 'system' && (
-        <div className="mb-2 sm:mb-4">
-          <div className="flex items-center gap-2 p-2 sm:p-3 bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg" style={{ border: '1px solid var(--semi-color-border)' }}>
-            <div className="w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-sm">
-              <Typography.Text className="text-white text-xs font-bold">S</Typography.Text>
+        <div className='mb-2 sm:mb-4'>
+          <div
+            className='flex items-center gap-2 p-2 sm:p-3 bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg'
+            style={{ border: '1px solid var(--semi-color-border)' }}
+          >
+            <div className='w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-sm'>
+              <Typography.Text className='text-white text-xs font-bold'>
+                S
+              </Typography.Text>
             </div>
-            <Typography.Text className="text-amber-700 text-xs sm:text-sm font-medium">
+            <Typography.Text className='text-amber-700 text-xs sm:text-sm font-medium'>
               {t('系统消息')}
             </Typography.Text>
           </div>
@@ -196,7 +217,7 @@ const MessageContent = ({
       )}
 
       {isEditing ? (
-        <div className="space-y-3">
+        <div className='space-y-3'>
           <TextArea
             value={editValue}
             onChange={(value) => onEditValueChange(value)}
@@ -207,27 +228,27 @@ const MessageContent = ({
               fontSize: styleState.isMobile ? '14px' : '15px',
               lineHeight: '1.6',
             }}
-            className="!border-blue-200 focus:!border-blue-400 !bg-blue-50/50"
+            className='!border-blue-200 focus:!border-blue-400 !bg-blue-50/50'
           />
-          <div className="flex items-center gap-2 w-full">
+          <div className='flex items-center gap-2 w-full'>
             <Button
-              size="small"
-              type="danger"
-              theme="light"
+              size='small'
+              type='danger'
+              theme='light'
               icon={<X size={14} />}
               onClick={onEditCancel}
-              className="flex-1"
+              className='flex-1'
             >
               {t('取消')}
             </Button>
             <Button
-              size="small"
-              type="warning"
-              theme="solid"
+              size='small'
+              type='warning'
+              theme='solid'
               icon={<Check size={14} />}
               onClick={onEditSave}
               disabled={!editValue || editValue.trim() === ''}
-              className="flex-1"
+              className='flex-1'
             >
               {t('保存')}
             </Button>
@@ -236,19 +257,23 @@ const MessageContent = ({
       ) : (
         (() => {
           if (Array.isArray(message.content)) {
-            const textContent = message.content.find(item => item.type === 'text');
-            const imageContents = message.content.filter(item => item.type === 'image_url');
+            const textContent = message.content.find(
+              (item) => item.type === 'text',
+            );
+            const imageContents = message.content.filter(
+              (item) => item.type === 'image_url',
+            );
 
             return (
               <div>
                 {imageContents.length > 0 && (
-                  <div className="mb-3 space-y-2">
+                  <div className='mb-3 space-y-2'>
                     {imageContents.map((imgItem, index) => (
-                      <div key={index} className="max-w-sm">
+                      <div key={index} className='max-w-sm'>
                         <img
                           src={imgItem.image_url.url}
                           alt={`用户上传的图片 ${index + 1}`}
-                          className="rounded-lg max-w-full h-auto shadow-sm border"
+                          className='rounded-lg max-w-full h-auto shadow-sm border'
                           style={{ maxHeight: '300px' }}
                           onError={(e) => {
                             e.target.style.display = 'none';
@@ -256,7 +281,7 @@ const MessageContent = ({
                           }}
                         />
                         <div
-                          className="text-red-500 text-sm p-2 bg-red-50 rounded-lg border border-red-200"
+                          className='text-red-500 text-sm p-2 bg-red-50 rounded-lg border border-red-200'
                           style={{ display: 'none' }}
                         >
                           图片加载失败: {imgItem.image_url.url}
@@ -266,28 +291,42 @@ const MessageContent = ({
                   </div>
                 )}
 
-                {textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
-                  <div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
-                    <MarkdownRenderer
-                      content={textContent.text}
-                      className={message.role === 'user' ? 'user-message' : ''}
-                      animated={false}
-                      previousContentLength={0}
-                    />
-                  </div>
-                )}
+                {textContent &&
+                  textContent.text &&
+                  typeof textContent.text === 'string' &&
+                  textContent.text.trim() !== '' && (
+                    <div
+                      className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}
+                    >
+                      <MarkdownRenderer
+                        content={textContent.text}
+                        className={
+                          message.role === 'user' ? 'user-message' : ''
+                        }
+                        animated={false}
+                        previousContentLength={0}
+                      />
+                    </div>
+                  )}
               </div>
             );
           }
 
           if (typeof message.content === 'string') {
             if (message.role === 'assistant') {
-              if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
+              if (
+                finalDisplayableFinalContent &&
+                finalDisplayableFinalContent.trim() !== ''
+              ) {
                 // 获取上一次的内容长度
                 let prevLength = 0;
                 if (isThinkingStatus && lastContentRef.current) {
                   // 只有当前内容包含上一次内容时,才使用上一次的长度
-                  if (finalDisplayableFinalContent.startsWith(lastContentRef.current)) {
+                  if (
+                    finalDisplayableFinalContent.startsWith(
+                      lastContentRef.current,
+                    )
+                  ) {
                     prevLength = lastContentRef.current.length;
                   }
                 }
@@ -298,10 +337,10 @@ const MessageContent = ({
                 }
 
                 return (
-                  <div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
+                  <div className='prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm'>
                     <MarkdownRenderer
                       content={finalDisplayableFinalContent}
-                      className=""
+                      className=''
                       animated={isThinkingStatus}
                       previousContentLength={prevLength}
                     />
@@ -310,7 +349,9 @@ const MessageContent = ({
               }
             } else {
               return (
-                <div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
+                <div
+                  className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}
+                >
                   <MarkdownRenderer
                     content={message.content}
                     className={message.role === 'user' ? 'user-message' : ''}
@@ -329,4 +370,4 @@ const MessageContent = ({
   );
 };
 
-export default MessageContent; 
+export default MessageContent;

+ 65 - 47
web/src/components/playground/OptimizedComponents.js

@@ -24,56 +24,74 @@ import SettingsPanel from './SettingsPanel';
 import DebugPanel from './DebugPanel';
 
 // 优化的消息内容组件
-export const OptimizedMessageContent = React.memo(MessageContent, (prevProps, nextProps) => {
-  // 只有这些属性变化时才重新渲染
-  return (
-    prevProps.message.id === nextProps.message.id &&
-    prevProps.message.content === nextProps.message.content &&
-    prevProps.message.status === nextProps.message.status &&
-    prevProps.message.role === nextProps.message.role &&
-    prevProps.message.reasoningContent === nextProps.message.reasoningContent &&
-    prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
-    prevProps.isEditing === nextProps.isEditing &&
-    prevProps.editValue === nextProps.editValue &&
-    prevProps.styleState.isMobile === nextProps.styleState.isMobile
-  );
-});
+export const OptimizedMessageContent = React.memo(
+  MessageContent,
+  (prevProps, nextProps) => {
+    // 只有这些属性变化时才重新渲染
+    return (
+      prevProps.message.id === nextProps.message.id &&
+      prevProps.message.content === nextProps.message.content &&
+      prevProps.message.status === nextProps.message.status &&
+      prevProps.message.role === nextProps.message.role &&
+      prevProps.message.reasoningContent ===
+        nextProps.message.reasoningContent &&
+      prevProps.message.isReasoningExpanded ===
+        nextProps.message.isReasoningExpanded &&
+      prevProps.isEditing === nextProps.isEditing &&
+      prevProps.editValue === nextProps.editValue &&
+      prevProps.styleState.isMobile === nextProps.styleState.isMobile
+    );
+  },
+);
 
 // 优化的消息操作组件
-export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, nextProps) => {
-  return (
-    prevProps.message.id === nextProps.message.id &&
-    prevProps.message.role === nextProps.message.role &&
-    prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating &&
-    prevProps.isEditing === nextProps.isEditing &&
-    prevProps.onMessageReset === nextProps.onMessageReset
-  );
-});
+export const OptimizedMessageActions = React.memo(
+  MessageActions,
+  (prevProps, nextProps) => {
+    return (
+      prevProps.message.id === nextProps.message.id &&
+      prevProps.message.role === nextProps.message.role &&
+      prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating &&
+      prevProps.isEditing === nextProps.isEditing &&
+      prevProps.onMessageReset === nextProps.onMessageReset
+    );
+  },
+);
 
 // 优化的设置面板组件
-export const OptimizedSettingsPanel = React.memo(SettingsPanel, (prevProps, nextProps) => {
-  return (
-    JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
-    JSON.stringify(prevProps.parameterEnabled) === JSON.stringify(nextProps.parameterEnabled) &&
-    JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
-    JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
-    prevProps.customRequestMode === nextProps.customRequestMode &&
-    prevProps.customRequestBody === nextProps.customRequestBody &&
-    prevProps.showDebugPanel === nextProps.showDebugPanel &&
-    prevProps.showSettings === nextProps.showSettings &&
-    JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
-    JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages)
-  );
-});
+export const OptimizedSettingsPanel = React.memo(
+  SettingsPanel,
+  (prevProps, nextProps) => {
+    return (
+      JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
+      JSON.stringify(prevProps.parameterEnabled) ===
+        JSON.stringify(nextProps.parameterEnabled) &&
+      JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
+      JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
+      prevProps.customRequestMode === nextProps.customRequestMode &&
+      prevProps.customRequestBody === nextProps.customRequestBody &&
+      prevProps.showDebugPanel === nextProps.showDebugPanel &&
+      prevProps.showSettings === nextProps.showSettings &&
+      JSON.stringify(prevProps.previewPayload) ===
+        JSON.stringify(nextProps.previewPayload) &&
+      JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages)
+    );
+  },
+);
 
 // 优化的调试面板组件
-export const OptimizedDebugPanel = React.memo(DebugPanel, (prevProps, nextProps) => {
-  return (
-    prevProps.show === nextProps.show &&
-    prevProps.activeTab === nextProps.activeTab &&
-    JSON.stringify(prevProps.debugData) === JSON.stringify(nextProps.debugData) &&
-    JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
-    prevProps.customRequestMode === nextProps.customRequestMode &&
-    prevProps.showDebugPanel === nextProps.showDebugPanel
-  );
-}); 
+export const OptimizedDebugPanel = React.memo(
+  DebugPanel,
+  (prevProps, nextProps) => {
+    return (
+      prevProps.show === nextProps.show &&
+      prevProps.activeTab === nextProps.activeTab &&
+      JSON.stringify(prevProps.debugData) ===
+        JSON.stringify(nextProps.debugData) &&
+      JSON.stringify(prevProps.previewPayload) ===
+        JSON.stringify(nextProps.previewPayload) &&
+      prevProps.customRequestMode === nextProps.customRequestMode &&
+      prevProps.showDebugPanel === nextProps.showDebugPanel
+    );
+  },
+);

+ 105 - 71
web/src/components/playground/ParameterControl.jsx

@@ -18,13 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import {
-  Input,
-  Slider,
-  Typography,
-  Button,
-  Tag,
-} from '@douyinfe/semi-ui';
+import { Input, Slider, Typography, Button, Tag } from '@douyinfe/semi-ui';
 import {
   Hash,
   Thermometer,
@@ -46,28 +40,36 @@ const ParameterControl = ({
   return (
     <>
       {/* Temperature */}
-      <div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.temperature || disabled ? 'opacity-50' : ''}`}>
-        <div className="flex items-center justify-between mb-2">
-          <div className="flex items-center gap-2">
-            <Thermometer size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+      <div
+        className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.temperature || disabled ? 'opacity-50' : ''}`}
+      >
+        <div className='flex items-center justify-between mb-2'>
+          <div className='flex items-center gap-2'>
+            <Thermometer size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               Temperature
             </Typography.Text>
-            <Tag size="small" shape='circle'>
+            <Tag size='small' shape='circle'>
               {inputs.temperature}
             </Tag>
           </div>
           <Button
             theme={parameterEnabled.temperature ? 'solid' : 'borderless'}
             type={parameterEnabled.temperature ? 'primary' : 'tertiary'}
-            size="small"
-            icon={parameterEnabled.temperature ? <Check size={10} /> : <X size={10} />}
+            size='small'
+            icon={
+              parameterEnabled.temperature ? (
+                <Check size={10} />
+              ) : (
+                <X size={10} />
+              )
+            }
             onClick={() => onParameterToggle('temperature')}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={disabled}
           />
         </div>
-        <Typography.Text className="text-xs text-gray-500 mb-2">
+        <Typography.Text className='text-xs text-gray-500 mb-2'>
           控制输出的随机性和创造性
         </Typography.Text>
         <Slider
@@ -76,34 +78,38 @@ const ParameterControl = ({
           max={1}
           value={inputs.temperature}
           onChange={(value) => onInputChange('temperature', value)}
-          className="mt-2"
+          className='mt-2'
           disabled={!parameterEnabled.temperature || disabled}
         />
       </div>
 
       {/* Top P */}
-      <div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.top_p || disabled ? 'opacity-50' : ''}`}>
-        <div className="flex items-center justify-between mb-2">
-          <div className="flex items-center gap-2">
-            <Target size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+      <div
+        className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.top_p || disabled ? 'opacity-50' : ''}`}
+      >
+        <div className='flex items-center justify-between mb-2'>
+          <div className='flex items-center gap-2'>
+            <Target size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               Top P
             </Typography.Text>
-            <Tag size="small" shape='circle'>
+            <Tag size='small' shape='circle'>
               {inputs.top_p}
             </Tag>
           </div>
           <Button
             theme={parameterEnabled.top_p ? 'solid' : 'borderless'}
             type={parameterEnabled.top_p ? 'primary' : 'tertiary'}
-            size="small"
-            icon={parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />}
+            size='small'
+            icon={
+              parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />
+            }
             onClick={() => onParameterToggle('top_p')}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={disabled}
           />
         </div>
-        <Typography.Text className="text-xs text-gray-500 mb-2">
+        <Typography.Text className='text-xs text-gray-500 mb-2'>
           核采样,控制词汇选择的多样性
         </Typography.Text>
         <Slider
@@ -112,34 +118,42 @@ const ParameterControl = ({
           max={1}
           value={inputs.top_p}
           onChange={(value) => onInputChange('top_p', value)}
-          className="mt-2"
+          className='mt-2'
           disabled={!parameterEnabled.top_p || disabled}
         />
       </div>
 
       {/* Frequency Penalty */}
-      <div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.frequency_penalty || disabled ? 'opacity-50' : ''}`}>
-        <div className="flex items-center justify-between mb-2">
-          <div className="flex items-center gap-2">
-            <Repeat size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+      <div
+        className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.frequency_penalty || disabled ? 'opacity-50' : ''}`}
+      >
+        <div className='flex items-center justify-between mb-2'>
+          <div className='flex items-center gap-2'>
+            <Repeat size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               Frequency Penalty
             </Typography.Text>
-            <Tag size="small" shape='circle'>
+            <Tag size='small' shape='circle'>
               {inputs.frequency_penalty}
             </Tag>
           </div>
           <Button
             theme={parameterEnabled.frequency_penalty ? 'solid' : 'borderless'}
             type={parameterEnabled.frequency_penalty ? 'primary' : 'tertiary'}
-            size="small"
-            icon={parameterEnabled.frequency_penalty ? <Check size={10} /> : <X size={10} />}
+            size='small'
+            icon={
+              parameterEnabled.frequency_penalty ? (
+                <Check size={10} />
+              ) : (
+                <X size={10} />
+              )
+            }
             onClick={() => onParameterToggle('frequency_penalty')}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={disabled}
           />
         </div>
-        <Typography.Text className="text-xs text-gray-500 mb-2">
+        <Typography.Text className='text-xs text-gray-500 mb-2'>
           频率惩罚,减少重复词汇的出现
         </Typography.Text>
         <Slider
@@ -148,34 +162,42 @@ const ParameterControl = ({
           max={2}
           value={inputs.frequency_penalty}
           onChange={(value) => onInputChange('frequency_penalty', value)}
-          className="mt-2"
+          className='mt-2'
           disabled={!parameterEnabled.frequency_penalty || disabled}
         />
       </div>
 
       {/* Presence Penalty */}
-      <div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.presence_penalty || disabled ? 'opacity-50' : ''}`}>
-        <div className="flex items-center justify-between mb-2">
-          <div className="flex items-center gap-2">
-            <Ban size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+      <div
+        className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.presence_penalty || disabled ? 'opacity-50' : ''}`}
+      >
+        <div className='flex items-center justify-between mb-2'>
+          <div className='flex items-center gap-2'>
+            <Ban size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               Presence Penalty
             </Typography.Text>
-            <Tag size="small" shape='circle'>
+            <Tag size='small' shape='circle'>
               {inputs.presence_penalty}
             </Tag>
           </div>
           <Button
             theme={parameterEnabled.presence_penalty ? 'solid' : 'borderless'}
             type={parameterEnabled.presence_penalty ? 'primary' : 'tertiary'}
-            size="small"
-            icon={parameterEnabled.presence_penalty ? <Check size={10} /> : <X size={10} />}
+            size='small'
+            icon={
+              parameterEnabled.presence_penalty ? (
+                <Check size={10} />
+              ) : (
+                <X size={10} />
+              )
+            }
             onClick={() => onParameterToggle('presence_penalty')}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={disabled}
           />
         </div>
-        <Typography.Text className="text-xs text-gray-500 mb-2">
+        <Typography.Text className='text-xs text-gray-500 mb-2'>
           存在惩罚,鼓励讨论新话题
         </Typography.Text>
         <Slider
@@ -184,27 +206,35 @@ const ParameterControl = ({
           max={2}
           value={inputs.presence_penalty}
           onChange={(value) => onInputChange('presence_penalty', value)}
-          className="mt-2"
+          className='mt-2'
           disabled={!parameterEnabled.presence_penalty || disabled}
         />
       </div>
 
       {/* MaxTokens */}
-      <div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.max_tokens || disabled ? 'opacity-50' : ''}`}>
-        <div className="flex items-center justify-between mb-2">
-          <div className="flex items-center gap-2">
-            <Hash size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+      <div
+        className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.max_tokens || disabled ? 'opacity-50' : ''}`}
+      >
+        <div className='flex items-center justify-between mb-2'>
+          <div className='flex items-center gap-2'>
+            <Hash size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               Max Tokens
             </Typography.Text>
           </div>
           <Button
             theme={parameterEnabled.max_tokens ? 'solid' : 'borderless'}
             type={parameterEnabled.max_tokens ? 'primary' : 'tertiary'}
-            size="small"
-            icon={parameterEnabled.max_tokens ? <Check size={10} /> : <X size={10} />}
+            size='small'
+            icon={
+              parameterEnabled.max_tokens ? (
+                <Check size={10} />
+              ) : (
+                <X size={10} />
+              )
+            }
             onClick={() => onParameterToggle('max_tokens')}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={disabled}
           />
         </div>
@@ -216,30 +246,32 @@ const ParameterControl = ({
           defaultValue={0}
           value={inputs.max_tokens}
           onChange={(value) => onInputChange('max_tokens', value)}
-          className="!rounded-lg"
+          className='!rounded-lg'
           disabled={!parameterEnabled.max_tokens || disabled}
         />
       </div>
 
       {/* Seed */}
-      <div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.seed || disabled ? 'opacity-50' : ''}`}>
-        <div className="flex items-center justify-between mb-2">
-          <div className="flex items-center gap-2">
-            <Shuffle size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+      <div
+        className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.seed || disabled ? 'opacity-50' : ''}`}
+      >
+        <div className='flex items-center justify-between mb-2'>
+          <div className='flex items-center gap-2'>
+            <Shuffle size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               Seed
             </Typography.Text>
-            <Typography.Text className="text-xs text-gray-400">
+            <Typography.Text className='text-xs text-gray-400'>
               (可选,用于复现结果)
             </Typography.Text>
           </div>
           <Button
             theme={parameterEnabled.seed ? 'solid' : 'borderless'}
             type={parameterEnabled.seed ? 'primary' : 'tertiary'}
-            size="small"
+            size='small'
             icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
             onClick={() => onParameterToggle('seed')}
-            className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
+            className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
             disabled={disabled}
           />
         </div>
@@ -248,8 +280,10 @@ const ParameterControl = ({
           name='seed'
           autoComplete='new-password'
           value={inputs.seed || ''}
-          onChange={(value) => onInputChange('seed', value === '' ? null : value)}
-          className="!rounded-lg"
+          onChange={(value) =>
+            onInputChange('seed', value === '' ? null : value)
+          }
+          className='!rounded-lg'
           disabled={!parameterEnabled.seed || disabled}
         />
       </div>
@@ -257,4 +291,4 @@ const ParameterControl = ({
   );
 };
 
-export default ParameterControl; 
+export default ParameterControl;

+ 38 - 48
web/src/components/playground/SettingsPanel.jsx

@@ -18,20 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import {
-  Card,
-  Select,
-  Typography,
-  Button,
-  Switch,
-} from '@douyinfe/semi-ui';
-import {
-  Sparkles,
-  Users,
-  ToggleLeft,
-  X,
-  Settings,
-} from 'lucide-react';
+import { Card, Select, Typography, Button, Switch } from '@douyinfe/semi-ui';
+import { Sparkles, Users, ToggleLeft, X, Settings } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { renderGroupOption, selectFilter } from '../../helpers';
 import ParameterControl from './ParameterControl';
@@ -70,22 +58,22 @@ const SettingsPanel = ({
 
   return (
     <Card
-      className="h-full flex flex-col"
+      className='h-full flex flex-col'
       bordered={false}
       bodyStyle={{
         padding: styleState.isMobile ? '16px' : '24px',
         height: '100%',
         display: 'flex',
-        flexDirection: 'column'
+        flexDirection: 'column',
       }}
     >
       {/* 标题区域 - 与调试面板保持一致 */}
-      <div className="flex items-center justify-between mb-6 flex-shrink-0">
-        <div className="flex items-center">
-          <div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3">
-            <Settings size={20} className="text-white" />
+      <div className='flex items-center justify-between mb-6 flex-shrink-0'>
+        <div className='flex items-center'>
+          <div className='w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3'>
+            <Settings size={20} className='text-white' />
           </div>
-          <Typography.Title heading={5} className="mb-0">
+          <Typography.Title heading={5} className='mb-0'>
             {t('模型配置')}
           </Typography.Title>
         </div>
@@ -94,17 +82,17 @@ const SettingsPanel = ({
           <Button
             icon={<X size={16} />}
             onClick={onCloseSettings}
-            theme="borderless"
-            type="tertiary"
-            size="small"
-            className="!rounded-lg"
+            theme='borderless'
+            type='tertiary'
+            size='small'
+            className='!rounded-lg'
           />
         )}
       </div>
 
       {/* 移动端配置管理 */}
       {styleState.isMobile && (
-        <div className="mb-4 flex-shrink-0">
+        <div className='mb-4 flex-shrink-0'>
           <ConfigManager
             currentConfig={currentConfig}
             onConfigImport={onConfigImport}
@@ -115,7 +103,7 @@ const SettingsPanel = ({
         </div>
       )}
 
-      <div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
+      <div className='space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll'>
         {/* 自定义请求体编辑器 */}
         <CustomRequestEditor
           customRequestMode={customRequestMode}
@@ -127,13 +115,13 @@ const SettingsPanel = ({
 
         {/* 分组选择 */}
         <div className={customRequestMode ? 'opacity-50' : ''}>
-          <div className="flex items-center gap-2 mb-2">
-            <Users size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+          <div className='flex items-center gap-2 mb-2'>
+            <Users size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               {t('分组')}
             </Typography.Text>
             {customRequestMode && (
-              <Typography.Text className="text-xs text-orange-600">
+              <Typography.Text className='text-xs text-orange-600'>
                 (已在自定义模式中忽略)
               </Typography.Text>
             )}
@@ -152,20 +140,20 @@ const SettingsPanel = ({
             renderOptionItem={renderGroupOption}
             style={{ width: '100%' }}
             dropdownStyle={{ width: '100%', maxWidth: '100%' }}
-            className="!rounded-lg"
+            className='!rounded-lg'
             disabled={customRequestMode}
           />
         </div>
 
         {/* 模型选择 */}
         <div className={customRequestMode ? 'opacity-50' : ''}>
-          <div className="flex items-center gap-2 mb-2">
-            <Sparkles size={16} className="text-gray-500" />
-            <Typography.Text strong className="text-sm">
+          <div className='flex items-center gap-2 mb-2'>
+            <Sparkles size={16} className='text-gray-500' />
+            <Typography.Text strong className='text-sm'>
               {t('模型')}
             </Typography.Text>
             {customRequestMode && (
-              <Typography.Text className="text-xs text-orange-600">
+              <Typography.Text className='text-xs text-orange-600'>
                 (已在自定义模式中忽略)
               </Typography.Text>
             )}
@@ -183,7 +171,7 @@ const SettingsPanel = ({
             optionList={models}
             style={{ width: '100%' }}
             dropdownStyle={{ width: '100%', maxWidth: '100%' }}
-            className="!rounded-lg"
+            className='!rounded-lg'
             disabled={customRequestMode}
           />
         </div>
@@ -194,7 +182,9 @@ const SettingsPanel = ({
             imageUrls={inputs.imageUrls}
             imageEnabled={inputs.imageEnabled}
             onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
-            onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
+            onImageEnabledChange={(enabled) =>
+              onInputChange('imageEnabled', enabled)
+            }
             disabled={customRequestMode}
           />
         </div>
@@ -212,14 +202,14 @@ const SettingsPanel = ({
 
         {/* 流式输出开关 */}
         <div className={customRequestMode ? 'opacity-50' : ''}>
-          <div className="flex items-center justify-between">
-            <div className="flex items-center gap-2">
-              <ToggleLeft size={16} className="text-gray-500" />
-              <Typography.Text strong className="text-sm">
+          <div className='flex items-center justify-between'>
+            <div className='flex items-center gap-2'>
+              <ToggleLeft size={16} className='text-gray-500' />
+              <Typography.Text strong className='text-sm'>
                 流式输出
               </Typography.Text>
               {customRequestMode && (
-                <Typography.Text className="text-xs text-orange-600">
+                <Typography.Text className='text-xs text-orange-600'>
                   (已在自定义模式中忽略)
                 </Typography.Text>
               )}
@@ -227,9 +217,9 @@ const SettingsPanel = ({
             <Switch
               checked={inputs.stream}
               onChange={(checked) => onInputChange('stream', checked)}
-              checkedText="开"
-              uncheckedText="关"
-              size="small"
+              checkedText='开'
+              uncheckedText='关'
+              size='small'
               disabled={customRequestMode}
             />
           </div>
@@ -238,7 +228,7 @@ const SettingsPanel = ({
 
       {/* 桌面端的配置管理放在底部 */}
       {!styleState.isMobile && (
-        <div className="flex-shrink-0 pt-3">
+        <div className='flex-shrink-0 pt-3'>
           <ConfigManager
             currentConfig={currentConfig}
             onConfigImport={onConfigImport}
@@ -252,4 +242,4 @@ const SettingsPanel = ({
   );
 };
 
-export default SettingsPanel; 
+export default SettingsPanel;

+ 70 - 34
web/src/components/playground/ThinkingContent.jsx

@@ -28,17 +28,25 @@ const ThinkingContent = ({
   finalExtractedThinkingContent,
   thinkingSource,
   styleState,
-  onToggleReasoningExpansion
+  onToggleReasoningExpansion,
 }) => {
   const { t } = useTranslation();
   const scrollRef = useRef(null);
   const lastContentRef = useRef('');
 
-  const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
-  const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
+  const isThinkingStatus =
+    message.status === 'loading' || message.status === 'incomplete';
+  const headerText =
+    isThinkingStatus && !message.isThinkingComplete
+      ? t('思考中...')
+      : t('思考过程');
 
   useEffect(() => {
-    if (scrollRef.current && finalExtractedThinkingContent && message.isReasoningExpanded) {
+    if (
+      scrollRef.current &&
+      finalExtractedThinkingContent &&
+      message.isReasoningExpanded
+    ) {
       scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
     }
   }, [finalExtractedThinkingContent, message.isReasoningExpanded]);
@@ -63,72 +71,100 @@ const ThinkingContent = ({
   }
 
   return (
-    <div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
+    <div className='rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm'>
       <div
-        className="flex items-center justify-between p-3 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all"
+        className='flex items-center justify-between p-3 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all'
         style={{
-          background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
-          position: 'relative'
+          background:
+            'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
+          position: 'relative',
         }}
         onClick={() => onToggleReasoningExpansion(message.id)}
       >
-        <div className="absolute inset-0 overflow-hidden">
-          <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-          <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+        <div className='absolute inset-0 overflow-hidden'>
+          <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+          <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
         </div>
-        <div className="flex items-center gap-2 sm:gap-4 relative">
-          <div className="w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center shadow-lg">
-            <Brain style={{ color: 'white' }} size={styleState.isMobile ? 12 : 16} />
+        <div className='flex items-center gap-2 sm:gap-4 relative'>
+          <div className='w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center shadow-lg'>
+            <Brain
+              style={{ color: 'white' }}
+              size={styleState.isMobile ? 12 : 16}
+            />
           </div>
-          <div className="flex flex-col">
-            <Typography.Text strong style={{ color: 'white' }} className="text-sm sm:text-base">
+          <div className='flex flex-col'>
+            <Typography.Text
+              strong
+              style={{ color: 'white' }}
+              className='text-sm sm:text-base'
+            >
               {headerText}
             </Typography.Text>
             {thinkingSource && (
-              <Typography.Text style={{ color: 'white' }} className="text-xs mt-0.5 opacity-80 hidden sm:block">
+              <Typography.Text
+                style={{ color: 'white' }}
+                className='text-xs mt-0.5 opacity-80 hidden sm:block'
+              >
                 来源: {thinkingSource}
               </Typography.Text>
             )}
           </div>
         </div>
-        <div className="flex items-center gap-2 sm:gap-3 relative">
+        <div className='flex items-center gap-2 sm:gap-3 relative'>
           {isThinkingStatus && !message.isThinkingComplete && (
-            <div className="flex items-center gap-1 sm:gap-2">
-              <Loader2 style={{ color: 'white' }} className="animate-spin" size={styleState.isMobile ? 14 : 18} />
-              <Typography.Text style={{ color: 'white' }} className="text-xs sm:text-sm font-medium opacity-90">
+            <div className='flex items-center gap-1 sm:gap-2'>
+              <Loader2
+                style={{ color: 'white' }}
+                className='animate-spin'
+                size={styleState.isMobile ? 14 : 18}
+              />
+              <Typography.Text
+                style={{ color: 'white' }}
+                className='text-xs sm:text-sm font-medium opacity-90'
+              >
                 思考中
               </Typography.Text>
             </div>
           )}
           {(!isThinkingStatus || message.isThinkingComplete) && (
-            <div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center">
-              {message.isReasoningExpanded ?
-                <ChevronUp size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} /> :
-                <ChevronRight size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} />
-              }
+            <div className='w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center'>
+              {message.isReasoningExpanded ? (
+                <ChevronUp
+                  size={styleState.isMobile ? 12 : 16}
+                  style={{ color: 'white' }}
+                />
+              ) : (
+                <ChevronRight
+                  size={styleState.isMobile ? 12 : 16}
+                  style={{ color: 'white' }}
+                />
+              )}
             </div>
           )}
         </div>
       </div>
       <div
-        className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
-          } overflow-hidden bg-gradient-to-br from-purple-50 via-indigo-50 to-violet-50`}
+        className={`transition-all duration-500 ease-out ${
+          message.isReasoningExpanded
+            ? 'max-h-96 opacity-100'
+            : 'max-h-0 opacity-0'
+        } overflow-hidden bg-gradient-to-br from-purple-50 via-indigo-50 to-violet-50`}
       >
         {message.isReasoningExpanded && (
-          <div className="p-3 sm:p-5 pt-2 sm:pt-4">
+          <div className='p-3 sm:p-5 pt-2 sm:pt-4'>
             <div
               ref={scrollRef}
-              className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll"
+              className='bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll'
               style={{
                 maxHeight: '200px',
                 scrollbarWidth: 'thin',
-                scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent'
+                scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent',
               }}
             >
-              <div className="prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm">
+              <div className='prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm'>
                 <MarkdownRenderer
                   content={finalExtractedThinkingContent}
-                  className=""
+                  className=''
                   animated={isThinkingStatus}
                   previousContentLength={prevLength}
                 />
@@ -141,4 +177,4 @@ const ThinkingContent = ({
   );
 };
 
-export default ThinkingContent; 
+export default ThinkingContent;

+ 15 - 7
web/src/components/playground/configStorage.js

@@ -17,7 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../constants/playground.constants';
+import {
+  STORAGE_KEYS,
+  DEFAULT_CONFIG,
+} from '../../constants/playground.constants';
 
 const MESSAGES_STORAGE_KEY = 'playground_messages';
 
@@ -72,9 +75,12 @@ export const loadConfig = () => {
           ...DEFAULT_CONFIG.parameterEnabled,
           ...parsedConfig.parameterEnabled,
         },
-        showDebugPanel: parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
-        customRequestMode: parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
-        customRequestBody: parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
+        showDebugPanel:
+          parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
+        customRequestMode:
+          parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
+        customRequestBody:
+          parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
       };
 
       return mergedConfig;
@@ -180,7 +186,6 @@ export const exportConfig = (config, messages = null) => {
     link.click();
 
     URL.revokeObjectURL(link.href);
-
   } catch (error) {
     console.error('导出配置失败:', error);
   }
@@ -201,7 +206,10 @@ export const importConfig = (file) => {
 
           if (importedConfig.inputs && importedConfig.parameterEnabled) {
             // 如果导入的配置包含消息,也一起导入
-            if (importedConfig.messages && Array.isArray(importedConfig.messages)) {
+            if (
+              importedConfig.messages &&
+              Array.isArray(importedConfig.messages)
+            ) {
               saveMessages(importedConfig.messages);
             }
 
@@ -219,4 +227,4 @@ export const importConfig = (file) => {
       reject(new Error('导入配置失败: ' + error.message));
     }
   });
-}; 
+};

+ 1 - 1
web/src/components/playground/index.js

@@ -36,4 +36,4 @@ export {
   getConfigTimestamp,
   exportConfig,
   importConfig,
-} from './configStorage'; 
+} from './configStorage';

+ 232 - 206
web/src/components/settings/ChannelSelectorModal.jsx

@@ -17,7 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
+import React, {
+  useState,
+  useEffect,
+  forwardRef,
+  useImperativeHandle,
+} from 'react';
 import { useIsMobile } from '../../hooks/common/useIsMobile';
 import {
   Modal,
@@ -31,220 +36,241 @@ import {
 import { IconSearch } from '@douyinfe/semi-icons';
 import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
 
-const ChannelSelectorModal = forwardRef(({
-  visible,
-  onCancel,
-  onOk,
-  allChannels,
-  selectedChannelIds,
-  setSelectedChannelIds,
-  channelEndpoints,
-  updateChannelEndpoint,
-  t,
-}, ref) => {
-  const [searchText, setSearchText] = useState('');
-  const [currentPage, setCurrentPage] = useState(1);
-  const [pageSize, setPageSize] = useState(10);
-  const isMobile = useIsMobile();
-
-  const [filteredData, setFilteredData] = useState([]);
-
-  useImperativeHandle(ref, () => ({
-    resetPagination: () => {
-      setCurrentPage(1);
-      setSearchText('');
+const ChannelSelectorModal = forwardRef(
+  (
+    {
+      visible,
+      onCancel,
+      onOk,
+      allChannels,
+      selectedChannelIds,
+      setSelectedChannelIds,
+      channelEndpoints,
+      updateChannelEndpoint,
+      t,
     },
-  }));
-
-  useEffect(() => {
-    if (!allChannels) return;
-
-    const searchLower = searchText.trim().toLowerCase();
-    const matched = searchLower
-      ? allChannels.filter((item) => {
-        const name = (item.label || '').toLowerCase();
-        const baseUrl = (item._originalData?.base_url || '').toLowerCase();
-        return name.includes(searchLower) || baseUrl.includes(searchLower);
-      })
-      : allChannels;
-
-    setFilteredData(matched);
-  }, [allChannels, searchText]);
-
-  const total = filteredData.length;
-
-  const paginatedData = filteredData.slice(
-    (currentPage - 1) * pageSize,
-    currentPage * pageSize,
-  );
-
-  const updateEndpoint = (channelId, endpoint) => {
-    if (typeof updateChannelEndpoint === 'function') {
-      updateChannelEndpoint(channelId, endpoint);
-    }
-  };
-
-  const renderEndpointCell = (text, record) => {
-    const channelId = record.key || record.value;
-    const currentEndpoint = channelEndpoints[channelId] || '';
-
-    const getEndpointType = (ep) => {
-      if (ep === '/api/ratio_config') return 'ratio_config';
-      if (ep === '/api/pricing') return 'pricing';
-      return 'custom';
+    ref,
+  ) => {
+    const [searchText, setSearchText] = useState('');
+    const [currentPage, setCurrentPage] = useState(1);
+    const [pageSize, setPageSize] = useState(10);
+    const isMobile = useIsMobile();
+
+    const [filteredData, setFilteredData] = useState([]);
+
+    useImperativeHandle(ref, () => ({
+      resetPagination: () => {
+        setCurrentPage(1);
+        setSearchText('');
+      },
+    }));
+
+    useEffect(() => {
+      if (!allChannels) return;
+
+      const searchLower = searchText.trim().toLowerCase();
+      const matched = searchLower
+        ? allChannels.filter((item) => {
+            const name = (item.label || '').toLowerCase();
+            const baseUrl = (item._originalData?.base_url || '').toLowerCase();
+            return name.includes(searchLower) || baseUrl.includes(searchLower);
+          })
+        : allChannels;
+
+      setFilteredData(matched);
+    }, [allChannels, searchText]);
+
+    const total = filteredData.length;
+
+    const paginatedData = filteredData.slice(
+      (currentPage - 1) * pageSize,
+      currentPage * pageSize,
+    );
+
+    const updateEndpoint = (channelId, endpoint) => {
+      if (typeof updateChannelEndpoint === 'function') {
+        updateChannelEndpoint(channelId, endpoint);
+      }
     };
 
-    const currentType = getEndpointType(currentEndpoint);
+    const renderEndpointCell = (text, record) => {
+      const channelId = record.key || record.value;
+      const currentEndpoint = channelEndpoints[channelId] || '';
+
+      const getEndpointType = (ep) => {
+        if (ep === '/api/ratio_config') return 'ratio_config';
+        if (ep === '/api/pricing') return 'pricing';
+        return 'custom';
+      };
 
-    const handleTypeChange = (val) => {
-      if (val === 'ratio_config') {
-        updateEndpoint(channelId, '/api/ratio_config');
-      } else if (val === 'pricing') {
-        updateEndpoint(channelId, '/api/pricing');
-      } else {
-        if (currentType !== 'custom') {
-          updateEndpoint(channelId, '');
+      const currentType = getEndpointType(currentEndpoint);
+
+      const handleTypeChange = (val) => {
+        if (val === 'ratio_config') {
+          updateEndpoint(channelId, '/api/ratio_config');
+        } else if (val === 'pricing') {
+          updateEndpoint(channelId, '/api/pricing');
+        } else {
+          if (currentType !== 'custom') {
+            updateEndpoint(channelId, '');
+          }
         }
+      };
+
+      return (
+        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          <Select
+            size='small'
+            value={currentType}
+            onChange={handleTypeChange}
+            style={{ width: 120 }}
+            optionList={[
+              { label: 'ratio_config', value: 'ratio_config' },
+              { label: 'pricing', value: 'pricing' },
+              { label: 'custom', value: 'custom' },
+            ]}
+          />
+          {currentType === 'custom' && (
+            <Input
+              size='small'
+              value={currentEndpoint}
+              onChange={(val) => updateEndpoint(channelId, val)}
+              placeholder='/your/endpoint'
+              style={{ width: 160, fontSize: 12 }}
+            />
+          )}
+        </div>
+      );
+    };
+
+    const renderStatusCell = (status) => {
+      switch (status) {
+        case 1:
+          return (
+            <Tag
+              color='green'
+              shape='circle'
+              prefixIcon={<CheckCircle size={14} />}
+            >
+              {t('已启用')}
+            </Tag>
+          );
+        case 2:
+          return (
+            <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
+              {t('已禁用')}
+            </Tag>
+          );
+        case 3:
+          return (
+            <Tag
+              color='yellow'
+              shape='circle'
+              prefixIcon={<AlertCircle size={14} />}
+            >
+              {t('自动禁用')}
+            </Tag>
+          );
+        default:
+          return (
+            <Tag
+              color='grey'
+              shape='circle'
+              prefixIcon={<HelpCircle size={14} />}
+            >
+              {t('未知状态')}
+            </Tag>
+          );
       }
     };
 
+    const renderNameCell = (text) => (
+      <Highlight sourceString={text} searchWords={[searchText]} />
+    );
+
+    const renderBaseUrlCell = (text) => (
+      <Highlight sourceString={text} searchWords={[searchText]} />
+    );
+
+    const columns = [
+      {
+        title: t('名称'),
+        dataIndex: 'label',
+        render: renderNameCell,
+      },
+      {
+        title: t('源地址'),
+        dataIndex: '_originalData.base_url',
+        render: (_, record) =>
+          renderBaseUrlCell(record._originalData?.base_url || ''),
+      },
+      {
+        title: t('状态'),
+        dataIndex: '_originalData.status',
+        render: (_, record) =>
+          renderStatusCell(record._originalData?.status || 0),
+      },
+      {
+        title: t('同步接口'),
+        dataIndex: 'endpoint',
+        fixed: 'right',
+        render: renderEndpointCell,
+      },
+    ];
+
+    const rowSelection = {
+      selectedRowKeys: selectedChannelIds,
+      onChange: (keys) => setSelectedChannelIds(keys),
+    };
+
     return (
-      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
-        <Select
-          size="small"
-          value={currentType}
-          onChange={handleTypeChange}
-          style={{ width: 120 }}
-          optionList={[
-            { label: 'ratio_config', value: 'ratio_config' },
-            { label: 'pricing', value: 'pricing' },
-            { label: 'custom', value: 'custom' },
-          ]}
-        />
-        {currentType === 'custom' && (
+      <Modal
+        visible={visible}
+        onCancel={onCancel}
+        onOk={onOk}
+        title={
+          <span className='text-lg font-semibold'>{t('选择同步渠道')}</span>
+        }
+        size={isMobile ? 'full-width' : 'large'}
+        keepDOM
+        lazyRender={false}
+      >
+        <Space vertical style={{ width: '100%' }}>
           <Input
-            size="small"
-            value={currentEndpoint}
-            onChange={(val) => updateEndpoint(channelId, val)}
-            placeholder="/your/endpoint"
-            style={{ width: 160, fontSize: 12 }}
+            prefix={<IconSearch size={14} />}
+            placeholder={t('搜索渠道名称或地址')}
+            value={searchText}
+            onChange={setSearchText}
+            showClear
+          />
+
+          <Table
+            columns={columns}
+            dataSource={paginatedData}
+            rowKey='key'
+            rowSelection={rowSelection}
+            pagination={{
+              currentPage: currentPage,
+              pageSize: pageSize,
+              total: total,
+              showSizeChanger: true,
+              showQuickJumper: true,
+              pageSizeOptions: ['10', '20', '50', '100'],
+              onChange: (page, size) => {
+                setCurrentPage(page);
+                setPageSize(size);
+              },
+              onShowSizeChange: (curr, size) => {
+                setCurrentPage(1);
+                setPageSize(size);
+              },
+            }}
+            size='small'
           />
-        )}
-      </div>
+        </Space>
+      </Modal>
     );
-  };
-
-  const renderStatusCell = (status) => {
-    switch (status) {
-      case 1:
-        return (
-          <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
-            {t('已启用')}
-          </Tag>
-        );
-      case 2:
-        return (
-          <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
-            {t('已禁用')}
-          </Tag>
-        );
-      case 3:
-        return (
-          <Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
-            {t('自动禁用')}
-          </Tag>
-        );
-      default:
-        return (
-          <Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
-            {t('未知状态')}
-          </Tag>
-        );
-    }
-  };
-
-  const renderNameCell = (text) => (
-    <Highlight sourceString={text} searchWords={[searchText]} />
-  );
-
-  const renderBaseUrlCell = (text) => (
-    <Highlight sourceString={text} searchWords={[searchText]} />
-  );
-
-  const columns = [
-    {
-      title: t('名称'),
-      dataIndex: 'label',
-      render: renderNameCell,
-    },
-    {
-      title: t('源地址'),
-      dataIndex: '_originalData.base_url',
-      render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
-    },
-    {
-      title: t('状态'),
-      dataIndex: '_originalData.status',
-      render: (_, record) => renderStatusCell(record._originalData?.status || 0),
-    },
-    {
-      title: t('同步接口'),
-      dataIndex: 'endpoint',
-      fixed: 'right',
-      render: renderEndpointCell,
-    },
-  ];
-
-  const rowSelection = {
-    selectedRowKeys: selectedChannelIds,
-    onChange: (keys) => setSelectedChannelIds(keys),
-  };
-
-  return (
-    <Modal
-      visible={visible}
-      onCancel={onCancel}
-      onOk={onOk}
-      title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
-      size={isMobile ? 'full-width' : 'large'}
-      keepDOM
-      lazyRender={false}
-    >
-      <Space vertical style={{ width: '100%' }}>
-        <Input
-          prefix={<IconSearch size={14} />}
-          placeholder={t('搜索渠道名称或地址')}
-          value={searchText}
-          onChange={setSearchText}
-          showClear
-        />
-
-        <Table
-          columns={columns}
-          dataSource={paginatedData}
-          rowKey="key"
-          rowSelection={rowSelection}
-          pagination={{
-            currentPage: currentPage,
-            pageSize: pageSize,
-            total: total,
-            showSizeChanger: true,
-            showQuickJumper: true,
-            pageSizeOptions: ['10', '20', '50', '100'],
-            onChange: (page, size) => {
-              setCurrentPage(page);
-              setPageSize(size);
-            },
-            onShowSizeChange: (curr, size) => {
-              setCurrentPage(1);
-              setPageSize(size);
-            },
-          }}
-          size="small"
-        />
-      </Space>
-    </Modal>
-  );
-});
-
-export default ChannelSelectorModal; 
+  },
+);
+
+export default ChannelSelectorModal;

+ 1 - 1
web/src/components/settings/ChatsSetting.jsx

@@ -79,4 +79,4 @@ const ChatsSetting = () => {
   );
 };
 
-export default ChatsSetting; 
+export default ChatsSetting;

+ 15 - 9
web/src/components/settings/DashboardSetting.jsx

@@ -62,8 +62,7 @@ const DashboardSetting = () => {
         if (item.key in inputs) {
           newInputs[item.key] = item.value;
         }
-        if (item.key.endsWith('Enabled') &&
-          (item.key === 'DataExportEnabled')) {
+        if (item.key.endsWith('Enabled') && item.key === 'DataExportEnabled') {
           newInputs[item.key] = toBoolean(item.value);
         }
       });
@@ -91,8 +90,14 @@ const DashboardSetting = () => {
 
   // 用于迁移检测的旧键,下个版本会删除
   const hasLegacyData = useMemo(() => {
-    const legacyKeys = ['ApiInfo', 'Announcements', 'FAQ', 'UptimeKumaUrl', 'UptimeKumaSlug'];
-    return legacyKeys.some(k => inputs[k]);
+    const legacyKeys = [
+      'ApiInfo',
+      'Announcements',
+      'FAQ',
+      'UptimeKumaUrl',
+      'UptimeKumaSlug',
+    ];
+    return legacyKeys.some((k) => inputs[k]);
   }, [inputs]);
 
   useEffect(() => {
@@ -121,17 +126,18 @@ const DashboardSetting = () => {
       <Spin spinning={loading} size='large'>
         {/* 用于迁移检测的旧键模态框,下个版本会删除 */}
         <Modal
-          title="配置迁移确认"
+          title='配置迁移确认'
           visible={showMigrateModal}
           onOk={handleMigrate}
           onCancel={() => setShowMigrateModal(false)}
           confirmLoading={loading}
-          okText="确认迁移"
-          cancelText="取消"
+          okText='确认迁移'
+          cancelText='取消'
         >
           <p>检测到旧版本的配置数据,是否要迁移到新的配置格式?</p>
           <p style={{ color: '#f57c00', marginTop: '10px' }}>
-            <strong>注意:</strong>迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
+            <strong>注意:</strong>
+            迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
           </p>
         </Modal>
 
@@ -164,4 +170,4 @@ const DashboardSetting = () => {
   );
 };
 
-export default DashboardSetting; 
+export default DashboardSetting;

+ 1 - 1
web/src/components/settings/DrawingSetting.jsx

@@ -81,4 +81,4 @@ const DrawingSetting = () => {
   );
 };
 
-export default DrawingSetting; 
+export default DrawingSetting;

+ 6 - 2
web/src/components/settings/PaymentSetting.jsx

@@ -56,7 +56,11 @@ const PaymentSetting = () => {
         switch (item.key) {
           case 'TopupGroupRatio':
             try {
-              newInputs[item.key] = JSON.stringify(JSON.parse(item.value), null, 2);
+              newInputs[item.key] = JSON.stringify(
+                JSON.parse(item.value),
+                null,
+                2,
+              );
             } catch (error) {
               console.error('解析TopupGroupRatio出错:', error);
               newInputs[item.key] = item.value;
@@ -116,4 +120,4 @@ const PaymentSetting = () => {
   );
 };
 
-export default PaymentSetting; 
+export default PaymentSetting;

+ 10 - 12
web/src/components/settings/PersonalSetting.jsx

@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React, { useContext, useEffect, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
-import {
-  API,
-  copy,
-  showError,
-  showInfo,
-  showSuccess
-} from '../../helpers';
+import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
 import { UserContext } from '../../context/User';
 import { Modal } from '@douyinfe/semi-ui';
 import { useTranslation } from 'react-i18next';
@@ -271,7 +265,11 @@ const PersonalSetting = () => {
   const handleNotificationSettingChange = (type, value) => {
     setNotificationSettings((prev) => ({
       ...prev,
-      [type]: value.target ? value.target.value !== undefined ? value.target.value : value.target.checked : value, // handle checkbox properly
+      [type]: value.target
+        ? value.target.value !== undefined
+          ? value.target.value
+          : value.target.checked
+        : value, // handle checkbox properly
     }));
   };
 
@@ -302,14 +300,14 @@ const PersonalSetting = () => {
   };
 
   return (
-    <div className="mt-[60px]">
-      <div className="flex justify-center">
-        <div className="w-full max-w-7xl mx-auto px-2">
+    <div className='mt-[60px]'>
+      <div className='flex justify-center'>
+        <div className='w-full max-w-7xl mx-auto px-2'>
           {/* 顶部用户信息区域 */}
           <UserInfoHeader t={t} userState={userState} />
 
           {/* 账户管理和其他设置 */}
-          <div className="grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6">
+          <div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
             {/* 左侧:账户管理设置 */}
             <AccountManagement
               t={t}

+ 6 - 21
web/src/components/settings/RatioSetting.jsx

@@ -103,34 +103,19 @@ const RatioSetting = () => {
       <Card style={{ marginTop: '10px' }}>
         <Tabs type='card'>
           <Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
-            <ModelRatioSettings
-              options={inputs}
-              refresh={onRefresh}
-            />
+            <ModelRatioSettings options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
           <Tabs.TabPane tab={t('分组倍率设置')} itemKey='group'>
-            <GroupRatioSettings
-              options={inputs}
-              refresh={onRefresh}
-            />
+            <GroupRatioSettings options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
           <Tabs.TabPane tab={t('可视化倍率设置')} itemKey='visual'>
-            <ModelSettingsVisualEditor
-              options={inputs}
-              refresh={onRefresh}
-            />
+            <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
           <Tabs.TabPane tab={t('未设置倍率模型')} itemKey='unset_models'>
-            <ModelRatioNotSetEditor
-              options={inputs}
-              refresh={onRefresh}
-            />
+            <ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
           <Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
-            <UpstreamRatioSync
-              options={inputs}
-              refresh={onRefresh}
-            />
+            <UpstreamRatioSync options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
         </Tabs>
       </Card>
@@ -138,4 +123,4 @@ const RatioSetting = () => {
   );
 };
 
-export default RatioSetting; 
+export default RatioSetting;

+ 35 - 10
web/src/components/settings/SystemSetting.jsx

@@ -473,7 +473,10 @@ const SystemSetting = () => {
         value: inputs.LinuxDOClientSecret,
       });
     }
-    if (originInputs['LinuxDOMinimumTrustLevel'] !== inputs.LinuxDOMinimumTrustLevel) {
+    if (
+      originInputs['LinuxDOMinimumTrustLevel'] !==
+      inputs.LinuxDOMinimumTrustLevel
+    ) {
       options.push({
         key: 'LinuxDOMinimumTrustLevel',
         value: inputs.LinuxDOMinimumTrustLevel,
@@ -530,11 +533,15 @@ const SystemSetting = () => {
                         field='ServerAddress'
                         label={t('服务器地址')}
                         placeholder='https://yourdomain.com'
-                        extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
+                        extraText={t(
+                          '该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
+                        )}
                       />
                     </Col>
                   </Row>
-                  <Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
+                  <Button onClick={submitServerAddress}>
+                    {t('更新服务器地址')}
+                  </Button>
                 </Form.Section>
               </Card>
 
@@ -755,7 +762,10 @@ const SystemSetting = () => {
                     gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
                   >
                     <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input field='SMTPServer' label={t('SMTP 服务器地址')} />
+                      <Form.Input
+                        field='SMTPServer'
+                        label={t('SMTP 服务器地址')}
+                      />
                     </Col>
                     <Col xs={24} sm={24} md={8} lg={8} xl={8}>
                       <Form.Input field='SMTPPort' label={t('SMTP 端口')} />
@@ -769,7 +779,10 @@ const SystemSetting = () => {
                     style={{ marginTop: 16 }}
                   >
                     <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                      <Form.Input field='SMTPFrom' label={t('SMTP 发送者邮箱')} />
+                      <Form.Input
+                        field='SMTPFrom'
+                        label={t('SMTP 发送者邮箱')}
+                      />
                     </Col>
                     <Col xs={24} sm={24} md={8} lg={8} xl={8}>
                       <Form.Input
@@ -797,7 +810,9 @@ const SystemSetting = () => {
               <Card>
                 <Form.Section text={t('配置 OIDC')}>
                   <Text>
-                    {t('用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP')}
+                    {t(
+                      '用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP',
+                    )}
                   </Text>
                   <Banner
                     type='info'
@@ -805,7 +820,9 @@ const SystemSetting = () => {
                     style={{ marginBottom: 20, marginTop: 16 }}
                   />
                   <Text>
-                    {t('若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置')}
+                    {t(
+                      '若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置',
+                    )}
                   </Text>
                   <Row
                     gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
@@ -862,7 +879,9 @@ const SystemSetting = () => {
                       />
                     </Col>
                   </Row>
-                  <Button onClick={submitOIDCSettings}>{t('保存 OIDC 设置')}</Button>
+                  <Button onClick={submitOIDCSettings}>
+                    {t('保存 OIDC 设置')}
+                  </Button>
                 </Form.Section>
               </Card>
 
@@ -1033,7 +1052,9 @@ const SystemSetting = () => {
                       />
                     </Col>
                   </Row>
-                  <Button onClick={submitTurnstile}>{t('保存 Turnstile 设置')}</Button>
+                  <Button onClick={submitTurnstile}>
+                    {t('保存 Turnstile 设置')}
+                  </Button>
                 </Form.Section>
               </Card>
 
@@ -1048,7 +1069,11 @@ const SystemSetting = () => {
                 okText={t('确认')}
                 cancelText={t('取消')}
               >
-                <p>{t('您确定要取消密码登录功能吗?这可能会影响用户的登录方式。')}</p>
+                <p>
+                  {t(
+                    '您确定要取消密码登录功能吗?这可能会影响用户的登录方式。',
+                  )}
+                </p>
               </Modal>
             </div>
           )}

+ 199 - 141
web/src/components/settings/personal/cards/AccountManagement.jsx

@@ -27,7 +27,7 @@ import {
   Avatar,
   Tabs,
   TabPane,
-  Popover
+  Popover,
 } from '@douyinfe/semi-ui';
 import {
   IconMail,
@@ -35,7 +35,7 @@ import {
   IconGithubLogo,
   IconKey,
   IconLock,
-  IconDelete
+  IconDelete,
 } from '@douyinfe/semi-icons';
 import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
 import { UserPlus, ShieldCheck } from 'lucide-react';
@@ -43,7 +43,7 @@ import TelegramLoginButton from 'react-telegram-login';
 import {
   onGitHubOAuthClicked,
   onOIDCClicked,
-  onLinuxDOOAuthClicked
+  onLinuxDOOAuthClicked,
 } from '../../../../helpers';
 import TwoFASetting from '../components/TwoFASetting';
 
@@ -57,77 +57,89 @@ const AccountManagement = ({
   generateAccessToken,
   handleSystemTokenClick,
   setShowChangePasswordModal,
-  setShowAccountDeleteModal
+  setShowAccountDeleteModal,
 }) => {
   const renderAccountInfo = (accountId, label) => {
     if (!accountId || accountId === '') {
-      return <span className="text-gray-500">{t('未绑定')}</span>;
+      return <span className='text-gray-500'>{t('未绑定')}</span>;
     }
 
     const popContent = (
-      <div className="text-xs p-2">
+      <div className='text-xs p-2'>
         <Typography.Paragraph copyable={{ content: accountId }}>
           {accountId}
         </Typography.Paragraph>
         {label ? (
-          <div className="mt-1 text-[11px] text-gray-500">{label}</div>
+          <div className='mt-1 text-[11px] text-gray-500'>{label}</div>
         ) : null}
       </div>
     );
 
     return (
-      <Popover content={popContent} position="top" trigger="hover">
-        <span className="block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer">
+      <Popover content={popContent} position='top' trigger='hover'>
+        <span className='block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer'>
           {accountId}
         </span>
       </Popover>
     );
   };
   return (
-    <Card className="!rounded-2xl">
+    <Card className='!rounded-2xl'>
       {/* 卡片头部 */}
-      <div className="flex items-center mb-4">
-        <Avatar size="small" color="teal" className="mr-3 shadow-md">
+      <div className='flex items-center mb-4'>
+        <Avatar size='small' color='teal' className='mr-3 shadow-md'>
           <UserPlus size={16} />
         </Avatar>
         <div>
-          <Typography.Text className="text-lg font-medium">{t('账户管理')}</Typography.Text>
-          <div className="text-xs text-gray-600">{t('账户绑定、安全设置和身份验证')}</div>
+          <Typography.Text className='text-lg font-medium'>
+            {t('账户管理')}
+          </Typography.Text>
+          <div className='text-xs text-gray-600'>
+            {t('账户绑定、安全设置和身份验证')}
+          </div>
         </div>
       </div>
 
-      <Tabs type="card" defaultActiveKey="binding">
+      <Tabs type='card' defaultActiveKey='binding'>
         {/* 账户绑定 Tab */}
         <TabPane
           tab={
-            <div className="flex items-center">
-              <UserPlus size={16} className="mr-2" />
+            <div className='flex items-center'>
+              <UserPlus size={16} className='mr-2' />
               {t('账户绑定')}
             </div>
           }
-          itemKey="binding"
+          itemKey='binding'
         >
-          <div className="py-4">
-            <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+          <div className='py-4'>
+            <div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
               {/* 邮箱绑定 */}
-              <Card className="!rounded-xl">
-                <div className="flex items-center justify-between gap-3">
-                  <div className="flex items-center flex-1 min-w-0">
-                    <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
-                      <IconMail size="default" className="text-slate-600 dark:text-slate-300" />
+              <Card className='!rounded-xl'>
+                <div className='flex items-center justify-between gap-3'>
+                  <div className='flex items-center flex-1 min-w-0'>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <IconMail
+                        size='default'
+                        className='text-slate-600 dark:text-slate-300'
+                      />
                     </div>
-                    <div className="flex-1 min-w-0">
-                      <div className="font-medium text-gray-900">{t('邮箱')}</div>
-                      <div className="text-sm text-gray-500 truncate">
-                        {renderAccountInfo(userState.user?.email, t('邮箱地址'))}
+                    <div className='flex-1 min-w-0'>
+                      <div className='font-medium text-gray-900'>
+                        {t('邮箱')}
+                      </div>
+                      <div className='text-sm text-gray-500 truncate'>
+                        {renderAccountInfo(
+                          userState.user?.email,
+                          t('邮箱地址'),
+                        )}
                       </div>
                     </div>
                   </div>
-                  <div className="flex-shrink-0">
+                  <div className='flex-shrink-0'>
                     <Button
-                      type="primary"
-                      theme="outline"
-                      size="small"
+                      type='primary'
+                      theme='outline'
+                      size='small'
                       onClick={() => setShowEmailBindModal(true)}
                     >
                       {userState.user && userState.user.email !== ''
@@ -139,26 +151,31 @@ const AccountManagement = ({
               </Card>
 
               {/* 微信绑定 */}
-              <Card className="!rounded-xl">
-                <div className="flex items-center justify-between gap-3">
-                  <div className="flex items-center flex-1 min-w-0">
-                    <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
-                      <SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
+              <Card className='!rounded-xl'>
+                <div className='flex items-center justify-between gap-3'>
+                  <div className='flex items-center flex-1 min-w-0'>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <SiWechat
+                        size={20}
+                        className='text-slate-600 dark:text-slate-300'
+                      />
                     </div>
-                    <div className="flex-1 min-w-0">
-                      <div className="font-medium text-gray-900">{t('微信')}</div>
-                      <div className="text-sm text-gray-500 truncate">
+                    <div className='flex-1 min-w-0'>
+                      <div className='font-medium text-gray-900'>
+                        {t('微信')}
+                      </div>
+                      <div className='text-sm text-gray-500 truncate'>
                         {userState.user && userState.user.wechat_id !== ''
                           ? t('已绑定')
                           : t('未绑定')}
                       </div>
                     </div>
                   </div>
-                  <div className="flex-shrink-0">
+                  <div className='flex-shrink-0'>
                     <Button
-                      type="primary"
-                      theme="outline"
-                      size="small"
+                      type='primary'
+                      theme='outline'
+                      size='small'
                       disabled={!status.wechat_login}
                       onClick={() => setShowWeChatBindModal(true)}
                     >
@@ -173,25 +190,35 @@ const AccountManagement = ({
               </Card>
 
               {/* GitHub绑定 */}
-              <Card className="!rounded-xl">
-                <div className="flex items-center justify-between gap-3">
-                  <div className="flex items-center flex-1 min-w-0">
-                    <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
-                      <IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
+              <Card className='!rounded-xl'>
+                <div className='flex items-center justify-between gap-3'>
+                  <div className='flex items-center flex-1 min-w-0'>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <IconGithubLogo
+                        size='default'
+                        className='text-slate-600 dark:text-slate-300'
+                      />
                     </div>
-                    <div className="flex-1 min-w-0">
-                      <div className="font-medium text-gray-900">{t('GitHub')}</div>
-                      <div className="text-sm text-gray-500 truncate">
-                        {renderAccountInfo(userState.user?.github_id, t('GitHub ID'))}
+                    <div className='flex-1 min-w-0'>
+                      <div className='font-medium text-gray-900'>
+                        {t('GitHub')}
+                      </div>
+                      <div className='text-sm text-gray-500 truncate'>
+                        {renderAccountInfo(
+                          userState.user?.github_id,
+                          t('GitHub ID'),
+                        )}
                       </div>
                     </div>
                   </div>
-                  <div className="flex-shrink-0">
+                  <div className='flex-shrink-0'>
                     <Button
-                      type="primary"
-                      theme="outline"
-                      size="small"
-                      onClick={() => onGitHubOAuthClicked(status.github_client_id)}
+                      type='primary'
+                      theme='outline'
+                      size='small'
+                      onClick={() =>
+                        onGitHubOAuthClicked(status.github_client_id)
+                      }
                       disabled={
                         (userState.user && userState.user.github_id !== '') ||
                         !status.github_oauth
@@ -204,28 +231,38 @@ const AccountManagement = ({
               </Card>
 
               {/* OIDC绑定 */}
-              <Card className="!rounded-xl">
-                <div className="flex items-center justify-between gap-3">
-                  <div className="flex items-center flex-1 min-w-0">
-                    <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
-                      <IconShield size="default" className="text-slate-600 dark:text-slate-300" />
+              <Card className='!rounded-xl'>
+                <div className='flex items-center justify-between gap-3'>
+                  <div className='flex items-center flex-1 min-w-0'>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <IconShield
+                        size='default'
+                        className='text-slate-600 dark:text-slate-300'
+                      />
                     </div>
-                    <div className="flex-1 min-w-0">
-                      <div className="font-medium text-gray-900">{t('OIDC')}</div>
-                      <div className="text-sm text-gray-500 truncate">
-                        {renderAccountInfo(userState.user?.oidc_id, t('OIDC ID'))}
+                    <div className='flex-1 min-w-0'>
+                      <div className='font-medium text-gray-900'>
+                        {t('OIDC')}
+                      </div>
+                      <div className='text-sm text-gray-500 truncate'>
+                        {renderAccountInfo(
+                          userState.user?.oidc_id,
+                          t('OIDC ID'),
+                        )}
                       </div>
                     </div>
                   </div>
-                  <div className="flex-shrink-0">
+                  <div className='flex-shrink-0'>
                     <Button
-                      type="primary"
-                      theme="outline"
-                      size="small"
-                      onClick={() => onOIDCClicked(
-                        status.oidc_authorization_endpoint,
-                        status.oidc_client_id,
-                      )}
+                      type='primary'
+                      theme='outline'
+                      size='small'
+                      onClick={() =>
+                        onOIDCClicked(
+                          status.oidc_authorization_endpoint,
+                          status.oidc_client_id,
+                        )
+                      }
                       disabled={
                         (userState.user && userState.user.oidc_id !== '') ||
                         !status.oidc_enabled
@@ -238,27 +275,35 @@ const AccountManagement = ({
               </Card>
 
               {/* Telegram绑定 */}
-              <Card className="!rounded-xl">
-                <div className="flex items-center justify-between gap-3">
-                  <div className="flex items-center flex-1 min-w-0">
-                    <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
-                      <SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
+              <Card className='!rounded-xl'>
+                <div className='flex items-center justify-between gap-3'>
+                  <div className='flex items-center flex-1 min-w-0'>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <SiTelegram
+                        size={20}
+                        className='text-slate-600 dark:text-slate-300'
+                      />
                     </div>
-                    <div className="flex-1 min-w-0">
-                      <div className="font-medium text-gray-900">{t('Telegram')}</div>
-                      <div className="text-sm text-gray-500 truncate">
-                        {renderAccountInfo(userState.user?.telegram_id, t('Telegram ID'))}
+                    <div className='flex-1 min-w-0'>
+                      <div className='font-medium text-gray-900'>
+                        {t('Telegram')}
+                      </div>
+                      <div className='text-sm text-gray-500 truncate'>
+                        {renderAccountInfo(
+                          userState.user?.telegram_id,
+                          t('Telegram ID'),
+                        )}
                       </div>
                     </div>
                   </div>
-                  <div className="flex-shrink-0">
+                  <div className='flex-shrink-0'>
                     {status.telegram_oauth ? (
                       userState.user.telegram_id !== '' ? (
-                        <Button disabled={true} size="small">
+                        <Button disabled={true} size='small'>
                           {t('已绑定')}
                         </Button>
                       ) : (
-                        <div className="scale-75">
+                        <div className='scale-75'>
                           <TelegramLoginButton
                             dataAuthUrl='/api/oauth/telegram/bind'
                             botName={status.telegram_bot_name}
@@ -266,7 +311,7 @@ const AccountManagement = ({
                         </div>
                       )
                     ) : (
-                      <Button disabled={true} size="small">
+                      <Button disabled={true} size='small'>
                         {t('未启用')}
                       </Button>
                     )}
@@ -275,25 +320,35 @@ const AccountManagement = ({
               </Card>
 
               {/* LinuxDO绑定 */}
-              <Card className="!rounded-xl">
-                <div className="flex items-center justify-between gap-3">
-                  <div className="flex items-center flex-1 min-w-0">
-                    <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
-                      <SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
+              <Card className='!rounded-xl'>
+                <div className='flex items-center justify-between gap-3'>
+                  <div className='flex items-center flex-1 min-w-0'>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <SiLinux
+                        size={20}
+                        className='text-slate-600 dark:text-slate-300'
+                      />
                     </div>
-                    <div className="flex-1 min-w-0">
-                      <div className="font-medium text-gray-900">{t('LinuxDO')}</div>
-                      <div className="text-sm text-gray-500 truncate">
-                        {renderAccountInfo(userState.user?.linux_do_id, t('LinuxDO ID'))}
+                    <div className='flex-1 min-w-0'>
+                      <div className='font-medium text-gray-900'>
+                        {t('LinuxDO')}
+                      </div>
+                      <div className='text-sm text-gray-500 truncate'>
+                        {renderAccountInfo(
+                          userState.user?.linux_do_id,
+                          t('LinuxDO ID'),
+                        )}
                       </div>
                     </div>
                   </div>
-                  <div className="flex-shrink-0">
+                  <div className='flex-shrink-0'>
                     <Button
-                      type="primary"
-                      theme="outline"
-                      size="small"
-                      onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
+                      type='primary'
+                      theme='outline'
+                      size='small'
+                      onClick={() =>
+                        onLinuxDOOAuthClicked(status.linuxdo_client_id)
+                      }
                       disabled={
                         (userState.user && userState.user.linux_do_id !== '') ||
                         !status.linuxdo_oauth
@@ -311,37 +366,37 @@ const AccountManagement = ({
         {/* 安全设置 Tab */}
         <TabPane
           tab={
-            <div className="flex items-center">
-              <ShieldCheck size={16} className="mr-2" />
+            <div className='flex items-center'>
+              <ShieldCheck size={16} className='mr-2' />
               {t('安全设置')}
             </div>
           }
-          itemKey="security"
+          itemKey='security'
         >
-          <div className="py-4">
-            <div className="space-y-6">
+          <div className='py-4'>
+            <div className='space-y-6'>
               <Space vertical className='w-full'>
                 {/* 系统访问令牌 */}
-                <Card className="!rounded-xl w-full">
-                  <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
-                    <div className="flex items-start w-full sm:w-auto">
-                      <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
-                        <IconKey size="large" className="text-slate-600" />
+                <Card className='!rounded-xl w-full'>
+                  <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
+                    <div className='flex items-start w-full sm:w-auto'>
+                      <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
+                        <IconKey size='large' className='text-slate-600' />
                       </div>
-                      <div className="flex-1">
-                        <Typography.Title heading={6} className="mb-1">
+                      <div className='flex-1'>
+                        <Typography.Title heading={6} className='mb-1'>
                           {t('系统访问令牌')}
                         </Typography.Title>
-                        <Typography.Text type="tertiary" className="text-sm">
+                        <Typography.Text type='tertiary' className='text-sm'>
                           {t('用于API调用的身份验证令牌,请妥善保管')}
                         </Typography.Text>
                         {systemToken && (
-                          <div className="mt-3">
+                          <div className='mt-3'>
                             <Input
                               readonly
                               value={systemToken}
                               onClick={handleSystemTokenClick}
-                              size="large"
+                              size='large'
                               prefix={<IconKey />}
                             />
                           </div>
@@ -349,10 +404,10 @@ const AccountManagement = ({
                       </div>
                     </div>
                     <Button
-                      type="primary"
-                      theme="solid"
+                      type='primary'
+                      theme='solid'
                       onClick={generateAccessToken}
-                      className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
+                      className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
                       icon={<IconKey />}
                     >
                       {systemToken ? t('重新生成') : t('生成令牌')}
@@ -361,26 +416,26 @@ const AccountManagement = ({
                 </Card>
 
                 {/* 密码管理 */}
-                <Card className="!rounded-xl w-full">
-                  <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
-                    <div className="flex items-start w-full sm:w-auto">
-                      <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
-                        <IconLock size="large" className="text-slate-600" />
+                <Card className='!rounded-xl w-full'>
+                  <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
+                    <div className='flex items-start w-full sm:w-auto'>
+                      <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
+                        <IconLock size='large' className='text-slate-600' />
                       </div>
                       <div>
-                        <Typography.Title heading={6} className="mb-1">
+                        <Typography.Title heading={6} className='mb-1'>
                           {t('密码管理')}
                         </Typography.Title>
-                        <Typography.Text type="tertiary" className="text-sm">
+                        <Typography.Text type='tertiary' className='text-sm'>
                           {t('定期更改密码可以提高账户安全性')}
                         </Typography.Text>
                       </div>
                     </div>
                     <Button
-                      type="primary"
-                      theme="solid"
+                      type='primary'
+                      theme='solid'
                       onClick={() => setShowChangePasswordModal(true)}
-                      className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
+                      className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
                       icon={<IconLock />}
                     >
                       {t('修改密码')}
@@ -392,26 +447,29 @@ const AccountManagement = ({
                 <TwoFASetting t={t} />
 
                 {/* 危险区域 */}
-                <Card className="!rounded-xl w-full">
-                  <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
-                    <div className="flex items-start w-full sm:w-auto">
-                      <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
-                        <IconDelete size="large" className="text-slate-600" />
+                <Card className='!rounded-xl w-full'>
+                  <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
+                    <div className='flex items-start w-full sm:w-auto'>
+                      <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
+                        <IconDelete size='large' className='text-slate-600' />
                       </div>
                       <div>
-                        <Typography.Title heading={6} className="mb-1 text-slate-700">
+                        <Typography.Title
+                          heading={6}
+                          className='mb-1 text-slate-700'
+                        >
                           {t('删除账户')}
                         </Typography.Title>
-                        <Typography.Text type="tertiary" className="text-sm">
+                        <Typography.Text type='tertiary' className='text-sm'>
                           {t('此操作不可逆,所有数据将被永久删除')}
                         </Typography.Text>
                       </div>
                     </div>
                     <Button
-                      type="danger"
-                      theme="solid"
+                      type='danger'
+                      theme='solid'
                       onClick={() => setShowAccountDeleteModal(true)}
-                      className="w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
+                      className='w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600'
                       icon={<IconDelete />}
                     >
                       {t('删除账户')}

+ 112 - 72
web/src/components/settings/personal/cards/ModelsList.jsx

@@ -27,13 +27,13 @@ import {
   Tabs,
   TabPane,
   Typography,
-  Avatar
+  Avatar,
 } from '@douyinfe/semi-ui';
-import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
 import {
-  IconChevronDown,
-  IconChevronUp
-} from '@douyinfe/semi-icons';
+  IllustrationNoContent,
+  IllustrationNoContentDark,
+} from '@douyinfe/semi-illustrations';
+import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
 import { Settings } from 'lucide-react';
 import { renderModelTag, getModelCategories } from '../../../../helpers';
 
@@ -52,38 +52,48 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
   }, [isModelsExpanded]);
 
   return (
-    <div className="py-4">
+    <div className='py-4'>
       {/* 卡片头部 */}
-      <div className="flex items-center mb-4">
-        <Avatar size="small" color="green" className="mr-3 shadow-md">
+      <div className='flex items-center mb-4'>
+        <Avatar size='small' color='green' className='mr-3 shadow-md'>
           <Settings size={16} />
         </Avatar>
         <div>
-          <Typography.Text className="text-lg font-medium">{t('可用模型')}</Typography.Text>
-          <div className="text-xs text-gray-600">{t('查看当前可用的所有模型')}</div>
+          <Typography.Text className='text-lg font-medium'>
+            {t('可用模型')}
+          </Typography.Text>
+          <div className='text-xs text-gray-600'>
+            {t('查看当前可用的所有模型')}
+          </div>
         </div>
       </div>
 
       {/* 可用模型部分 */}
-      <div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
+      <div className='bg-gray-50 dark:bg-gray-800 rounded-xl'>
         {modelsLoading ? (
           // 骨架屏加载状态 - 模拟实际加载后的布局
-          <div className="space-y-4">
+          <div className='space-y-4'>
             {/* 模拟分类标签 */}
-            <div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
-              <div className="flex overflow-x-auto py-2 gap-2">
+            <div
+              className='mb-4'
+              style={{ borderBottom: '1px solid var(--semi-color-border)' }}
+            >
+              <div className='flex overflow-x-auto py-2 gap-2'>
                 {Array.from({ length: 8 }).map((_, index) => (
-                  <Skeleton.Button key={`cat-${index}`} style={{
-                    width: index === 0 ? 130 : 100 + Math.random() * 50,
-                    height: 36,
-                    borderRadius: 8
-                  }} />
+                  <Skeleton.Button
+                    key={`cat-${index}`}
+                    style={{
+                      width: index === 0 ? 130 : 100 + Math.random() * 50,
+                      height: 36,
+                      borderRadius: 8,
+                    }}
+                  />
                 ))}
               </div>
             </div>
 
             {/* 模拟模型标签列表 */}
-            <div className="flex flex-wrap gap-2">
+            <div className='flex flex-wrap gap-2'>
               {Array.from({ length: 20 }).map((_, index) => (
                 <Skeleton.Button
                   key={`model-${index}`}
@@ -91,17 +101,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
                     width: 100 + Math.random() * 100,
                     height: 32,
                     borderRadius: 16,
-                    margin: '4px'
+                    margin: '4px',
                   }}
                 />
               ))}
             </div>
           </div>
         ) : models.length === 0 ? (
-          <div className="py-8">
+          <div className='py-8'>
             <Empty
-              image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
-              darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
+              image={
+                <IllustrationNoContent style={{ width: 150, height: 150 }} />
+              }
+              darkModeImage={
+                <IllustrationNoContentDark
+                  style={{ width: 150, height: 150 }}
+                />
+              }
               description={t('没有可用模型')}
               style={{ padding: '24px 0' }}
             />
@@ -109,59 +125,81 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
         ) : (
           <>
             {/* 模型分类标签页 */}
-            <div className="mb-4">
+            <div className='mb-4'>
               <Tabs
-                type="card"
+                type='card'
                 activeKey={activeModelCategory}
-                onChange={key => setActiveModelCategory(key)}
-                className="mt-2"
+                onChange={(key) => setActiveModelCategory(key)}
+                className='mt-2'
                 collapsible
               >
-                {Object.entries(getModelCategories(t)).map(([key, category]) => {
-                  // 计算该分类下的模型数量
-                  const modelCount = key === 'all'
-                    ? models.length
-                    : models.filter(model => category.filter({ model_name: model })).length;
+                {Object.entries(getModelCategories(t)).map(
+                  ([key, category]) => {
+                    // 计算该分类下的模型数量
+                    const modelCount =
+                      key === 'all'
+                        ? models.length
+                        : models.filter((model) =>
+                            category.filter({ model_name: model }),
+                          ).length;
 
-                  if (modelCount === 0 && key !== 'all') return null;
+                    if (modelCount === 0 && key !== 'all') return null;
 
-                  return (
-                    <TabPane
-                      tab={
-                        <span className="flex items-center gap-2">
-                          {category.icon && <span className="w-4 h-4">{category.icon}</span>}
-                          {category.label}
-                          <Tag
-                            color={activeModelCategory === key ? 'red' : 'grey'}
-                            size='small'
-                            shape='circle'
-                          >
-                            {modelCount}
-                          </Tag>
-                        </span>
-                      }
-                      itemKey={key}
-                      key={key}
-                    />
-                  );
-                })}
+                    return (
+                      <TabPane
+                        tab={
+                          <span className='flex items-center gap-2'>
+                            {category.icon && (
+                              <span className='w-4 h-4'>{category.icon}</span>
+                            )}
+                            {category.label}
+                            <Tag
+                              color={
+                                activeModelCategory === key ? 'red' : 'grey'
+                              }
+                              size='small'
+                              shape='circle'
+                            >
+                              {modelCount}
+                            </Tag>
+                          </span>
+                        }
+                        itemKey={key}
+                        key={key}
+                      />
+                    );
+                  },
+                )}
               </Tabs>
             </div>
 
-            <div className="bg-white dark:bg-gray-700 rounded-lg p-3">
+            <div className='bg-white dark:bg-gray-700 rounded-lg p-3'>
               {(() => {
                 // 根据当前选中的分类过滤模型
                 const categories = getModelCategories(t);
-                const filteredModels = activeModelCategory === 'all'
-                  ? models
-                  : models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
+                const filteredModels =
+                  activeModelCategory === 'all'
+                    ? models
+                    : models.filter((model) =>
+                        categories[activeModelCategory].filter({
+                          model_name: model,
+                        }),
+                      );
 
                 // 如果过滤后没有模型,显示空状态
                 if (filteredModels.length === 0) {
                   return (
                     <Empty
-                      image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
-                      darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
+                      image={
+                        <IllustrationNoContent
+                          style={{ width: 120, height: 120 }}
+                        />
+                      }
+                      darkModeImage={
+                        <IllustrationNoContentDark
+                          style={{ width: 120, height: 120 }}
+                        />
+                      }
                       description={t('该分类下没有可用模型')}
                       style={{ padding: '16px 0' }}
                     />
@@ -171,13 +209,13 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
                 if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
                   return (
                     <Space wrap>
-                      {filteredModels.map((model) => (
+                      {filteredModels.map((model) =>
                         renderModelTag(model, {
                           size: 'small',
                           shape: 'circle',
                           onClick: () => copyText(model),
-                        })
-                      ))}
+                        }),
+                      )}
                     </Space>
                   );
                 } else {
@@ -185,17 +223,17 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
                     <>
                       <Collapsible isOpen={isModelsExpanded}>
                         <Space wrap>
-                          {filteredModels.map((model) => (
+                          {filteredModels.map((model) =>
                             renderModelTag(model, {
                               size: 'small',
                               shape: 'circle',
                               onClick: () => copyText(model),
-                            })
-                          ))}
+                            }),
+                          )}
                           <Tag
                             color='grey'
                             type='light'
-                            className="cursor-pointer !rounded-lg"
+                            className='cursor-pointer !rounded-lg'
                             onClick={() => setIsModelsExpanded(false)}
                             icon={<IconChevronUp />}
                           >
@@ -207,21 +245,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
                         <Space wrap>
                           {filteredModels
                             .slice(0, MODELS_DISPLAY_COUNT)
-                            .map((model) => (
+                            .map((model) =>
                               renderModelTag(model, {
                                 size: 'small',
                                 shape: 'circle',
                                 onClick: () => copyText(model),
-                              })
-                            ))}
+                              }),
+                            )}
                           <Tag
                             color='grey'
                             type='light'
-                            className="cursor-pointer !rounded-lg"
+                            className='cursor-pointer !rounded-lg'
                             onClick={() => setIsModelsExpanded(true)}
                             icon={<IconChevronDown />}
                           >
-                            {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
+                            {t('更多')}{' '}
+                            {filteredModels.length - MODELS_DISPLAY_COUNT}{' '}
+                            {t('个模型')}
                           </Tag>
                         </Space>
                       )}

+ 96 - 63
web/src/components/settings/personal/cards/NotificationSettings.jsx

@@ -27,14 +27,9 @@ import {
   Radio,
   Toast,
   Tabs,
-  TabPane
+  TabPane,
 } from '@douyinfe/semi-ui';
-import {
-  IconMail,
-  IconKey,
-  IconBell,
-  IconLink
-} from '@douyinfe/semi-icons';
+import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
 import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
 import { renderQuotaWithPrompt } from '../../../../helpers';
 import CodeViewer from '../../../playground/CodeViewer';
@@ -43,7 +38,7 @@ const NotificationSettings = ({
   t,
   notificationSettings,
   handleNotificationSettingChange,
-  saveNotificationSettings
+  saveNotificationSettings,
 }) => {
   const formApiRef = useRef(null);
 
@@ -62,7 +57,8 @@ const NotificationSettings = ({
   // 表单提交
   const handleSubmit = () => {
     if (formApiRef.current) {
-      formApiRef.current.validate()
+      formApiRef.current
+        .validate()
         .then(() => {
           saveNotificationSettings();
         })
@@ -77,26 +73,27 @@ const NotificationSettings = ({
 
   return (
     <Card
-      className="!rounded-2xl shadow-sm border-0"
+      className='!rounded-2xl shadow-sm border-0'
       footer={
-        <div className="flex justify-end">
-          <Button
-            type='primary'
-            onClick={handleSubmit}
-          >
+        <div className='flex justify-end'>
+          <Button type='primary' onClick={handleSubmit}>
             {t('保存设置')}
           </Button>
         </div>
       }
     >
       {/* 卡片头部 */}
-      <div className="flex items-center mb-4">
-        <Avatar size="small" color="blue" className="mr-3 shadow-md">
+      <div className='flex items-center mb-4'>
+        <Avatar size='small' color='blue' className='mr-3 shadow-md'>
           <Bell size={16} />
         </Avatar>
         <div>
-          <Typography.Text className="text-lg font-medium">{t('其他设置')}</Typography.Text>
-          <div className="text-xs text-gray-600">{t('通知、价格和隐私相关设置')}</div>
+          <Typography.Text className='text-lg font-medium'>
+            {t('其他设置')}
+          </Typography.Text>
+          <div className='text-xs text-gray-600'>
+            {t('通知、价格和隐私相关设置')}
+          </div>
         </div>
       </div>
 
@@ -106,18 +103,18 @@ const NotificationSettings = ({
         onSubmit={handleSubmit}
       >
         {() => (
-          <Tabs type="card" defaultActiveKey="notification">
+          <Tabs type='card' defaultActiveKey='notification'>
             {/* 通知配置 Tab */}
             <TabPane
               tab={
-                <div className="flex items-center">
-                  <Bell size={16} className="mr-2" />
+                <div className='flex items-center'>
+                  <Bell size={16} className='mr-2' />
                   {t('通知配置')}
                 </div>
               }
-              itemKey="notification"
+              itemKey='notification'
             >
-              <div className="py-4">
+              <div className='py-4'>
                 <Form.RadioGroup
                   field='warningType'
                   label={t('通知方式')}
@@ -125,15 +122,18 @@ const NotificationSettings = ({
                   onChange={(value) => handleFormChange('warningType', value)}
                   rules={[{ required: true, message: t('请选择通知方式') }]}
                 >
-                  <Radio value="email">{t('邮件通知')}</Radio>
-                  <Radio value="webhook">{t('Webhook通知')}</Radio>
+                  <Radio value='email'>{t('邮件通知')}</Radio>
+                  <Radio value='webhook'>{t('Webhook通知')}</Radio>
                 </Form.RadioGroup>
 
                 <Form.AutoComplete
                   field='warningThreshold'
                   label={
                     <span>
-                      {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
+                      {t('额度预警阈值')}{' '}
+                      {renderQuotaWithPrompt(
+                        notificationSettings.warningThreshold,
+                      )}
                     </span>
                   }
                   placeholder={t('请输入预警额度')}
@@ -145,7 +145,9 @@ const NotificationSettings = ({
                   ]}
                   onChange={(val) => handleFormChange('warningThreshold', val)}
                   prefix={<IconBell />}
-                  extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
+                  extraText={t(
+                    '当剩余额度低于此数值时,系统将通过选择的方式发送通知',
+                  )}
                   style={{ width: '100%', maxWidth: '300px' }}
                   rules={[
                     { required: true, message: t('请输入预警阈值') },
@@ -156,8 +158,8 @@ const NotificationSettings = ({
                           return Promise.reject(t('预警阈值必须为正数'));
                         }
                         return Promise.resolve();
-                      }
-                    }
+                      },
+                    },
                   ]}
                 />
 
@@ -167,9 +169,13 @@ const NotificationSettings = ({
                     field='notificationEmail'
                     label={t('通知邮箱')}
                     placeholder={t('留空则使用账号绑定的邮箱')}
-                    onChange={(val) => handleFormChange('notificationEmail', val)}
+                    onChange={(val) =>
+                      handleFormChange('notificationEmail', val)
+                    }
                     prefix={<IconMail />}
-                    extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
+                    extraText={t(
+                      '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
+                    )}
                     showClear
                   />
                 )}
@@ -180,20 +186,25 @@ const NotificationSettings = ({
                     <Form.Input
                       field='webhookUrl'
                       label={t('Webhook地址')}
-                      placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
+                      placeholder={t(
+                        '请输入Webhook地址,例如: https://example.com/webhook',
+                      )}
                       onChange={(val) => handleFormChange('webhookUrl', val)}
                       prefix={<IconLink />}
-                      extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
+                      extraText={t(
+                        '只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
+                      )}
                       showClear
                       rules={[
                         {
-                          required: notificationSettings.warningType === 'webhook',
-                          message: t('请输入Webhook地址')
+                          required:
+                            notificationSettings.warningType === 'webhook',
+                          message: t('请输入Webhook地址'),
                         },
                         {
                           pattern: /^https:\/\/.+/,
-                          message: t('Webhook地址必须以https://开头')
-                        }
+                          message: t('Webhook地址必须以https://开头'),
+                        },
                       ]}
                     />
 
@@ -203,7 +214,9 @@ const NotificationSettings = ({
                       placeholder={t('请输入密钥')}
                       onChange={(val) => handleFormChange('webhookSecret', val)}
                       prefix={<IconKey />}
-                      extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
+                      extraText={t(
+                        '密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
+                      )}
                       showClear
                     />
 
@@ -212,22 +225,36 @@ const NotificationSettings = ({
                         <div style={{ height: '200px', marginBottom: '12px' }}>
                           <CodeViewer
                             content={{
-                              "type": "quota_exceed",
-                              "title": "额度预警通知",
-                              "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
-                              "values": ["$0.99"],
-                              "timestamp": 1739950503
+                              type: 'quota_exceed',
+                              title: '额度预警通知',
+                              content:
+                                '您的额度即将用尽,当前剩余额度为 {{value}}',
+                              values: ['$0.99'],
+                              timestamp: 1739950503,
                             }}
-                            title="webhook"
-                            language="json"
+                            title='webhook'
+                            language='json'
                           />
                         </div>
-                        <div className="text-xs text-gray-500 leading-relaxed">
-                          <div><strong>type:</strong> {t('通知类型 (quota_exceed: 额度预警)')} </div>
-                          <div><strong>title:</strong> {t('通知标题')}</div>
-                          <div><strong>content:</strong> {t('通知内容,支持 {{value}} 变量占位符')}</div>
-                          <div><strong>values:</strong> {t('按顺序替换content中的变量占位符')}</div>
-                          <div><strong>timestamp:</strong> {t('Unix时间戳')}</div>
+                        <div className='text-xs text-gray-500 leading-relaxed'>
+                          <div>
+                            <strong>type:</strong>{' '}
+                            {t('通知类型 (quota_exceed: 额度预警)')}{' '}
+                          </div>
+                          <div>
+                            <strong>title:</strong> {t('通知标题')}
+                          </div>
+                          <div>
+                            <strong>content:</strong>{' '}
+                            {t('通知内容,支持 {{value}} 变量占位符')}
+                          </div>
+                          <div>
+                            <strong>values:</strong>{' '}
+                            {t('按顺序替换content中的变量占位符')}
+                          </div>
+                          <div>
+                            <strong>timestamp:</strong> {t('Unix时间戳')}
+                          </div>
                         </div>
                       </div>
                     </Form.Slot>
@@ -239,21 +266,25 @@ const NotificationSettings = ({
             {/* 价格设置 Tab */}
             <TabPane
               tab={
-                <div className="flex items-center">
-                  <DollarSign size={16} className="mr-2" />
+                <div className='flex items-center'>
+                  <DollarSign size={16} className='mr-2' />
                   {t('价格设置')}
                 </div>
               }
-              itemKey="pricing"
+              itemKey='pricing'
             >
-              <div className="py-4">
+              <div className='py-4'>
                 <Form.Switch
                   field='acceptUnsetModelRatioModel'
                   label={t('接受未设置价格模型')}
                   checkedText={t('开')}
                   uncheckedText={t('关')}
-                  onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
-                  extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
+                  onChange={(value) =>
+                    handleFormChange('acceptUnsetModelRatioModel', value)
+                  }
+                  extraText={t(
+                    '当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
+                  )}
                 />
               </div>
             </TabPane>
@@ -261,21 +292,23 @@ const NotificationSettings = ({
             {/* 隐私设置 Tab */}
             <TabPane
               tab={
-                <div className="flex items-center">
-                  <ShieldCheck size={16} className="mr-2" />
+                <div className='flex items-center'>
+                  <ShieldCheck size={16} className='mr-2' />
                   {t('隐私设置')}
                 </div>
               }
-              itemKey="privacy"
+              itemKey='privacy'
             >
-              <div className="py-4">
+              <div className='py-4'>
                 <Form.Switch
                   field='recordIpLog'
                   label={t('记录请求与错误日志IP')}
                   checkedText={t('开')}
                   uncheckedText={t('关')}
                   onChange={(value) => handleFormChange('recordIpLog', value)}
-                  extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
+                  extraText={t(
+                    '开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
+                  )}
                 />
               </div>
             </TabPane>

+ 189 - 129
web/src/components/settings/personal/components/TwoFASetting.jsx

@@ -17,12 +17,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 import { API, showError, showSuccess, showWarning } from '../../../../helpers';
-import { Banner, Button, Card, Checkbox, Divider, Input, Modal, Tag, Typography, Steps, Space, Badge } from '@douyinfe/semi-ui';
+import {
+  Banner,
+  Button,
+  Card,
+  Checkbox,
+  Divider,
+  Input,
+  Modal,
+  Tag,
+  Typography,
+  Steps,
+  Space,
+  Badge,
+} from '@douyinfe/semi-ui';
 import {
   IconShield,
   IconAlertTriangle,
   IconRefresh,
-  IconCopy
+  IconCopy,
 } from '@douyinfe/semi-icons';
 import React, { useEffect, useState } from 'react';
 
@@ -35,7 +48,7 @@ const TwoFASetting = ({ t }) => {
   const [status, setStatus] = useState({
     enabled: false,
     locked: false,
-    backup_codes_remaining: 0
+    backup_codes_remaining: 0,
   });
 
   // 模态框状态
@@ -96,7 +109,7 @@ const TwoFASetting = ({ t }) => {
     setLoading(true);
     try {
       const res = await API.post('/api/user/2fa/enable', {
-        code: verificationCode
+        code: verificationCode,
       });
       if (res.data.success) {
         showSuccess(t('两步验证启用成功!'));
@@ -130,7 +143,7 @@ const TwoFASetting = ({ t }) => {
     setLoading(true);
     try {
       const res = await API.post('/api/user/2fa/disable', {
-        code: verificationCode
+        code: verificationCode,
       });
       if (res.data.success) {
         showSuccess(t('两步验证已禁用'));
@@ -158,7 +171,7 @@ const TwoFASetting = ({ t }) => {
     setLoading(true);
     try {
       const res = await API.post('/api/user/2fa/backup_codes', {
-        code: verificationCode
+        code: verificationCode,
       });
       if (res.data.success) {
         setBackupCodes(res.data.data.backup_codes);
@@ -177,11 +190,14 @@ const TwoFASetting = ({ t }) => {
 
   // 通用复制函数
   const copyTextToClipboard = (text, successMessage = t('已复制到剪贴板')) => {
-    navigator.clipboard.writeText(text).then(() => {
-      showSuccess(successMessage);
-    }).catch(() => {
-      showError(t('复制失败,请手动复制'));
-    });
+    navigator.clipboard
+      .writeText(text)
+      .then(() => {
+        showSuccess(successMessage);
+      })
+      .catch(() => {
+        showError(t('复制失败,请手动复制'));
+      });
   };
 
   const copyBackupCodes = () => {
@@ -192,28 +208,25 @@ const TwoFASetting = ({ t }) => {
   // 备用码展示组件
   const BackupCodesDisplay = ({ codes, title, onCopy }) => {
     return (
-      <Card
-        className="!rounded-xl"
-        style={{ width: '100%' }}
-      >
-        <div className="space-y-3">
-          <div className="flex items-center justify-between">
-            <Text strong className="text-slate-700 dark:text-slate-200">
+      <Card className='!rounded-xl' style={{ width: '100%' }}>
+        <div className='space-y-3'>
+          <div className='flex items-center justify-between'>
+            <Text strong className='text-slate-700 dark:text-slate-200'>
               {title}
             </Text>
           </div>
 
-          <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
+          <div className='grid grid-cols-1 sm:grid-cols-2 gap-2'>
             {codes.map((code, index) => (
-              <div
-                key={index}
-                className="rounded-lg p-3"
-              >
-                <div className="flex items-center justify-between">
-                  <Text code className="text-sm font-mono text-slate-700 dark:text-slate-200">
+              <div key={index} className='rounded-lg p-3'>
+                <div className='flex items-center justify-between'>
+                  <Text
+                    code
+                    className='text-sm font-mono text-slate-700 dark:text-slate-200'
+                  >
                     {code}
                   </Text>
-                  <Text type="quaternary" className="text-xs">
+                  <Text type='quaternary' className='text-xs'>
                     #{(index + 1).toString().padStart(2, '0')}
                   </Text>
                 </div>
@@ -223,11 +236,11 @@ const TwoFASetting = ({ t }) => {
 
           <Divider margin={12} />
           <Button
-            type="primary"
-            theme="solid"
+            type='primary'
+            theme='solid'
             icon={<IconCopy />}
             onClick={onCopy}
-            className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full"
+            className='!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full'
           >
             {t('复制所有代码')}
           </Button>
@@ -243,24 +256,24 @@ const TwoFASetting = ({ t }) => {
         {currentStep > 0 && (
           <Button
             onClick={() => setCurrentStep(currentStep - 1)}
-            className="!rounded-lg"
+            className='!rounded-lg'
           >
             {t('上一步')}
           </Button>
         )}
         {currentStep < 2 ? (
           <Button
-            type="primary"
-            theme="solid"
+            type='primary'
+            theme='solid'
             onClick={() => setCurrentStep(currentStep + 1)}
-            className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
+            className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
           >
             {t('下一步')}
           </Button>
         ) : (
           <Button
-            type="primary"
-            theme="solid"
+            type='primary'
+            theme='solid'
             loading={loading}
             onClick={() => {
               if (!verificationCode) {
@@ -269,7 +282,7 @@ const TwoFASetting = ({ t }) => {
               }
               handleEnable2FA();
             }}
-            className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
+            className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
           >
             {t('完成设置并启用两步验证')}
           </Button>
@@ -288,17 +301,17 @@ const TwoFASetting = ({ t }) => {
             setVerificationCode('');
             setConfirmDisable(false);
           }}
-          className="!rounded-lg"
+          className='!rounded-lg'
         >
           {t('取消')}
         </Button>
         <Button
-          type="danger"
-          theme="solid"
+          type='danger'
+          theme='solid'
           loading={loading}
           disabled={!confirmDisable || !verificationCode}
           onClick={handleDisable2FA}
-          className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
+          className='!rounded-lg !bg-slate-500 hover:!bg-slate-600'
         >
           {t('确认禁用')}
         </Button>
@@ -311,14 +324,14 @@ const TwoFASetting = ({ t }) => {
     if (backupCodes.length > 0) {
       return (
         <Button
-          type="primary"
-          theme="solid"
+          type='primary'
+          theme='solid'
           onClick={() => {
             setBackupModalVisible(false);
             setVerificationCode('');
             setBackupCodes([]);
           }}
-          className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
+          className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
         >
           {t('完成')}
         </Button>
@@ -333,17 +346,17 @@ const TwoFASetting = ({ t }) => {
             setVerificationCode('');
             setBackupCodes([]);
           }}
-          className="!rounded-lg"
+          className='!rounded-lg'
         >
           {t('取消')}
         </Button>
         <Button
-          type="primary"
-          theme="solid"
+          type='primary'
+          theme='solid'
           loading={loading}
           disabled={!verificationCode}
           onClick={handleRegenerateBackupCodes}
-          className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
+          className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
         >
           {t('生成新的备用码')}
         </Button>
@@ -353,67 +366,82 @@ const TwoFASetting = ({ t }) => {
 
   return (
     <>
-      <Card className="!rounded-xl w-full">
-        <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
-          <div className="flex items-start w-full sm:w-auto">
-            <div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4 flex-shrink-0">
-              <IconShield size="large" className="text-slate-600 dark:text-slate-300" />
+      <Card className='!rounded-xl w-full'>
+        <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
+          <div className='flex items-start w-full sm:w-auto'>
+            <div className='w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4 flex-shrink-0'>
+              <IconShield
+                size='large'
+                className='text-slate-600 dark:text-slate-300'
+              />
             </div>
-            <div className="flex-1">
-              <div className="flex items-center gap-2 mb-1">
-                <Typography.Title heading={6} className="mb-0">
+            <div className='flex-1'>
+              <div className='flex items-center gap-2 mb-1'>
+                <Typography.Title heading={6} className='mb-0'>
                   {t('两步验证设置')}
                 </Typography.Title>
                 {status.enabled ? (
-                  <Tag color="green" shape="circle" size="small">{t('已启用')}</Tag>
+                  <Tag color='green' shape='circle' size='small'>
+                    {t('已启用')}
+                  </Tag>
                 ) : (
-                  <Tag color="red" shape="circle" size="small">{t('未启用')}</Tag>
+                  <Tag color='red' shape='circle' size='small'>
+                    {t('未启用')}
+                  </Tag>
                 )}
                 {status.locked && (
-                  <Tag color="orange" shape="circle" size="small">{t('账户已锁定')}</Tag>
+                  <Tag color='orange' shape='circle' size='small'>
+                    {t('账户已锁定')}
+                  </Tag>
                 )}
               </div>
-              <Typography.Text type="tertiary" className="text-sm">
-                {t('两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。')}
+              <Typography.Text type='tertiary' className='text-sm'>
+                {t(
+                  '两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。',
+                )}
               </Typography.Text>
               {status.enabled && (
-                <div className="mt-2">
-                  <Text size="small" type="secondary">{t('剩余备用码:')}{status.backup_codes_remaining || 0}{t('个')}</Text>
+                <div className='mt-2'>
+                  <Text size='small' type='secondary'>
+                    {t('剩余备用码:')}
+                    {status.backup_codes_remaining || 0}
+                    {t('个')}
+                  </Text>
                 </div>
               )}
             </div>
           </div>
-          <div className="flex flex-col space-y-2 w-full sm:w-auto">
+          <div className='flex flex-col space-y-2 w-full sm:w-auto'>
             {!status.enabled ? (
               <Button
-                type="primary"
-                theme="solid"
-                size="default"
+                type='primary'
+                theme='solid'
+                size='default'
                 onClick={handleSetup2FA}
                 loading={loading}
-                className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
+                className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
                 icon={<IconShield />}
               >
                 {t('启用验证')}
               </Button>
             ) : (
-              <div className="flex flex-col space-y-2">
+              <div className='flex flex-col space-y-2'>
                 <Button
-                  type="danger"
-                  theme="solid"
-                  size="default"
+                  type='danger'
+                  theme='solid'
+                  size='default'
                   onClick={() => setDisableModalVisible(true)}
-                  className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
+                  className='!rounded-lg !bg-slate-500 hover:!bg-slate-600'
                   icon={<IconAlertTriangle />}
                 >
                   {t('禁用两步验证')}
                 </Button>
                 <Button
-                  type="primary"
-                  theme="solid"
-                  size="default"
+                  type='primary'
+                  theme='solid'
+                  size='default'
                   onClick={() => setBackupModalVisible(true)}
-                  className="!rounded-lg"
+                  className='!rounded-lg'
                   icon={<IconRefresh />}
                 >
                   {t('重新生成备用码')}
@@ -427,8 +455,8 @@ const TwoFASetting = ({ t }) => {
       {/* 2FA设置模态框 */}
       <Modal
         title={
-          <div className="flex items-center">
-            <IconShield className="mr-2 text-slate-600" />
+          <div className='flex items-center'>
+            <IconShield className='mr-2 text-slate-600' />
             {t('设置两步验证')}
           </div>
         }
@@ -444,36 +472,50 @@ const TwoFASetting = ({ t }) => {
         style={{ maxWidth: '90vw' }}
       >
         {setupData && (
-          <div className="space-y-6">
+          <div className='space-y-6'>
             {/* 步骤进度 */}
-            <Steps type="basic" size="small" current={currentStep}>
-              <Steps.Step title={t('扫描二维码')} description={t('使用认证器应用扫描二维码')} />
-              <Steps.Step title={t('保存备用码')} description={t('保存备用码以备不时之需')} />
-              <Steps.Step title={t('验证设置')} description={t('输入验证码完成设置')} />
+            <Steps type='basic' size='small' current={currentStep}>
+              <Steps.Step
+                title={t('扫描二维码')}
+                description={t('使用认证器应用扫描二维码')}
+              />
+              <Steps.Step
+                title={t('保存备用码')}
+                description={t('保存备用码以备不时之需')}
+              />
+              <Steps.Step
+                title={t('验证设置')}
+                description={t('输入验证码完成设置')}
+              />
             </Steps>
 
             {/* 步骤内容 */}
-            <div className="rounded-xl">
+            <div className='rounded-xl'>
               {currentStep === 0 && (
                 <div>
-                  <Paragraph className="text-gray-600 dark:text-gray-300 mb-4">
-                    {t('使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:')}
+                  <Paragraph className='text-gray-600 dark:text-gray-300 mb-4'>
+                    {t(
+                      '使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:',
+                    )}
                   </Paragraph>
-                  <div className="flex justify-center mb-4">
-                    <div className="bg-white p-4 rounded-lg shadow-sm">
+                  <div className='flex justify-center mb-4'>
+                    <div className='bg-white p-4 rounded-lg shadow-sm'>
                       <QRCodeSVG value={setupData.qr_code_data} size={180} />
                     </div>
                   </div>
-                  <div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
-                    <Text className="text-blue-800 dark:text-blue-200 text-sm">
-                      {t('或手动输入密钥:')}<Text code copyable className="ml-2">{setupData.secret}</Text>
+                  <div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-3'>
+                    <Text className='text-blue-800 dark:text-blue-200 text-sm'>
+                      {t('或手动输入密钥:')}
+                      <Text code copyable className='ml-2'>
+                        {setupData.secret}
+                      </Text>
                     </Text>
                   </div>
                 </div>
               )}
 
               {currentStep === 1 && (
-                <div className="space-y-4">
+                <div className='space-y-4'>
                   {/* 备用码展示 */}
                   <BackupCodesDisplay
                     codes={setupData.backup_codes}
@@ -491,9 +533,9 @@ const TwoFASetting = ({ t }) => {
                   placeholder={t('输入认证器应用显示的6位数字验证码')}
                   value={verificationCode}
                   onChange={setVerificationCode}
-                  size="large"
+                  size='large'
                   maxLength={6}
-                  className="!rounded-lg"
+                  className='!rounded-lg'
                 />
               )}
             </div>
@@ -504,8 +546,8 @@ const TwoFASetting = ({ t }) => {
       {/* 禁用2FA模态框 */}
       <Modal
         title={
-          <div className="flex items-center">
-            <IconAlertTriangle className="mr-2 text-red-500" />
+          <div className='flex items-center'>
+            <IconAlertTriangle className='mr-2 text-red-500' />
             {t('禁用两步验证')}
           </div>
         }
@@ -519,36 +561,41 @@ const TwoFASetting = ({ t }) => {
         width={550}
         style={{ maxWidth: '90vw' }}
       >
-        <div className="space-y-6">
+        <div className='space-y-6'>
           {/* 警告提示 */}
-          <div className="rounded-xl">
+          <div className='rounded-xl'>
             <Banner
-              type="warning"
-              description={t('警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!')}
-              className="!rounded-lg"
+              type='warning'
+              description={t(
+                '警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!',
+              )}
+              className='!rounded-lg'
             />
           </div>
 
           {/* 内容区域 */}
-          <div className="space-y-4">
+          <div className='space-y-4'>
             <div>
-              <Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
+              <Text
+                strong
+                className='block mb-2 text-slate-700 dark:text-slate-200'
+              >
                 {t('禁用后的影响:')}
               </Text>
-              <ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
-                <li className="flex items-start gap-2">
+              <ul className='space-y-2 text-sm text-slate-600 dark:text-slate-300'>
+                <li className='flex items-start gap-2'>
                   <Badge dot type='warning' />
                   {t('降低您账户的安全性')}
                 </li>
-                <li className="flex items-start gap-2">
+                <li className='flex items-start gap-2'>
                   <Badge dot type='warning' />
                   {t('需要重新完整设置才能再次启用')}
                 </li>
-                <li className="flex items-start gap-2">
+                <li className='flex items-start gap-2'>
                   <Badge dot type='danger' />
                   {t('永久删除您的两步验证设置')}
                 </li>
-                <li className="flex items-start gap-2">
+                <li className='flex items-start gap-2'>
                   <Badge dot type='danger' />
                   {t('永久删除所有备用码(包括未使用的)')}
                 </li>
@@ -557,17 +604,20 @@ const TwoFASetting = ({ t }) => {
 
             <Divider margin={16} />
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
+                <Text
+                  strong
+                  className='block mb-2 text-slate-700 dark:text-slate-200'
+                >
                   {t('验证身份')}
                 </Text>
                 <Input
                   placeholder={t('请输入认证器验证码或备用码')}
                   value={verificationCode}
                   onChange={setVerificationCode}
-                  size="large"
-                  className="!rounded-lg"
+                  size='large'
+                  className='!rounded-lg'
                 />
               </div>
 
@@ -575,9 +625,11 @@ const TwoFASetting = ({ t }) => {
                 <Checkbox
                   checked={confirmDisable}
                   onChange={(e) => setConfirmDisable(e.target.checked)}
-                  className="text-sm"
+                  className='text-sm'
                 >
-                  {t('我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销')}
+                  {t(
+                    '我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销',
+                  )}
                 </Checkbox>
               </div>
             </div>
@@ -588,8 +640,8 @@ const TwoFASetting = ({ t }) => {
       {/* 重新生成备用码模态框 */}
       <Modal
         title={
-          <div className="flex items-center">
-            <IconRefresh className="mr-2 text-slate-600" />
+          <div className='flex items-center'>
+            <IconRefresh className='mr-2 text-slate-600' />
             {t('重新生成备用码')}
           </div>
         }
@@ -603,30 +655,35 @@ const TwoFASetting = ({ t }) => {
         width={500}
         style={{ maxWidth: '90vw' }}
       >
-        <div className="space-y-6">
+        <div className='space-y-6'>
           {backupCodes.length === 0 ? (
             <>
               {/* 警告提示 */}
-              <div className="rounded-xl">
+              <div className='rounded-xl'>
                 <Banner
-                  type="warning"
-                  description={t('重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。')}
-                  className="!rounded-lg"
+                  type='warning'
+                  description={t(
+                    '重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。',
+                  )}
+                  className='!rounded-lg'
                 />
               </div>
 
               {/* 验证区域 */}
-              <div className="space-y-4">
+              <div className='space-y-4'>
                 <div>
-                  <Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
+                  <Text
+                    strong
+                    className='block mb-2 text-slate-700 dark:text-slate-200'
+                  >
                     {t('验证身份')}
                   </Text>
                   <Input
                     placeholder={t('请输入认证器验证码')}
                     value={verificationCode}
                     onChange={setVerificationCode}
-                    size="large"
-                    className="!rounded-lg"
+                    size='large'
+                    className='!rounded-lg'
                   />
                 </div>
               </div>
@@ -635,13 +692,16 @@ const TwoFASetting = ({ t }) => {
             <>
               {/* 成功提示 */}
               <Space vertical style={{ width: '100%' }}>
-                <div className="flex items-center justify-center gap-2">
+                <div className='flex items-center justify-center gap-2'>
                   <Badge dot type='success' />
-                  <Text strong className="text-lg text-slate-700 dark:text-slate-200">
+                  <Text
+                    strong
+                    className='text-lg text-slate-700 dark:text-slate-200'
+                  >
                     {t('新的备用码已生成')}
                   </Text>
                 </div>
-                <Text className="text-slate-500 dark:text-slate-400 text-sm">
+                <Text className='text-slate-500 dark:text-slate-400 text-sm'>
                   {t('旧的备用码已失效,请保存新的备用码')}
                 </Text>
 
@@ -660,4 +720,4 @@ const TwoFASetting = ({ t }) => {
   );
 };
 
-export default TwoFASetting;
+export default TwoFASetting;

+ 94 - 53
web/src/components/settings/personal/components/UserInfoHeader.jsx

@@ -18,12 +18,23 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import { Avatar, Card, Tag, Divider, Typography, Badge } from '@douyinfe/semi-ui';
-import { isRoot, isAdmin, renderQuota, stringToColor } from '../../../../helpers';
+import {
+  Avatar,
+  Card,
+  Tag,
+  Divider,
+  Typography,
+  Badge,
+} from '@douyinfe/semi-ui';
+import {
+  isRoot,
+  isAdmin,
+  renderQuota,
+  stringToColor,
+} from '../../../../helpers';
 import { Coins, BarChart2, Users } from 'lucide-react';
 
 const UserInfoHeader = ({ t, userState }) => {
-
   const getUsername = () => {
     if (userState.user) {
       return userState.user.username;
@@ -42,31 +53,33 @@ const UserInfoHeader = ({ t, userState }) => {
 
   return (
     <Card
-      className="!rounded-2xl overflow-hidden"
+      className='!rounded-2xl overflow-hidden'
       cover={
         <div
-          className="relative h-32"
+          className='relative h-32'
           style={{
             '--palette-primary-darkerChannel': '0 75 80',
             backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
             backgroundSize: 'cover',
             backgroundPosition: 'center',
-            backgroundRepeat: 'no-repeat'
+            backgroundRepeat: 'no-repeat',
           }}
         >
           {/* 用户信息内容 */}
-          <div className="relative z-10 h-full flex flex-col justify-end p-6">
-            <div className="flex items-center">
-              <div className="flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0">
-                <Avatar
-                  size='large'
-                  color={stringToColor(getUsername())}
-                >
+          <div className='relative z-10 h-full flex flex-col justify-end p-6'>
+            <div className='flex items-center'>
+              <div className='flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0'>
+                <Avatar size='large' color={stringToColor(getUsername())}>
                   {getAvatarText()}
                 </Avatar>
-                <div className="flex-1 min-w-0 flex flex-col justify-between">
-                  <div className="text-3xl font-bold truncate" style={{ color: 'white' }}>{getUsername()}</div>
-                  <div className="flex flex-wrap items-center gap-2">
+                <div className='flex-1 min-w-0 flex flex-col justify-between'>
+                  <div
+                    className='text-3xl font-bold truncate'
+                    style={{ color: 'white' }}
+                  >
+                    {getUsername()}
+                  </div>
+                  <div className='flex flex-wrap items-center gap-2'>
                     {isRoot() ? (
                       <Tag
                         size='large'
@@ -92,11 +105,7 @@ const UserInfoHeader = ({ t, userState }) => {
                         {t('普通用户')}
                       </Tag>
                     )}
-                    <Tag
-                      size='large'
-                      shape='circle'
-                      style={{ color: 'white' }}
-                    >
+                    <Tag size='large' shape='circle' style={{ color: 'white' }}>
                       ID: {userState?.user?.id}
                     </Tag>
                   </div>
@@ -108,34 +117,50 @@ const UserInfoHeader = ({ t, userState }) => {
       }
     >
       {/* 当前余额和桌面版统计信息 */}
-      <div className="flex items-start justify-between gap-6">
+      <div className='flex items-start justify-between gap-6'>
         {/* 当前余额显示 */}
         <Badge count={t('当前余额')} position='rightTop' type='danger'>
-          <div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide">
+          <div className='text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide'>
             {renderQuota(userState?.user?.quota)}
           </div>
         </Badge>
 
         {/* 桌面版统计信息(Semi UI 卡片) */}
-        <div className="hidden lg:block flex-shrink-0">
-          <Card size="small" className="!rounded-xl" bodyStyle={{ padding: '12px 16px' }}>
-            <div className="flex items-center gap-4">
-              <div className="flex items-center gap-2">
+        <div className='hidden lg:block flex-shrink-0'>
+          <Card
+            size='small'
+            className='!rounded-xl'
+            bodyStyle={{ padding: '12px 16px' }}
+          >
+            <div className='flex items-center gap-4'>
+              <div className='flex items-center gap-2'>
                 <Coins size={16} />
-                <Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
-                <Typography.Text size="small" type="tertiary" strong>{renderQuota(userState?.user?.used_quota)}</Typography.Text>
+                <Typography.Text size='small' type='tertiary'>
+                  {t('历史消耗')}
+                </Typography.Text>
+                <Typography.Text size='small' type='tertiary' strong>
+                  {renderQuota(userState?.user?.used_quota)}
+                </Typography.Text>
               </div>
-              <Divider layout="vertical" />
-              <div className="flex items-center gap-2">
+              <Divider layout='vertical' />
+              <div className='flex items-center gap-2'>
                 <BarChart2 size={16} />
-                <Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
-                <Typography.Text size="small" type="tertiary" strong>{userState.user?.request_count || 0}</Typography.Text>
+                <Typography.Text size='small' type='tertiary'>
+                  {t('请求次数')}
+                </Typography.Text>
+                <Typography.Text size='small' type='tertiary' strong>
+                  {userState.user?.request_count || 0}
+                </Typography.Text>
               </div>
-              <Divider layout="vertical" />
-              <div className="flex items-center gap-2">
+              <Divider layout='vertical' />
+              <div className='flex items-center gap-2'>
                 <Users size={16} />
-                <Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
-                <Typography.Text size="small" type="tertiary" strong>{userState?.user?.group || t('默认')}</Typography.Text>
+                <Typography.Text size='small' type='tertiary'>
+                  {t('用户分组')}
+                </Typography.Text>
+                <Typography.Text size='small' type='tertiary' strong>
+                  {userState?.user?.group || t('默认')}
+                </Typography.Text>
               </div>
             </div>
           </Card>
@@ -143,31 +168,47 @@ const UserInfoHeader = ({ t, userState }) => {
       </div>
 
       {/* 移动端和中等屏幕统计信息卡片 */}
-      <div className="lg:hidden mt-2">
-        <Card size="small" className="!rounded-xl" bodyStyle={{ padding: '12px 16px' }} >
-          <div className="space-y-3">
-            <div className="flex items-center justify-between">
-              <div className="flex items-center gap-2">
+      <div className='lg:hidden mt-2'>
+        <Card
+          size='small'
+          className='!rounded-xl'
+          bodyStyle={{ padding: '12px 16px' }}
+        >
+          <div className='space-y-3'>
+            <div className='flex items-center justify-between'>
+              <div className='flex items-center gap-2'>
                 <Coins size={16} />
-                <Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
+                <Typography.Text size='small' type='tertiary'>
+                  {t('历史消耗')}
+                </Typography.Text>
               </div>
-              <Typography.Text size="small" type="tertiary" strong>{renderQuota(userState?.user?.used_quota)}</Typography.Text>
+              <Typography.Text size='small' type='tertiary' strong>
+                {renderQuota(userState?.user?.used_quota)}
+              </Typography.Text>
             </div>
             <Divider margin='8px' />
-            <div className="flex items-center justify-between">
-              <div className="flex items-center gap-2">
+            <div className='flex items-center justify-between'>
+              <div className='flex items-center gap-2'>
                 <BarChart2 size={16} />
-                <Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
+                <Typography.Text size='small' type='tertiary'>
+                  {t('请求次数')}
+                </Typography.Text>
               </div>
-              <Typography.Text size="small" type="tertiary" strong>{userState.user?.request_count || 0}</Typography.Text>
+              <Typography.Text size='small' type='tertiary' strong>
+                {userState.user?.request_count || 0}
+              </Typography.Text>
             </div>
             <Divider margin='8px' />
-            <div className="flex items-center justify-between">
-              <div className="flex items-center gap-2">
+            <div className='flex items-center justify-between'>
+              <div className='flex items-center gap-2'>
                 <Users size={16} />
-                <Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
+                <Typography.Text size='small' type='tertiary'>
+                  {t('用户分组')}
+                </Typography.Text>
               </div>
-              <Typography.Text size="small" type="tertiary" strong>{userState?.user?.group || t('默认')}</Typography.Text>
+              <Typography.Text size='small' type='tertiary' strong>
+                {userState?.user?.group || t('默认')}
+              </Typography.Text>
             </div>
           </div>
         </Card>
@@ -176,4 +217,4 @@ const UserInfoHeader = ({ t, userState }) => {
   );
 };
 
-export default UserInfoHeader;
+export default UserInfoHeader;

+ 13 - 11
web/src/components/settings/personal/modals/AccountDeleteModal.jsx

@@ -32,13 +32,13 @@ const AccountDeleteModal = ({
   userState,
   turnstileEnabled,
   turnstileSiteKey,
-  setTurnstileToken
+  setTurnstileToken,
 }) => {
   return (
     <Modal
       title={
-        <div className="flex items-center">
-          <IconDelete className="mr-2 text-red-500" />
+        <div className='flex items-center'>
+          <IconDelete className='mr-2 text-red-500' />
           {t('删除账户确认')}
         </div>
       }
@@ -47,35 +47,37 @@ const AccountDeleteModal = ({
       onOk={deleteAccount}
       size={'small'}
       centered={true}
-      className="modern-modal"
+      className='modern-modal'
     >
-      <div className="space-y-4 py-4">
+      <div className='space-y-4 py-4'>
         <Banner
           type='danger'
           description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
           closeIcon={null}
-          className="!rounded-lg"
+          className='!rounded-lg'
         />
 
         <div>
-          <Typography.Text strong className="block mb-2 text-red-600">
+          <Typography.Text strong className='block mb-2 text-red-600'>
             {t('请输入您的用户名以确认删除')}
           </Typography.Text>
           <Input
-            placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })}
+            placeholder={t('输入你的账户名{{username}}以确认删除', {
+              username: ` ${userState?.user?.username} `,
+            })}
             name='self_account_deletion_confirmation'
             value={inputs.self_account_deletion_confirmation}
             onChange={(value) =>
               handleInputChange('self_account_deletion_confirmation', value)
             }
-            size="large"
-            className="!rounded-lg"
+            size='large'
+            className='!rounded-lg'
             prefix={<IconUser />}
           />
         </div>
 
         {turnstileEnabled && (
-          <div className="flex justify-center">
+          <div className='flex justify-center'>
             <Turnstile
               sitekey={turnstileSiteKey}
               onVerify={(token) => {

+ 23 - 21
web/src/components/settings/personal/modals/ChangePasswordModal.jsx

@@ -31,13 +31,13 @@ const ChangePasswordModal = ({
   changePassword,
   turnstileEnabled,
   turnstileSiteKey,
-  setTurnstileToken
+  setTurnstileToken,
 }) => {
   return (
     <Modal
       title={
-        <div className="flex items-center">
-          <IconLock className="mr-2 text-orange-500" />
+        <div className='flex items-center'>
+          <IconLock className='mr-2 text-orange-500' />
           {t('修改密码')}
         </div>
       }
@@ -46,43 +46,45 @@ const ChangePasswordModal = ({
       onOk={changePassword}
       size={'small'}
       centered={true}
-      className="modern-modal"
+      className='modern-modal'
     >
-      <div className="space-y-4 py-4">
+      <div className='space-y-4 py-4'>
         <div>
-          <Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text>
+          <Typography.Text strong className='block mb-2'>
+            {t('原密码')}
+          </Typography.Text>
           <Input
             name='original_password'
             placeholder={t('请输入原密码')}
             type='password'
             value={inputs.original_password}
-            onChange={(value) =>
-              handleInputChange('original_password', value)
-            }
-            size="large"
-            className="!rounded-lg"
+            onChange={(value) => handleInputChange('original_password', value)}
+            size='large'
+            className='!rounded-lg'
             prefix={<IconLock />}
           />
         </div>
 
         <div>
-          <Typography.Text strong className="block mb-2">{t('新密码')}</Typography.Text>
+          <Typography.Text strong className='block mb-2'>
+            {t('新密码')}
+          </Typography.Text>
           <Input
             name='set_new_password'
             placeholder={t('请输入新密码')}
             type='password'
             value={inputs.set_new_password}
-            onChange={(value) =>
-              handleInputChange('set_new_password', value)
-            }
-            size="large"
-            className="!rounded-lg"
+            onChange={(value) => handleInputChange('set_new_password', value)}
+            size='large'
+            className='!rounded-lg'
             prefix={<IconLock />}
           />
         </div>
 
         <div>
-          <Typography.Text strong className="block mb-2">{t('确认新密码')}</Typography.Text>
+          <Typography.Text strong className='block mb-2'>
+            {t('确认新密码')}
+          </Typography.Text>
           <Input
             name='set_new_password_confirmation'
             placeholder={t('请再次输入新密码')}
@@ -91,14 +93,14 @@ const ChangePasswordModal = ({
             onChange={(value) =>
               handleInputChange('set_new_password_confirmation', value)
             }
-            size="large"
-            className="!rounded-lg"
+            size='large'
+            className='!rounded-lg'
             prefix={<IconLock />}
           />
         </div>
 
         {turnstileEnabled && (
-          <div className="flex justify-center">
+          <div className='flex justify-center'>
             <Turnstile
               sitekey={turnstileSiteKey}
               onVerify={(token) => {

+ 17 - 15
web/src/components/settings/personal/modals/EmailBindModal.jsx

@@ -35,13 +35,13 @@ const EmailBindModal = ({
   countdown,
   turnstileEnabled,
   turnstileSiteKey,
-  setTurnstileToken
+  setTurnstileToken,
 }) => {
   return (
     <Modal
       title={
-        <div className="flex items-center">
-          <IconMail className="mr-2 text-blue-500" />
+        <div className='flex items-center'>
+          <IconMail className='mr-2 text-blue-500' />
           {t('绑定邮箱地址')}
         </div>
       }
@@ -51,28 +51,30 @@ const EmailBindModal = ({
       size={'small'}
       centered={true}
       maskClosable={false}
-      className="modern-modal"
+      className='modern-modal'
     >
-      <div className="space-y-4 py-4">
-        <div className="flex gap-3">
+      <div className='space-y-4 py-4'>
+        <div className='flex gap-3'>
           <Input
             placeholder={t('输入邮箱地址')}
             onChange={(value) => handleInputChange('email', value)}
             name='email'
             type='email'
-            size="large"
-            className="!rounded-lg flex-1"
+            size='large'
+            className='!rounded-lg flex-1'
             prefix={<IconMail />}
           />
           <Button
             onClick={sendVerificationCode}
             disabled={disableButton || loading}
-            className="!rounded-lg"
-            type="primary"
-            theme="outline"
+            className='!rounded-lg'
+            type='primary'
+            theme='outline'
             size='large'
           >
-            {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
+            {disableButton
+              ? `${t('重新发送')} (${countdown})`
+              : t('获取验证码')}
           </Button>
         </div>
 
@@ -83,13 +85,13 @@ const EmailBindModal = ({
           onChange={(value) =>
             handleInputChange('email_verification_code', value)
           }
-          size="large"
-          className="!rounded-lg"
+          size='large'
+          className='!rounded-lg'
           prefix={<IconKey />}
         />
 
         {turnstileEnabled && (
-          <div className="flex justify-center">
+          <div className='flex justify-center'>
             <Turnstile
               sitekey={turnstileSiteKey}
               onVerify={(token) => {

+ 16 - 16
web/src/components/settings/personal/modals/WeChatBindModal.jsx

@@ -29,13 +29,13 @@ const WeChatBindModal = ({
   inputs,
   handleInputChange,
   bindWeChat,
-  status
+  status,
 }) => {
   return (
     <Modal
       title={
-        <div className="flex items-center">
-          <SiWechat className="mr-2 text-green-500" size={20} />
+        <div className='flex items-center'>
+          <SiWechat className='mr-2 text-green-500' size={20} />
           {t('绑定微信账户')}
         </div>
       }
@@ -44,30 +44,30 @@ const WeChatBindModal = ({
       footer={null}
       size={'small'}
       centered={true}
-      className="modern-modal"
+      className='modern-modal'
     >
-      <div className="space-y-4 py-4 text-center">
-        <Image src={status.wechat_qrcode} className="mx-auto" />
-        <div className="text-gray-600">
-          <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
+      <div className='space-y-4 py-4 text-center'>
+        <Image src={status.wechat_qrcode} className='mx-auto' />
+        <div className='text-gray-600'>
+          <p>
+            {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
+          </p>
         </div>
         <Input
           placeholder={t('验证码')}
           name='wechat_verification_code'
           value={inputs.wechat_verification_code}
-          onChange={(v) =>
-            handleInputChange('wechat_verification_code', v)
-          }
-          size="large"
-          className="!rounded-lg"
+          onChange={(v) => handleInputChange('wechat_verification_code', v)}
+          size='large'
+          className='!rounded-lg'
           prefix={<IconKey />}
         />
         <Button
-          type="primary"
-          theme="solid"
+          type='primary'
+          theme='solid'
           size='large'
           onClick={bindWeChat}
-          className="!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700"
+          className='!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700'
           icon={<SiWechat size={16} />}
         >
           {t('绑定')}

+ 24 - 22
web/src/components/setup/SetupWizard.jsx

@@ -130,7 +130,11 @@ const SetupWizard = () => {
           return true; // 如果已经初始化,可以继续
         }
         // 检查必填字段
-        if (!formData.username || !formData.password || !formData.confirmPassword) {
+        if (
+          !formData.username ||
+          !formData.password ||
+          !formData.confirmPassword
+        ) {
           showError(t('请填写完整的管理员账号信息'));
           return false;
         }
@@ -226,12 +230,7 @@ const SetupWizard = () => {
   const getStepContent = (step) => {
     switch (step) {
       case 0:
-        return (
-          <DatabaseStep
-            setupStatus={setupStatus}
-            t={t}
-          />
-        );
+        return <DatabaseStep setupStatus={setupStatus} t={t} />;
       case 1:
         return (
           <AdminStep
@@ -252,11 +251,7 @@ const SetupWizard = () => {
         );
       case 3:
         return (
-          <CompleteStep
-            setupStatus={setupStatus}
-            formData={formData}
-            t={t}
-          />
+          <CompleteStep setupStatus={setupStatus} formData={formData} t={t} />
         );
       default:
         return null;
@@ -275,21 +270,25 @@ const SetupWizard = () => {
 
   return (
     <div className='min-h-screen flex items-center justify-center px-4'>
-      <div className="w-full max-w-4xl">
-        <Card className="!rounded-2xl shadow-sm border-0">
-          <div className="mb-4">
-            <div className="text-xl font-semibold">{t('系统初始化')}</div>
-            <div className="text-xs text-gray-600">
+      <div className='w-full max-w-4xl'>
+        <Card className='!rounded-2xl shadow-sm border-0'>
+          <div className='mb-4'>
+            <div className='text-xl font-semibold'>{t('系统初始化')}</div>
+            <div className='text-xs text-gray-600'>
               {t('欢迎使用,请完成以下设置以开始使用系统')}
             </div>
           </div>
 
-          <div className="px-2 py-2">
-            <Steps type="basic" current={currentStep}>
+          <div className='px-2 py-2'>
+            <Steps type='basic' current={currentStep}>
               {steps.map((item, index) => (
                 <Steps.Step
                   key={item.title}
-                  title={<span className={currentStep === index ? 'shine-text' : ''}>{item.title}</span>}
+                  title={
+                    <span className={currentStep === index ? 'shine-text' : ''}>
+                      {item.title}
+                    </span>
+                  }
                   description={item.description}
                 />
               ))}
@@ -306,9 +305,12 @@ const SetupWizard = () => {
             initValues={formData}
           >
             {/* 步骤内容:保持所有字段挂载,仅隐藏非当前步骤 */}
-            <div className="steps-content">
+            <div className='steps-content'>
               {[0, 1, 2, 3].map((idx) => (
-                <div key={idx} style={{ display: currentStep === idx ? 'block' : 'none' }}>
+                <div
+                  key={idx}
+                  style={{ display: currentStep === idx ? 'block' : 'none' }}
+                >
                   {React.cloneElement(getStepContent(idx), {
                     ...stepNavigationProps,
                     renderNavigationButtons: () => (

+ 7 - 14
web/src/components/setup/components/StepNavigation.jsx

@@ -32,29 +32,22 @@ const StepNavigation = ({
   next,
   onSubmit,
   loading,
-  t
+  t,
 }) => {
   return (
-    <div className="flex justify-between items-center pt-4">
+    <div className='flex justify-between items-center pt-4'>
       {/* 上一步按钮 */}
       {currentStep > 0 && (
-        <Button
-          onClick={prev}
-          className="!rounded-lg"
-        >
+        <Button onClick={prev} className='!rounded-lg'>
           {t('上一步')}
         </Button>
       )}
 
-      <div className="flex-1"></div>
+      <div className='flex-1'></div>
 
       {/* 下一步按钮 */}
       {currentStep < steps.length - 1 && (
-        <Button
-          type="primary"
-          onClick={next}
-          className="!rounded-lg"
-        >
+        <Button type='primary' onClick={next} className='!rounded-lg'>
           {t('下一步')}
         </Button>
       )}
@@ -62,10 +55,10 @@ const StepNavigation = ({
       {/* 完成按钮 */}
       {currentStep === steps.length - 1 && (
         <Button
-          type="primary"
+          type='primary'
           onClick={onSubmit}
           loading={loading}
-          className="!rounded-lg"
+          className='!rounded-lg'
           icon={<IconCheckCircleStroked />}
         >
           {t('初始化系统')}

+ 11 - 11
web/src/components/setup/components/steps/AdminStep.jsx

@@ -31,7 +31,7 @@ const AdminStep = ({
   setFormData,
   formRef,
   renderNavigationButtons,
-  t
+  t,
 }) => {
   return (
     <>
@@ -40,11 +40,11 @@ const AdminStep = ({
           type='info'
           closeIcon={null}
           description={
-            <div className="flex items-center">
+            <div className='flex items-center'>
               <span>{t('管理员账号已经初始化过,请继续设置其他参数')}</span>
             </div>
           }
-          className="!rounded-lg"
+          className='!rounded-lg'
         />
       ) : (
         <>
@@ -55,7 +55,7 @@ const AdminStep = ({
             prefix={<IconUser />}
             showClear
             noLabel={false}
-            validateStatus="default"
+            validateStatus='default'
             rules={[{ required: true, message: t('请输入管理员用户名') }]}
             initValue={formData.username || ''}
             onChange={(value) => {
@@ -70,11 +70,11 @@ const AdminStep = ({
             prefix={<IconLock />}
             showClear
             noLabel={false}
-            mode="password"
-            validateStatus="default"
+            mode='password'
+            validateStatus='default'
             rules={[
               { required: true, message: t('请输入管理员密码') },
-              { min: 8, message: t('密码长度至少为8个字符') }
+              { min: 8, message: t('密码长度至少为8个字符') },
             ]}
             initValue={formData.password || ''}
             onChange={(value) => {
@@ -89,8 +89,8 @@ const AdminStep = ({
             prefix={<IconLock />}
             showClear
             noLabel={false}
-            mode="password"
-            validateStatus="default"
+            mode='password'
+            validateStatus='default'
             rules={[
               { required: true, message: t('请确认管理员密码') },
               {
@@ -102,8 +102,8 @@ const AdminStep = ({
                     }
                   }
                   return Promise.resolve();
-                }
-              }
+                },
+              },
             ]}
             initValue={formData.confirmPassword || ''}
             onChange={(value) => {

+ 20 - 10
web/src/components/setup/components/steps/CompleteStep.jsx

@@ -31,29 +31,39 @@ const CompleteStep = ({
   setupStatus,
   formData,
   renderNavigationButtons,
-  t
+  t,
 }) => {
   return (
-    <div className="text-center">
-      <Avatar color="green" className="mx-auto mb-4 shadow-lg">
+    <div className='text-center'>
+      <Avatar color='green' className='mx-auto mb-4 shadow-lg'>
         <CheckCircle size={24} />
       </Avatar>
-      <Title heading={3} className="mb-2">{t('准备完成初始化')}</Title>
-      <Text type="secondary" className="mb-6 block">
+      <Title heading={3} className='mb-2'>
+        {t('准备完成初始化')}
+      </Title>
+      <Text type='secondary' className='mb-6 block'>
         {t('请确认以下设置信息,点击"初始化系统"开始配置')}
       </Text>
 
       <Descriptions>
         <Descriptions.Item itemKey={t('数据库类型')}>
-          {setupStatus.database_type === 'sqlite' ? 'SQLite' :
-            setupStatus.database_type === 'mysql' ? 'MySQL' : 'PostgreSQL'}
+          {setupStatus.database_type === 'sqlite'
+            ? 'SQLite'
+            : setupStatus.database_type === 'mysql'
+              ? 'MySQL'
+              : 'PostgreSQL'}
         </Descriptions.Item>
         <Descriptions.Item itemKey={t('管理员账号')}>
-          {setupStatus.root_init ? t('已初始化') : (formData.username || t('未设置'))}
+          {setupStatus.root_init
+            ? t('已初始化')
+            : formData.username || t('未设置')}
         </Descriptions.Item>
         <Descriptions.Item itemKey={t('使用模式')}>
-          {formData.usageMode === 'external' ? t('对外运营模式') :
-            formData.usageMode === 'self' ? t('自用模式') : t('演示站点模式')}
+          {formData.usageMode === 'external'
+            ? t('对外运营模式')
+            : formData.usageMode === 'self'
+              ? t('自用模式')
+              : t('演示站点模式')}
         </Descriptions.Item>
       </Descriptions>
 

+ 9 - 7
web/src/components/setup/components/steps/DatabaseStep.jsx

@@ -40,14 +40,16 @@ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
                   '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
                 )}
               </p>
-              <p className="mt-1">
-                <strong>{t(
-                  '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
-                )}</strong>
+              <p className='mt-1'>
+                <strong>
+                  {t(
+                    '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
+                  )}
+                </strong>
               </p>
             </div>
           }
-          className="!rounded-lg"
+          className='!rounded-lg'
           fullMode={false}
           bordered
         />
@@ -68,7 +70,7 @@ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
               </p>
             </div>
           }
-          className="!rounded-lg"
+          className='!rounded-lg'
           fullMode={false}
           bordered
         />
@@ -89,7 +91,7 @@ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
               </p>
             </div>
           }
-          className="!rounded-lg"
+          className='!rounded-lg'
           fullMode={false}
           bordered
         />

+ 4 - 4
web/src/components/setup/components/steps/UsageModeStep.jsx

@@ -28,7 +28,7 @@ const UsageModeStep = ({
   formData,
   handleUsageModeChange,
   renderNavigationButtons,
-  t
+  t,
 }) => {
   return (
     <>
@@ -37,9 +37,9 @@ const UsageModeStep = ({
         onChange={handleUsageModeChange}
         type='card'
         direction='horizontal'
-        className="mt-4"
-        aria-label="使用模式选择"
-        name="usage-mode-selection"
+        className='mt-4'
+        aria-label='使用模式选择'
+        name='usage-mode-selection'
       >
         <Radio
           value='external'

+ 1 - 1
web/src/components/setup/index.jsx

@@ -26,4 +26,4 @@ export { default as StepNavigation } from './components/StepNavigation';
 export { default as DatabaseStep } from './components/steps/DatabaseStep';
 export { default as AdminStep } from './components/steps/AdminStep';
 export { default as UsageModeStep } from './components/steps/UsageModeStep';
-export { default as CompleteStep } from './components/steps/CompleteStep';
+export { default as CompleteStep } from './components/steps/CompleteStep';

+ 56 - 30
web/src/components/table/channels/ChannelsActions.jsx

@@ -24,7 +24,7 @@ import {
   Modal,
   Switch,
   Typography,
-  Select
+  Select,
 } from '@douyinfe/semi-ui';
 import CompactModeToggle from '../../common/ui/CompactModeToggle';
 
@@ -52,19 +52,19 @@ const ChannelsActions = ({
   activePage,
   pageSize,
   setActivePage,
-  t
+  t,
 }) => {
   return (
-    <div className="flex flex-col gap-2">
+    <div className='flex flex-col gap-2'>
       {/* 第一行:批量操作按钮 + 设置开关 */}
-      <div className="flex flex-col md:flex-row justify-between gap-2">
+      <div className='flex flex-col md:flex-row justify-between gap-2'>
         {/* 左侧:批量操作按钮 */}
-        <div className="flex flex-wrap md:flex-nowrap items-center gap-2 w-full md:w-auto order-2 md:order-1">
+        <div className='flex flex-wrap md:flex-nowrap items-center gap-2 w-full md:w-auto order-2 md:order-1'>
           <Button
             size='small'
             disabled={!enableBatchDelete}
             type='danger'
-            className="w-full md:w-auto"
+            className='w-full md:w-auto'
             onClick={() => {
               Modal.confirm({
                 title: t('确定是否要删除所选通道?'),
@@ -81,7 +81,7 @@ const ChannelsActions = ({
             disabled={!enableBatchDelete}
             type='tertiary'
             onClick={() => setShowBatchSetTag(true)}
-            className="w-full md:w-auto"
+            className='w-full md:w-auto'
           >
             {t('批量设置标签')}
           </Button>
@@ -95,7 +95,7 @@ const ChannelsActions = ({
                   <Button
                     size='small'
                     type='tertiary'
-                    className="w-full"
+                    className='w-full'
                     onClick={() => {
                       Modal.confirm({
                         title: t('确定?'),
@@ -112,11 +112,13 @@ const ChannelsActions = ({
                 <Dropdown.Item>
                   <Button
                     size='small'
-                    className="w-full"
+                    className='w-full'
                     onClick={() => {
                       Modal.confirm({
                         title: t('确定是否要修复数据库一致性?'),
-                        content: t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'),
+                        content: t(
+                          '进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用',
+                        ),
                         onOk: () => fixChannelsAbilities(),
                         size: 'sm',
                         centered: true,
@@ -130,7 +132,7 @@ const ChannelsActions = ({
                   <Button
                     size='small'
                     type='secondary'
-                    className="w-full"
+                    className='w-full'
                     onClick={() => {
                       Modal.confirm({
                         title: t('确定?'),
@@ -148,7 +150,7 @@ const ChannelsActions = ({
                   <Button
                     size='small'
                     type='danger'
-                    className="w-full"
+                    className='w-full'
                     onClick={() => {
                       Modal.confirm({
                         title: t('确定是否要删除禁用通道?'),
@@ -165,7 +167,12 @@ const ChannelsActions = ({
               </Dropdown.Menu>
             }
           >
-            <Button size='small' theme='light' type='tertiary' className="w-full md:w-auto">
+            <Button
+              size='small'
+              theme='light'
+              type='tertiary'
+              className='w-full md:w-auto'
+            >
               {t('批量操作')}
             </Button>
           </Dropdown>
@@ -178,9 +185,9 @@ const ChannelsActions = ({
         </div>
 
         {/* 右侧:设置开关区域 */}
-        <div className="flex flex-col md:flex-row items-start md:items-center gap-2 w-full md:w-auto order-1 md:order-2">
-          <div className="flex items-center justify-between w-full md:w-auto">
-            <Typography.Text strong className="mr-2">
+        <div className='flex flex-col md:flex-row items-start md:items-center gap-2 w-full md:w-auto order-1 md:order-2'>
+          <div className='flex items-center justify-between w-full md:w-auto'>
+            <Typography.Text strong className='mr-2'>
               {t('使用ID排序')}
             </Typography.Text>
             <Switch
@@ -189,18 +196,30 @@ const ChannelsActions = ({
               onChange={(v) => {
                 localStorage.setItem('id-sort', v + '');
                 setIdSort(v);
-                const { searchKeyword, searchGroup, searchModel } = getFormValues();
-                if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
+                const { searchKeyword, searchGroup, searchModel } =
+                  getFormValues();
+                if (
+                  searchKeyword === '' &&
+                  searchGroup === '' &&
+                  searchModel === ''
+                ) {
                   loadChannels(activePage, pageSize, v, enableTagMode);
                 } else {
-                  searchChannels(enableTagMode, activeTypeKey, statusFilter, activePage, pageSize, v);
+                  searchChannels(
+                    enableTagMode,
+                    activeTypeKey,
+                    statusFilter,
+                    activePage,
+                    pageSize,
+                    v,
+                  );
                 }
               }}
             />
           </div>
 
-          <div className="flex items-center justify-between w-full md:w-auto">
-            <Typography.Text strong className="mr-2">
+          <div className='flex items-center justify-between w-full md:w-auto'>
+            <Typography.Text strong className='mr-2'>
               {t('开启批量操作')}
             </Typography.Text>
             <Switch
@@ -213,8 +232,8 @@ const ChannelsActions = ({
             />
           </div>
 
-          <div className="flex items-center justify-between w-full md:w-auto">
-            <Typography.Text strong className="mr-2">
+          <div className='flex items-center justify-between w-full md:w-auto'>
+            <Typography.Text strong className='mr-2'>
               {t('标签聚合模式')}
             </Typography.Text>
             <Switch
@@ -229,8 +248,8 @@ const ChannelsActions = ({
             />
           </div>
 
-          <div className="flex items-center justify-between w-full md:w-auto">
-            <Typography.Text strong className="mr-2">
+          <div className='flex items-center justify-between w-full md:w-auto'>
+            <Typography.Text strong className='mr-2'>
               {t('状态筛选')}
             </Typography.Text>
             <Select
@@ -240,12 +259,19 @@ const ChannelsActions = ({
                 localStorage.setItem('channel-status-filter', v);
                 setStatusFilter(v);
                 setActivePage(1);
-                loadChannels(1, pageSize, idSort, enableTagMode, activeTypeKey, v);
+                loadChannels(
+                  1,
+                  pageSize,
+                  idSort,
+                  enableTagMode,
+                  activeTypeKey,
+                  v,
+                );
               }}
             >
-              <Select.Option value="all">{t('全部')}</Select.Option>
-              <Select.Option value="enabled">{t('已启用')}</Select.Option>
-              <Select.Option value="disabled">{t('已禁用')}</Select.Option>
+              <Select.Option value='all'>{t('全部')}</Select.Option>
+              <Select.Option value='enabled'>{t('已启用')}</Select.Option>
+              <Select.Option value='disabled'>{t('已禁用')}</Select.Option>
             </Select>
           </div>
         </div>
@@ -254,4 +280,4 @@ const ChannelsActions = ({
   );
 };
 
-export default ChannelsActions; 
+export default ChannelsActions;

+ 62 - 70
web/src/components/table/channels/ChannelsColumnDefs.jsx

@@ -27,14 +27,14 @@ import {
   SplitButtonGroup,
   Tag,
   Tooltip,
-  Typography
+  Typography,
 } from '@douyinfe/semi-ui';
 import {
   timestamp2string,
   renderGroup,
   renderQuota,
   getChannelIcon,
-  renderQuotaWithAmount
+  renderQuotaWithAmount,
 } from '../../../helpers';
 import { CHANNEL_OPTIONS } from '../../../constants';
 import { IconTreeTriangleDown, IconMore } from '@douyinfe/semi-icons';
@@ -51,27 +51,22 @@ const renderType = (type, channelInfo = undefined, t) => {
   let icon = getChannelIcon(type);
 
   if (channelInfo?.is_multi_key) {
-    icon = (
+    icon =
       channelInfo?.multi_key_mode === 'random' ? (
-        <div className="flex items-center gap-1">
-          <FaRandom className="text-blue-500" />
+        <div className='flex items-center gap-1'>
+          <FaRandom className='text-blue-500' />
           {icon}
         </div>
       ) : (
-        <div className="flex items-center gap-1">
-          <IconTreeTriangleDown className="text-blue-500" />
+        <div className='flex items-center gap-1'>
+          <IconTreeTriangleDown className='text-blue-500' />
           {icon}
         </div>
-      )
-    )
+      );
   }
 
   return (
-    <Tag
-      color={type2label[type]?.color}
-      shape='circle'
-      prefixIcon={icon}
-    >
+    <Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}>
       {type2label[type]?.label}
     </Tag>
   );
@@ -79,11 +74,7 @@ const renderType = (type, channelInfo = undefined, t) => {
 
 const renderTagType = (t) => {
   return (
-    <Tag
-      color='light-blue'
-      shape='circle'
-      type='light'
-    >
+    <Tag color='light-blue' shape='circle' type='light'>
       {t('标签聚合')}
     </Tag>
   );
@@ -95,7 +86,8 @@ const renderStatus = (status, channelInfo = undefined, t) => {
       let keySize = channelInfo.multi_key_size;
       let enabledKeySize = keySize;
       if (channelInfo.multi_key_status_list) {
-        enabledKeySize = keySize - Object.keys(channelInfo.multi_key_status_list).length;
+        enabledKeySize =
+          keySize - Object.keys(channelInfo.multi_key_status_list).length;
       }
       return renderMultiKeyStatus(status, keySize, enabledKeySize, t);
     }
@@ -155,7 +147,7 @@ const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => {
         </Tag>
       );
   }
-}
+};
 
 const renderResponseTime = (responseTime, t) => {
   let time = responseTime / 1000;
@@ -212,7 +204,7 @@ export const getChannelsColumns = ({
   activePage,
   channels,
   setShowMultiKeyManageModal,
-  setCurrentMultiKeyChannel
+  setCurrentMultiKeyChannel,
 }) => {
   return [
     {
@@ -276,7 +268,9 @@ export const getChannelsColumns = ({
           return (
             <div>
               <Tooltip
-                content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
+                content={
+                  t('原因:') + reason + t(',时间:') + timestamp2string(time)
+                }
               >
                 {renderStatus(text, record.channel_info, t)}
               </Tooltip>
@@ -291,9 +285,7 @@ export const getChannelsColumns = ({
       key: COLUMN_KEYS.RESPONSE_TIME,
       title: t('响应时间'),
       dataIndex: 'response_time',
-      render: (text, record, index) => (
-        <div>{renderResponseTime(text, t)}</div>
-      ),
+      render: (text, record, index) => <div>{renderResponseTime(text, t)}</div>,
     },
     {
       key: COLUMN_KEYS.BALANCE,
@@ -309,7 +301,9 @@ export const getChannelsColumns = ({
                     {renderQuota(record.used_quota)}
                   </Tag>
                 </Tooltip>
-                <Tooltip content={t('剩余额度$') + record.balance + t(',点击更新')}>
+                <Tooltip
+                  content={t('剩余额度$') + record.balance + t(',点击更新')}
+                >
                   <Tag
                     color='white'
                     type='ghost'
@@ -351,7 +345,7 @@ export const getChannelsColumns = ({
                 innerButtons
                 defaultValue={record.priority}
                 min={-999}
-                size="small"
+                size='small'
               />
             </div>
           );
@@ -364,7 +358,10 @@ export const getChannelsColumns = ({
               onBlur={(e) => {
                 Modal.warning({
                   title: t('修改子渠道优先级'),
-                  content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
+                  content:
+                    t('确定要修改所有子渠道优先级为 ') +
+                    e.target.value +
+                    t(' 吗?'),
                   onOk: () => {
                     if (e.target.value === '') {
                       return;
@@ -379,7 +376,7 @@ export const getChannelsColumns = ({
               innerButtons
               defaultValue={record.priority}
               min={-999}
-              size="small"
+              size='small'
             />
           );
         }
@@ -403,7 +400,7 @@ export const getChannelsColumns = ({
                 innerButtons
                 defaultValue={record.weight}
                 min={0}
-                size="small"
+                size='small'
               />
             </div>
           );
@@ -416,7 +413,10 @@ export const getChannelsColumns = ({
               onBlur={(e) => {
                 Modal.warning({
                   title: t('修改子渠道权重'),
-                  content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
+                  content:
+                    t('确定要修改所有子渠道权重为 ') +
+                    e.target.value +
+                    t(' 吗?'),
                   onOk: () => {
                     if (e.target.value === '') {
                       return;
@@ -431,7 +431,7 @@ export const getChannelsColumns = ({
               innerButtons
               defaultValue={record.weight}
               min={-999}
-              size="small"
+              size='small'
             />
           );
         }
@@ -484,18 +484,18 @@ export const getChannelsColumns = ({
           return (
             <Space wrap>
               <SplitButtonGroup
-                className="overflow-hidden"
+                className='overflow-hidden'
                 aria-label={t('测试单个渠道操作项目组')}
               >
                 <Button
-                  size="small"
+                  size='small'
                   type='tertiary'
                   onClick={() => testChannel(record, '')}
                 >
                   {t('测试')}
                 </Button>
                 <Button
-                  size="small"
+                  size='small'
                   type='tertiary'
                   icon={<IconTreeTriangleDown />}
                   onClick={() => {
@@ -505,32 +505,28 @@ export const getChannelsColumns = ({
                 />
               </SplitButtonGroup>
 
-              {
-                record.status === 1 ? (
-                  <Button
-                    type='danger'
-                    size="small"
-                    onClick={() => manageChannel(record.id, 'disable', record)}
-                  >
-                    {t('禁用')}
-                  </Button>
-                ) : (
-                  <Button
-                    size="small"
-                    onClick={() => manageChannel(record.id, 'enable', record)}
-                  >
-                    {t('启用')}
-                  </Button>
-                )
-              }
+              {record.status === 1 ? (
+                <Button
+                  type='danger'
+                  size='small'
+                  onClick={() => manageChannel(record.id, 'disable', record)}
+                >
+                  {t('禁用')}
+                </Button>
+              ) : (
+                <Button
+                  size='small'
+                  onClick={() => manageChannel(record.id, 'enable', record)}
+                >
+                  {t('启用')}
+                </Button>
+              )}
 
               {record.channel_info?.is_multi_key ? (
-                <SplitButtonGroup
-                  aria-label={t('多密钥渠道操作项目组')}
-                >
+                <SplitButtonGroup aria-label={t('多密钥渠道操作项目组')}>
                   <Button
                     type='tertiary'
-                    size="small"
+                    size='small'
                     onClick={() => {
                       setEditingChannel(record);
                       setShowEdit(true);
@@ -549,12 +545,12 @@ export const getChannelsColumns = ({
                           setCurrentMultiKeyChannel(record);
                           setShowMultiKeyManageModal(true);
                         },
-                      }
+                      },
                     ]}
                   >
                     <Button
                       type='tertiary'
-                      size="small"
+                      size='small'
                       icon={<IconTreeTriangleDown />}
                     />
                   </Dropdown>
@@ -562,7 +558,7 @@ export const getChannelsColumns = ({
               ) : (
                 <Button
                   type='tertiary'
-                  size="small"
+                  size='small'
                   onClick={() => {
                     setEditingChannel(record);
                     setShowEdit(true);
@@ -577,11 +573,7 @@ export const getChannelsColumns = ({
                 position='bottomRight'
                 menu={moreMenuItems}
               >
-                <Button
-                  icon={<IconMore />}
-                  type='tertiary'
-                  size="small"
-                />
+                <Button icon={<IconMore />} type='tertiary' size='small' />
               </Dropdown>
             </Space>
           );
@@ -591,21 +583,21 @@ export const getChannelsColumns = ({
             <Space wrap>
               <Button
                 type='tertiary'
-                size="small"
+                size='small'
                 onClick={() => manageTag(record.key, 'enable')}
               >
                 {t('启用全部')}
               </Button>
               <Button
                 type='tertiary'
-                size="small"
+                size='small'
                 onClick={() => manageTag(record.key, 'disable')}
               >
                 {t('禁用全部')}
               </Button>
               <Button
                 type='tertiary'
-                size="small"
+                size='small'
                 onClick={() => {
                   setShowEditTag(true);
                   setEditingTag(record.key);
@@ -619,4 +611,4 @@ export const getChannelsColumns = ({
       },
     },
   ];
-}; 
+};

+ 23 - 23
web/src/components/table/channels/ChannelsFilters.jsx

@@ -34,16 +34,16 @@ const ChannelsFilters = ({
   groupOptions,
   loading,
   searching,
-  t
+  t,
 }) => {
   return (
-    <div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
-      <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+    <div className='flex flex-col md:flex-row justify-between items-center gap-2 w-full'>
+      <div className='flex gap-2 w-full md:w-auto order-2 md:order-1'>
         <Button
           size='small'
           theme='light'
           type='primary'
-          className="w-full md:w-auto"
+          className='w-full md:w-auto'
           onClick={() => {
             setEditingChannel({
               id: undefined,
@@ -57,7 +57,7 @@ const ChannelsFilters = ({
         <Button
           size='small'
           type='tertiary'
-          className="w-full md:w-auto"
+          className='w-full md:w-auto'
           onClick={refresh}
         >
           {t('刷新')}
@@ -67,54 +67,54 @@ const ChannelsFilters = ({
           size='small'
           type='tertiary'
           onClick={() => setShowColumnSelector(true)}
-          className="w-full md:w-auto"
+          className='w-full md:w-auto'
         >
           {t('列设置')}
         </Button>
       </div>
 
-      <div className="flex flex-col md:flex-row items-center gap-2 w-full md:w-auto order-1 md:order-2">
+      <div className='flex flex-col md:flex-row items-center gap-2 w-full md:w-auto order-1 md:order-2'>
         <Form
           initValues={formInitValues}
           getFormApi={(api) => setFormApi(api)}
           onSubmit={() => searchChannels(enableTagMode)}
           allowEmpty={true}
-          autoComplete="off"
-          layout="horizontal"
-          trigger="change"
+          autoComplete='off'
+          layout='horizontal'
+          trigger='change'
           stopValidateWithError={false}
-          className="flex flex-col md:flex-row items-center gap-2 w-full"
+          className='flex flex-col md:flex-row items-center gap-2 w-full'
         >
-          <div className="relative w-full md:w-64">
+          <div className='relative w-full md:w-64'>
             <Form.Input
               size='small'
-              field="searchKeyword"
+              field='searchKeyword'
               prefix={<IconSearch />}
               placeholder={t('渠道ID,名称,密钥,API地址')}
               showClear
               pure
             />
           </div>
-          <div className="w-full md:w-48">
+          <div className='w-full md:w-48'>
             <Form.Input
               size='small'
-              field="searchModel"
+              field='searchModel'
               prefix={<IconSearch />}
               placeholder={t('模型关键字')}
               showClear
               pure
             />
           </div>
-          <div className="w-full md:w-32">
+          <div className='w-full md:w-32'>
             <Form.Select
               size='small'
-              field="searchGroup"
+              field='searchGroup'
               placeholder={t('选择分组')}
               optionList={[
                 { label: t('选择分组'), value: null },
                 ...groupOptions,
               ]}
-              className="w-full"
+              className='w-full'
               showClear
               pure
               onChange={() => {
@@ -127,10 +127,10 @@ const ChannelsFilters = ({
           </div>
           <Button
             size='small'
-            type="tertiary"
-            htmlType="submit"
+            type='tertiary'
+            htmlType='submit'
             loading={loading || searching}
-            className="w-full md:w-auto"
+            className='w-full md:w-auto'
           >
             {t('查询')}
           </Button>
@@ -146,7 +146,7 @@ const ChannelsFilters = ({
                 }, 100);
               }
             }}
-            className="w-full md:w-auto"
+            className='w-full md:w-auto'
           >
             {t('重置')}
           </Button>
@@ -156,4 +156,4 @@ const ChannelsFilters = ({
   );
 };
 
-export default ChannelsFilters; 
+export default ChannelsFilters;

+ 11 - 9
web/src/components/table/channels/ChannelsTable.jsx

@@ -22,7 +22,7 @@ import { Empty } from '@douyinfe/semi-ui';
 import CardTable from '../../common/ui/CardTable';
 import {
   IllustrationNoResult,
-  IllustrationNoResultDark
+  IllustrationNoResultDark,
 } from '@douyinfe/semi-illustrations';
 import { getChannelsColumns } from './ChannelsColumnDefs';
 
@@ -142,25 +142,27 @@ const ChannelsTable = (channelsData) => {
       rowSelection={
         enableBatchDelete
           ? {
-            onChange: (selectedRowKeys, selectedRows) => {
-              setSelectedChannels(selectedRows);
-            },
-          }
+              onChange: (selectedRowKeys, selectedRows) => {
+                setSelectedChannels(selectedRows);
+              },
+            }
           : null
       }
       empty={
         <Empty
           image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
-          darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+          darkModeImage={
+            <IllustrationNoResultDark style={{ width: 150, height: 150 }} />
+          }
           description={t('搜索无结果')}
           style={{ padding: 30 }}
         />
       }
-      className="rounded-xl overflow-hidden"
-      size="middle"
+      className='rounded-xl overflow-hidden'
+      size='middle'
       loading={loading || searching}
     />
   );
 };
 
-export default ChannelsTable; 
+export default ChannelsTable;

+ 18 - 10
web/src/components/table/channels/ChannelsTabs.jsx

@@ -33,7 +33,7 @@ const ChannelsTabs = ({
   pageSize,
   idSort,
   setActivePage,
-  t
+  t,
 }) => {
   if (enableTagMode) return null;
 
@@ -46,24 +46,29 @@ const ChannelsTabs = ({
   return (
     <Tabs
       activeKey={activeTypeKey}
-      type="card"
+      type='card'
       collapsible
       onChange={handleTabChange}
-      className="mb-2"
+      className='mb-2'
     >
       <TabPane
-        itemKey="all"
+        itemKey='all'
         tab={
-          <span className="flex items-center gap-2">
+          <span className='flex items-center gap-2'>
             {t('全部')}
-            <Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} shape='circle'>
+            <Tag
+              color={activeTypeKey === 'all' ? 'red' : 'grey'}
+              shape='circle'
+            >
               {channelTypeCounts['all'] || 0}
             </Tag>
           </span>
         }
       />
 
-      {CHANNEL_OPTIONS.filter((opt) => availableTypeKeys.includes(String(opt.value))).map((option) => {
+      {CHANNEL_OPTIONS.filter((opt) =>
+        availableTypeKeys.includes(String(opt.value)),
+      ).map((option) => {
         const key = String(option.value);
         const count = channelTypeCounts[option.value] || 0;
         return (
@@ -71,10 +76,13 @@ const ChannelsTabs = ({
             key={key}
             itemKey={key}
             tab={
-              <span className="flex items-center gap-2">
+              <span className='flex items-center gap-2'>
                 {getChannelIcon(option.value)}
                 {option.label}
-                <Tag color={activeTypeKey === key ? 'red' : 'grey'} shape='circle'>
+                <Tag
+                  color={activeTypeKey === key ? 'red' : 'grey'}
+                  shape='circle'
+                >
                   {count}
                 </Tag>
               </span>
@@ -86,4 +94,4 @@ const ChannelsTabs = ({
   );
 };
 
-export default ChannelsTabs; 
+export default ChannelsTabs;

+ 2 - 2
web/src/components/table/channels/index.jsx

@@ -64,7 +64,7 @@ const ChannelsPage = () => {
 
       {/* Main Content */}
       <CardPro
-        type="type3"
+        type='type3'
         tabsArea={<ChannelsTabs {...channelsData} />}
         actionsArea={<ChannelsActions {...channelsData} />}
         searchArea={<ChannelsFilters {...channelsData} />}
@@ -85,4 +85,4 @@ const ChannelsPage = () => {
   );
 };
 
-export default ChannelsPage; 
+export default ChannelsPage;

+ 10 - 7
web/src/components/table/channels/modals/BatchTagModal.jsx

@@ -27,7 +27,7 @@ const BatchTagModal = ({
   batchSetTagValue,
   setBatchSetTagValue,
   selectedChannels,
-  t
+  t,
 }) => {
   return (
     <Modal
@@ -37,10 +37,10 @@ const BatchTagModal = ({
       onCancel={() => setShowBatchSetTag(false)}
       maskClosable={false}
       centered={true}
-      size="small"
-      className="!rounded-lg"
+      size='small'
+      className='!rounded-lg'
     >
-      <div className="mb-5">
+      <div className='mb-5'>
         <Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
       </div>
       <Input
@@ -48,13 +48,16 @@ const BatchTagModal = ({
         value={batchSetTagValue}
         onChange={(v) => setBatchSetTagValue(v)}
       />
-      <div className="mt-4">
+      <div className='mt-4'>
         <Typography.Text type='secondary'>
-          {t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
+          {t('已选择 ${count} 个渠道').replace(
+            '${count}',
+            selectedChannels.length,
+          )}
         </Typography.Text>
       </div>
     </Modal>
   );
 };
 
-export default BatchTagModal; 
+export default BatchTagModal;

+ 5 - 10
web/src/components/table/channels/modals/ColumnSelectorModal.jsx

@@ -74,10 +74,8 @@ const ColumnSelectorModal = ({
       visible={showColumnSelector}
       onCancel={() => setShowColumnSelector(false)}
       footer={
-        <div className="flex justify-end">
-          <Button onClick={() => initDefaultColumns()}>
-            {t('重置')}
-          </Button>
+        <div className='flex justify-end'>
+          <Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
           <Button onClick={() => setShowColumnSelector(false)}>
             {t('取消')}
           </Button>
@@ -100,7 +98,7 @@ const ColumnSelectorModal = ({
         </Checkbox>
       </div>
       <div
-        className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
+        className='flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4'
         style={{ border: '1px solid var(--semi-color-border)' }}
       >
         {allColumns.map((column) => {
@@ -110,10 +108,7 @@ const ColumnSelectorModal = ({
           }
 
           return (
-            <div
-              key={column.key}
-              className="w-1/2 mb-4 pr-2"
-            >
+            <div key={column.key} className='w-1/2 mb-4 pr-2'>
               <Checkbox
                 checked={!!visibleColumns[column.key]}
                 onChange={(e) =>
@@ -130,4 +125,4 @@ const ColumnSelectorModal = ({
   );
 };
 
-export default ColumnSelectorModal; 
+export default ColumnSelectorModal;

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 369 - 163
web/src/components/table/channels/modals/EditChannelModal.jsx


+ 94 - 41
web/src/components/table/channels/modals/EditTagModal.jsx

@@ -289,7 +289,7 @@ const EditTagModal = (props) => {
         t('已新增 {{count}} 个模型:{{list}}', {
           count: addedModels.length,
           list: addedModels.join(', '),
-        })
+        }),
       );
     } else {
       showInfo(t('未发现新增模型'));
@@ -301,8 +301,10 @@ const EditTagModal = (props) => {
       placement='right'
       title={
         <Space>
-          <Tag color="blue" shape="circle">{t('编辑')}</Tag>
-          <Title heading={4} className="m-0">
+          <Tag color='blue' shape='circle'>
+            {t('编辑')}
+          </Tag>
+          <Title heading={4} className='m-0'>
             {t('编辑标签')}
           </Title>
         </Space>
@@ -312,10 +314,10 @@ const EditTagModal = (props) => {
       width={600}
       onCancel={handleClose}
       footer={
-        <div className="flex justify-end bg-white">
+        <div className='flex justify-end bg-white'>
           <Space>
             <Button
-              theme="solid"
+              theme='solid'
               onClick={() => formApiRef.current?.submitForm()}
               loading={loading}
               icon={<IconSave />}
@@ -323,8 +325,8 @@ const EditTagModal = (props) => {
               {t('保存')}
             </Button>
             <Button
-              theme="light"
-              type="primary"
+              theme='light'
+              type='primary'
               onClick={handleClose}
               icon={<IconClose />}
             >
@@ -343,26 +345,28 @@ const EditTagModal = (props) => {
       >
         {() => (
           <Spin spinning={loading}>
-            <div className="p-2">
-              <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+            <div className='p-2'>
+              <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
                 {/* Header: Tag Info */}
-                <div className="flex items-center mb-2">
-                  <Avatar size="small" color="blue" className="mr-2 shadow-md">
+                <div className='flex items-center mb-2'>
+                  <Avatar size='small' color='blue' className='mr-2 shadow-md'>
                     <IconBookmark size={16} />
                   </Avatar>
                   <div>
-                    <Text className="text-lg font-medium">{t('标签信息')}</Text>
-                    <div className="text-xs text-gray-600">{t('标签的基本配置')}</div>
+                    <Text className='text-lg font-medium'>{t('标签信息')}</Text>
+                    <div className='text-xs text-gray-600'>
+                      {t('标签的基本配置')}
+                    </div>
                   </div>
                 </div>
 
                 <Banner
-                  type="warning"
+                  type='warning'
                   description={t('所有编辑均为覆盖操作,留空则不更改')}
-                  className="!rounded-lg mb-4"
+                  className='!rounded-lg mb-4'
                 />
 
-                <div className="space-y-4">
+                <div className='space-y-4'>
                   <Form.Input
                     field='new_tag'
                     label={t('标签名称')}
@@ -372,23 +376,31 @@ const EditTagModal = (props) => {
                 </div>
               </Card>
 
-              <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+              <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
                 {/* Header: Model Config */}
-                <div className="flex items-center mb-2">
-                  <Avatar size="small" color="purple" className="mr-2 shadow-md">
+                <div className='flex items-center mb-2'>
+                  <Avatar
+                    size='small'
+                    color='purple'
+                    className='mr-2 shadow-md'
+                  >
                     <IconCode size={16} />
                   </Avatar>
                   <div>
-                    <Text className="text-lg font-medium">{t('模型配置')}</Text>
-                    <div className="text-xs text-gray-600">{t('模型选择和映射设置')}</div>
+                    <Text className='text-lg font-medium'>{t('模型配置')}</Text>
+                    <div className='text-xs text-gray-600'>
+                      {t('模型选择和映射设置')}
+                    </div>
                   </div>
                 </div>
 
-                <div className="space-y-4">
+                <div className='space-y-4'>
                   <Banner
-                    type="info"
-                    description={t('当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。')}
-                    className="!rounded-lg mb-4"
+                    type='info'
+                    description={t(
+                      '当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。',
+                    )}
+                    className='!rounded-lg mb-4'
                   />
                   <Form.Select
                     field='models'
@@ -408,46 +420,87 @@ const EditTagModal = (props) => {
                     label={t('自定义模型名称')}
                     placeholder={t('输入自定义模型名称')}
                     onChange={(value) => setCustomModel(value.trim())}
-                    suffix={<Button size='small' type='primary' onClick={addCustomModels}>{t('填入')}</Button>}
+                    suffix={
+                      <Button
+                        size='small'
+                        type='primary'
+                        onClick={addCustomModels}
+                      >
+                        {t('填入')}
+                      </Button>
+                    }
                   />
 
                   <Form.TextArea
                     field='model_mapping'
                     label={t('模型重定向')}
-                    placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改')}
+                    placeholder={t(
+                      '此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改',
+                    )}
                     autosize
-                    onChange={(value) => handleInputChange('model_mapping', value)}
-                    extraText={(
+                    onChange={(value) =>
+                      handleInputChange('model_mapping', value)
+                    }
+                    extraText={
                       <Space>
-                        <Text className="!text-semi-color-primary cursor-pointer" onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}>{t('填入模板')}</Text>
-                        <Text className="!text-semi-color-primary cursor-pointer" onClick={() => handleInputChange('model_mapping', JSON.stringify({}, null, 2))}>{t('清空重定向')}</Text>
-                        <Text className="!text-semi-color-primary cursor-pointer" onClick={() => handleInputChange('model_mapping', '')}>{t('不更改')}</Text>
+                        <Text
+                          className='!text-semi-color-primary cursor-pointer'
+                          onClick={() =>
+                            handleInputChange(
+                              'model_mapping',
+                              JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
+                            )
+                          }
+                        >
+                          {t('填入模板')}
+                        </Text>
+                        <Text
+                          className='!text-semi-color-primary cursor-pointer'
+                          onClick={() =>
+                            handleInputChange(
+                              'model_mapping',
+                              JSON.stringify({}, null, 2),
+                            )
+                          }
+                        >
+                          {t('清空重定向')}
+                        </Text>
+                        <Text
+                          className='!text-semi-color-primary cursor-pointer'
+                          onClick={() => handleInputChange('model_mapping', '')}
+                        >
+                          {t('不更改')}
+                        </Text>
                       </Space>
-                    )}
+                    }
                   />
                 </div>
               </Card>
 
-              <Card className="!rounded-2xl shadow-sm border-0">
+              <Card className='!rounded-2xl shadow-sm border-0'>
                 {/* Header: Group Settings */}
-                <div className="flex items-center mb-2">
-                  <Avatar size="small" color="green" className="mr-2 shadow-md">
+                <div className='flex items-center mb-2'>
+                  <Avatar size='small' color='green' className='mr-2 shadow-md'>
                     <IconUser size={16} />
                   </Avatar>
                   <div>
-                    <Text className="text-lg font-medium">{t('分组设置')}</Text>
-                    <div className="text-xs text-gray-600">{t('用户分组配置')}</div>
+                    <Text className='text-lg font-medium'>{t('分组设置')}</Text>
+                    <div className='text-xs text-gray-600'>
+                      {t('用户分组配置')}
+                    </div>
                   </div>
                 </div>
 
-                <div className="space-y-4">
+                <div className='space-y-4'>
                   <Form.Select
                     field='groups'
                     label={t('分组')}
                     placeholder={t('请选择可以使用该渠道的分组,留空则不更改')}
                     multiple
                     allowAdditions
-                    additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
+                    additionLabel={t(
+                      '请在系统设置页面编辑分组倍率以添加新的分组:',
+                    )}
                     optionList={groupOptions}
                     style={{ width: '100%' }}
                     onChange={(value) => handleInputChange('groups', value)}
@@ -462,4 +515,4 @@ const EditTagModal = (props) => {
   );
 };
 
-export default EditTagModal; 
+export default EditTagModal;

+ 108 - 49
web/src/components/table/channels/modals/ModelSelectModal.jsx

@@ -19,16 +19,31 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React, { useState, useEffect } from 'react';
 import { useIsMobile } from '../../../../hooks/common/useIsMobile';
-import { Modal, Checkbox, Spin, Input, Typography, Empty, Tabs, Collapse } from '@douyinfe/semi-ui';
+import {
+  Modal,
+  Checkbox,
+  Spin,
+  Input,
+  Typography,
+  Empty,
+  Tabs,
+  Collapse,
+} from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
-  IllustrationNoResultDark
+  IllustrationNoResultDark,
 } from '@douyinfe/semi-illustrations';
 import { IconSearch } from '@douyinfe/semi-icons';
 import { useTranslation } from 'react-i18next';
 import { getModelCategories } from '../../../../helpers/render';
 
-const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCancel }) => {
+const ModelSelectModal = ({
+  visible,
+  models = [],
+  selected = [],
+  onConfirm,
+  onCancel,
+}) => {
   const { t } = useTranslation();
   const [checkedList, setCheckedList] = useState(selected);
   const [keyword, setKeyword] = useState('');
@@ -36,11 +51,15 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
 
   const isMobile = useIsMobile();
 
-  const filteredModels = models.filter((m) => m.toLowerCase().includes(keyword.toLowerCase()));
+  const filteredModels = models.filter((m) =>
+    m.toLowerCase().includes(keyword.toLowerCase()),
+  );
 
   // 分类模型:新获取的模型和已有模型
-  const newModels = filteredModels.filter(model => !selected.includes(model));
-  const existingModels = filteredModels.filter(model => selected.includes(model));
+  const newModels = filteredModels.filter((model) => !selected.includes(model));
+  const existingModels = filteredModels.filter((model) =>
+    selected.includes(model),
+  );
 
   // 同步外部选中值
   useEffect(() => {
@@ -68,7 +87,7 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
     const categorizedModels = {};
     const uncategorizedModels = [];
 
-    models.forEach(model => {
+    models.forEach((model) => {
       let foundCategory = false;
       for (const [key, category] of Object.entries(categories)) {
         if (key !== 'all' && category.filter({ model_name: model })) {
@@ -76,7 +95,7 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
             categorizedModels[key] = {
               label: category.label,
               icon: category.icon,
-              models: []
+              models: [],
             };
           }
           categorizedModels[key].models.push(model);
@@ -94,7 +113,7 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
       categorizedModels['other'] = {
         label: t('其他'),
         icon: null,
-        models: uncategorizedModels
+        models: uncategorizedModels,
       };
     }
 
@@ -106,14 +125,22 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
 
   // Tab列表配置
   const tabList = [
-    ...(newModels.length > 0 ? [{
-      tab: `${t('新获取的模型')} (${newModels.length})`,
-      itemKey: 'new'
-    }] : []),
-    ...(existingModels.length > 0 ? [{
-      tab: `${t('已有的模型')} (${existingModels.length})`,
-      itemKey: 'existing'
-    }] : [])
+    ...(newModels.length > 0
+      ? [
+          {
+            tab: `${t('新获取的模型')} (${newModels.length})`,
+            itemKey: 'new',
+          },
+        ]
+      : []),
+    ...(existingModels.length > 0
+      ? [
+          {
+            tab: `${t('已有的模型')} (${existingModels.length})`,
+            itemKey: 'existing',
+          },
+        ]
+      : []),
   ];
 
   // 处理分类全选/取消全选
@@ -122,14 +149,16 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
 
     if (isChecked) {
       // 全选:添加该分类下所有未选中的模型
-      categoryModels.forEach(model => {
+      categoryModels.forEach((model) => {
         if (!newCheckedList.includes(model)) {
           newCheckedList.push(model);
         }
       });
     } else {
       // 取消全选:移除该分类下所有已选中的模型
-      newCheckedList = newCheckedList.filter(model => !categoryModels.includes(model));
+      newCheckedList = newCheckedList.filter(
+        (model) => !categoryModels.includes(model),
+      );
     }
 
     setCheckedList(newCheckedList);
@@ -137,12 +166,17 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
 
   // 检查分类是否全选
   const isCategoryAllSelected = (categoryModels) => {
-    return categoryModels.length > 0 && categoryModels.every(model => checkedList.includes(model));
+    return (
+      categoryModels.length > 0 &&
+      categoryModels.every((model) => checkedList.includes(model))
+    );
   };
 
   // 检查分类是否部分选中
   const isCategoryIndeterminate = (categoryModels) => {
-    const selectedCount = categoryModels.filter(model => checkedList.includes(model)).length;
+    const selectedCount = categoryModels.filter((model) =>
+      checkedList.includes(model),
+    ).length;
     return selectedCount > 0 && selectedCount < categoryModels.length;
   };
 
@@ -151,10 +185,15 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
     if (categoryEntries.length === 0) return null;
 
     // 生成所有面板的key,确保都展开
-    const allActiveKeys = categoryEntries.map((_, index) => `${categoryKeyPrefix}_${index}`);
+    const allActiveKeys = categoryEntries.map(
+      (_, index) => `${categoryKeyPrefix}_${index}`,
+    );
 
     return (
-      <Collapse key={`${categoryKeyPrefix}_${categoryEntries.length}`} defaultActiveKey={[]}>
+      <Collapse
+        key={`${categoryKeyPrefix}_${categoryEntries.length}`}
+        defaultActiveKey={[]}
+      >
         {categoryEntries.map(([key, categoryData], index) => (
           <Collapse.Panel
             key={`${categoryKeyPrefix}_${index}`}
@@ -166,24 +205,29 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
                 indeterminate={isCategoryIndeterminate(categoryData.models)}
                 onChange={(e) => {
                   e.stopPropagation(); // 防止触发面板折叠
-                  handleCategorySelectAll(categoryData.models, e.target.checked);
+                  handleCategorySelectAll(
+                    categoryData.models,
+                    e.target.checked,
+                  );
                 }}
                 onClick={(e) => e.stopPropagation()} // 防止点击checkbox时折叠面板
               />
             }
           >
-            <div className="flex items-center gap-2 mb-3">
+            <div className='flex items-center gap-2 mb-3'>
               {categoryData.icon}
-              <Typography.Text type="secondary" size="small">
+              <Typography.Text type='secondary' size='small'>
                 {t('已选择 {{selected}} / {{total}}', {
-                  selected: categoryData.models.filter(model => checkedList.includes(model)).length,
-                  total: categoryData.models.length
+                  selected: categoryData.models.filter((model) =>
+                    checkedList.includes(model),
+                  ).length,
+                  total: categoryData.models.length,
                 })}
               </Typography.Text>
             </div>
-            <div className="grid grid-cols-2 gap-x-4">
+            <div className='grid grid-cols-2 gap-x-4'>
               {categoryData.models.map((model) => (
-                <Checkbox key={model} value={model} className="my-1">
+                <Checkbox key={model} value={model} className='my-1'>
                   {model}
                 </Checkbox>
               ))}
@@ -197,14 +241,14 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
   return (
     <Modal
       header={
-        <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4">
-          <Typography.Title heading={5} className="m-0">
+        <div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4'>
+          <Typography.Title heading={5} className='m-0'>
             {t('选择模型')}
           </Typography.Title>
-          <div className="flex-shrink-0">
+          <div className='flex-shrink-0'>
             <Tabs
-              type="slash"
-              size="small"
+              type='slash'
+              size='small'
               tabList={tabList}
               activeKey={activeTab}
               onChange={(key) => setActiveTab(key)}
@@ -234,17 +278,22 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
         <div style={{ maxHeight: 400, overflowY: 'auto', paddingRight: 8 }}>
           {filteredModels.length === 0 ? (
             <Empty
-              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
-              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              image={
+                <IllustrationNoResult style={{ width: 150, height: 150 }} />
+              }
+              darkModeImage={
+                <IllustrationNoResultDark style={{ width: 150, height: 150 }} />
+              }
               description={t('暂无匹配模型')}
               style={{ padding: 30 }}
             />
           ) : (
-            <Checkbox.Group value={checkedList} onChange={(vals) => setCheckedList(vals)}>
+            <Checkbox.Group
+              value={checkedList}
+              onChange={(vals) => setCheckedList(vals)}
+            >
               {activeTab === 'new' && newModels.length > 0 && (
-                <div>
-                  {renderModelsByCategory(newModelsByCategory, 'new')}
-                </div>
+                <div>{renderModelsByCategory(newModelsByCategory, 'new')}</div>
               )}
               {activeTab === 'existing' && existingModels.length > 0 && (
                 <div>
@@ -256,20 +305,30 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
         </div>
       </Spin>
 
-      <Typography.Text type="secondary" size="small" className="block text-right mt-4">
-        <div className="flex items-center justify-end gap-2">
+      <Typography.Text
+        type='secondary'
+        size='small'
+        className='block text-right mt-4'
+      >
+        <div className='flex items-center justify-end gap-2'>
           {(() => {
-            const currentModels = activeTab === 'new' ? newModels : existingModels;
-            const currentSelected = currentModels.filter(model => checkedList.includes(model)).length;
-            const isAllSelected = currentModels.length > 0 && currentSelected === currentModels.length;
-            const isIndeterminate = currentSelected > 0 && currentSelected < currentModels.length;
+            const currentModels =
+              activeTab === 'new' ? newModels : existingModels;
+            const currentSelected = currentModels.filter((model) =>
+              checkedList.includes(model),
+            ).length;
+            const isAllSelected =
+              currentModels.length > 0 &&
+              currentSelected === currentModels.length;
+            const isIndeterminate =
+              currentSelected > 0 && currentSelected < currentModels.length;
 
             return (
               <>
                 <span>
                   {t('已选择 {{selected}} / {{total}}', {
                     selected: currentSelected,
-                    total: currentModels.length
+                    total: currentModels.length,
                   })}
                 </span>
                 <Checkbox
@@ -288,4 +347,4 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
   );
 };
 
-export default ModelSelectModal; 
+export default ModelSelectModal;

+ 116 - 109
web/src/components/table/channels/modals/ModelTestModal.jsx

@@ -24,7 +24,7 @@ import {
   Input,
   Table,
   Tag,
-  Typography
+  Typography,
 } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
 import { copy, showError, showInfo, showSuccess } from '../../../../helpers';
@@ -47,16 +47,16 @@ const ModelTestModal = ({
   setModelTablePage,
   allSelectingRef,
   isMobile,
-  t
+  t,
 }) => {
   const hasChannel = Boolean(currentTestChannel);
 
   const filteredModels = hasChannel
     ? currentTestChannel.models
-      .split(',')
-      .filter((model) =>
-        model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
-      )
+        .split(',')
+        .filter((model) =>
+          model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
+        )
     : [];
 
   const handleCopySelected = () => {
@@ -66,7 +66,12 @@ const ModelTestModal = ({
     }
     copy(selectedModelKeys.join(',')).then((ok) => {
       if (ok) {
-        showSuccess(t('已复制 ${count} 个模型').replace('${count}', selectedModelKeys.length));
+        showSuccess(
+          t('已复制 ${count} 个模型').replace(
+            '${count}',
+            selectedModelKeys.length,
+          ),
+        );
       } else {
         showError(t('复制失败,请手动复制'));
       }
@@ -93,16 +98,17 @@ const ModelTestModal = ({
       title: t('模型名称'),
       dataIndex: 'model',
       render: (text) => (
-        <div className="flex items-center">
+        <div className='flex items-center'>
           <Typography.Text strong>{text}</Typography.Text>
         </div>
-      )
+      ),
     },
     {
       title: t('状态'),
       dataIndex: 'status',
       render: (text, record) => {
-        const testResult = modelTestResults[`${currentTestChannel.id}-${record.model}`];
+        const testResult =
+          modelTestResults[`${currentTestChannel.id}-${record.model}`];
         const isTesting = testingModels.has(record.model);
 
         if (isTesting) {
@@ -122,21 +128,21 @@ const ModelTestModal = ({
         }
 
         return (
-          <div className="flex items-center gap-2">
-            <Tag
-              color={testResult.success ? 'green' : 'red'}
-              shape='circle'
-            >
+          <div className='flex items-center gap-2'>
+            <Tag color={testResult.success ? 'green' : 'red'} shape='circle'>
               {testResult.success ? t('成功') : t('失败')}
             </Tag>
             {testResult.success && (
-              <Typography.Text type="tertiary">
-                {t('请求时长: ${time}s').replace('${time}', testResult.time.toFixed(2))}
+              <Typography.Text type='tertiary'>
+                {t('请求时长: ${time}s').replace(
+                  '${time}',
+                  testResult.time.toFixed(2),
+                )}
               </Typography.Text>
             )}
           </div>
         );
-      }
+      },
     },
     {
       title: '',
@@ -153,8 +159,8 @@ const ModelTestModal = ({
             {t('测试')}
           </Button>
         );
-      }
-    }
+      },
+    },
   ];
 
   const dataSource = (() => {
@@ -169,108 +175,109 @@ const ModelTestModal = ({
 
   return (
     <Modal
-      title={hasChannel ? (
-        <div className="flex flex-col gap-2 w-full">
-          <div className="flex items-center gap-2">
-            <Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
-              {currentTestChannel.name} {t('渠道的模型测试')}
-            </Typography.Text>
-            <Typography.Text type="tertiary" size="small">
-              {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
-            </Typography.Text>
+      title={
+        hasChannel ? (
+          <div className='flex flex-col gap-2 w-full'>
+            <div className='flex items-center gap-2'>
+              <Typography.Text
+                strong
+                className='!text-[var(--semi-color-text-0)] !text-base'
+              >
+                {currentTestChannel.name} {t('渠道的模型测试')}
+              </Typography.Text>
+              <Typography.Text type='tertiary' size='small'>
+                {t('共')} {currentTestChannel.models.split(',').length}{' '}
+                {t('个模型')}
+              </Typography.Text>
+            </div>
           </div>
-        </div>
-      ) : null}
+        ) : null
+      }
       visible={showModelTestModal}
       onCancel={handleCloseModal}
-      footer={hasChannel ? (
-        <div className="flex justify-end">
-          {isBatchTesting ? (
-            <Button
-              type='danger'
-              onClick={handleCloseModal}
-            >
-              {t('停止测试')}
-            </Button>
-          ) : (
+      footer={
+        hasChannel ? (
+          <div className='flex justify-end'>
+            {isBatchTesting ? (
+              <Button type='danger' onClick={handleCloseModal}>
+                {t('停止测试')}
+              </Button>
+            ) : (
+              <Button type='tertiary' onClick={handleCloseModal}>
+                {t('取消')}
+              </Button>
+            )}
             <Button
-              type='tertiary'
-              onClick={handleCloseModal}
+              onClick={batchTestModels}
+              loading={isBatchTesting}
+              disabled={isBatchTesting}
             >
-              {t('取消')}
+              {isBatchTesting
+                ? t('测试中...')
+                : t('批量测试${count}个模型').replace(
+                    '${count}',
+                    filteredModels.length,
+                  )}
             </Button>
-          )}
-          <Button
-            onClick={batchTestModels}
-            loading={isBatchTesting}
-            disabled={isBatchTesting}
-          >
-            {isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
-              '${count}',
-              filteredModels.length
-            )}
-          </Button>
-        </div>
-      ) : null}
+          </div>
+        ) : null
+      }
       maskClosable={!isBatchTesting}
-      className="!rounded-lg"
+      className='!rounded-lg'
       size={isMobile ? 'full-width' : 'large'}
     >
-      {hasChannel && (<div className="model-test-scroll">
-        {/* 搜索与操作按钮 */}
-        <div className="flex items-center justify-end gap-2 w-full mb-2">
-          <Input
-            placeholder={t('搜索模型...')}
-            value={modelSearchKeyword}
-            onChange={(v) => {
-              setModelSearchKeyword(v);
-              setModelTablePage(1);
-            }}
-            className="!w-full"
-            prefix={<IconSearch />}
-            showClear
-          />
+      {hasChannel && (
+        <div className='model-test-scroll'>
+          {/* 搜索与操作按钮 */}
+          <div className='flex items-center justify-end gap-2 w-full mb-2'>
+            <Input
+              placeholder={t('搜索模型...')}
+              value={modelSearchKeyword}
+              onChange={(v) => {
+                setModelSearchKeyword(v);
+                setModelTablePage(1);
+              }}
+              className='!w-full'
+              prefix={<IconSearch />}
+              showClear
+            />
 
-          <Button onClick={handleCopySelected}>
-            {t('复制已选')}
-          </Button>
+            <Button onClick={handleCopySelected}>{t('复制已选')}</Button>
 
-          <Button
-            type='tertiary'
-            onClick={handleSelectSuccess}
-          >
-            {t('选择成功')}
-          </Button>
-        </div>
+            <Button type='tertiary' onClick={handleSelectSuccess}>
+              {t('选择成功')}
+            </Button>
+          </div>
 
-        <Table
-          columns={columns}
-          dataSource={dataSource}
-          rowSelection={{
-            selectedRowKeys: selectedModelKeys,
-            onChange: (keys) => {
-              if (allSelectingRef.current) {
-                allSelectingRef.current = false;
-                return;
-              }
-              setSelectedModelKeys(keys);
-            },
-            onSelectAll: (checked) => {
-              allSelectingRef.current = true;
-              setSelectedModelKeys(checked ? filteredModels : []);
-            },
-          }}
-          pagination={{
-            currentPage: modelTablePage,
-            pageSize: MODEL_TABLE_PAGE_SIZE,
-            total: filteredModels.length,
-            showSizeChanger: false,
-            onPageChange: (page) => setModelTablePage(page),
-          }}
-        />
-      </div>)}
+          <Table
+            columns={columns}
+            dataSource={dataSource}
+            rowSelection={{
+              selectedRowKeys: selectedModelKeys,
+              onChange: (keys) => {
+                if (allSelectingRef.current) {
+                  allSelectingRef.current = false;
+                  return;
+                }
+                setSelectedModelKeys(keys);
+              },
+              onSelectAll: (checked) => {
+                allSelectingRef.current = true;
+                setSelectedModelKeys(checked ? filteredModels : []);
+              },
+            }}
+            pagination={{
+              currentPage: modelTablePage,
+              pageSize: MODEL_TABLE_PAGE_SIZE,
+              total: filteredModels.length,
+              showSizeChanger: false,
+              onPageChange: (page) => setModelTablePage(page),
+            }}
+          />
+        </div>
+      )}
     </Modal>
   );
 };
 
-export default ModelTestModal; 
+export default ModelTestModal;

+ 185 - 73
web/src/components/table/channels/modals/MultiKeyManageModal.jsx

@@ -35,19 +35,22 @@ import {
   Col,
   Badge,
   Progress,
-  Card
+  Card,
 } from '@douyinfe/semi-ui';
-import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
-import { API, showError, showSuccess, timestamp2string } from '../../../../helpers';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark,
+} from '@douyinfe/semi-illustrations';
+import {
+  API,
+  showError,
+  showSuccess,
+  timestamp2string,
+} from '../../../../helpers';
 
 const { Text } = Typography;
 
-const MultiKeyManageModal = ({
-  visible,
-  onCancel,
-  channel,
-  onRefresh
-}) => {
+const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
   const { t } = useTranslation();
   const [loading, setLoading] = useState(false);
   const [keyStatusList, setKeyStatusList] = useState([]);
@@ -68,7 +71,11 @@ const MultiKeyManageModal = ({
   const [statusFilter, setStatusFilter] = useState(null); // null=all, 1=enabled, 2=manual_disabled, 3=auto_disabled
 
   // Load key status data
-  const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
+  const loadKeyStatus = async (
+    page = currentPage,
+    size = pageSize,
+    status = statusFilter,
+  ) => {
     if (!channel?.id) return;
 
     setLoading(true);
@@ -77,7 +84,7 @@ const MultiKeyManageModal = ({
         channel_id: channel.id,
         action: 'get_key_status',
         page: page,
-        page_size: size
+        page_size: size,
       };
 
       // Add status filter if specified
@@ -113,13 +120,13 @@ const MultiKeyManageModal = ({
   // Disable a specific key
   const handleDisableKey = async (keyIndex) => {
     const operationId = `disable_${keyIndex}`;
-    setOperationLoading(prev => ({ ...prev, [operationId]: true }));
+    setOperationLoading((prev) => ({ ...prev, [operationId]: true }));
 
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         action: 'disable_key',
-        key_index: keyIndex
+        key_index: keyIndex,
       });
 
       if (res.data.success) {
@@ -132,20 +139,20 @@ const MultiKeyManageModal = ({
     } catch (error) {
       showError(t('禁用密钥失败'));
     } finally {
-      setOperationLoading(prev => ({ ...prev, [operationId]: false }));
+      setOperationLoading((prev) => ({ ...prev, [operationId]: false }));
     }
   };
 
   // Enable a specific key
   const handleEnableKey = async (keyIndex) => {
     const operationId = `enable_${keyIndex}`;
-    setOperationLoading(prev => ({ ...prev, [operationId]: true }));
+    setOperationLoading((prev) => ({ ...prev, [operationId]: true }));
 
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         action: 'enable_key',
-        key_index: keyIndex
+        key_index: keyIndex,
       });
 
       if (res.data.success) {
@@ -158,18 +165,18 @@ const MultiKeyManageModal = ({
     } catch (error) {
       showError(t('启用密钥失败'));
     } finally {
-      setOperationLoading(prev => ({ ...prev, [operationId]: false }));
+      setOperationLoading((prev) => ({ ...prev, [operationId]: false }));
     }
   };
 
   // Enable all disabled keys
   const handleEnableAll = async () => {
-    setOperationLoading(prev => ({ ...prev, enable_all: true }));
+    setOperationLoading((prev) => ({ ...prev, enable_all: true }));
 
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
-        action: 'enable_all_keys'
+        action: 'enable_all_keys',
       });
 
       if (res.data.success) {
@@ -184,18 +191,18 @@ const MultiKeyManageModal = ({
     } catch (error) {
       showError(t('启用所有密钥失败'));
     } finally {
-      setOperationLoading(prev => ({ ...prev, enable_all: false }));
+      setOperationLoading((prev) => ({ ...prev, enable_all: false }));
     }
   };
 
   // Disable all enabled keys
   const handleDisableAll = async () => {
-    setOperationLoading(prev => ({ ...prev, disable_all: true }));
+    setOperationLoading((prev) => ({ ...prev, disable_all: true }));
 
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
-        action: 'disable_all_keys'
+        action: 'disable_all_keys',
       });
 
       if (res.data.success) {
@@ -210,18 +217,18 @@ const MultiKeyManageModal = ({
     } catch (error) {
       showError(t('禁用所有密钥失败'));
     } finally {
-      setOperationLoading(prev => ({ ...prev, disable_all: false }));
+      setOperationLoading((prev) => ({ ...prev, disable_all: false }));
     }
   };
 
   // Delete all disabled keys
   const handleDeleteDisabledKeys = async () => {
-    setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
+    setOperationLoading((prev) => ({ ...prev, delete_disabled: true }));
 
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
-        action: 'delete_disabled_keys'
+        action: 'delete_disabled_keys',
       });
 
       if (res.data.success) {
@@ -236,7 +243,7 @@ const MultiKeyManageModal = ({
     } catch (error) {
       showError(t('删除禁用密钥失败'));
     } finally {
-      setOperationLoading(prev => ({ ...prev, delete_disabled: false }));
+      setOperationLoading((prev) => ({ ...prev, delete_disabled: false }));
     }
   };
 
@@ -246,7 +253,7 @@ const MultiKeyManageModal = ({
     loadKeyStatus(page, pageSize);
   };
 
-  // Handle page size change  
+  // Handle page size change
   const handlePageSizeChange = (size) => {
     setPageSize(size);
     setCurrentPage(1); // Reset to first page
@@ -283,9 +290,12 @@ const MultiKeyManageModal = ({
   }, [visible]);
 
   // Percentages for progress display
-  const enabledPercent = total > 0 ? Math.round((enabledCount / total) * 100) : 0;
-  const manualDisabledPercent = total > 0 ? Math.round((manualDisabledCount / total) * 100) : 0;
-  const autoDisabledPercent = total > 0 ? Math.round((autoDisabledCount / total) * 100) : 0;
+  const enabledPercent =
+    total > 0 ? Math.round((enabledCount / total) * 100) : 0;
+  const manualDisabledPercent =
+    total > 0 ? Math.round((manualDisabledCount / total) * 100) : 0;
+  const autoDisabledPercent =
+    total > 0 ? Math.round((autoDisabledCount / total) * 100) : 0;
 
   // 取消饼图:不再需要图表数据与配置
 
@@ -293,13 +303,29 @@ const MultiKeyManageModal = ({
   const renderStatusTag = (status) => {
     switch (status) {
       case 1:
-        return <Tag color='green' shape='circle' size='small'>{t('已启用')}</Tag>;
+        return (
+          <Tag color='green' shape='circle' size='small'>
+            {t('已启用')}
+          </Tag>
+        );
       case 2:
-        return <Tag color='red' shape='circle' size='small'>{t('已禁用')}</Tag>;
+        return (
+          <Tag color='red' shape='circle' size='small'>
+            {t('已禁用')}
+          </Tag>
+        );
       case 3:
-        return <Tag color='orange' shape='circle' size='small'>{t('自动禁用')}</Tag>;
+        return (
+          <Tag color='orange' shape='circle' size='small'>
+            {t('自动禁用')}
+          </Tag>
+        );
       default:
-        return <Tag color='grey' shape='circle' size='small'>{t('未知状态')}</Tag>;
+        return (
+          <Tag color='grey' shape='circle' size='small'>
+            {t('未知状态')}
+          </Tag>
+        );
     }
   };
 
@@ -349,9 +375,7 @@ const MultiKeyManageModal = ({
         }
         return (
           <Tooltip content={timestamp2string(time)}>
-            <Text style={{ fontSize: '12px' }}>
-              {timestamp2string(time)}
-            </Text>
+            <Text style={{ fontSize: '12px' }}>{timestamp2string(time)}</Text>
           </Tooltip>
         );
       },
@@ -393,14 +417,18 @@ const MultiKeyManageModal = ({
         <Space>
           <Text>{t('多密钥管理')}</Text>
           {channel?.name && (
-            <Tag size='small' shape='circle' color='white'>{channel.name}</Tag>
+            <Tag size='small' shape='circle' color='white'>
+              {channel.name}
+            </Tag>
           )}
           <Tag size='small' shape='circle' color='white'>
             {t('总密钥数')}: {total}
           </Tag>
           {channel?.channel_info?.multi_key_mode && (
             <Tag size='small' shape='circle' color='white'>
-              {channel.channel_info.multi_key_mode === 'random' ? t('随机模式') : t('轮询模式')}
+              {channel.channel_info.multi_key_mode === 'random'
+                ? t('随机模式')
+                : t('轮询模式')}
             </Tag>
           )}
         </Space>
@@ -410,60 +438,123 @@ const MultiKeyManageModal = ({
       width={900}
       footer={null}
     >
-      <div className="flex flex-col mb-5">
+      <div className='flex flex-col mb-5'>
         {/* Stats & Mode */}
         <div
-          className="rounded-xl p-4 mb-3"
+          className='rounded-xl p-4 mb-3'
           style={{
             background: 'var(--semi-color-bg-1)',
-            border: '1px solid var(--semi-color-border)'
+            border: '1px solid var(--semi-color-border)',
           }}
         >
-          <Row gutter={16} align="middle">
+          <Row gutter={16} align='middle'>
             <Col span={8}>
-              <div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
-                <div className="flex items-center gap-2 mb-2">
+              <div
+                style={{
+                  background: 'var(--semi-color-bg-0)',
+                  border: '1px solid var(--semi-color-border)',
+                  borderRadius: 12,
+                  padding: 12,
+                }}
+              >
+                <div className='flex items-center gap-2 mb-2'>
                   <Badge dot type='success' />
                   <Text type='tertiary'>{t('已启用')}</Text>
                 </div>
-                <div className="flex items-end gap-2 mb-2">
-                  <Text style={{ fontSize: 18, fontWeight: 700, color: '#22c55e' }}>{enabledCount}</Text>
-                  <Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
+                <div className='flex items-end gap-2 mb-2'>
+                  <Text
+                    style={{ fontSize: 18, fontWeight: 700, color: '#22c55e' }}
+                  >
+                    {enabledCount}
+                  </Text>
+                  <Text
+                    style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}
+                  >
+                    / {total}
+                  </Text>
                 </div>
-                <Progress percent={enabledPercent} showInfo={false} size="small" stroke="#22c55e" style={{ height: 6, borderRadius: 999 }} />
+                <Progress
+                  percent={enabledPercent}
+                  showInfo={false}
+                  size='small'
+                  stroke='#22c55e'
+                  style={{ height: 6, borderRadius: 999 }}
+                />
               </div>
             </Col>
             <Col span={8}>
-              <div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
-                <div className="flex items-center gap-2 mb-2">
+              <div
+                style={{
+                  background: 'var(--semi-color-bg-0)',
+                  border: '1px solid var(--semi-color-border)',
+                  borderRadius: 12,
+                  padding: 12,
+                }}
+              >
+                <div className='flex items-center gap-2 mb-2'>
                   <Badge dot type='danger' />
                   <Text type='tertiary'>{t('手动禁用')}</Text>
                 </div>
-                <div className="flex items-end gap-2 mb-2">
-                  <Text style={{ fontSize: 18, fontWeight: 700, color: '#ef4444' }}>{manualDisabledCount}</Text>
-                  <Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
+                <div className='flex items-end gap-2 mb-2'>
+                  <Text
+                    style={{ fontSize: 18, fontWeight: 700, color: '#ef4444' }}
+                  >
+                    {manualDisabledCount}
+                  </Text>
+                  <Text
+                    style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}
+                  >
+                    / {total}
+                  </Text>
                 </div>
-                <Progress percent={manualDisabledPercent} showInfo={false} size="small" stroke="#ef4444" style={{ height: 6, borderRadius: 999 }} />
+                <Progress
+                  percent={manualDisabledPercent}
+                  showInfo={false}
+                  size='small'
+                  stroke='#ef4444'
+                  style={{ height: 6, borderRadius: 999 }}
+                />
               </div>
             </Col>
             <Col span={8}>
-              <div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
-                <div className="flex items-center gap-2 mb-2">
+              <div
+                style={{
+                  background: 'var(--semi-color-bg-0)',
+                  border: '1px solid var(--semi-color-border)',
+                  borderRadius: 12,
+                  padding: 12,
+                }}
+              >
+                <div className='flex items-center gap-2 mb-2'>
                   <Badge dot type='warning' />
                   <Text type='tertiary'>{t('自动禁用')}</Text>
                 </div>
-                <div className="flex items-end gap-2 mb-2">
-                  <Text style={{ fontSize: 18, fontWeight: 700, color: '#f59e0b' }}>{autoDisabledCount}</Text>
-                  <Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
+                <div className='flex items-end gap-2 mb-2'>
+                  <Text
+                    style={{ fontSize: 18, fontWeight: 700, color: '#f59e0b' }}
+                  >
+                    {autoDisabledCount}
+                  </Text>
+                  <Text
+                    style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}
+                  >
+                    / {total}
+                  </Text>
                 </div>
-                <Progress percent={autoDisabledPercent} showInfo={false} size="small" stroke="#f59e0b" style={{ height: 6, borderRadius: 999 }} />
+                <Progress
+                  percent={autoDisabledPercent}
+                  showInfo={false}
+                  size='small'
+                  stroke='#f59e0b'
+                  style={{ height: 6, borderRadius: 999 }}
+                />
               </div>
             </Col>
           </Row>
         </div>
 
         {/* Table */}
-        <div className="flex-1 flex flex-col min-h-0">
+        <div className='flex-1 flex flex-col min-h-0'>
           <Spin spinning={loading}>
             <Card className='!rounded-xl'>
               <Table
@@ -478,15 +569,26 @@ const MultiKeyManageModal = ({
                             size='small'
                             placeholder={t('全部状态')}
                           >
-                            <Select.Option value={null}>{t('全部状态')}</Select.Option>
-                            <Select.Option value={1}>{t('已启用')}</Select.Option>
-                            <Select.Option value={2}>{t('手动禁用')}</Select.Option>
-                            <Select.Option value={3}>{t('自动禁用')}</Select.Option>
+                            <Select.Option value={null}>
+                              {t('全部状态')}
+                            </Select.Option>
+                            <Select.Option value={1}>
+                              {t('已启用')}
+                            </Select.Option>
+                            <Select.Option value={2}>
+                              {t('手动禁用')}
+                            </Select.Option>
+                            <Select.Option value={3}>
+                              {t('自动禁用')}
+                            </Select.Option>
                           </Select>
                         </Col>
                       </Row>
                     </Col>
-                    <Col span={10} style={{ display: 'flex', justifyContent: 'flex-end' }}>
+                    <Col
+                      span={10}
+                      style={{ display: 'flex', justifyContent: 'flex-end' }}
+                    >
                       <Space>
                         <Button
                           size='small'
@@ -496,7 +598,7 @@ const MultiKeyManageModal = ({
                         >
                           {t('刷新')}
                         </Button>
-                        {(manualDisabledCount + autoDisabledCount) > 0 && (
+                        {manualDisabledCount + autoDisabledCount > 0 && (
                           <Popconfirm
                             title={t('确定要启用所有密钥吗?')}
                             onConfirm={handleEnableAll}
@@ -529,7 +631,9 @@ const MultiKeyManageModal = ({
                         )}
                         <Popconfirm
                           title={t('确定要删除所有已自动禁用的密钥吗?')}
-                          content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
+                          content={t(
+                            '此操作不可撤销,将永久删除已自动禁用的密钥',
+                          )}
                           onConfirm={handleDeleteDisabledKeys}
                           okType={'danger'}
                           position={'topRight'}
@@ -562,7 +666,7 @@ const MultiKeyManageModal = ({
                   onShowSizeChange: (current, size) => {
                     setCurrentPage(1);
                     handlePageSizeChange(size);
-                  }
+                  },
                 }}
                 size='small'
                 bordered={false}
@@ -570,8 +674,16 @@ const MultiKeyManageModal = ({
                 scroll={{ x: 'max-content' }}
                 empty={
                   <Empty
-                    image={<IllustrationNoResult style={{ width: 140, height: 140 }} />}
-                    darkModeImage={<IllustrationNoResultDark style={{ width: 140, height: 140 }} />}
+                    image={
+                      <IllustrationNoResult
+                        style={{ width: 140, height: 140 }}
+                      />
+                    }
+                    darkModeImage={
+                      <IllustrationNoResultDark
+                        style={{ width: 140, height: 140 }}
+                      />
+                    }
                     title={t('暂无密钥数据')}
                     description={t('请检查渠道配置或刷新重试')}
                     style={{ padding: 30 }}
@@ -586,4 +698,4 @@ const MultiKeyManageModal = ({
   );
 };
 
-export default MultiKeyManageModal; 
+export default MultiKeyManageModal;

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä