🎨 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
This commit is contained in:
+16
-8
@@ -1,13 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: { browser: true, es2021: true, node: 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'],
|
plugins: ['header', 'react-hooks'],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx}'],
|
files: ['**/*.{js,jsx}'],
|
||||||
rules: {
|
rules: {
|
||||||
'header/header': [2, 'block', [
|
'header/header': [
|
||||||
|
2,
|
||||||
|
'block',
|
||||||
|
[
|
||||||
'',
|
'',
|
||||||
'Copyright (C) 2025 QuantumNous',
|
'Copyright (C) 2025 QuantumNous',
|
||||||
'',
|
'',
|
||||||
@@ -25,10 +32,11 @@ module.exports = {
|
|||||||
'along with this program. If not, see <https://www.gnu.org/licenses/>.',
|
'along with this program. If not, see <https://www.gnu.org/licenses/>.',
|
||||||
'',
|
'',
|
||||||
'For commercial licensing, please contact support@quantumnous.com',
|
'For commercial licensing, please contact support@quantumnous.com',
|
||||||
''
|
'',
|
||||||
]],
|
],
|
||||||
'no-multiple-empty-lines': ['error', { max: 1 }]
|
],
|
||||||
}
|
'no-multiple-empty-lines': ['error', { max: 1 }],
|
||||||
}
|
},
|
||||||
]
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
+4
-3
@@ -1,12 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/logo.png" />
|
<link rel="icon" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<meta name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
||||||
|
/>
|
||||||
<title>New API</title>
|
<title>New API</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -15,5 +17,4 @@
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/index.jsx"></script>
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -22,4 +22,4 @@ export default {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
+1
-4
@@ -73,10 +73,7 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path='/forbidden' element={<Forbidden />} />
|
||||||
path='/forbidden'
|
|
||||||
element={<Forbidden />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path='/console/models'
|
path='/console/models'
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -31,17 +31,10 @@ import {
|
|||||||
setUserData,
|
setUserData,
|
||||||
onGitHubOAuthClicked,
|
onGitHubOAuthClicked,
|
||||||
onOIDCClicked,
|
onOIDCClicked,
|
||||||
onLinuxDOOAuthClicked
|
onLinuxDOOAuthClicked,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import {
|
import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Form,
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||||
import TelegramLoginButton from 'react-telegram-login';
|
import TelegramLoginButton from 'react-telegram-login';
|
||||||
@@ -77,7 +70,8 @@ const LoginForm = () => {
|
|||||||
const [emailLoginLoading, setEmailLoginLoading] = useState(false);
|
const [emailLoginLoading, setEmailLoginLoading] = useState(false);
|
||||||
const [loginLoading, setLoginLoading] = useState(false);
|
const [loginLoading, setLoginLoading] = useState(false);
|
||||||
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
||||||
const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = useState(false);
|
const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] =
|
||||||
|
useState(false);
|
||||||
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
|
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
|
||||||
const [showTwoFA, setShowTwoFA] = useState(false);
|
const [showTwoFA, setShowTwoFA] = useState(false);
|
||||||
|
|
||||||
@@ -247,10 +241,7 @@ const LoginForm = () => {
|
|||||||
const handleOIDCClick = () => {
|
const handleOIDCClick = () => {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
onOIDCClicked(
|
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
|
||||||
status.oidc_authorization_endpoint,
|
|
||||||
status.oidc_client_id
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// 由于重定向,这里不会执行到,但为了完整性添加
|
// 由于重定向,这里不会执行到,但为了完整性添加
|
||||||
setTimeout(() => setOidcLoading(false), 3000);
|
setTimeout(() => setOidcLoading(false), 3000);
|
||||||
@@ -306,73 +297,87 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
const renderOAuthOptions = () => {
|
const renderOAuthOptions = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<div className="w-full max-w-md">
|
<div className='w-full max-w-md'>
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
<div className='flex items-center justify-center mb-6 gap-2'>
|
||||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
<img src={logo} alt='Logo' className='h-10 rounded-full' />
|
||||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
<Title heading={3} className='!text-gray-800'>
|
||||||
|
{systemName}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-0 !rounded-2xl overflow-hidden">
|
<Card className='border-0 !rounded-2xl overflow-hidden'>
|
||||||
<div className="flex justify-center pt-6 pb-2">
|
<div className='flex justify-center pt-6 pb-2'>
|
||||||
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('登 录')}</Title>
|
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
|
||||||
|
{t('登 录')}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-8">
|
<div className='px-2 py-8'>
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
{status.wechat_login && (
|
{status.wechat_login && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
|
icon={
|
||||||
|
<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />
|
||||||
|
}
|
||||||
onClick={onWeChatLoginClicked}
|
onClick={onWeChatLoginClicked}
|
||||||
loading={wechatLoading}
|
loading={wechatLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 微信 继续')}</span>
|
<span className='ml-3'>{t('使用 微信 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.github_oauth && (
|
{status.github_oauth && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<IconGithubLogo size="large" />}
|
icon={<IconGithubLogo size='large' />}
|
||||||
onClick={handleGitHubClick}
|
onClick={handleGitHubClick}
|
||||||
loading={githubLoading}
|
loading={githubLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 GitHub 继续')}</span>
|
<span className='ml-3'>{t('使用 GitHub 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.oidc_enabled && (
|
{status.oidc_enabled && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<OIDCIcon style={{ color: '#1877F2' }} />}
|
icon={<OIDCIcon style={{ color: '#1877F2' }} />}
|
||||||
onClick={handleOIDCClick}
|
onClick={handleOIDCClick}
|
||||||
loading={oidcLoading}
|
loading={oidcLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 OIDC 继续')}</span>
|
<span className='ml-3'>{t('使用 OIDC 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.linuxdo_oauth && (
|
{status.linuxdo_oauth && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
|
icon={
|
||||||
|
<LinuxDoIcon
|
||||||
|
style={{
|
||||||
|
color: '#E95420',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={handleLinuxDOClick}
|
onClick={handleLinuxDOClick}
|
||||||
loading={linuxdoLoading}
|
loading={linuxdoLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 LinuxDO 继续')}</span>
|
<span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.telegram_oauth && (
|
{status.telegram_oauth && (
|
||||||
<div className="flex justify-center my-2">
|
<div className='flex justify-center my-2'>
|
||||||
<TelegramLoginButton
|
<TelegramLoginButton
|
||||||
dataOnauth={onTelegramLoginClicked}
|
dataOnauth={onTelegramLoginClicked}
|
||||||
botName={status.telegram_bot_name}
|
botName={status.telegram_bot_name}
|
||||||
@@ -385,24 +390,24 @@ const LoginForm = () => {
|
|||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
type="primary"
|
type='primary'
|
||||||
className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
|
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" />}
|
icon={<IconMail size='large' />}
|
||||||
onClick={handleEmailLoginClick}
|
onClick={handleEmailLoginClick}
|
||||||
loading={emailLoginLoading}
|
loading={emailLoginLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 邮箱或用户名 登录')}</span>
|
<span className='ml-3'>{t('使用 邮箱或用户名 登录')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!status.self_use_mode_enabled && (
|
{!status.self_use_mode_enabled && (
|
||||||
<div className="mt-6 text-center text-sm">
|
<div className='mt-6 text-center text-sm'>
|
||||||
<Text>
|
<Text>
|
||||||
{t('没有账户?')}{' '}
|
{t('没有账户?')}{' '}
|
||||||
<Link
|
<Link
|
||||||
to="/register"
|
to='/register'
|
||||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
className='text-blue-600 hover:text-blue-800 font-medium'
|
||||||
>
|
>
|
||||||
{t('注册')}
|
{t('注册')}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -418,44 +423,46 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
const renderEmailLoginForm = () => {
|
const renderEmailLoginForm = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<div className="w-full max-w-md">
|
<div className='w-full max-w-md'>
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
<div className='flex items-center justify-center mb-6 gap-2'>
|
||||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
<img src={logo} alt='Logo' className='h-10 rounded-full' />
|
||||||
<Title heading={3}>{systemName}</Title>
|
<Title heading={3}>{systemName}</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-0 !rounded-2xl overflow-hidden">
|
<Card className='border-0 !rounded-2xl overflow-hidden'>
|
||||||
<div className="flex justify-center pt-6 pb-2">
|
<div className='flex justify-center pt-6 pb-2'>
|
||||||
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('登 录')}</Title>
|
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
|
||||||
|
{t('登 录')}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-8">
|
<div className='px-2 py-8'>
|
||||||
<Form className="space-y-3">
|
<Form className='space-y-3'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="username"
|
field='username'
|
||||||
label={t('用户名或邮箱')}
|
label={t('用户名或邮箱')}
|
||||||
placeholder={t('请输入您的用户名或邮箱地址')}
|
placeholder={t('请输入您的用户名或邮箱地址')}
|
||||||
name="username"
|
name='username'
|
||||||
onChange={(value) => handleChange('username', value)}
|
onChange={(value) => handleChange('username', value)}
|
||||||
prefix={<IconMail />}
|
prefix={<IconMail />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="password"
|
field='password'
|
||||||
label={t('密码')}
|
label={t('密码')}
|
||||||
placeholder={t('请输入您的密码')}
|
placeholder={t('请输入您的密码')}
|
||||||
name="password"
|
name='password'
|
||||||
mode="password"
|
mode='password'
|
||||||
onChange={(value) => handleChange('password', value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
<div className='space-y-2 pt-2'>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
type="primary"
|
type='primary'
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loginLoading}
|
loading={loginLoading}
|
||||||
>
|
>
|
||||||
@@ -463,9 +470,9 @@ const LoginForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
onClick={handleResetPasswordClick}
|
onClick={handleResetPasswordClick}
|
||||||
loading={resetPasswordLoading}
|
loading={resetPasswordLoading}
|
||||||
>
|
>
|
||||||
@@ -474,17 +481,21 @@ const LoginForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Form>
|
</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'>
|
<Divider margin='12px' align='center'>
|
||||||
{t('或')}
|
{t('或')}
|
||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
<div className='mt-4 text-center'>
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme='outline'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
onClick={handleOtherLoginOptionsClick}
|
onClick={handleOtherLoginOptionsClick}
|
||||||
loading={otherLoginOptionsLoading}
|
loading={otherLoginOptionsLoading}
|
||||||
>
|
>
|
||||||
@@ -495,12 +506,12 @@ const LoginForm = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!status.self_use_mode_enabled && (
|
{!status.self_use_mode_enabled && (
|
||||||
<div className="mt-6 text-center text-sm">
|
<div className='mt-6 text-center text-sm'>
|
||||||
<Text>
|
<Text>
|
||||||
{t('没有账户?')}{' '}
|
{t('没有账户?')}{' '}
|
||||||
<Link
|
<Link
|
||||||
to="/register"
|
to='/register'
|
||||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
className='text-blue-600 hover:text-blue-800 font-medium'
|
||||||
>
|
>
|
||||||
{t('注册')}
|
{t('注册')}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -529,21 +540,25 @@ const LoginForm = () => {
|
|||||||
loading: wechatCodeSubmitLoading,
|
loading: wechatCodeSubmitLoading,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
|
<img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mb-4">
|
<div className='text-center mb-4'>
|
||||||
<p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
|
<p>
|
||||||
|
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="wechat_verification_code"
|
field='wechat_verification_code'
|
||||||
placeholder={t('验证码')}
|
placeholder={t('验证码')}
|
||||||
label={t('验证码')}
|
label={t('验证码')}
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={(value) => handleChange('wechat_verification_code', value)}
|
onChange={(value) =>
|
||||||
|
handleChange('wechat_verification_code', value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -555,10 +570,18 @@ const LoginForm = () => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<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">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
两步验证
|
两步验证
|
||||||
@@ -580,19 +603,32 @@ const LoginForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
className='blur-ball blur-ball-indigo'
|
||||||
<div className="w-full max-w-sm mt-[60px]">
|
style={{ top: '-80px', right: '-80px', transform: 'none' }}
|
||||||
{showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
/>
|
||||||
|
<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()
|
? renderEmailLoginForm()
|
||||||
: renderOAuthOptions()}
|
: renderOAuthOptions()}
|
||||||
{renderWeChatLoginModal()}
|
{renderWeChatLoginModal()}
|
||||||
{render2FAModal()}
|
{render2FAModal()}
|
||||||
|
|
||||||
{turnstileEnabled && (
|
{turnstileEnabled && (
|
||||||
<div className="flex justify-center mt-6">
|
<div className='flex justify-center mt-6'>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={turnstileSiteKey}
|
sitekey={turnstileSiteKey}
|
||||||
onVerify={(token) => {
|
onVerify={(token) => {
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { UserContext } from '../../context/User';
|
||||||
import Loading from '../common/ui/Loading';
|
import Loading from '../common/ui/Loading';
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
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 { useSearchParams, Link } from 'react-router-dom';
|
||||||
import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
|
import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
|
||||||
import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
|
import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
|
||||||
@@ -55,7 +62,7 @@ const PasswordResetConfirm = () => {
|
|||||||
if (formApi) {
|
if (formApi) {
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
email: email || '',
|
email: email || '',
|
||||||
newPassword: newPassword || ''
|
newPassword: newPassword || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [searchParams, newPassword, formApi]);
|
}, [searchParams, newPassword, formApi]);
|
||||||
@@ -97,40 +104,53 @@ const PasswordResetConfirm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
className='blur-ball blur-ball-indigo'
|
||||||
<div className="w-full max-w-sm mt-[60px]">
|
style={{ top: '-80px', right: '-80px', transform: 'none' }}
|
||||||
<div className="flex flex-col items-center">
|
/>
|
||||||
<div className="w-full max-w-md">
|
<div
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
className='blur-ball blur-ball-teal'
|
||||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
style={{ top: '50%', left: '-120px' }}
|
||||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
/>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<Card className="border-0 !rounded-2xl overflow-hidden">
|
<Card className='border-0 !rounded-2xl overflow-hidden'>
|
||||||
<div className="flex justify-center pt-6 pb-2">
|
<div className='flex justify-center pt-6 pb-2'>
|
||||||
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置确认')}</Title>
|
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
|
||||||
|
{t('密码重置确认')}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-8">
|
<div className='px-2 py-8'>
|
||||||
{!isValidResetLink && (
|
{!isValidResetLink && (
|
||||||
<Banner
|
<Banner
|
||||||
type="danger"
|
type='danger'
|
||||||
description={t('无效的重置链接,请重新发起密码重置请求')}
|
description={t('无效的重置链接,请重新发起密码重置请求')}
|
||||||
className="mb-4 !rounded-lg"
|
className='mb-4 !rounded-lg'
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Form
|
<Form
|
||||||
getFormApi={(api) => setFormApi(api)}
|
getFormApi={(api) => setFormApi(api)}
|
||||||
initValues={{ email: email || '', newPassword: newPassword || '' }}
|
initValues={{
|
||||||
className="space-y-4"
|
email: email || '',
|
||||||
|
newPassword: newPassword || '',
|
||||||
|
}}
|
||||||
|
className='space-y-4'
|
||||||
>
|
>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="email"
|
field='email'
|
||||||
label={t('邮箱')}
|
label={t('邮箱')}
|
||||||
name="email"
|
name='email'
|
||||||
disabled={true}
|
disabled={true}
|
||||||
prefix={<IconMail />}
|
prefix={<IconMail />}
|
||||||
placeholder={email ? '' : t('等待获取邮箱信息...')}
|
placeholder={email ? '' : t('等待获取邮箱信息...')}
|
||||||
@@ -138,19 +158,21 @@ const PasswordResetConfirm = () => {
|
|||||||
|
|
||||||
{newPassword && (
|
{newPassword && (
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="newPassword"
|
field='newPassword'
|
||||||
label={t('新密码')}
|
label={t('新密码')}
|
||||||
name="newPassword"
|
name='newPassword'
|
||||||
disabled={true}
|
disabled={true}
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
suffix={
|
suffix={
|
||||||
<Button
|
<Button
|
||||||
icon={<IconCopy />}
|
icon={<IconCopy />}
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await copy(newPassword);
|
await copy(newPassword);
|
||||||
showNotice(`${t('密码已复制到剪贴板:')} ${newPassword}`);
|
showNotice(
|
||||||
|
`${t('密码已复制到剪贴板:')} ${newPassword}`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('复制')}
|
{t('复制')}
|
||||||
@@ -159,23 +181,32 @@ const PasswordResetConfirm = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
<div className='space-y-2 pt-2'>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
type="primary"
|
type='primary'
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disableButton || newPassword || !isValidResetLink}
|
disabled={
|
||||||
|
disableButton || newPassword || !isValidResetLink
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{newPassword ? t('密码重置完成') : t('确认重置密码')}
|
{newPassword ? t('密码重置完成') : t('确认重置密码')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm">
|
<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>
|
<Text>
|
||||||
|
<Link
|
||||||
|
to='/login'
|
||||||
|
className='text-blue-600 hover:text-blue-800 font-medium'
|
||||||
|
>
|
||||||
|
{t('返回登录')}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
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 Turnstile from 'react-turnstile';
|
||||||
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
||||||
import { IconMail } from '@douyinfe/semi-icons';
|
import { IconMail } from '@douyinfe/semi-icons';
|
||||||
@@ -97,57 +104,77 @@ const PasswordResetForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
className='blur-ball blur-ball-indigo'
|
||||||
<div className="w-full max-w-sm mt-[60px]">
|
style={{ top: '-80px', right: '-80px', transform: 'none' }}
|
||||||
<div className="flex flex-col items-center">
|
/>
|
||||||
<div className="w-full max-w-md">
|
<div
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
className='blur-ball blur-ball-teal'
|
||||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
style={{ top: '50%', left: '-120px' }}
|
||||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
/>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<Card className="border-0 !rounded-2xl overflow-hidden">
|
<Card className='border-0 !rounded-2xl overflow-hidden'>
|
||||||
<div className="flex justify-center pt-6 pb-2">
|
<div className='flex justify-center pt-6 pb-2'>
|
||||||
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置')}</Title>
|
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
|
||||||
|
{t('密码重置')}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-8">
|
<div className='px-2 py-8'>
|
||||||
<Form className="space-y-3">
|
<Form className='space-y-3'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="email"
|
field='email'
|
||||||
label={t('邮箱')}
|
label={t('邮箱')}
|
||||||
placeholder={t('请输入您的邮箱地址')}
|
placeholder={t('请输入您的邮箱地址')}
|
||||||
name="email"
|
name='email'
|
||||||
value={email}
|
value={email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
prefix={<IconMail />}
|
prefix={<IconMail />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
<div className='space-y-2 pt-2'>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
type="primary"
|
type='primary'
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disableButton}
|
disabled={disableButton}
|
||||||
>
|
>
|
||||||
{disableButton ? `${t('重试')} (${countdown})` : t('提交')}
|
{disableButton
|
||||||
|
? `${t('重试')} (${countdown})`
|
||||||
|
: t('提交')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm">
|
<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>
|
<Text>
|
||||||
|
{t('想起来了?')}{' '}
|
||||||
|
<Link
|
||||||
|
to='/login'
|
||||||
|
className='text-blue-600 hover:text-blue-800 font-medium'
|
||||||
|
>
|
||||||
|
{t('登录')}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{turnstileEnabled && (
|
{turnstileEnabled && (
|
||||||
<div className="flex justify-center mt-6">
|
<div className='flex justify-center mt-6'>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={turnstileSiteKey}
|
sitekey={turnstileSiteKey}
|
||||||
onVerify={(token) => {
|
onVerify={(token) => {
|
||||||
|
|||||||
@@ -27,20 +27,19 @@ import {
|
|||||||
showSuccess,
|
showSuccess,
|
||||||
updateAPI,
|
updateAPI,
|
||||||
getSystemName,
|
getSystemName,
|
||||||
setUserData
|
setUserData,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import {
|
import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Form,
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
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 {
|
import {
|
||||||
onGitHubOAuthClicked,
|
onGitHubOAuthClicked,
|
||||||
onLinuxDOOAuthClicked,
|
onLinuxDOOAuthClicked,
|
||||||
@@ -78,7 +77,8 @@ const RegisterForm = () => {
|
|||||||
const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
|
const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
|
||||||
const [registerLoading, setRegisterLoading] = useState(false);
|
const [registerLoading, setRegisterLoading] = useState(false);
|
||||||
const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
|
const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
|
||||||
const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
|
const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] =
|
||||||
|
useState(false);
|
||||||
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
|
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
|
||||||
const [disableButton, setDisableButton] = useState(false);
|
const [disableButton, setDisableButton] = useState(false);
|
||||||
const [countdown, setCountdown] = useState(30);
|
const [countdown, setCountdown] = useState(30);
|
||||||
@@ -236,10 +236,7 @@ const RegisterForm = () => {
|
|||||||
const handleOIDCClick = () => {
|
const handleOIDCClick = () => {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
onOIDCClicked(
|
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
|
||||||
status.oidc_authorization_endpoint,
|
|
||||||
status.oidc_client_id
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => setOidcLoading(false), 3000);
|
setTimeout(() => setOidcLoading(false), 3000);
|
||||||
}
|
}
|
||||||
@@ -303,73 +300,87 @@ const RegisterForm = () => {
|
|||||||
|
|
||||||
const renderOAuthOptions = () => {
|
const renderOAuthOptions = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<div className="w-full max-w-md">
|
<div className='w-full max-w-md'>
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
<div className='flex items-center justify-center mb-6 gap-2'>
|
||||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
<img src={logo} alt='Logo' className='h-10 rounded-full' />
|
||||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
<Title heading={3} className='!text-gray-800'>
|
||||||
|
{systemName}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-0 !rounded-2xl overflow-hidden">
|
<Card className='border-0 !rounded-2xl overflow-hidden'>
|
||||||
<div className="flex justify-center pt-6 pb-2">
|
<div className='flex justify-center pt-6 pb-2'>
|
||||||
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
|
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
|
||||||
|
{t('注 册')}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-8">
|
<div className='px-2 py-8'>
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
{status.wechat_login && (
|
{status.wechat_login && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
|
icon={
|
||||||
|
<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />
|
||||||
|
}
|
||||||
onClick={onWeChatLoginClicked}
|
onClick={onWeChatLoginClicked}
|
||||||
loading={wechatLoading}
|
loading={wechatLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 微信 继续')}</span>
|
<span className='ml-3'>{t('使用 微信 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.github_oauth && (
|
{status.github_oauth && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<IconGithubLogo size="large" />}
|
icon={<IconGithubLogo size='large' />}
|
||||||
onClick={handleGitHubClick}
|
onClick={handleGitHubClick}
|
||||||
loading={githubLoading}
|
loading={githubLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 GitHub 继续')}</span>
|
<span className='ml-3'>{t('使用 GitHub 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.oidc_enabled && (
|
{status.oidc_enabled && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<OIDCIcon style={{ color: '#1877F2' }} />}
|
icon={<OIDCIcon style={{ color: '#1877F2' }} />}
|
||||||
onClick={handleOIDCClick}
|
onClick={handleOIDCClick}
|
||||||
loading={oidcLoading}
|
loading={oidcLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 OIDC 继续')}</span>
|
<span className='ml-3'>{t('使用 OIDC 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.linuxdo_oauth && (
|
{status.linuxdo_oauth && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
|
icon={
|
||||||
|
<LinuxDoIcon
|
||||||
|
style={{
|
||||||
|
color: '#E95420',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={handleLinuxDOClick}
|
onClick={handleLinuxDOClick}
|
||||||
loading={linuxdoLoading}
|
loading={linuxdoLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 LinuxDO 继续')}</span>
|
<span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.telegram_oauth && (
|
{status.telegram_oauth && (
|
||||||
<div className="flex justify-center my-2">
|
<div className='flex justify-center my-2'>
|
||||||
<TelegramLoginButton
|
<TelegramLoginButton
|
||||||
dataOnauth={onTelegramLoginClicked}
|
dataOnauth={onTelegramLoginClicked}
|
||||||
botName={status.telegram_bot_name}
|
botName={status.telegram_bot_name}
|
||||||
@@ -382,19 +393,27 @@ const RegisterForm = () => {
|
|||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
type="primary"
|
type='primary'
|
||||||
className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
|
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" />}
|
icon={<IconMail size='large' />}
|
||||||
onClick={handleEmailRegisterClick}
|
onClick={handleEmailRegisterClick}
|
||||||
loading={emailRegisterLoading}
|
loading={emailRegisterLoading}
|
||||||
>
|
>
|
||||||
<span className="ml-3">{t('使用 用户名 注册')}</span>
|
<span className='ml-3'>{t('使用 用户名 注册')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm">
|
<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>
|
<Text>
|
||||||
|
{t('已有账户?')}{' '}
|
||||||
|
<Link
|
||||||
|
to='/login'
|
||||||
|
className='text-blue-600 hover:text-blue-800 font-medium'
|
||||||
|
>
|
||||||
|
{t('登录')}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -405,44 +424,48 @@ const RegisterForm = () => {
|
|||||||
|
|
||||||
const renderEmailRegisterForm = () => {
|
const renderEmailRegisterForm = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<div className="w-full max-w-md">
|
<div className='w-full max-w-md'>
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
<div className='flex items-center justify-center mb-6 gap-2'>
|
||||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
<img src={logo} alt='Logo' className='h-10 rounded-full' />
|
||||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
<Title heading={3} className='!text-gray-800'>
|
||||||
|
{systemName}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-0 !rounded-2xl overflow-hidden">
|
<Card className='border-0 !rounded-2xl overflow-hidden'>
|
||||||
<div className="flex justify-center pt-6 pb-2">
|
<div className='flex justify-center pt-6 pb-2'>
|
||||||
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
|
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
|
||||||
|
{t('注 册')}
|
||||||
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-8">
|
<div className='px-2 py-8'>
|
||||||
<Form className="space-y-3">
|
<Form className='space-y-3'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="username"
|
field='username'
|
||||||
label={t('用户名')}
|
label={t('用户名')}
|
||||||
placeholder={t('请输入用户名')}
|
placeholder={t('请输入用户名')}
|
||||||
name="username"
|
name='username'
|
||||||
onChange={(value) => handleChange('username', value)}
|
onChange={(value) => handleChange('username', value)}
|
||||||
prefix={<IconUser />}
|
prefix={<IconUser />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="password"
|
field='password'
|
||||||
label={t('密码')}
|
label={t('密码')}
|
||||||
placeholder={t('输入密码,最短 8 位,最长 20 位')}
|
placeholder={t('输入密码,最短 8 位,最长 20 位')}
|
||||||
name="password"
|
name='password'
|
||||||
mode="password"
|
mode='password'
|
||||||
onChange={(value) => handleChange('password', value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="password2"
|
field='password2'
|
||||||
label={t('确认密码')}
|
label={t('确认密码')}
|
||||||
placeholder={t('确认密码')}
|
placeholder={t('确认密码')}
|
||||||
name="password2"
|
name='password2'
|
||||||
mode="password"
|
mode='password'
|
||||||
onChange={(value) => handleChange('password2', value)}
|
onChange={(value) => handleChange('password2', value)}
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
/>
|
/>
|
||||||
@@ -450,11 +473,11 @@ const RegisterForm = () => {
|
|||||||
{showEmailVerification && (
|
{showEmailVerification && (
|
||||||
<>
|
<>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="email"
|
field='email'
|
||||||
label={t('邮箱')}
|
label={t('邮箱')}
|
||||||
placeholder={t('输入邮箱地址')}
|
placeholder={t('输入邮箱地址')}
|
||||||
name="email"
|
name='email'
|
||||||
type="email"
|
type='email'
|
||||||
onChange={(value) => handleChange('email', value)}
|
onChange={(value) => handleChange('email', value)}
|
||||||
prefix={<IconMail />}
|
prefix={<IconMail />}
|
||||||
suffix={
|
suffix={
|
||||||
@@ -463,27 +486,31 @@ const RegisterForm = () => {
|
|||||||
loading={verificationCodeLoading}
|
loading={verificationCodeLoading}
|
||||||
disabled={disableButton || verificationCodeLoading}
|
disabled={disableButton || verificationCodeLoading}
|
||||||
>
|
>
|
||||||
{disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
|
{disableButton
|
||||||
|
? `${t('重新发送')} (${countdown})`
|
||||||
|
: t('获取验证码')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="verification_code"
|
field='verification_code'
|
||||||
label={t('验证码')}
|
label={t('验证码')}
|
||||||
placeholder={t('输入验证码')}
|
placeholder={t('输入验证码')}
|
||||||
name="verification_code"
|
name='verification_code'
|
||||||
onChange={(value) => handleChange('verification_code', value)}
|
onChange={(value) =>
|
||||||
|
handleChange('verification_code', value)
|
||||||
|
}
|
||||||
prefix={<IconKey />}
|
prefix={<IconKey />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
<div className='space-y-2 pt-2'>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
type="primary"
|
type='primary'
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={registerLoading}
|
loading={registerLoading}
|
||||||
>
|
>
|
||||||
@@ -492,17 +519,21 @@ const RegisterForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Form>
|
</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'>
|
<Divider margin='12px' align='center'>
|
||||||
{t('或')}
|
{t('或')}
|
||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
<div className='mt-4 text-center'>
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme='outline'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
className="w-full !rounded-full"
|
className='w-full !rounded-full'
|
||||||
onClick={handleOtherRegisterOptionsClick}
|
onClick={handleOtherRegisterOptionsClick}
|
||||||
loading={otherRegisterOptionsLoading}
|
loading={otherRegisterOptionsLoading}
|
||||||
>
|
>
|
||||||
@@ -512,8 +543,16 @@ const RegisterForm = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm">
|
<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>
|
<Text>
|
||||||
|
{t('已有账户?')}{' '}
|
||||||
|
<Link
|
||||||
|
to='/login'
|
||||||
|
className='text-blue-600 hover:text-blue-800 font-medium'
|
||||||
|
>
|
||||||
|
{t('登录')}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -536,21 +575,25 @@ const RegisterForm = () => {
|
|||||||
loading: wechatCodeSubmitLoading,
|
loading: wechatCodeSubmitLoading,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
|
<img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mb-4">
|
<div className='text-center mb-4'>
|
||||||
<p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
|
<p>
|
||||||
|
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="wechat_verification_code"
|
field='wechat_verification_code'
|
||||||
placeholder={t('验证码')}
|
placeholder={t('验证码')}
|
||||||
label={t('验证码')}
|
label={t('验证码')}
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={(value) => handleChange('wechat_verification_code', value)}
|
onChange={(value) =>
|
||||||
|
handleChange('wechat_verification_code', value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -558,18 +601,31 @@ const RegisterForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
className='blur-ball blur-ball-indigo'
|
||||||
<div className="w-full max-w-sm mt-[60px]">
|
style={{ top: '-80px', right: '-80px', transform: 'none' }}
|
||||||
{showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
/>
|
||||||
|
<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()
|
? renderEmailRegisterForm()
|
||||||
: renderOAuthOptions()}
|
: renderOAuthOptions()}
|
||||||
{renderWeChatLoginModal()}
|
{renderWeChatLoginModal()}
|
||||||
|
|
||||||
{turnstileEnabled && (
|
{turnstileEnabled && (
|
||||||
<div className="flex justify-center mt-6">
|
<div className='flex justify-center mt-6'>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={turnstileSiteKey}
|
sitekey={turnstileSiteKey}
|
||||||
onVerify={(token) => {
|
onVerify={(token) => {
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { API, showError, showSuccess } from '../../helpers';
|
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';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
@@ -44,7 +51,7 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/user/login/2fa', {
|
const res = await API.post('/api/user/login/2fa', {
|
||||||
code: verificationCode
|
code: verificationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -72,30 +79,30 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
|
|
||||||
if (isModal) {
|
if (isModal) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Paragraph className="text-gray-600 dark:text-gray-300">
|
<Paragraph className='text-gray-600 dark:text-gray-300'>
|
||||||
请输入认证器应用显示的验证码完成登录
|
请输入认证器应用显示的验证码完成登录
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="code"
|
field='code'
|
||||||
label={useBackupCode ? "备用码" : "验证码"}
|
label={useBackupCode ? '备用码' : '验证码'}
|
||||||
placeholder={useBackupCode ? "请输入8位备用码" : "请输入6位验证码"}
|
placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={setVerificationCode}
|
onChange={setVerificationCode}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
size="large"
|
size='large'
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
type="primary"
|
type='primary'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
block
|
block
|
||||||
size="large"
|
size='large'
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
>
|
>
|
||||||
验证并登录
|
验证并登录
|
||||||
@@ -106,8 +113,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUseBackupCode(!useBackupCode);
|
setUseBackupCode(!useBackupCode);
|
||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
@@ -119,8 +126,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
|
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
style={{ color: '#1890ff', padding: 0 }}
|
style={{ color: '#1890ff', padding: 0 }}
|
||||||
>
|
>
|
||||||
@@ -129,15 +136,14 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
<div className='bg-gray-50 dark:bg-gray-800 rounded-lg p-3'>
|
||||||
<Text size="small" type="secondary">
|
<Text size='small' type='secondary'>
|
||||||
<strong>提示:</strong>
|
<strong>提示:</strong>
|
||||||
<br />
|
<br />
|
||||||
• 验证码每30秒更新一次
|
• 验证码每30秒更新一次
|
||||||
<br />
|
<br />
|
||||||
• 如果无法获取验证码,请使用备用码
|
• 如果无法获取验证码,请使用备用码
|
||||||
<br />
|
<br />• 每个备用码只能使用一次
|
||||||
• 每个备用码只能使用一次
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,39 +151,41 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: '60vh'
|
minHeight: '60vh',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Card style={{ width: 400, padding: 24 }}>
|
<Card style={{ width: 400, padding: 24 }}>
|
||||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||||
<Title heading={3}>两步验证</Title>
|
<Title heading={3}>两步验证</Title>
|
||||||
<Paragraph type="secondary">
|
<Paragraph type='secondary'>
|
||||||
请输入认证器应用显示的验证码完成登录
|
请输入认证器应用显示的验证码完成登录
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field="code"
|
field='code'
|
||||||
label={useBackupCode ? "备用码" : "验证码"}
|
label={useBackupCode ? '备用码' : '验证码'}
|
||||||
placeholder={useBackupCode ? "请输入8位备用码" : "请输入6位验证码"}
|
placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={setVerificationCode}
|
onChange={setVerificationCode}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
size="large"
|
size='large'
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
type="primary"
|
type='primary'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
block
|
block
|
||||||
size="large"
|
size='large'
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
>
|
>
|
||||||
验证并登录
|
验证并登录
|
||||||
@@ -188,8 +196,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUseBackupCode(!useBackupCode);
|
setUseBackupCode(!useBackupCode);
|
||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
@@ -201,8 +209,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
|
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
style={{ color: '#1890ff', padding: 0 }}
|
style={{ color: '#1890ff', padding: 0 }}
|
||||||
>
|
>
|
||||||
@@ -211,15 +219,21 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 24, padding: 16, background: '#f6f8fa', borderRadius: 6 }}>
|
<div
|
||||||
<Text size="small" type="secondary">
|
style={{
|
||||||
|
marginTop: 24,
|
||||||
|
padding: 16,
|
||||||
|
background: '#f6f8fa',
|
||||||
|
borderRadius: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size='small' type='secondary'>
|
||||||
<strong>提示:</strong>
|
<strong>提示:</strong>
|
||||||
<br />
|
<br />
|
||||||
• 验证码每30秒更新一次
|
• 验证码每30秒更新一次
|
||||||
<br />
|
<br />
|
||||||
• 如果无法获取验证码,请使用备用码
|
• 如果无法获取验证码,请使用备用码
|
||||||
<br />
|
<br />• 每个备用码只能使用一次
|
||||||
• 每个备用码只能使用一次
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export function PreCode(props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="copy-code-button"
|
className='copy-code-button'
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '8px',
|
top: '8px',
|
||||||
@@ -174,14 +174,15 @@ export function PreCode(props) {
|
|||||||
>
|
>
|
||||||
<Tooltip content={t('复制代码')}>
|
<Tooltip content={t('复制代码')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
icon={<IconCopy />}
|
icon={<IconCopy />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const code = ref.current.querySelector('code')?.innerText ?? '';
|
const code =
|
||||||
|
ref.current.querySelector('code')?.innerText ?? '';
|
||||||
copy(code).then((success) => {
|
copy(code).then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.success(t('代码已复制到剪贴板'));
|
Toast.success(t('代码已复制到剪贴板'));
|
||||||
@@ -217,7 +218,13 @@ export function PreCode(props) {
|
|||||||
backgroundColor: 'var(--semi-color-bg-1)',
|
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预览:
|
HTML预览:
|
||||||
</div>
|
</div>
|
||||||
<div dangerouslySetInnerHTML={{ __html: htmlCode }} />
|
<div dangerouslySetInnerHTML={{ __html: htmlCode }} />
|
||||||
@@ -258,7 +265,7 @@ function CustomCode(props) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="small" onClick={toggleCollapsed} theme="solid">
|
<Button size='small' onClick={toggleCollapsed} theme='solid'>
|
||||||
{t('显示更多')}
|
{t('显示更多')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -367,7 +374,16 @@ function _MarkdownContent(props) {
|
|||||||
components={{
|
components={{
|
||||||
pre: PreCode,
|
pre: PreCode,
|
||||||
code: CustomCode,
|
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) => {
|
a: (aProps) => {
|
||||||
const href = aProps.href || '';
|
const href = aProps.href || '';
|
||||||
if (/\.(aac|mp3|opus|wav)$/.test(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)) {
|
if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
|
||||||
return (
|
return (
|
||||||
<video controls style={{ width: '100%', maxWidth: '100%', margin: '12px 0' }}>
|
<video
|
||||||
|
controls
|
||||||
|
style={{ width: '100%', maxWidth: '100%', margin: '12px 0' }}
|
||||||
|
>
|
||||||
<source src={href} />
|
<source src={href} />
|
||||||
</video>
|
</video>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const isInternal = /^\/#/i.test(href);
|
const isInternal = /^\/#/i.test(href);
|
||||||
const target = isInternal ? '_self' : aProps.target ?? '_blank';
|
const target = isInternal ? '_self' : (aProps.target ?? '_blank');
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
{...aProps}
|
{...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)' }} />,
|
h1: (props) => (
|
||||||
h2: (props) => <h2 {...props} style={{ fontSize: '20px', fontWeight: 'bold', margin: '18px 0 10px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
<h1
|
||||||
h3: (props) => <h3 {...props} style={{ fontSize: '18px', fontWeight: 'bold', margin: '16px 0 8px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
{...props}
|
||||||
h4: (props) => <h4 {...props} style={{ fontSize: '16px', fontWeight: 'bold', margin: '14px 0 6px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
style={{
|
||||||
h5: (props) => <h5 {...props} style={{ fontSize: '14px', fontWeight: 'bold', margin: '12px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
fontSize: '24px',
|
||||||
h6: (props) => <h6 {...props} style={{ fontSize: '13px', fontWeight: 'bold', margin: '10px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
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) => (
|
||||||
<blockquote
|
<blockquote
|
||||||
{...props}
|
{...props}
|
||||||
style={{
|
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',
|
paddingLeft: '16px',
|
||||||
margin: '12px 0',
|
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',
|
padding: '8px 16px',
|
||||||
borderRadius: '0 4px 4px 0',
|
borderRadius: '0 4px 4px 0',
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
@@ -424,9 +507,36 @@ function _MarkdownContent(props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
ul: (props) => <ul {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
|
ul: (props) => (
|
||||||
ol: (props) => <ol {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
|
<ul
|
||||||
li: (props) => <li {...props} style={{ margin: '4px 0', lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
|
{...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) => (
|
table: (props) => (
|
||||||
<div style={{ overflow: 'auto', margin: '12px 0' }}>
|
<div style={{ overflow: 'auto', margin: '12px 0' }}>
|
||||||
<table
|
<table
|
||||||
@@ -434,7 +544,9 @@ function _MarkdownContent(props) {
|
|||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderCollapse: 'collapse',
|
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',
|
borderRadius: '6px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
@@ -446,8 +558,12 @@ function _MarkdownContent(props) {
|
|||||||
{...props}
|
{...props}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.2)' : 'var(--semi-color-fill-1)',
|
backgroundColor: isUserMessage
|
||||||
border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
|
? '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',
|
fontWeight: 'bold',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
color: isUserMessage ? 'white' : 'inherit',
|
color: isUserMessage ? 'white' : 'inherit',
|
||||||
@@ -459,7 +575,9 @@ function _MarkdownContent(props) {
|
|||||||
{...props}
|
{...props}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
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',
|
color: isUserMessage ? 'white' : 'inherit',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -496,25 +614,29 @@ export function MarkdownRenderer(props) {
|
|||||||
color: 'var(--semi-color-text-0)',
|
color: 'var(--semi-color-text-0)',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
dir="auto"
|
dir='auto'
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
color: 'var(--semi-color-text-2)',
|
color: 'var(--semi-color-text-2)',
|
||||||
}}>
|
}}
|
||||||
<div style={{
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
border: '2px solid var(--semi-color-border)',
|
border: '2px solid var(--semi-color-border)',
|
||||||
borderTop: '2px solid var(--semi-color-primary)',
|
borderTop: '2px solid var(--semi-color-primary)',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
animation: 'spin 1s linear infinite',
|
animation: 'spin 1s linear infinite',
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
正在渲染...
|
正在渲染...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -59,12 +59,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-message a {
|
.user-message a {
|
||||||
color: #87CEEB !important;
|
color: #87ceeb !important;
|
||||||
/* 浅蓝色链接 */
|
/* 浅蓝色链接 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message a:hover {
|
.user-message a:hover {
|
||||||
color: #B0E0E6 !important;
|
color: #b0e0e6 !important;
|
||||||
/* hover时更浅的蓝色 */
|
/* hover时更浅的蓝色 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +298,12 @@ pre:hover .copy-code-button {
|
|||||||
.markdown-body hr {
|
.markdown-body hr {
|
||||||
border: none;
|
border: none;
|
||||||
height: 1px;
|
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;
|
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;
|
margin-right: 8px;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const TwoFactorAuthModal = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
placeholder
|
placeholder,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -56,10 +56,18 @@ const TwoFactorAuthModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<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">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{title || t('安全验证')}
|
{title || t('安全验证')}
|
||||||
@@ -69,11 +77,9 @@ const TwoFactorAuthModal = ({
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button onClick={onCancel}>
|
<Button onClick={onCancel}>{t('取消')}</Button>
|
||||||
{t('取消')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!code || loading}
|
disabled={!code || loading}
|
||||||
onClick={onVerify}
|
onClick={onVerify}
|
||||||
@@ -85,18 +91,29 @@ const TwoFactorAuthModal = ({
|
|||||||
width={500}
|
width={500}
|
||||||
style={{ maxWidth: '90vw' }}
|
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='bg-blue-50 dark:bg-blue-900 rounded-lg p-4'>
|
||||||
<div className="flex items-start">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="text-blue-800 dark:text-blue-200">
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
className='text-blue-800 dark:text-blue-200'
|
||||||
|
>
|
||||||
{t('安全验证')}
|
{t('安全验证')}
|
||||||
</Typography.Text>
|
</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('为了保护账户安全,请验证您的两步验证码。')}
|
{description || t('为了保护账户安全,请验证您的两步验证码。')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,19 +122,19 @@ const TwoFactorAuthModal = ({
|
|||||||
|
|
||||||
{/* 验证码输入 */}
|
{/* 验证码输入 */}
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="block mb-2">
|
<Typography.Text strong className='block mb-2'>
|
||||||
{t('验证身份')}
|
{t('验证身份')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder || t('请输入认证器验证码或备用码')}
|
placeholder={placeholder || t('请输入认证器验证码或备用码')}
|
||||||
value={code}
|
value={code}
|
||||||
onChange={onCodeChange}
|
onChange={onCodeChange}
|
||||||
size="large"
|
size='large'
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
autoFocus
|
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位备用码')}
|
{t('支持6位TOTP验证码或8位备用码')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,47 +71,38 @@ const CardPro = ({
|
|||||||
const hasMobileHideableContent = actionsArea || searchArea;
|
const hasMobileHideableContent = actionsArea || searchArea;
|
||||||
|
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
const hasContent = statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
|
const hasContent =
|
||||||
|
statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
|
||||||
if (!hasContent) return null;
|
if (!hasContent) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full">
|
<div className='flex flex-col w-full'>
|
||||||
{/* 统计信息区域 - 用于type2 */}
|
{/* 统计信息区域 - 用于type2 */}
|
||||||
{type === 'type2' && statsArea && (
|
{type === 'type2' && statsArea && <>{statsArea}</>}
|
||||||
<>
|
|
||||||
{statsArea}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 描述信息区域 - 用于type1和type3 */}
|
{/* 描述信息区域 - 用于type1和type3 */}
|
||||||
{(type === 'type1' || type === 'type3') && descriptionArea && (
|
{(type === 'type1' || type === 'type3') && descriptionArea && (
|
||||||
<>
|
<>{descriptionArea}</>
|
||||||
{descriptionArea}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 第一个分隔线 - 在描述信息或统计信息后面 */}
|
{/* 第一个分隔线 - 在描述信息或统计信息后面 */}
|
||||||
{((type === 'type1' || type === 'type3') && descriptionArea) ||
|
{((type === 'type1' || type === 'type3') && descriptionArea) ||
|
||||||
(type === 'type2' && statsArea) ? (
|
(type === 'type2' && statsArea) ? (
|
||||||
<Divider margin="12px" />
|
<Divider margin='12px' />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* 类型切换/标签区域 - 主要用于type3 */}
|
{/* 类型切换/标签区域 - 主要用于type3 */}
|
||||||
{type === 'type3' && tabsArea && (
|
{type === 'type3' && tabsArea && <>{tabsArea}</>}
|
||||||
<>
|
|
||||||
{tabsArea}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 移动端操作切换按钮 */}
|
{/* 移动端操作切换按钮 */}
|
||||||
{isMobile && hasMobileHideableContent && (
|
{isMobile && hasMobileHideableContent && (
|
||||||
<>
|
<>
|
||||||
<div className="w-full mb-2">
|
<div className='w-full mb-2'>
|
||||||
<Button
|
<Button
|
||||||
onClick={toggleMobileActions}
|
onClick={toggleMobileActions}
|
||||||
icon={showMobileActions ? <IconEyeClosed /> : <IconEyeOpened />}
|
icon={showMobileActions ? <IconEyeClosed /> : <IconEyeOpened />}
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
theme='outline'
|
theme='outline'
|
||||||
block
|
block
|
||||||
>
|
>
|
||||||
@@ -126,32 +117,24 @@ const CardPro = ({
|
|||||||
className={`flex flex-col gap-2 ${isMobile && !showMobileActions ? 'hidden' : ''}`}
|
className={`flex flex-col gap-2 ${isMobile && !showMobileActions ? 'hidden' : ''}`}
|
||||||
>
|
>
|
||||||
{/* 操作按钮区域 - 用于type1和type3 */}
|
{/* 操作按钮区域 - 用于type1和type3 */}
|
||||||
{(type === 'type1' || type === 'type3') && actionsArea && (
|
{(type === 'type1' || type === 'type3') &&
|
||||||
Array.isArray(actionsArea) ? (
|
actionsArea &&
|
||||||
|
(Array.isArray(actionsArea) ? (
|
||||||
actionsArea.map((area, idx) => (
|
actionsArea.map((area, idx) => (
|
||||||
<React.Fragment key={idx}>
|
<React.Fragment key={idx}>
|
||||||
{idx !== 0 && <Divider />}
|
{idx !== 0 && <Divider />}
|
||||||
<div className="w-full">
|
<div className='w-full'>{area}</div>
|
||||||
{area}
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full">
|
<div className='w-full'>{actionsArea}</div>
|
||||||
{actionsArea}
|
))}
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 当同时存在操作区和搜索区时,插入分隔线 */}
|
{/* 当同时存在操作区和搜索区时,插入分隔线 */}
|
||||||
{(actionsArea && searchArea) && <Divider />}
|
{actionsArea && searchArea && <Divider />}
|
||||||
|
|
||||||
{/* 搜索表单区域 - 所有类型都可能有 */}
|
{/* 搜索表单区域 - 所有类型都可能有 */}
|
||||||
{searchArea && (
|
{searchArea && <div className='w-full'>{searchArea}</div>}
|
||||||
<div className="w-full">
|
|
||||||
{searchArea}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
@@ -75,18 +83,22 @@ const CardTable = ({
|
|||||||
|
|
||||||
const renderSkeletonCard = (key) => {
|
const renderSkeletonCard = (key) => {
|
||||||
const placeholder = (
|
const placeholder = (
|
||||||
<div className="p-2">
|
<div className='p-2'>
|
||||||
{visibleCols.map((col, idx) => {
|
{visibleCols.map((col, idx) => {
|
||||||
if (!col.title) {
|
if (!col.title) {
|
||||||
return (
|
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 }} />
|
<Skeleton.Title active style={{ width: 100, height: 24 }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 style={{ width: 80, height: 14 }} />
|
||||||
<Skeleton.Title
|
<Skeleton.Title
|
||||||
active
|
active
|
||||||
@@ -103,14 +115,14 @@ const CardTable = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={key} className="!rounded-2xl shadow-sm">
|
<Card key={key} className='!rounded-2xl shadow-sm'>
|
||||||
<Skeleton loading={true} active placeholder={placeholder}></Skeleton>
|
<Skeleton loading={true} active placeholder={placeholder}></Skeleton>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className='flex flex-col gap-2'>
|
||||||
{[1, 2, 3].map((i) => renderSkeletonCard(i))}
|
{[1, 2, 3].map((i) => renderSkeletonCard(i))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -127,9 +139,12 @@ const CardTable = ({
|
|||||||
(!tableProps.rowExpandable || tableProps.rowExpandable(record));
|
(!tableProps.rowExpandable || tableProps.rowExpandable(record));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
|
<Card key={rowKeyVal} className='!rounded-2xl shadow-sm'>
|
||||||
{columns.map((col, colIdx) => {
|
{columns.map((col, colIdx) => {
|
||||||
if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
|
if (
|
||||||
|
tableProps?.visibleColumns &&
|
||||||
|
!tableProps.visibleColumns[col.key]
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +155,7 @@ const CardTable = ({
|
|||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return (
|
return (
|
||||||
<div key={col.key || colIdx} className="mt-2 flex justify-end">
|
<div key={col.key || colIdx} className='mt-2 flex justify-end'>
|
||||||
{cellContent}
|
{cellContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -149,14 +164,16 @@ const CardTable = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.key || colIdx}
|
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)' }}
|
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}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 break-all flex justify-end items-center gap-1">
|
<div className='flex-1 break-all flex justify-end items-center gap-1'>
|
||||||
{cellContent !== undefined && cellContent !== null ? cellContent : '-'}
|
{cellContent !== undefined && cellContent !== null
|
||||||
|
? cellContent
|
||||||
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -177,7 +194,7 @@ const CardTable = ({
|
|||||||
{showDetails ? t('收起') : t('详情')}
|
{showDetails ? t('收起') : t('详情')}
|
||||||
</Button>
|
</Button>
|
||||||
<Collapsible isOpen={showDetails} keepDOM>
|
<Collapsible isOpen={showDetails} keepDOM>
|
||||||
<div className="pt-2">
|
<div className='pt-2'>
|
||||||
{tableProps.expandedRowRender(record, index)}
|
{tableProps.expandedRowRender(record, index)}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@@ -190,19 +207,23 @@ const CardTable = ({
|
|||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
if (tableProps.empty) return tableProps.empty;
|
if (tableProps.empty) return tableProps.empty;
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center p-4">
|
<div className='flex justify-center p-4'>
|
||||||
<Empty description="No Data" />
|
<Empty description='No Data' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className='flex flex-col gap-2'>
|
||||||
{dataSource.map((record, index) => (
|
{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 && (
|
{!hidePagination && tableProps.pagination && dataSource.length > 0 && (
|
||||||
<div className="mt-2 flex justify-center">
|
<div className='mt-2 flex justify-center'>
|
||||||
<Pagination {...tableProps.pagination} />
|
<Pagination {...tableProps.pagination} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ const parseChannelKeys = (keyData, t) => {
|
|||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
return parsed.map((item, index) => ({
|
return parsed.map((item, index) => ({
|
||||||
id: 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',
|
type: typeof item === 'string' ? 'text' : 'json',
|
||||||
label: `${t('密钥')} ${index + 1}`
|
label: `${t('密钥')} ${index + 1}`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -52,23 +53,25 @@ const parseChannelKeys = (keyData, t) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是多行密钥(按换行符分割)
|
// 检查是否是多行密钥(按换行符分割)
|
||||||
const lines = trimmed.split('\n').filter(line => line.trim());
|
const lines = trimmed.split('\n').filter((line) => line.trim());
|
||||||
if (lines.length > 1) {
|
if (lines.length > 1) {
|
||||||
return lines.map((line, index) => ({
|
return lines.map((line, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
content: line.trim(),
|
content: line.trim(),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: `${t('密钥')} ${index + 1}`
|
label: `${t('密钥')} ${index + 1}`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单个密钥
|
// 单个密钥
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
content: trimmed,
|
content: trimmed,
|
||||||
type: trimmed.startsWith('{') ? 'json' : 'text',
|
type: trimmed.startsWith('{') ? 'json' : 'text',
|
||||||
label: t('密钥')
|
label: t('密钥'),
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +88,7 @@ const ChannelKeyDisplay = ({
|
|||||||
showSuccessIcon = true,
|
showSuccessIcon = true,
|
||||||
successText,
|
successText,
|
||||||
showWarning = true,
|
showWarning = true,
|
||||||
warningText
|
warningText,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -103,34 +106,42 @@ const ChannelKeyDisplay = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
{/* 成功状态 */}
|
{/* 成功状态 */}
|
||||||
{showSuccessIcon && (
|
{showSuccessIcon && (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<Typography.Text strong className="text-green-700">
|
<Typography.Text strong className='text-green-700'>
|
||||||
{successText || t('验证成功')}
|
{successText || t('验证成功')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 密钥内容 */}
|
{/* 密钥内容 */}
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Typography.Text strong>
|
<Typography.Text strong>
|
||||||
{isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')}
|
{isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{isMultipleKeys && (
|
{isMultipleKeys && (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Typography.Text type="tertiary" size="small">
|
<Typography.Text type='tertiary' size='small'>
|
||||||
{t('共 {{count}} 个密钥', { count: parsedKeys.length })}
|
{t('共 {{count}} 个密钥', { count: parsedKeys.length })}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
onClick={handleCopyAll}
|
onClick={handleCopyAll}
|
||||||
>
|
>
|
||||||
{t('复制全部')}
|
{t('复制全部')}
|
||||||
@@ -139,26 +150,39 @@ 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) => (
|
{parsedKeys.map((keyItem) => (
|
||||||
<Card key={keyItem.id} className="!rounded-lg !border !border-gray-200 dark:!border-gray-700">
|
<Card
|
||||||
<div className="space-y-2">
|
key={keyItem.id}
|
||||||
<div className="flex items-center justify-between">
|
className='!rounded-lg !border !border-gray-200 dark:!border-gray-700'
|
||||||
<Typography.Text strong size="small" className="text-gray-700 dark:text-gray-300">
|
>
|
||||||
|
<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}
|
{keyItem.label}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
{keyItem.type === 'json' && (
|
{keyItem.type === 'json' && (
|
||||||
<Tag size="small" color="blue">{t('JSON')}</Tag>
|
<Tag size='small' color='blue'>
|
||||||
|
{t('JSON')}
|
||||||
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
icon={
|
icon={
|
||||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
className='w-3 h-3'
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
}
|
}
|
||||||
onClick={() => handleCopyKey(keyItem.content)}
|
onClick={() => handleCopyKey(keyItem.content)}
|
||||||
@@ -168,17 +192,21 @@ const ChannelKeyDisplay = ({
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<Typography.Text
|
||||||
code
|
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}
|
{keyItem.content}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{keyItem.type === 'json' && (
|
{keyItem.type === 'json' && (
|
||||||
<Typography.Text type="tertiary" size="small" className="block">
|
<Typography.Text
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
className='block'
|
||||||
|
>
|
||||||
{t('JSON格式密钥,请确保格式正确')}
|
{t('JSON格式密钥,请确保格式正确')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -188,12 +216,26 @@ const ChannelKeyDisplay = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMultipleKeys && (
|
{isMultipleKeys && (
|
||||||
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
|
<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">
|
<Typography.Text
|
||||||
<svg className="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
type='tertiary'
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
{t('检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。')}
|
{t(
|
||||||
|
'检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。',
|
||||||
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -201,17 +243,31 @@ const ChannelKeyDisplay = ({
|
|||||||
|
|
||||||
{/* 安全警告 */}
|
{/* 安全警告 */}
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900 rounded-lg p-4">
|
<div className='bg-yellow-50 dark:bg-yellow-900 rounded-lg p-4'>
|
||||||
<div className="flex items-start">
|
<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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="text-yellow-800 dark:text-yellow-200">
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
className='text-yellow-800 dark:text-yellow-200'
|
||||||
|
>
|
||||||
{t('安全提醒')}
|
{t('安全提醒')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text className="block text-yellow-700 dark:text-yellow-300 text-sm mt-1">
|
<Typography.Text className='block text-yellow-700 dark:text-yellow-300 text-sm mt-1'>
|
||||||
{warningText || t('请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。')}
|
{warningText ||
|
||||||
|
t(
|
||||||
|
'请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。',
|
||||||
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,11 +36,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import { IconPlus, IconDelete, IconAlertTriangle } from '@douyinfe/semi-icons';
|
||||||
IconPlus,
|
|
||||||
IconDelete,
|
|
||||||
IconAlertTriangle,
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -88,7 +84,7 @@ const JSONEditor = ({
|
|||||||
// 将键值对数组转换为对象(重复键时后面的会覆盖前面的)
|
// 将键值对数组转换为对象(重复键时后面的会覆盖前面的)
|
||||||
const keyValueArrayToObject = useCallback((arr) => {
|
const keyValueArrayToObject = useCallback((arr) => {
|
||||||
const result = {};
|
const result = {};
|
||||||
arr.forEach(item => {
|
arr.forEach((item) => {
|
||||||
if (item.key) {
|
if (item.key) {
|
||||||
result[item.key] = item.value;
|
result[item.key] = item.value;
|
||||||
}
|
}
|
||||||
@@ -115,7 +111,8 @@ const JSONEditor = ({
|
|||||||
// 手动模式下的本地文本缓冲
|
// 手动模式下的本地文本缓冲
|
||||||
const [manualText, setManualText] = useState(() => {
|
const [manualText, setManualText] = useState(() => {
|
||||||
if (typeof value === 'string') return value;
|
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 '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +137,7 @@ const JSONEditor = ({
|
|||||||
const keyCount = {};
|
const keyCount = {};
|
||||||
const duplicates = new Set();
|
const duplicates = new Set();
|
||||||
|
|
||||||
keyValuePairs.forEach(pair => {
|
keyValuePairs.forEach((pair) => {
|
||||||
if (pair.key) {
|
if (pair.key) {
|
||||||
keyCount[pair.key] = (keyCount[pair.key] || 0) + 1;
|
keyCount[pair.key] = (keyCount[pair.key] || 0) + 1;
|
||||||
if (keyCount[pair.key] > 1) {
|
if (keyCount[pair.key] > 1) {
|
||||||
@@ -178,16 +175,21 @@ const JSONEditor = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editMode !== 'manual') {
|
if (editMode !== 'manual') {
|
||||||
if (typeof value === 'string') setManualText(value);
|
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('');
|
else setManualText('');
|
||||||
}
|
}
|
||||||
}, [value, editMode]);
|
}, [value, editMode]);
|
||||||
|
|
||||||
// 处理可视化编辑的数据变化
|
// 处理可视化编辑的数据变化
|
||||||
const handleVisualChange = useCallback((newPairs) => {
|
const handleVisualChange = useCallback(
|
||||||
|
(newPairs) => {
|
||||||
setKeyValuePairs(newPairs);
|
setKeyValuePairs(newPairs);
|
||||||
const jsonObject = keyValueArrayToObject(newPairs);
|
const jsonObject = keyValueArrayToObject(newPairs);
|
||||||
const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2);
|
const jsonString =
|
||||||
|
Object.keys(jsonObject).length === 0
|
||||||
|
? ''
|
||||||
|
: JSON.stringify(jsonObject, null, 2);
|
||||||
|
|
||||||
setJsonError('');
|
setJsonError('');
|
||||||
|
|
||||||
@@ -197,10 +199,13 @@ const JSONEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChange?.(jsonString);
|
onChange?.(jsonString);
|
||||||
}, [onChange, formApi, field, keyValueArrayToObject]);
|
},
|
||||||
|
[onChange, formApi, field, keyValueArrayToObject],
|
||||||
|
);
|
||||||
|
|
||||||
// 处理手动编辑的数据变化
|
// 处理手动编辑的数据变化
|
||||||
const handleManualChange = useCallback((newValue) => {
|
const handleManualChange = useCallback(
|
||||||
|
(newValue) => {
|
||||||
setManualText(newValue);
|
setManualText(newValue);
|
||||||
if (newValue && newValue.trim()) {
|
if (newValue && newValue.trim()) {
|
||||||
try {
|
try {
|
||||||
@@ -216,13 +221,19 @@ const JSONEditor = ({
|
|||||||
setJsonError('');
|
setJsonError('');
|
||||||
onChange?.('');
|
onChange?.('');
|
||||||
}
|
}
|
||||||
}, [onChange, objectToKeyValueArray, keyValuePairs]);
|
},
|
||||||
|
[onChange, objectToKeyValueArray, keyValuePairs],
|
||||||
|
);
|
||||||
|
|
||||||
// 切换编辑模式
|
// 切换编辑模式
|
||||||
const toggleEditMode = useCallback(() => {
|
const toggleEditMode = useCallback(() => {
|
||||||
if (editMode === 'visual') {
|
if (editMode === 'visual') {
|
||||||
const jsonObject = keyValueArrayToObject(keyValuePairs);
|
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');
|
setEditMode('manual');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@@ -242,12 +253,19 @@ const JSONEditor = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]);
|
}, [
|
||||||
|
editMode,
|
||||||
|
value,
|
||||||
|
manualText,
|
||||||
|
keyValuePairs,
|
||||||
|
keyValueArrayToObject,
|
||||||
|
objectToKeyValueArray,
|
||||||
|
]);
|
||||||
|
|
||||||
// 添加键值对
|
// 添加键值对
|
||||||
const addKeyValue = useCallback(() => {
|
const addKeyValue = useCallback(() => {
|
||||||
const newPairs = [...keyValuePairs];
|
const newPairs = [...keyValuePairs];
|
||||||
const existingKeys = newPairs.map(p => p.key);
|
const existingKeys = newPairs.map((p) => p.key);
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
let newKey = `field_${counter}`;
|
let newKey = `field_${counter}`;
|
||||||
while (existingKeys.includes(newKey)) {
|
while (existingKeys.includes(newKey)) {
|
||||||
@@ -257,32 +275,41 @@ const JSONEditor = ({
|
|||||||
newPairs.push({
|
newPairs.push({
|
||||||
id: generateUniqueId(),
|
id: generateUniqueId(),
|
||||||
key: newKey,
|
key: newKey,
|
||||||
value: ''
|
value: '',
|
||||||
});
|
});
|
||||||
handleVisualChange(newPairs);
|
handleVisualChange(newPairs);
|
||||||
}, [keyValuePairs, handleVisualChange]);
|
}, [keyValuePairs, handleVisualChange]);
|
||||||
|
|
||||||
// 删除键值对
|
// 删除键值对
|
||||||
const removeKeyValue = useCallback((id) => {
|
const removeKeyValue = useCallback(
|
||||||
const newPairs = keyValuePairs.filter(pair => pair.id !== id);
|
(id) => {
|
||||||
|
const newPairs = keyValuePairs.filter((pair) => pair.id !== id);
|
||||||
handleVisualChange(newPairs);
|
handleVisualChange(newPairs);
|
||||||
}, [keyValuePairs, handleVisualChange]);
|
},
|
||||||
|
[keyValuePairs, handleVisualChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 更新键名
|
// 更新键名
|
||||||
const updateKey = useCallback((id, newKey) => {
|
const updateKey = useCallback(
|
||||||
const newPairs = keyValuePairs.map(pair =>
|
(id, newKey) => {
|
||||||
pair.id === id ? { ...pair, key: newKey } : pair
|
const newPairs = keyValuePairs.map((pair) =>
|
||||||
|
pair.id === id ? { ...pair, key: newKey } : pair,
|
||||||
);
|
);
|
||||||
handleVisualChange(newPairs);
|
handleVisualChange(newPairs);
|
||||||
}, [keyValuePairs, handleVisualChange]);
|
},
|
||||||
|
[keyValuePairs, handleVisualChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 更新值
|
// 更新值
|
||||||
const updateValue = useCallback((id, newValue) => {
|
const updateValue = useCallback(
|
||||||
const newPairs = keyValuePairs.map(pair =>
|
(id, newValue) => {
|
||||||
pair.id === id ? { ...pair, value: newValue } : pair
|
const newPairs = keyValuePairs.map((pair) =>
|
||||||
|
pair.id === id ? { ...pair, value: newValue } : pair,
|
||||||
);
|
);
|
||||||
handleVisualChange(newPairs);
|
handleVisualChange(newPairs);
|
||||||
}, [keyValuePairs, handleVisualChange]);
|
},
|
||||||
|
[keyValuePairs, handleVisualChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 填入模板
|
// 填入模板
|
||||||
const fillTemplate = useCallback(() => {
|
const fillTemplate = useCallback(() => {
|
||||||
@@ -298,7 +325,14 @@ const JSONEditor = ({
|
|||||||
onChange?.(templateString);
|
onChange?.(templateString);
|
||||||
setJsonError('');
|
setJsonError('');
|
||||||
}
|
}
|
||||||
}, [template, onChange, formApi, field, objectToKeyValueArray, keyValuePairs]);
|
}, [
|
||||||
|
template,
|
||||||
|
onChange,
|
||||||
|
formApi,
|
||||||
|
field,
|
||||||
|
objectToKeyValueArray,
|
||||||
|
keyValuePairs,
|
||||||
|
]);
|
||||||
|
|
||||||
// 渲染值输入控件(支持嵌套)
|
// 渲染值输入控件(支持嵌套)
|
||||||
const renderValueInput = (pairId, value) => {
|
const renderValueInput = (pairId, value) => {
|
||||||
@@ -306,12 +340,12 @@ const JSONEditor = ({
|
|||||||
|
|
||||||
if (valueType === 'boolean') {
|
if (valueType === 'boolean') {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={value}
|
checked={value}
|
||||||
onChange={(newValue) => updateValue(pairId, newValue)}
|
onChange={(newValue) => updateValue(pairId, newValue)}
|
||||||
/>
|
/>
|
||||||
<Text type="tertiary" className="ml-2">
|
<Text type='tertiary' className='ml-2'>
|
||||||
{value ? t('true') : t('false')}
|
{value ? t('true') : t('false')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,29 +407,29 @@ const JSONEditor = ({
|
|||||||
// 渲染键值对编辑器
|
// 渲染键值对编辑器
|
||||||
const renderKeyValueEditor = () => {
|
const renderKeyValueEditor = () => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className='space-y-1'>
|
||||||
{/* 重复键警告 */}
|
{/* 重复键警告 */}
|
||||||
{duplicateKeys.size > 0 && (
|
{duplicateKeys.size > 0 && (
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
icon={<IconAlertTriangle />}
|
icon={<IconAlertTriangle />}
|
||||||
description={
|
description={
|
||||||
<div>
|
<div>
|
||||||
<Text strong>{t('存在重复的键名:')}</Text>
|
<Text strong>{t('存在重复的键名:')}</Text>
|
||||||
<Text>{Array.from(duplicateKeys).join(', ')}</Text>
|
<Text>{Array.from(duplicateKeys).join(', ')}</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text type="tertiary" size="small">
|
<Text type='tertiary' size='small'>
|
||||||
{t('注意:JSON中重复的键只会保留最后一个同名键的值')}
|
{t('注意:JSON中重复的键只会保留最后一个同名键的值')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="mb-3"
|
className='mb-3'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keyValuePairs.length === 0 && (
|
{keyValuePairs.length === 0 && (
|
||||||
<div className="text-center py-6 px-4">
|
<div className='text-center py-6 px-4'>
|
||||||
<Text type="tertiary" className="text-gray-500 text-sm">
|
<Text type='tertiary' className='text-gray-500 text-sm'>
|
||||||
{t('暂无数据,点击下方按钮添加键值对')}
|
{t('暂无数据,点击下方按钮添加键值对')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,13 +437,14 @@ const JSONEditor = ({
|
|||||||
|
|
||||||
{keyValuePairs.map((pair, index) => {
|
{keyValuePairs.map((pair, index) => {
|
||||||
const isDuplicate = duplicateKeys.has(pair.key);
|
const isDuplicate = duplicateKeys.has(pair.key);
|
||||||
const isLastDuplicate = isDuplicate &&
|
const isLastDuplicate =
|
||||||
keyValuePairs.slice(index + 1).every(p => p.key !== pair.key);
|
isDuplicate &&
|
||||||
|
keyValuePairs.slice(index + 1).every((p) => p.key !== pair.key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row key={pair.id} gutter={8} align="middle">
|
<Row key={pair.id} gutter={8} align='middle'>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<div className="relative">
|
<div className='relative'>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('键名')}
|
placeholder={t('键名')}
|
||||||
value={pair.key}
|
value={pair.key}
|
||||||
@@ -425,24 +460,22 @@ const JSONEditor = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconAlertTriangle
|
<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={{
|
style={{
|
||||||
color: isLastDuplicate ? '#ff7d00' : '#faad14',
|
color: isLastDuplicate ? '#ff7d00' : '#faad14',
|
||||||
fontSize: '14px'
|
fontSize: '14px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={16}>
|
<Col span={16}>{renderValueInput(pair.id, pair.value)}</Col>
|
||||||
{renderValueInput(pair.id, pair.value)}
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDelete />}
|
icon={<IconDelete />}
|
||||||
type="danger"
|
type='danger'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
onClick={() => removeKeyValue(pair.id)}
|
onClick={() => removeKeyValue(pair.id)}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
@@ -451,11 +484,11 @@ const JSONEditor = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div className="mt-2 flex justify-center">
|
<div className='mt-2 flex justify-center'>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
onClick={addKeyValue}
|
onClick={addKeyValue}
|
||||||
>
|
>
|
||||||
{t('添加键值对')}
|
{t('添加键值对')}
|
||||||
@@ -467,27 +500,27 @@ const JSONEditor = ({
|
|||||||
|
|
||||||
// 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
|
// 渲染区域编辑器(特殊格式)- 也需要改造以支持重复键
|
||||||
const renderRegionEditor = () => {
|
const renderRegionEditor = () => {
|
||||||
const defaultPair = keyValuePairs.find(pair => pair.key === 'default');
|
const defaultPair = keyValuePairs.find((pair) => pair.key === 'default');
|
||||||
const modelPairs = keyValuePairs.filter(pair => pair.key !== 'default');
|
const modelPairs = keyValuePairs.filter((pair) => pair.key !== 'default');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className='space-y-2'>
|
||||||
{/* 重复键警告 */}
|
{/* 重复键警告 */}
|
||||||
{duplicateKeys.size > 0 && (
|
{duplicateKeys.size > 0 && (
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
icon={<IconAlertTriangle />}
|
icon={<IconAlertTriangle />}
|
||||||
description={
|
description={
|
||||||
<div>
|
<div>
|
||||||
<Text strong>{t('存在重复的键名:')}</Text>
|
<Text strong>{t('存在重复的键名:')}</Text>
|
||||||
<Text>{Array.from(duplicateKeys).join(', ')}</Text>
|
<Text>{Array.from(duplicateKeys).join(', ')}</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text type="tertiary" size="small">
|
<Text type='tertiary' size='small'>
|
||||||
{t('注意:JSON中重复的键只会保留最后一个同名键的值')}
|
{t('注意:JSON中重复的键只会保留最后一个同名键的值')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="mb-3"
|
className='mb-3'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -500,11 +533,14 @@ const JSONEditor = ({
|
|||||||
if (defaultPair) {
|
if (defaultPair) {
|
||||||
updateValue(defaultPair.id, value);
|
updateValue(defaultPair.id, value);
|
||||||
} else {
|
} else {
|
||||||
const newPairs = [...keyValuePairs, {
|
const newPairs = [
|
||||||
|
...keyValuePairs,
|
||||||
|
{
|
||||||
id: generateUniqueId(),
|
id: generateUniqueId(),
|
||||||
key: 'default',
|
key: 'default',
|
||||||
value: value
|
value: value,
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
handleVisualChange(newPairs);
|
handleVisualChange(newPairs);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -517,9 +553,9 @@ const JSONEditor = ({
|
|||||||
{modelPairs.map((pair) => {
|
{modelPairs.map((pair) => {
|
||||||
const isDuplicate = duplicateKeys.has(pair.key);
|
const isDuplicate = duplicateKeys.has(pair.key);
|
||||||
return (
|
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}>
|
<Col span={10}>
|
||||||
<div className="relative">
|
<div className='relative'>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('模型名称')}
|
placeholder={t('模型名称')}
|
||||||
value={pair.key}
|
value={pair.key}
|
||||||
@@ -529,7 +565,7 @@ const JSONEditor = ({
|
|||||||
{isDuplicate && (
|
{isDuplicate && (
|
||||||
<Tooltip content={t('重复的键名')}>
|
<Tooltip content={t('重复的键名')}>
|
||||||
<IconAlertTriangle
|
<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' }}
|
style={{ color: '#faad14', fontSize: '14px' }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -546,8 +582,8 @@ const JSONEditor = ({
|
|||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDelete />}
|
icon={<IconDelete />}
|
||||||
type="danger"
|
type='danger'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
onClick={() => removeKeyValue(pair.id)}
|
onClick={() => removeKeyValue(pair.id)}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
@@ -556,12 +592,12 @@ const JSONEditor = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div className="mt-2 flex justify-center">
|
<div className='mt-2 flex justify-center'>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
onClick={addKeyValue}
|
onClick={addKeyValue}
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
>
|
>
|
||||||
{t('添加模型区域')}
|
{t('添加模型区域')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -590,9 +626,9 @@ const JSONEditor = ({
|
|||||||
<Form.Slot label={label}>
|
<Form.Slot label={label}>
|
||||||
<Card
|
<Card
|
||||||
header={
|
header={
|
||||||
<div className="flex justify-between items-center">
|
<div className='flex justify-between items-center'>
|
||||||
<Tabs
|
<Tabs
|
||||||
type="slash"
|
type='slash'
|
||||||
activeKey={editMode}
|
activeKey={editMode}
|
||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
if (key === 'manual' && editMode === 'visual') {
|
if (key === 'manual' && editMode === 'visual') {
|
||||||
@@ -602,16 +638,12 @@ const JSONEditor = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabPane tab={t('可视化')} itemKey="visual" />
|
<TabPane tab={t('可视化')} itemKey='visual' />
|
||||||
<TabPane tab={t('手动编辑')} itemKey="manual" />
|
<TabPane tab={t('手动编辑')} itemKey='manual' />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{template && templateLabel && (
|
{template && templateLabel && (
|
||||||
<Button
|
<Button type='tertiary' onClick={fillTemplate} size='small'>
|
||||||
type="tertiary"
|
|
||||||
onClick={fillTemplate}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{templateLabel}
|
{templateLabel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -619,14 +651,14 @@ const JSONEditor = ({
|
|||||||
}
|
}
|
||||||
headerStyle={{ padding: '12px 16px' }}
|
headerStyle={{ padding: '12px 16px' }}
|
||||||
bodyStyle={{ padding: '16px' }}
|
bodyStyle={{ padding: '16px' }}
|
||||||
className="!rounded-2xl"
|
className='!rounded-2xl'
|
||||||
>
|
>
|
||||||
{/* JSON错误提示 */}
|
{/* JSON错误提示 */}
|
||||||
{hasJsonError && (
|
{hasJsonError && (
|
||||||
<Banner
|
<Banner
|
||||||
type="danger"
|
type='danger'
|
||||||
description={`JSON 格式错误: ${jsonError}`}
|
description={`JSON 格式错误: ${jsonError}`}
|
||||||
className="mb-3"
|
className='mb-3'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -668,14 +700,12 @@ const JSONEditor = ({
|
|||||||
{/* 额外文本显示在卡片底部 */}
|
{/* 额外文本显示在卡片底部 */}
|
||||||
{extraText && (
|
{extraText && (
|
||||||
<Divider margin='12px' align='center'>
|
<Divider margin='12px' align='center'>
|
||||||
<Text type="tertiary" size="small">{extraText}</Text>
|
<Text type='tertiary' size='small'>
|
||||||
|
{extraText}
|
||||||
|
</Text>
|
||||||
</Divider>
|
</Divider>
|
||||||
)}
|
)}
|
||||||
{extraFooter && (
|
{extraFooter && <div className='mt-1'>{extraFooter}</div>}
|
||||||
<div className="mt-1">
|
|
||||||
{extraFooter}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Form.Slot>
|
</Form.Slot>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,13 +21,9 @@ import React from 'react';
|
|||||||
import { Spin } from '@douyinfe/semi-ui';
|
import { Spin } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
const Loading = ({ size = 'small' }) => {
|
const Loading = ({ size = 'small' }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
|
<div className='fixed inset-0 w-screen h-screen flex items-center justify-center'>
|
||||||
<Spin
|
<Spin size={size} spinning={true} />
|
||||||
size={size}
|
|
||||||
spinning={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
forwardRef
|
forwardRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +34,9 @@ import React, {
|
|||||||
* 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器
|
* 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const ScrollableContainer = forwardRef(({
|
const ScrollableContainer = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
children,
|
children,
|
||||||
maxHeight = '24rem',
|
maxHeight = '24rem',
|
||||||
className = '',
|
className = '',
|
||||||
@@ -46,7 +48,9 @@ const ScrollableContainer = forwardRef(({
|
|||||||
onScroll,
|
onScroll,
|
||||||
onScrollStateChange,
|
onScrollStateChange,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const debounceTimerRef = useRef(null);
|
const debounceTimerRef = useRef(null);
|
||||||
@@ -78,7 +82,9 @@ const ScrollableContainer = forwardRef(({
|
|||||||
|
|
||||||
const element = scrollRef.current;
|
const element = scrollRef.current;
|
||||||
const isScrollable = element.scrollHeight > element.clientHeight;
|
const isScrollable = element.scrollHeight > element.clientHeight;
|
||||||
const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold;
|
const isAtBottom =
|
||||||
|
element.scrollTop + element.clientHeight >=
|
||||||
|
element.scrollHeight - scrollThreshold;
|
||||||
const shouldShowHint = isScrollable && !isAtBottom;
|
const shouldShowHint = isScrollable && !isAtBottom;
|
||||||
|
|
||||||
setShowScrollHint(shouldShowHint);
|
setShowScrollHint(shouldShowHint);
|
||||||
@@ -90,24 +96,29 @@ const ScrollableContainer = forwardRef(({
|
|||||||
showScrollHint: shouldShowHint,
|
showScrollHint: shouldShowHint,
|
||||||
scrollTop: element.scrollTop,
|
scrollTop: element.scrollTop,
|
||||||
scrollHeight: element.scrollHeight,
|
scrollHeight: element.scrollHeight,
|
||||||
clientHeight: element.clientHeight
|
clientHeight: element.clientHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [scrollThreshold]);
|
}, [scrollThreshold]);
|
||||||
|
|
||||||
const debouncedCheckScrollable = useMemo(() =>
|
const debouncedCheckScrollable = useMemo(
|
||||||
debounce(checkScrollable, debounceDelay),
|
() => debounce(checkScrollable, debounceDelay),
|
||||||
[debounce, checkScrollable, debounceDelay]
|
[debounce, checkScrollable, debounceDelay],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleScroll = useCallback((e) => {
|
const handleScroll = useCallback(
|
||||||
|
(e) => {
|
||||||
debouncedCheckScrollable();
|
debouncedCheckScrollable();
|
||||||
if (onScrollRef.current) {
|
if (onScrollRef.current) {
|
||||||
onScrollRef.current(e);
|
onScrollRef.current(e);
|
||||||
}
|
}
|
||||||
}, [debouncedCheckScrollable]);
|
},
|
||||||
|
[debouncedCheckScrollable],
|
||||||
|
);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
checkScrollable: () => {
|
checkScrollable: () => {
|
||||||
checkScrollable();
|
checkScrollable();
|
||||||
},
|
},
|
||||||
@@ -129,10 +140,14 @@ const ScrollableContainer = forwardRef(({
|
|||||||
scrollHeight: element.scrollHeight,
|
scrollHeight: element.scrollHeight,
|
||||||
clientHeight: element.clientHeight,
|
clientHeight: element.clientHeight,
|
||||||
isScrollable: element.scrollHeight > element.clientHeight,
|
isScrollable: element.scrollHeight > element.clientHeight,
|
||||||
isAtBottom: element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold
|
isAtBottom:
|
||||||
|
element.scrollTop + element.clientHeight >=
|
||||||
|
element.scrollHeight - scrollThreshold,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
}), [checkScrollable, scrollThreshold]);
|
}),
|
||||||
|
[checkScrollable, scrollThreshold],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -154,7 +169,7 @@ const ScrollableContainer = forwardRef(({
|
|||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
characterData: true
|
characterData: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
@@ -185,13 +200,19 @@ const ScrollableContainer = forwardRef(({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const containerStyle = useMemo(() => ({
|
const containerStyle = useMemo(
|
||||||
maxHeight
|
() => ({
|
||||||
}), [maxHeight]);
|
maxHeight,
|
||||||
|
}),
|
||||||
|
[maxHeight],
|
||||||
|
);
|
||||||
|
|
||||||
const fadeIndicatorStyle = useMemo(() => ({
|
const fadeIndicatorStyle = useMemo(
|
||||||
opacity: showScrollHint ? 1 : 0
|
() => ({
|
||||||
}), [showScrollHint]);
|
opacity: showScrollHint ? 1 : 0,
|
||||||
|
}),
|
||||||
|
[showScrollHint],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -213,7 +234,8 @@ const ScrollableContainer = forwardRef(({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ScrollableContainer.displayName = 'ScrollableContainer';
|
ScrollableContainer.displayName = 'ScrollableContainer';
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,17 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
||||||
import { useContainerWidth } from '../../../hooks/common/useContainerWidth';
|
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';
|
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +57,7 @@ const SelectableButtonGroup = ({
|
|||||||
collapsible = true,
|
collapsible = true,
|
||||||
collapseHeight = 200,
|
collapseHeight = 200,
|
||||||
withCheckbox = false,
|
withCheckbox = false,
|
||||||
loading = false
|
loading = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [skeletonCount] = useState(12);
|
const [skeletonCount] = useState(12);
|
||||||
@@ -64,15 +74,13 @@ const SelectableButtonGroup = ({
|
|||||||
}, [text, containerWidth]);
|
}, [text, containerWidth]);
|
||||||
|
|
||||||
const textElement = (
|
const textElement = (
|
||||||
<span ref={textRef} className="sbg-ellipsis">
|
<span ref={textRef} className='sbg-ellipsis'>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return isOverflowing ? (
|
return isOverflowing ? (
|
||||||
<Tooltip content={text}>
|
<Tooltip content={text}>{textElement}</Tooltip>
|
||||||
{textElement}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
) : (
|
||||||
textElement
|
textElement
|
||||||
);
|
);
|
||||||
@@ -127,15 +135,12 @@ const SelectableButtonGroup = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderSkeletonButtons = () => {
|
const renderSkeletonButtons = () => {
|
||||||
|
|
||||||
const placeholder = (
|
const placeholder = (
|
||||||
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
|
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
|
||||||
{Array.from({ length: skeletonCount }).map((_, index) => (
|
{Array.from({ length: skeletonCount }).map((_, index) => (
|
||||||
<Col
|
<Col span={getColSpan()} key={index}>
|
||||||
span={getColSpan()}
|
<div
|
||||||
key={index}
|
style={{
|
||||||
>
|
|
||||||
<div style={{
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -144,8 +149,9 @@ const SelectableButtonGroup = ({
|
|||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
borderRadius: 'var(--semi-border-radius-medium)',
|
borderRadius: 'var(--semi-border-radius-medium)',
|
||||||
padding: '0 12px',
|
padding: '0 12px',
|
||||||
gap: '6px'
|
gap: '6px',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{withCheckbox && (
|
{withCheckbox && (
|
||||||
<Skeleton.Title active style={{ width: 14, height: 14 }} />
|
<Skeleton.Title active style={{ width: 14, height: 14 }} />
|
||||||
)}
|
)}
|
||||||
@@ -153,7 +159,7 @@ const SelectableButtonGroup = ({
|
|||||||
active
|
active
|
||||||
style={{
|
style={{
|
||||||
width: `${60 + (index % 3) * 20}px`,
|
width: `${60 + (index % 3) * 20}px`,
|
||||||
height: 14
|
height: 14,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,26 +173,29 @@ const SelectableButtonGroup = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentElement = showSkeleton ? renderSkeletonButtons() : (
|
const contentElement = showSkeleton ? (
|
||||||
|
renderSkeletonButtons()
|
||||||
|
) : (
|
||||||
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
|
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
|
||||||
{items.map((item) => {
|
{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)
|
const isActive = Array.isArray(activeValue)
|
||||||
? activeValue.includes(item.value)
|
? activeValue.includes(item.value)
|
||||||
: activeValue === item.value;
|
: activeValue === item.value;
|
||||||
|
|
||||||
if (withCheckbox) {
|
if (withCheckbox) {
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col span={getColSpan()} key={item.value}>
|
||||||
span={getColSpan()}
|
|
||||||
key={item.value}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => { /* disabled */ }}
|
onClick={() => {
|
||||||
|
/* disabled */
|
||||||
|
}}
|
||||||
theme={isActive ? 'light' : 'outline'}
|
theme={isActive ? 'light' : 'outline'}
|
||||||
type={isActive ? 'primary' : 'tertiary'}
|
type={isActive ? 'primary' : 'tertiary'}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className="sbg-button"
|
className='sbg-button'
|
||||||
icon={
|
icon={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isActive}
|
checked={isActive}
|
||||||
@@ -197,11 +206,18 @@ const SelectableButtonGroup = ({
|
|||||||
}
|
}
|
||||||
style={{ width: '100%', cursor: 'default' }}
|
style={{ width: '100%', cursor: 'default' }}
|
||||||
>
|
>
|
||||||
<div className="sbg-content">
|
<div className='sbg-content'>
|
||||||
{item.icon && (<span className="sbg-icon">{item.icon}</span>)}
|
{item.icon && <span className='sbg-icon'>{item.icon}</span>}
|
||||||
<ConditionalTooltipText text={item.label} />
|
<ConditionalTooltipText text={item.label} />
|
||||||
{item.tagCount !== undefined && shouldShowTags && (
|
{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>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -210,23 +226,27 @@ const SelectableButtonGroup = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col span={getColSpan()} key={item.value}>
|
||||||
span={getColSpan()}
|
|
||||||
key={item.value}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onChange(item.value)}
|
onClick={() => onChange(item.value)}
|
||||||
theme={isActive ? 'light' : 'outline'}
|
theme={isActive ? 'light' : 'outline'}
|
||||||
type={isActive ? 'primary' : 'tertiary'}
|
type={isActive ? 'primary' : 'tertiary'}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className="sbg-button"
|
className='sbg-button'
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<div className="sbg-content">
|
<div className='sbg-content'>
|
||||||
{item.icon && (<span className="sbg-icon">{item.icon}</span>)}
|
{item.icon && <span className='sbg-icon'>{item.icon}</span>}
|
||||||
<ConditionalTooltipText text={item.label} />
|
<ConditionalTooltipText text={item.label} />
|
||||||
{item.tagCount !== undefined && shouldShowTags && (
|
{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>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -237,9 +257,12 @@ const SelectableButtonGroup = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`} ref={containerRef}>
|
<div
|
||||||
|
className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<Divider margin="12px" align="left">
|
<Divider margin='12px' align='left'>
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
<Skeleton.Title active style={{ width: 80, height: 14 }} />
|
<Skeleton.Title active style={{ width: 80, height: 14 }} />
|
||||||
) : (
|
) : (
|
||||||
@@ -249,23 +272,30 @@ const SelectableButtonGroup = ({
|
|||||||
)}
|
)}
|
||||||
{needCollapse && !showSkeleton ? (
|
{needCollapse && !showSkeleton ? (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<Collapsible isOpen={isOpen} collapseHeight={collapseHeight} style={{ ...maskStyle }}>
|
<Collapsible
|
||||||
|
isOpen={isOpen}
|
||||||
|
collapseHeight={collapseHeight}
|
||||||
|
style={{ ...maskStyle }}
|
||||||
|
>
|
||||||
{contentElement}
|
{contentElement}
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
{isOpen ? null : (
|
{isOpen ? null : (
|
||||||
<div onClick={toggle} style={{ ...linkStyle }}>
|
<div onClick={toggle} style={{ ...linkStyle }}>
|
||||||
<IconChevronDown size="small" />
|
<IconChevronDown size='small' />
|
||||||
<span>{t('展开更多')}</span>
|
<span>{t('展开更多')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div onClick={toggle} style={{
|
<div
|
||||||
|
onClick={toggle}
|
||||||
|
style={{
|
||||||
...linkStyle,
|
...linkStyle,
|
||||||
position: 'static',
|
position: 'static',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
bottom: 'auto'
|
bottom: 'auto',
|
||||||
}}>
|
}}
|
||||||
<IconChevronUp size="small" />
|
>
|
||||||
|
<IconChevronUp size='small' />
|
||||||
<span>{t('收起')}</span>
|
<span>{t('收起')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import React from 'react';
|
|||||||
import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui';
|
import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui';
|
||||||
import { Bell } from 'lucide-react';
|
import { Bell } from 'lucide-react';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
import {
|
||||||
|
IllustrationConstruction,
|
||||||
|
IllustrationConstructionDark,
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
const AnnouncementsPanel = ({
|
const AnnouncementsPanel = ({
|
||||||
@@ -29,36 +32,43 @@ const AnnouncementsPanel = ({
|
|||||||
announcementLegendData,
|
announcementLegendData,
|
||||||
CARD_PROPS,
|
CARD_PROPS,
|
||||||
ILLUSTRATION_SIZE,
|
ILLUSTRATION_SIZE,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
{...CARD_PROPS}
|
{...CARD_PROPS}
|
||||||
className="shadow-sm !rounded-2xl lg:col-span-2"
|
className='shadow-sm !rounded-2xl lg:col-span-2'
|
||||||
title={
|
title={
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
|
<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 items-center gap-2'>
|
||||||
<Bell size={16} />
|
<Bell size={16} />
|
||||||
{t('系统公告')}
|
{t('系统公告')}
|
||||||
<Tag color="white" shape="circle">
|
<Tag color='white' shape='circle'>
|
||||||
{t('显示最新20条')}
|
{t('显示最新20条')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
{/* 图例 */}
|
{/* 图例 */}
|
||||||
<div className="flex flex-wrap gap-3 text-xs">
|
<div className='flex flex-wrap gap-3 text-xs'>
|
||||||
{announcementLegendData.map((legend, index) => (
|
{announcementLegendData.map((legend, index) => (
|
||||||
<div key={index} className="flex items-center gap-1">
|
<div key={index} className='flex items-center gap-1'>
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 rounded-full"
|
className='w-2 h-2 rounded-full'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
|
backgroundColor:
|
||||||
legend.color === 'blue' ? '#3b82f6' :
|
legend.color === 'grey'
|
||||||
legend.color === 'green' ? '#10b981' :
|
? '#8b9aa7'
|
||||||
legend.color === 'orange' ? '#f59e0b' :
|
: legend.color === 'blue'
|
||||||
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
|
? '#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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,9 +76,9 @@ const AnnouncementsPanel = ({
|
|||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<ScrollableContainer maxHeight="24rem">
|
<ScrollableContainer maxHeight='24rem'>
|
||||||
{announcementData.length > 0 ? (
|
{announcementData.length > 0 ? (
|
||||||
<Timeline mode="left">
|
<Timeline mode='left'>
|
||||||
{announcementData.map((item, idx) => {
|
{announcementData.map((item, idx) => {
|
||||||
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
|
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
|
||||||
return (
|
return (
|
||||||
@@ -76,16 +86,20 @@ const AnnouncementsPanel = ({
|
|||||||
key={idx}
|
key={idx}
|
||||||
type={item.type || 'default'}
|
type={item.type || 'default'}
|
||||||
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
|
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
|
||||||
extra={item.extra ? (
|
extra={
|
||||||
|
item.extra ? (
|
||||||
<div
|
<div
|
||||||
className="text-xs text-gray-500"
|
className='text-xs text-gray-500'
|
||||||
dangerouslySetInnerHTML={{ __html: htmlExtra }}
|
dangerouslySetInnerHTML={{ __html: htmlExtra }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked.parse(item.content || ''),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
@@ -93,10 +107,12 @@ const AnnouncementsPanel = ({
|
|||||||
})}
|
})}
|
||||||
</Timeline>
|
</Timeline>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center items-center py-8">
|
<div className='flex justify-center items-center py-8'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
darkModeImage={
|
||||||
|
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||||
|
}
|
||||||
title={t('暂无系统公告')}
|
title={t('暂无系统公告')}
|
||||||
description={t('请联系管理员在系统设置中配置公告信息')}
|
description={t('请联系管理员在系统设置中配置公告信息')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
|
import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
|
||||||
import { Server, Gauge, ExternalLink } from 'lucide-react';
|
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';
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
const ApiInfoPanel = ({
|
const ApiInfoPanel = ({
|
||||||
@@ -30,12 +33,12 @@ const ApiInfoPanel = ({
|
|||||||
CARD_PROPS,
|
CARD_PROPS,
|
||||||
FLEX_CENTER_GAP2,
|
FLEX_CENTER_GAP2,
|
||||||
ILLUSTRATION_SIZE,
|
ILLUSTRATION_SIZE,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
{...CARD_PROPS}
|
{...CARD_PROPS}
|
||||||
className="bg-gray-50 border-0 !rounded-2xl"
|
className='bg-gray-50 border-0 !rounded-2xl'
|
||||||
title={
|
title={
|
||||||
<div className={FLEX_CENTER_GAP2}>
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
<Server size={16} />
|
<Server size={16} />
|
||||||
@@ -44,66 +47,65 @@ const ApiInfoPanel = ({
|
|||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<ScrollableContainer maxHeight="24rem">
|
<ScrollableContainer maxHeight='24rem'>
|
||||||
{apiInfoData.length > 0 ? (
|
{apiInfoData.length > 0 ? (
|
||||||
apiInfoData.map((api) => (
|
apiInfoData.map((api) => (
|
||||||
<React.Fragment key={api.id}>
|
<React.Fragment key={api.id}>
|
||||||
<div className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
|
<div className='flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer'>
|
||||||
<div className="flex-shrink-0 mr-3">
|
<div className='flex-shrink-0 mr-3'>
|
||||||
<Avatar
|
<Avatar size='extra-small' color={api.color}>
|
||||||
size="extra-small"
|
|
||||||
color={api.color}
|
|
||||||
>
|
|
||||||
{api.route.substring(0, 2)}
|
{api.route.substring(0, 2)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className='flex-1'>
|
||||||
<div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
|
<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">
|
<span className='text-sm font-medium text-gray-900 !font-bold break-all'>
|
||||||
{api.route}
|
{api.route}
|
||||||
</span>
|
</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
|
<Tag
|
||||||
prefixIcon={<Gauge size={12} />}
|
prefixIcon={<Gauge size={12} />}
|
||||||
size="small"
|
size='small'
|
||||||
color="white"
|
color='white'
|
||||||
shape='circle'
|
shape='circle'
|
||||||
onClick={() => handleSpeedTest(api.url)}
|
onClick={() => handleSpeedTest(api.url)}
|
||||||
className="cursor-pointer hover:opacity-80 text-xs"
|
className='cursor-pointer hover:opacity-80 text-xs'
|
||||||
>
|
>
|
||||||
{t('测速')}
|
{t('测速')}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag
|
<Tag
|
||||||
prefixIcon={<ExternalLink size={12} />}
|
prefixIcon={<ExternalLink size={12} />}
|
||||||
size="small"
|
size='small'
|
||||||
color="white"
|
color='white'
|
||||||
shape='circle'
|
shape='circle'
|
||||||
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
|
onClick={() =>
|
||||||
className="cursor-pointer hover:opacity-80 text-xs"
|
window.open(api.url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
className='cursor-pointer hover:opacity-80 text-xs'
|
||||||
>
|
>
|
||||||
{t('跳转')}
|
{t('跳转')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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)}
|
onClick={() => handleCopyUrl(api.url)}
|
||||||
>
|
>
|
||||||
{api.url}
|
{api.url}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-500">
|
<div className='text-gray-500'>{api.description}</div>
|
||||||
{api.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center items-center py-8">
|
<div className='flex justify-center items-center py-8'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
darkModeImage={
|
||||||
|
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||||
|
}
|
||||||
title={t('暂无API信息')}
|
title={t('暂无API信息')}
|
||||||
description={t('请联系管理员在系统设置中配置API信息')}
|
description={t('请联系管理员在系统设置中配置API信息')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { PieChart } from 'lucide-react';
|
|||||||
import {
|
import {
|
||||||
IconHistogram,
|
IconHistogram,
|
||||||
IconPulse,
|
IconPulse,
|
||||||
IconPieChart2Stroked
|
IconPieChart2Stroked,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { VChart } from '@visactor/react-vchart';
|
import { VChart } from '@visactor/react-vchart';
|
||||||
|
|
||||||
@@ -38,76 +38,76 @@ const ChartsPanel = ({
|
|||||||
CHART_CONFIG,
|
CHART_CONFIG,
|
||||||
FLEX_CENTER_GAP2,
|
FLEX_CENTER_GAP2,
|
||||||
hasApiInfoPanel,
|
hasApiInfoPanel,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
{...CARD_PROPS}
|
{...CARD_PROPS}
|
||||||
className={`!rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
|
className={`!rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
|
||||||
title={
|
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}>
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
<PieChart size={16} />
|
<PieChart size={16} />
|
||||||
{t('模型数据分析')}
|
{t('模型数据分析')}
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
type="button"
|
type='button'
|
||||||
activeKey={activeChartTab}
|
activeKey={activeChartTab}
|
||||||
onChange={setActiveChartTab}
|
onChange={setActiveChartTab}
|
||||||
>
|
>
|
||||||
<TabPane tab={
|
<TabPane
|
||||||
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<IconHistogram />
|
<IconHistogram />
|
||||||
{t('消耗分布')}
|
{t('消耗分布')}
|
||||||
</span>
|
</span>
|
||||||
} itemKey="1" />
|
}
|
||||||
<TabPane tab={
|
itemKey='1'
|
||||||
|
/>
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<IconPulse />
|
<IconPulse />
|
||||||
{t('消耗趋势')}
|
{t('消耗趋势')}
|
||||||
</span>
|
</span>
|
||||||
} itemKey="2" />
|
}
|
||||||
<TabPane tab={
|
itemKey='2'
|
||||||
|
/>
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<IconPieChart2Stroked />
|
<IconPieChart2Stroked />
|
||||||
{t('调用次数分布')}
|
{t('调用次数分布')}
|
||||||
</span>
|
</span>
|
||||||
} itemKey="3" />
|
}
|
||||||
<TabPane tab={
|
itemKey='3'
|
||||||
|
/>
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<IconHistogram />
|
<IconHistogram />
|
||||||
{t('调用次数排行')}
|
{t('调用次数排行')}
|
||||||
</span>
|
</span>
|
||||||
} itemKey="4" />
|
}
|
||||||
|
itemKey='4'
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<div className="h-96 p-2">
|
<div className='h-96 p-2'>
|
||||||
{activeChartTab === '1' && (
|
{activeChartTab === '1' && (
|
||||||
<VChart
|
<VChart spec={spec_line} option={CHART_CONFIG} />
|
||||||
spec={spec_line}
|
|
||||||
option={CHART_CONFIG}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{activeChartTab === '2' && (
|
{activeChartTab === '2' && (
|
||||||
<VChart
|
<VChart spec={spec_model_line} option={CHART_CONFIG} />
|
||||||
spec={spec_model_line}
|
|
||||||
option={CHART_CONFIG}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{activeChartTab === '3' && (
|
{activeChartTab === '3' && (
|
||||||
<VChart
|
<VChart spec={spec_pie} option={CHART_CONFIG} />
|
||||||
spec={spec_pie}
|
|
||||||
option={CHART_CONFIG}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{activeChartTab === '4' && (
|
{activeChartTab === '4' && (
|
||||||
<VChart
|
<VChart spec={spec_rank_bar} option={CHART_CONFIG} />
|
||||||
spec={spec_rank_bar}
|
|
||||||
option={CHART_CONFIG}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -27,19 +27,19 @@ const DashboardHeader = ({
|
|||||||
showSearchModal,
|
showSearchModal,
|
||||||
refresh,
|
refresh,
|
||||||
loading,
|
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 (
|
return (
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className='flex items-center justify-between mb-4'>
|
||||||
<h2
|
<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 }}
|
style={{ opacity: greetingVisible ? 1 : 0 }}
|
||||||
>
|
>
|
||||||
{getGreeting}
|
{getGreeting}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex gap-3">
|
<div className='flex gap-3'>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
icon={<Search size={16} />}
|
icon={<Search size={16} />}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import { Card, Collapse, Empty } from '@douyinfe/semi-ui';
|
|||||||
import { HelpCircle } from 'lucide-react';
|
import { HelpCircle } from 'lucide-react';
|
||||||
import { IconPlus, IconMinus } from '@douyinfe/semi-icons';
|
import { IconPlus, IconMinus } from '@douyinfe/semi-icons';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
import {
|
||||||
|
IllustrationConstruction,
|
||||||
|
IllustrationConstructionDark,
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
const FaqPanel = ({
|
const FaqPanel = ({
|
||||||
@@ -30,12 +33,12 @@ const FaqPanel = ({
|
|||||||
CARD_PROPS,
|
CARD_PROPS,
|
||||||
FLEX_CENTER_GAP2,
|
FLEX_CENTER_GAP2,
|
||||||
ILLUSTRATION_SIZE,
|
ILLUSTRATION_SIZE,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
{...CARD_PROPS}
|
{...CARD_PROPS}
|
||||||
className="shadow-sm !rounded-2xl lg:col-span-1"
|
className='shadow-sm !rounded-2xl lg:col-span-1'
|
||||||
title={
|
title={
|
||||||
<div className={FLEX_CENTER_GAP2}>
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
<HelpCircle size={16} />
|
<HelpCircle size={16} />
|
||||||
@@ -44,7 +47,7 @@ const FaqPanel = ({
|
|||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<ScrollableContainer maxHeight="24rem">
|
<ScrollableContainer maxHeight='24rem'>
|
||||||
{faqData.length > 0 ? (
|
{faqData.length > 0 ? (
|
||||||
<Collapse
|
<Collapse
|
||||||
accordion
|
accordion
|
||||||
@@ -58,16 +61,20 @@ const FaqPanel = ({
|
|||||||
itemKey={index.toString()}
|
itemKey={index.toString()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked.parse(item.answer || ''),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
))}
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center items-center py-8">
|
<div className='flex justify-center items-center py-8'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
darkModeImage={
|
||||||
|
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||||
|
}
|
||||||
title={t('暂无常见问答')}
|
title={t('暂无常见问答')}
|
||||||
description={t('请联系管理员在系统设置中配置常见问答')}
|
description={t('请联系管理员在系统设置中配置常见问答')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ const StatsCards = ({
|
|||||||
loading,
|
loading,
|
||||||
getTrendSpec,
|
getTrendSpec,
|
||||||
CARD_PROPS,
|
CARD_PROPS,
|
||||||
CHART_CONFIG
|
CHART_CONFIG,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<div className='mb-4'>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||||
{groupedStatsData.map((group, idx) => (
|
{groupedStatsData.map((group, idx) => (
|
||||||
<Card
|
<Card
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -42,24 +42,24 @@ const StatsCards = ({
|
|||||||
className={`${group.color} border-0 !rounded-2xl w-full`}
|
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||||
title={group.title}
|
title={group.title}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
{group.items.map((item, itemIdx) => (
|
{group.items.map((item, itemIdx) => (
|
||||||
<div
|
<div
|
||||||
key={itemIdx}
|
key={itemIdx}
|
||||||
className="flex items-center justify-between cursor-pointer"
|
className='flex items-center justify-between cursor-pointer'
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<Avatar
|
<Avatar
|
||||||
className="mr-3"
|
className='mr-3'
|
||||||
size="small"
|
size='small'
|
||||||
color={item.avatarColor}
|
color={item.avatarColor}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-500">{item.title}</div>
|
<div className='text-xs text-gray-500'>{item.title}</div>
|
||||||
<div className="text-lg font-semibold">
|
<div className='text-lg font-semibold'>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
loading={loading}
|
loading={loading}
|
||||||
active
|
active
|
||||||
@@ -67,7 +67,11 @@ const StatsCards = ({
|
|||||||
<Skeleton.Paragraph
|
<Skeleton.Paragraph
|
||||||
active
|
active
|
||||||
rows={1}
|
rows={1}
|
||||||
style={{ width: '65px', height: '24px', marginTop: '4px' }}
|
style={{
|
||||||
|
width: '65px',
|
||||||
|
height: '24px',
|
||||||
|
marginTop: '4px',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -78,9 +82,9 @@ const StatsCards = ({
|
|||||||
</div>
|
</div>
|
||||||
{item.title === t('当前余额') ? (
|
{item.title === t('当前余额') ? (
|
||||||
<Tag
|
<Tag
|
||||||
color="white"
|
color='white'
|
||||||
shape='circle'
|
shape='circle'
|
||||||
size="large"
|
size='large'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate('/console/topup');
|
navigate('/console/topup');
|
||||||
@@ -88,13 +92,16 @@ const StatsCards = ({
|
|||||||
>
|
>
|
||||||
{t('充值')}
|
{t('充值')}
|
||||||
</Tag>
|
</Tag>
|
||||||
) : (loading || (item.trendData && item.trendData.length > 0)) && (
|
) : (
|
||||||
<div className="w-24 h-10">
|
(loading ||
|
||||||
|
(item.trendData && item.trendData.length > 0)) && (
|
||||||
|
<div className='w-24 h-10'>
|
||||||
<VChart
|
<VChart
|
||||||
spec={getTrendSpec(item.trendData, item.trendColor)}
|
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||||
option={CHART_CONFIG}
|
option={CHART_CONFIG}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -18,9 +18,20 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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 { 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';
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
const UptimePanel = ({
|
const UptimePanel = ({
|
||||||
@@ -33,15 +44,15 @@ const UptimePanel = ({
|
|||||||
renderMonitorList,
|
renderMonitorList,
|
||||||
CARD_PROPS,
|
CARD_PROPS,
|
||||||
ILLUSTRATION_SIZE,
|
ILLUSTRATION_SIZE,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
{...CARD_PROPS}
|
{...CARD_PROPS}
|
||||||
className="shadow-sm !rounded-2xl lg:col-span-1"
|
className='shadow-sm !rounded-2xl lg:col-span-1'
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center justify-between w-full gap-2">
|
<div className='flex items-center justify-between w-full gap-2'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Gauge size={16} />
|
<Gauge size={16} />
|
||||||
{t('服务可用性')}
|
{t('服务可用性')}
|
||||||
</div>
|
</div>
|
||||||
@@ -49,39 +60,43 @@ const UptimePanel = ({
|
|||||||
icon={<RefreshCw size={14} />}
|
icon={<RefreshCw size={14} />}
|
||||||
onClick={loadUptimeData}
|
onClick={loadUptimeData}
|
||||||
loading={uptimeLoading}
|
loading={uptimeLoading}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type='tertiary'
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="relative">
|
<div className='relative'>
|
||||||
<Spin spinning={uptimeLoading}>
|
<Spin spinning={uptimeLoading}>
|
||||||
{uptimeData.length > 0 ? (
|
{uptimeData.length > 0 ? (
|
||||||
uptimeData.length === 1 ? (
|
uptimeData.length === 1 ? (
|
||||||
<ScrollableContainer maxHeight="24rem">
|
<ScrollableContainer maxHeight='24rem'>
|
||||||
{renderMonitorList(uptimeData[0].monitors)}
|
{renderMonitorList(uptimeData[0].monitors)}
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
) : (
|
) : (
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type='card'
|
||||||
collapsible
|
collapsible
|
||||||
activeKey={activeUptimeTab}
|
activeKey={activeUptimeTab}
|
||||||
onChange={setActiveUptimeTab}
|
onChange={setActiveUptimeTab}
|
||||||
size="small"
|
size='small'
|
||||||
>
|
>
|
||||||
{uptimeData.map((group, groupIdx) => (
|
{uptimeData.map((group, groupIdx) => (
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<span className="flex items-center gap-2">
|
<span className='flex items-center gap-2'>
|
||||||
<Gauge size={14} />
|
<Gauge size={14} />
|
||||||
{group.categoryName}
|
{group.categoryName}
|
||||||
<Tag
|
<Tag
|
||||||
color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
|
color={
|
||||||
|
activeUptimeTab === group.categoryName
|
||||||
|
? 'red'
|
||||||
|
: 'grey'
|
||||||
|
}
|
||||||
size='small'
|
size='small'
|
||||||
shape='circle'
|
shape='circle'
|
||||||
>
|
>
|
||||||
@@ -92,7 +107,7 @@ const UptimePanel = ({
|
|||||||
itemKey={group.categoryName}
|
itemKey={group.categoryName}
|
||||||
key={groupIdx}
|
key={groupIdx}
|
||||||
>
|
>
|
||||||
<ScrollableContainer maxHeight="21.5rem">
|
<ScrollableContainer maxHeight='21.5rem'>
|
||||||
{renderMonitorList(group.monitors)}
|
{renderMonitorList(group.monitors)}
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
@@ -100,10 +115,12 @@ const UptimePanel = ({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center items-center py-8">
|
<div className='flex justify-center items-center py-8'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
darkModeImage={
|
||||||
|
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||||
|
}
|
||||||
title={t('暂无监控数据')}
|
title={t('暂无监控数据')}
|
||||||
description={t('请联系管理员在系统设置中配置Uptime')}
|
description={t('请联系管理员在系统设置中配置Uptime')}
|
||||||
/>
|
/>
|
||||||
@@ -114,15 +131,15 @@ const UptimePanel = ({
|
|||||||
|
|
||||||
{/* 图例 */}
|
{/* 图例 */}
|
||||||
{uptimeData.length > 0 && (
|
{uptimeData.length > 0 && (
|
||||||
<div className="p-3 bg-gray-50 rounded-b-2xl">
|
<div className='p-3 bg-gray-50 rounded-b-2xl'>
|
||||||
<div className="flex flex-wrap gap-3 text-xs justify-center">
|
<div className='flex flex-wrap gap-3 text-xs justify-center'>
|
||||||
{uptimeLegendData.map((legend, index) => (
|
{uptimeLegendData.map((legend, index) => (
|
||||||
<div key={index} className="flex items-center gap-1">
|
<div key={index} className='flex items-center gap-1'>
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 rounded-full"
|
className='w-2 h-2 rounded-full'
|
||||||
style={{ backgroundColor: legend.color }}
|
style={{ backgroundColor: legend.color }}
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-600">{legend.label}</span>
|
<span className='text-gray-600'>{legend.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import {
|
|||||||
FLEX_CENTER_GAP2,
|
FLEX_CENTER_GAP2,
|
||||||
ILLUSTRATION_SIZE,
|
ILLUSTRATION_SIZE,
|
||||||
ANNOUNCEMENT_LEGEND_DATA,
|
ANNOUNCEMENT_LEGEND_DATA,
|
||||||
UPTIME_STATUS_MAP
|
UPTIME_STATUS_MAP,
|
||||||
} from '../../constants/dashboard.constants';
|
} from '../../constants/dashboard.constants';
|
||||||
import {
|
import {
|
||||||
getTrendSpec,
|
getTrendSpec,
|
||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
handleSpeedTest,
|
handleSpeedTest,
|
||||||
getUptimeStatusColor,
|
getUptimeStatusColor,
|
||||||
getUptimeStatusText,
|
getUptimeStatusText,
|
||||||
renderMonitorList
|
renderMonitorList,
|
||||||
} from '../../helpers/dashboard';
|
} from '../../helpers/dashboard';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
@@ -70,7 +70,7 @@ const Dashboard = () => {
|
|||||||
dashboardData.setPieData,
|
dashboardData.setPieData,
|
||||||
dashboardData.setLineData,
|
dashboardData.setLineData,
|
||||||
dashboardData.setModelColors,
|
dashboardData.setModelColors,
|
||||||
dashboardData.t
|
dashboardData.t,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========== 统计数据 ==========
|
// ========== 统计数据 ==========
|
||||||
@@ -82,12 +82,12 @@ const Dashboard = () => {
|
|||||||
dashboardData.trendData,
|
dashboardData.trendData,
|
||||||
dashboardData.performanceMetrics,
|
dashboardData.performanceMetrics,
|
||||||
dashboardData.navigate,
|
dashboardData.navigate,
|
||||||
dashboardData.t
|
dashboardData.t,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========== 数据处理 ==========
|
// ========== 数据处理 ==========
|
||||||
const initChart = async () => {
|
const initChart = async () => {
|
||||||
await dashboardData.loadQuotaData().then(data => {
|
await dashboardData.loadQuotaData().then((data) => {
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
dashboardCharts.updateChartData(data);
|
dashboardCharts.updateChartData(data);
|
||||||
}
|
}
|
||||||
@@ -108,25 +108,30 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
// ========== 数据准备 ==========
|
// ========== 数据准备 ==========
|
||||||
const apiInfoData = statusState?.status?.api_info || [];
|
const apiInfoData = statusState?.status?.api_info || [];
|
||||||
const announcementData = (statusState?.status?.announcements || []).map(item => {
|
const announcementData = (statusState?.status?.announcements || []).map(
|
||||||
|
(item) => {
|
||||||
const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
|
const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
|
||||||
const absoluteTime = pubDate && !isNaN(pubDate.getTime())
|
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')}`
|
? `${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 || '');
|
: item?.publishDate || '';
|
||||||
const relativeTime = getRelativeTime(item.publishDate);
|
const relativeTime = getRelativeTime(item.publishDate);
|
||||||
return ({
|
return {
|
||||||
...item,
|
...item,
|
||||||
time: absoluteTime,
|
time: absoluteTime,
|
||||||
relative: relativeTime
|
relative: relativeTime,
|
||||||
});
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
const faqData = statusState?.status?.faq || [];
|
const faqData = statusState?.status?.faq || [];
|
||||||
|
|
||||||
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(([status, info]) => ({
|
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(
|
||||||
|
([status, info]) => ({
|
||||||
status: Number(status),
|
status: Number(status),
|
||||||
color: info.color,
|
color: info.color,
|
||||||
label: dashboardData.t(info.label)
|
label: dashboardData.t(info.label),
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// ========== Effects ==========
|
// ========== Effects ==========
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -134,7 +139,7 @@ const Dashboard = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className='h-full'>
|
||||||
<DashboardHeader
|
<DashboardHeader
|
||||||
getGreeting={dashboardData.getGreeting}
|
getGreeting={dashboardData.getGreeting}
|
||||||
greetingVisible={dashboardData.greetingVisible}
|
greetingVisible={dashboardData.greetingVisible}
|
||||||
@@ -166,8 +171,10 @@ const Dashboard = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* API信息和图表面板 */}
|
{/* API信息和图表面板 */}
|
||||||
<div className="mb-4">
|
<div className='mb-4'>
|
||||||
<div className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
|
<div
|
||||||
|
className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}
|
||||||
|
>
|
||||||
<ChartsPanel
|
<ChartsPanel
|
||||||
activeChartTab={dashboardData.activeChartTab}
|
activeChartTab={dashboardData.activeChartTab}
|
||||||
setActiveChartTab={dashboardData.setActiveChartTab}
|
setActiveChartTab={dashboardData.setActiveChartTab}
|
||||||
@@ -198,16 +205,18 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
{/* 系统公告和常见问答卡片 */}
|
{/* 系统公告和常见问答卡片 */}
|
||||||
{dashboardData.hasInfoPanels && (
|
{dashboardData.hasInfoPanels && (
|
||||||
<div className="mb-4">
|
<div className='mb-4'>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
<div className='grid grid-cols-1 lg:grid-cols-4 gap-4'>
|
||||||
{/* 公告卡片 */}
|
{/* 公告卡片 */}
|
||||||
{dashboardData.announcementsEnabled && (
|
{dashboardData.announcementsEnabled && (
|
||||||
<AnnouncementsPanel
|
<AnnouncementsPanel
|
||||||
announcementData={announcementData}
|
announcementData={announcementData}
|
||||||
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(item => ({
|
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(
|
||||||
|
(item) => ({
|
||||||
...item,
|
...item,
|
||||||
label: dashboardData.t(item.label)
|
label: dashboardData.t(item.label),
|
||||||
}))}
|
}),
|
||||||
|
)}
|
||||||
CARD_PROPS={CARD_PROPS}
|
CARD_PROPS={CARD_PROPS}
|
||||||
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||||
t={dashboardData.t}
|
t={dashboardData.t}
|
||||||
@@ -234,12 +243,19 @@ const Dashboard = () => {
|
|||||||
setActiveUptimeTab={dashboardData.setActiveUptimeTab}
|
setActiveUptimeTab={dashboardData.setActiveUptimeTab}
|
||||||
loadUptimeData={dashboardData.loadUptimeData}
|
loadUptimeData={dashboardData.loadUptimeData}
|
||||||
uptimeLegendData={uptimeLegendData}
|
uptimeLegendData={uptimeLegendData}
|
||||||
renderMonitorList={(monitors) => renderMonitorList(
|
renderMonitorList={(monitors) =>
|
||||||
|
renderMonitorList(
|
||||||
monitors,
|
monitors,
|
||||||
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
|
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
|
||||||
(status) => getUptimeStatusText(status, UPTIME_STATUS_MAP, dashboardData.t),
|
(status) =>
|
||||||
dashboardData.t
|
getUptimeStatusText(
|
||||||
)}
|
status,
|
||||||
|
UPTIME_STATUS_MAP,
|
||||||
|
dashboardData.t,
|
||||||
|
),
|
||||||
|
dashboardData.t,
|
||||||
|
)
|
||||||
|
}
|
||||||
CARD_PROPS={CARD_PROPS}
|
CARD_PROPS={CARD_PROPS}
|
||||||
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||||
t={dashboardData.t}
|
t={dashboardData.t}
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ const SearchModal = ({
|
|||||||
dataExportDefaultTime,
|
dataExportDefaultTime,
|
||||||
timeOptions,
|
timeOptions,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
|
|
||||||
const FORM_FIELD_PROPS = {
|
const FORM_FIELD_PROPS = {
|
||||||
className: "w-full mb-2 !rounded-lg",
|
className: 'w-full mb-2 !rounded-lg',
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFormField = (Component, props) => (
|
const createFormField = (Component, props) => (
|
||||||
@@ -54,7 +54,7 @@ const SearchModal = ({
|
|||||||
size={isMobile ? 'full-width' : 'small'}
|
size={isMobile ? 'full-width' : 'small'}
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<Form ref={formRef} layout='vertical' className="w-full">
|
<Form ref={formRef} layout='vertical' className='w-full'>
|
||||||
{createFormField(Form.DatePicker, {
|
{createFormField(Form.DatePicker, {
|
||||||
field: 'start_timestamp',
|
field: 'start_timestamp',
|
||||||
label: t('起始时间'),
|
label: t('起始时间'),
|
||||||
@@ -62,7 +62,7 @@ const SearchModal = ({
|
|||||||
value: start_timestamp,
|
value: start_timestamp,
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
name: 'start_timestamp',
|
name: 'start_timestamp',
|
||||||
onChange: (value) => handleInputChange(value, 'start_timestamp')
|
onChange: (value) => handleInputChange(value, 'start_timestamp'),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{createFormField(Form.DatePicker, {
|
{createFormField(Form.DatePicker, {
|
||||||
@@ -72,7 +72,7 @@ const SearchModal = ({
|
|||||||
value: end_timestamp,
|
value: end_timestamp,
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
name: 'end_timestamp',
|
name: 'end_timestamp',
|
||||||
onChange: (value) => handleInputChange(value, 'end_timestamp')
|
onChange: (value) => handleInputChange(value, 'end_timestamp'),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{createFormField(Form.Select, {
|
{createFormField(Form.Select, {
|
||||||
@@ -82,16 +82,18 @@ const SearchModal = ({
|
|||||||
placeholder: t('时间粒度'),
|
placeholder: t('时间粒度'),
|
||||||
name: 'data_export_default_time',
|
name: 'data_export_default_time',
|
||||||
optionList: timeOptions,
|
optionList: timeOptions,
|
||||||
onChange: (value) => handleInputChange(value, 'data_export_default_time')
|
onChange: (value) =>
|
||||||
|
handleInputChange(value, 'data_export_default_time'),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{isAdminUser && createFormField(Form.Input, {
|
{isAdminUser &&
|
||||||
|
createFormField(Form.Input, {
|
||||||
field: 'username',
|
field: 'username',
|
||||||
label: t('用户名称'),
|
label: t('用户名称'),
|
||||||
value: username,
|
value: username,
|
||||||
placeholder: t('可选值'),
|
placeholder: t('可选值'),
|
||||||
name: 'username',
|
name: 'username',
|
||||||
onChange: (value) => handleInputChange(value, 'username')
|
onChange: (value) => handleInputChange(value, 'username'),
|
||||||
})}
|
})}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -40,54 +40,140 @@ const FooterBar = () => {
|
|||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const customFooter = useMemo(() => (
|
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>
|
<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 md:hidden bottom-[20px] left-[-50px] w-[80px] h-[80px] rounded-full bg-[#FFD166] opacity-60"></div>
|
<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 && (
|
{isDemoSiteMode && (
|
||||||
<div className="flex flex-col md:flex-row justify-between w-full max-w-[1110px] mb-10 gap-8">
|
<div className='flex flex-col md:flex-row justify-between w-full max-w-[1110px] mb-10 gap-8'>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt={systemName}
|
alt={systemName}
|
||||||
className="w-16 h-16 rounded-full bg-gray-800 p-1.5 object-contain"
|
className='w-16 h-16 rounded-full bg-gray-800 p-1.5 object-contain'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 w-full">
|
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 w-full'>
|
||||||
<div className="text-left">
|
<div className='text-left'>
|
||||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('关于我们')}</p>
|
<p className='!text-semi-color-text-0 font-semibold mb-5'>
|
||||||
<div className="flex flex-col gap-4">
|
{t('关于我们')}
|
||||||
<a href="https://docs.newapi.pro/wiki/project-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('关于项目')}</a>
|
</p>
|
||||||
<a href="https://docs.newapi.pro/support/community-interaction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('联系我们')}</a>
|
<div className='flex flex-col gap-4'>
|
||||||
<a href="https://docs.newapi.pro/wiki/features-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('功能特性')}</a>
|
<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>
|
||||||
|
|
||||||
<div className="text-left">
|
<div className='text-left'>
|
||||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('文档')}</p>
|
<p className='!text-semi-color-text-0 font-semibold mb-5'>
|
||||||
<div className="flex flex-col gap-4">
|
{t('文档')}
|
||||||
<a href="https://docs.newapi.pro/getting-started/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('快速开始')}</a>
|
</p>
|
||||||
<a href="https://docs.newapi.pro/installation/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('安装指南')}</a>
|
<div className='flex flex-col gap-4'>
|
||||||
<a href="https://docs.newapi.pro/api/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">{t('API 文档')}</a>
|
<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>
|
||||||
|
|
||||||
<div className="text-left">
|
<div className='text-left'>
|
||||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('相关项目')}</p>
|
<p className='!text-semi-color-text-0 font-semibold mb-5'>
|
||||||
<div className="flex flex-col gap-4">
|
{t('相关项目')}
|
||||||
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">One API</a>
|
</p>
|
||||||
<a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">Midjourney-Proxy</a>
|
<div className='flex flex-col gap-4'>
|
||||||
<a href="https://github.com/Deeptrain-Community/chatnio" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">chatnio</a>
|
<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>
|
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>
|
||||||
|
|
||||||
<div className="text-left">
|
<div className='text-left'>
|
||||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('基于New API的项目')}</p>
|
<p className='!text-semi-color-text-0 font-semibold mb-5'>
|
||||||
<div className="flex flex-col gap-4">
|
{t('基于New API的项目')}
|
||||||
<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>
|
</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> */}
|
{/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1">VoAPI</a> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,30 +181,50 @@ const FooterBar = () => {
|
|||||||
</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-col md:flex-row items-center justify-between w-full max-w-[1110px] gap-6'>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<Typography.Text className="text-sm !text-semi-color-text-1">© {currentYear} {systemName}. {t('版权所有')}</Typography.Text>
|
<Typography.Text className='text-sm !text-semi-color-text-1'>
|
||||||
|
© {currentYear} {systemName}. {t('版权所有')}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm">
|
<div className='text-sm'>
|
||||||
<span className="!text-semi-color-text-1">{t('设计与开发由')} </span>
|
<span className='!text-semi-color-text-1'>
|
||||||
<a href="https://github.com/QuantumNous/new-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-primary font-medium">New API</a>
|
{t('设计与开发由')}{' '}
|
||||||
<span className="!text-semi-color-text-1"> & </span>
|
</span>
|
||||||
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-primary font-medium">One API</a>
|
<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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
), [logo, systemName, t, currentYear, isDemoSiteMode]);
|
),
|
||||||
|
[logo, systemName, t, currentYear, isDemoSiteMode],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFooter();
|
loadFooter();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className='w-full'>
|
||||||
{footer ? (
|
{footer ? (
|
||||||
<div
|
<div
|
||||||
className="custom-footer"
|
className='custom-footer'
|
||||||
dangerouslySetInnerHTML={{ __html: footer }}
|
dangerouslySetInnerHTML={{ __html: footer }}
|
||||||
></div>
|
></div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const ActionButtons = ({
|
|||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 md:gap-3">
|
<div className='flex items-center gap-2 md:gap-3'>
|
||||||
<NewYearButton isNewYear={isNewYear} />
|
<NewYearButton isNewYear={isNewYear} />
|
||||||
|
|
||||||
<NotificationButton
|
<NotificationButton
|
||||||
@@ -50,11 +50,7 @@ const ActionButtons = ({
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ThemeToggle
|
<ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
|
||||||
theme={theme}
|
|
||||||
onThemeToggle={onThemeToggle}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
currentLang={currentLang}
|
currentLang={currentLang}
|
||||||
|
|||||||
@@ -38,35 +38,35 @@ const HeaderLogo = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to="/" className="group flex items-center gap-2">
|
<Link to='/' className='group flex items-center gap-2'>
|
||||||
<div className="relative w-8 h-8 md:w-8 md:h-8">
|
<div className='relative w-8 h-8 md:w-8 md:h-8'>
|
||||||
<SkeletonWrapper
|
<SkeletonWrapper loading={isLoading || !logoLoaded} type='image' />
|
||||||
loading={isLoading || !logoLoaded}
|
|
||||||
type="image"
|
|
||||||
/>
|
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="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'}`}
|
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>
|
||||||
<div className="hidden md:flex items-center gap-2">
|
<div className='hidden md:flex items-center gap-2'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<SkeletonWrapper
|
<SkeletonWrapper
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
type="title"
|
type='title'
|
||||||
width={120}
|
width={120}
|
||||||
height={24}
|
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}
|
{systemName}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</SkeletonWrapper>
|
</SkeletonWrapper>
|
||||||
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
|
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
|
||||||
<Tag
|
<Tag
|
||||||
color={isSelfUseMode ? 'purple' : 'blue'}
|
color={isSelfUseMode ? 'purple' : 'blue'}
|
||||||
className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
|
className='text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm'
|
||||||
size="small"
|
size='small'
|
||||||
shape='circle'
|
shape='circle'
|
||||||
>
|
>
|
||||||
{isSelfUseMode ? t('自用模式') : t('演示站点')}
|
{isSelfUseMode ? t('自用模式') : t('演示站点')}
|
||||||
|
|||||||
@@ -25,21 +25,21 @@ import { CN, GB } from 'country-flag-icons/react/3x2';
|
|||||||
const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
position="bottomRight"
|
position='bottomRight'
|
||||||
render={
|
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
|
<Dropdown.Item
|
||||||
onClick={() => onLanguageChange('zh')}
|
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'}`}
|
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>
|
<span>中文</span>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => onLanguageChange('en')}
|
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'}`}
|
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>
|
<span>English</span>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
@@ -48,9 +48,9 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
|||||||
<Button
|
<Button
|
||||||
icon={<Languages size={18} />}
|
icon={<Languages size={18} />}
|
||||||
aria-label={t('切换语言')}
|
aria-label={t('切换语言')}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
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"
|
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>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,13 +36,19 @@ const MobileMenuButton = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={
|
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}
|
onClick={onToggle}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
className="!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700"
|
className='!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,21 +21,16 @@ import React from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import SkeletonWrapper from './SkeletonWrapper';
|
import SkeletonWrapper from './SkeletonWrapper';
|
||||||
|
|
||||||
const Navigation = ({
|
const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => {
|
||||||
mainNavLinks,
|
|
||||||
isMobile,
|
|
||||||
isLoading,
|
|
||||||
userState
|
|
||||||
}) => {
|
|
||||||
const renderNavLinks = () => {
|
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 hoverClasses = 'hover:text-semi-color-primary';
|
||||||
const spacingClasses = isMobile ? 'p-1' : 'p-2';
|
const spacingClasses = isMobile ? 'p-1' : 'p-2';
|
||||||
|
|
||||||
const commonLinkClasses = `${baseClasses} ${spacingClasses} ${hoverClasses}`;
|
const commonLinkClasses = `${baseClasses} ${spacingClasses} ${hoverClasses}`;
|
||||||
|
|
||||||
return mainNavLinks.map((link) => {
|
return mainNavLinks.map((link) => {
|
||||||
|
|
||||||
const linkContent = <span>{link.text}</span>;
|
const linkContent = <span>{link.text}</span>;
|
||||||
|
|
||||||
if (link.isExternal) {
|
if (link.isExternal) {
|
||||||
@@ -58,11 +53,7 @@ const Navigation = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link key={link.itemKey} to={targetPath} className={commonLinkClasses}>
|
||||||
key={link.itemKey}
|
|
||||||
to={targetPath}
|
|
||||||
className={commonLinkClasses}
|
|
||||||
>
|
|
||||||
{linkContent}
|
{linkContent}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -70,10 +61,10 @@ const Navigation = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<SkeletonWrapper
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
type="navigation"
|
type='navigation'
|
||||||
count={4}
|
count={4}
|
||||||
width={60}
|
width={60}
|
||||||
height={16}
|
height={16}
|
||||||
|
|||||||
@@ -36,21 +36,24 @@ const NewYearButton = ({ isNewYear }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
position="bottomRight"
|
position='bottomRight'
|
||||||
render={
|
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={handleNewYearClick} className="!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-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!!! 🎉
|
Happy New Year!!! 🎉
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
icon={<span className="text-xl">🎉</span>}
|
icon={<span className='text-xl'>🎉</span>}
|
||||||
aria-label="New Year"
|
aria-label='New Year'
|
||||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full"
|
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full'
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,14 +26,15 @@ const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
|
|||||||
icon: <Bell size={18} />,
|
icon: <Bell size={18} />,
|
||||||
'aria-label': t('系统公告'),
|
'aria-label': t('系统公告'),
|
||||||
onClick: onNoticeOpen,
|
onClick: onNoticeOpen,
|
||||||
theme: "borderless",
|
theme: 'borderless',
|
||||||
type: "tertiary",
|
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",
|
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) {
|
if (unreadCount > 0) {
|
||||||
return (
|
return (
|
||||||
<Badge count={unreadCount} type="danger" overflowCount={99}>
|
<Badge count={unreadCount} type='danger' overflowCount={99}>
|
||||||
<Button {...buttonProps} />
|
<Button {...buttonProps} />
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,13 +62,17 @@ const SkeletonWrapper = ({
|
|||||||
// 用户区域骨架屏 (头像 + 文本)
|
// 用户区域骨架屏 (头像 + 文本)
|
||||||
const renderUserAreaSkeleton = () => {
|
const renderUserAreaSkeleton = () => {
|
||||||
return (
|
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
|
<Skeleton
|
||||||
loading={true}
|
loading={true}
|
||||||
active
|
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
|
<Skeleton
|
||||||
loading={true}
|
loading={true}
|
||||||
active
|
active
|
||||||
@@ -107,12 +111,7 @@ const SkeletonWrapper = ({
|
|||||||
<Skeleton
|
<Skeleton
|
||||||
loading={true}
|
loading={true}
|
||||||
active
|
active
|
||||||
placeholder={
|
placeholder={<Skeleton.Title active style={{ width, height: 24 }} />}
|
||||||
<Skeleton.Title
|
|
||||||
active
|
|
||||||
style={{ width, height: 24 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -124,12 +123,7 @@ const SkeletonWrapper = ({
|
|||||||
<Skeleton
|
<Skeleton
|
||||||
loading={true}
|
loading={true}
|
||||||
active
|
active
|
||||||
placeholder={
|
placeholder={<Skeleton.Title active style={{ width, height }} />}
|
||||||
<Skeleton.Title
|
|
||||||
active
|
|
||||||
style={{ width, height }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,29 +25,32 @@ import { useActualTheme } from '../../../context/Theme';
|
|||||||
const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
||||||
const actualTheme = useActualTheme();
|
const actualTheme = useActualTheme();
|
||||||
|
|
||||||
const themeOptions = useMemo(() => ([
|
const themeOptions = useMemo(
|
||||||
|
() => [
|
||||||
{
|
{
|
||||||
key: 'light',
|
key: 'light',
|
||||||
icon: <Sun size={18} />,
|
icon: <Sun size={18} />,
|
||||||
buttonIcon: <Sun size={18} />,
|
buttonIcon: <Sun size={18} />,
|
||||||
label: t('浅色模式'),
|
label: t('浅色模式'),
|
||||||
description: t('始终使用浅色主题')
|
description: t('始终使用浅色主题'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'dark',
|
key: 'dark',
|
||||||
icon: <Moon size={18} />,
|
icon: <Moon size={18} />,
|
||||||
buttonIcon: <Moon size={18} />,
|
buttonIcon: <Moon size={18} />,
|
||||||
label: t('深色模式'),
|
label: t('深色模式'),
|
||||||
description: t('始终使用深色主题')
|
description: t('始终使用深色主题'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'auto',
|
key: 'auto',
|
||||||
icon: <Monitor size={18} />,
|
icon: <Monitor size={18} />,
|
||||||
buttonIcon: <Monitor size={18} />,
|
buttonIcon: <Monitor size={18} />,
|
||||||
label: t('自动模式'),
|
label: t('自动模式'),
|
||||||
description: t('跟随系统主题设置')
|
description: t('跟随系统主题设置'),
|
||||||
}
|
},
|
||||||
]), [t]);
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const getItemClassName = (isSelected) =>
|
const getItemClassName = (isSelected) =>
|
||||||
isSelected
|
isSelected
|
||||||
@@ -55,13 +58,13 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
|||||||
: 'hover:!bg-semi-color-fill-1';
|
: 'hover:!bg-semi-color-fill-1';
|
||||||
|
|
||||||
const currentButtonIcon = useMemo(() => {
|
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;
|
return currentOption?.buttonIcon || themeOptions[2].buttonIcon;
|
||||||
}, [theme, themeOptions]);
|
}, [theme, themeOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
position="bottomRight"
|
position='bottomRight'
|
||||||
render={
|
render={
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
{themeOptions.map((option) => (
|
{themeOptions.map((option) => (
|
||||||
@@ -71,9 +74,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
|||||||
onClick={() => onThemeToggle(option.key)}
|
onClick={() => onThemeToggle(option.key)}
|
||||||
className={getItemClassName(theme === option.key)}
|
className={getItemClassName(theme === option.key)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
<span className="text-xs text-semi-color-text-2">
|
<span className='text-xs text-semi-color-text-2'>
|
||||||
{option.description}
|
{option.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,8 +86,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
|||||||
{theme === 'auto' && (
|
{theme === 'auto' && (
|
||||||
<>
|
<>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<div className="px-3 py-2 text-xs text-semi-color-text-2">
|
<div className='px-3 py-2 text-xs text-semi-color-text-2'>
|
||||||
{t('当前跟随系统')}:{actualTheme === 'dark' ? t('深色') : t('浅色')}
|
{t('当前跟随系统')}:
|
||||||
|
{actualTheme === 'dark' ? t('深色') : t('浅色')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -94,9 +98,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
|||||||
<Button
|
<Button
|
||||||
icon={currentButtonIcon}
|
icon={currentButtonIcon}
|
||||||
aria-label={t('切换主题')}
|
aria-label={t('切换主题')}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
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"
|
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>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
Typography,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
IconExit,
|
IconExit,
|
||||||
@@ -48,7 +43,7 @@ const UserArea = ({
|
|||||||
return (
|
return (
|
||||||
<SkeletonWrapper
|
<SkeletonWrapper
|
||||||
loading={true}
|
loading={true}
|
||||||
type="userArea"
|
type='userArea'
|
||||||
width={50}
|
width={50}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
@@ -58,17 +53,20 @@ const UserArea = ({
|
|||||||
if (userState.user) {
|
if (userState.user) {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
position="bottomRight"
|
position='bottomRight'
|
||||||
render={
|
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
|
<Dropdown.Item
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/console/personal');
|
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">
|
<div className='flex items-center gap-2'>
|
||||||
<IconUserSetting size="small" className="text-gray-500 dark:text-gray-400" />
|
<IconUserSetting
|
||||||
|
size='small'
|
||||||
|
className='text-gray-500 dark:text-gray-400'
|
||||||
|
/>
|
||||||
<span>{t('个人设置')}</span>
|
<span>{t('个人设置')}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
@@ -76,10 +74,13 @@ const UserArea = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/console/token');
|
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">
|
<div className='flex items-center gap-2'>
|
||||||
<IconKey size="small" className="text-gray-500 dark:text-gray-400" />
|
<IconKey
|
||||||
|
size='small'
|
||||||
|
className='text-gray-500 dark:text-gray-400'
|
||||||
|
/>
|
||||||
<span>{t('令牌管理')}</span>
|
<span>{t('令牌管理')}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
@@ -87,16 +88,25 @@ const UserArea = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/console/topup');
|
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">
|
<div className='flex items-center gap-2'>
|
||||||
<IconCreditCard size="small" className="text-gray-500 dark:text-gray-400" />
|
<IconCreditCard
|
||||||
|
size='small'
|
||||||
|
className='text-gray-500 dark:text-gray-400'
|
||||||
|
/>
|
||||||
<span>{t('钱包管理')}</span>
|
<span>{t('钱包管理')}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown.Item>
|
</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">
|
<Dropdown.Item
|
||||||
<div className="flex items-center gap-2">
|
onClick={logout}
|
||||||
<IconExit size="small" className="text-gray-500 dark:text-gray-400" />
|
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>
|
<span>{t('退出')}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
@@ -104,74 +114,76 @@ const UserArea = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
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"
|
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
|
<Avatar
|
||||||
size="extra-small"
|
size='extra-small'
|
||||||
color={stringToColor(userState.user.username)}
|
color={stringToColor(userState.user.username)}
|
||||||
className="mr-1"
|
className='mr-1'
|
||||||
>
|
>
|
||||||
{userState.user.username[0].toUpperCase()}
|
{userState.user.username[0].toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span className="hidden md:inline">
|
<span className='hidden md:inline'>
|
||||||
<Typography.Text className="!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1">
|
<Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
|
||||||
{userState.user.username}
|
{userState.user.username}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</span>
|
</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>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const showRegisterButton = !isSelfUseMode;
|
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 loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`;
|
||||||
|
|
||||||
let registerButtonClasses = `${commonSizingAndLayoutClass}`;
|
let registerButtonClasses = `${commonSizingAndLayoutClass}`;
|
||||||
|
|
||||||
const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5";
|
const loginButtonTextSpanClass =
|
||||||
const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
|
'!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 (showRegisterButton) {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
loginButtonClasses += " !rounded-full";
|
loginButtonClasses += ' !rounded-full';
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
loginButtonClasses += " !rounded-full";
|
loginButtonClasses += ' !rounded-full';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<Link to="/login" className="flex">
|
<Link to='/login' className='flex'>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
className={loginButtonClasses}
|
className={loginButtonClasses}
|
||||||
>
|
>
|
||||||
<span className={loginButtonTextSpanClass}>
|
<span className={loginButtonTextSpanClass}>{t('登录')}</span>
|
||||||
{t('登录')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{showRegisterButton && (
|
{showRegisterButton && (
|
||||||
<div className="hidden md:block">
|
<div className='hidden md:block'>
|
||||||
<Link to="/register" className="flex -ml-px">
|
<Link to='/register' className='flex -ml-px'>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
type="primary"
|
type='primary'
|
||||||
className={registerButtonClasses}
|
className={registerButtonClasses}
|
||||||
>
|
>
|
||||||
<span className={registerButtonTextSpanClass}>
|
<span className={registerButtonTextSpanClass}>{t('注册')}</span>
|
||||||
{t('注册')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
const { mainNavLinks } = useNavigation(t, docsLink);
|
const { mainNavLinks } = useNavigation(t, docsLink);
|
||||||
|
|
||||||
return (
|
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
|
<NoticeModal
|
||||||
visible={noticeVisible}
|
visible={noticeVisible}
|
||||||
onClose={handleNoticeClose}
|
onClose={handleNoticeClose}
|
||||||
@@ -72,9 +72,9 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
unreadKeys={getUnreadKeys()}
|
unreadKeys={getUnreadKeys()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full px-2">
|
<div className='w-full px-2'>
|
||||||
<div className="flex items-center justify-between h-16">
|
<div className='flex items-center justify-between h-16'>
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<MobileMenuButton
|
<MobileMenuButton
|
||||||
isConsoleRoute={isConsoleRoute}
|
isConsoleRoute={isConsoleRoute}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
|||||||
@@ -18,15 +18,31 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useContext, useMemo } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { API, showError, getRelativeTime } from '../../helpers';
|
import { API, showError, getRelativeTime } from '../../helpers';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
import {
|
||||||
|
IllustrationNoContent,
|
||||||
|
IllustrationNoContentDark,
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
import { Bell, Megaphone } from 'lucide-react';
|
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 { t } = useTranslation();
|
||||||
const [noticeContent, setNoticeContent] = useState('');
|
const [noticeContent, setNoticeContent] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
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 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(() => {
|
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 pubDate = item?.publishDate ? new Date(item.publishDate) : null;
|
||||||
const absoluteTime = pubDate && !isNaN(pubDate.getTime())
|
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')}`
|
? `${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 || '');
|
: item?.publishDate || '';
|
||||||
return ({
|
return {
|
||||||
key: getKeyForItem(item),
|
key: getKeyForItem(item),
|
||||||
type: item.type || 'default',
|
type: item.type || 'default',
|
||||||
time: absoluteTime,
|
time: absoluteTime,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
extra: item.extra,
|
extra: item.extra,
|
||||||
relative: getRelativeTime(item.publishDate),
|
relative: getRelativeTime(item.publishDate),
|
||||||
isUnread: unreadSet.has(getKeyForItem(item))
|
isUnread: unreadSet.has(getKeyForItem(item)),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
}, [announcements, unreadSet]);
|
}, [announcements, unreadSet]);
|
||||||
|
|
||||||
@@ -100,15 +118,23 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
|
|||||||
|
|
||||||
const renderMarkdownNotice = () => {
|
const renderMarkdownNotice = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
return (
|
||||||
|
<div className='py-12'>
|
||||||
|
<Empty description={t('加载中...')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!noticeContent) {
|
if (!noticeContent) {
|
||||||
return (
|
return (
|
||||||
<div className="py-12">
|
<div className='py-12'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
image={
|
||||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
<IllustrationNoContent style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoContentDark style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
description={t('暂无公告')}
|
description={t('暂无公告')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +144,7 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
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 = () => {
|
const renderAnnouncementTimeline = () => {
|
||||||
if (processedAnnouncements.length === 0) {
|
if (processedAnnouncements.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="py-12">
|
<div className='py-12'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
image={
|
||||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
<IllustrationNoContent style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoContentDark style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
description={t('暂无系统公告')}
|
description={t('暂无系统公告')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,8 +167,8 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
|
<div className='max-h-[55vh] overflow-y-auto pr-2 card-content-scroll'>
|
||||||
<Timeline mode="left">
|
<Timeline mode='left'>
|
||||||
{processedAnnouncements.map((item, idx) => {
|
{processedAnnouncements.map((item, idx) => {
|
||||||
const htmlContent = marked.parse(item.content || '');
|
const htmlContent = marked.parse(item.content || '');
|
||||||
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
|
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
|
||||||
@@ -147,12 +177,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
|
|||||||
key={idx}
|
key={idx}
|
||||||
type={item.type}
|
type={item.type}
|
||||||
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
|
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
|
||||||
extra={item.extra ? (
|
extra={
|
||||||
|
item.extra ? (
|
||||||
<div
|
<div
|
||||||
className="text-xs text-gray-500"
|
className='text-xs text-gray-500'
|
||||||
dangerouslySetInnerHTML={{ __html: htmlExtra }}
|
dangerouslySetInnerHTML={{ __html: htmlExtra }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null
|
||||||
|
}
|
||||||
className={item.isUnread ? '' : ''}
|
className={item.isUnread ? '' : ''}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -179,26 +211,40 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className='flex items-center justify-between w-full'>
|
||||||
<span>{t('系统公告')}</span>
|
<span>{t('系统公告')}</span>
|
||||||
<Tabs
|
<Tabs activeKey={activeTab} onChange={setActiveTab} type='button'>
|
||||||
activeKey={activeTab}
|
<TabPane
|
||||||
onChange={setActiveTab}
|
tab={
|
||||||
type='button'
|
<span className='flex items-center gap-1'>
|
||||||
>
|
<Bell size={14} /> {t('通知')}
|
||||||
<TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' />
|
</span>
|
||||||
<TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' />
|
}
|
||||||
|
itemKey='inApp'
|
||||||
|
/>
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<Megaphone size={14} /> {t('系统公告')}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
itemKey='system'
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={(
|
footer={
|
||||||
<div className="flex justify-end">
|
<div className='flex justify-end'>
|
||||||
<Button type='secondary' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
|
<Button type='secondary' onClick={handleCloseTodayNotice}>
|
||||||
<Button type="primary" onClick={onClose}>{t('关闭公告')}</Button>
|
{t('今日关闭')}
|
||||||
|
</Button>
|
||||||
|
<Button type='primary' onClick={onClose}>
|
||||||
|
{t('关闭公告')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
size={isMobile ? 'full-width' : 'large'}
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
>
|
>
|
||||||
{renderBody()}
|
{renderBody()}
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||||||
import { useIsMobile } from '../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../hooks/common/useIsMobile';
|
||||||
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
|
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { UserContext } from '../../context/User';
|
||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -42,9 +48,12 @@ const PageLayout = () => {
|
|||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const location = useLocation();
|
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.startsWith('/console/chat') &&
|
||||||
location.pathname !== '/console/playground';
|
location.pathname !== '/console/playground';
|
||||||
|
|
||||||
@@ -120,7 +129,10 @@ const PageLayout = () => {
|
|||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HeaderBar onMobileMenuToggle={() => setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
|
<HeaderBar
|
||||||
|
onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
|
||||||
|
drawerOpen={drawerOpen}
|
||||||
|
/>
|
||||||
</Header>
|
</Header>
|
||||||
<Layout
|
<Layout
|
||||||
style={{
|
style={{
|
||||||
@@ -142,12 +154,20 @@ const PageLayout = () => {
|
|||||||
width: 'var(--sidebar-current-width)',
|
width: 'var(--sidebar-current-width)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SiderBar onNavigate={() => { if (isMobile) setDrawerOpen(false); }} />
|
<SiderBar
|
||||||
|
onNavigate={() => {
|
||||||
|
if (isMobile) setDrawerOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
)}
|
)}
|
||||||
<Layout
|
<Layout
|
||||||
style={{
|
style={{
|
||||||
marginLeft: isMobile ? '0' : showSider ? 'var(--sidebar-current-width)' : '0',
|
marginLeft: isMobile
|
||||||
|
? '0'
|
||||||
|
: showSider
|
||||||
|
? 'var(--sidebar-current-width)'
|
||||||
|
: '0',
|
||||||
flex: '1 1 auto',
|
flex: '1 1 auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ const SetupCheck = ({ children }) => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusState?.status?.setup === false && location.pathname !== '/setup') {
|
if (
|
||||||
|
statusState?.status?.setup === false &&
|
||||||
|
location.pathname !== '/setup'
|
||||||
|
) {
|
||||||
window.location.href = '/setup';
|
window.location.href = '/setup';
|
||||||
}
|
}
|
||||||
}, [statusState?.status?.setup, location.pathname]);
|
}, [statusState?.status?.setup, location.pathname]);
|
||||||
|
|||||||
@@ -23,17 +23,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { getLucideIcon } from '../../helpers/render';
|
import { getLucideIcon } from '../../helpers/render';
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
|
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
|
||||||
import {
|
import { isAdmin, isRoot, showError } from '../../helpers';
|
||||||
isAdmin,
|
|
||||||
isRoot,
|
|
||||||
showError
|
|
||||||
} from '../../helpers';
|
|
||||||
|
|
||||||
import {
|
import { Nav, Divider, Button } from '@douyinfe/semi-ui';
|
||||||
Nav,
|
|
||||||
Divider,
|
|
||||||
Button,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
|
|
||||||
const routerMap = {
|
const routerMap = {
|
||||||
home: '/',
|
home: '/',
|
||||||
@@ -275,14 +267,17 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
key={item.itemKey}
|
key={item.itemKey}
|
||||||
itemKey={item.itemKey}
|
itemKey={item.itemKey}
|
||||||
text={
|
text={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<span className="truncate font-medium text-sm" style={{ color: textColor }}>
|
<span
|
||||||
|
className='truncate font-medium text-sm'
|
||||||
|
style={{ color: textColor }}
|
||||||
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
icon={
|
icon={
|
||||||
<div className="sidebar-icon-container flex-shrink-0">
|
<div className='sidebar-icon-container flex-shrink-0'>
|
||||||
{getLucideIcon(item.itemKey, isSelected)}
|
{getLucideIcon(item.itemKey, isSelected)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -302,14 +297,17 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
key={item.itemKey}
|
key={item.itemKey}
|
||||||
itemKey={item.itemKey}
|
itemKey={item.itemKey}
|
||||||
text={
|
text={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<span className="truncate font-medium text-sm" style={{ color: textColor }}>
|
<span
|
||||||
|
className='truncate font-medium text-sm'
|
||||||
|
style={{ color: textColor }}
|
||||||
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
icon={
|
icon={
|
||||||
<div className="sidebar-icon-container flex-shrink-0">
|
<div className='sidebar-icon-container flex-shrink-0'>
|
||||||
{getLucideIcon(item.itemKey, isSelected)}
|
{getLucideIcon(item.itemKey, isSelected)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -323,7 +321,10 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
key={subItem.itemKey}
|
key={subItem.itemKey}
|
||||||
itemKey={subItem.itemKey}
|
itemKey={subItem.itemKey}
|
||||||
text={
|
text={
|
||||||
<span className="truncate font-medium text-sm" style={{ color: subTextColor }}>
|
<span
|
||||||
|
className='truncate font-medium text-sm'
|
||||||
|
style={{ color: subTextColor }}
|
||||||
|
>
|
||||||
{subItem.text}
|
{subItem.text}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -339,18 +340,18 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="sidebar-container"
|
className='sidebar-container'
|
||||||
style={{ width: 'var(--sidebar-current-width)' }}
|
style={{ width: 'var(--sidebar-current-width)' }}
|
||||||
>
|
>
|
||||||
<Nav
|
<Nav
|
||||||
className="sidebar-nav"
|
className='sidebar-nav'
|
||||||
defaultIsCollapsed={collapsed}
|
defaultIsCollapsed={collapsed}
|
||||||
isCollapsed={collapsed}
|
isCollapsed={collapsed}
|
||||||
onCollapseChange={toggleCollapsed}
|
onCollapseChange={toggleCollapsed}
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
itemStyle="sidebar-nav-item"
|
itemStyle='sidebar-nav-item'
|
||||||
hoverStyle="sidebar-nav-item:hover"
|
hoverStyle='sidebar-nav-item:hover'
|
||||||
selectedStyle="sidebar-nav-item-selected"
|
selectedStyle='sidebar-nav-item-selected'
|
||||||
renderWrapper={({ itemElement, props }) => {
|
renderWrapper={({ itemElement, props }) => {
|
||||||
const to = routerMapState[props.itemKey] || routerMap[props.itemKey];
|
const to = routerMapState[props.itemKey] || routerMap[props.itemKey];
|
||||||
|
|
||||||
@@ -381,27 +382,25 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 聊天区域 */}
|
{/* 聊天区域 */}
|
||||||
<div className="sidebar-section">
|
<div className='sidebar-section'>
|
||||||
{!collapsed && (
|
{!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>}
|
||||||
<div className="sidebar-group-label">{t('聊天')}</div>
|
|
||||||
)}
|
|
||||||
{chatMenuItems.map((item) => renderSubItem(item))}
|
{chatMenuItems.map((item) => renderSubItem(item))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 控制台区域 */}
|
{/* 控制台区域 */}
|
||||||
<Divider className="sidebar-divider" />
|
<Divider className='sidebar-divider' />
|
||||||
<div>
|
<div>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="sidebar-group-label">{t('控制台')}</div>
|
<div className='sidebar-group-label'>{t('控制台')}</div>
|
||||||
)}
|
)}
|
||||||
{workspaceItems.map((item) => renderNavItem(item))}
|
{workspaceItems.map((item) => renderNavItem(item))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 个人中心区域 */}
|
{/* 个人中心区域 */}
|
||||||
<Divider className="sidebar-divider" />
|
<Divider className='sidebar-divider' />
|
||||||
<div>
|
<div>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="sidebar-group-label">{t('个人中心')}</div>
|
<div className='sidebar-group-label'>{t('个人中心')}</div>
|
||||||
)}
|
)}
|
||||||
{financeItems.map((item) => renderNavItem(item))}
|
{financeItems.map((item) => renderNavItem(item))}
|
||||||
</div>
|
</div>
|
||||||
@@ -409,10 +408,10 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
{/* 管理员区域 - 只在管理员时显示 */}
|
{/* 管理员区域 - 只在管理员时显示 */}
|
||||||
{isAdmin() && (
|
{isAdmin() && (
|
||||||
<>
|
<>
|
||||||
<Divider className="sidebar-divider" />
|
<Divider className='sidebar-divider' />
|
||||||
<div>
|
<div>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="sidebar-group-label">{t('管理员')}</div>
|
<div className='sidebar-group-label'>{t('管理员')}</div>
|
||||||
)}
|
)}
|
||||||
{adminItems.map((item) => renderNavItem(item))}
|
{adminItems.map((item) => renderNavItem(item))}
|
||||||
</div>
|
</div>
|
||||||
@@ -421,22 +420,28 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
{/* 底部折叠按钮 */}
|
{/* 底部折叠按钮 */}
|
||||||
<div className="sidebar-collapse-button">
|
<div className='sidebar-collapse-button'>
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme='outline'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={
|
icon={
|
||||||
<ChevronLeft
|
<ChevronLeft
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
color="var(--semi-color-text-2)"
|
color='var(--semi-color-text-2)'
|
||||||
style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
style={{
|
||||||
|
transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
icononly={collapsed}
|
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}
|
{!collapsed ? t('收起侧边栏') : null}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Card, Chat, Typography, Button } from '@douyinfe/semi-ui';
|
||||||
Card,
|
import { MessageSquare, Eye, EyeOff } from 'lucide-react';
|
||||||
Chat,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
MessageSquare,
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import CustomInputRender from './CustomInputRender';
|
import CustomInputRender from './CustomInputRender';
|
||||||
|
|
||||||
@@ -57,37 +48,43 @@ const ChatArea = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="h-full"
|
className='h-full'
|
||||||
bordered={false}
|
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 ? (
|
{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='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 justify-between'>
|
||||||
<div className="flex items-center gap-3">
|
<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">
|
<div className='w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center'>
|
||||||
<MessageSquare size={20} className="text-white" />
|
<MessageSquare size={20} className='text-white' />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={5} className="!text-white mb-0">
|
<Typography.Title heading={5} className='!text-white mb-0'>
|
||||||
{t('AI 对话')}
|
{t('AI 对话')}
|
||||||
</Typography.Title>
|
</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('选择模型开始对话')}
|
{inputs.model || t('选择模型开始对话')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Button
|
<Button
|
||||||
icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
|
icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||||
onClick={onToggleDebugPanel}
|
onClick={onToggleDebugPanel}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="primary"
|
type='primary'
|
||||||
size="small"
|
size='small'
|
||||||
className="!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10"
|
className='!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10'
|
||||||
>
|
>
|
||||||
{showDebugPanel ? t('隐藏调试') : t('显示调试')}
|
{showDebugPanel ? t('隐藏调试') : t('显示调试')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -97,7 +94,7 @@ const ChatArea = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 聊天内容区域 */}
|
{/* 聊天内容区域 */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className='flex-1 overflow-hidden'>
|
||||||
<Chat
|
<Chat
|
||||||
ref={chatRef}
|
ref={chatRef}
|
||||||
chatBoxRenderConfig={{
|
chatBoxRenderConfig={{
|
||||||
@@ -110,7 +107,7 @@ const ChatArea = ({
|
|||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
chats={message}
|
chats={message}
|
||||||
onMessageSend={onMessageSend}
|
onMessageSend={onMessageSend}
|
||||||
@@ -121,7 +118,7 @@ const ChatArea = ({
|
|||||||
showStopGenerate
|
showStopGenerate
|
||||||
onStopGenerator={onStopGenerator}
|
onStopGenerator={onStopGenerator}
|
||||||
onClear={onClearMessages}
|
onClear={onClearMessages}
|
||||||
className="h-full"
|
className='h-full'
|
||||||
placeholder={t('请输入您的问题...')}
|
placeholder={t('请输入您的问题...')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -102,15 +102,17 @@ const highlightJson = (str) => {
|
|||||||
color = '#569cd6';
|
color = '#569cd6';
|
||||||
}
|
}
|
||||||
return `<span style="color: ${color}">${match}</span>`;
|
return `<span style="color: ${color}">${match}</span>`;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isJsonLike = (content, language) => {
|
const isJsonLike = (content, language) => {
|
||||||
if (language === 'json') return true;
|
if (language === 'json') return true;
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
return (
|
||||||
(trimmed.startsWith('[') && trimmed.endsWith(']'));
|
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||||
|
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatContent = (content) => {
|
const formatContent = (content) => {
|
||||||
@@ -148,7 +150,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
const contentMetrics = useMemo(() => {
|
const contentMetrics = useMemo(() => {
|
||||||
const length = formattedContent.length;
|
const length = formattedContent.length;
|
||||||
const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_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 };
|
return { length, isLarge, isVeryLarge };
|
||||||
}, [formattedContent.length]);
|
}, [formattedContent.length]);
|
||||||
|
|
||||||
@@ -156,8 +161,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
if (!contentMetrics.isLarge || isExpanded) {
|
if (!contentMetrics.isLarge || isExpanded) {
|
||||||
return formattedContent;
|
return formattedContent;
|
||||||
}
|
}
|
||||||
return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
|
return (
|
||||||
'\n\n// ... 内容被截断以提升性能 ...';
|
formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
|
||||||
|
'\n\n// ... 内容被截断以提升性能 ...'
|
||||||
|
);
|
||||||
}, [formattedContent, contentMetrics.isLarge, isExpanded]);
|
}, [formattedContent, contentMetrics.isLarge, isExpanded]);
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
const highlightedContent = useMemo(() => {
|
||||||
@@ -174,7 +181,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
|
|
||||||
const handleCopy = useCallback(async () => {
|
const handleCopy = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const textToCopy = typeof content === 'object' && content !== null
|
const textToCopy =
|
||||||
|
typeof content === 'object' && content !== null
|
||||||
? JSON.stringify(content, null, 2)
|
? JSON.stringify(content, null, 2)
|
||||||
: content;
|
: content;
|
||||||
|
|
||||||
@@ -205,10 +213,11 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
}, [isExpanded, contentMetrics.isVeryLarge]);
|
}, [isExpanded, contentMetrics.isVeryLarge]);
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
const placeholderText = {
|
const placeholderText =
|
||||||
|
{
|
||||||
preview: t('正在构造请求体预览...'),
|
preview: t('正在构造请求体预览...'),
|
||||||
request: t('暂无请求数据'),
|
request: t('暂无请求数据'),
|
||||||
response: t('暂无响应数据')
|
response: t('暂无响应数据'),
|
||||||
}[title] || t('暂无数据');
|
}[title] || t('暂无数据');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -222,7 +231,7 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
const contentPadding = contentMetrics.isLarge ? '52px' : '16px';
|
const contentPadding = contentMetrics.isLarge ? '52px' : '16px';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={codeThemeStyles.container} className="h-full">
|
<div style={codeThemeStyles.container} className='h-full'>
|
||||||
{/* 性能警告 */}
|
{/* 性能警告 */}
|
||||||
{contentMetrics.isLarge && (
|
{contentMetrics.isLarge && (
|
||||||
<div style={codeThemeStyles.performanceWarning}>
|
<div style={codeThemeStyles.performanceWarning}>
|
||||||
@@ -250,8 +259,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
<Button
|
<Button
|
||||||
icon={<Copy size={14} />}
|
icon={<Copy size={14} />}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -268,25 +277,29 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
...codeThemeStyles.content,
|
...codeThemeStyles.content,
|
||||||
paddingTop: contentPadding,
|
paddingTop: contentPadding,
|
||||||
}}
|
}}
|
||||||
className="model-settings-scroll"
|
className='model-settings-scroll'
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: '200px',
|
height: '200px',
|
||||||
color: '#888'
|
color: '#888',
|
||||||
}}>
|
}}
|
||||||
<div style={{
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
width: '20px',
|
width: '20px',
|
||||||
height: '20px',
|
height: '20px',
|
||||||
border: '2px solid #444',
|
border: '2px solid #444',
|
||||||
borderTop: '2px solid #888',
|
borderTop: '2px solid #888',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
animation: 'spin 1s linear infinite',
|
animation: 'spin 1s linear infinite',
|
||||||
marginRight: '8px'
|
marginRight: '8px',
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
{t('正在处理大内容...')}
|
{t('正在处理大内容...')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -296,18 +309,22 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
|
|
||||||
{/* 展开/收起按钮 */}
|
{/* 展开/收起按钮 */}
|
||||||
{contentMetrics.isLarge && !isProcessing && (
|
{contentMetrics.isLarge && !isProcessing && (
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
...codeThemeStyles.actionButton,
|
...codeThemeStyles.actionButton,
|
||||||
bottom: '12px',
|
bottom: '12px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}>
|
<Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}>
|
||||||
<Button
|
<Button
|
||||||
icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
icon={
|
||||||
|
isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />
|
||||||
|
}
|
||||||
onClick={handleToggleExpand}
|
onClick={handleToggleExpand}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -317,8 +334,16 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|||||||
>
|
>
|
||||||
{isExpanded ? t('收起') : t('展开')}
|
{isExpanded ? t('收起') : t('展开')}
|
||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
<span style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}>
|
<span
|
||||||
(+{Math.round((contentMetrics.length - PERFORMANCE_CONFIG.PREVIEW_LENGTH) / 1000)}K)
|
style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}
|
||||||
|
>
|
||||||
|
(+
|
||||||
|
{Math.round(
|
||||||
|
(contentMetrics.length -
|
||||||
|
PERFORMANCE_CONFIG.PREVIEW_LENGTH) /
|
||||||
|
1000,
|
||||||
|
)}
|
||||||
|
K)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,21 +18,16 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import {
|
import { Button, Typography, Toast, Modal, Dropdown } from '@douyinfe/semi-ui';
|
||||||
Button,
|
import { Download, Upload, RotateCcw, Settings2 } from 'lucide-react';
|
||||||
Typography,
|
|
||||||
Toast,
|
|
||||||
Modal,
|
|
||||||
Dropdown,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
Download,
|
|
||||||
Upload,
|
|
||||||
RotateCcw,
|
|
||||||
Settings2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { exportConfig, importConfig, clearConfig, hasStoredConfig, getConfigTimestamp } from './configStorage';
|
import {
|
||||||
|
exportConfig,
|
||||||
|
importConfig,
|
||||||
|
clearConfig,
|
||||||
|
hasStoredConfig,
|
||||||
|
getConfigTimestamp,
|
||||||
|
} from './configStorage';
|
||||||
|
|
||||||
const ConfigManager = ({
|
const ConfigManager = ({
|
||||||
currentConfig,
|
currentConfig,
|
||||||
@@ -51,7 +46,10 @@ const ConfigManager = ({
|
|||||||
...currentConfig,
|
...currentConfig,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp));
|
localStorage.setItem(
|
||||||
|
'playground_config',
|
||||||
|
JSON.stringify(configWithTimestamp),
|
||||||
|
);
|
||||||
|
|
||||||
exportConfig(currentConfig, messages);
|
exportConfig(currentConfig, messages);
|
||||||
Toast.success({
|
Toast.success({
|
||||||
@@ -104,7 +102,9 @@ const ConfigManager = ({
|
|||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('重置配置'),
|
title: t('重置配置'),
|
||||||
content: t('将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?'),
|
content: t(
|
||||||
|
'将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?',
|
||||||
|
),
|
||||||
okText: t('确定重置'),
|
okText: t('确定重置'),
|
||||||
cancelText: t('取消'),
|
cancelText: t('取消'),
|
||||||
okButtonProps: {
|
okButtonProps: {
|
||||||
@@ -114,7 +114,9 @@ const ConfigManager = ({
|
|||||||
// 询问是否同时重置消息
|
// 询问是否同时重置消息
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('重置选项'),
|
title: t('重置选项'),
|
||||||
content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'),
|
content: t(
|
||||||
|
'是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。',
|
||||||
|
),
|
||||||
okText: t('同时重置消息'),
|
okText: t('同时重置消息'),
|
||||||
cancelText: t('仅重置配置'),
|
cancelText: t('仅重置配置'),
|
||||||
okButtonProps: {
|
okButtonProps: {
|
||||||
@@ -159,7 +161,7 @@ const ConfigManager = ({
|
|||||||
name: 'export',
|
name: 'export',
|
||||||
onClick: handleExport,
|
onClick: handleExport,
|
||||||
children: (
|
children: (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Download size={14} />
|
<Download size={14} />
|
||||||
{t('导出配置')}
|
{t('导出配置')}
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +172,7 @@ const ConfigManager = ({
|
|||||||
name: 'import',
|
name: 'import',
|
||||||
onClick: handleImportClick,
|
onClick: handleImportClick,
|
||||||
children: (
|
children: (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Upload size={14} />
|
<Upload size={14} />
|
||||||
{t('导入配置')}
|
{t('导入配置')}
|
||||||
</div>
|
</div>
|
||||||
@@ -184,7 +186,7 @@ const ConfigManager = ({
|
|||||||
name: 'reset',
|
name: 'reset',
|
||||||
onClick: handleReset,
|
onClick: handleReset,
|
||||||
children: (
|
children: (
|
||||||
<div className="flex items-center gap-2 text-red-600">
|
<div className='flex items-center gap-2 text-red-600'>
|
||||||
<RotateCcw size={14} />
|
<RotateCcw size={14} />
|
||||||
{t('重置配置')}
|
{t('重置配置')}
|
||||||
</div>
|
</div>
|
||||||
@@ -197,24 +199,24 @@ const ConfigManager = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger="click"
|
trigger='click'
|
||||||
position="bottomLeft"
|
position='bottomLeft'
|
||||||
showTick
|
showTick
|
||||||
menu={dropdownItems}
|
menu={dropdownItems}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<Settings2 size={14} />}
|
icon={<Settings2 size={14} />}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
className="!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50"
|
className='!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50'
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type='file'
|
||||||
accept=".json"
|
accept='.json'
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
@@ -224,42 +226,42 @@ const ConfigManager = ({
|
|||||||
|
|
||||||
// 桌面端显示紧凑的按钮组
|
// 桌面端显示紧凑的按钮组
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
{/* 配置状态信息和重置按钮 */}
|
{/* 配置状态信息和重置按钮 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<Typography.Text className="text-xs text-gray-500">
|
<Typography.Text className='text-xs text-gray-500'>
|
||||||
{getConfigStatus()}
|
{getConfigStatus()}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Button
|
<Button
|
||||||
icon={<RotateCcw size={12} />}
|
icon={<RotateCcw size={12} />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="danger"
|
type='danger'
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
className="!rounded-full !text-xs !px-2"
|
className='!rounded-full !text-xs !px-2'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导出和导入按钮 */}
|
{/* 导出和导入按钮 */}
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
icon={<Download size={12} />}
|
icon={<Download size={12} />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
type="primary"
|
type='primary'
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
className="!rounded-lg flex-1 !text-xs !h-7"
|
className='!rounded-lg flex-1 !text-xs !h-7'
|
||||||
>
|
>
|
||||||
{t('导出')}
|
{t('导出')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
icon={<Upload size={12} />}
|
icon={<Upload size={12} />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
type="primary"
|
type='primary'
|
||||||
onClick={handleImportClick}
|
onClick={handleImportClick}
|
||||||
className="!rounded-lg flex-1 !text-xs !h-7"
|
className='!rounded-lg flex-1 !text-xs !h-7'
|
||||||
>
|
>
|
||||||
{t('导入')}
|
{t('导入')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -267,8 +269,8 @@ const ConfigManager = ({
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type='file'
|
||||||
accept=".json"
|
accept='.json'
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import React from 'react';
|
|||||||
|
|
||||||
const CustomInputRender = (props) => {
|
const CustomInputRender = (props) => {
|
||||||
const { detailProps } = props;
|
const { detailProps } = props;
|
||||||
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
|
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
|
||||||
|
detailProps;
|
||||||
|
|
||||||
// 清空按钮
|
// 清空按钮
|
||||||
const styledClearNode = clearContextNode
|
const styledClearNode = clearContextNode
|
||||||
@@ -36,7 +37,7 @@ const CustomInputRender = (props) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -52,21 +53,19 @@ const CustomInputRender = (props) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2 sm:p-4">
|
<div className='p-2 sm:p-4'>
|
||||||
<div
|
<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)' }}
|
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{/* 清空对话按钮 - 左边 */}
|
{/* 清空对话按钮 - 左边 */}
|
||||||
{styledClearNode}
|
{styledClearNode}
|
||||||
<div className="flex-1">
|
<div className='flex-1'>{inputNode}</div>
|
||||||
{inputNode}
|
|
||||||
</div>
|
|
||||||
{/* 发送按钮 - 右边 */}
|
{/* 发送按钮 - 右边 */}
|
||||||
{styledSendNode}
|
{styledSendNode}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,13 +25,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Banner,
|
Banner,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import { Code, Edit, Check, X, AlertTriangle } from 'lucide-react';
|
||||||
Code,
|
|
||||||
Edit,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
AlertTriangle,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const CustomRequestEditor = ({
|
const CustomRequestEditor = ({
|
||||||
@@ -48,12 +42,22 @@ const CustomRequestEditor = ({
|
|||||||
|
|
||||||
// 当切换到自定义模式时,用默认payload初始化
|
// 当切换到自定义模式时,用默认payload初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customRequestMode && (!customRequestBody || customRequestBody.trim() === '')) {
|
if (
|
||||||
const defaultJson = defaultPayload ? JSON.stringify(defaultPayload, null, 2) : '';
|
customRequestMode &&
|
||||||
|
(!customRequestBody || customRequestBody.trim() === '')
|
||||||
|
) {
|
||||||
|
const defaultJson = defaultPayload
|
||||||
|
? JSON.stringify(defaultPayload, null, 2)
|
||||||
|
: '';
|
||||||
setLocalValue(defaultJson);
|
setLocalValue(defaultJson);
|
||||||
onCustomRequestBodyChange(defaultJson);
|
onCustomRequestBodyChange(defaultJson);
|
||||||
}
|
}
|
||||||
}, [customRequestMode, defaultPayload, customRequestBody, onCustomRequestBodyChange]);
|
}, [
|
||||||
|
customRequestMode,
|
||||||
|
defaultPayload,
|
||||||
|
customRequestBody,
|
||||||
|
onCustomRequestBodyChange,
|
||||||
|
]);
|
||||||
|
|
||||||
// 同步外部传入的customRequestBody到本地状态
|
// 同步外部传入的customRequestBody到本地状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -113,21 +117,21 @@ const CustomRequestEditor = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
{/* 自定义模式开关 */}
|
{/* 自定义模式开关 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Code size={16} className="text-gray-500" />
|
<Code size={16} className='text-gray-500' />
|
||||||
<Typography.Text strong className="text-sm">
|
<Typography.Text strong className='text-sm'>
|
||||||
自定义请求体模式
|
自定义请求体模式
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={customRequestMode}
|
checked={customRequestMode}
|
||||||
onChange={handleModeToggle}
|
onChange={handleModeToggle}
|
||||||
checkedText="开"
|
checkedText='开'
|
||||||
uncheckedText="关"
|
uncheckedText='关'
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,43 +139,43 @@ const CustomRequestEditor = ({
|
|||||||
<>
|
<>
|
||||||
{/* 提示信息 */}
|
{/* 提示信息 */}
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
|
description='启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。'
|
||||||
icon={<AlertTriangle size={16} />}
|
icon={<AlertTriangle size={16} />}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* JSON编辑器 */}
|
{/* JSON编辑器 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<Typography.Text strong className='text-sm'>
|
||||||
请求体 JSON
|
请求体 JSON
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
{isValid ? (
|
{isValid ? (
|
||||||
<div className="flex items-center gap-1 text-green-600">
|
<div className='flex items-center gap-1 text-green-600'>
|
||||||
<Check size={14} />
|
<Check size={14} />
|
||||||
<Typography.Text className="text-xs">
|
<Typography.Text className='text-xs'>
|
||||||
格式正确
|
格式正确
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1 text-red-600">
|
<div className='flex items-center gap-1 text-red-600'>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
<Typography.Text className="text-xs">
|
<Typography.Text className='text-xs'>
|
||||||
格式错误
|
格式错误
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<Edit size={14} />}
|
icon={<Edit size={14} />}
|
||||||
onClick={formatJson}
|
onClick={formatJson}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
>
|
>
|
||||||
格式化
|
格式化
|
||||||
</Button>
|
</Button>
|
||||||
@@ -191,12 +195,12 @@ const CustomRequestEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{!isValid && errorMessage && (
|
{!isValid && errorMessage && (
|
||||||
<Typography.Text type="danger" className="text-xs mt-1 block">
|
<Typography.Text type='danger' className='text-xs mt-1 block'>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Typography.Text>
|
</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格式的请求体。您可以参考预览面板中的默认请求体格式。
|
请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,14 +26,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import { Code, Zap, Clock, X, Eye, Send } from 'lucide-react';
|
||||||
Code,
|
|
||||||
Zap,
|
|
||||||
Clock,
|
|
||||||
X,
|
|
||||||
Eye,
|
|
||||||
Send,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import CodeViewer from './CodeViewer';
|
import CodeViewer from './CodeViewer';
|
||||||
|
|
||||||
@@ -76,7 +69,7 @@ const DebugPanel = ({
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
render={
|
render={
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
{items.map(item => {
|
{items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key={item.itemKey}
|
key={item.itemKey}
|
||||||
@@ -104,21 +97,21 @@ const DebugPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="h-full flex flex-col"
|
className='h-full flex flex-col'
|
||||||
bordered={false}
|
bordered={false}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
padding: styleState.isMobile ? '16px' : '24px',
|
padding: styleState.isMobile ? '16px' : '24px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
<div className='flex items-center justify-between mb-6 flex-shrink-0'>
|
||||||
<div className="flex items-center">
|
<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">
|
<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" />
|
<Code size={20} className='text-white' />
|
||||||
</div>
|
</div>
|
||||||
<Typography.Title heading={5} className="mb-0">
|
<Typography.Title heading={5} className='mb-0'>
|
||||||
{t('调试信息')}
|
{t('调试信息')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,75 +120,84 @@ const DebugPanel = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<X size={16} />}
|
icon={<X size={16} />}
|
||||||
onClick={onCloseDebugPanel}
|
onClick={onCloseDebugPanel}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden debug-panel">
|
<div className='flex-1 overflow-hidden debug-panel'>
|
||||||
<Tabs
|
<Tabs
|
||||||
renderArrow={renderArrow}
|
renderArrow={renderArrow}
|
||||||
type="card"
|
type='card'
|
||||||
collapsible
|
collapsible
|
||||||
className="h-full"
|
className='h-full'
|
||||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
>
|
>
|
||||||
<TabPane tab={
|
<TabPane
|
||||||
<div className="flex items-center gap-2">
|
tab={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
<Eye size={16} />
|
<Eye size={16} />
|
||||||
{t('预览请求体')}
|
{t('预览请求体')}
|
||||||
{customRequestMode && (
|
{customRequestMode && (
|
||||||
<span className="px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full">
|
<span className='px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full'>
|
||||||
自定义
|
自定义
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
} itemKey="preview">
|
}
|
||||||
|
itemKey='preview'
|
||||||
|
>
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
content={debugData.previewRequest}
|
content={debugData.previewRequest}
|
||||||
title="preview"
|
title='preview'
|
||||||
language="json"
|
language='json'
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab={
|
<TabPane
|
||||||
<div className="flex items-center gap-2">
|
tab={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
<Send size={16} />
|
<Send size={16} />
|
||||||
{t('实际请求体')}
|
{t('实际请求体')}
|
||||||
</div>
|
</div>
|
||||||
} itemKey="request">
|
}
|
||||||
|
itemKey='request'
|
||||||
|
>
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
content={debugData.request}
|
content={debugData.request}
|
||||||
title="request"
|
title='request'
|
||||||
language="json"
|
language='json'
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab={
|
<TabPane
|
||||||
<div className="flex items-center gap-2">
|
tab={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
<Zap size={16} />
|
<Zap size={16} />
|
||||||
{t('响应')}
|
{t('响应')}
|
||||||
</div>
|
</div>
|
||||||
} itemKey="response">
|
}
|
||||||
|
itemKey='response'
|
||||||
|
>
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
content={debugData.response}
|
content={debugData.response}
|
||||||
title="response"
|
title='response'
|
||||||
language="json"
|
language='json'
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</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) && (
|
{(debugData.timestamp || debugData.previewTimestamp) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Clock size={14} className="text-gray-500" />
|
<Clock size={14} className='text-gray-500' />
|
||||||
<Typography.Text className="text-xs text-gray-500">
|
<Typography.Text className='text-xs text-gray-500'>
|
||||||
{activeKey === 'preview' && debugData.previewTimestamp
|
{activeKey === 'preview' && debugData.previewTimestamp
|
||||||
? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}`
|
? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}`
|
||||||
: debugData.timestamp
|
: debugData.timestamp
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import { Settings, Eye, EyeOff } from 'lucide-react';
|
||||||
Settings,
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
const FloatingButtons = ({
|
const FloatingButtons = ({
|
||||||
styleState,
|
styleState,
|
||||||
@@ -55,7 +51,7 @@ const FloatingButtons = ({
|
|||||||
onClick={onToggleSettings}
|
onClick={onToggleSettings}
|
||||||
theme='solid'
|
theme='solid'
|
||||||
type='primary'
|
type='primary'
|
||||||
className="lg:hidden"
|
className='lg:hidden'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -64,8 +60,8 @@ const FloatingButtons = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={showDebugPanel ? <EyeOff size={18} /> : <Eye size={18} />}
|
icon={showDebugPanel ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
onClick={onToggleDebugPanel}
|
onClick={onToggleDebugPanel}
|
||||||
theme="solid"
|
theme='solid'
|
||||||
type={showDebugPanel ? "danger" : "primary"}
|
type={showDebugPanel ? 'danger' : 'primary'}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
right: 16,
|
right: 16,
|
||||||
@@ -80,7 +76,7 @@ const FloatingButtons = ({
|
|||||||
? 'linear-gradient(to right, #e11d48, #be123c)'
|
? 'linear-gradient(to right, #e11d48, #be123c)'
|
||||||
: 'linear-gradient(to right, #4f46e5, #6366f1)',
|
: 'linear-gradient(to right, #4f46e5, #6366f1)',
|
||||||
}}
|
}}
|
||||||
className="lg:hidden"
|
className='lg:hidden'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,21 +18,17 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Input, Typography, Button, Switch } from '@douyinfe/semi-ui';
|
||||||
Input,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Switch,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import { IconFile } from '@douyinfe/semi-icons';
|
import { IconFile } from '@douyinfe/semi-icons';
|
||||||
import {
|
import { FileText, Plus, X, Image } from 'lucide-react';
|
||||||
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 handleAddImageUrl = () => {
|
||||||
const newUrls = [...imageUrls, ''];
|
const newUrls = [...imageUrls, ''];
|
||||||
onImageUrlsChange(newUrls);
|
onImageUrlsChange(newUrls);
|
||||||
@@ -51,75 +47,87 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={disabled ? 'opacity-50' : ''}>
|
<div className={disabled ? 'opacity-50' : ''}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Image size={16} className={imageEnabled && !disabled ? "text-blue-500" : "text-gray-400"} />
|
<Image
|
||||||
<Typography.Text strong className="text-sm">
|
size={16}
|
||||||
|
className={
|
||||||
|
imageEnabled && !disabled ? 'text-blue-500' : 'text-gray-400'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
图片地址
|
图片地址
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{disabled && (
|
{disabled && (
|
||||||
<Typography.Text className="text-xs text-orange-600">
|
<Typography.Text className='text-xs text-orange-600'>
|
||||||
(已在自定义模式中忽略)
|
(已在自定义模式中忽略)
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={imageEnabled}
|
checked={imageEnabled}
|
||||||
onChange={onImageEnabledChange}
|
onChange={onImageEnabledChange}
|
||||||
checkedText="启用"
|
checkedText='启用'
|
||||||
uncheckedText="停用"
|
uncheckedText='停用'
|
||||||
size="small"
|
size='small'
|
||||||
className="flex-shrink-0"
|
className='flex-shrink-0'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<Plus size={14} />}
|
icon={<Plus size={14} />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
type="primary"
|
type='primary'
|
||||||
onClick={handleAddImageUrl}
|
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}
|
disabled={!imageEnabled || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!imageEnabled ? (
|
{!imageEnabled ? (
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||||
{disabled ? '图片功能在自定义请求体模式下不可用' : '启用后可添加图片URL进行多模态对话'}
|
{disabled
|
||||||
|
? '图片功能在自定义请求体模式下不可用'
|
||||||
|
: '启用后可添加图片URL进行多模态对话'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : imageUrls.length === 0 ? (
|
) : imageUrls.length === 0 ? (
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||||
{disabled ? '图片功能在自定义请求体模式下不可用' : '点击 + 按钮添加图片URL进行多模态对话'}
|
{disabled
|
||||||
|
? '图片功能在自定义请求体模式下不可用'
|
||||||
|
: '点击 + 按钮添加图片URL进行多模态对话'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2 block">
|
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||||
已添加 {imageUrls.length} 张图片{disabled ? ' (自定义模式下不可用)' : ''}
|
已添加 {imageUrls.length} 张图片
|
||||||
|
{disabled ? ' (自定义模式下不可用)' : ''}
|
||||||
</Typography.Text>
|
</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) => (
|
{imageUrls.map((url, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className='flex items-center gap-2'>
|
||||||
<div className="flex-1">
|
<div className='flex-1'>
|
||||||
<Input
|
<Input
|
||||||
placeholder={`https://example.com/image${index + 1}.jpg`}
|
placeholder={`https://example.com/image${index + 1}.jpg`}
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(value) => handleUpdateImageUrl(index, value)}
|
onChange={(value) => handleUpdateImageUrl(index, value)}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
size="small"
|
size='small'
|
||||||
prefix={<IconFile size='small' />}
|
prefix={<IconFile size='small' />}
|
||||||
disabled={!imageEnabled || disabled}
|
disabled={!imageEnabled || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
icon={<X size={12} />}
|
icon={<X size={12} />}
|
||||||
size="small"
|
size='small'
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="danger"
|
type='danger'
|
||||||
onClick={() => handleRemoveImageUrl(index)}
|
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}
|
disabled={!imageEnabled || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
Button,
|
import { RefreshCw, Copy, Trash2, UserCheck, Edit } from 'lucide-react';
|
||||||
Tooltip,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
RefreshCw,
|
|
||||||
Copy,
|
|
||||||
Trash2,
|
|
||||||
UserCheck,
|
|
||||||
Edit,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const MessageActions = ({
|
const MessageActions = ({
|
||||||
@@ -40,23 +31,32 @@ const MessageActions = ({
|
|||||||
onRoleToggle,
|
onRoleToggle,
|
||||||
onMessageEdit,
|
onMessageEdit,
|
||||||
isAnyMessageGenerating = false,
|
isAnyMessageGenerating = false,
|
||||||
isEditing = false
|
isEditing = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLoading = message.status === 'loading' || message.status === 'incomplete';
|
const isLoading =
|
||||||
|
message.status === 'loading' || message.status === 'incomplete';
|
||||||
const shouldDisableActions = isAnyMessageGenerating || isEditing;
|
const shouldDisableActions = isAnyMessageGenerating || isEditing;
|
||||||
const canToggleRole = message.role === 'assistant' || message.role === 'system';
|
const canToggleRole =
|
||||||
const canEdit = !isLoading && message.content && typeof onMessageEdit === 'function' && !isEditing;
|
message.role === 'assistant' || message.role === 'system';
|
||||||
|
const canEdit =
|
||||||
|
!isLoading &&
|
||||||
|
message.content &&
|
||||||
|
typeof onMessageEdit === 'function' &&
|
||||||
|
!isEditing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-0.5">
|
<div className='flex items-center gap-0.5'>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')} position="top">
|
<Tooltip
|
||||||
|
content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<RefreshCw size={styleState.isMobile ? 12 : 14} />}
|
icon={<RefreshCw size={styleState.isMobile ? 12 : 14} />}
|
||||||
onClick={() => !shouldDisableActions && onMessageReset(message)}
|
onClick={() => !shouldDisableActions && onMessageReset(message)}
|
||||||
disabled={shouldDisableActions}
|
disabled={shouldDisableActions}
|
||||||
@@ -67,11 +67,11 @@ const MessageActions = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{message.content && (
|
{message.content && (
|
||||||
<Tooltip content={t('复制')} position="top">
|
<Tooltip content={t('复制')} position='top'>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<Copy size={styleState.isMobile ? 12 : 14} />}
|
icon={<Copy size={styleState.isMobile ? 12 : 14} />}
|
||||||
onClick={() => onMessageCopy(message)}
|
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`}
|
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 && (
|
{canEdit && (
|
||||||
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')} position="top">
|
<Tooltip
|
||||||
|
content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<Edit size={styleState.isMobile ? 12 : 14} />}
|
icon={<Edit size={styleState.isMobile ? 12 : 14} />}
|
||||||
onClick={() => !shouldDisableActions && onMessageEdit(message)}
|
onClick={() => !shouldDisableActions && onMessageEdit(message)}
|
||||||
disabled={shouldDisableActions}
|
disabled={shouldDisableActions}
|
||||||
@@ -104,27 +107,36 @@ const MessageActions = ({
|
|||||||
? t('切换为System角色')
|
? t('切换为System角色')
|
||||||
: t('切换为Assistant角色')
|
: t('切换为Assistant角色')
|
||||||
}
|
}
|
||||||
position="top"
|
position='top'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<UserCheck size={styleState.isMobile ? 12 : 14} />}
|
icon={<UserCheck size={styleState.isMobile ? 12 : 14} />}
|
||||||
onClick={() => !shouldDisableActions && onRoleToggle && onRoleToggle(message)}
|
onClick={() =>
|
||||||
|
!shouldDisableActions && onRoleToggle && onRoleToggle(message)
|
||||||
|
}
|
||||||
disabled={shouldDisableActions}
|
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`}
|
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>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')} position="top">
|
<Tooltip
|
||||||
|
content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<Trash2 size={styleState.isMobile ? 12 : 14} />}
|
icon={<Trash2 size={styleState.isMobile ? 12 : 14} />}
|
||||||
onClick={() => !shouldDisableActions && onMessageDelete(message)}
|
onClick={() => !shouldDisableActions && onMessageDelete(message)}
|
||||||
disabled={shouldDisableActions}
|
disabled={shouldDisableActions}
|
||||||
|
|||||||
@@ -18,18 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import {
|
import { Typography, TextArea, Button } from '@douyinfe/semi-ui';
|
||||||
Typography,
|
|
||||||
TextArea,
|
|
||||||
Button,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
|
import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
|
||||||
import ThinkingContent from './ThinkingContent';
|
import ThinkingContent from './ThinkingContent';
|
||||||
import {
|
import { Loader2, Check, X } from 'lucide-react';
|
||||||
Loader2,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const MessageContent = ({
|
const MessageContent = ({
|
||||||
@@ -41,13 +33,14 @@ const MessageContent = ({
|
|||||||
onEditSave,
|
onEditSave,
|
||||||
onEditCancel,
|
onEditCancel,
|
||||||
editValue,
|
editValue,
|
||||||
onEditValueChange
|
onEditValueChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const previousContentLengthRef = useRef(0);
|
const previousContentLengthRef = useRef(0);
|
||||||
const lastContentRef = useRef('');
|
const lastContentRef = useRef('');
|
||||||
|
|
||||||
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
const isThinkingStatus =
|
||||||
|
message.status === 'loading' || message.status === 'incomplete';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isThinkingStatus) {
|
if (!isThinkingStatus) {
|
||||||
@@ -60,8 +53,9 @@ const MessageContent = ({
|
|||||||
let errorText;
|
let errorText;
|
||||||
|
|
||||||
if (Array.isArray(message.content)) {
|
if (Array.isArray(message.content)) {
|
||||||
const textContent = message.content.find(item => item.type === 'text');
|
const textContent = message.content.find((item) => item.type === 'text');
|
||||||
errorText = textContent && textContent.text && typeof textContent.text === 'string'
|
errorText =
|
||||||
|
textContent && textContent.text && typeof textContent.text === 'string'
|
||||||
? textContent.text
|
? textContent.text
|
||||||
: t('请求发生错误');
|
: t('请求发生错误');
|
||||||
} else if (typeof message.content === 'string') {
|
} else if (typeof message.content === 'string') {
|
||||||
@@ -72,21 +66,21 @@ const MessageContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${className}`}>
|
<div className={`${className}`}>
|
||||||
<Typography.Text className="text-white">
|
<Typography.Text className='text-white'>{errorText}</Typography.Text>
|
||||||
{errorText}
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentExtractedThinkingContent = null;
|
let currentExtractedThinkingContent = null;
|
||||||
let currentDisplayableFinalContent = "";
|
let currentDisplayableFinalContent = '';
|
||||||
let thinkingSource = null;
|
let thinkingSource = null;
|
||||||
|
|
||||||
const getTextContent = (content) => {
|
const getTextContent = (content) => {
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
const textItem = content.find(item => item.type === 'text');
|
const textItem = content.find((item) => item.type === 'text');
|
||||||
return textItem && textItem.text && typeof textItem.text === 'string' ? textItem.text : '';
|
return textItem && textItem.text && typeof textItem.text === 'string'
|
||||||
|
? textItem.text
|
||||||
|
: '';
|
||||||
} else if (typeof content === 'string') {
|
} else if (typeof content === 'string') {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -97,7 +91,7 @@ const MessageContent = ({
|
|||||||
|
|
||||||
if (message.role === 'assistant') {
|
if (message.role === 'assistant') {
|
||||||
let baseContentForDisplay = getTextContent(message.content);
|
let baseContentForDisplay = getTextContent(message.content);
|
||||||
let combinedThinkingContent = "";
|
let combinedThinkingContent = '';
|
||||||
|
|
||||||
if (message.reasoningContent) {
|
if (message.reasoningContent) {
|
||||||
combinedThinkingContent = message.reasoningContent;
|
combinedThinkingContent = message.reasoningContent;
|
||||||
@@ -112,7 +106,9 @@ const MessageContent = ({
|
|||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
|
|
||||||
while ((match = thinkTagRegex.exec(baseContentForDisplay)) !== null) {
|
while ((match = thinkTagRegex.exec(baseContentForDisplay)) !== null) {
|
||||||
replyParts.push(baseContentForDisplay.substring(lastIndex, match.index));
|
replyParts.push(
|
||||||
|
baseContentForDisplay.substring(lastIndex, match.index),
|
||||||
|
);
|
||||||
thoughtsFromPairedTags.push(match[1]);
|
thoughtsFromPairedTags.push(match[1]);
|
||||||
lastIndex = match.index + match[0].length;
|
lastIndex = match.index + match[0].length;
|
||||||
}
|
}
|
||||||
@@ -125,7 +121,9 @@ const MessageContent = ({
|
|||||||
} else {
|
} else {
|
||||||
combinedThinkingContent = pairedThoughtsStr;
|
combinedThinkingContent = pairedThoughtsStr;
|
||||||
}
|
}
|
||||||
thinkingSource = thinkingSource ? thinkingSource + ' & <think> tags' : '<think> tags';
|
thinkingSource = thinkingSource
|
||||||
|
? thinkingSource + ' & <think> tags'
|
||||||
|
: '<think> tags';
|
||||||
}
|
}
|
||||||
|
|
||||||
baseContentForDisplay = replyParts.join('');
|
baseContentForDisplay = replyParts.join('');
|
||||||
@@ -134,37 +132,55 @@ const MessageContent = ({
|
|||||||
if (isThinkingStatus) {
|
if (isThinkingStatus) {
|
||||||
const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>');
|
const lastOpenThinkIndex = baseContentForDisplay.lastIndexOf('<think>');
|
||||||
if (lastOpenThinkIndex !== -1) {
|
if (lastOpenThinkIndex !== -1) {
|
||||||
const fragmentAfterLastOpen = baseContentForDisplay.substring(lastOpenThinkIndex);
|
const fragmentAfterLastOpen =
|
||||||
|
baseContentForDisplay.substring(lastOpenThinkIndex);
|
||||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
const unclosedThought = fragmentAfterLastOpen
|
||||||
|
.substring('<think>'.length)
|
||||||
|
.trim();
|
||||||
if (unclosedThought) {
|
if (unclosedThought) {
|
||||||
if (combinedThinkingContent) {
|
if (combinedThinkingContent) {
|
||||||
combinedThinkingContent += '\n\n---\n\n' + unclosedThought;
|
combinedThinkingContent += '\n\n---\n\n' + unclosedThought;
|
||||||
} else {
|
} else {
|
||||||
combinedThinkingContent = unclosedThought;
|
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;
|
currentExtractedThinkingContent = combinedThinkingContent || null;
|
||||||
currentDisplayableFinalContent = baseContentForDisplay.replace(/<\/?think>/g, '').trim();
|
currentDisplayableFinalContent = baseContentForDisplay
|
||||||
|
.replace(/<\/?think>/g, '')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalExtractedThinkingContent = currentExtractedThinkingContent;
|
const finalExtractedThinkingContent = currentExtractedThinkingContent;
|
||||||
const finalDisplayableFinalContent = currentDisplayableFinalContent;
|
const finalDisplayableFinalContent = currentDisplayableFinalContent;
|
||||||
|
|
||||||
if (message.role === 'assistant' &&
|
if (
|
||||||
|
message.role === 'assistant' &&
|
||||||
isThinkingStatus &&
|
isThinkingStatus &&
|
||||||
!finalExtractedThinkingContent &&
|
!finalExtractedThinkingContent &&
|
||||||
(!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
(!finalDisplayableFinalContent ||
|
||||||
|
finalDisplayableFinalContent.trim() === '')
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50`}>
|
<div
|
||||||
<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">
|
className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50`}
|
||||||
<Loader2 className="animate-spin text-white" size={styleState.isMobile ? 16 : 20} />
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -173,12 +189,17 @@ const MessageContent = ({
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{message.role === 'system' && (
|
{message.role === 'system' && (
|
||||||
<div className="mb-2 sm:mb-4">
|
<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
|
||||||
<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">
|
className='flex items-center gap-2 p-2 sm:p-3 bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg'
|
||||||
<Typography.Text className="text-white text-xs font-bold">S</Typography.Text>
|
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>
|
</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('系统消息')}
|
{t('系统消息')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +217,7 @@ const MessageContent = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
<TextArea
|
<TextArea
|
||||||
value={editValue}
|
value={editValue}
|
||||||
onChange={(value) => onEditValueChange(value)}
|
onChange={(value) => onEditValueChange(value)}
|
||||||
@@ -207,27 +228,27 @@ const MessageContent = ({
|
|||||||
fontSize: styleState.isMobile ? '14px' : '15px',
|
fontSize: styleState.isMobile ? '14px' : '15px',
|
||||||
lineHeight: '1.6',
|
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
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type="danger"
|
type='danger'
|
||||||
theme="light"
|
theme='light'
|
||||||
icon={<X size={14} />}
|
icon={<X size={14} />}
|
||||||
onClick={onEditCancel}
|
onClick={onEditCancel}
|
||||||
className="flex-1"
|
className='flex-1'
|
||||||
>
|
>
|
||||||
{t('取消')}
|
{t('取消')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type="warning"
|
type='warning'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
icon={<Check size={14} />}
|
icon={<Check size={14} />}
|
||||||
onClick={onEditSave}
|
onClick={onEditSave}
|
||||||
disabled={!editValue || editValue.trim() === ''}
|
disabled={!editValue || editValue.trim() === ''}
|
||||||
className="flex-1"
|
className='flex-1'
|
||||||
>
|
>
|
||||||
{t('保存')}
|
{t('保存')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -236,19 +257,23 @@ const MessageContent = ({
|
|||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
if (Array.isArray(message.content)) {
|
if (Array.isArray(message.content)) {
|
||||||
const textContent = message.content.find(item => item.type === 'text');
|
const textContent = message.content.find(
|
||||||
const imageContents = message.content.filter(item => item.type === 'image_url');
|
(item) => item.type === 'text',
|
||||||
|
);
|
||||||
|
const imageContents = message.content.filter(
|
||||||
|
(item) => item.type === 'image_url',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{imageContents.length > 0 && (
|
{imageContents.length > 0 && (
|
||||||
<div className="mb-3 space-y-2">
|
<div className='mb-3 space-y-2'>
|
||||||
{imageContents.map((imgItem, index) => (
|
{imageContents.map((imgItem, index) => (
|
||||||
<div key={index} className="max-w-sm">
|
<div key={index} className='max-w-sm'>
|
||||||
<img
|
<img
|
||||||
src={imgItem.image_url.url}
|
src={imgItem.image_url.url}
|
||||||
alt={`用户上传的图片 ${index + 1}`}
|
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' }}
|
style={{ maxHeight: '300px' }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
@@ -256,7 +281,7 @@ const MessageContent = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<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' }}
|
style={{ display: 'none' }}
|
||||||
>
|
>
|
||||||
图片加载失败: {imgItem.image_url.url}
|
图片加载失败: {imgItem.image_url.url}
|
||||||
@@ -266,11 +291,18 @@ const MessageContent = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
|
{textContent &&
|
||||||
<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' : ''}`}>
|
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
|
<MarkdownRenderer
|
||||||
content={textContent.text}
|
content={textContent.text}
|
||||||
className={message.role === 'user' ? 'user-message' : ''}
|
className={
|
||||||
|
message.role === 'user' ? 'user-message' : ''
|
||||||
|
}
|
||||||
animated={false}
|
animated={false}
|
||||||
previousContentLength={0}
|
previousContentLength={0}
|
||||||
/>
|
/>
|
||||||
@@ -282,12 +314,19 @@ const MessageContent = ({
|
|||||||
|
|
||||||
if (typeof message.content === 'string') {
|
if (typeof message.content === 'string') {
|
||||||
if (message.role === 'assistant') {
|
if (message.role === 'assistant') {
|
||||||
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
|
if (
|
||||||
|
finalDisplayableFinalContent &&
|
||||||
|
finalDisplayableFinalContent.trim() !== ''
|
||||||
|
) {
|
||||||
// 获取上一次的内容长度
|
// 获取上一次的内容长度
|
||||||
let prevLength = 0;
|
let prevLength = 0;
|
||||||
if (isThinkingStatus && lastContentRef.current) {
|
if (isThinkingStatus && lastContentRef.current) {
|
||||||
// 只有当前内容包含上一次内容时,才使用上一次的长度
|
// 只有当前内容包含上一次内容时,才使用上一次的长度
|
||||||
if (finalDisplayableFinalContent.startsWith(lastContentRef.current)) {
|
if (
|
||||||
|
finalDisplayableFinalContent.startsWith(
|
||||||
|
lastContentRef.current,
|
||||||
|
)
|
||||||
|
) {
|
||||||
prevLength = lastContentRef.current.length;
|
prevLength = lastContentRef.current.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,10 +337,10 @@ const MessageContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<MarkdownRenderer
|
||||||
content={finalDisplayableFinalContent}
|
content={finalDisplayableFinalContent}
|
||||||
className=""
|
className=''
|
||||||
animated={isThinkingStatus}
|
animated={isThinkingStatus}
|
||||||
previousContentLength={prevLength}
|
previousContentLength={prevLength}
|
||||||
/>
|
/>
|
||||||
@@ -310,7 +349,9 @@ const MessageContent = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
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
|
<MarkdownRenderer
|
||||||
content={message.content}
|
content={message.content}
|
||||||
className={message.role === 'user' ? 'user-message' : ''}
|
className={message.role === 'user' ? 'user-message' : ''}
|
||||||
|
|||||||
@@ -24,23 +24,30 @@ import SettingsPanel from './SettingsPanel';
|
|||||||
import DebugPanel from './DebugPanel';
|
import DebugPanel from './DebugPanel';
|
||||||
|
|
||||||
// 优化的消息内容组件
|
// 优化的消息内容组件
|
||||||
export const OptimizedMessageContent = React.memo(MessageContent, (prevProps, nextProps) => {
|
export const OptimizedMessageContent = React.memo(
|
||||||
|
MessageContent,
|
||||||
|
(prevProps, nextProps) => {
|
||||||
// 只有这些属性变化时才重新渲染
|
// 只有这些属性变化时才重新渲染
|
||||||
return (
|
return (
|
||||||
prevProps.message.id === nextProps.message.id &&
|
prevProps.message.id === nextProps.message.id &&
|
||||||
prevProps.message.content === nextProps.message.content &&
|
prevProps.message.content === nextProps.message.content &&
|
||||||
prevProps.message.status === nextProps.message.status &&
|
prevProps.message.status === nextProps.message.status &&
|
||||||
prevProps.message.role === nextProps.message.role &&
|
prevProps.message.role === nextProps.message.role &&
|
||||||
prevProps.message.reasoningContent === nextProps.message.reasoningContent &&
|
prevProps.message.reasoningContent ===
|
||||||
prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
|
nextProps.message.reasoningContent &&
|
||||||
|
prevProps.message.isReasoningExpanded ===
|
||||||
|
nextProps.message.isReasoningExpanded &&
|
||||||
prevProps.isEditing === nextProps.isEditing &&
|
prevProps.isEditing === nextProps.isEditing &&
|
||||||
prevProps.editValue === nextProps.editValue &&
|
prevProps.editValue === nextProps.editValue &&
|
||||||
prevProps.styleState.isMobile === nextProps.styleState.isMobile
|
prevProps.styleState.isMobile === nextProps.styleState.isMobile
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 优化的消息操作组件
|
// 优化的消息操作组件
|
||||||
export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, nextProps) => {
|
export const OptimizedMessageActions = React.memo(
|
||||||
|
MessageActions,
|
||||||
|
(prevProps, nextProps) => {
|
||||||
return (
|
return (
|
||||||
prevProps.message.id === nextProps.message.id &&
|
prevProps.message.id === nextProps.message.id &&
|
||||||
prevProps.message.role === nextProps.message.role &&
|
prevProps.message.role === nextProps.message.role &&
|
||||||
@@ -48,32 +55,43 @@ export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, ne
|
|||||||
prevProps.isEditing === nextProps.isEditing &&
|
prevProps.isEditing === nextProps.isEditing &&
|
||||||
prevProps.onMessageReset === nextProps.onMessageReset
|
prevProps.onMessageReset === nextProps.onMessageReset
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 优化的设置面板组件
|
// 优化的设置面板组件
|
||||||
export const OptimizedSettingsPanel = React.memo(SettingsPanel, (prevProps, nextProps) => {
|
export const OptimizedSettingsPanel = React.memo(
|
||||||
|
SettingsPanel,
|
||||||
|
(prevProps, nextProps) => {
|
||||||
return (
|
return (
|
||||||
JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
|
JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
|
||||||
JSON.stringify(prevProps.parameterEnabled) === JSON.stringify(nextProps.parameterEnabled) &&
|
JSON.stringify(prevProps.parameterEnabled) ===
|
||||||
|
JSON.stringify(nextProps.parameterEnabled) &&
|
||||||
JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
|
JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
|
||||||
JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
|
JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
|
||||||
prevProps.customRequestMode === nextProps.customRequestMode &&
|
prevProps.customRequestMode === nextProps.customRequestMode &&
|
||||||
prevProps.customRequestBody === nextProps.customRequestBody &&
|
prevProps.customRequestBody === nextProps.customRequestBody &&
|
||||||
prevProps.showDebugPanel === nextProps.showDebugPanel &&
|
prevProps.showDebugPanel === nextProps.showDebugPanel &&
|
||||||
prevProps.showSettings === nextProps.showSettings &&
|
prevProps.showSettings === nextProps.showSettings &&
|
||||||
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
|
JSON.stringify(prevProps.previewPayload) ===
|
||||||
|
JSON.stringify(nextProps.previewPayload) &&
|
||||||
JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages)
|
JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages)
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 优化的调试面板组件
|
// 优化的调试面板组件
|
||||||
export const OptimizedDebugPanel = React.memo(DebugPanel, (prevProps, nextProps) => {
|
export const OptimizedDebugPanel = React.memo(
|
||||||
|
DebugPanel,
|
||||||
|
(prevProps, nextProps) => {
|
||||||
return (
|
return (
|
||||||
prevProps.show === nextProps.show &&
|
prevProps.show === nextProps.show &&
|
||||||
prevProps.activeTab === nextProps.activeTab &&
|
prevProps.activeTab === nextProps.activeTab &&
|
||||||
JSON.stringify(prevProps.debugData) === JSON.stringify(nextProps.debugData) &&
|
JSON.stringify(prevProps.debugData) ===
|
||||||
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
|
JSON.stringify(nextProps.debugData) &&
|
||||||
|
JSON.stringify(prevProps.previewPayload) ===
|
||||||
|
JSON.stringify(nextProps.previewPayload) &&
|
||||||
prevProps.customRequestMode === nextProps.customRequestMode &&
|
prevProps.customRequestMode === nextProps.customRequestMode &&
|
||||||
prevProps.showDebugPanel === nextProps.showDebugPanel
|
prevProps.showDebugPanel === nextProps.showDebugPanel
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -18,13 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Input, Slider, Typography, Button, Tag } from '@douyinfe/semi-ui';
|
||||||
Input,
|
|
||||||
Slider,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Tag,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
import {
|
||||||
Hash,
|
Hash,
|
||||||
Thermometer,
|
Thermometer,
|
||||||
@@ -46,28 +40,36 @@ const ParameterControl = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Temperature */}
|
{/* Temperature */}
|
||||||
<div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.temperature || disabled ? 'opacity-50' : ''}`}>
|
<div
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.temperature || disabled ? 'opacity-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Thermometer size={16} className="text-gray-500" />
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Thermometer size={16} className='text-gray-500' />
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
Temperature
|
Temperature
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Tag size="small" shape='circle'>
|
<Tag size='small' shape='circle'>
|
||||||
{inputs.temperature}
|
{inputs.temperature}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
theme={parameterEnabled.temperature ? 'solid' : 'borderless'}
|
theme={parameterEnabled.temperature ? 'solid' : 'borderless'}
|
||||||
type={parameterEnabled.temperature ? 'primary' : 'tertiary'}
|
type={parameterEnabled.temperature ? 'primary' : 'tertiary'}
|
||||||
size="small"
|
size='small'
|
||||||
icon={parameterEnabled.temperature ? <Check size={10} /> : <X size={10} />}
|
icon={
|
||||||
|
parameterEnabled.temperature ? (
|
||||||
|
<Check size={10} />
|
||||||
|
) : (
|
||||||
|
<X size={10} />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={() => onParameterToggle('temperature')}
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||||
控制输出的随机性和创造性
|
控制输出的随机性和创造性
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -76,34 +78,38 @@ const ParameterControl = ({
|
|||||||
max={1}
|
max={1}
|
||||||
value={inputs.temperature}
|
value={inputs.temperature}
|
||||||
onChange={(value) => onInputChange('temperature', value)}
|
onChange={(value) => onInputChange('temperature', value)}
|
||||||
className="mt-2"
|
className='mt-2'
|
||||||
disabled={!parameterEnabled.temperature || disabled}
|
disabled={!parameterEnabled.temperature || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top P */}
|
{/* Top P */}
|
||||||
<div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.top_p || disabled ? 'opacity-50' : ''}`}>
|
<div
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.top_p || disabled ? 'opacity-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Target size={16} className="text-gray-500" />
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Target size={16} className='text-gray-500' />
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
Top P
|
Top P
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Tag size="small" shape='circle'>
|
<Tag size='small' shape='circle'>
|
||||||
{inputs.top_p}
|
{inputs.top_p}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
theme={parameterEnabled.top_p ? 'solid' : 'borderless'}
|
theme={parameterEnabled.top_p ? 'solid' : 'borderless'}
|
||||||
type={parameterEnabled.top_p ? 'primary' : 'tertiary'}
|
type={parameterEnabled.top_p ? 'primary' : 'tertiary'}
|
||||||
size="small"
|
size='small'
|
||||||
icon={parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />}
|
icon={
|
||||||
|
parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />
|
||||||
|
}
|
||||||
onClick={() => onParameterToggle('top_p')}
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||||
核采样,控制词汇选择的多样性
|
核采样,控制词汇选择的多样性
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -112,34 +118,42 @@ const ParameterControl = ({
|
|||||||
max={1}
|
max={1}
|
||||||
value={inputs.top_p}
|
value={inputs.top_p}
|
||||||
onChange={(value) => onInputChange('top_p', value)}
|
onChange={(value) => onInputChange('top_p', value)}
|
||||||
className="mt-2"
|
className='mt-2'
|
||||||
disabled={!parameterEnabled.top_p || disabled}
|
disabled={!parameterEnabled.top_p || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frequency Penalty */}
|
{/* Frequency Penalty */}
|
||||||
<div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.frequency_penalty || disabled ? 'opacity-50' : ''}`}>
|
<div
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.frequency_penalty || disabled ? 'opacity-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Repeat size={16} className="text-gray-500" />
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Repeat size={16} className='text-gray-500' />
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
Frequency Penalty
|
Frequency Penalty
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Tag size="small" shape='circle'>
|
<Tag size='small' shape='circle'>
|
||||||
{inputs.frequency_penalty}
|
{inputs.frequency_penalty}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
theme={parameterEnabled.frequency_penalty ? 'solid' : 'borderless'}
|
theme={parameterEnabled.frequency_penalty ? 'solid' : 'borderless'}
|
||||||
type={parameterEnabled.frequency_penalty ? 'primary' : 'tertiary'}
|
type={parameterEnabled.frequency_penalty ? 'primary' : 'tertiary'}
|
||||||
size="small"
|
size='small'
|
||||||
icon={parameterEnabled.frequency_penalty ? <Check size={10} /> : <X size={10} />}
|
icon={
|
||||||
|
parameterEnabled.frequency_penalty ? (
|
||||||
|
<Check size={10} />
|
||||||
|
) : (
|
||||||
|
<X size={10} />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={() => onParameterToggle('frequency_penalty')}
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||||
频率惩罚,减少重复词汇的出现
|
频率惩罚,减少重复词汇的出现
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -148,34 +162,42 @@ const ParameterControl = ({
|
|||||||
max={2}
|
max={2}
|
||||||
value={inputs.frequency_penalty}
|
value={inputs.frequency_penalty}
|
||||||
onChange={(value) => onInputChange('frequency_penalty', value)}
|
onChange={(value) => onInputChange('frequency_penalty', value)}
|
||||||
className="mt-2"
|
className='mt-2'
|
||||||
disabled={!parameterEnabled.frequency_penalty || disabled}
|
disabled={!parameterEnabled.frequency_penalty || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Presence Penalty */}
|
{/* Presence Penalty */}
|
||||||
<div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.presence_penalty || disabled ? 'opacity-50' : ''}`}>
|
<div
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.presence_penalty || disabled ? 'opacity-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Ban size={16} className="text-gray-500" />
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Ban size={16} className='text-gray-500' />
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
Presence Penalty
|
Presence Penalty
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Tag size="small" shape='circle'>
|
<Tag size='small' shape='circle'>
|
||||||
{inputs.presence_penalty}
|
{inputs.presence_penalty}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
theme={parameterEnabled.presence_penalty ? 'solid' : 'borderless'}
|
theme={parameterEnabled.presence_penalty ? 'solid' : 'borderless'}
|
||||||
type={parameterEnabled.presence_penalty ? 'primary' : 'tertiary'}
|
type={parameterEnabled.presence_penalty ? 'primary' : 'tertiary'}
|
||||||
size="small"
|
size='small'
|
||||||
icon={parameterEnabled.presence_penalty ? <Check size={10} /> : <X size={10} />}
|
icon={
|
||||||
|
parameterEnabled.presence_penalty ? (
|
||||||
|
<Check size={10} />
|
||||||
|
) : (
|
||||||
|
<X size={10} />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={() => onParameterToggle('presence_penalty')}
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text className="text-xs text-gray-500 mb-2">
|
<Typography.Text className='text-xs text-gray-500 mb-2'>
|
||||||
存在惩罚,鼓励讨论新话题
|
存在惩罚,鼓励讨论新话题
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -184,27 +206,35 @@ const ParameterControl = ({
|
|||||||
max={2}
|
max={2}
|
||||||
value={inputs.presence_penalty}
|
value={inputs.presence_penalty}
|
||||||
onChange={(value) => onInputChange('presence_penalty', value)}
|
onChange={(value) => onInputChange('presence_penalty', value)}
|
||||||
className="mt-2"
|
className='mt-2'
|
||||||
disabled={!parameterEnabled.presence_penalty || disabled}
|
disabled={!parameterEnabled.presence_penalty || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MaxTokens */}
|
{/* MaxTokens */}
|
||||||
<div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.max_tokens || disabled ? 'opacity-50' : ''}`}>
|
<div
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.max_tokens || disabled ? 'opacity-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Hash size={16} className="text-gray-500" />
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Hash size={16} className='text-gray-500' />
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
Max Tokens
|
Max Tokens
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
theme={parameterEnabled.max_tokens ? 'solid' : 'borderless'}
|
theme={parameterEnabled.max_tokens ? 'solid' : 'borderless'}
|
||||||
type={parameterEnabled.max_tokens ? 'primary' : 'tertiary'}
|
type={parameterEnabled.max_tokens ? 'primary' : 'tertiary'}
|
||||||
size="small"
|
size='small'
|
||||||
icon={parameterEnabled.max_tokens ? <Check size={10} /> : <X size={10} />}
|
icon={
|
||||||
|
parameterEnabled.max_tokens ? (
|
||||||
|
<Check size={10} />
|
||||||
|
) : (
|
||||||
|
<X size={10} />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={() => onParameterToggle('max_tokens')}
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,30 +246,32 @@ const ParameterControl = ({
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
value={inputs.max_tokens}
|
value={inputs.max_tokens}
|
||||||
onChange={(value) => onInputChange('max_tokens', value)}
|
onChange={(value) => onInputChange('max_tokens', value)}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
disabled={!parameterEnabled.max_tokens || disabled}
|
disabled={!parameterEnabled.max_tokens || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Seed */}
|
{/* Seed */}
|
||||||
<div className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.seed || disabled ? 'opacity-50' : ''}`}>
|
<div
|
||||||
<div className="flex items-center justify-between mb-2">
|
className={`transition-opacity duration-200 mb-4 ${!parameterEnabled.seed || disabled ? 'opacity-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<Shuffle size={16} className="text-gray-500" />
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Typography.Text strong className="text-sm">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Shuffle size={16} className='text-gray-500' />
|
||||||
|
<Typography.Text strong className='text-sm'>
|
||||||
Seed
|
Seed
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text className="text-xs text-gray-400">
|
<Typography.Text className='text-xs text-gray-400'>
|
||||||
(可选,用于复现结果)
|
(可选,用于复现结果)
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
theme={parameterEnabled.seed ? 'solid' : 'borderless'}
|
theme={parameterEnabled.seed ? 'solid' : 'borderless'}
|
||||||
type={parameterEnabled.seed ? 'primary' : 'tertiary'}
|
type={parameterEnabled.seed ? 'primary' : 'tertiary'}
|
||||||
size="small"
|
size='small'
|
||||||
icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
|
icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
|
||||||
onClick={() => onParameterToggle('seed')}
|
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}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -248,8 +280,10 @@ const ParameterControl = ({
|
|||||||
name='seed'
|
name='seed'
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
value={inputs.seed || ''}
|
value={inputs.seed || ''}
|
||||||
onChange={(value) => onInputChange('seed', value === '' ? null : value)}
|
onChange={(value) =>
|
||||||
className="!rounded-lg"
|
onInputChange('seed', value === '' ? null : value)
|
||||||
|
}
|
||||||
|
className='!rounded-lg'
|
||||||
disabled={!parameterEnabled.seed || disabled}
|
disabled={!parameterEnabled.seed || disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,20 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Card, Select, Typography, Button, Switch } from '@douyinfe/semi-ui';
|
||||||
Card,
|
import { Sparkles, Users, ToggleLeft, X, Settings } from 'lucide-react';
|
||||||
Select,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Switch,
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
Sparkles,
|
|
||||||
Users,
|
|
||||||
ToggleLeft,
|
|
||||||
X,
|
|
||||||
Settings,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { renderGroupOption, selectFilter } from '../../helpers';
|
import { renderGroupOption, selectFilter } from '../../helpers';
|
||||||
import ParameterControl from './ParameterControl';
|
import ParameterControl from './ParameterControl';
|
||||||
@@ -70,22 +58,22 @@ const SettingsPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="h-full flex flex-col"
|
className='h-full flex flex-col'
|
||||||
bordered={false}
|
bordered={false}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
padding: styleState.isMobile ? '16px' : '24px',
|
padding: styleState.isMobile ? '16px' : '24px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 标题区域 - 与调试面板保持一致 */}
|
{/* 标题区域 - 与调试面板保持一致 */}
|
||||||
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
<div className='flex items-center justify-between mb-6 flex-shrink-0'>
|
||||||
<div className="flex items-center">
|
<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">
|
<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" />
|
<Settings size={20} className='text-white' />
|
||||||
</div>
|
</div>
|
||||||
<Typography.Title heading={5} className="mb-0">
|
<Typography.Title heading={5} className='mb-0'>
|
||||||
{t('模型配置')}
|
{t('模型配置')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,17 +82,17 @@ const SettingsPanel = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<X size={16} />}
|
icon={<X size={16} />}
|
||||||
onClick={onCloseSettings}
|
onClick={onCloseSettings}
|
||||||
theme="borderless"
|
theme='borderless'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端配置管理 */}
|
{/* 移动端配置管理 */}
|
||||||
{styleState.isMobile && (
|
{styleState.isMobile && (
|
||||||
<div className="mb-4 flex-shrink-0">
|
<div className='mb-4 flex-shrink-0'>
|
||||||
<ConfigManager
|
<ConfigManager
|
||||||
currentConfig={currentConfig}
|
currentConfig={currentConfig}
|
||||||
onConfigImport={onConfigImport}
|
onConfigImport={onConfigImport}
|
||||||
@@ -115,7 +103,7 @@ const SettingsPanel = ({
|
|||||||
</div>
|
</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
|
<CustomRequestEditor
|
||||||
customRequestMode={customRequestMode}
|
customRequestMode={customRequestMode}
|
||||||
@@ -127,13 +115,13 @@ const SettingsPanel = ({
|
|||||||
|
|
||||||
{/* 分组选择 */}
|
{/* 分组选择 */}
|
||||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className='flex items-center gap-2 mb-2'>
|
||||||
<Users size={16} className="text-gray-500" />
|
<Users size={16} className='text-gray-500' />
|
||||||
<Typography.Text strong className="text-sm">
|
<Typography.Text strong className='text-sm'>
|
||||||
{t('分组')}
|
{t('分组')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{customRequestMode && (
|
{customRequestMode && (
|
||||||
<Typography.Text className="text-xs text-orange-600">
|
<Typography.Text className='text-xs text-orange-600'>
|
||||||
(已在自定义模式中忽略)
|
(已在自定义模式中忽略)
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -152,20 +140,20 @@ const SettingsPanel = ({
|
|||||||
renderOptionItem={renderGroupOption}
|
renderOptionItem={renderGroupOption}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
disabled={customRequestMode}
|
disabled={customRequestMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 模型选择 */}
|
{/* 模型选择 */}
|
||||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className='flex items-center gap-2 mb-2'>
|
||||||
<Sparkles size={16} className="text-gray-500" />
|
<Sparkles size={16} className='text-gray-500' />
|
||||||
<Typography.Text strong className="text-sm">
|
<Typography.Text strong className='text-sm'>
|
||||||
{t('模型')}
|
{t('模型')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{customRequestMode && (
|
{customRequestMode && (
|
||||||
<Typography.Text className="text-xs text-orange-600">
|
<Typography.Text className='text-xs text-orange-600'>
|
||||||
(已在自定义模式中忽略)
|
(已在自定义模式中忽略)
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -183,7 +171,7 @@ const SettingsPanel = ({
|
|||||||
optionList={models}
|
optionList={models}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
disabled={customRequestMode}
|
disabled={customRequestMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +182,9 @@ const SettingsPanel = ({
|
|||||||
imageUrls={inputs.imageUrls}
|
imageUrls={inputs.imageUrls}
|
||||||
imageEnabled={inputs.imageEnabled}
|
imageEnabled={inputs.imageEnabled}
|
||||||
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
|
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
|
||||||
onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
|
onImageEnabledChange={(enabled) =>
|
||||||
|
onInputChange('imageEnabled', enabled)
|
||||||
|
}
|
||||||
disabled={customRequestMode}
|
disabled={customRequestMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,14 +202,14 @@ const SettingsPanel = ({
|
|||||||
|
|
||||||
{/* 流式输出开关 */}
|
{/* 流式输出开关 */}
|
||||||
<div className={customRequestMode ? 'opacity-50' : ''}>
|
<div className={customRequestMode ? 'opacity-50' : ''}>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<ToggleLeft size={16} className="text-gray-500" />
|
<ToggleLeft size={16} className='text-gray-500' />
|
||||||
<Typography.Text strong className="text-sm">
|
<Typography.Text strong className='text-sm'>
|
||||||
流式输出
|
流式输出
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{customRequestMode && (
|
{customRequestMode && (
|
||||||
<Typography.Text className="text-xs text-orange-600">
|
<Typography.Text className='text-xs text-orange-600'>
|
||||||
(已在自定义模式中忽略)
|
(已在自定义模式中忽略)
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -227,9 +217,9 @@ const SettingsPanel = ({
|
|||||||
<Switch
|
<Switch
|
||||||
checked={inputs.stream}
|
checked={inputs.stream}
|
||||||
onChange={(checked) => onInputChange('stream', checked)}
|
onChange={(checked) => onInputChange('stream', checked)}
|
||||||
checkedText="开"
|
checkedText='开'
|
||||||
uncheckedText="关"
|
uncheckedText='关'
|
||||||
size="small"
|
size='small'
|
||||||
disabled={customRequestMode}
|
disabled={customRequestMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,7 +228,7 @@ const SettingsPanel = ({
|
|||||||
|
|
||||||
{/* 桌面端的配置管理放在底部 */}
|
{/* 桌面端的配置管理放在底部 */}
|
||||||
{!styleState.isMobile && (
|
{!styleState.isMobile && (
|
||||||
<div className="flex-shrink-0 pt-3">
|
<div className='flex-shrink-0 pt-3'>
|
||||||
<ConfigManager
|
<ConfigManager
|
||||||
currentConfig={currentConfig}
|
currentConfig={currentConfig}
|
||||||
onConfigImport={onConfigImport}
|
onConfigImport={onConfigImport}
|
||||||
|
|||||||
@@ -28,17 +28,25 @@ const ThinkingContent = ({
|
|||||||
finalExtractedThinkingContent,
|
finalExtractedThinkingContent,
|
||||||
thinkingSource,
|
thinkingSource,
|
||||||
styleState,
|
styleState,
|
||||||
onToggleReasoningExpansion
|
onToggleReasoningExpansion,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
const lastContentRef = useRef('');
|
const lastContentRef = useRef('');
|
||||||
|
|
||||||
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
const isThinkingStatus =
|
||||||
const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
|
message.status === 'loading' || message.status === 'incomplete';
|
||||||
|
const headerText =
|
||||||
|
isThinkingStatus && !message.isThinkingComplete
|
||||||
|
? t('思考中...')
|
||||||
|
: t('思考过程');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current && finalExtractedThinkingContent && message.isReasoningExpanded) {
|
if (
|
||||||
|
scrollRef.current &&
|
||||||
|
finalExtractedThinkingContent &&
|
||||||
|
message.isReasoningExpanded
|
||||||
|
) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [finalExtractedThinkingContent, message.isReasoningExpanded]);
|
}, [finalExtractedThinkingContent, message.isReasoningExpanded]);
|
||||||
@@ -63,72 +71,100 @@ const ThinkingContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<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={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
|
background:
|
||||||
position: 'relative'
|
'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
onClick={() => onToggleReasoningExpansion(message.id)}
|
onClick={() => onToggleReasoningExpansion(message.id)}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<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 -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 -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 sm:gap-4 relative">
|
<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">
|
<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} />
|
<Brain
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
size={styleState.isMobile ? 12 : 16}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<Typography.Text strong style={{ color: 'white' }} className="text-sm sm:text-base">
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
className='text-sm sm:text-base'
|
||||||
|
>
|
||||||
{headerText}
|
{headerText}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{thinkingSource && (
|
{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}
|
来源: {thinkingSource}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{isThinkingStatus && !message.isThinkingComplete && (
|
||||||
<div className="flex items-center gap-1 sm:gap-2">
|
<div className='flex items-center gap-1 sm:gap-2'>
|
||||||
<Loader2 style={{ color: 'white' }} className="animate-spin" size={styleState.isMobile ? 14 : 18} />
|
<Loader2
|
||||||
<Typography.Text style={{ color: 'white' }} className="text-xs sm:text-sm font-medium opacity-90">
|
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>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(!isThinkingStatus || message.isThinkingComplete) && (
|
{(!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">
|
<div className='w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center'>
|
||||||
{message.isReasoningExpanded ?
|
{message.isReasoningExpanded ? (
|
||||||
<ChevronUp size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} /> :
|
<ChevronUp
|
||||||
<ChevronRight size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} />
|
size={styleState.isMobile ? 12 : 16}
|
||||||
}
|
style={{ color: 'white' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
size={styleState.isMobile ? 12 : 16}
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
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`}
|
} overflow-hidden bg-gradient-to-br from-purple-50 via-indigo-50 to-violet-50`}
|
||||||
>
|
>
|
||||||
{message.isReasoningExpanded && (
|
{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
|
<div
|
||||||
ref={scrollRef}
|
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={{
|
style={{
|
||||||
maxHeight: '200px',
|
maxHeight: '200px',
|
||||||
scrollbarWidth: 'thin',
|
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
|
<MarkdownRenderer
|
||||||
content={finalExtractedThinkingContent}
|
content={finalExtractedThinkingContent}
|
||||||
className=""
|
className=''
|
||||||
animated={isThinkingStatus}
|
animated={isThinkingStatus}
|
||||||
previousContentLength={prevLength}
|
previousContentLength={prevLength}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
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';
|
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
||||||
|
|
||||||
@@ -72,9 +75,12 @@ export const loadConfig = () => {
|
|||||||
...DEFAULT_CONFIG.parameterEnabled,
|
...DEFAULT_CONFIG.parameterEnabled,
|
||||||
...parsedConfig.parameterEnabled,
|
...parsedConfig.parameterEnabled,
|
||||||
},
|
},
|
||||||
showDebugPanel: parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
|
showDebugPanel:
|
||||||
customRequestMode: parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
|
parsedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
|
||||||
customRequestBody: parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
|
customRequestMode:
|
||||||
|
parsedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
|
||||||
|
customRequestBody:
|
||||||
|
parsedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
|
||||||
};
|
};
|
||||||
|
|
||||||
return mergedConfig;
|
return mergedConfig;
|
||||||
@@ -180,7 +186,6 @@ export const exportConfig = (config, messages = null) => {
|
|||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
URL.revokeObjectURL(link.href);
|
URL.revokeObjectURL(link.href);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出配置失败:', error);
|
console.error('导出配置失败:', error);
|
||||||
}
|
}
|
||||||
@@ -201,7 +206,10 @@ export const importConfig = (file) => {
|
|||||||
|
|
||||||
if (importedConfig.inputs && importedConfig.parameterEnabled) {
|
if (importedConfig.inputs && importedConfig.parameterEnabled) {
|
||||||
// 如果导入的配置包含消息,也一起导入
|
// 如果导入的配置包含消息,也一起导入
|
||||||
if (importedConfig.messages && Array.isArray(importedConfig.messages)) {
|
if (
|
||||||
|
importedConfig.messages &&
|
||||||
|
Array.isArray(importedConfig.messages)
|
||||||
|
) {
|
||||||
saveMessages(importedConfig.messages);
|
saveMessages(importedConfig.messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
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 { useIsMobile } from '../../hooks/common/useIsMobile';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@@ -31,7 +36,9 @@ import {
|
|||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
|
import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
|
||||||
|
|
||||||
const ChannelSelectorModal = forwardRef(({
|
const ChannelSelectorModal = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
visible,
|
visible,
|
||||||
onCancel,
|
onCancel,
|
||||||
onOk,
|
onOk,
|
||||||
@@ -41,7 +48,9 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
channelEndpoints,
|
channelEndpoints,
|
||||||
updateChannelEndpoint,
|
updateChannelEndpoint,
|
||||||
t,
|
t,
|
||||||
}, ref) => {
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
@@ -111,7 +120,7 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<Select
|
<Select
|
||||||
size="small"
|
size='small'
|
||||||
value={currentType}
|
value={currentType}
|
||||||
onChange={handleTypeChange}
|
onChange={handleTypeChange}
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
@@ -123,10 +132,10 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
/>
|
/>
|
||||||
{currentType === 'custom' && (
|
{currentType === 'custom' && (
|
||||||
<Input
|
<Input
|
||||||
size="small"
|
size='small'
|
||||||
value={currentEndpoint}
|
value={currentEndpoint}
|
||||||
onChange={(val) => updateEndpoint(channelId, val)}
|
onChange={(val) => updateEndpoint(channelId, val)}
|
||||||
placeholder="/your/endpoint"
|
placeholder='/your/endpoint'
|
||||||
style={{ width: 160, fontSize: 12 }}
|
style={{ width: 160, fontSize: 12 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -138,7 +147,11 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
<Tag
|
||||||
|
color='green'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<CheckCircle size={14} />}
|
||||||
|
>
|
||||||
{t('已启用')}
|
{t('已启用')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -150,13 +163,21 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
<Tag
|
||||||
|
color='yellow'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<AlertCircle size={14} />}
|
||||||
|
>
|
||||||
{t('自动禁用')}
|
{t('自动禁用')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
<Tag
|
||||||
|
color='grey'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<HelpCircle size={14} />}
|
||||||
|
>
|
||||||
{t('未知状态')}
|
{t('未知状态')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -180,12 +201,14 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
{
|
{
|
||||||
title: t('源地址'),
|
title: t('源地址'),
|
||||||
dataIndex: '_originalData.base_url',
|
dataIndex: '_originalData.base_url',
|
||||||
render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
|
render: (_, record) =>
|
||||||
|
renderBaseUrlCell(record._originalData?.base_url || ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('状态'),
|
title: t('状态'),
|
||||||
dataIndex: '_originalData.status',
|
dataIndex: '_originalData.status',
|
||||||
render: (_, record) => renderStatusCell(record._originalData?.status || 0),
|
render: (_, record) =>
|
||||||
|
renderStatusCell(record._originalData?.status || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('同步接口'),
|
title: t('同步接口'),
|
||||||
@@ -205,7 +228,9 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
visible={visible}
|
visible={visible}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onOk={onOk}
|
onOk={onOk}
|
||||||
title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
|
title={
|
||||||
|
<span className='text-lg font-semibold'>{t('选择同步渠道')}</span>
|
||||||
|
}
|
||||||
size={isMobile ? 'full-width' : 'large'}
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
keepDOM
|
keepDOM
|
||||||
lazyRender={false}
|
lazyRender={false}
|
||||||
@@ -222,7 +247,7 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={paginatedData}
|
dataSource={paginatedData}
|
||||||
rowKey="key"
|
rowKey='key'
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage: currentPage,
|
currentPage: currentPage,
|
||||||
@@ -240,11 +265,12 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
setPageSize(size);
|
setPageSize(size);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default ChannelSelectorModal;
|
export default ChannelSelectorModal;
|
||||||
@@ -62,8 +62,7 @@ const DashboardSetting = () => {
|
|||||||
if (item.key in inputs) {
|
if (item.key in inputs) {
|
||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
}
|
}
|
||||||
if (item.key.endsWith('Enabled') &&
|
if (item.key.endsWith('Enabled') && item.key === 'DataExportEnabled') {
|
||||||
(item.key === 'DataExportEnabled')) {
|
|
||||||
newInputs[item.key] = toBoolean(item.value);
|
newInputs[item.key] = toBoolean(item.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -91,8 +90,14 @@ const DashboardSetting = () => {
|
|||||||
|
|
||||||
// 用于迁移检测的旧键,下个版本会删除
|
// 用于迁移检测的旧键,下个版本会删除
|
||||||
const hasLegacyData = useMemo(() => {
|
const hasLegacyData = useMemo(() => {
|
||||||
const legacyKeys = ['ApiInfo', 'Announcements', 'FAQ', 'UptimeKumaUrl', 'UptimeKumaSlug'];
|
const legacyKeys = [
|
||||||
return legacyKeys.some(k => inputs[k]);
|
'ApiInfo',
|
||||||
|
'Announcements',
|
||||||
|
'FAQ',
|
||||||
|
'UptimeKumaUrl',
|
||||||
|
'UptimeKumaSlug',
|
||||||
|
];
|
||||||
|
return legacyKeys.some((k) => inputs[k]);
|
||||||
}, [inputs]);
|
}, [inputs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -121,17 +126,18 @@ const DashboardSetting = () => {
|
|||||||
<Spin spinning={loading} size='large'>
|
<Spin spinning={loading} size='large'>
|
||||||
{/* 用于迁移检测的旧键模态框,下个版本会删除 */}
|
{/* 用于迁移检测的旧键模态框,下个版本会删除 */}
|
||||||
<Modal
|
<Modal
|
||||||
title="配置迁移确认"
|
title='配置迁移确认'
|
||||||
visible={showMigrateModal}
|
visible={showMigrateModal}
|
||||||
onOk={handleMigrate}
|
onOk={handleMigrate}
|
||||||
onCancel={() => setShowMigrateModal(false)}
|
onCancel={() => setShowMigrateModal(false)}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
okText="确认迁移"
|
okText='确认迁移'
|
||||||
cancelText="取消"
|
cancelText='取消'
|
||||||
>
|
>
|
||||||
<p>检测到旧版本的配置数据,是否要迁移到新的配置格式?</p>
|
<p>检测到旧版本的配置数据,是否要迁移到新的配置格式?</p>
|
||||||
<p style={{ color: '#f57c00', marginTop: '10px' }}>
|
<p style={{ color: '#f57c00', marginTop: '10px' }}>
|
||||||
<strong>注意:</strong>迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
|
<strong>注意:</strong>
|
||||||
|
迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
|
||||||
</p>
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ const PaymentSetting = () => {
|
|||||||
switch (item.key) {
|
switch (item.key) {
|
||||||
case 'TopupGroupRatio':
|
case 'TopupGroupRatio':
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error('解析TopupGroupRatio出错:', error);
|
console.error('解析TopupGroupRatio出错:', error);
|
||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
|
|||||||
@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
|
||||||
API,
|
|
||||||
copy,
|
|
||||||
showError,
|
|
||||||
showInfo,
|
|
||||||
showSuccess
|
|
||||||
} from '../../helpers';
|
|
||||||
import { UserContext } from '../../context/User';
|
import { UserContext } from '../../context/User';
|
||||||
import { Modal } from '@douyinfe/semi-ui';
|
import { Modal } from '@douyinfe/semi-ui';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -271,7 +265,11 @@ const PersonalSetting = () => {
|
|||||||
const handleNotificationSettingChange = (type, value) => {
|
const handleNotificationSettingChange = (type, value) => {
|
||||||
setNotificationSettings((prev) => ({
|
setNotificationSettings((prev) => ({
|
||||||
...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 (
|
return (
|
||||||
<div className="mt-[60px]">
|
<div className='mt-[60px]'>
|
||||||
<div className="flex justify-center">
|
<div className='flex justify-center'>
|
||||||
<div className="w-full max-w-7xl mx-auto px-2">
|
<div className='w-full max-w-7xl mx-auto px-2'>
|
||||||
{/* 顶部用户信息区域 */}
|
{/* 顶部用户信息区域 */}
|
||||||
<UserInfoHeader t={t} userState={userState} />
|
<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
|
<AccountManagement
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -103,34 +103,19 @@ const RatioSetting = () => {
|
|||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<Tabs type='card'>
|
<Tabs type='card'>
|
||||||
<Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
|
<Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
|
||||||
<ModelRatioSettings
|
<ModelRatioSettings options={inputs} refresh={onRefresh} />
|
||||||
options={inputs}
|
|
||||||
refresh={onRefresh}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={t('分组倍率设置')} itemKey='group'>
|
<Tabs.TabPane tab={t('分组倍率设置')} itemKey='group'>
|
||||||
<GroupRatioSettings
|
<GroupRatioSettings options={inputs} refresh={onRefresh} />
|
||||||
options={inputs}
|
|
||||||
refresh={onRefresh}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey='visual'>
|
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey='visual'>
|
||||||
<ModelSettingsVisualEditor
|
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
|
||||||
options={inputs}
|
|
||||||
refresh={onRefresh}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={t('未设置倍率模型')} itemKey='unset_models'>
|
<Tabs.TabPane tab={t('未设置倍率模型')} itemKey='unset_models'>
|
||||||
<ModelRatioNotSetEditor
|
<ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
|
||||||
options={inputs}
|
|
||||||
refresh={onRefresh}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
|
<Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
|
||||||
<UpstreamRatioSync
|
<UpstreamRatioSync options={inputs} refresh={onRefresh} />
|
||||||
options={inputs}
|
|
||||||
refresh={onRefresh}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -473,7 +473,10 @@ const SystemSetting = () => {
|
|||||||
value: inputs.LinuxDOClientSecret,
|
value: inputs.LinuxDOClientSecret,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (originInputs['LinuxDOMinimumTrustLevel'] !== inputs.LinuxDOMinimumTrustLevel) {
|
if (
|
||||||
|
originInputs['LinuxDOMinimumTrustLevel'] !==
|
||||||
|
inputs.LinuxDOMinimumTrustLevel
|
||||||
|
) {
|
||||||
options.push({
|
options.push({
|
||||||
key: 'LinuxDOMinimumTrustLevel',
|
key: 'LinuxDOMinimumTrustLevel',
|
||||||
value: inputs.LinuxDOMinimumTrustLevel,
|
value: inputs.LinuxDOMinimumTrustLevel,
|
||||||
@@ -530,11 +533,15 @@ const SystemSetting = () => {
|
|||||||
field='ServerAddress'
|
field='ServerAddress'
|
||||||
label={t('服务器地址')}
|
label={t('服务器地址')}
|
||||||
placeholder='https://yourdomain.com'
|
placeholder='https://yourdomain.com'
|
||||||
extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
|
extraText={t(
|
||||||
|
'该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
|
<Button onClick={submitServerAddress}>
|
||||||
|
{t('更新服务器地址')}
|
||||||
|
</Button>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -755,7 +762,10 @@ const SystemSetting = () => {
|
|||||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
>
|
>
|
||||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
<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>
|
||||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||||
<Form.Input field='SMTPPort' label={t('SMTP 端口')} />
|
<Form.Input field='SMTPPort' label={t('SMTP 端口')} />
|
||||||
@@ -769,7 +779,10 @@ const SystemSetting = () => {
|
|||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
>
|
>
|
||||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
<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>
|
||||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
@@ -797,7 +810,9 @@ const SystemSetting = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<Form.Section text={t('配置 OIDC')}>
|
<Form.Section text={t('配置 OIDC')}>
|
||||||
<Text>
|
<Text>
|
||||||
{t('用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP')}
|
{t(
|
||||||
|
'用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Banner
|
<Banner
|
||||||
type='info'
|
type='info'
|
||||||
@@ -805,7 +820,9 @@ const SystemSetting = () => {
|
|||||||
style={{ marginBottom: 20, marginTop: 16 }}
|
style={{ marginBottom: 20, marginTop: 16 }}
|
||||||
/>
|
/>
|
||||||
<Text>
|
<Text>
|
||||||
{t('若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置')}
|
{t(
|
||||||
|
'若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Row
|
<Row
|
||||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
@@ -862,7 +879,9 @@ const SystemSetting = () => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Button onClick={submitOIDCSettings}>{t('保存 OIDC 设置')}</Button>
|
<Button onClick={submitOIDCSettings}>
|
||||||
|
{t('保存 OIDC 设置')}
|
||||||
|
</Button>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -1033,7 +1052,9 @@ const SystemSetting = () => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Button onClick={submitTurnstile}>{t('保存 Turnstile 设置')}</Button>
|
<Button onClick={submitTurnstile}>
|
||||||
|
{t('保存 Turnstile 设置')}
|
||||||
|
</Button>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -1048,7 +1069,11 @@ const SystemSetting = () => {
|
|||||||
okText={t('确认')}
|
okText={t('确认')}
|
||||||
cancelText={t('取消')}
|
cancelText={t('取消')}
|
||||||
>
|
>
|
||||||
<p>{t('您确定要取消密码登录功能吗?这可能会影响用户的登录方式。')}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
'您确定要取消密码登录功能吗?这可能会影响用户的登录方式。',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabPane,
|
TabPane,
|
||||||
Popover
|
Popover,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconMail,
|
IconMail,
|
||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
IconGithubLogo,
|
IconGithubLogo,
|
||||||
IconKey,
|
IconKey,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconDelete
|
IconDelete,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
||||||
import { UserPlus, ShieldCheck } from 'lucide-react';
|
import { UserPlus, ShieldCheck } from 'lucide-react';
|
||||||
@@ -43,7 +43,7 @@ import TelegramLoginButton from 'react-telegram-login';
|
|||||||
import {
|
import {
|
||||||
onGitHubOAuthClicked,
|
onGitHubOAuthClicked,
|
||||||
onOIDCClicked,
|
onOIDCClicked,
|
||||||
onLinuxDOOAuthClicked
|
onLinuxDOOAuthClicked,
|
||||||
} from '../../../../helpers';
|
} from '../../../../helpers';
|
||||||
import TwoFASetting from '../components/TwoFASetting';
|
import TwoFASetting from '../components/TwoFASetting';
|
||||||
|
|
||||||
@@ -57,77 +57,89 @@ const AccountManagement = ({
|
|||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
handleSystemTokenClick,
|
handleSystemTokenClick,
|
||||||
setShowChangePasswordModal,
|
setShowChangePasswordModal,
|
||||||
setShowAccountDeleteModal
|
setShowAccountDeleteModal,
|
||||||
}) => {
|
}) => {
|
||||||
const renderAccountInfo = (accountId, label) => {
|
const renderAccountInfo = (accountId, label) => {
|
||||||
if (!accountId || accountId === '') {
|
if (!accountId || accountId === '') {
|
||||||
return <span className="text-gray-500">{t('未绑定')}</span>;
|
return <span className='text-gray-500'>{t('未绑定')}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<div className="text-xs p-2">
|
<div className='text-xs p-2'>
|
||||||
<Typography.Paragraph copyable={{ content: accountId }}>
|
<Typography.Paragraph copyable={{ content: accountId }}>
|
||||||
{accountId}
|
{accountId}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
{label ? (
|
{label ? (
|
||||||
<div className="mt-1 text-[11px] text-gray-500">{label}</div>
|
<div className='mt-1 text-[11px] text-gray-500'>{label}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} position="top" trigger="hover">
|
<Popover content={popContent} position='top' trigger='hover'>
|
||||||
<span className="block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer">
|
<span className='block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer'>
|
||||||
{accountId}
|
{accountId}
|
||||||
</span>
|
</span>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Card className="!rounded-2xl">
|
<Card className='!rounded-2xl'>
|
||||||
{/* 卡片头部 */}
|
{/* 卡片头部 */}
|
||||||
<div className="flex items-center mb-4">
|
<div className='flex items-center mb-4'>
|
||||||
<Avatar size="small" color="teal" className="mr-3 shadow-md">
|
<Avatar size='small' color='teal' className='mr-3 shadow-md'>
|
||||||
<UserPlus size={16} />
|
<UserPlus size={16} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text className="text-lg font-medium">{t('账户管理')}</Typography.Text>
|
<Typography.Text className='text-lg font-medium'>
|
||||||
<div className="text-xs text-gray-600">{t('账户绑定、安全设置和身份验证')}</div>
|
{t('账户管理')}
|
||||||
|
</Typography.Text>
|
||||||
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('账户绑定、安全设置和身份验证')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs type="card" defaultActiveKey="binding">
|
<Tabs type='card' defaultActiveKey='binding'>
|
||||||
{/* 账户绑定 Tab */}
|
{/* 账户绑定 Tab */}
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<UserPlus size={16} className="mr-2" />
|
<UserPlus size={16} className='mr-2' />
|
||||||
{t('账户绑定')}
|
{t('账户绑定')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="binding"
|
itemKey='binding'
|
||||||
>
|
>
|
||||||
<div className="py-4">
|
<div className='py-4'>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
|
||||||
{/* 邮箱绑定 */}
|
{/* 邮箱绑定 */}
|
||||||
<Card className="!rounded-xl">
|
<Card className='!rounded-xl'>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<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">
|
<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" />
|
<IconMail
|
||||||
|
size='default'
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className='flex-1 min-w-0'>
|
||||||
<div className="font-medium text-gray-900">{t('邮箱')}</div>
|
<div className='font-medium text-gray-900'>
|
||||||
<div className="text-sm text-gray-500 truncate">
|
{t('邮箱')}
|
||||||
{renderAccountInfo(userState.user?.email, t('邮箱地址'))}
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
|
{renderAccountInfo(
|
||||||
|
userState.user?.email,
|
||||||
|
t('邮箱地址'),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => setShowEmailBindModal(true)}
|
onClick={() => setShowEmailBindModal(true)}
|
||||||
>
|
>
|
||||||
{userState.user && userState.user.email !== ''
|
{userState.user && userState.user.email !== ''
|
||||||
@@ -139,26 +151,31 @@ const AccountManagement = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 微信绑定 */}
|
{/* 微信绑定 */}
|
||||||
<Card className="!rounded-xl">
|
<Card className='!rounded-xl'>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<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">
|
<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" />
|
<SiWechat
|
||||||
|
size={20}
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className='flex-1 min-w-0'>
|
||||||
<div className="font-medium text-gray-900">{t('微信')}</div>
|
<div className='font-medium text-gray-900'>
|
||||||
<div className="text-sm text-gray-500 truncate">
|
{t('微信')}
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
{userState.user && userState.user.wechat_id !== ''
|
{userState.user && userState.user.wechat_id !== ''
|
||||||
? t('已绑定')
|
? t('已绑定')
|
||||||
: t('未绑定')}
|
: t('未绑定')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
size="small"
|
size='small'
|
||||||
disabled={!status.wechat_login}
|
disabled={!status.wechat_login}
|
||||||
onClick={() => setShowWeChatBindModal(true)}
|
onClick={() => setShowWeChatBindModal(true)}
|
||||||
>
|
>
|
||||||
@@ -173,25 +190,35 @@ const AccountManagement = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* GitHub绑定 */}
|
{/* GitHub绑定 */}
|
||||||
<Card className="!rounded-xl">
|
<Card className='!rounded-xl'>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<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">
|
<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" />
|
<IconGithubLogo
|
||||||
|
size='default'
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className='flex-1 min-w-0'>
|
||||||
<div className="font-medium text-gray-900">{t('GitHub')}</div>
|
<div className='font-medium text-gray-900'>
|
||||||
<div className="text-sm text-gray-500 truncate">
|
{t('GitHub')}
|
||||||
{renderAccountInfo(userState.user?.github_id, t('GitHub ID'))}
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
|
{renderAccountInfo(
|
||||||
|
userState.user?.github_id,
|
||||||
|
t('GitHub ID'),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
|
onClick={() =>
|
||||||
|
onGitHubOAuthClicked(status.github_client_id)
|
||||||
|
}
|
||||||
disabled={
|
disabled={
|
||||||
(userState.user && userState.user.github_id !== '') ||
|
(userState.user && userState.user.github_id !== '') ||
|
||||||
!status.github_oauth
|
!status.github_oauth
|
||||||
@@ -204,28 +231,38 @@ const AccountManagement = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* OIDC绑定 */}
|
{/* OIDC绑定 */}
|
||||||
<Card className="!rounded-xl">
|
<Card className='!rounded-xl'>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<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">
|
<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" />
|
<IconShield
|
||||||
|
size='default'
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className='flex-1 min-w-0'>
|
||||||
<div className="font-medium text-gray-900">{t('OIDC')}</div>
|
<div className='font-medium text-gray-900'>
|
||||||
<div className="text-sm text-gray-500 truncate">
|
{t('OIDC')}
|
||||||
{renderAccountInfo(userState.user?.oidc_id, t('OIDC ID'))}
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
|
{renderAccountInfo(
|
||||||
|
userState.user?.oidc_id,
|
||||||
|
t('OIDC ID'),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => onOIDCClicked(
|
onClick={() =>
|
||||||
|
onOIDCClicked(
|
||||||
status.oidc_authorization_endpoint,
|
status.oidc_authorization_endpoint,
|
||||||
status.oidc_client_id,
|
status.oidc_client_id,
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
disabled={
|
disabled={
|
||||||
(userState.user && userState.user.oidc_id !== '') ||
|
(userState.user && userState.user.oidc_id !== '') ||
|
||||||
!status.oidc_enabled
|
!status.oidc_enabled
|
||||||
@@ -238,27 +275,35 @@ const AccountManagement = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Telegram绑定 */}
|
{/* Telegram绑定 */}
|
||||||
<Card className="!rounded-xl">
|
<Card className='!rounded-xl'>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<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">
|
<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" />
|
<SiTelegram
|
||||||
|
size={20}
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className='flex-1 min-w-0'>
|
||||||
<div className="font-medium text-gray-900">{t('Telegram')}</div>
|
<div className='font-medium text-gray-900'>
|
||||||
<div className="text-sm text-gray-500 truncate">
|
{t('Telegram')}
|
||||||
{renderAccountInfo(userState.user?.telegram_id, t('Telegram ID'))}
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
|
{renderAccountInfo(
|
||||||
|
userState.user?.telegram_id,
|
||||||
|
t('Telegram ID'),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
{status.telegram_oauth ? (
|
{status.telegram_oauth ? (
|
||||||
userState.user.telegram_id !== '' ? (
|
userState.user.telegram_id !== '' ? (
|
||||||
<Button disabled={true} size="small">
|
<Button disabled={true} size='small'>
|
||||||
{t('已绑定')}
|
{t('已绑定')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="scale-75">
|
<div className='scale-75'>
|
||||||
<TelegramLoginButton
|
<TelegramLoginButton
|
||||||
dataAuthUrl='/api/oauth/telegram/bind'
|
dataAuthUrl='/api/oauth/telegram/bind'
|
||||||
botName={status.telegram_bot_name}
|
botName={status.telegram_bot_name}
|
||||||
@@ -266,7 +311,7 @@ const AccountManagement = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Button disabled={true} size="small">
|
<Button disabled={true} size='small'>
|
||||||
{t('未启用')}
|
{t('未启用')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -275,25 +320,35 @@ const AccountManagement = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* LinuxDO绑定 */}
|
{/* LinuxDO绑定 */}
|
||||||
<Card className="!rounded-xl">
|
<Card className='!rounded-xl'>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<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">
|
<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" />
|
<SiLinux
|
||||||
|
size={20}
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className='flex-1 min-w-0'>
|
||||||
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
|
<div className='font-medium text-gray-900'>
|
||||||
<div className="text-sm text-gray-500 truncate">
|
{t('LinuxDO')}
|
||||||
{renderAccountInfo(userState.user?.linux_do_id, t('LinuxDO ID'))}
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
|
{renderAccountInfo(
|
||||||
|
userState.user?.linux_do_id,
|
||||||
|
t('LinuxDO ID'),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
|
onClick={() =>
|
||||||
|
onLinuxDOOAuthClicked(status.linuxdo_client_id)
|
||||||
|
}
|
||||||
disabled={
|
disabled={
|
||||||
(userState.user && userState.user.linux_do_id !== '') ||
|
(userState.user && userState.user.linux_do_id !== '') ||
|
||||||
!status.linuxdo_oauth
|
!status.linuxdo_oauth
|
||||||
@@ -311,37 +366,37 @@ const AccountManagement = ({
|
|||||||
{/* 安全设置 Tab */}
|
{/* 安全设置 Tab */}
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<ShieldCheck size={16} className="mr-2" />
|
<ShieldCheck size={16} className='mr-2' />
|
||||||
{t('安全设置')}
|
{t('安全设置')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="security"
|
itemKey='security'
|
||||||
>
|
>
|
||||||
<div className="py-4">
|
<div className='py-4'>
|
||||||
<div className="space-y-6">
|
<div className='space-y-6'>
|
||||||
<Space vertical className='w-full'>
|
<Space vertical className='w-full'>
|
||||||
{/* 系统访问令牌 */}
|
{/* 系统访问令牌 */}
|
||||||
<Card className="!rounded-xl 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 flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||||
<div className="flex items-start w-full sm:w-auto">
|
<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">
|
<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" />
|
<IconKey size='large' className='text-slate-600' />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className='flex-1'>
|
||||||
<Typography.Title heading={6} className="mb-1">
|
<Typography.Title heading={6} className='mb-1'>
|
||||||
{t('系统访问令牌')}
|
{t('系统访问令牌')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="tertiary" className="text-sm">
|
<Typography.Text type='tertiary' className='text-sm'>
|
||||||
{t('用于API调用的身份验证令牌,请妥善保管')}
|
{t('用于API调用的身份验证令牌,请妥善保管')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{systemToken && (
|
{systemToken && (
|
||||||
<div className="mt-3">
|
<div className='mt-3'>
|
||||||
<Input
|
<Input
|
||||||
readonly
|
readonly
|
||||||
value={systemToken}
|
value={systemToken}
|
||||||
onClick={handleSystemTokenClick}
|
onClick={handleSystemTokenClick}
|
||||||
size="large"
|
size='large'
|
||||||
prefix={<IconKey />}
|
prefix={<IconKey />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,10 +404,10 @@ const AccountManagement = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
onClick={generateAccessToken}
|
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 />}
|
icon={<IconKey />}
|
||||||
>
|
>
|
||||||
{systemToken ? t('重新生成') : t('生成令牌')}
|
{systemToken ? t('重新生成') : t('生成令牌')}
|
||||||
@@ -361,26 +416,26 @@ const AccountManagement = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 密码管理 */}
|
{/* 密码管理 */}
|
||||||
<Card className="!rounded-xl 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 flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||||
<div className="flex items-start w-full sm:w-auto">
|
<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">
|
<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" />
|
<IconLock size='large' className='text-slate-600' />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={6} className="mb-1">
|
<Typography.Title heading={6} className='mb-1'>
|
||||||
{t('密码管理')}
|
{t('密码管理')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="tertiary" className="text-sm">
|
<Typography.Text type='tertiary' className='text-sm'>
|
||||||
{t('定期更改密码可以提高账户安全性')}
|
{t('定期更改密码可以提高账户安全性')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
onClick={() => setShowChangePasswordModal(true)}
|
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 />}
|
icon={<IconLock />}
|
||||||
>
|
>
|
||||||
{t('修改密码')}
|
{t('修改密码')}
|
||||||
@@ -392,26 +447,29 @@ const AccountManagement = ({
|
|||||||
<TwoFASetting t={t} />
|
<TwoFASetting t={t} />
|
||||||
|
|
||||||
{/* 危险区域 */}
|
{/* 危险区域 */}
|
||||||
<Card className="!rounded-xl 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 flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||||
<div className="flex items-start w-full sm:w-auto">
|
<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">
|
<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" />
|
<IconDelete size='large' className='text-slate-600' />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={6} className="mb-1 text-slate-700">
|
<Typography.Title
|
||||||
|
heading={6}
|
||||||
|
className='mb-1 text-slate-700'
|
||||||
|
>
|
||||||
{t('删除账户')}
|
{t('删除账户')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="tertiary" className="text-sm">
|
<Typography.Text type='tertiary' className='text-sm'>
|
||||||
{t('此操作不可逆,所有数据将被永久删除')}
|
{t('此操作不可逆,所有数据将被永久删除')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="danger"
|
type='danger'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
onClick={() => setShowAccountDeleteModal(true)}
|
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 />}
|
icon={<IconDelete />}
|
||||||
>
|
>
|
||||||
{t('删除账户')}
|
{t('删除账户')}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
TabPane,
|
TabPane,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar
|
Avatar,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
IllustrationNoContent,
|
||||||
IconChevronUp
|
IllustrationNoContentDark,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-illustrations';
|
||||||
|
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import { renderModelTag, getModelCategories } from '../../../../helpers';
|
import { renderModelTag, getModelCategories } from '../../../../helpers';
|
||||||
|
|
||||||
@@ -52,38 +52,48 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
}, [isModelsExpanded]);
|
}, [isModelsExpanded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-4">
|
<div className='py-4'>
|
||||||
{/* 卡片头部 */}
|
{/* 卡片头部 */}
|
||||||
<div className="flex items-center mb-4">
|
<div className='flex items-center mb-4'>
|
||||||
<Avatar size="small" color="green" className="mr-3 shadow-md">
|
<Avatar size='small' color='green' className='mr-3 shadow-md'>
|
||||||
<Settings size={16} />
|
<Settings size={16} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text className="text-lg font-medium">{t('可用模型')}</Typography.Text>
|
<Typography.Text className='text-lg font-medium'>
|
||||||
<div className="text-xs text-gray-600">{t('查看当前可用的所有模型')}</div>
|
{t('可用模型')}
|
||||||
|
</Typography.Text>
|
||||||
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('查看当前可用的所有模型')}
|
||||||
|
</div>
|
||||||
</div>
|
</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 ? (
|
{modelsLoading ? (
|
||||||
// 骨架屏加载状态 - 模拟实际加载后的布局
|
// 骨架屏加载状态 - 模拟实际加载后的布局
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
{/* 模拟分类标签 */}
|
{/* 模拟分类标签 */}
|
||||||
<div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
|
<div
|
||||||
<div className="flex overflow-x-auto py-2 gap-2">
|
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) => (
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
<Skeleton.Button key={`cat-${index}`} style={{
|
<Skeleton.Button
|
||||||
|
key={`cat-${index}`}
|
||||||
|
style={{
|
||||||
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 8
|
borderRadius: 8,
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 模拟模型标签列表 */}
|
{/* 模拟模型标签列表 */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className='flex flex-wrap gap-2'>
|
||||||
{Array.from({ length: 20 }).map((_, index) => (
|
{Array.from({ length: 20 }).map((_, index) => (
|
||||||
<Skeleton.Button
|
<Skeleton.Button
|
||||||
key={`model-${index}`}
|
key={`model-${index}`}
|
||||||
@@ -91,17 +101,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
width: 100 + Math.random() * 100,
|
width: 100 + Math.random() * 100,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
margin: '4px'
|
margin: '4px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : models.length === 0 ? (
|
) : models.length === 0 ? (
|
||||||
<div className="py-8">
|
<div className='py-8'>
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
image={
|
||||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
<IllustrationNoContent style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoContentDark
|
||||||
|
style={{ width: 150, height: 150 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
description={t('没有可用模型')}
|
description={t('没有可用模型')}
|
||||||
style={{ padding: '24px 0' }}
|
style={{ padding: '24px 0' }}
|
||||||
/>
|
/>
|
||||||
@@ -109,30 +125,38 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 模型分类标签页 */}
|
{/* 模型分类标签页 */}
|
||||||
<div className="mb-4">
|
<div className='mb-4'>
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type='card'
|
||||||
activeKey={activeModelCategory}
|
activeKey={activeModelCategory}
|
||||||
onChange={key => setActiveModelCategory(key)}
|
onChange={(key) => setActiveModelCategory(key)}
|
||||||
className="mt-2"
|
className='mt-2'
|
||||||
collapsible
|
collapsible
|
||||||
>
|
>
|
||||||
{Object.entries(getModelCategories(t)).map(([key, category]) => {
|
{Object.entries(getModelCategories(t)).map(
|
||||||
|
([key, category]) => {
|
||||||
// 计算该分类下的模型数量
|
// 计算该分类下的模型数量
|
||||||
const modelCount = key === 'all'
|
const modelCount =
|
||||||
|
key === 'all'
|
||||||
? models.length
|
? models.length
|
||||||
: models.filter(model => category.filter({ model_name: model })).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 (
|
return (
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<span className="flex items-center gap-2">
|
<span className='flex items-center gap-2'>
|
||||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
{category.icon && (
|
||||||
|
<span className='w-4 h-4'>{category.icon}</span>
|
||||||
|
)}
|
||||||
{category.label}
|
{category.label}
|
||||||
<Tag
|
<Tag
|
||||||
color={activeModelCategory === key ? 'red' : 'grey'}
|
color={
|
||||||
|
activeModelCategory === key ? 'red' : 'grey'
|
||||||
|
}
|
||||||
size='small'
|
size='small'
|
||||||
shape='circle'
|
shape='circle'
|
||||||
>
|
>
|
||||||
@@ -144,24 +168,38 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
key={key}
|
key={key}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</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 categories = getModelCategories(t);
|
||||||
const filteredModels = activeModelCategory === 'all'
|
const filteredModels =
|
||||||
|
activeModelCategory === 'all'
|
||||||
? models
|
? models
|
||||||
: models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
|
: models.filter((model) =>
|
||||||
|
categories[activeModelCategory].filter({
|
||||||
|
model_name: model,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 如果过滤后没有模型,显示空状态
|
// 如果过滤后没有模型,显示空状态
|
||||||
if (filteredModels.length === 0) {
|
if (filteredModels.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
|
image={
|
||||||
darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
|
<IllustrationNoContent
|
||||||
|
style={{ width: 120, height: 120 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoContentDark
|
||||||
|
style={{ width: 120, height: 120 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
description={t('该分类下没有可用模型')}
|
description={t('该分类下没有可用模型')}
|
||||||
style={{ padding: '16px 0' }}
|
style={{ padding: '16px 0' }}
|
||||||
/>
|
/>
|
||||||
@@ -171,13 +209,13 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
|
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{filteredModels.map((model) => (
|
{filteredModels.map((model) =>
|
||||||
renderModelTag(model, {
|
renderModelTag(model, {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
shape: 'circle',
|
shape: 'circle',
|
||||||
onClick: () => copyText(model),
|
onClick: () => copyText(model),
|
||||||
})
|
}),
|
||||||
))}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -185,17 +223,17 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
<>
|
<>
|
||||||
<Collapsible isOpen={isModelsExpanded}>
|
<Collapsible isOpen={isModelsExpanded}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{filteredModels.map((model) => (
|
{filteredModels.map((model) =>
|
||||||
renderModelTag(model, {
|
renderModelTag(model, {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
shape: 'circle',
|
shape: 'circle',
|
||||||
onClick: () => copyText(model),
|
onClick: () => copyText(model),
|
||||||
})
|
}),
|
||||||
))}
|
)}
|
||||||
<Tag
|
<Tag
|
||||||
color='grey'
|
color='grey'
|
||||||
type='light'
|
type='light'
|
||||||
className="cursor-pointer !rounded-lg"
|
className='cursor-pointer !rounded-lg'
|
||||||
onClick={() => setIsModelsExpanded(false)}
|
onClick={() => setIsModelsExpanded(false)}
|
||||||
icon={<IconChevronUp />}
|
icon={<IconChevronUp />}
|
||||||
>
|
>
|
||||||
@@ -207,21 +245,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
|||||||
<Space wrap>
|
<Space wrap>
|
||||||
{filteredModels
|
{filteredModels
|
||||||
.slice(0, MODELS_DISPLAY_COUNT)
|
.slice(0, MODELS_DISPLAY_COUNT)
|
||||||
.map((model) => (
|
.map((model) =>
|
||||||
renderModelTag(model, {
|
renderModelTag(model, {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
shape: 'circle',
|
shape: 'circle',
|
||||||
onClick: () => copyText(model),
|
onClick: () => copyText(model),
|
||||||
})
|
}),
|
||||||
))}
|
)}
|
||||||
<Tag
|
<Tag
|
||||||
color='grey'
|
color='grey'
|
||||||
type='light'
|
type='light'
|
||||||
className="cursor-pointer !rounded-lg"
|
className='cursor-pointer !rounded-lg'
|
||||||
onClick={() => setIsModelsExpanded(true)}
|
onClick={() => setIsModelsExpanded(true)}
|
||||||
icon={<IconChevronDown />}
|
icon={<IconChevronDown />}
|
||||||
>
|
>
|
||||||
{t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
{t('更多')}{' '}
|
||||||
|
{filteredModels.length - MODELS_DISPLAY_COUNT}{' '}
|
||||||
|
{t('个模型')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,14 +27,9 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
Toast,
|
Toast,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabPane
|
TabPane,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
|
||||||
IconMail,
|
|
||||||
IconKey,
|
|
||||||
IconBell,
|
|
||||||
IconLink
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
|
import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
|
||||||
import { renderQuotaWithPrompt } from '../../../../helpers';
|
import { renderQuotaWithPrompt } from '../../../../helpers';
|
||||||
import CodeViewer from '../../../playground/CodeViewer';
|
import CodeViewer from '../../../playground/CodeViewer';
|
||||||
@@ -43,7 +38,7 @@ const NotificationSettings = ({
|
|||||||
t,
|
t,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
handleNotificationSettingChange,
|
handleNotificationSettingChange,
|
||||||
saveNotificationSettings
|
saveNotificationSettings,
|
||||||
}) => {
|
}) => {
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
|
|
||||||
@@ -62,7 +57,8 @@ const NotificationSettings = ({
|
|||||||
// 表单提交
|
// 表单提交
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (formApiRef.current) {
|
if (formApiRef.current) {
|
||||||
formApiRef.current.validate()
|
formApiRef.current
|
||||||
|
.validate()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
saveNotificationSettings();
|
saveNotificationSettings();
|
||||||
})
|
})
|
||||||
@@ -77,26 +73,27 @@ const NotificationSettings = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="!rounded-2xl shadow-sm border-0"
|
className='!rounded-2xl shadow-sm border-0'
|
||||||
footer={
|
footer={
|
||||||
<div className="flex justify-end">
|
<div className='flex justify-end'>
|
||||||
<Button
|
<Button type='primary' onClick={handleSubmit}>
|
||||||
type='primary'
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
{t('保存设置')}
|
{t('保存设置')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 卡片头部 */}
|
{/* 卡片头部 */}
|
||||||
<div className="flex items-center mb-4">
|
<div className='flex items-center mb-4'>
|
||||||
<Avatar size="small" color="blue" className="mr-3 shadow-md">
|
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
|
||||||
<Bell size={16} />
|
<Bell size={16} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text className="text-lg font-medium">{t('其他设置')}</Typography.Text>
|
<Typography.Text className='text-lg font-medium'>
|
||||||
<div className="text-xs text-gray-600">{t('通知、价格和隐私相关设置')}</div>
|
{t('其他设置')}
|
||||||
|
</Typography.Text>
|
||||||
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('通知、价格和隐私相关设置')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,18 +103,18 @@ const NotificationSettings = ({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<Tabs type="card" defaultActiveKey="notification">
|
<Tabs type='card' defaultActiveKey='notification'>
|
||||||
{/* 通知配置 Tab */}
|
{/* 通知配置 Tab */}
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<Bell size={16} className="mr-2" />
|
<Bell size={16} className='mr-2' />
|
||||||
{t('通知配置')}
|
{t('通知配置')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="notification"
|
itemKey='notification'
|
||||||
>
|
>
|
||||||
<div className="py-4">
|
<div className='py-4'>
|
||||||
<Form.RadioGroup
|
<Form.RadioGroup
|
||||||
field='warningType'
|
field='warningType'
|
||||||
label={t('通知方式')}
|
label={t('通知方式')}
|
||||||
@@ -125,15 +122,18 @@ const NotificationSettings = ({
|
|||||||
onChange={(value) => handleFormChange('warningType', value)}
|
onChange={(value) => handleFormChange('warningType', value)}
|
||||||
rules={[{ required: true, message: t('请选择通知方式') }]}
|
rules={[{ required: true, message: t('请选择通知方式') }]}
|
||||||
>
|
>
|
||||||
<Radio value="email">{t('邮件通知')}</Radio>
|
<Radio value='email'>{t('邮件通知')}</Radio>
|
||||||
<Radio value="webhook">{t('Webhook通知')}</Radio>
|
<Radio value='webhook'>{t('Webhook通知')}</Radio>
|
||||||
</Form.RadioGroup>
|
</Form.RadioGroup>
|
||||||
|
|
||||||
<Form.AutoComplete
|
<Form.AutoComplete
|
||||||
field='warningThreshold'
|
field='warningThreshold'
|
||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
|
{t('额度预警阈值')}{' '}
|
||||||
|
{renderQuotaWithPrompt(
|
||||||
|
notificationSettings.warningThreshold,
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placeholder={t('请输入预警额度')}
|
placeholder={t('请输入预警额度')}
|
||||||
@@ -145,7 +145,9 @@ const NotificationSettings = ({
|
|||||||
]}
|
]}
|
||||||
onChange={(val) => handleFormChange('warningThreshold', val)}
|
onChange={(val) => handleFormChange('warningThreshold', val)}
|
||||||
prefix={<IconBell />}
|
prefix={<IconBell />}
|
||||||
extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
|
extraText={t(
|
||||||
|
'当剩余额度低于此数值时,系统将通过选择的方式发送通知',
|
||||||
|
)}
|
||||||
style={{ width: '100%', maxWidth: '300px' }}
|
style={{ width: '100%', maxWidth: '300px' }}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: t('请输入预警阈值') },
|
{ required: true, message: t('请输入预警阈值') },
|
||||||
@@ -156,8 +158,8 @@ const NotificationSettings = ({
|
|||||||
return Promise.reject(t('预警阈值必须为正数'));
|
return Promise.reject(t('预警阈值必须为正数'));
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -167,9 +169,13 @@ const NotificationSettings = ({
|
|||||||
field='notificationEmail'
|
field='notificationEmail'
|
||||||
label={t('通知邮箱')}
|
label={t('通知邮箱')}
|
||||||
placeholder={t('留空则使用账号绑定的邮箱')}
|
placeholder={t('留空则使用账号绑定的邮箱')}
|
||||||
onChange={(val) => handleFormChange('notificationEmail', val)}
|
onChange={(val) =>
|
||||||
|
handleFormChange('notificationEmail', val)
|
||||||
|
}
|
||||||
prefix={<IconMail />}
|
prefix={<IconMail />}
|
||||||
extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
|
extraText={t(
|
||||||
|
'设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
|
||||||
|
)}
|
||||||
showClear
|
showClear
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -180,20 +186,25 @@ const NotificationSettings = ({
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
field='webhookUrl'
|
field='webhookUrl'
|
||||||
label={t('Webhook地址')}
|
label={t('Webhook地址')}
|
||||||
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
|
placeholder={t(
|
||||||
|
'请输入Webhook地址,例如: https://example.com/webhook',
|
||||||
|
)}
|
||||||
onChange={(val) => handleFormChange('webhookUrl', val)}
|
onChange={(val) => handleFormChange('webhookUrl', val)}
|
||||||
prefix={<IconLink />}
|
prefix={<IconLink />}
|
||||||
extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
|
extraText={t(
|
||||||
|
'只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
|
||||||
|
)}
|
||||||
showClear
|
showClear
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: notificationSettings.warningType === 'webhook',
|
required:
|
||||||
message: t('请输入Webhook地址')
|
notificationSettings.warningType === 'webhook',
|
||||||
|
message: t('请输入Webhook地址'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: /^https:\/\/.+/,
|
pattern: /^https:\/\/.+/,
|
||||||
message: t('Webhook地址必须以https://开头')
|
message: t('Webhook地址必须以https://开头'),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -203,7 +214,9 @@ const NotificationSettings = ({
|
|||||||
placeholder={t('请输入密钥')}
|
placeholder={t('请输入密钥')}
|
||||||
onChange={(val) => handleFormChange('webhookSecret', val)}
|
onChange={(val) => handleFormChange('webhookSecret', val)}
|
||||||
prefix={<IconKey />}
|
prefix={<IconKey />}
|
||||||
extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
|
extraText={t(
|
||||||
|
'密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
|
||||||
|
)}
|
||||||
showClear
|
showClear
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -212,22 +225,36 @@ const NotificationSettings = ({
|
|||||||
<div style={{ height: '200px', marginBottom: '12px' }}>
|
<div style={{ height: '200px', marginBottom: '12px' }}>
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
content={{
|
content={{
|
||||||
"type": "quota_exceed",
|
type: 'quota_exceed',
|
||||||
"title": "额度预警通知",
|
title: '额度预警通知',
|
||||||
"content": "您的额度即将用尽,当前剩余额度为 {{value}}",
|
content:
|
||||||
"values": ["$0.99"],
|
'您的额度即将用尽,当前剩余额度为 {{value}}',
|
||||||
"timestamp": 1739950503
|
values: ['$0.99'],
|
||||||
|
timestamp: 1739950503,
|
||||||
}}
|
}}
|
||||||
title="webhook"
|
title='webhook'
|
||||||
language="json"
|
language='json'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 leading-relaxed">
|
<div className='text-xs text-gray-500 leading-relaxed'>
|
||||||
<div><strong>type:</strong> {t('通知类型 (quota_exceed: 额度预警)')} </div>
|
<div>
|
||||||
<div><strong>title:</strong> {t('通知标题')}</div>
|
<strong>type:</strong>{' '}
|
||||||
<div><strong>content:</strong> {t('通知内容,支持 {{value}} 变量占位符')}</div>
|
{t('通知类型 (quota_exceed: 额度预警)')}{' '}
|
||||||
<div><strong>values:</strong> {t('按顺序替换content中的变量占位符')}</div>
|
</div>
|
||||||
<div><strong>timestamp:</strong> {t('Unix时间戳')}</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>
|
||||||
</div>
|
</div>
|
||||||
</Form.Slot>
|
</Form.Slot>
|
||||||
@@ -239,21 +266,25 @@ const NotificationSettings = ({
|
|||||||
{/* 价格设置 Tab */}
|
{/* 价格设置 Tab */}
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<DollarSign size={16} className="mr-2" />
|
<DollarSign size={16} className='mr-2' />
|
||||||
{t('价格设置')}
|
{t('价格设置')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="pricing"
|
itemKey='pricing'
|
||||||
>
|
>
|
||||||
<div className="py-4">
|
<div className='py-4'>
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field='acceptUnsetModelRatioModel'
|
field='acceptUnsetModelRatioModel'
|
||||||
label={t('接受未设置价格模型')}
|
label={t('接受未设置价格模型')}
|
||||||
checkedText={t('开')}
|
checkedText={t('开')}
|
||||||
uncheckedText={t('关')}
|
uncheckedText={t('关')}
|
||||||
onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
|
onChange={(value) =>
|
||||||
extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
|
handleFormChange('acceptUnsetModelRatioModel', value)
|
||||||
|
}
|
||||||
|
extraText={t(
|
||||||
|
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
@@ -261,21 +292,23 @@ const NotificationSettings = ({
|
|||||||
{/* 隐私设置 Tab */}
|
{/* 隐私设置 Tab */}
|
||||||
<TabPane
|
<TabPane
|
||||||
tab={
|
tab={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<ShieldCheck size={16} className="mr-2" />
|
<ShieldCheck size={16} className='mr-2' />
|
||||||
{t('隐私设置')}
|
{t('隐私设置')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
itemKey="privacy"
|
itemKey='privacy'
|
||||||
>
|
>
|
||||||
<div className="py-4">
|
<div className='py-4'>
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field='recordIpLog'
|
field='recordIpLog'
|
||||||
label={t('记录请求与错误日志IP')}
|
label={t('记录请求与错误日志IP')}
|
||||||
checkedText={t('开')}
|
checkedText={t('开')}
|
||||||
uncheckedText={t('关')}
|
uncheckedText={t('关')}
|
||||||
onChange={(value) => handleFormChange('recordIpLog', value)}
|
onChange={(value) => handleFormChange('recordIpLog', value)}
|
||||||
extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
|
extraText={t(
|
||||||
|
'开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|||||||
@@ -17,12 +17,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { API, showError, showSuccess, showWarning } from '../../../../helpers';
|
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 {
|
import {
|
||||||
IconShield,
|
IconShield,
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconCopy
|
IconCopy,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -35,7 +48,7 @@ const TwoFASetting = ({ t }) => {
|
|||||||
const [status, setStatus] = useState({
|
const [status, setStatus] = useState({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
backup_codes_remaining: 0
|
backup_codes_remaining: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
@@ -96,7 +109,7 @@ const TwoFASetting = ({ t }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/user/2fa/enable', {
|
const res = await API.post('/api/user/2fa/enable', {
|
||||||
code: verificationCode
|
code: verificationCode,
|
||||||
});
|
});
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('两步验证启用成功!'));
|
showSuccess(t('两步验证启用成功!'));
|
||||||
@@ -130,7 +143,7 @@ const TwoFASetting = ({ t }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/user/2fa/disable', {
|
const res = await API.post('/api/user/2fa/disable', {
|
||||||
code: verificationCode
|
code: verificationCode,
|
||||||
});
|
});
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('两步验证已禁用'));
|
showSuccess(t('两步验证已禁用'));
|
||||||
@@ -158,7 +171,7 @@ const TwoFASetting = ({ t }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/user/2fa/backup_codes', {
|
const res = await API.post('/api/user/2fa/backup_codes', {
|
||||||
code: verificationCode
|
code: verificationCode,
|
||||||
});
|
});
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
setBackupCodes(res.data.data.backup_codes);
|
setBackupCodes(res.data.data.backup_codes);
|
||||||
@@ -177,9 +190,12 @@ const TwoFASetting = ({ t }) => {
|
|||||||
|
|
||||||
// 通用复制函数
|
// 通用复制函数
|
||||||
const copyTextToClipboard = (text, successMessage = t('已复制到剪贴板')) => {
|
const copyTextToClipboard = (text, successMessage = t('已复制到剪贴板')) => {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
showSuccess(successMessage);
|
showSuccess(successMessage);
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
showError(t('复制失败,请手动复制'));
|
showError(t('复制失败,请手动复制'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -192,28 +208,25 @@ const TwoFASetting = ({ t }) => {
|
|||||||
// 备用码展示组件
|
// 备用码展示组件
|
||||||
const BackupCodesDisplay = ({ codes, title, onCopy }) => {
|
const BackupCodesDisplay = ({ codes, title, onCopy }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card className='!rounded-xl' style={{ width: '100%' }}>
|
||||||
className="!rounded-xl"
|
<div className='space-y-3'>
|
||||||
style={{ width: '100%' }}
|
<div className='flex items-center justify-between'>
|
||||||
>
|
<Text strong className='text-slate-700 dark:text-slate-200'>
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Text strong className="text-slate-700 dark:text-slate-200">
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</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) => (
|
{codes.map((code, index) => (
|
||||||
<div
|
<div key={index} className='rounded-lg p-3'>
|
||||||
key={index}
|
<div className='flex items-center justify-between'>
|
||||||
className="rounded-lg p-3"
|
<Text
|
||||||
|
code
|
||||||
|
className='text-sm font-mono text-slate-700 dark:text-slate-200'
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Text code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
|
||||||
{code}
|
{code}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="quaternary" className="text-xs">
|
<Text type='quaternary' className='text-xs'>
|
||||||
#{(index + 1).toString().padStart(2, '0')}
|
#{(index + 1).toString().padStart(2, '0')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,11 +236,11 @@ const TwoFASetting = ({ t }) => {
|
|||||||
|
|
||||||
<Divider margin={12} />
|
<Divider margin={12} />
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
icon={<IconCopy />}
|
icon={<IconCopy />}
|
||||||
onClick={onCopy}
|
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('复制所有代码')}
|
{t('复制所有代码')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -243,24 +256,24 @@ const TwoFASetting = ({ t }) => {
|
|||||||
{currentStep > 0 && (
|
{currentStep > 0 && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setCurrentStep(currentStep - 1)}
|
onClick={() => setCurrentStep(currentStep - 1)}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
>
|
>
|
||||||
{t('上一步')}
|
{t('上一步')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{currentStep < 2 ? (
|
{currentStep < 2 ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
onClick={() => setCurrentStep(currentStep + 1)}
|
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('下一步')}
|
{t('下一步')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!verificationCode) {
|
if (!verificationCode) {
|
||||||
@@ -269,7 +282,7 @@ const TwoFASetting = ({ t }) => {
|
|||||||
}
|
}
|
||||||
handleEnable2FA();
|
handleEnable2FA();
|
||||||
}}
|
}}
|
||||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||||
>
|
>
|
||||||
{t('完成设置并启用两步验证')}
|
{t('完成设置并启用两步验证')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -288,17 +301,17 @@ const TwoFASetting = ({ t }) => {
|
|||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
setConfirmDisable(false);
|
setConfirmDisable(false);
|
||||||
}}
|
}}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
>
|
>
|
||||||
{t('取消')}
|
{t('取消')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="danger"
|
type='danger'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!confirmDisable || !verificationCode}
|
disabled={!confirmDisable || !verificationCode}
|
||||||
onClick={handleDisable2FA}
|
onClick={handleDisable2FA}
|
||||||
className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
|
className='!rounded-lg !bg-slate-500 hover:!bg-slate-600'
|
||||||
>
|
>
|
||||||
{t('确认禁用')}
|
{t('确认禁用')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -311,14 +324,14 @@ const TwoFASetting = ({ t }) => {
|
|||||||
if (backupCodes.length > 0) {
|
if (backupCodes.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBackupModalVisible(false);
|
setBackupModalVisible(false);
|
||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
setBackupCodes([]);
|
setBackupCodes([]);
|
||||||
}}
|
}}
|
||||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||||
>
|
>
|
||||||
{t('完成')}
|
{t('完成')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -333,17 +346,17 @@ const TwoFASetting = ({ t }) => {
|
|||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
setBackupCodes([]);
|
setBackupCodes([]);
|
||||||
}}
|
}}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
>
|
>
|
||||||
{t('取消')}
|
{t('取消')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!verificationCode}
|
disabled={!verificationCode}
|
||||||
onClick={handleRegenerateBackupCodes}
|
onClick={handleRegenerateBackupCodes}
|
||||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||||
>
|
>
|
||||||
{t('生成新的备用码')}
|
{t('生成新的备用码')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -353,67 +366,82 @@ const TwoFASetting = ({ t }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="!rounded-xl 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 flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||||
<div className="flex items-start w-full sm:w-auto">
|
<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">
|
<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" />
|
<IconShield
|
||||||
|
size='large'
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className='flex-1'>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className='flex items-center gap-2 mb-1'>
|
||||||
<Typography.Title heading={6} className="mb-0">
|
<Typography.Title heading={6} className='mb-0'>
|
||||||
{t('两步验证设置')}
|
{t('两步验证设置')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
{status.enabled ? (
|
{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 && (
|
{status.locked && (
|
||||||
<Tag color="orange" shape="circle" size="small">{t('账户已锁定')}</Tag>
|
<Tag color='orange' shape='circle' size='small'>
|
||||||
|
{t('账户已锁定')}
|
||||||
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text type="tertiary" className="text-sm">
|
<Typography.Text type='tertiary' className='text-sm'>
|
||||||
{t('两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。')}
|
{t(
|
||||||
|
'两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。',
|
||||||
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{status.enabled && (
|
{status.enabled && (
|
||||||
<div className="mt-2">
|
<div className='mt-2'>
|
||||||
<Text size="small" type="secondary">{t('剩余备用码:')}{status.backup_codes_remaining || 0}{t('个')}</Text>
|
<Text size='small' type='secondary'>
|
||||||
|
{t('剩余备用码:')}
|
||||||
|
{status.backup_codes_remaining || 0}
|
||||||
|
{t('个')}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 ? (
|
{!status.enabled ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
size="default"
|
size='default'
|
||||||
onClick={handleSetup2FA}
|
onClick={handleSetup2FA}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||||
icon={<IconShield />}
|
icon={<IconShield />}
|
||||||
>
|
>
|
||||||
{t('启用验证')}
|
{t('启用验证')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className='flex flex-col space-y-2'>
|
||||||
<Button
|
<Button
|
||||||
type="danger"
|
type='danger'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
size="default"
|
size='default'
|
||||||
onClick={() => setDisableModalVisible(true)}
|
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 />}
|
icon={<IconAlertTriangle />}
|
||||||
>
|
>
|
||||||
{t('禁用两步验证')}
|
{t('禁用两步验证')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
size="default"
|
size='default'
|
||||||
onClick={() => setBackupModalVisible(true)}
|
onClick={() => setBackupModalVisible(true)}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
>
|
>
|
||||||
{t('重新生成备用码')}
|
{t('重新生成备用码')}
|
||||||
@@ -427,8 +455,8 @@ const TwoFASetting = ({ t }) => {
|
|||||||
{/* 2FA设置模态框 */}
|
{/* 2FA设置模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<IconShield className="mr-2 text-slate-600" />
|
<IconShield className='mr-2 text-slate-600' />
|
||||||
{t('设置两步验证')}
|
{t('设置两步验证')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -444,36 +472,50 @@ const TwoFASetting = ({ t }) => {
|
|||||||
style={{ maxWidth: '90vw' }}
|
style={{ maxWidth: '90vw' }}
|
||||||
>
|
>
|
||||||
{setupData && (
|
{setupData && (
|
||||||
<div className="space-y-6">
|
<div className='space-y-6'>
|
||||||
{/* 步骤进度 */}
|
{/* 步骤进度 */}
|
||||||
<Steps type="basic" size="small" current={currentStep}>
|
<Steps type='basic' size='small' current={currentStep}>
|
||||||
<Steps.Step title={t('扫描二维码')} description={t('使用认证器应用扫描二维码')} />
|
<Steps.Step
|
||||||
<Steps.Step title={t('保存备用码')} description={t('保存备用码以备不时之需')} />
|
title={t('扫描二维码')}
|
||||||
<Steps.Step title={t('验证设置')} description={t('输入验证码完成设置')} />
|
description={t('使用认证器应用扫描二维码')}
|
||||||
|
/>
|
||||||
|
<Steps.Step
|
||||||
|
title={t('保存备用码')}
|
||||||
|
description={t('保存备用码以备不时之需')}
|
||||||
|
/>
|
||||||
|
<Steps.Step
|
||||||
|
title={t('验证设置')}
|
||||||
|
description={t('输入验证码完成设置')}
|
||||||
|
/>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
{/* 步骤内容 */}
|
{/* 步骤内容 */}
|
||||||
<div className="rounded-xl">
|
<div className='rounded-xl'>
|
||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
<div>
|
<div>
|
||||||
<Paragraph className="text-gray-600 dark:text-gray-300 mb-4">
|
<Paragraph className='text-gray-600 dark:text-gray-300 mb-4'>
|
||||||
{t('使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:')}
|
{t(
|
||||||
|
'使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:',
|
||||||
|
)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<div className="flex justify-center mb-4">
|
<div className='flex justify-center mb-4'>
|
||||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
<div className='bg-white p-4 rounded-lg shadow-sm'>
|
||||||
<QRCodeSVG value={setupData.qr_code_data} size={180} />
|
<QRCodeSVG value={setupData.qr_code_data} size={180} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
|
<div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-3'>
|
||||||
<Text className="text-blue-800 dark:text-blue-200 text-sm">
|
<Text className='text-blue-800 dark:text-blue-200 text-sm'>
|
||||||
{t('或手动输入密钥:')}<Text code copyable className="ml-2">{setupData.secret}</Text>
|
{t('或手动输入密钥:')}
|
||||||
|
<Text code copyable className='ml-2'>
|
||||||
|
{setupData.secret}
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
{/* 备用码展示 */}
|
{/* 备用码展示 */}
|
||||||
<BackupCodesDisplay
|
<BackupCodesDisplay
|
||||||
codes={setupData.backup_codes}
|
codes={setupData.backup_codes}
|
||||||
@@ -491,9 +533,9 @@ const TwoFASetting = ({ t }) => {
|
|||||||
placeholder={t('输入认证器应用显示的6位数字验证码')}
|
placeholder={t('输入认证器应用显示的6位数字验证码')}
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={setVerificationCode}
|
onChange={setVerificationCode}
|
||||||
size="large"
|
size='large'
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -504,8 +546,8 @@ const TwoFASetting = ({ t }) => {
|
|||||||
{/* 禁用2FA模态框 */}
|
{/* 禁用2FA模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<IconAlertTriangle className="mr-2 text-red-500" />
|
<IconAlertTriangle className='mr-2 text-red-500' />
|
||||||
{t('禁用两步验证')}
|
{t('禁用两步验证')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -519,36 +561,41 @@ const TwoFASetting = ({ t }) => {
|
|||||||
width={550}
|
width={550}
|
||||||
style={{ maxWidth: '90vw' }}
|
style={{ maxWidth: '90vw' }}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className='space-y-6'>
|
||||||
{/* 警告提示 */}
|
{/* 警告提示 */}
|
||||||
<div className="rounded-xl">
|
<div className='rounded-xl'>
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
description={t('警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!')}
|
description={t(
|
||||||
className="!rounded-lg"
|
'警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!',
|
||||||
|
)}
|
||||||
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<div>
|
<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('禁用后的影响:')}
|
{t('禁用后的影响:')}
|
||||||
</Text>
|
</Text>
|
||||||
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
|
<ul className='space-y-2 text-sm text-slate-600 dark:text-slate-300'>
|
||||||
<li className="flex items-start gap-2">
|
<li className='flex items-start gap-2'>
|
||||||
<Badge dot type='warning' />
|
<Badge dot type='warning' />
|
||||||
{t('降低您账户的安全性')}
|
{t('降低您账户的安全性')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className='flex items-start gap-2'>
|
||||||
<Badge dot type='warning' />
|
<Badge dot type='warning' />
|
||||||
{t('需要重新完整设置才能再次启用')}
|
{t('需要重新完整设置才能再次启用')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className='flex items-start gap-2'>
|
||||||
<Badge dot type='danger' />
|
<Badge dot type='danger' />
|
||||||
{t('永久删除您的两步验证设置')}
|
{t('永久删除您的两步验证设置')}
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className='flex items-start gap-2'>
|
||||||
<Badge dot type='danger' />
|
<Badge dot type='danger' />
|
||||||
{t('永久删除所有备用码(包括未使用的)')}
|
{t('永久删除所有备用码(包括未使用的)')}
|
||||||
</li>
|
</li>
|
||||||
@@ -557,17 +604,20 @@ const TwoFASetting = ({ t }) => {
|
|||||||
|
|
||||||
<Divider margin={16} />
|
<Divider margin={16} />
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<div>
|
<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('验证身份')}
|
{t('验证身份')}
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('请输入认证器验证码或备用码')}
|
placeholder={t('请输入认证器验证码或备用码')}
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={setVerificationCode}
|
onChange={setVerificationCode}
|
||||||
size="large"
|
size='large'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -575,9 +625,11 @@ const TwoFASetting = ({ t }) => {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={confirmDisable}
|
checked={confirmDisable}
|
||||||
onChange={(e) => setConfirmDisable(e.target.checked)}
|
onChange={(e) => setConfirmDisable(e.target.checked)}
|
||||||
className="text-sm"
|
className='text-sm'
|
||||||
>
|
>
|
||||||
{t('我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销')}
|
{t(
|
||||||
|
'我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销',
|
||||||
|
)}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -588,8 +640,8 @@ const TwoFASetting = ({ t }) => {
|
|||||||
{/* 重新生成备用码模态框 */}
|
{/* 重新生成备用码模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<IconRefresh className="mr-2 text-slate-600" />
|
<IconRefresh className='mr-2 text-slate-600' />
|
||||||
{t('重新生成备用码')}
|
{t('重新生成备用码')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -603,30 +655,35 @@ const TwoFASetting = ({ t }) => {
|
|||||||
width={500}
|
width={500}
|
||||||
style={{ maxWidth: '90vw' }}
|
style={{ maxWidth: '90vw' }}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className='space-y-6'>
|
||||||
{backupCodes.length === 0 ? (
|
{backupCodes.length === 0 ? (
|
||||||
<>
|
<>
|
||||||
{/* 警告提示 */}
|
{/* 警告提示 */}
|
||||||
<div className="rounded-xl">
|
<div className='rounded-xl'>
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
description={t('重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。')}
|
description={t(
|
||||||
className="!rounded-lg"
|
'重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。',
|
||||||
|
)}
|
||||||
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 验证区域 */}
|
{/* 验证区域 */}
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<div>
|
<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('验证身份')}
|
{t('验证身份')}
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('请输入认证器验证码')}
|
placeholder={t('请输入认证器验证码')}
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={setVerificationCode}
|
onChange={setVerificationCode}
|
||||||
size="large"
|
size='large'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -635,13 +692,16 @@ const TwoFASetting = ({ t }) => {
|
|||||||
<>
|
<>
|
||||||
{/* 成功提示 */}
|
{/* 成功提示 */}
|
||||||
<Space vertical style={{ width: '100%' }}>
|
<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' />
|
<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('新的备用码已生成')}
|
{t('新的备用码已生成')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Text className="text-slate-500 dark:text-slate-400 text-sm">
|
<Text className='text-slate-500 dark:text-slate-400 text-sm'>
|
||||||
{t('旧的备用码已失效,请保存新的备用码')}
|
{t('旧的备用码已失效,请保存新的备用码')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,23 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Avatar, Card, Tag, Divider, Typography, Badge } from '@douyinfe/semi-ui';
|
import {
|
||||||
import { isRoot, isAdmin, renderQuota, stringToColor } from '../../../../helpers';
|
Avatar,
|
||||||
|
Card,
|
||||||
|
Tag,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
Badge,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
isRoot,
|
||||||
|
isAdmin,
|
||||||
|
renderQuota,
|
||||||
|
stringToColor,
|
||||||
|
} from '../../../../helpers';
|
||||||
import { Coins, BarChart2, Users } from 'lucide-react';
|
import { Coins, BarChart2, Users } from 'lucide-react';
|
||||||
|
|
||||||
const UserInfoHeader = ({ t, userState }) => {
|
const UserInfoHeader = ({ t, userState }) => {
|
||||||
|
|
||||||
const getUsername = () => {
|
const getUsername = () => {
|
||||||
if (userState.user) {
|
if (userState.user) {
|
||||||
return userState.user.username;
|
return userState.user.username;
|
||||||
@@ -42,31 +53,33 @@ const UserInfoHeader = ({ t, userState }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="!rounded-2xl overflow-hidden"
|
className='!rounded-2xl overflow-hidden'
|
||||||
cover={
|
cover={
|
||||||
<div
|
<div
|
||||||
className="relative h-32"
|
className='relative h-32'
|
||||||
style={{
|
style={{
|
||||||
'--palette-primary-darkerChannel': '0 75 80',
|
'--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')`,
|
backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
backgroundRepeat: 'no-repeat'
|
backgroundRepeat: 'no-repeat',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 用户信息内容 */}
|
{/* 用户信息内容 */}
|
||||||
<div className="relative z-10 h-full flex flex-col justify-end p-6">
|
<div className='relative z-10 h-full flex flex-col justify-end p-6'>
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<div className="flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0">
|
<div className='flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0'>
|
||||||
<Avatar
|
<Avatar size='large' color={stringToColor(getUsername())}>
|
||||||
size='large'
|
|
||||||
color={stringToColor(getUsername())}
|
|
||||||
>
|
|
||||||
{getAvatarText()}
|
{getAvatarText()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0 flex flex-col justify-between">
|
<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
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
className='text-3xl font-bold truncate'
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
>
|
||||||
|
{getUsername()}
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
{isRoot() ? (
|
{isRoot() ? (
|
||||||
<Tag
|
<Tag
|
||||||
size='large'
|
size='large'
|
||||||
@@ -92,11 +105,7 @@ const UserInfoHeader = ({ t, userState }) => {
|
|||||||
{t('普通用户')}
|
{t('普通用户')}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<Tag
|
<Tag size='large' shape='circle' style={{ color: 'white' }}>
|
||||||
size='large'
|
|
||||||
shape='circle'
|
|
||||||
style={{ color: 'white' }}
|
|
||||||
>
|
|
||||||
ID: {userState?.user?.id}
|
ID: {userState?.user?.id}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</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'>
|
<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)}
|
{renderQuota(userState?.user?.quota)}
|
||||||
</div>
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
{/* 桌面版统计信息(Semi UI 卡片) */}
|
{/* 桌面版统计信息(Semi UI 卡片) */}
|
||||||
<div className="hidden lg:block flex-shrink-0">
|
<div className='hidden lg:block flex-shrink-0'>
|
||||||
<Card size="small" className="!rounded-xl" bodyStyle={{ padding: '12px 16px' }}>
|
<Card
|
||||||
<div className="flex items-center gap-4">
|
size='small'
|
||||||
<div className="flex items-center gap-2">
|
className='!rounded-xl'
|
||||||
|
bodyStyle={{ padding: '12px 16px' }}
|
||||||
|
>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
<Coins size={16} />
|
<Coins size={16} />
|
||||||
<Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
|
<Typography.Text size='small' type='tertiary'>
|
||||||
<Typography.Text size="small" type="tertiary" strong>{renderQuota(userState?.user?.used_quota)}</Typography.Text>
|
{t('历史消耗')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text size='small' type='tertiary' strong>
|
||||||
|
{renderQuota(userState?.user?.used_quota)}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Divider layout="vertical" />
|
<Divider layout='vertical' />
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<BarChart2 size={16} />
|
<BarChart2 size={16} />
|
||||||
<Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
|
<Typography.Text size='small' type='tertiary'>
|
||||||
<Typography.Text size="small" type="tertiary" strong>{userState.user?.request_count || 0}</Typography.Text>
|
{t('请求次数')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text size='small' type='tertiary' strong>
|
||||||
|
{userState.user?.request_count || 0}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Divider layout="vertical" />
|
<Divider layout='vertical' />
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
<Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
|
<Typography.Text size='small' type='tertiary'>
|
||||||
<Typography.Text size="small" type="tertiary" strong>{userState?.user?.group || t('默认')}</Typography.Text>
|
{t('用户分组')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text size='small' type='tertiary' strong>
|
||||||
|
{userState?.user?.group || t('默认')}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -143,31 +168,47 @@ const UserInfoHeader = ({ t, userState }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端和中等屏幕统计信息卡片 */}
|
{/* 移动端和中等屏幕统计信息卡片 */}
|
||||||
<div className="lg:hidden mt-2">
|
<div className='lg:hidden mt-2'>
|
||||||
<Card size="small" className="!rounded-xl" bodyStyle={{ padding: '12px 16px' }} >
|
<Card
|
||||||
<div className="space-y-3">
|
size='small'
|
||||||
<div className="flex items-center justify-between">
|
className='!rounded-xl'
|
||||||
<div className="flex items-center gap-2">
|
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} />
|
<Coins size={16} />
|
||||||
<Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
|
<Typography.Text size='small' type='tertiary'>
|
||||||
|
{t('历史消耗')}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<Divider margin='8px' />
|
<Divider margin='8px' />
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<BarChart2 size={16} />
|
<BarChart2 size={16} />
|
||||||
<Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
|
<Typography.Text size='small' type='tertiary'>
|
||||||
|
{t('请求次数')}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<Divider margin='8px' />
|
<Divider margin='8px' />
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
<Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
|
<Typography.Text size='small' type='tertiary'>
|
||||||
|
{t('用户分组')}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ const AccountDeleteModal = ({
|
|||||||
userState,
|
userState,
|
||||||
turnstileEnabled,
|
turnstileEnabled,
|
||||||
turnstileSiteKey,
|
turnstileSiteKey,
|
||||||
setTurnstileToken
|
setTurnstileToken,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<IconDelete className="mr-2 text-red-500" />
|
<IconDelete className='mr-2 text-red-500' />
|
||||||
{t('删除账户确认')}
|
{t('删除账户确认')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -47,35 +47,37 @@ const AccountDeleteModal = ({
|
|||||||
onOk={deleteAccount}
|
onOk={deleteAccount}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
centered={true}
|
centered={true}
|
||||||
className="modern-modal"
|
className='modern-modal'
|
||||||
>
|
>
|
||||||
<div className="space-y-4 py-4">
|
<div className='space-y-4 py-4'>
|
||||||
<Banner
|
<Banner
|
||||||
type='danger'
|
type='danger'
|
||||||
description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
|
description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="block mb-2 text-red-600">
|
<Typography.Text strong className='block mb-2 text-red-600'>
|
||||||
{t('请输入您的用户名以确认删除')}
|
{t('请输入您的用户名以确认删除')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })}
|
placeholder={t('输入你的账户名{{username}}以确认删除', {
|
||||||
|
username: ` ${userState?.user?.username} `,
|
||||||
|
})}
|
||||||
name='self_account_deletion_confirmation'
|
name='self_account_deletion_confirmation'
|
||||||
value={inputs.self_account_deletion_confirmation}
|
value={inputs.self_account_deletion_confirmation}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleInputChange('self_account_deletion_confirmation', value)
|
handleInputChange('self_account_deletion_confirmation', value)
|
||||||
}
|
}
|
||||||
size="large"
|
size='large'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
prefix={<IconUser />}
|
prefix={<IconUser />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{turnstileEnabled && (
|
{turnstileEnabled && (
|
||||||
<div className="flex justify-center">
|
<div className='flex justify-center'>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={turnstileSiteKey}
|
sitekey={turnstileSiteKey}
|
||||||
onVerify={(token) => {
|
onVerify={(token) => {
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ const ChangePasswordModal = ({
|
|||||||
changePassword,
|
changePassword,
|
||||||
turnstileEnabled,
|
turnstileEnabled,
|
||||||
turnstileSiteKey,
|
turnstileSiteKey,
|
||||||
setTurnstileToken
|
setTurnstileToken,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<IconLock className="mr-2 text-orange-500" />
|
<IconLock className='mr-2 text-orange-500' />
|
||||||
{t('修改密码')}
|
{t('修改密码')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -46,43 +46,45 @@ const ChangePasswordModal = ({
|
|||||||
onOk={changePassword}
|
onOk={changePassword}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
centered={true}
|
centered={true}
|
||||||
className="modern-modal"
|
className='modern-modal'
|
||||||
>
|
>
|
||||||
<div className="space-y-4 py-4">
|
<div className='space-y-4 py-4'>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text>
|
<Typography.Text strong className='block mb-2'>
|
||||||
|
{t('原密码')}
|
||||||
|
</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
name='original_password'
|
name='original_password'
|
||||||
placeholder={t('请输入原密码')}
|
placeholder={t('请输入原密码')}
|
||||||
type='password'
|
type='password'
|
||||||
value={inputs.original_password}
|
value={inputs.original_password}
|
||||||
onChange={(value) =>
|
onChange={(value) => handleInputChange('original_password', value)}
|
||||||
handleInputChange('original_password', value)
|
size='large'
|
||||||
}
|
className='!rounded-lg'
|
||||||
size="large"
|
|
||||||
className="!rounded-lg"
|
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="block mb-2">{t('新密码')}</Typography.Text>
|
<Typography.Text strong className='block mb-2'>
|
||||||
|
{t('新密码')}
|
||||||
|
</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
name='set_new_password'
|
name='set_new_password'
|
||||||
placeholder={t('请输入新密码')}
|
placeholder={t('请输入新密码')}
|
||||||
type='password'
|
type='password'
|
||||||
value={inputs.set_new_password}
|
value={inputs.set_new_password}
|
||||||
onChange={(value) =>
|
onChange={(value) => handleInputChange('set_new_password', value)}
|
||||||
handleInputChange('set_new_password', value)
|
size='large'
|
||||||
}
|
className='!rounded-lg'
|
||||||
size="large"
|
|
||||||
className="!rounded-lg"
|
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text strong className="block mb-2">{t('确认新密码')}</Typography.Text>
|
<Typography.Text strong className='block mb-2'>
|
||||||
|
{t('确认新密码')}
|
||||||
|
</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
name='set_new_password_confirmation'
|
name='set_new_password_confirmation'
|
||||||
placeholder={t('请再次输入新密码')}
|
placeholder={t('请再次输入新密码')}
|
||||||
@@ -91,14 +93,14 @@ const ChangePasswordModal = ({
|
|||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleInputChange('set_new_password_confirmation', value)
|
handleInputChange('set_new_password_confirmation', value)
|
||||||
}
|
}
|
||||||
size="large"
|
size='large'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{turnstileEnabled && (
|
{turnstileEnabled && (
|
||||||
<div className="flex justify-center">
|
<div className='flex justify-center'>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={turnstileSiteKey}
|
sitekey={turnstileSiteKey}
|
||||||
onVerify={(token) => {
|
onVerify={(token) => {
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ const EmailBindModal = ({
|
|||||||
countdown,
|
countdown,
|
||||||
turnstileEnabled,
|
turnstileEnabled,
|
||||||
turnstileSiteKey,
|
turnstileSiteKey,
|
||||||
setTurnstileToken
|
setTurnstileToken,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<IconMail className="mr-2 text-blue-500" />
|
<IconMail className='mr-2 text-blue-500' />
|
||||||
{t('绑定邮箱地址')}
|
{t('绑定邮箱地址')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -51,28 +51,30 @@ const EmailBindModal = ({
|
|||||||
size={'small'}
|
size={'small'}
|
||||||
centered={true}
|
centered={true}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
className="modern-modal"
|
className='modern-modal'
|
||||||
>
|
>
|
||||||
<div className="space-y-4 py-4">
|
<div className='space-y-4 py-4'>
|
||||||
<div className="flex gap-3">
|
<div className='flex gap-3'>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('输入邮箱地址')}
|
placeholder={t('输入邮箱地址')}
|
||||||
onChange={(value) => handleInputChange('email', value)}
|
onChange={(value) => handleInputChange('email', value)}
|
||||||
name='email'
|
name='email'
|
||||||
type='email'
|
type='email'
|
||||||
size="large"
|
size='large'
|
||||||
className="!rounded-lg flex-1"
|
className='!rounded-lg flex-1'
|
||||||
prefix={<IconMail />}
|
prefix={<IconMail />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={sendVerificationCode}
|
onClick={sendVerificationCode}
|
||||||
disabled={disableButton || loading}
|
disabled={disableButton || loading}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="outline"
|
theme='outline'
|
||||||
size='large'
|
size='large'
|
||||||
>
|
>
|
||||||
{disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
|
{disableButton
|
||||||
|
? `${t('重新发送')} (${countdown})`
|
||||||
|
: t('获取验证码')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,13 +85,13 @@ const EmailBindModal = ({
|
|||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleInputChange('email_verification_code', value)
|
handleInputChange('email_verification_code', value)
|
||||||
}
|
}
|
||||||
size="large"
|
size='large'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
prefix={<IconKey />}
|
prefix={<IconKey />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{turnstileEnabled && (
|
{turnstileEnabled && (
|
||||||
<div className="flex justify-center">
|
<div className='flex justify-center'>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={turnstileSiteKey}
|
sitekey={turnstileSiteKey}
|
||||||
onVerify={(token) => {
|
onVerify={(token) => {
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ const WeChatBindModal = ({
|
|||||||
inputs,
|
inputs,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
bindWeChat,
|
bindWeChat,
|
||||||
status
|
status,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<SiWechat className="mr-2 text-green-500" size={20} />
|
<SiWechat className='mr-2 text-green-500' size={20} />
|
||||||
{t('绑定微信账户')}
|
{t('绑定微信账户')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -44,30 +44,30 @@ const WeChatBindModal = ({
|
|||||||
footer={null}
|
footer={null}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
centered={true}
|
centered={true}
|
||||||
className="modern-modal"
|
className='modern-modal'
|
||||||
>
|
>
|
||||||
<div className="space-y-4 py-4 text-center">
|
<div className='space-y-4 py-4 text-center'>
|
||||||
<Image src={status.wechat_qrcode} className="mx-auto" />
|
<Image src={status.wechat_qrcode} className='mx-auto' />
|
||||||
<div className="text-gray-600">
|
<div className='text-gray-600'>
|
||||||
<p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
|
<p>
|
||||||
|
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('验证码')}
|
placeholder={t('验证码')}
|
||||||
name='wechat_verification_code'
|
name='wechat_verification_code'
|
||||||
value={inputs.wechat_verification_code}
|
value={inputs.wechat_verification_code}
|
||||||
onChange={(v) =>
|
onChange={(v) => handleInputChange('wechat_verification_code', v)}
|
||||||
handleInputChange('wechat_verification_code', v)
|
size='large'
|
||||||
}
|
className='!rounded-lg'
|
||||||
size="large"
|
|
||||||
className="!rounded-lg"
|
|
||||||
prefix={<IconKey />}
|
prefix={<IconKey />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
theme="solid"
|
theme='solid'
|
||||||
size='large'
|
size='large'
|
||||||
onClick={bindWeChat}
|
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} />}
|
icon={<SiWechat size={16} />}
|
||||||
>
|
>
|
||||||
{t('绑定')}
|
{t('绑定')}
|
||||||
|
|||||||
@@ -130,7 +130,11 @@ const SetupWizard = () => {
|
|||||||
return true; // 如果已经初始化,可以继续
|
return true; // 如果已经初始化,可以继续
|
||||||
}
|
}
|
||||||
// 检查必填字段
|
// 检查必填字段
|
||||||
if (!formData.username || !formData.password || !formData.confirmPassword) {
|
if (
|
||||||
|
!formData.username ||
|
||||||
|
!formData.password ||
|
||||||
|
!formData.confirmPassword
|
||||||
|
) {
|
||||||
showError(t('请填写完整的管理员账号信息'));
|
showError(t('请填写完整的管理员账号信息'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -226,12 +230,7 @@ const SetupWizard = () => {
|
|||||||
const getStepContent = (step) => {
|
const getStepContent = (step) => {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
return (
|
return <DatabaseStep setupStatus={setupStatus} t={t} />;
|
||||||
<DatabaseStep
|
|
||||||
setupStatus={setupStatus}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<AdminStep
|
<AdminStep
|
||||||
@@ -252,11 +251,7 @@ const SetupWizard = () => {
|
|||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<CompleteStep
|
<CompleteStep setupStatus={setupStatus} formData={formData} t={t} />
|
||||||
setupStatus={setupStatus}
|
|
||||||
formData={formData}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -275,21 +270,25 @@ const SetupWizard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen flex items-center justify-center px-4'>
|
<div className='min-h-screen flex items-center justify-center px-4'>
|
||||||
<div className="w-full max-w-4xl">
|
<div className='w-full max-w-4xl'>
|
||||||
<Card className="!rounded-2xl shadow-sm border-0">
|
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||||
<div className="mb-4">
|
<div className='mb-4'>
|
||||||
<div className="text-xl font-semibold">{t('系统初始化')}</div>
|
<div className='text-xl font-semibold'>{t('系统初始化')}</div>
|
||||||
<div className="text-xs text-gray-600">
|
<div className='text-xs text-gray-600'>
|
||||||
{t('欢迎使用,请完成以下设置以开始使用系统')}
|
{t('欢迎使用,请完成以下设置以开始使用系统')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-2 py-2">
|
<div className='px-2 py-2'>
|
||||||
<Steps type="basic" current={currentStep}>
|
<Steps type='basic' current={currentStep}>
|
||||||
{steps.map((item, index) => (
|
{steps.map((item, index) => (
|
||||||
<Steps.Step
|
<Steps.Step
|
||||||
key={item.title}
|
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}
|
description={item.description}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -306,9 +305,12 @@ const SetupWizard = () => {
|
|||||||
initValues={formData}
|
initValues={formData}
|
||||||
>
|
>
|
||||||
{/* 步骤内容:保持所有字段挂载,仅隐藏非当前步骤 */}
|
{/* 步骤内容:保持所有字段挂载,仅隐藏非当前步骤 */}
|
||||||
<div className="steps-content">
|
<div className='steps-content'>
|
||||||
{[0, 1, 2, 3].map((idx) => (
|
{[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), {
|
{React.cloneElement(getStepContent(idx), {
|
||||||
...stepNavigationProps,
|
...stepNavigationProps,
|
||||||
renderNavigationButtons: () => (
|
renderNavigationButtons: () => (
|
||||||
|
|||||||
@@ -32,29 +32,22 @@ const StepNavigation = ({
|
|||||||
next,
|
next,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
loading,
|
loading,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center pt-4">
|
<div className='flex justify-between items-center pt-4'>
|
||||||
{/* 上一步按钮 */}
|
{/* 上一步按钮 */}
|
||||||
{currentStep > 0 && (
|
{currentStep > 0 && (
|
||||||
<Button
|
<Button onClick={prev} className='!rounded-lg'>
|
||||||
onClick={prev}
|
|
||||||
className="!rounded-lg"
|
|
||||||
>
|
|
||||||
{t('上一步')}
|
{t('上一步')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1"></div>
|
<div className='flex-1'></div>
|
||||||
|
|
||||||
{/* 下一步按钮 */}
|
{/* 下一步按钮 */}
|
||||||
{currentStep < steps.length - 1 && (
|
{currentStep < steps.length - 1 && (
|
||||||
<Button
|
<Button type='primary' onClick={next} className='!rounded-lg'>
|
||||||
type="primary"
|
|
||||||
onClick={next}
|
|
||||||
className="!rounded-lg"
|
|
||||||
>
|
|
||||||
{t('下一步')}
|
{t('下一步')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -62,10 +55,10 @@ const StepNavigation = ({
|
|||||||
{/* 完成按钮 */}
|
{/* 完成按钮 */}
|
||||||
{currentStep === steps.length - 1 && (
|
{currentStep === steps.length - 1 && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type='primary'
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
icon={<IconCheckCircleStroked />}
|
icon={<IconCheckCircleStroked />}
|
||||||
>
|
>
|
||||||
{t('初始化系统')}
|
{t('初始化系统')}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const AdminStep = ({
|
|||||||
setFormData,
|
setFormData,
|
||||||
formRef,
|
formRef,
|
||||||
renderNavigationButtons,
|
renderNavigationButtons,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -40,11 +40,11 @@ const AdminStep = ({
|
|||||||
type='info'
|
type='info'
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
description={
|
description={
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<span>{t('管理员账号已经初始化过,请继续设置其他参数')}</span>
|
<span>{t('管理员账号已经初始化过,请继续设置其他参数')}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -55,7 +55,7 @@ const AdminStep = ({
|
|||||||
prefix={<IconUser />}
|
prefix={<IconUser />}
|
||||||
showClear
|
showClear
|
||||||
noLabel={false}
|
noLabel={false}
|
||||||
validateStatus="default"
|
validateStatus='default'
|
||||||
rules={[{ required: true, message: t('请输入管理员用户名') }]}
|
rules={[{ required: true, message: t('请输入管理员用户名') }]}
|
||||||
initValue={formData.username || ''}
|
initValue={formData.username || ''}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -70,11 +70,11 @@ const AdminStep = ({
|
|||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
showClear
|
showClear
|
||||||
noLabel={false}
|
noLabel={false}
|
||||||
mode="password"
|
mode='password'
|
||||||
validateStatus="default"
|
validateStatus='default'
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: t('请输入管理员密码') },
|
{ required: true, message: t('请输入管理员密码') },
|
||||||
{ min: 8, message: t('密码长度至少为8个字符') }
|
{ min: 8, message: t('密码长度至少为8个字符') },
|
||||||
]}
|
]}
|
||||||
initValue={formData.password || ''}
|
initValue={formData.password || ''}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -89,8 +89,8 @@ const AdminStep = ({
|
|||||||
prefix={<IconLock />}
|
prefix={<IconLock />}
|
||||||
showClear
|
showClear
|
||||||
noLabel={false}
|
noLabel={false}
|
||||||
mode="password"
|
mode='password'
|
||||||
validateStatus="default"
|
validateStatus='default'
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: t('请确认管理员密码') },
|
{ required: true, message: t('请确认管理员密码') },
|
||||||
{
|
{
|
||||||
@@ -102,8 +102,8 @@ const AdminStep = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
initValue={formData.confirmPassword || ''}
|
initValue={formData.confirmPassword || ''}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
|||||||
@@ -31,29 +31,39 @@ const CompleteStep = ({
|
|||||||
setupStatus,
|
setupStatus,
|
||||||
formData,
|
formData,
|
||||||
renderNavigationButtons,
|
renderNavigationButtons,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className='text-center'>
|
||||||
<Avatar color="green" className="mx-auto mb-4 shadow-lg">
|
<Avatar color='green' className='mx-auto mb-4 shadow-lg'>
|
||||||
<CheckCircle size={24} />
|
<CheckCircle size={24} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Title heading={3} className="mb-2">{t('准备完成初始化')}</Title>
|
<Title heading={3} className='mb-2'>
|
||||||
<Text type="secondary" className="mb-6 block">
|
{t('准备完成初始化')}
|
||||||
|
</Title>
|
||||||
|
<Text type='secondary' className='mb-6 block'>
|
||||||
{t('请确认以下设置信息,点击"初始化系统"开始配置')}
|
{t('请确认以下设置信息,点击"初始化系统"开始配置')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Descriptions>
|
<Descriptions>
|
||||||
<Descriptions.Item itemKey={t('数据库类型')}>
|
<Descriptions.Item itemKey={t('数据库类型')}>
|
||||||
{setupStatus.database_type === 'sqlite' ? 'SQLite' :
|
{setupStatus.database_type === 'sqlite'
|
||||||
setupStatus.database_type === 'mysql' ? 'MySQL' : 'PostgreSQL'}
|
? 'SQLite'
|
||||||
|
: setupStatus.database_type === 'mysql'
|
||||||
|
? 'MySQL'
|
||||||
|
: 'PostgreSQL'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item itemKey={t('管理员账号')}>
|
<Descriptions.Item itemKey={t('管理员账号')}>
|
||||||
{setupStatus.root_init ? t('已初始化') : (formData.username || t('未设置'))}
|
{setupStatus.root_init
|
||||||
|
? t('已初始化')
|
||||||
|
: formData.username || t('未设置')}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item itemKey={t('使用模式')}>
|
<Descriptions.Item itemKey={t('使用模式')}>
|
||||||
{formData.usageMode === 'external' ? t('对外运营模式') :
|
{formData.usageMode === 'external'
|
||||||
formData.usageMode === 'self' ? t('自用模式') : t('演示站点模式')}
|
? t('对外运营模式')
|
||||||
|
: formData.usageMode === 'self'
|
||||||
|
? t('自用模式')
|
||||||
|
: t('演示站点模式')}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,16 @@ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
|
|||||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1">
|
<p className='mt-1'>
|
||||||
<strong>{t(
|
<strong>
|
||||||
|
{t(
|
||||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||||
)}</strong>
|
)}
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
fullMode={false}
|
fullMode={false}
|
||||||
bordered
|
bordered
|
||||||
/>
|
/>
|
||||||
@@ -68,7 +70,7 @@ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
fullMode={false}
|
fullMode={false}
|
||||||
bordered
|
bordered
|
||||||
/>
|
/>
|
||||||
@@ -89,7 +91,7 @@ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
fullMode={false}
|
fullMode={false}
|
||||||
bordered
|
bordered
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const UsageModeStep = ({
|
|||||||
formData,
|
formData,
|
||||||
handleUsageModeChange,
|
handleUsageModeChange,
|
||||||
renderNavigationButtons,
|
renderNavigationButtons,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -37,9 +37,9 @@ const UsageModeStep = ({
|
|||||||
onChange={handleUsageModeChange}
|
onChange={handleUsageModeChange}
|
||||||
type='card'
|
type='card'
|
||||||
direction='horizontal'
|
direction='horizontal'
|
||||||
className="mt-4"
|
className='mt-4'
|
||||||
aria-label="使用模式选择"
|
aria-label='使用模式选择'
|
||||||
name="usage-mode-selection"
|
name='usage-mode-selection'
|
||||||
>
|
>
|
||||||
<Radio
|
<Radio
|
||||||
value='external'
|
value='external'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
Select
|
Select,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||||
|
|
||||||
@@ -52,19 +52,19 @@ const ChannelsActions = ({
|
|||||||
activePage,
|
activePage,
|
||||||
pageSize,
|
pageSize,
|
||||||
setActivePage,
|
setActivePage,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
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
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
disabled={!enableBatchDelete}
|
disabled={!enableBatchDelete}
|
||||||
type='danger'
|
type='danger'
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('确定是否要删除所选通道?'),
|
title: t('确定是否要删除所选通道?'),
|
||||||
@@ -81,7 +81,7 @@ const ChannelsActions = ({
|
|||||||
disabled={!enableBatchDelete}
|
disabled={!enableBatchDelete}
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
onClick={() => setShowBatchSetTag(true)}
|
onClick={() => setShowBatchSetTag(true)}
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
>
|
>
|
||||||
{t('批量设置标签')}
|
{t('批量设置标签')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -95,7 +95,7 @@ const ChannelsActions = ({
|
|||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
className="w-full"
|
className='w-full'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('确定?'),
|
title: t('确定?'),
|
||||||
@@ -112,11 +112,13 @@ const ChannelsActions = ({
|
|||||||
<Dropdown.Item>
|
<Dropdown.Item>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
className="w-full"
|
className='w-full'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('确定是否要修复数据库一致性?'),
|
title: t('确定是否要修复数据库一致性?'),
|
||||||
content: t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'),
|
content: t(
|
||||||
|
'进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用',
|
||||||
|
),
|
||||||
onOk: () => fixChannelsAbilities(),
|
onOk: () => fixChannelsAbilities(),
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
centered: true,
|
centered: true,
|
||||||
@@ -130,7 +132,7 @@ const ChannelsActions = ({
|
|||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='secondary'
|
type='secondary'
|
||||||
className="w-full"
|
className='w-full'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('确定?'),
|
title: t('确定?'),
|
||||||
@@ -148,7 +150,7 @@ const ChannelsActions = ({
|
|||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='danger'
|
type='danger'
|
||||||
className="w-full"
|
className='w-full'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('确定是否要删除禁用通道?'),
|
title: t('确定是否要删除禁用通道?'),
|
||||||
@@ -165,7 +167,12 @@ const ChannelsActions = ({
|
|||||||
</Dropdown.Menu>
|
</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('批量操作')}
|
{t('批量操作')}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -178,9 +185,9 @@ const ChannelsActions = ({
|
|||||||
</div>
|
</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 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">
|
<div className='flex items-center justify-between w-full md:w-auto'>
|
||||||
<Typography.Text strong className="mr-2">
|
<Typography.Text strong className='mr-2'>
|
||||||
{t('使用ID排序')}
|
{t('使用ID排序')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -189,18 +196,30 @@ const ChannelsActions = ({
|
|||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
localStorage.setItem('id-sort', v + '');
|
localStorage.setItem('id-sort', v + '');
|
||||||
setIdSort(v);
|
setIdSort(v);
|
||||||
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
const { searchKeyword, searchGroup, searchModel } =
|
||||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
getFormValues();
|
||||||
|
if (
|
||||||
|
searchKeyword === '' &&
|
||||||
|
searchGroup === '' &&
|
||||||
|
searchModel === ''
|
||||||
|
) {
|
||||||
loadChannels(activePage, pageSize, v, enableTagMode);
|
loadChannels(activePage, pageSize, v, enableTagMode);
|
||||||
} else {
|
} else {
|
||||||
searchChannels(enableTagMode, activeTypeKey, statusFilter, activePage, pageSize, v);
|
searchChannels(
|
||||||
|
enableTagMode,
|
||||||
|
activeTypeKey,
|
||||||
|
statusFilter,
|
||||||
|
activePage,
|
||||||
|
pageSize,
|
||||||
|
v,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full md:w-auto">
|
<div className='flex items-center justify-between w-full md:w-auto'>
|
||||||
<Typography.Text strong className="mr-2">
|
<Typography.Text strong className='mr-2'>
|
||||||
{t('开启批量操作')}
|
{t('开启批量操作')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -213,8 +232,8 @@ const ChannelsActions = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full md:w-auto">
|
<div className='flex items-center justify-between w-full md:w-auto'>
|
||||||
<Typography.Text strong className="mr-2">
|
<Typography.Text strong className='mr-2'>
|
||||||
{t('标签聚合模式')}
|
{t('标签聚合模式')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -229,8 +248,8 @@ const ChannelsActions = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full md:w-auto">
|
<div className='flex items-center justify-between w-full md:w-auto'>
|
||||||
<Typography.Text strong className="mr-2">
|
<Typography.Text strong className='mr-2'>
|
||||||
{t('状态筛选')}
|
{t('状态筛选')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
@@ -240,12 +259,19 @@ const ChannelsActions = ({
|
|||||||
localStorage.setItem('channel-status-filter', v);
|
localStorage.setItem('channel-status-filter', v);
|
||||||
setStatusFilter(v);
|
setStatusFilter(v);
|
||||||
setActivePage(1);
|
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='all'>{t('全部')}</Select.Option>
|
||||||
<Select.Option value="enabled">{t('已启用')}</Select.Option>
|
<Select.Option value='enabled'>{t('已启用')}</Select.Option>
|
||||||
<Select.Option value="disabled">{t('已禁用')}</Select.Option>
|
<Select.Option value='disabled'>{t('已禁用')}</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ import {
|
|||||||
SplitButtonGroup,
|
SplitButtonGroup,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
timestamp2string,
|
timestamp2string,
|
||||||
renderGroup,
|
renderGroup,
|
||||||
renderQuota,
|
renderQuota,
|
||||||
getChannelIcon,
|
getChannelIcon,
|
||||||
renderQuotaWithAmount
|
renderQuotaWithAmount,
|
||||||
} from '../../../helpers';
|
} from '../../../helpers';
|
||||||
import { CHANNEL_OPTIONS } from '../../../constants';
|
import { CHANNEL_OPTIONS } from '../../../constants';
|
||||||
import { IconTreeTriangleDown, IconMore } from '@douyinfe/semi-icons';
|
import { IconTreeTriangleDown, IconMore } from '@douyinfe/semi-icons';
|
||||||
@@ -51,27 +51,22 @@ const renderType = (type, channelInfo = undefined, t) => {
|
|||||||
let icon = getChannelIcon(type);
|
let icon = getChannelIcon(type);
|
||||||
|
|
||||||
if (channelInfo?.is_multi_key) {
|
if (channelInfo?.is_multi_key) {
|
||||||
icon = (
|
icon =
|
||||||
channelInfo?.multi_key_mode === 'random' ? (
|
channelInfo?.multi_key_mode === 'random' ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className='flex items-center gap-1'>
|
||||||
<FaRandom className="text-blue-500" />
|
<FaRandom className='text-blue-500' />
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className='flex items-center gap-1'>
|
||||||
<IconTreeTriangleDown className="text-blue-500" />
|
<IconTreeTriangleDown className='text-blue-500' />
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}>
|
||||||
color={type2label[type]?.color}
|
|
||||||
shape='circle'
|
|
||||||
prefixIcon={icon}
|
|
||||||
>
|
|
||||||
{type2label[type]?.label}
|
{type2label[type]?.label}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -79,11 +74,7 @@ const renderType = (type, channelInfo = undefined, t) => {
|
|||||||
|
|
||||||
const renderTagType = (t) => {
|
const renderTagType = (t) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag color='light-blue' shape='circle' type='light'>
|
||||||
color='light-blue'
|
|
||||||
shape='circle'
|
|
||||||
type='light'
|
|
||||||
>
|
|
||||||
{t('标签聚合')}
|
{t('标签聚合')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -95,7 +86,8 @@ const renderStatus = (status, channelInfo = undefined, t) => {
|
|||||||
let keySize = channelInfo.multi_key_size;
|
let keySize = channelInfo.multi_key_size;
|
||||||
let enabledKeySize = keySize;
|
let enabledKeySize = keySize;
|
||||||
if (channelInfo.multi_key_status_list) {
|
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);
|
return renderMultiKeyStatus(status, keySize, enabledKeySize, t);
|
||||||
}
|
}
|
||||||
@@ -155,7 +147,7 @@ const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => {
|
|||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderResponseTime = (responseTime, t) => {
|
const renderResponseTime = (responseTime, t) => {
|
||||||
let time = responseTime / 1000;
|
let time = responseTime / 1000;
|
||||||
@@ -212,7 +204,7 @@ export const getChannelsColumns = ({
|
|||||||
activePage,
|
activePage,
|
||||||
channels,
|
channels,
|
||||||
setShowMultiKeyManageModal,
|
setShowMultiKeyManageModal,
|
||||||
setCurrentMultiKeyChannel
|
setCurrentMultiKeyChannel,
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -276,7 +268,9 @@ export const getChannelsColumns = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
|
content={
|
||||||
|
t('原因:') + reason + t(',时间:') + timestamp2string(time)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{renderStatus(text, record.channel_info, t)}
|
{renderStatus(text, record.channel_info, t)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -291,9 +285,7 @@ export const getChannelsColumns = ({
|
|||||||
key: COLUMN_KEYS.RESPONSE_TIME,
|
key: COLUMN_KEYS.RESPONSE_TIME,
|
||||||
title: t('响应时间'),
|
title: t('响应时间'),
|
||||||
dataIndex: 'response_time',
|
dataIndex: 'response_time',
|
||||||
render: (text, record, index) => (
|
render: (text, record, index) => <div>{renderResponseTime(text, t)}</div>,
|
||||||
<div>{renderResponseTime(text, t)}</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: COLUMN_KEYS.BALANCE,
|
key: COLUMN_KEYS.BALANCE,
|
||||||
@@ -309,7 +301,9 @@ export const getChannelsColumns = ({
|
|||||||
{renderQuota(record.used_quota)}
|
{renderQuota(record.used_quota)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={t('剩余额度$') + record.balance + t(',点击更新')}>
|
<Tooltip
|
||||||
|
content={t('剩余额度$') + record.balance + t(',点击更新')}
|
||||||
|
>
|
||||||
<Tag
|
<Tag
|
||||||
color='white'
|
color='white'
|
||||||
type='ghost'
|
type='ghost'
|
||||||
@@ -351,7 +345,7 @@ export const getChannelsColumns = ({
|
|||||||
innerButtons
|
innerButtons
|
||||||
defaultValue={record.priority}
|
defaultValue={record.priority}
|
||||||
min={-999}
|
min={-999}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -364,7 +358,10 @@ export const getChannelsColumns = ({
|
|||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
Modal.warning({
|
Modal.warning({
|
||||||
title: t('修改子渠道优先级'),
|
title: t('修改子渠道优先级'),
|
||||||
content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
|
content:
|
||||||
|
t('确定要修改所有子渠道优先级为 ') +
|
||||||
|
e.target.value +
|
||||||
|
t(' 吗?'),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
if (e.target.value === '') {
|
if (e.target.value === '') {
|
||||||
return;
|
return;
|
||||||
@@ -379,7 +376,7 @@ export const getChannelsColumns = ({
|
|||||||
innerButtons
|
innerButtons
|
||||||
defaultValue={record.priority}
|
defaultValue={record.priority}
|
||||||
min={-999}
|
min={-999}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -403,7 +400,7 @@ export const getChannelsColumns = ({
|
|||||||
innerButtons
|
innerButtons
|
||||||
defaultValue={record.weight}
|
defaultValue={record.weight}
|
||||||
min={0}
|
min={0}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -416,7 +413,10 @@ export const getChannelsColumns = ({
|
|||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
Modal.warning({
|
Modal.warning({
|
||||||
title: t('修改子渠道权重'),
|
title: t('修改子渠道权重'),
|
||||||
content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
|
content:
|
||||||
|
t('确定要修改所有子渠道权重为 ') +
|
||||||
|
e.target.value +
|
||||||
|
t(' 吗?'),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
if (e.target.value === '') {
|
if (e.target.value === '') {
|
||||||
return;
|
return;
|
||||||
@@ -431,7 +431,7 @@ export const getChannelsColumns = ({
|
|||||||
innerButtons
|
innerButtons
|
||||||
defaultValue={record.weight}
|
defaultValue={record.weight}
|
||||||
min={-999}
|
min={-999}
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -484,18 +484,18 @@ export const getChannelsColumns = ({
|
|||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<SplitButtonGroup
|
<SplitButtonGroup
|
||||||
className="overflow-hidden"
|
className='overflow-hidden'
|
||||||
aria-label={t('测试单个渠道操作项目组')}
|
aria-label={t('测试单个渠道操作项目组')}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
onClick={() => testChannel(record, '')}
|
onClick={() => testChannel(record, '')}
|
||||||
>
|
>
|
||||||
{t('测试')}
|
{t('测试')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
icon={<IconTreeTriangleDown />}
|
icon={<IconTreeTriangleDown />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -505,32 +505,28 @@ export const getChannelsColumns = ({
|
|||||||
/>
|
/>
|
||||||
</SplitButtonGroup>
|
</SplitButtonGroup>
|
||||||
|
|
||||||
{
|
{record.status === 1 ? (
|
||||||
record.status === 1 ? (
|
|
||||||
<Button
|
<Button
|
||||||
type='danger'
|
type='danger'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => manageChannel(record.id, 'disable', record)}
|
onClick={() => manageChannel(record.id, 'disable', record)}
|
||||||
>
|
>
|
||||||
{t('禁用')}
|
{t('禁用')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => manageChannel(record.id, 'enable', record)}
|
onClick={() => manageChannel(record.id, 'enable', record)}
|
||||||
>
|
>
|
||||||
{t('启用')}
|
{t('启用')}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
{record.channel_info?.is_multi_key ? (
|
{record.channel_info?.is_multi_key ? (
|
||||||
<SplitButtonGroup
|
<SplitButtonGroup aria-label={t('多密钥渠道操作项目组')}>
|
||||||
aria-label={t('多密钥渠道操作项目组')}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingChannel(record);
|
setEditingChannel(record);
|
||||||
setShowEdit(true);
|
setShowEdit(true);
|
||||||
@@ -549,12 +545,12 @@ export const getChannelsColumns = ({
|
|||||||
setCurrentMultiKeyChannel(record);
|
setCurrentMultiKeyChannel(record);
|
||||||
setShowMultiKeyManageModal(true);
|
setShowMultiKeyManageModal(true);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
icon={<IconTreeTriangleDown />}
|
icon={<IconTreeTriangleDown />}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -562,7 +558,7 @@ export const getChannelsColumns = ({
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingChannel(record);
|
setEditingChannel(record);
|
||||||
setShowEdit(true);
|
setShowEdit(true);
|
||||||
@@ -577,11 +573,7 @@ export const getChannelsColumns = ({
|
|||||||
position='bottomRight'
|
position='bottomRight'
|
||||||
menu={moreMenuItems}
|
menu={moreMenuItems}
|
||||||
>
|
>
|
||||||
<Button
|
<Button icon={<IconMore />} type='tertiary' size='small' />
|
||||||
icon={<IconMore />}
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
@@ -591,21 +583,21 @@ export const getChannelsColumns = ({
|
|||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => manageTag(record.key, 'enable')}
|
onClick={() => manageTag(record.key, 'enable')}
|
||||||
>
|
>
|
||||||
{t('启用全部')}
|
{t('启用全部')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => manageTag(record.key, 'disable')}
|
onClick={() => manageTag(record.key, 'disable')}
|
||||||
>
|
>
|
||||||
{t('禁用全部')}
|
{t('禁用全部')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEditTag(true);
|
setShowEditTag(true);
|
||||||
setEditingTag(record.key);
|
setEditingTag(record.key);
|
||||||
|
|||||||
@@ -34,16 +34,16 @@ const ChannelsFilters = ({
|
|||||||
groupOptions,
|
groupOptions,
|
||||||
loading,
|
loading,
|
||||||
searching,
|
searching,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
|
<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 gap-2 w-full md:w-auto order-2 md:order-1'>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
theme='light'
|
theme='light'
|
||||||
type='primary'
|
type='primary'
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingChannel({
|
setEditingChannel({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
@@ -57,7 +57,7 @@ const ChannelsFilters = ({
|
|||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
>
|
>
|
||||||
{t('刷新')}
|
{t('刷新')}
|
||||||
@@ -67,54 +67,54 @@ const ChannelsFilters = ({
|
|||||||
size='small'
|
size='small'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
onClick={() => setShowColumnSelector(true)}
|
onClick={() => setShowColumnSelector(true)}
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
>
|
>
|
||||||
{t('列设置')}
|
{t('列设置')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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
|
<Form
|
||||||
initValues={formInitValues}
|
initValues={formInitValues}
|
||||||
getFormApi={(api) => setFormApi(api)}
|
getFormApi={(api) => setFormApi(api)}
|
||||||
onSubmit={() => searchChannels(enableTagMode)}
|
onSubmit={() => searchChannels(enableTagMode)}
|
||||||
allowEmpty={true}
|
allowEmpty={true}
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
layout="horizontal"
|
layout='horizontal'
|
||||||
trigger="change"
|
trigger='change'
|
||||||
stopValidateWithError={false}
|
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
|
<Form.Input
|
||||||
size='small'
|
size='small'
|
||||||
field="searchKeyword"
|
field='searchKeyword'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('渠道ID,名称,密钥,API地址')}
|
placeholder={t('渠道ID,名称,密钥,API地址')}
|
||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-48">
|
<div className='w-full md:w-48'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
size='small'
|
size='small'
|
||||||
field="searchModel"
|
field='searchModel'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('模型关键字')}
|
placeholder={t('模型关键字')}
|
||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-32">
|
<div className='w-full md:w-32'>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
size='small'
|
size='small'
|
||||||
field="searchGroup"
|
field='searchGroup'
|
||||||
placeholder={t('选择分组')}
|
placeholder={t('选择分组')}
|
||||||
optionList={[
|
optionList={[
|
||||||
{ label: t('选择分组'), value: null },
|
{ label: t('选择分组'), value: null },
|
||||||
...groupOptions,
|
...groupOptions,
|
||||||
]}
|
]}
|
||||||
className="w-full"
|
className='w-full'
|
||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
@@ -127,10 +127,10 @@ const ChannelsFilters = ({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type="tertiary"
|
type='tertiary'
|
||||||
htmlType="submit"
|
htmlType='submit'
|
||||||
loading={loading || searching}
|
loading={loading || searching}
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -146,7 +146,7 @@ const ChannelsFilters = ({
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full md:w-auto"
|
className='w-full md:w-auto'
|
||||||
>
|
>
|
||||||
{t('重置')}
|
{t('重置')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Empty } from '@douyinfe/semi-ui';
|
|||||||
import CardTable from '../../common/ui/CardTable';
|
import CardTable from '../../common/ui/CardTable';
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
IllustrationNoResultDark
|
IllustrationNoResultDark,
|
||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { getChannelsColumns } from './ChannelsColumnDefs';
|
import { getChannelsColumns } from './ChannelsColumnDefs';
|
||||||
|
|
||||||
@@ -151,13 +151,15 @@ const ChannelsTable = (channelsData) => {
|
|||||||
empty={
|
empty={
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
darkModeImage={
|
||||||
|
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
description={t('搜索无结果')}
|
description={t('搜索无结果')}
|
||||||
style={{ padding: 30 }}
|
style={{ padding: 30 }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
className="rounded-xl overflow-hidden"
|
className='rounded-xl overflow-hidden'
|
||||||
size="middle"
|
size='middle'
|
||||||
loading={loading || searching}
|
loading={loading || searching}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const ChannelsTabs = ({
|
|||||||
pageSize,
|
pageSize,
|
||||||
idSort,
|
idSort,
|
||||||
setActivePage,
|
setActivePage,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
if (enableTagMode) return null;
|
if (enableTagMode) return null;
|
||||||
|
|
||||||
@@ -46,24 +46,29 @@ const ChannelsTabs = ({
|
|||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeTypeKey}
|
activeKey={activeTypeKey}
|
||||||
type="card"
|
type='card'
|
||||||
collapsible
|
collapsible
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
className="mb-2"
|
className='mb-2'
|
||||||
>
|
>
|
||||||
<TabPane
|
<TabPane
|
||||||
itemKey="all"
|
itemKey='all'
|
||||||
tab={
|
tab={
|
||||||
<span className="flex items-center gap-2">
|
<span className='flex items-center gap-2'>
|
||||||
{t('全部')}
|
{t('全部')}
|
||||||
<Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} shape='circle'>
|
<Tag
|
||||||
|
color={activeTypeKey === 'all' ? 'red' : 'grey'}
|
||||||
|
shape='circle'
|
||||||
|
>
|
||||||
{channelTypeCounts['all'] || 0}
|
{channelTypeCounts['all'] || 0}
|
||||||
</Tag>
|
</Tag>
|
||||||
</span>
|
</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 key = String(option.value);
|
||||||
const count = channelTypeCounts[option.value] || 0;
|
const count = channelTypeCounts[option.value] || 0;
|
||||||
return (
|
return (
|
||||||
@@ -71,10 +76,13 @@ const ChannelsTabs = ({
|
|||||||
key={key}
|
key={key}
|
||||||
itemKey={key}
|
itemKey={key}
|
||||||
tab={
|
tab={
|
||||||
<span className="flex items-center gap-2">
|
<span className='flex items-center gap-2'>
|
||||||
{getChannelIcon(option.value)}
|
{getChannelIcon(option.value)}
|
||||||
{option.label}
|
{option.label}
|
||||||
<Tag color={activeTypeKey === key ? 'red' : 'grey'} shape='circle'>
|
<Tag
|
||||||
|
color={activeTypeKey === key ? 'red' : 'grey'}
|
||||||
|
shape='circle'
|
||||||
|
>
|
||||||
{count}
|
{count}
|
||||||
</Tag>
|
</Tag>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const ChannelsPage = () => {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<CardPro
|
<CardPro
|
||||||
type="type3"
|
type='type3'
|
||||||
tabsArea={<ChannelsTabs {...channelsData} />}
|
tabsArea={<ChannelsTabs {...channelsData} />}
|
||||||
actionsArea={<ChannelsActions {...channelsData} />}
|
actionsArea={<ChannelsActions {...channelsData} />}
|
||||||
searchArea={<ChannelsFilters {...channelsData} />}
|
searchArea={<ChannelsFilters {...channelsData} />}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const BatchTagModal = ({
|
|||||||
batchSetTagValue,
|
batchSetTagValue,
|
||||||
setBatchSetTagValue,
|
setBatchSetTagValue,
|
||||||
selectedChannels,
|
selectedChannels,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -37,10 +37,10 @@ const BatchTagModal = ({
|
|||||||
onCancel={() => setShowBatchSetTag(false)}
|
onCancel={() => setShowBatchSetTag(false)}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
centered={true}
|
centered={true}
|
||||||
size="small"
|
size='small'
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
>
|
>
|
||||||
<div className="mb-5">
|
<div className='mb-5'>
|
||||||
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
|
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -48,9 +48,12 @@ const BatchTagModal = ({
|
|||||||
value={batchSetTagValue}
|
value={batchSetTagValue}
|
||||||
onChange={(v) => setBatchSetTagValue(v)}
|
onChange={(v) => setBatchSetTagValue(v)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
<div className='mt-4'>
|
||||||
<Typography.Text type='secondary'>
|
<Typography.Text type='secondary'>
|
||||||
{t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
|
{t('已选择 ${count} 个渠道').replace(
|
||||||
|
'${count}',
|
||||||
|
selectedChannels.length,
|
||||||
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -74,10 +74,8 @@ const ColumnSelectorModal = ({
|
|||||||
visible={showColumnSelector}
|
visible={showColumnSelector}
|
||||||
onCancel={() => setShowColumnSelector(false)}
|
onCancel={() => setShowColumnSelector(false)}
|
||||||
footer={
|
footer={
|
||||||
<div className="flex justify-end">
|
<div className='flex justify-end'>
|
||||||
<Button onClick={() => initDefaultColumns()}>
|
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setShowColumnSelector(false)}>
|
<Button onClick={() => setShowColumnSelector(false)}>
|
||||||
{t('取消')}
|
{t('取消')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -100,7 +98,7 @@ const ColumnSelectorModal = ({
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<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)' }}
|
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||||
>
|
>
|
||||||
{allColumns.map((column) => {
|
{allColumns.map((column) => {
|
||||||
@@ -110,10 +108,7 @@ const ColumnSelectorModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={column.key} className='w-1/2 mb-4 pr-2'>
|
||||||
key={column.key}
|
|
||||||
className="w-1/2 mb-4 pr-2"
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!visibleColumns[column.key]}
|
checked={!!visibleColumns[column.key]}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -289,7 +289,7 @@ const EditTagModal = (props) => {
|
|||||||
t('已新增 {{count}} 个模型:{{list}}', {
|
t('已新增 {{count}} 个模型:{{list}}', {
|
||||||
count: addedModels.length,
|
count: addedModels.length,
|
||||||
list: addedModels.join(', '),
|
list: addedModels.join(', '),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showInfo(t('未发现新增模型'));
|
showInfo(t('未发现新增模型'));
|
||||||
@@ -301,8 +301,10 @@ const EditTagModal = (props) => {
|
|||||||
placement='right'
|
placement='right'
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<Tag color="blue" shape="circle">{t('编辑')}</Tag>
|
<Tag color='blue' shape='circle'>
|
||||||
<Title heading={4} className="m-0">
|
{t('编辑')}
|
||||||
|
</Tag>
|
||||||
|
<Title heading={4} className='m-0'>
|
||||||
{t('编辑标签')}
|
{t('编辑标签')}
|
||||||
</Title>
|
</Title>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -312,10 +314,10 @@ const EditTagModal = (props) => {
|
|||||||
width={600}
|
width={600}
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
footer={
|
footer={
|
||||||
<div className="flex justify-end bg-white">
|
<div className='flex justify-end bg-white'>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
theme="solid"
|
theme='solid'
|
||||||
onClick={() => formApiRef.current?.submitForm()}
|
onClick={() => formApiRef.current?.submitForm()}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
icon={<IconSave />}
|
icon={<IconSave />}
|
||||||
@@ -323,8 +325,8 @@ const EditTagModal = (props) => {
|
|||||||
{t('保存')}
|
{t('保存')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme="light"
|
theme='light'
|
||||||
type="primary"
|
type='primary'
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
icon={<IconClose />}
|
icon={<IconClose />}
|
||||||
>
|
>
|
||||||
@@ -343,26 +345,28 @@ const EditTagModal = (props) => {
|
|||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className="p-2">
|
<div className='p-2'>
|
||||||
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
|
||||||
{/* Header: Tag Info */}
|
{/* Header: Tag Info */}
|
||||||
<div className="flex items-center mb-2">
|
<div className='flex items-center mb-2'>
|
||||||
<Avatar size="small" color="blue" className="mr-2 shadow-md">
|
<Avatar size='small' color='blue' className='mr-2 shadow-md'>
|
||||||
<IconBookmark size={16} />
|
<IconBookmark size={16} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Text className="text-lg font-medium">{t('标签信息')}</Text>
|
<Text className='text-lg font-medium'>{t('标签信息')}</Text>
|
||||||
<div className="text-xs text-gray-600">{t('标签的基本配置')}</div>
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('标签的基本配置')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
type="warning"
|
type='warning'
|
||||||
description={t('所有编辑均为覆盖操作,留空则不更改')}
|
description={t('所有编辑均为覆盖操作,留空则不更改')}
|
||||||
className="!rounded-lg mb-4"
|
className='!rounded-lg mb-4'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field='new_tag'
|
field='new_tag'
|
||||||
label={t('标签名称')}
|
label={t('标签名称')}
|
||||||
@@ -372,23 +376,31 @@ const EditTagModal = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
|
||||||
{/* Header: Model Config */}
|
{/* Header: Model Config */}
|
||||||
<div className="flex items-center mb-2">
|
<div className='flex items-center mb-2'>
|
||||||
<Avatar size="small" color="purple" className="mr-2 shadow-md">
|
<Avatar
|
||||||
|
size='small'
|
||||||
|
color='purple'
|
||||||
|
className='mr-2 shadow-md'
|
||||||
|
>
|
||||||
<IconCode size={16} />
|
<IconCode size={16} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Text className="text-lg font-medium">{t('模型配置')}</Text>
|
<Text className='text-lg font-medium'>{t('模型配置')}</Text>
|
||||||
<div className="text-xs text-gray-600">{t('模型选择和映射设置')}</div>
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('模型选择和映射设置')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Banner
|
<Banner
|
||||||
type="info"
|
type='info'
|
||||||
description={t('当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。')}
|
description={t(
|
||||||
className="!rounded-lg mb-4"
|
'当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。',
|
||||||
|
)}
|
||||||
|
className='!rounded-lg mb-4'
|
||||||
/>
|
/>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
field='models'
|
field='models'
|
||||||
@@ -408,46 +420,87 @@ const EditTagModal = (props) => {
|
|||||||
label={t('自定义模型名称')}
|
label={t('自定义模型名称')}
|
||||||
placeholder={t('输入自定义模型名称')}
|
placeholder={t('输入自定义模型名称')}
|
||||||
onChange={(value) => setCustomModel(value.trim())}
|
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
|
<Form.TextArea
|
||||||
field='model_mapping'
|
field='model_mapping'
|
||||||
label={t('模型重定向')}
|
label={t('模型重定向')}
|
||||||
placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改')}
|
placeholder={t(
|
||||||
autosize
|
'此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改',
|
||||||
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>
|
|
||||||
</Space>
|
|
||||||
)}
|
)}
|
||||||
|
autosize
|
||||||
|
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>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="!rounded-2xl shadow-sm border-0">
|
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||||
{/* Header: Group Settings */}
|
{/* Header: Group Settings */}
|
||||||
<div className="flex items-center mb-2">
|
<div className='flex items-center mb-2'>
|
||||||
<Avatar size="small" color="green" className="mr-2 shadow-md">
|
<Avatar size='small' color='green' className='mr-2 shadow-md'>
|
||||||
<IconUser size={16} />
|
<IconUser size={16} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Text className="text-lg font-medium">{t('分组设置')}</Text>
|
<Text className='text-lg font-medium'>{t('分组设置')}</Text>
|
||||||
<div className="text-xs text-gray-600">{t('用户分组配置')}</div>
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('用户分组配置')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className='space-y-4'>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
field='groups'
|
field='groups'
|
||||||
label={t('分组')}
|
label={t('分组')}
|
||||||
placeholder={t('请选择可以使用该渠道的分组,留空则不更改')}
|
placeholder={t('请选择可以使用该渠道的分组,留空则不更改')}
|
||||||
multiple
|
multiple
|
||||||
allowAdditions
|
allowAdditions
|
||||||
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
|
additionLabel={t(
|
||||||
|
'请在系统设置页面编辑分组倍率以添加新的分组:',
|
||||||
|
)}
|
||||||
optionList={groupOptions}
|
optionList={groupOptions}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
onChange={(value) => handleInputChange('groups', value)}
|
onChange={(value) => handleInputChange('groups', value)}
|
||||||
|
|||||||
@@ -19,16 +19,31 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
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 {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
IllustrationNoResultDark
|
IllustrationNoResultDark,
|
||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getModelCategories } from '../../../../helpers/render';
|
import { getModelCategories } from '../../../../helpers/render';
|
||||||
|
|
||||||
const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCancel }) => {
|
const ModelSelectModal = ({
|
||||||
|
visible,
|
||||||
|
models = [],
|
||||||
|
selected = [],
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [checkedList, setCheckedList] = useState(selected);
|
const [checkedList, setCheckedList] = useState(selected);
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
@@ -36,11 +51,15 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
|
|
||||||
const isMobile = useIsMobile();
|
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 newModels = filteredModels.filter((model) => !selected.includes(model));
|
||||||
const existingModels = filteredModels.filter(model => selected.includes(model));
|
const existingModels = filteredModels.filter((model) =>
|
||||||
|
selected.includes(model),
|
||||||
|
);
|
||||||
|
|
||||||
// 同步外部选中值
|
// 同步外部选中值
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +87,7 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
const categorizedModels = {};
|
const categorizedModels = {};
|
||||||
const uncategorizedModels = [];
|
const uncategorizedModels = [];
|
||||||
|
|
||||||
models.forEach(model => {
|
models.forEach((model) => {
|
||||||
let foundCategory = false;
|
let foundCategory = false;
|
||||||
for (const [key, category] of Object.entries(categories)) {
|
for (const [key, category] of Object.entries(categories)) {
|
||||||
if (key !== 'all' && category.filter({ model_name: model })) {
|
if (key !== 'all' && category.filter({ model_name: model })) {
|
||||||
@@ -76,7 +95,7 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
categorizedModels[key] = {
|
categorizedModels[key] = {
|
||||||
label: category.label,
|
label: category.label,
|
||||||
icon: category.icon,
|
icon: category.icon,
|
||||||
models: []
|
models: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
categorizedModels[key].models.push(model);
|
categorizedModels[key].models.push(model);
|
||||||
@@ -94,7 +113,7 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
categorizedModels['other'] = {
|
categorizedModels['other'] = {
|
||||||
label: t('其他'),
|
label: t('其他'),
|
||||||
icon: null,
|
icon: null,
|
||||||
models: uncategorizedModels
|
models: uncategorizedModels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,14 +125,22 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
|
|
||||||
// Tab列表配置
|
// Tab列表配置
|
||||||
const tabList = [
|
const tabList = [
|
||||||
...(newModels.length > 0 ? [{
|
...(newModels.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
tab: `${t('新获取的模型')} (${newModels.length})`,
|
tab: `${t('新获取的模型')} (${newModels.length})`,
|
||||||
itemKey: 'new'
|
itemKey: 'new',
|
||||||
}] : []),
|
},
|
||||||
...(existingModels.length > 0 ? [{
|
]
|
||||||
|
: []),
|
||||||
|
...(existingModels.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
tab: `${t('已有的模型')} (${existingModels.length})`,
|
tab: `${t('已有的模型')} (${existingModels.length})`,
|
||||||
itemKey: 'existing'
|
itemKey: 'existing',
|
||||||
}] : [])
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 处理分类全选/取消全选
|
// 处理分类全选/取消全选
|
||||||
@@ -122,14 +149,16 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
|
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
// 全选:添加该分类下所有未选中的模型
|
// 全选:添加该分类下所有未选中的模型
|
||||||
categoryModels.forEach(model => {
|
categoryModels.forEach((model) => {
|
||||||
if (!newCheckedList.includes(model)) {
|
if (!newCheckedList.includes(model)) {
|
||||||
newCheckedList.push(model);
|
newCheckedList.push(model);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 取消全选:移除该分类下所有已选中的模型
|
// 取消全选:移除该分类下所有已选中的模型
|
||||||
newCheckedList = newCheckedList.filter(model => !categoryModels.includes(model));
|
newCheckedList = newCheckedList.filter(
|
||||||
|
(model) => !categoryModels.includes(model),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCheckedList(newCheckedList);
|
setCheckedList(newCheckedList);
|
||||||
@@ -137,12 +166,17 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
|
|
||||||
// 检查分类是否全选
|
// 检查分类是否全选
|
||||||
const isCategoryAllSelected = (categoryModels) => {
|
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 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;
|
return selectedCount > 0 && selectedCount < categoryModels.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,10 +185,15 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
if (categoryEntries.length === 0) return null;
|
if (categoryEntries.length === 0) return null;
|
||||||
|
|
||||||
// 生成所有面板的key,确保都展开
|
// 生成所有面板的key,确保都展开
|
||||||
const allActiveKeys = categoryEntries.map((_, index) => `${categoryKeyPrefix}_${index}`);
|
const allActiveKeys = categoryEntries.map(
|
||||||
|
(_, index) => `${categoryKeyPrefix}_${index}`,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse key={`${categoryKeyPrefix}_${categoryEntries.length}`} defaultActiveKey={[]}>
|
<Collapse
|
||||||
|
key={`${categoryKeyPrefix}_${categoryEntries.length}`}
|
||||||
|
defaultActiveKey={[]}
|
||||||
|
>
|
||||||
{categoryEntries.map(([key, categoryData], index) => (
|
{categoryEntries.map(([key, categoryData], index) => (
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key={`${categoryKeyPrefix}_${index}`}
|
key={`${categoryKeyPrefix}_${index}`}
|
||||||
@@ -166,24 +205,29 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
indeterminate={isCategoryIndeterminate(categoryData.models)}
|
indeterminate={isCategoryIndeterminate(categoryData.models)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.stopPropagation(); // 防止触发面板折叠
|
e.stopPropagation(); // 防止触发面板折叠
|
||||||
handleCategorySelectAll(categoryData.models, e.target.checked);
|
handleCategorySelectAll(
|
||||||
|
categoryData.models,
|
||||||
|
e.target.checked,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()} // 防止点击checkbox时折叠面板
|
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}
|
{categoryData.icon}
|
||||||
<Typography.Text type="secondary" size="small">
|
<Typography.Text type='secondary' size='small'>
|
||||||
{t('已选择 {{selected}} / {{total}}', {
|
{t('已选择 {{selected}} / {{total}}', {
|
||||||
selected: categoryData.models.filter(model => checkedList.includes(model)).length,
|
selected: categoryData.models.filter((model) =>
|
||||||
total: categoryData.models.length
|
checkedList.includes(model),
|
||||||
|
).length,
|
||||||
|
total: categoryData.models.length,
|
||||||
})}
|
})}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4">
|
<div className='grid grid-cols-2 gap-x-4'>
|
||||||
{categoryData.models.map((model) => (
|
{categoryData.models.map((model) => (
|
||||||
<Checkbox key={model} value={model} className="my-1">
|
<Checkbox key={model} value={model} className='my-1'>
|
||||||
{model}
|
{model}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
@@ -197,14 +241,14 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
header={
|
header={
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4">
|
<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">
|
<Typography.Title heading={5} className='m-0'>
|
||||||
{t('选择模型')}
|
{t('选择模型')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<div className="flex-shrink-0">
|
<div className='flex-shrink-0'>
|
||||||
<Tabs
|
<Tabs
|
||||||
type="slash"
|
type='slash'
|
||||||
size="small"
|
size='small'
|
||||||
tabList={tabList}
|
tabList={tabList}
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={(key) => setActiveTab(key)}
|
onChange={(key) => setActiveTab(key)}
|
||||||
@@ -234,17 +278,22 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
<div style={{ maxHeight: 400, overflowY: 'auto', paddingRight: 8 }}>
|
<div style={{ maxHeight: 400, overflowY: 'auto', paddingRight: 8 }}>
|
||||||
{filteredModels.length === 0 ? (
|
{filteredModels.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
image={
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
<IllustrationNoResult style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
description={t('暂无匹配模型')}
|
description={t('暂无匹配模型')}
|
||||||
style={{ padding: 30 }}
|
style={{ padding: 30 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Checkbox.Group value={checkedList} onChange={(vals) => setCheckedList(vals)}>
|
<Checkbox.Group
|
||||||
|
value={checkedList}
|
||||||
|
onChange={(vals) => setCheckedList(vals)}
|
||||||
|
>
|
||||||
{activeTab === 'new' && newModels.length > 0 && (
|
{activeTab === 'new' && newModels.length > 0 && (
|
||||||
<div>
|
<div>{renderModelsByCategory(newModelsByCategory, 'new')}</div>
|
||||||
{renderModelsByCategory(newModelsByCategory, 'new')}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{activeTab === 'existing' && existingModels.length > 0 && (
|
{activeTab === 'existing' && existingModels.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -256,20 +305,30 @@ const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCa
|
|||||||
</div>
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<Typography.Text type="secondary" size="small" className="block text-right mt-4">
|
<Typography.Text
|
||||||
<div className="flex items-center justify-end gap-2">
|
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 currentModels =
|
||||||
const currentSelected = currentModels.filter(model => checkedList.includes(model)).length;
|
activeTab === 'new' ? newModels : existingModels;
|
||||||
const isAllSelected = currentModels.length > 0 && currentSelected === currentModels.length;
|
const currentSelected = currentModels.filter((model) =>
|
||||||
const isIndeterminate = currentSelected > 0 && currentSelected < currentModels.length;
|
checkedList.includes(model),
|
||||||
|
).length;
|
||||||
|
const isAllSelected =
|
||||||
|
currentModels.length > 0 &&
|
||||||
|
currentSelected === currentModels.length;
|
||||||
|
const isIndeterminate =
|
||||||
|
currentSelected > 0 && currentSelected < currentModels.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{t('已选择 {{selected}} / {{total}}', {
|
{t('已选择 {{selected}} / {{total}}', {
|
||||||
selected: currentSelected,
|
selected: currentSelected,
|
||||||
total: currentModels.length
|
total: currentModels.length,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Typography
|
Typography,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { copy, showError, showInfo, showSuccess } from '../../../../helpers';
|
import { copy, showError, showInfo, showSuccess } from '../../../../helpers';
|
||||||
@@ -47,7 +47,7 @@ const ModelTestModal = ({
|
|||||||
setModelTablePage,
|
setModelTablePage,
|
||||||
allSelectingRef,
|
allSelectingRef,
|
||||||
isMobile,
|
isMobile,
|
||||||
t
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const hasChannel = Boolean(currentTestChannel);
|
const hasChannel = Boolean(currentTestChannel);
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ const ModelTestModal = ({
|
|||||||
? currentTestChannel.models
|
? currentTestChannel.models
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter((model) =>
|
.filter((model) =>
|
||||||
model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
|
model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -66,7 +66,12 @@ const ModelTestModal = ({
|
|||||||
}
|
}
|
||||||
copy(selectedModelKeys.join(',')).then((ok) => {
|
copy(selectedModelKeys.join(',')).then((ok) => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
showSuccess(t('已复制 ${count} 个模型').replace('${count}', selectedModelKeys.length));
|
showSuccess(
|
||||||
|
t('已复制 ${count} 个模型').replace(
|
||||||
|
'${count}',
|
||||||
|
selectedModelKeys.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showError(t('复制失败,请手动复制'));
|
showError(t('复制失败,请手动复制'));
|
||||||
}
|
}
|
||||||
@@ -93,16 +98,17 @@ const ModelTestModal = ({
|
|||||||
title: t('模型名称'),
|
title: t('模型名称'),
|
||||||
dataIndex: 'model',
|
dataIndex: 'model',
|
||||||
render: (text) => (
|
render: (text) => (
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<Typography.Text strong>{text}</Typography.Text>
|
<Typography.Text strong>{text}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('状态'),
|
title: t('状态'),
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const testResult = modelTestResults[`${currentTestChannel.id}-${record.model}`];
|
const testResult =
|
||||||
|
modelTestResults[`${currentTestChannel.id}-${record.model}`];
|
||||||
const isTesting = testingModels.has(record.model);
|
const isTesting = testingModels.has(record.model);
|
||||||
|
|
||||||
if (isTesting) {
|
if (isTesting) {
|
||||||
@@ -122,21 +128,21 @@ const ModelTestModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Tag
|
<Tag color={testResult.success ? 'green' : 'red'} shape='circle'>
|
||||||
color={testResult.success ? 'green' : 'red'}
|
|
||||||
shape='circle'
|
|
||||||
>
|
|
||||||
{testResult.success ? t('成功') : t('失败')}
|
{testResult.success ? t('成功') : t('失败')}
|
||||||
</Tag>
|
</Tag>
|
||||||
{testResult.success && (
|
{testResult.success && (
|
||||||
<Typography.Text type="tertiary">
|
<Typography.Text type='tertiary'>
|
||||||
{t('请求时长: ${time}s').replace('${time}', testResult.time.toFixed(2))}
|
{t('请求时长: ${time}s').replace(
|
||||||
|
'${time}',
|
||||||
|
testResult.time.toFixed(2),
|
||||||
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -153,8 +159,8 @@ const ModelTestModal = ({
|
|||||||
{t('测试')}
|
{t('测试')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataSource = (() => {
|
const dataSource = (() => {
|
||||||
@@ -169,34 +175,35 @@ const ModelTestModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={hasChannel ? (
|
title={
|
||||||
<div className="flex flex-col gap-2 w-full">
|
hasChannel ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex flex-col gap-2 w-full'>
|
||||||
<Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
|
<div className='flex items-center gap-2'>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
className='!text-[var(--semi-color-text-0)] !text-base'
|
||||||
|
>
|
||||||
{currentTestChannel.name} {t('渠道的模型测试')}
|
{currentTestChannel.name} {t('渠道的模型测试')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text type="tertiary" size="small">
|
<Typography.Text type='tertiary' size='small'>
|
||||||
{t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
|
{t('共')} {currentTestChannel.models.split(',').length}{' '}
|
||||||
|
{t('个模型')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null
|
||||||
|
}
|
||||||
visible={showModelTestModal}
|
visible={showModelTestModal}
|
||||||
onCancel={handleCloseModal}
|
onCancel={handleCloseModal}
|
||||||
footer={hasChannel ? (
|
footer={
|
||||||
<div className="flex justify-end">
|
hasChannel ? (
|
||||||
|
<div className='flex justify-end'>
|
||||||
{isBatchTesting ? (
|
{isBatchTesting ? (
|
||||||
<Button
|
<Button type='danger' onClick={handleCloseModal}>
|
||||||
type='danger'
|
|
||||||
onClick={handleCloseModal}
|
|
||||||
>
|
|
||||||
{t('停止测试')}
|
{t('停止测试')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button type='tertiary' onClick={handleCloseModal}>
|
||||||
type='tertiary'
|
|
||||||
onClick={handleCloseModal}
|
|
||||||
>
|
|
||||||
{t('取消')}
|
{t('取消')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -205,20 +212,24 @@ const ModelTestModal = ({
|
|||||||
loading={isBatchTesting}
|
loading={isBatchTesting}
|
||||||
disabled={isBatchTesting}
|
disabled={isBatchTesting}
|
||||||
>
|
>
|
||||||
{isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
|
{isBatchTesting
|
||||||
|
? t('测试中...')
|
||||||
|
: t('批量测试${count}个模型').replace(
|
||||||
'${count}',
|
'${count}',
|
||||||
filteredModels.length
|
filteredModels.length,
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null
|
||||||
|
}
|
||||||
maskClosable={!isBatchTesting}
|
maskClosable={!isBatchTesting}
|
||||||
className="!rounded-lg"
|
className='!rounded-lg'
|
||||||
size={isMobile ? 'full-width' : 'large'}
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
>
|
>
|
||||||
{hasChannel && (<div className="model-test-scroll">
|
{hasChannel && (
|
||||||
|
<div className='model-test-scroll'>
|
||||||
{/* 搜索与操作按钮 */}
|
{/* 搜索与操作按钮 */}
|
||||||
<div className="flex items-center justify-end gap-2 w-full mb-2">
|
<div className='flex items-center justify-end gap-2 w-full mb-2'>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('搜索模型...')}
|
placeholder={t('搜索模型...')}
|
||||||
value={modelSearchKeyword}
|
value={modelSearchKeyword}
|
||||||
@@ -226,19 +237,14 @@ const ModelTestModal = ({
|
|||||||
setModelSearchKeyword(v);
|
setModelSearchKeyword(v);
|
||||||
setModelTablePage(1);
|
setModelTablePage(1);
|
||||||
}}
|
}}
|
||||||
className="!w-full"
|
className='!w-full'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
showClear
|
showClear
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleCopySelected}>
|
<Button onClick={handleCopySelected}>{t('复制已选')}</Button>
|
||||||
{t('复制已选')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button type='tertiary' onClick={handleSelectSuccess}>
|
||||||
type='tertiary'
|
|
||||||
onClick={handleSelectSuccess}
|
|
||||||
>
|
|
||||||
{t('选择成功')}
|
{t('选择成功')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +274,8 @@ const ModelTestModal = ({
|
|||||||
onPageChange: (page) => setModelTablePage(page),
|
onPageChange: (page) => setModelTablePage(page),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>)}
|
</div>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,19 +35,22 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Badge,
|
Badge,
|
||||||
Progress,
|
Progress,
|
||||||
Card
|
Card,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
|
import {
|
||||||
import { API, showError, showSuccess, timestamp2string } from '../../../../helpers';
|
IllustrationNoResult,
|
||||||
|
IllustrationNoResultDark,
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
|
import {
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
timestamp2string,
|
||||||
|
} from '../../../../helpers';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const MultiKeyManageModal = ({
|
const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
||||||
visible,
|
|
||||||
onCancel,
|
|
||||||
channel,
|
|
||||||
onRefresh
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [keyStatusList, setKeyStatusList] = useState([]);
|
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
|
const [statusFilter, setStatusFilter] = useState(null); // null=all, 1=enabled, 2=manual_disabled, 3=auto_disabled
|
||||||
|
|
||||||
// Load key status data
|
// 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;
|
if (!channel?.id) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -77,7 +84,7 @@ const MultiKeyManageModal = ({
|
|||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'get_key_status',
|
action: 'get_key_status',
|
||||||
page: page,
|
page: page,
|
||||||
page_size: size
|
page_size: size,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add status filter if specified
|
// Add status filter if specified
|
||||||
@@ -113,13 +120,13 @@ const MultiKeyManageModal = ({
|
|||||||
// Disable a specific key
|
// Disable a specific key
|
||||||
const handleDisableKey = async (keyIndex) => {
|
const handleDisableKey = async (keyIndex) => {
|
||||||
const operationId = `disable_${keyIndex}`;
|
const operationId = `disable_${keyIndex}`;
|
||||||
setOperationLoading(prev => ({ ...prev, [operationId]: true }));
|
setOperationLoading((prev) => ({ ...prev, [operationId]: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/channel/multi_key/manage', {
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'disable_key',
|
action: 'disable_key',
|
||||||
key_index: keyIndex
|
key_index: keyIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -132,20 +139,20 @@ const MultiKeyManageModal = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('禁用密钥失败'));
|
showError(t('禁用密钥失败'));
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading(prev => ({ ...prev, [operationId]: false }));
|
setOperationLoading((prev) => ({ ...prev, [operationId]: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable a specific key
|
// Enable a specific key
|
||||||
const handleEnableKey = async (keyIndex) => {
|
const handleEnableKey = async (keyIndex) => {
|
||||||
const operationId = `enable_${keyIndex}`;
|
const operationId = `enable_${keyIndex}`;
|
||||||
setOperationLoading(prev => ({ ...prev, [operationId]: true }));
|
setOperationLoading((prev) => ({ ...prev, [operationId]: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/channel/multi_key/manage', {
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'enable_key',
|
action: 'enable_key',
|
||||||
key_index: keyIndex
|
key_index: keyIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -158,18 +165,18 @@ const MultiKeyManageModal = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('启用密钥失败'));
|
showError(t('启用密钥失败'));
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading(prev => ({ ...prev, [operationId]: false }));
|
setOperationLoading((prev) => ({ ...prev, [operationId]: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable all disabled keys
|
// Enable all disabled keys
|
||||||
const handleEnableAll = async () => {
|
const handleEnableAll = async () => {
|
||||||
setOperationLoading(prev => ({ ...prev, enable_all: true }));
|
setOperationLoading((prev) => ({ ...prev, enable_all: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/channel/multi_key/manage', {
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'enable_all_keys'
|
action: 'enable_all_keys',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -184,18 +191,18 @@ const MultiKeyManageModal = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('启用所有密钥失败'));
|
showError(t('启用所有密钥失败'));
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading(prev => ({ ...prev, enable_all: false }));
|
setOperationLoading((prev) => ({ ...prev, enable_all: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable all enabled keys
|
// Disable all enabled keys
|
||||||
const handleDisableAll = async () => {
|
const handleDisableAll = async () => {
|
||||||
setOperationLoading(prev => ({ ...prev, disable_all: true }));
|
setOperationLoading((prev) => ({ ...prev, disable_all: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/channel/multi_key/manage', {
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'disable_all_keys'
|
action: 'disable_all_keys',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -210,18 +217,18 @@ const MultiKeyManageModal = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('禁用所有密钥失败'));
|
showError(t('禁用所有密钥失败'));
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading(prev => ({ ...prev, disable_all: false }));
|
setOperationLoading((prev) => ({ ...prev, disable_all: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete all disabled keys
|
// Delete all disabled keys
|
||||||
const handleDeleteDisabledKeys = async () => {
|
const handleDeleteDisabledKeys = async () => {
|
||||||
setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
|
setOperationLoading((prev) => ({ ...prev, delete_disabled: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await API.post('/api/channel/multi_key/manage', {
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
action: 'delete_disabled_keys'
|
action: 'delete_disabled_keys',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
@@ -236,7 +243,7 @@ const MultiKeyManageModal = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('删除禁用密钥失败'));
|
showError(t('删除禁用密钥失败'));
|
||||||
} finally {
|
} finally {
|
||||||
setOperationLoading(prev => ({ ...prev, delete_disabled: false }));
|
setOperationLoading((prev) => ({ ...prev, delete_disabled: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,9 +290,12 @@ const MultiKeyManageModal = ({
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
// Percentages for progress display
|
// Percentages for progress display
|
||||||
const enabledPercent = total > 0 ? Math.round((enabledCount / total) * 100) : 0;
|
const enabledPercent =
|
||||||
const manualDisabledPercent = total > 0 ? Math.round((manualDisabledCount / total) * 100) : 0;
|
total > 0 ? Math.round((enabledCount / total) * 100) : 0;
|
||||||
const autoDisabledPercent = total > 0 ? Math.round((autoDisabledCount / 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) => {
|
const renderStatusTag = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return <Tag color='green' shape='circle' size='small'>{t('已启用')}</Tag>;
|
return (
|
||||||
|
<Tag color='green' shape='circle' size='small'>
|
||||||
|
{t('已启用')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return <Tag color='red' shape='circle' size='small'>{t('已禁用')}</Tag>;
|
return (
|
||||||
|
<Tag color='red' shape='circle' size='small'>
|
||||||
|
{t('已禁用')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return <Tag color='orange' shape='circle' size='small'>{t('自动禁用')}</Tag>;
|
return (
|
||||||
|
<Tag color='orange' shape='circle' size='small'>
|
||||||
|
{t('自动禁用')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
default:
|
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 (
|
return (
|
||||||
<Tooltip content={timestamp2string(time)}>
|
<Tooltip content={timestamp2string(time)}>
|
||||||
<Text style={{ fontSize: '12px' }}>
|
<Text style={{ fontSize: '12px' }}>{timestamp2string(time)}</Text>
|
||||||
{timestamp2string(time)}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -393,14 +417,18 @@ const MultiKeyManageModal = ({
|
|||||||
<Space>
|
<Space>
|
||||||
<Text>{t('多密钥管理')}</Text>
|
<Text>{t('多密钥管理')}</Text>
|
||||||
{channel?.name && (
|
{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'>
|
<Tag size='small' shape='circle' color='white'>
|
||||||
{t('总密钥数')}: {total}
|
{t('总密钥数')}: {total}
|
||||||
</Tag>
|
</Tag>
|
||||||
{channel?.channel_info?.multi_key_mode && (
|
{channel?.channel_info?.multi_key_mode && (
|
||||||
<Tag size='small' shape='circle' color='white'>
|
<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>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -410,60 +438,123 @@ const MultiKeyManageModal = ({
|
|||||||
width={900}
|
width={900}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col mb-5">
|
<div className='flex flex-col mb-5'>
|
||||||
{/* Stats & Mode */}
|
{/* Stats & Mode */}
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 mb-3"
|
className='rounded-xl p-4 mb-3'
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--semi-color-bg-1)',
|
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}>
|
<Col span={8}>
|
||||||
<div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
|
<div
|
||||||
<div className="flex items-center gap-2 mb-2">
|
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' />
|
<Badge dot type='success' />
|
||||||
<Text type='tertiary'>{t('已启用')}</Text>
|
<Text type='tertiary'>{t('已启用')}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-2 mb-2">
|
<div className='flex items-end gap-2 mb-2'>
|
||||||
<Text style={{ fontSize: 18, fontWeight: 700, color: '#22c55e' }}>{enabledCount}</Text>
|
<Text
|
||||||
<Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
|
style={{ fontSize: 18, fontWeight: 700, color: '#22c55e' }}
|
||||||
|
>
|
||||||
|
{enabledCount}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}
|
||||||
|
>
|
||||||
|
/ {total}
|
||||||
|
</Text>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
|
<div
|
||||||
<div className="flex items-center gap-2 mb-2">
|
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' />
|
<Badge dot type='danger' />
|
||||||
<Text type='tertiary'>{t('手动禁用')}</Text>
|
<Text type='tertiary'>{t('手动禁用')}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-2 mb-2">
|
<div className='flex items-end gap-2 mb-2'>
|
||||||
<Text style={{ fontSize: 18, fontWeight: 700, color: '#ef4444' }}>{manualDisabledCount}</Text>
|
<Text
|
||||||
<Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
|
style={{ fontSize: 18, fontWeight: 700, color: '#ef4444' }}
|
||||||
|
>
|
||||||
|
{manualDisabledCount}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}
|
||||||
|
>
|
||||||
|
/ {total}
|
||||||
|
</Text>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
|
<div
|
||||||
<div className="flex items-center gap-2 mb-2">
|
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' />
|
<Badge dot type='warning' />
|
||||||
<Text type='tertiary'>{t('自动禁用')}</Text>
|
<Text type='tertiary'>{t('自动禁用')}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-2 mb-2">
|
<div className='flex items-end gap-2 mb-2'>
|
||||||
<Text style={{ fontSize: 18, fontWeight: 700, color: '#f59e0b' }}>{autoDisabledCount}</Text>
|
<Text
|
||||||
<Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
|
style={{ fontSize: 18, fontWeight: 700, color: '#f59e0b' }}
|
||||||
|
>
|
||||||
|
{autoDisabledCount}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}
|
||||||
|
>
|
||||||
|
/ {total}
|
||||||
|
</Text>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
<div className='flex-1 flex flex-col min-h-0'>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Card className='!rounded-xl'>
|
<Card className='!rounded-xl'>
|
||||||
<Table
|
<Table
|
||||||
@@ -478,15 +569,26 @@ const MultiKeyManageModal = ({
|
|||||||
size='small'
|
size='small'
|
||||||
placeholder={t('全部状态')}
|
placeholder={t('全部状态')}
|
||||||
>
|
>
|
||||||
<Select.Option value={null}>{t('全部状态')}</Select.Option>
|
<Select.Option value={null}>
|
||||||
<Select.Option value={1}>{t('已启用')}</Select.Option>
|
{t('全部状态')}
|
||||||
<Select.Option value={2}>{t('手动禁用')}</Select.Option>
|
</Select.Option>
|
||||||
<Select.Option value={3}>{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>
|
</Select>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<Col
|
||||||
|
span={10}
|
||||||
|
style={{ display: 'flex', justifyContent: 'flex-end' }}
|
||||||
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
@@ -496,7 +598,7 @@ const MultiKeyManageModal = ({
|
|||||||
>
|
>
|
||||||
{t('刷新')}
|
{t('刷新')}
|
||||||
</Button>
|
</Button>
|
||||||
{(manualDisabledCount + autoDisabledCount) > 0 && (
|
{manualDisabledCount + autoDisabledCount > 0 && (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('确定要启用所有密钥吗?')}
|
title={t('确定要启用所有密钥吗?')}
|
||||||
onConfirm={handleEnableAll}
|
onConfirm={handleEnableAll}
|
||||||
@@ -529,7 +631,9 @@ const MultiKeyManageModal = ({
|
|||||||
)}
|
)}
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('确定要删除所有已自动禁用的密钥吗?')}
|
title={t('确定要删除所有已自动禁用的密钥吗?')}
|
||||||
content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
|
content={t(
|
||||||
|
'此操作不可撤销,将永久删除已自动禁用的密钥',
|
||||||
|
)}
|
||||||
onConfirm={handleDeleteDisabledKeys}
|
onConfirm={handleDeleteDisabledKeys}
|
||||||
okType={'danger'}
|
okType={'danger'}
|
||||||
position={'topRight'}
|
position={'topRight'}
|
||||||
@@ -562,7 +666,7 @@ const MultiKeyManageModal = ({
|
|||||||
onShowSizeChange: (current, size) => {
|
onShowSizeChange: (current, size) => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
handlePageSizeChange(size);
|
handlePageSizeChange(size);
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
size='small'
|
size='small'
|
||||||
bordered={false}
|
bordered={false}
|
||||||
@@ -570,8 +674,16 @@ const MultiKeyManageModal = ({
|
|||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
empty={
|
empty={
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationNoResult style={{ width: 140, height: 140 }} />}
|
image={
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 140, height: 140 }} />}
|
<IllustrationNoResult
|
||||||
|
style={{ width: 140, height: 140 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoResultDark
|
||||||
|
style={{ width: 140, height: 140 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
title={t('暂无密钥数据')}
|
title={t('暂无密钥数据')}
|
||||||
description={t('请检查渠道配置或刷新重试')}
|
description={t('请检查渠道配置或刷新重试')}
|
||||||
style={{ padding: 30 }}
|
style={{ padding: 30 }}
|
||||||
|
|||||||
@@ -36,20 +36,22 @@ const MjLogsActions = ({
|
|||||||
const showSkeleton = useMinimumLoadingTime(loading);
|
const showSkeleton = useMinimumLoadingTime(loading);
|
||||||
|
|
||||||
const placeholder = (
|
const placeholder = (
|
||||||
<div className="flex items-center mb-2 md:mb-0">
|
<div className='flex items-center mb-2 md:mb-0'>
|
||||||
<IconEyeOpened className="mr-2" />
|
<IconEyeOpened className='mr-2' />
|
||||||
<Skeleton.Title style={{ width: 300, height: 21, borderRadius: 6 }} />
|
<Skeleton.Title style={{ width: 300, height: 21, borderRadius: 6 }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
<div className='flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full'>
|
||||||
<Skeleton loading={showSkeleton} active placeholder={placeholder}>
|
<Skeleton loading={showSkeleton} active placeholder={placeholder}>
|
||||||
<div className="flex items-center mb-2 md:mb-0">
|
<div className='flex items-center mb-2 md:mb-0'>
|
||||||
<IconEyeOpened className="mr-2" />
|
<IconEyeOpened className='mr-2' />
|
||||||
<Text>
|
<Text>
|
||||||
{isAdminUser && showBanner
|
{isAdminUser && showBanner
|
||||||
? t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')
|
? t(
|
||||||
|
'当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。',
|
||||||
|
)
|
||||||
: t('Midjourney 任务记录')}
|
: t('Midjourney 任务记录')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Button, Progress, Tag, Typography } from '@douyinfe/semi-ui';
|
||||||
Button,
|
|
||||||
Progress,
|
|
||||||
Tag,
|
|
||||||
Typography
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
import {
|
||||||
Palette,
|
Palette,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
@@ -49,7 +44,7 @@ import {
|
|||||||
Loader,
|
Loader,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Hash,
|
Hash,
|
||||||
Video
|
Video,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
@@ -153,7 +148,11 @@ function renderType(type, t) {
|
|||||||
);
|
);
|
||||||
case 'INPAINT':
|
case 'INPAINT':
|
||||||
return (
|
return (
|
||||||
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
|
<Tag
|
||||||
|
color='violet'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<PaintBucket size={14} />}
|
||||||
|
>
|
||||||
{t('局部重绘-提交')}
|
{t('局部重绘-提交')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -177,7 +176,11 @@ function renderType(type, t) {
|
|||||||
);
|
);
|
||||||
case 'SWAP_FACE':
|
case 'SWAP_FACE':
|
||||||
return (
|
return (
|
||||||
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
|
<Tag
|
||||||
|
color='light-green'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<UserCheck size={14} />}
|
||||||
|
>
|
||||||
{t('换脸')}
|
{t('换脸')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -194,7 +197,11 @@ function renderCode(code, t) {
|
|||||||
switch (code) {
|
switch (code) {
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
<Tag
|
||||||
|
color='green'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<CheckCircle size={14} />}
|
||||||
|
>
|
||||||
{t('已提交')}
|
{t('已提交')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -229,7 +236,11 @@ function renderStatus(type, t) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'SUCCESS':
|
case 'SUCCESS':
|
||||||
return (
|
return (
|
||||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
<Tag
|
||||||
|
color='green'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<CheckCircle size={14} />}
|
||||||
|
>
|
||||||
{t('成功')}
|
{t('成功')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -259,7 +270,11 @@ function renderStatus(type, t) {
|
|||||||
);
|
);
|
||||||
case 'MODAL':
|
case 'MODAL':
|
||||||
return (
|
return (
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
<Tag
|
||||||
|
color='yellow'
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<AlertCircle size={14} />}
|
||||||
|
>
|
||||||
{t('窗口等待')}
|
{t('窗口等待')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@@ -415,7 +430,7 @@ export const getMjLogsColumns = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size='small'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openImageModal(text);
|
openImageModal(text);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -37,23 +37,23 @@ const MjLogsFilters = ({
|
|||||||
getFormApi={(api) => setFormApi(api)}
|
getFormApi={(api) => setFormApi(api)}
|
||||||
onSubmit={refresh}
|
onSubmit={refresh}
|
||||||
allowEmpty={true}
|
allowEmpty={true}
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
layout="vertical"
|
layout='vertical'
|
||||||
trigger="change"
|
trigger='change'
|
||||||
stopValidateWithError={false}
|
stopValidateWithError={false}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className='flex flex-col gap-2'>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2'>
|
||||||
{/* 时间选择器 */}
|
{/* 时间选择器 */}
|
||||||
<div className="col-span-1 lg:col-span-2">
|
<div className='col-span-1 lg:col-span-2'>
|
||||||
<Form.DatePicker
|
<Form.DatePicker
|
||||||
field='dateRange'
|
field='dateRange'
|
||||||
className="w-full"
|
className='w-full'
|
||||||
type='dateTimeRange'
|
type='dateTimeRange'
|
||||||
placeholder={[t('开始时间'), t('结束时间')]}
|
placeholder={[t('开始时间'), t('结束时间')]}
|
||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ const MjLogsFilters = ({
|
|||||||
placeholder={t('任务 ID')}
|
placeholder={t('任务 ID')}
|
||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 渠道 ID - 仅管理员可见 */}
|
{/* 渠道 ID - 仅管理员可见 */}
|
||||||
@@ -75,20 +75,20 @@ const MjLogsFilters = ({
|
|||||||
placeholder={t('渠道 ID')}
|
placeholder={t('渠道 ID')}
|
||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
size="small"
|
size='small'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
{/* 操作按钮区域 */}
|
||||||
<div className="flex justify-between items-center">
|
<div className='flex justify-between items-center'>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
htmlType='submit'
|
htmlType='submit'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="small"
|
size='small'
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -102,14 +102,14 @@ const MjLogsFilters = ({
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size="small"
|
size='small'
|
||||||
>
|
>
|
||||||
{t('重置')}
|
{t('重置')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
onClick={() => setShowColumnSelector(true)}
|
onClick={() => setShowColumnSelector(true)}
|
||||||
size="small"
|
size='small'
|
||||||
>
|
>
|
||||||
{t('列设置')}
|
{t('列设置')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -55,14 +55,7 @@ const MjLogsTable = (mjLogsData) => {
|
|||||||
openImageModal,
|
openImageModal,
|
||||||
isAdminUser,
|
isAdminUser,
|
||||||
});
|
});
|
||||||
}, [
|
}, [t, COLUMN_KEYS, copyText, openContentModal, openImageModal, isAdminUser]);
|
||||||
t,
|
|
||||||
COLUMN_KEYS,
|
|
||||||
copyText,
|
|
||||||
openContentModal,
|
|
||||||
openImageModal,
|
|
||||||
isAdminUser,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Filter columns based on visibility settings
|
// Filter columns based on visibility settings
|
||||||
const getVisibleColumns = () => {
|
const getVisibleColumns = () => {
|
||||||
@@ -86,13 +79,11 @@ const MjLogsTable = (mjLogsData) => {
|
|||||||
rowKey='key'
|
rowKey='key'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
className="rounded-xl overflow-hidden"
|
className='rounded-xl overflow-hidden'
|
||||||
size="middle"
|
size='middle'
|
||||||
empty={
|
empty={
|
||||||
<Empty
|
<Empty
|
||||||
image={
|
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||||
<IllustrationNoResult style={{ width: 150, height: 150 }} />
|
|
||||||
}
|
|
||||||
darkModeImage={
|
darkModeImage={
|
||||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const MjLogsPage = () => {
|
|||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<CardPro
|
<CardPro
|
||||||
type="type2"
|
type='type2'
|
||||||
statsArea={<MjLogsActions {...mjLogsData} />}
|
statsArea={<MjLogsActions {...mjLogsData} />}
|
||||||
searchArea={<MjLogsFilters {...mjLogsData} />}
|
searchArea={<MjLogsFilters {...mjLogsData} />}
|
||||||
paginationArea={createCardProPagination({
|
paginationArea={createCardProPagination({
|
||||||
|
|||||||
@@ -51,10 +51,8 @@ const ColumnSelectorModal = ({
|
|||||||
visible={showColumnSelector}
|
visible={showColumnSelector}
|
||||||
onCancel={() => setShowColumnSelector(false)}
|
onCancel={() => setShowColumnSelector(false)}
|
||||||
footer={
|
footer={
|
||||||
<div className="flex justify-end">
|
<div className='flex justify-end'>
|
||||||
<Button onClick={() => initDefaultColumns()}>
|
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setShowColumnSelector(false)}>
|
<Button onClick={() => setShowColumnSelector(false)}>
|
||||||
{t('取消')}
|
{t('取消')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -77,7 +75,7 @@ const ColumnSelectorModal = ({
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<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)' }}
|
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||||
>
|
>
|
||||||
{allColumns.map((column) => {
|
{allColumns.map((column) => {
|
||||||
@@ -91,7 +89,7 @@ const ColumnSelectorModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<Checkbox
|
||||||
checked={!!visibleColumns[column.key]}
|
checked={!!visibleColumns[column.key]}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user