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,
'Copyright (C) 2025 QuantumNous', 'block',
'', [
'This program is free software: you can redistribute it and/or modify', '',
'it under the terms of the GNU Affero General Public License as', 'Copyright (C) 2025 QuantumNous',
'published by the Free Software Foundation, either version 3 of the', '',
'License, or (at your option) any later version.', 'This program is free software: you can redistribute it and/or modify',
'', 'it under the terms of the GNU Affero General Public License as',
'This program is distributed in the hope that it will be useful,', 'published by the Free Software Foundation, either version 3 of the',
'but WITHOUT ANY WARRANTY; without even the implied warranty of', 'License, or (at your option) any later version.',
'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the', '',
'GNU Affero General Public License for more details.', 'This program is distributed in the hope that it will be useful,',
'', 'but WITHOUT ANY WARRANTY; without even the implied warranty of',
'You should have received a copy of the GNU Affero General Public License', 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the',
'along with this program. If not, see <https://www.gnu.org/licenses/>.', 'GNU Affero General Public License for more details.',
'', '',
'For commercial licensing, please contact support@quantumnous.com', 'You should have received a copy of the GNU Affero General Public License',
'' 'along with this program. If not, see <https://www.gnu.org/licenses/>.',
]], '',
'no-multiple-empty-lines': ['error', { max: 1 }] 'For commercial licensing, please contact support@quantumnous.com',
} '',
} ],
] ],
}; 'no-multiple-empty-lines': ['error', { max: 1 }],
\ No newline at end of file },
},
],
};
<!doctype html> <!doctype html>
<html lang="zh"> <html lang="zh">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" />
<meta
name="description"
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
/>
<title>New API</title>
</head>
<head> <body>
<meta charset="utf-8" /> <noscript>You need to enable JavaScript to run this app.</noscript>
<link rel="icon" href="/logo.png" /> <div id="root"></div>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <script type="module" src="/src/index.jsx"></script>
<meta name="theme-color" content="#ffffff" /> </body>
<meta name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> </html>
<title>New API</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
\ No newline at end of file
...@@ -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
display: 'flex', style={{
justifyContent: 'center', display: 'flex',
alignItems: 'center', justifyContent: 'center',
minHeight: '60vh' alignItems: 'center',
}}> 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>
...@@ -227,4 +241,4 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => { ...@@ -227,4 +241,4 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
); );
}; };
export default TwoFAVerification; export default TwoFAVerification;
\ No newline at end of file
...@@ -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);
} }
...@@ -441,4 +446,4 @@ pre:hover .copy-code-button { ...@@ -441,4 +446,4 @@ pre:hover .copy-code-button {
.animate-fade-in { .animate-fade-in {
animation: fade-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; animation: fade-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
will-change: opacity, transform; will-change: opacity, transform;
} }
\ No newline at end of file
...@@ -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>
...@@ -126,4 +143,4 @@ const TwoFactorAuthModal = ({ ...@@ -126,4 +143,4 @@ const TwoFactorAuthModal = ({
); );
}; };
export default TwoFactorAuthModal; export default TwoFactorAuthModal;
\ No newline at end of file
...@@ -27,15 +27,15 @@ const { Text } = Typography; ...@@ -27,15 +27,15 @@ const { Text } = Typography;
/** /**
* CardPro 高级卡片组件 * CardPro 高级卡片组件
* *
* 布局分为6个区域: * 布局分为6个区域:
* 1. 统计信息区域 (statsArea) * 1. 统计信息区域 (statsArea)
* 2. 描述信息区域 (descriptionArea) * 2. 描述信息区域 (descriptionArea)
* 3. 类型切换/标签区域 (tabsArea) * 3. 类型切换/标签区域 (tabsArea)
* 4. 操作按钮区域 (actionsArea) * 4. 操作按钮区域 (actionsArea)
* 5. 搜索表单区域 (searchArea) * 5. 搜索表单区域 (searchArea)
* 6. 分页区域 (paginationArea) - 固定在卡片底部 * 6. 分页区域 (paginationArea) - 固定在卡片底部
* *
* 支持三种布局类型: * 支持三种布局类型:
* - type1: 操作型 (如TokensTable) - 描述信息 + 操作按钮 + 搜索表单 * - type1: 操作型 (如TokensTable) - 描述信息 + 操作按钮 + 搜索表单
* - type2: 查询型 (如LogsTable) - 统计信息 + 搜索表单 * - type2: 查询型 (如LogsTable) - 统计信息 + 搜索表单
...@@ -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>
); );
...@@ -214,4 +197,4 @@ CardPro.propTypes = { ...@@ -214,4 +197,4 @@ CardPro.propTypes = {
t: PropTypes.func, t: PropTypes.func,
}; };
export default CardPro; export default CardPro;
\ No newline at end of file
...@@ -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';
...@@ -27,7 +35,7 @@ import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTi ...@@ -27,7 +35,7 @@ import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTi
/** /**
* CardTable 响应式表格组件 * CardTable 响应式表格组件
* *
* 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。 * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
* 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。 * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
*/ */
...@@ -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>
)} )}
...@@ -218,4 +239,4 @@ CardTable.propTypes = { ...@@ -218,4 +239,4 @@ CardTable.propTypes = {
hidePagination: PropTypes.bool, hidePagination: PropTypes.bool,
}; };
export default CardTable; export default CardTable;
\ No newline at end of file
...@@ -65,4 +65,4 @@ CompactModeToggle.propTypes = { ...@@ -65,4 +65,4 @@ CompactModeToggle.propTypes = {
className: PropTypes.string, className: PropTypes.string,
}; };
export default CompactModeToggle; export default CompactModeToggle;
\ No newline at end of file
...@@ -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>
); );
}; };
......
...@@ -57,4 +57,4 @@ export const renderDescription = (text, maxWidth = 200) => { ...@@ -57,4 +57,4 @@ export const renderDescription = (text, maxWidth = 200) => {
{text || '-'} {text || '-'}
</Text> </Text>
); );
}; };
\ No newline at end of file
...@@ -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={
<div item.extra ? (
className="text-xs text-gray-500" <div
dangerouslySetInnerHTML={{ __html: htmlExtra }} className='text-xs text-gray-500'
/> 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('请联系管理员在系统设置中配置公告信息')}
/> />
...@@ -107,4 +123,4 @@ const AnnouncementsPanel = ({ ...@@ -107,4 +123,4 @@ const AnnouncementsPanel = ({
); );
}; };
export default AnnouncementsPanel; export default AnnouncementsPanel;
\ No newline at end of file
...@@ -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信息')}
/> />
...@@ -114,4 +116,4 @@ const ApiInfoPanel = ({ ...@@ -114,4 +116,4 @@ const ApiInfoPanel = ({
); );
}; };
export default ApiInfoPanel; export default ApiInfoPanel;
\ No newline at end of file
...@@ -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,80 +38,80 @@ const ChartsPanel = ({ ...@@ -38,80 +38,80 @@ 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
<span> tab={
<IconHistogram /> <span>
{t('消耗分布')} <IconHistogram />
</span> {t('消耗分布')}
} itemKey="1" /> </span>
<TabPane tab={ }
<span> itemKey='1'
<IconPulse /> />
{t('消耗趋势')} <TabPane
</span> tab={
} itemKey="2" /> <span>
<TabPane tab={ <IconPulse />
<span> {t('消耗趋势')}
<IconPieChart2Stroked /> </span>
{t('调用次数分布')} }
</span> itemKey='2'
} itemKey="3" /> />
<TabPane tab={ <TabPane
<span> tab={
<IconHistogram /> <span>
{t('调用次数排行')} <IconPieChart2Stroked />
</span> {t('调用次数分布')}
} itemKey="4" /> </span>
}
itemKey='3'
/>
<TabPane
tab={
<span>
<IconHistogram />
{t('调用次数排行')}
</span>
}
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>
); );
}; };
export default ChartsPanel; export default ChartsPanel;
\ No newline at end of file
...@@ -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} />}
...@@ -58,4 +58,4 @@ const DashboardHeader = ({ ...@@ -58,4 +58,4 @@ const DashboardHeader = ({
); );
}; };
export default DashboardHeader; export default DashboardHeader;
\ No newline at end of file
...@@ -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('请联系管理员在系统设置中配置常见问答')}
/> />
...@@ -78,4 +85,4 @@ const FaqPanel = ({ ...@@ -78,4 +85,4 @@ const FaqPanel = ({
); );
}; };
export default FaqPanel; export default FaqPanel;
\ No newline at end of file
...@@ -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 ||
<VChart (item.trendData && item.trendData.length > 0)) && (
spec={getTrendSpec(item.trendData, item.trendColor)} <div className='w-24 h-10'>
option={CHART_CONFIG} <VChart
/> spec={getTrendSpec(item.trendData, item.trendColor)}
</div> option={CHART_CONFIG}
/>
</div>
)
)} )}
</div> </div>
))} ))}
...@@ -106,4 +113,4 @@ const StatsCards = ({ ...@@ -106,4 +113,4 @@ const StatsCards = ({
); );
}; };
export default StatsCards; export default StatsCards;
\ No newline at end of file
...@@ -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>
...@@ -132,4 +149,4 @@ const UptimePanel = ({ ...@@ -132,4 +149,4 @@ const UptimePanel = ({
); );
}; };
export default UptimePanel; export default UptimePanel;
\ No newline at end of file
...@@ -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(
const pubDate = item?.publishDate ? new Date(item.publishDate) : null; (item) => {
const absoluteTime = pubDate && !isNaN(pubDate.getTime()) const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
? `${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')}` const absoluteTime =
: (item?.publishDate || ''); pubDate && !isNaN(pubDate.getTime())
const relativeTime = getRelativeTime(item.publishDate); ? `${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')}`
return ({ : item?.publishDate || '';
...item, const relativeTime = getRelativeTime(item.publishDate);
time: absoluteTime, return {
relative: relativeTime ...item,
}); time: absoluteTime,
}); 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: Number(status), ([status, info]) => ({
color: info.color, status: Number(status),
label: dashboardData.t(info.label) color: info.color,
})); 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) => ({
label: dashboardData.t(item.label) ...item,
}))} 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) =>
monitors, renderMonitorList(
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP), monitors,
(status) => getUptimeStatusText(status, UPTIME_STATUS_MAP, dashboardData.t), (status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
dashboardData.t (status) =>
)} 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}
...@@ -252,4 +268,4 @@ const Dashboard = () => { ...@@ -252,4 +268,4 @@ const Dashboard = () => {
); );
}; };
export default Dashboard; export default Dashboard;
\ No newline at end of file
...@@ -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,20 +82,22 @@ const SearchModal = ({ ...@@ -82,20 +82,22 @@ 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 &&
field: 'username', createFormField(Form.Input, {
label: t('用户名称'), field: 'username',
value: username, label: t('用户名称'),
placeholder: t('可选值'), value: username,
name: 'username', placeholder: t('可选值'),
onChange: (value) => handleInputChange(value, 'username') name: 'username',
})} onChange: (value) => handleInputChange(value, 'username'),
})}
</Form> </Form>
</Modal> </Modal>
); );
}; };
export default SearchModal; export default SearchModal;
\ No newline at end of file
...@@ -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', {
icon: <Sun size={18} />, key: 'light',
buttonIcon: <Sun size={18} />, icon: <Sun size={18} />,
label: t('浅色模式'), buttonIcon: <Sun size={18} />,
description: t('始终使用浅色主题') label: t('浅色模式'),
}, description: t('始终使用浅色主题'),
{ },
key: 'dark', {
icon: <Moon size={18} />, key: 'dark',
buttonIcon: <Moon size={18} />, icon: <Moon size={18} />,
label: t('深色模式'), buttonIcon: <Moon size={18} />,
description: t('始终使用深色主题') label: t('深色模式'),
}, description: t('始终使用深色主题'),
{ },
key: 'auto', {
icon: <Monitor size={18} />, key: 'auto',
buttonIcon: <Monitor size={18} />, icon: <Monitor size={18} />,
label: t('自动模式'), buttonIcon: <Monitor size={18} />,
description: t('跟随系统主题设置') label: 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.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 && !isNaN(pubDate.getTime())
: (item?.publishDate || ''); ? `${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')}`
return ({ : item?.publishDate || '';
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={
<div item.extra ? (
className="text-xs text-gray-500" <div
dangerouslySetInnerHTML={{ __html: htmlExtra }} className='text-xs text-gray-500'
/> 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()}
...@@ -206,4 +252,4 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK ...@@ -206,4 +252,4 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
); );
}; };
export default NoticeModal; export default NoticeModal;
\ No newline at end of file
...@@ -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]);
...@@ -34,4 +37,4 @@ const SetupCheck = ({ children }) => { ...@@ -34,4 +37,4 @@ const SetupCheck = ({ children }) => {
return children; return children;
}; };
export default SetupCheck; export default SetupCheck;
\ No newline at end of file
...@@ -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>
...@@ -129,4 +126,4 @@ const ChatArea = ({ ...@@ -129,4 +126,4 @@ const ChatArea = ({
); );
}; };
export default ChatArea; export default ChatArea;
\ No newline at end of file
...@@ -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,9 +181,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => { ...@@ -174,9 +181,10 @@ 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 =
? JSON.stringify(content, null, 2) typeof content === 'object' && content !== null
: content; ? JSON.stringify(content, null, 2)
: content;
const success = await copy(textToCopy); const success = await copy(textToCopy);
setCopied(true); setCopied(true);
...@@ -205,11 +213,12 @@ const CodeViewer = ({ content, title, language = 'json' }) => { ...@@ -205,11 +213,12 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
}, [isExpanded, contentMetrics.isVeryLarge]); }, [isExpanded, contentMetrics.isVeryLarge]);
if (!content) { if (!content) {
const placeholderText = { const placeholderText =
preview: t('正在构造请求体预览...'), {
request: t('暂无请求数据'), preview: t('正在构造请求体预览...'),
response: t('暂无响应数据') request: t('暂无请求数据'),
}[title] || t('暂无数据'); response: t('暂无响应数据'),
}[title] || t('暂无数据');
return ( return (
<div style={codeThemeStyles.noContent}> <div style={codeThemeStyles.noContent}>
...@@ -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
display: 'flex', style={{
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
height: '200px', justifyContent: 'center',
color: '#888' height: '200px',
}}> color: '#888',
<div style={{ }}
width: '20px', >
height: '20px', <div
border: '2px solid #444', style={{
borderTop: '2px solid #888', width: '20px',
borderRadius: '50%', height: '20px',
animation: 'spin 1s linear infinite', border: '2px solid #444',
marginRight: '8px' borderTop: '2px solid #888',
}} /> borderRadius: '50%',
animation: 'spin 1s linear infinite',
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
...codeThemeStyles.actionButton, style={{
bottom: '12px', ...codeThemeStyles.actionButton,
left: '50%', bottom: '12px',
transform: 'translateX(-50%)', left: '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>
...@@ -329,4 +354,4 @@ const CodeViewer = ({ content, title, language = 'json' }) => { ...@@ -329,4 +354,4 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
); );
}; };
export default CodeViewer; export default CodeViewer;
\ No newline at end of file
...@@ -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' }}
/> />
...@@ -276,4 +278,4 @@ const ConfigManager = ({ ...@@ -276,4 +278,4 @@ const ConfigManager = ({
); );
}; };
export default ConfigManager; export default ConfigManager;
\ No newline at end of file
...@@ -21,23 +21,24 @@ import React from 'react'; ...@@ -21,23 +21,24 @@ 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
? React.cloneElement(clearContextNode, { ? React.cloneElement(clearContextNode, {
className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`, className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`,
style: { style: {
...clearContextNode.props.style, ...clearContextNode.props.style,
width: '32px', width: '32px',
height: '32px', height: '32px',
minWidth: '32px', minWidth: '32px',
padding: 0, padding: 0,
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>
...@@ -74,4 +73,4 @@ const CustomInputRender = (props) => { ...@@ -74,4 +73,4 @@ const CustomInputRender = (props) => {
); );
}; };
export default CustomInputRender; export default CustomInputRender;
\ No newline at end of file
...@@ -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>
...@@ -206,4 +210,4 @@ const CustomRequestEditor = ({ ...@@ -206,4 +210,4 @@ const CustomRequestEditor = ({
); );
}; };
export default CustomRequestEditor; export default CustomRequestEditor;
\ No newline at end of file
...@@ -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={
<Eye size={16} /> <div className='flex items-center gap-2'>
{t('预览请求体')} <Eye size={16} />
{customRequestMode && ( {t('预览请求体')}
<span className="px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full"> {customRequestMode && (
自定义 <span className='px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full'>
</span> 自定义
)} </span>
</div> )}
} itemKey="preview"> </div>
}
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={
<Send size={16} /> <div className='flex items-center gap-2'>
{t('实际请求体')} <Send size={16} />
</div> {t('实际请求体')}
} itemKey="request"> </div>
}
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={
<Zap size={16} /> <div className='flex items-center gap-2'>
{t('响应')} <Zap size={16} />
</div> {t('响应')}
} itemKey="response"> </div>
}
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
...@@ -209,4 +211,4 @@ const DebugPanel = ({ ...@@ -209,4 +211,4 @@ const DebugPanel = ({
); );
}; };
export default DebugPanel; export default DebugPanel;
\ No newline at end of file
...@@ -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,11 +76,11 @@ const FloatingButtons = ({ ...@@ -80,11 +76,11 @@ 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'
/> />
)} )}
</> </>
); );
}; };
export default FloatingButtons; export default FloatingButtons;
\ No newline at end of file
...@@ -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>
...@@ -129,4 +137,4 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab ...@@ -129,4 +137,4 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
); );
}; };
export default ImageUrlInput; export default ImageUrlInput;
\ No newline at end of file
...@@ -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}
...@@ -137,4 +149,4 @@ const MessageActions = ({ ...@@ -137,4 +149,4 @@ const MessageActions = ({
); );
}; };
export default MessageActions; export default MessageActions;
\ No newline at end of file
...@@ -24,56 +24,74 @@ import SettingsPanel from './SettingsPanel'; ...@@ -24,56 +24,74 @@ 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,
return ( (prevProps, nextProps) => {
prevProps.message.id === nextProps.message.id && // 只有这些属性变化时才重新渲染
prevProps.message.content === nextProps.message.content && return (
prevProps.message.status === nextProps.message.status && prevProps.message.id === nextProps.message.id &&
prevProps.message.role === nextProps.message.role && prevProps.message.content === nextProps.message.content &&
prevProps.message.reasoningContent === nextProps.message.reasoningContent && prevProps.message.status === nextProps.message.status &&
prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded && prevProps.message.role === nextProps.message.role &&
prevProps.isEditing === nextProps.isEditing && prevProps.message.reasoningContent ===
prevProps.editValue === nextProps.editValue && nextProps.message.reasoningContent &&
prevProps.styleState.isMobile === nextProps.styleState.isMobile prevProps.message.isReasoningExpanded ===
); nextProps.message.isReasoningExpanded &&
}); prevProps.isEditing === nextProps.isEditing &&
prevProps.editValue === nextProps.editValue &&
prevProps.styleState.isMobile === nextProps.styleState.isMobile
);
},
);
// 优化的消息操作组件 // 优化的消息操作组件
export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, nextProps) => { export const OptimizedMessageActions = React.memo(
return ( MessageActions,
prevProps.message.id === nextProps.message.id && (prevProps, nextProps) => {
prevProps.message.role === nextProps.message.role && return (
prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating && prevProps.message.id === nextProps.message.id &&
prevProps.isEditing === nextProps.isEditing && prevProps.message.role === nextProps.message.role &&
prevProps.onMessageReset === nextProps.onMessageReset prevProps.isAnyMessageGenerating === nextProps.isAnyMessageGenerating &&
); prevProps.isEditing === nextProps.isEditing &&
}); prevProps.onMessageReset === nextProps.onMessageReset
);
},
);
// 优化的设置面板组件 // 优化的设置面板组件
export const OptimizedSettingsPanel = React.memo(SettingsPanel, (prevProps, nextProps) => { export const OptimizedSettingsPanel = React.memo(
return ( SettingsPanel,
JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) && (prevProps, nextProps) => {
JSON.stringify(prevProps.parameterEnabled) === JSON.stringify(nextProps.parameterEnabled) && return (
JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) && JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) && JSON.stringify(prevProps.parameterEnabled) ===
prevProps.customRequestMode === nextProps.customRequestMode && JSON.stringify(nextProps.parameterEnabled) &&
prevProps.customRequestBody === nextProps.customRequestBody && JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
prevProps.showDebugPanel === nextProps.showDebugPanel && JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
prevProps.showSettings === nextProps.showSettings && prevProps.customRequestMode === nextProps.customRequestMode &&
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) && prevProps.customRequestBody === nextProps.customRequestBody &&
JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages) prevProps.showDebugPanel === nextProps.showDebugPanel &&
); prevProps.showSettings === nextProps.showSettings &&
}); JSON.stringify(prevProps.previewPayload) ===
JSON.stringify(nextProps.previewPayload) &&
JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages)
);
},
);
// 优化的调试面板组件 // 优化的调试面板组件
export const OptimizedDebugPanel = React.memo(DebugPanel, (prevProps, nextProps) => { export const OptimizedDebugPanel = React.memo(
return ( DebugPanel,
prevProps.show === nextProps.show && (prevProps, nextProps) => {
prevProps.activeTab === nextProps.activeTab && return (
JSON.stringify(prevProps.debugData) === JSON.stringify(nextProps.debugData) && prevProps.show === nextProps.show &&
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) && prevProps.activeTab === nextProps.activeTab &&
prevProps.customRequestMode === nextProps.customRequestMode && JSON.stringify(prevProps.debugData) ===
prevProps.showDebugPanel === nextProps.showDebugPanel JSON.stringify(nextProps.debugData) &&
); JSON.stringify(prevProps.previewPayload) ===
}); JSON.stringify(nextProps.previewPayload) &&
\ No newline at end of file prevProps.customRequestMode === nextProps.customRequestMode &&
prevProps.showDebugPanel === nextProps.showDebugPanel
);
},
);
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