Commit 0d57b1ac by t0ng7u

🎨 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
parent 41cf516e
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 = { ...@@ -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 }],
} },
] },
],
}; };
<!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>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<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 { ...@@ -22,4 +22,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };
...@@ -73,10 +73,7 @@ function App() { ...@@ -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={
......
...@@ -20,7 +20,13 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -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 ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 ...@@ -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 = () => { ...@@ -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) => {
......
...@@ -17,7 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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>
......
...@@ -59,12 +59,12 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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>
)} )}
......
...@@ -21,13 +21,9 @@ import React from 'react'; ...@@ -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, { ...@@ -24,7 +24,7 @@ import React, {
useCallback, useCallback,
useMemo, useMemo,
useImperativeHandle, useImperativeHandle,
forwardRef forwardRef,
} from 'react'; } from 'react';
/** /**
...@@ -34,7 +34,9 @@ import 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(({ ...@@ -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(({ ...@@ -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(({ ...@@ -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(({ ...@@ -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(({ ...@@ -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(({ ...@@ -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(({ ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'; ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'; ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'; ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 { ...@@ -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 { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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>
......
...@@ -41,7 +41,7 @@ const ActionButtons = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'; ...@@ -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 }) => { ...@@ -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 = ({ ...@@ -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'; ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'; ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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'; ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 }) => { ...@@ -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'; ...@@ -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, import { Nav, Divider, Button } from '@douyinfe/semi-ui';
showError
} from '../../helpers';
import {
Nav,
Divider,
Button,
} from '@douyinfe/semi-ui';
const routerMap = { const routerMap = {
home: '/', home: '/',
...@@ -54,7 +46,7 @@ const routerMap = { ...@@ -54,7 +46,7 @@ const routerMap = {
personal: '/console/personal', personal: '/console/personal',
}; };
const SiderBar = ({ onNavigate = () => { } }) => { const SiderBar = ({ onNavigate = () => {} }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [collapsed, toggleCollapsed] = useSidebarCollapsed(); const [collapsed, toggleCollapsed] = useSidebarCollapsed();
...@@ -275,14 +267,17 @@ const SiderBar = ({ onNavigate = () => { } }) => { ...@@ -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 = () => { } }) => { ...@@ -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 = () => { } }) => { ...@@ -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 = () => { } }) => { ...@@ -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 = () => { } }) => { ...@@ -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 = () => { } }) => { ...@@ -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 = () => { } }) => { ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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' }) => { ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'; ...@@ -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) => { ...@@ -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) => { ...@@ -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 { ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 { ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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}
......
...@@ -24,23 +24,30 @@ import SettingsPanel from './SettingsPanel'; ...@@ -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 ...@@ -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
); );
}); },
\ No newline at end of file );
...@@ -18,20 +18,8 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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}
/> />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment