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 = {
root: true,
env: { browser: true, es2021: true, node: true },
parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } },
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
plugins: ['header', 'react-hooks'],
overrides: [
{
files: ['**/*.{js,jsx}'],
rules: {
'header/header': [2, 'block', [
'header/header': [
2,
'block',
[
'',
'Copyright (C) 2025 QuantumNous',
'',
......@@ -25,10 +32,11 @@ module.exports = {
'along with this program. If not, see <https://www.gnu.org/licenses/>.',
'',
'For commercial licensing, please contact support@quantumnous.com',
''
]],
'no-multiple-empty-lines': ['error', { max: 1 }]
}
}
]
'',
],
],
'no-multiple-empty-lines': ['error', { max: 1 }],
},
},
],
};
<!doctype html>
<html lang="zh">
<head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" />
<meta name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" />
<meta
name="description"
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
/>
<title>New API</title>
</head>
</head>
<body>
<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>
</body>
</html>
......@@ -22,4 +22,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};
......@@ -73,10 +73,7 @@ function App() {
</Suspense>
}
/>
<Route
path='/forbidden'
element={<Forbidden />}
/>
<Route path='/forbidden' element={<Forbidden />} />
<Route
path='/console/models'
element={
......
......@@ -20,7 +20,13 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useContext, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
import {
API,
showError,
showSuccess,
updateAPI,
setUserData,
} from '../../helpers';
import { UserContext } from '../../context/User';
import Loading from '../common/ui/Loading';
......
......@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
import {
API,
copy,
showError,
showNotice,
getLogo,
getSystemName,
} from '../../helpers';
import { useSearchParams, Link } from 'react-router-dom';
import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
......@@ -55,7 +62,7 @@ const PasswordResetConfirm = () => {
if (formApi) {
formApi.setValues({
email: email || '',
newPassword: newPassword || ''
newPassword: newPassword || '',
});
}
}, [searchParams, newPassword, formApi]);
......@@ -97,40 +104,53 @@ const PasswordResetConfirm = () => {
}
return (
<div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
<div className="w-full max-w-sm mt-[60px]">
<div className="flex flex-col items-center">
<div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
<div
className='blur-ball blur-ball-indigo'
style={{ top: '-80px', right: '-80px', transform: 'none' }}
/>
<div
className='blur-ball blur-ball-teal'
style={{ top: '50%', left: '-120px' }}
/>
<div className='w-full max-w-sm mt-[60px]'>
<div className='flex flex-col items-center'>
<div className='w-full max-w-md'>
<div className='flex items-center justify-center mb-6 gap-2'>
<img src={logo} alt='Logo' className='h-10 rounded-full' />
<Title heading={3} className='!text-gray-800'>
{systemName}
</Title>
</div>
<Card className="border-0 !rounded-2xl overflow-hidden">
<div className="flex justify-center pt-6 pb-2">
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置确认')}</Title>
<Card className='border-0 !rounded-2xl overflow-hidden'>
<div className='flex justify-center pt-6 pb-2'>
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
{t('密码重置确认')}
</Title>
</div>
<div className="px-2 py-8">
<div className='px-2 py-8'>
{!isValidResetLink && (
<Banner
type="danger"
type='danger'
description={t('无效的重置链接,请重新发起密码重置请求')}
className="mb-4 !rounded-lg"
className='mb-4 !rounded-lg'
closeIcon={null}
/>
)}
<Form
getFormApi={(api) => setFormApi(api)}
initValues={{ email: email || '', newPassword: newPassword || '' }}
className="space-y-4"
initValues={{
email: email || '',
newPassword: newPassword || '',
}}
className='space-y-4'
>
<Form.Input
field="email"
field='email'
label={t('邮箱')}
name="email"
name='email'
disabled={true}
prefix={<IconMail />}
placeholder={email ? '' : t('等待获取邮箱信息...')}
......@@ -138,19 +158,21 @@ const PasswordResetConfirm = () => {
{newPassword && (
<Form.Input
field="newPassword"
field='newPassword'
label={t('新密码')}
name="newPassword"
name='newPassword'
disabled={true}
prefix={<IconLock />}
suffix={
<Button
icon={<IconCopy />}
type="tertiary"
theme="borderless"
type='tertiary'
theme='borderless'
onClick={async () => {
await copy(newPassword);
showNotice(`${t('密码已复制到剪贴板:')} ${newPassword}`);
showNotice(
`${t('密码已复制到剪贴板:')} ${newPassword}`,
);
}}
>
{t('复制')}
......@@ -159,23 +181,32 @@ const PasswordResetConfirm = () => {
/>
)}
<div className="space-y-2 pt-2">
<div className='space-y-2 pt-2'>
<Button
theme="solid"
className="w-full !rounded-full"
type="primary"
htmlType="submit"
theme='solid'
className='w-full !rounded-full'
type='primary'
htmlType='submit'
onClick={handleSubmit}
loading={loading}
disabled={disableButton || newPassword || !isValidResetLink}
disabled={
disableButton || newPassword || !isValidResetLink
}
>
{newPassword ? t('密码重置完成') : t('确认重置密码')}
</Button>
</div>
</Form>
<div className="mt-6 text-center text-sm">
<Text><Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('返回登录')}</Link></Text>
<div className='mt-6 text-center text-sm'>
<Text>
<Link
to='/login'
className='text-blue-600 hover:text-blue-800 font-medium'
>
{t('返回登录')}
</Link>
</Text>
</div>
</div>
</Card>
......
......@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../../helpers';
import {
API,
getLogo,
showError,
showInfo,
showSuccess,
getSystemName,
} from '../../helpers';
import Turnstile from 'react-turnstile';
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
import { IconMail } from '@douyinfe/semi-icons';
......@@ -97,57 +104,77 @@ const PasswordResetForm = () => {
}
return (
<div className="relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
<div className="w-full max-w-sm mt-[60px]">
<div className="flex flex-col items-center">
<div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
<div
className='blur-ball blur-ball-indigo'
style={{ top: '-80px', right: '-80px', transform: 'none' }}
/>
<div
className='blur-ball blur-ball-teal'
style={{ top: '50%', left: '-120px' }}
/>
<div className='w-full max-w-sm mt-[60px]'>
<div className='flex flex-col items-center'>
<div className='w-full max-w-md'>
<div className='flex items-center justify-center mb-6 gap-2'>
<img src={logo} alt='Logo' className='h-10 rounded-full' />
<Title heading={3} className='!text-gray-800'>
{systemName}
</Title>
</div>
<Card className="border-0 !rounded-2xl overflow-hidden">
<div className="flex justify-center pt-6 pb-2">
<Title heading={3} className="text-gray-800 dark:text-gray-200">{t('密码重置')}</Title>
<Card className='border-0 !rounded-2xl overflow-hidden'>
<div className='flex justify-center pt-6 pb-2'>
<Title heading={3} className='text-gray-800 dark:text-gray-200'>
{t('密码重置')}
</Title>
</div>
<div className="px-2 py-8">
<Form className="space-y-3">
<div className='px-2 py-8'>
<Form className='space-y-3'>
<Form.Input
field="email"
field='email'
label={t('邮箱')}
placeholder={t('请输入您的邮箱地址')}
name="email"
name='email'
value={email}
onChange={handleChange}
prefix={<IconMail />}
/>
<div className="space-y-2 pt-2">
<div className='space-y-2 pt-2'>
<Button
theme="solid"
className="w-full !rounded-full"
type="primary"
htmlType="submit"
theme='solid'
className='w-full !rounded-full'
type='primary'
htmlType='submit'
onClick={handleSubmit}
loading={loading}
disabled={disableButton}
>
{disableButton ? `${t('重试')} (${countdown})` : t('提交')}
{disableButton
? `${t('重试')} (${countdown})`
: t('提交')}
</Button>
</div>
</Form>
<div className="mt-6 text-center text-sm">
<Text>{t('想起来了?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
<div className='mt-6 text-center text-sm'>
<Text>
{t('想起来了?')}{' '}
<Link
to='/login'
className='text-blue-600 hover:text-blue-800 font-medium'
>
{t('登录')}
</Link>
</Text>
</div>
</div>
</Card>
{turnstileEnabled && (
<div className="flex justify-center mt-6">
<div className='flex justify-center mt-6'>
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
......
......@@ -17,7 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { API, showError, showSuccess } from '../../helpers';
import { Button, Card, Divider, Form, Input, Typography } from '@douyinfe/semi-ui';
import {
Button,
Card,
Divider,
Form,
Input,
Typography,
} from '@douyinfe/semi-ui';
import React, { useState } from 'react';
const { Title, Text, Paragraph } = Typography;
......@@ -44,7 +51,7 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
setLoading(true);
try {
const res = await API.post('/api/user/login/2fa', {
code: verificationCode
code: verificationCode,
});
if (res.data.success) {
......@@ -72,30 +79,30 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
if (isModal) {
return (
<div className="space-y-4">
<Paragraph className="text-gray-600 dark:text-gray-300">
<div className='space-y-4'>
<Paragraph className='text-gray-600 dark:text-gray-300'>
请输入认证器应用显示的验证码完成登录
</Paragraph>
<Form onSubmit={handleSubmit}>
<Form.Input
field="code"
label={useBackupCode ? "备用码" : "验证码"}
placeholder={useBackupCode ? "请输入8位备用码" : "请输入6位验证码"}
field='code'
label={useBackupCode ? '备用码' : '验证码'}
placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
value={verificationCode}
onChange={setVerificationCode}
onKeyPress={handleKeyPress}
size="large"
size='large'
style={{ marginBottom: 16 }}
autoFocus
/>
<Button
htmlType="submit"
type="primary"
htmlType='submit'
type='primary'
loading={loading}
block
size="large"
size='large'
style={{ marginBottom: 16 }}
>
验证并登录
......@@ -106,8 +113,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
<div style={{ textAlign: 'center' }}>
<Button
theme="borderless"
type="tertiary"
theme='borderless'
type='tertiary'
onClick={() => {
setUseBackupCode(!useBackupCode);
setVerificationCode('');
......@@ -119,8 +126,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
{onBack && (
<Button
theme="borderless"
type="tertiary"
theme='borderless'
type='tertiary'
onClick={onBack}
style={{ color: '#1890ff', padding: 0 }}
>
......@@ -129,15 +136,14 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
)}
</div>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<Text size="small" type="secondary">
<div className='bg-gray-50 dark:bg-gray-800 rounded-lg p-3'>
<Text size='small' type='secondary'>
<strong>提示:</strong>
<br />
• 验证码每30秒更新一次
<br />
• 如果无法获取验证码,请使用备用码
<br />
• 每个备用码只能使用一次
<br />• 每个备用码只能使用一次
</Text>
</div>
</div>
......@@ -145,39 +151,41 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
}
return (
<div style={{
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '60vh'
}}>
minHeight: '60vh',
}}
>
<Card style={{ width: 400, padding: 24 }}>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<Title heading={3}>两步验证</Title>
<Paragraph type="secondary">
<Paragraph type='secondary'>
请输入认证器应用显示的验证码完成登录
</Paragraph>
</div>
<Form onSubmit={handleSubmit}>
<Form.Input
field="code"
label={useBackupCode ? "备用码" : "验证码"}
placeholder={useBackupCode ? "请输入8位备用码" : "请输入6位验证码"}
field='code'
label={useBackupCode ? '备用码' : '验证码'}
placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
value={verificationCode}
onChange={setVerificationCode}
onKeyPress={handleKeyPress}
size="large"
size='large'
style={{ marginBottom: 16 }}
autoFocus
/>
<Button
htmlType="submit"
type="primary"
htmlType='submit'
type='primary'
loading={loading}
block
size="large"
size='large'
style={{ marginBottom: 16 }}
>
验证并登录
......@@ -188,8 +196,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
<div style={{ textAlign: 'center' }}>
<Button
theme="borderless"
type="tertiary"
theme='borderless'
type='tertiary'
onClick={() => {
setUseBackupCode(!useBackupCode);
setVerificationCode('');
......@@ -201,8 +209,8 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
{onBack && (
<Button
theme="borderless"
type="tertiary"
theme='borderless'
type='tertiary'
onClick={onBack}
style={{ color: '#1890ff', padding: 0 }}
>
......@@ -211,15 +219,21 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
)}
</div>
<div style={{ marginTop: 24, padding: 16, background: '#f6f8fa', borderRadius: 6 }}>
<Text size="small" type="secondary">
<div
style={{
marginTop: 24,
padding: 16,
background: '#f6f8fa',
borderRadius: 6,
}}
>
<Text size='small' type='secondary'>
<strong>提示:</strong>
<br />
• 验证码每30秒更新一次
<br />
• 如果无法获取验证码,请使用备用码
<br />
• 每个备用码只能使用一次
<br />• 每个备用码只能使用一次
</Text>
</div>
</Card>
......
......@@ -59,12 +59,12 @@
}
.user-message a {
color: #87CEEB !important;
color: #87ceeb !important;
/* 浅蓝色链接 */
}
.user-message a:hover {
color: #B0E0E6 !important;
color: #b0e0e6 !important;
/* hover时更浅的蓝色 */
}
......@@ -298,7 +298,12 @@ pre:hover .copy-code-button {
.markdown-body hr {
border: none;
height: 1px;
background: linear-gradient(to right, transparent, var(--semi-color-border), transparent);
background: linear-gradient(
to right,
transparent,
var(--semi-color-border),
transparent
);
margin: 24px 0;
}
......@@ -332,7 +337,7 @@ pre:hover .copy-code-button {
}
/* 任务列表样式 */
.markdown-body input[type="checkbox"] {
.markdown-body input[type='checkbox'] {
margin-right: 8px;
transform: scale(1.1);
}
......
......@@ -43,7 +43,7 @@ const TwoFactorAuthModal = ({
onCancel,
title,
description,
placeholder
placeholder,
}) => {
const { t } = useTranslation();
......@@ -56,10 +56,18 @@ const TwoFactorAuthModal = ({
return (
<Modal
title={
<div className="flex items-center">
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3">
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
<div className='flex items-center'>
<div className='w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3'>
<svg
className='w-4 h-4 text-blue-600 dark:text-blue-400'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fillRule='evenodd'
d='M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
clipRule='evenodd'
/>
</svg>
</div>
{title || t('安全验证')}
......@@ -69,11 +77,9 @@ const TwoFactorAuthModal = ({
onCancel={onCancel}
footer={
<>
<Button onClick={onCancel}>
{t('取消')}
</Button>
<Button onClick={onCancel}>{t('取消')}</Button>
<Button
type="primary"
type='primary'
loading={loading}
disabled={!code || loading}
onClick={onVerify}
......@@ -85,18 +91,29 @@ const TwoFactorAuthModal = ({
width={500}
style={{ maxWidth: '90vw' }}
>
<div className="space-y-6">
<div className='space-y-6'>
{/* 安全提示 */}
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-4">
<div className="flex items-start">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
<div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-4'>
<div className='flex items-start'>
<svg
className='w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fillRule='evenodd'
d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
clipRule='evenodd'
/>
</svg>
<div>
<Typography.Text strong className="text-blue-800 dark:text-blue-200">
<Typography.Text
strong
className='text-blue-800 dark:text-blue-200'
>
{t('安全验证')}
</Typography.Text>
<Typography.Text className="block text-blue-700 dark:text-blue-300 text-sm mt-1">
<Typography.Text className='block text-blue-700 dark:text-blue-300 text-sm mt-1'>
{description || t('为了保护账户安全,请验证您的两步验证码。')}
</Typography.Text>
</div>
......@@ -105,19 +122,19 @@ const TwoFactorAuthModal = ({
{/* 验证码输入 */}
<div>
<Typography.Text strong className="block mb-2">
<Typography.Text strong className='block mb-2'>
{t('验证身份')}
</Typography.Text>
<Input
placeholder={placeholder || t('请输入认证器验证码或备用码')}
value={code}
onChange={onCodeChange}
size="large"
size='large'
maxLength={8}
onKeyDown={handleKeyDown}
autoFocus
/>
<Typography.Text type="tertiary" size="small" className="mt-2 block">
<Typography.Text type='tertiary' size='small' className='mt-2 block'>
{t('支持6位TOTP验证码或8位备用码')}
</Typography.Text>
</div>
......
......@@ -71,47 +71,38 @@ const CardPro = ({
const hasMobileHideableContent = actionsArea || searchArea;
const renderHeader = () => {
const hasContent = statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
const hasContent =
statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
if (!hasContent) return null;
return (
<div className="flex flex-col w-full">
<div className='flex flex-col w-full'>
{/* 统计信息区域 - 用于type2 */}
{type === 'type2' && statsArea && (
<>
{statsArea}
</>
)}
{type === 'type2' && statsArea && <>{statsArea}</>}
{/* 描述信息区域 - 用于type1和type3 */}
{(type === 'type1' || type === 'type3') && descriptionArea && (
<>
{descriptionArea}
</>
<>{descriptionArea}</>
)}
{/* 第一个分隔线 - 在描述信息或统计信息后面 */}
{((type === 'type1' || type === 'type3') && descriptionArea) ||
(type === 'type2' && statsArea) ? (
<Divider margin="12px" />
<Divider margin='12px' />
) : null}
{/* 类型切换/标签区域 - 主要用于type3 */}
{type === 'type3' && tabsArea && (
<>
{tabsArea}
</>
)}
{type === 'type3' && tabsArea && <>{tabsArea}</>}
{/* 移动端操作切换按钮 */}
{isMobile && hasMobileHideableContent && (
<>
<div className="w-full mb-2">
<div className='w-full mb-2'>
<Button
onClick={toggleMobileActions}
icon={showMobileActions ? <IconEyeClosed /> : <IconEyeOpened />}
type="tertiary"
size="small"
type='tertiary'
size='small'
theme='outline'
block
>
......@@ -126,32 +117,24 @@ const CardPro = ({
className={`flex flex-col gap-2 ${isMobile && !showMobileActions ? 'hidden' : ''}`}
>
{/* 操作按钮区域 - 用于type1和type3 */}
{(type === 'type1' || type === 'type3') && actionsArea && (
Array.isArray(actionsArea) ? (
{(type === 'type1' || type === 'type3') &&
actionsArea &&
(Array.isArray(actionsArea) ? (
actionsArea.map((area, idx) => (
<React.Fragment key={idx}>
{idx !== 0 && <Divider />}
<div className="w-full">
{area}
</div>
<div className='w-full'>{area}</div>
</React.Fragment>
))
) : (
<div className="w-full">
{actionsArea}
</div>
)
)}
<div className='w-full'>{actionsArea}</div>
))}
{/* 当同时存在操作区和搜索区时,插入分隔线 */}
{(actionsArea && searchArea) && <Divider />}
{actionsArea && searchArea && <Divider />}
{/* 搜索表单区域 - 所有类型都可能有 */}
{searchArea && (
<div className="w-full">
{searchArea}
</div>
)}
{searchArea && <div className='w-full'>{searchArea}</div>}
</div>
</div>
);
......
......@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Table, Card, Skeleton, Pagination, Empty, Button, Collapsible } from '@douyinfe/semi-ui';
import {
Table,
Card,
Skeleton,
Pagination,
Empty,
Button,
Collapsible,
} from '@douyinfe/semi-ui';
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
import PropTypes from 'prop-types';
import { useIsMobile } from '../../../hooks/common/useIsMobile';
......@@ -75,18 +83,22 @@ const CardTable = ({
const renderSkeletonCard = (key) => {
const placeholder = (
<div className="p-2">
<div className='p-2'>
{visibleCols.map((col, idx) => {
if (!col.title) {
return (
<div key={idx} className="mt-2 flex justify-end">
<div key={idx} className='mt-2 flex justify-end'>
<Skeleton.Title active style={{ width: 100, height: 24 }} />
</div>
);
}
return (
<div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed" style={{ borderColor: 'var(--semi-color-border)' }}>
<div
key={idx}
className='flex justify-between items-center py-1 border-b last:border-b-0 border-dashed'
style={{ borderColor: 'var(--semi-color-border)' }}
>
<Skeleton.Title active style={{ width: 80, height: 14 }} />
<Skeleton.Title
active
......@@ -103,14 +115,14 @@ const CardTable = ({
);
return (
<Card key={key} className="!rounded-2xl shadow-sm">
<Card key={key} className='!rounded-2xl shadow-sm'>
<Skeleton loading={true} active placeholder={placeholder}></Skeleton>
</Card>
);
};
return (
<div className="flex flex-col gap-2">
<div className='flex flex-col gap-2'>
{[1, 2, 3].map((i) => renderSkeletonCard(i))}
</div>
);
......@@ -127,9 +139,12 @@ const CardTable = ({
(!tableProps.rowExpandable || tableProps.rowExpandable(record));
return (
<Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
<Card key={rowKeyVal} className='!rounded-2xl shadow-sm'>
{columns.map((col, colIdx) => {
if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
if (
tableProps?.visibleColumns &&
!tableProps.visibleColumns[col.key]
) {
return null;
}
......@@ -140,7 +155,7 @@ const CardTable = ({
if (!title) {
return (
<div key={col.key || colIdx} className="mt-2 flex justify-end">
<div key={col.key || colIdx} className='mt-2 flex justify-end'>
{cellContent}
</div>
);
......@@ -149,14 +164,16 @@ const CardTable = ({
return (
<div
key={col.key || colIdx}
className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed"
className='flex justify-between items-start py-1 border-b last:border-b-0 border-dashed'
style={{ borderColor: 'var(--semi-color-border)' }}
>
<span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
<span className='font-medium text-gray-600 mr-2 whitespace-nowrap select-none'>
{title}
</span>
<div className="flex-1 break-all flex justify-end items-center gap-1">
{cellContent !== undefined && cellContent !== null ? cellContent : '-'}
<div className='flex-1 break-all flex justify-end items-center gap-1'>
{cellContent !== undefined && cellContent !== null
? cellContent
: '-'}
</div>
</div>
);
......@@ -177,7 +194,7 @@ const CardTable = ({
{showDetails ? t('收起') : t('详情')}
</Button>
<Collapsible isOpen={showDetails} keepDOM>
<div className="pt-2">
<div className='pt-2'>
{tableProps.expandedRowRender(record, index)}
</div>
</Collapsible>
......@@ -190,19 +207,23 @@ const CardTable = ({
if (isEmpty) {
if (tableProps.empty) return tableProps.empty;
return (
<div className="flex justify-center p-4">
<Empty description="No Data" />
<div className='flex justify-center p-4'>
<Empty description='No Data' />
</div>
);
}
return (
<div className="flex flex-col gap-2">
<div className='flex flex-col gap-2'>
{dataSource.map((record, index) => (
<MobileRowCard key={getRowKey(record, index)} record={record} index={index} />
<MobileRowCard
key={getRowKey(record, index)}
record={record}
index={index}
/>
))}
{!hidePagination && tableProps.pagination && dataSource.length > 0 && (
<div className="mt-2 flex justify-center">
<div className='mt-2 flex justify-center'>
<Pagination {...tableProps.pagination} />
</div>
)}
......
......@@ -21,13 +21,9 @@ import React from 'react';
import { Spin } from '@douyinfe/semi-ui';
const Loading = ({ size = 'small' }) => {
return (
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
<Spin
size={size}
spinning={true}
/>
<div className='fixed inset-0 w-screen h-screen flex items-center justify-center'>
<Spin size={size} spinning={true} />
</div>
);
};
......
......@@ -24,7 +24,7 @@ import React, {
useCallback,
useMemo,
useImperativeHandle,
forwardRef
forwardRef,
} from 'react';
/**
......@@ -34,7 +34,9 @@ import React, {
* 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器
*
*/
const ScrollableContainer = forwardRef(({
const ScrollableContainer = forwardRef(
(
{
children,
maxHeight = '24rem',
className = '',
......@@ -46,7 +48,9 @@ const ScrollableContainer = forwardRef(({
onScroll,
onScrollStateChange,
...props
}, ref) => {
},
ref,
) => {
const scrollRef = useRef(null);
const containerRef = useRef(null);
const debounceTimerRef = useRef(null);
......@@ -78,7 +82,9 @@ const ScrollableContainer = forwardRef(({
const element = scrollRef.current;
const isScrollable = element.scrollHeight > element.clientHeight;
const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold;
const isAtBottom =
element.scrollTop + element.clientHeight >=
element.scrollHeight - scrollThreshold;
const shouldShowHint = isScrollable && !isAtBottom;
setShowScrollHint(shouldShowHint);
......@@ -90,24 +96,29 @@ const ScrollableContainer = forwardRef(({
showScrollHint: shouldShowHint,
scrollTop: element.scrollTop,
scrollHeight: element.scrollHeight,
clientHeight: element.clientHeight
clientHeight: element.clientHeight,
});
}
}, [scrollThreshold]);
const debouncedCheckScrollable = useMemo(() =>
debounce(checkScrollable, debounceDelay),
[debounce, checkScrollable, debounceDelay]
const debouncedCheckScrollable = useMemo(
() => debounce(checkScrollable, debounceDelay),
[debounce, checkScrollable, debounceDelay],
);
const handleScroll = useCallback((e) => {
const handleScroll = useCallback(
(e) => {
debouncedCheckScrollable();
if (onScrollRef.current) {
onScrollRef.current(e);
}
}, [debouncedCheckScrollable]);
},
[debouncedCheckScrollable],
);
useImperativeHandle(ref, () => ({
useImperativeHandle(
ref,
() => ({
checkScrollable: () => {
checkScrollable();
},
......@@ -129,10 +140,14 @@ const ScrollableContainer = forwardRef(({
scrollHeight: element.scrollHeight,
clientHeight: element.clientHeight,
isScrollable: element.scrollHeight > element.clientHeight,
isAtBottom: element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold
isAtBottom:
element.scrollTop + element.clientHeight >=
element.scrollHeight - scrollThreshold,
};
}
}), [checkScrollable, scrollThreshold]);
},
}),
[checkScrollable, scrollThreshold],
);
useEffect(() => {
const timer = setTimeout(() => {
......@@ -154,7 +169,7 @@ const ScrollableContainer = forwardRef(({
childList: true,
subtree: true,
attributes: true,
characterData: true
characterData: true,
});
return () => observer.disconnect();
......@@ -185,13 +200,19 @@ const ScrollableContainer = forwardRef(({
};
}, []);
const containerStyle = useMemo(() => ({
maxHeight
}), [maxHeight]);
const containerStyle = useMemo(
() => ({
maxHeight,
}),
[maxHeight],
);
const fadeIndicatorStyle = useMemo(() => ({
opacity: showScrollHint ? 1 : 0
}), [showScrollHint]);
const fadeIndicatorStyle = useMemo(
() => ({
opacity: showScrollHint ? 1 : 0,
}),
[showScrollHint],
);
return (
<div
......@@ -213,7 +234,8 @@ const ScrollableContainer = forwardRef(({
/>
</div>
);
});
},
);
ScrollableContainer.displayName = 'ScrollableContainer';
......
......@@ -20,7 +20,17 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useState, useRef, useEffect } from 'react';
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
import { useContainerWidth } from '../../../hooks/common/useContainerWidth';
import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton, Tooltip } from '@douyinfe/semi-ui';
import {
Divider,
Button,
Tag,
Row,
Col,
Collapsible,
Checkbox,
Skeleton,
Tooltip,
} from '@douyinfe/semi-ui';
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
/**
......@@ -47,7 +57,7 @@ const SelectableButtonGroup = ({
collapsible = true,
collapseHeight = 200,
withCheckbox = false,
loading = false
loading = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [skeletonCount] = useState(12);
......@@ -64,15 +74,13 @@ const SelectableButtonGroup = ({
}, [text, containerWidth]);
const textElement = (
<span ref={textRef} className="sbg-ellipsis">
<span ref={textRef} className='sbg-ellipsis'>
{text}
</span>
);
return isOverflowing ? (
<Tooltip content={text}>
{textElement}
</Tooltip>
<Tooltip content={text}>{textElement}</Tooltip>
) : (
textElement
);
......@@ -127,15 +135,12 @@ const SelectableButtonGroup = ({
};
const renderSkeletonButtons = () => {
const placeholder = (
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
{Array.from({ length: skeletonCount }).map((_, index) => (
<Col
span={getColSpan()}
key={index}
>
<div style={{
<Col span={getColSpan()} key={index}>
<div
style={{
width: '100%',
height: '32px',
display: 'flex',
......@@ -144,8 +149,9 @@ const SelectableButtonGroup = ({
border: '1px solid var(--semi-color-border)',
borderRadius: 'var(--semi-border-radius-medium)',
padding: '0 12px',
gap: '6px'
}}>
gap: '6px',
}}
>
{withCheckbox && (
<Skeleton.Title active style={{ width: 14, height: 14 }} />
)}
......@@ -153,7 +159,7 @@ const SelectableButtonGroup = ({
active
style={{
width: `${60 + (index % 3) * 20}px`,
height: 14
height: 14,
}}
/>
</div>
......@@ -167,26 +173,29 @@ const SelectableButtonGroup = ({
);
};
const contentElement = showSkeleton ? renderSkeletonButtons() : (
const contentElement = showSkeleton ? (
renderSkeletonButtons()
) : (
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
{items.map((item) => {
const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0);
const isDisabled =
item.disabled ||
(typeof item.tagCount === 'number' && item.tagCount === 0);
const isActive = Array.isArray(activeValue)
? activeValue.includes(item.value)
: activeValue === item.value;
if (withCheckbox) {
return (
<Col
span={getColSpan()}
key={item.value}
>
<Col span={getColSpan()} key={item.value}>
<Button
onClick={() => { /* disabled */ }}
onClick={() => {
/* disabled */
}}
theme={isActive ? 'light' : 'outline'}
type={isActive ? 'primary' : 'tertiary'}
disabled={isDisabled}
className="sbg-button"
className='sbg-button'
icon={
<Checkbox
checked={isActive}
......@@ -197,11 +206,18 @@ const SelectableButtonGroup = ({
}
style={{ width: '100%', cursor: 'default' }}
>
<div className="sbg-content">
{item.icon && (<span className="sbg-icon">{item.icon}</span>)}
<div className='sbg-content'>
{item.icon && <span className='sbg-icon'>{item.icon}</span>}
<ConditionalTooltipText text={item.label} />
{item.tagCount !== undefined && shouldShowTags && (
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
<Tag
className='sbg-tag'
color='white'
shape='circle'
size='small'
>
{item.tagCount}
</Tag>
)}
</div>
</Button>
......@@ -210,23 +226,27 @@ const SelectableButtonGroup = ({
}
return (
<Col
span={getColSpan()}
key={item.value}
>
<Col span={getColSpan()} key={item.value}>
<Button
onClick={() => onChange(item.value)}
theme={isActive ? 'light' : 'outline'}
type={isActive ? 'primary' : 'tertiary'}
disabled={isDisabled}
className="sbg-button"
className='sbg-button'
style={{ width: '100%' }}
>
<div className="sbg-content">
{item.icon && (<span className="sbg-icon">{item.icon}</span>)}
<div className='sbg-content'>
{item.icon && <span className='sbg-icon'>{item.icon}</span>}
<ConditionalTooltipText text={item.label} />
{item.tagCount !== undefined && shouldShowTags && (
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
<Tag
className='sbg-tag'
color='white'
shape='circle'
size='small'
>
{item.tagCount}
</Tag>
)}
</div>
</Button>
......@@ -237,9 +257,12 @@ const SelectableButtonGroup = ({
);
return (
<div className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`} ref={containerRef}>
<div
className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`}
ref={containerRef}
>
{title && (
<Divider margin="12px" align="left">
<Divider margin='12px' align='left'>
{showSkeleton ? (
<Skeleton.Title active style={{ width: 80, height: 14 }} />
) : (
......@@ -249,23 +272,30 @@ const SelectableButtonGroup = ({
)}
{needCollapse && !showSkeleton ? (
<div style={{ position: 'relative' }}>
<Collapsible isOpen={isOpen} collapseHeight={collapseHeight} style={{ ...maskStyle }}>
<Collapsible
isOpen={isOpen}
collapseHeight={collapseHeight}
style={{ ...maskStyle }}
>
{contentElement}
</Collapsible>
{isOpen ? null : (
<div onClick={toggle} style={{ ...linkStyle }}>
<IconChevronDown size="small" />
<IconChevronDown size='small' />
<span>{t('展开更多')}</span>
</div>
)}
{isOpen && (
<div onClick={toggle} style={{
<div
onClick={toggle}
style={{
...linkStyle,
position: 'static',
marginTop: 8,
bottom: 'auto'
}}>
<IconChevronUp size="small" />
bottom: 'auto',
}}
>
<IconChevronUp size='small' />
<span>{t('收起')}</span>
</div>
)}
......
......@@ -21,7 +21,10 @@ import React from 'react';
import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui';
import { Bell } from 'lucide-react';
import { marked } from 'marked';
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
import {
IllustrationConstruction,
IllustrationConstructionDark,
} from '@douyinfe/semi-illustrations';
import ScrollableContainer from '../common/ui/ScrollableContainer';
const AnnouncementsPanel = ({
......@@ -29,36 +32,43 @@ const AnnouncementsPanel = ({
announcementLegendData,
CARD_PROPS,
ILLUSTRATION_SIZE,
t
t,
}) => {
return (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-2"
className='shadow-sm !rounded-2xl lg:col-span-2'
title={
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
<div className="flex items-center gap-2">
<div className='flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full'>
<div className='flex items-center gap-2'>
<Bell size={16} />
{t('系统公告')}
<Tag color="white" shape="circle">
<Tag color='white' shape='circle'>
{t('显示最新20条')}
</Tag>
</div>
{/* 图例 */}
<div className="flex flex-wrap gap-3 text-xs">
<div className='flex flex-wrap gap-3 text-xs'>
{announcementLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div key={index} className='flex items-center gap-1'>
<div
className="w-2 h-2 rounded-full"
className='w-2 h-2 rounded-full'
style={{
backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
legend.color === 'blue' ? '#3b82f6' :
legend.color === 'green' ? '#10b981' :
legend.color === 'orange' ? '#f59e0b' :
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
backgroundColor:
legend.color === 'grey'
? '#8b9aa7'
: legend.color === 'blue'
? '#3b82f6'
: legend.color === 'green'
? '#10b981'
: legend.color === 'orange'
? '#f59e0b'
: legend.color === 'red'
? '#ef4444'
: '#8b9aa7',
}}
/>
<span className="text-gray-600">{legend.label}</span>
<span className='text-gray-600'>{legend.label}</span>
</div>
))}
</div>
......@@ -66,9 +76,9 @@ const AnnouncementsPanel = ({
}
bodyStyle={{ padding: 0 }}
>
<ScrollableContainer maxHeight="24rem">
<ScrollableContainer maxHeight='24rem'>
{announcementData.length > 0 ? (
<Timeline mode="left">
<Timeline mode='left'>
{announcementData.map((item, idx) => {
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
return (
......@@ -76,16 +86,20 @@ const AnnouncementsPanel = ({
key={idx}
type={item.type || 'default'}
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
extra={item.extra ? (
extra={
item.extra ? (
<div
className="text-xs text-gray-500"
className='text-xs text-gray-500'
dangerouslySetInnerHTML={{ __html: htmlExtra }}
/>
) : null}
) : null
}
>
<div>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
dangerouslySetInnerHTML={{
__html: marked.parse(item.content || ''),
}}
/>
</div>
</Timeline.Item>
......@@ -93,10 +107,12 @@ const AnnouncementsPanel = ({
})}
</Timeline>
) : (
<div className="flex justify-center items-center py-8">
<div className='flex justify-center items-center py-8'>
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
darkModeImage={
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
}
title={t('暂无系统公告')}
description={t('请联系管理员在系统设置中配置公告信息')}
/>
......
......@@ -20,7 +20,10 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
import { Server, Gauge, ExternalLink } from 'lucide-react';
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
import {
IllustrationConstruction,
IllustrationConstructionDark,
} from '@douyinfe/semi-illustrations';
import ScrollableContainer from '../common/ui/ScrollableContainer';
const ApiInfoPanel = ({
......@@ -30,12 +33,12 @@ const ApiInfoPanel = ({
CARD_PROPS,
FLEX_CENTER_GAP2,
ILLUSTRATION_SIZE,
t
t,
}) => {
return (
<Card
{...CARD_PROPS}
className="bg-gray-50 border-0 !rounded-2xl"
className='bg-gray-50 border-0 !rounded-2xl'
title={
<div className={FLEX_CENTER_GAP2}>
<Server size={16} />
......@@ -44,66 +47,65 @@ const ApiInfoPanel = ({
}
bodyStyle={{ padding: 0 }}
>
<ScrollableContainer maxHeight="24rem">
<ScrollableContainer maxHeight='24rem'>
{apiInfoData.length > 0 ? (
apiInfoData.map((api) => (
<React.Fragment key={api.id}>
<div className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
<div className="flex-shrink-0 mr-3">
<Avatar
size="extra-small"
color={api.color}
>
<div className='flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer'>
<div className='flex-shrink-0 mr-3'>
<Avatar size='extra-small' color={api.color}>
{api.route.substring(0, 2)}
</Avatar>
</div>
<div className="flex-1">
<div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
<span className="text-sm font-medium text-gray-900 !font-bold break-all">
<div className='flex-1'>
<div className='flex flex-wrap items-center justify-between mb-1 w-full gap-2'>
<span className='text-sm font-medium text-gray-900 !font-bold break-all'>
{api.route}
</span>
<div className="flex items-center gap-1 mt-1 lg:mt-0">
<div className='flex items-center gap-1 mt-1 lg:mt-0'>
<Tag
prefixIcon={<Gauge size={12} />}
size="small"
color="white"
size='small'
color='white'
shape='circle'
onClick={() => handleSpeedTest(api.url)}
className="cursor-pointer hover:opacity-80 text-xs"
className='cursor-pointer hover:opacity-80 text-xs'
>
{t('测速')}
</Tag>
<Tag
prefixIcon={<ExternalLink size={12} />}
size="small"
color="white"
size='small'
color='white'
shape='circle'
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
className="cursor-pointer hover:opacity-80 text-xs"
onClick={() =>
window.open(api.url, '_blank', 'noopener,noreferrer')
}
className='cursor-pointer hover:opacity-80 text-xs'
>
{t('跳转')}
</Tag>
</div>
</div>
<div
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
className='!text-semi-color-primary break-all cursor-pointer hover:underline mb-1'
onClick={() => handleCopyUrl(api.url)}
>
{api.url}
</div>
<div className="text-gray-500">
{api.description}
</div>
<div className='text-gray-500'>{api.description}</div>
</div>
</div>
<Divider />
</React.Fragment>
))
) : (
<div className="flex justify-center items-center py-8">
<div className='flex justify-center items-center py-8'>
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
darkModeImage={
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
}
title={t('暂无API信息')}
description={t('请联系管理员在系统设置中配置API信息')}
/>
......
......@@ -23,7 +23,7 @@ import { PieChart } from 'lucide-react';
import {
IconHistogram,
IconPulse,
IconPieChart2Stroked
IconPieChart2Stroked,
} from '@douyinfe/semi-icons';
import { VChart } from '@visactor/react-vchart';
......@@ -38,76 +38,76 @@ const ChartsPanel = ({
CHART_CONFIG,
FLEX_CENTER_GAP2,
hasApiInfoPanel,
t
t,
}) => {
return (
<Card
{...CARD_PROPS}
className={`!rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
title={
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
<div className='flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3'>
<div className={FLEX_CENTER_GAP2}>
<PieChart size={16} />
{t('模型数据分析')}
</div>
<Tabs
type="button"
type='button'
activeKey={activeChartTab}
onChange={setActiveChartTab}
>
<TabPane tab={
<TabPane
tab={
<span>
<IconHistogram />
{t('消耗分布')}
</span>
} itemKey="1" />
<TabPane tab={
}
itemKey='1'
/>
<TabPane
tab={
<span>
<IconPulse />
{t('消耗趋势')}
</span>
} itemKey="2" />
<TabPane tab={
}
itemKey='2'
/>
<TabPane
tab={
<span>
<IconPieChart2Stroked />
{t('调用次数分布')}
</span>
} itemKey="3" />
<TabPane tab={
}
itemKey='3'
/>
<TabPane
tab={
<span>
<IconHistogram />
{t('调用次数排行')}
</span>
} itemKey="4" />
}
itemKey='4'
/>
</Tabs>
</div>
}
bodyStyle={{ padding: 0 }}
>
<div className="h-96 p-2">
<div className='h-96 p-2'>
{activeChartTab === '1' && (
<VChart
spec={spec_line}
option={CHART_CONFIG}
/>
<VChart spec={spec_line} option={CHART_CONFIG} />
)}
{activeChartTab === '2' && (
<VChart
spec={spec_model_line}
option={CHART_CONFIG}
/>
<VChart spec={spec_model_line} option={CHART_CONFIG} />
)}
{activeChartTab === '3' && (
<VChart
spec={spec_pie}
option={CHART_CONFIG}
/>
<VChart spec={spec_pie} option={CHART_CONFIG} />
)}
{activeChartTab === '4' && (
<VChart
spec={spec_rank_bar}
option={CHART_CONFIG}
/>
<VChart spec={spec_rank_bar} option={CHART_CONFIG} />
)}
</div>
</Card>
......
......@@ -27,19 +27,19 @@ const DashboardHeader = ({
showSearchModal,
refresh,
loading,
t
t,
}) => {
const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full";
const ICON_BUTTON_CLASS = 'text-white hover:bg-opacity-80 !rounded-full';
return (
<div className="flex items-center justify-between mb-4">
<div className='flex items-center justify-between mb-4'>
<h2
className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out"
className='text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out'
style={{ opacity: greetingVisible ? 1 : 0 }}
>
{getGreeting}
</h2>
<div className="flex gap-3">
<div className='flex gap-3'>
<Button
type='tertiary'
icon={<Search size={16} />}
......
......@@ -22,7 +22,10 @@ import { Card, Collapse, Empty } from '@douyinfe/semi-ui';
import { HelpCircle } from 'lucide-react';
import { IconPlus, IconMinus } from '@douyinfe/semi-icons';
import { marked } from 'marked';
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
import {
IllustrationConstruction,
IllustrationConstructionDark,
} from '@douyinfe/semi-illustrations';
import ScrollableContainer from '../common/ui/ScrollableContainer';
const FaqPanel = ({
......@@ -30,12 +33,12 @@ const FaqPanel = ({
CARD_PROPS,
FLEX_CENTER_GAP2,
ILLUSTRATION_SIZE,
t
t,
}) => {
return (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1"
className='shadow-sm !rounded-2xl lg:col-span-1'
title={
<div className={FLEX_CENTER_GAP2}>
<HelpCircle size={16} />
......@@ -44,7 +47,7 @@ const FaqPanel = ({
}
bodyStyle={{ padding: 0 }}
>
<ScrollableContainer maxHeight="24rem">
<ScrollableContainer maxHeight='24rem'>
{faqData.length > 0 ? (
<Collapse
accordion
......@@ -58,16 +61,20 @@ const FaqPanel = ({
itemKey={index.toString()}
>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
dangerouslySetInnerHTML={{
__html: marked.parse(item.answer || ''),
}}
/>
</Collapse.Panel>
))}
</Collapse>
) : (
<div className="flex justify-center items-center py-8">
<div className='flex justify-center items-center py-8'>
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
darkModeImage={
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
}
title={t('暂无常见问答')}
description={t('请联系管理员在系统设置中配置常见问答')}
/>
......
......@@ -28,13 +28,13 @@ const StatsCards = ({
loading,
getTrendSpec,
CARD_PROPS,
CHART_CONFIG
CHART_CONFIG,
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
return (
<div className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className='mb-4'>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
{groupedStatsData.map((group, idx) => (
<Card
key={idx}
......@@ -42,24 +42,24 @@ const StatsCards = ({
className={`${group.color} border-0 !rounded-2xl w-full`}
title={group.title}
>
<div className="space-y-4">
<div className='space-y-4'>
{group.items.map((item, itemIdx) => (
<div
key={itemIdx}
className="flex items-center justify-between cursor-pointer"
className='flex items-center justify-between cursor-pointer'
onClick={item.onClick}
>
<div className="flex items-center">
<div className='flex items-center'>
<Avatar
className="mr-3"
size="small"
className='mr-3'
size='small'
color={item.avatarColor}
>
{item.icon}
</Avatar>
<div>
<div className="text-xs text-gray-500">{item.title}</div>
<div className="text-lg font-semibold">
<div className='text-xs text-gray-500'>{item.title}</div>
<div className='text-lg font-semibold'>
<Skeleton
loading={loading}
active
......@@ -67,7 +67,11 @@ const StatsCards = ({
<Skeleton.Paragraph
active
rows={1}
style={{ width: '65px', height: '24px', marginTop: '4px' }}
style={{
width: '65px',
height: '24px',
marginTop: '4px',
}}
/>
}
>
......@@ -78,9 +82,9 @@ const StatsCards = ({
</div>
{item.title === t('当前余额') ? (
<Tag
color="white"
color='white'
shape='circle'
size="large"
size='large'
onClick={(e) => {
e.stopPropagation();
navigate('/console/topup');
......@@ -88,13 +92,16 @@ const StatsCards = ({
>
{t('充值')}
</Tag>
) : (loading || (item.trendData && item.trendData.length > 0)) && (
<div className="w-24 h-10">
) : (
(loading ||
(item.trendData && item.trendData.length > 0)) && (
<div className='w-24 h-10'>
<VChart
spec={getTrendSpec(item.trendData, item.trendColor)}
option={CHART_CONFIG}
/>
</div>
)
)}
</div>
))}
......
......@@ -18,9 +18,20 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Card, Button, Spin, Tabs, TabPane, Tag, Empty } from '@douyinfe/semi-ui';
import {
Card,
Button,
Spin,
Tabs,
TabPane,
Tag,
Empty,
} from '@douyinfe/semi-ui';
import { Gauge, RefreshCw } from 'lucide-react';
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
import {
IllustrationConstruction,
IllustrationConstructionDark,
} from '@douyinfe/semi-illustrations';
import ScrollableContainer from '../common/ui/ScrollableContainer';
const UptimePanel = ({
......@@ -33,15 +44,15 @@ const UptimePanel = ({
renderMonitorList,
CARD_PROPS,
ILLUSTRATION_SIZE,
t
t,
}) => {
return (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1"
className='shadow-sm !rounded-2xl lg:col-span-1'
title={
<div className="flex items-center justify-between w-full gap-2">
<div className="flex items-center gap-2">
<div className='flex items-center justify-between w-full gap-2'>
<div className='flex items-center gap-2'>
<Gauge size={16} />
{t('服务可用性')}
</div>
......@@ -49,39 +60,43 @@ const UptimePanel = ({
icon={<RefreshCw size={14} />}
onClick={loadUptimeData}
loading={uptimeLoading}
size="small"
theme="borderless"
size='small'
theme='borderless'
type='tertiary'
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
className='text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full'
/>
</div>
}
bodyStyle={{ padding: 0 }}
>
{/* 内容区域 */}
<div className="relative">
<div className='relative'>
<Spin spinning={uptimeLoading}>
{uptimeData.length > 0 ? (
uptimeData.length === 1 ? (
<ScrollableContainer maxHeight="24rem">
<ScrollableContainer maxHeight='24rem'>
{renderMonitorList(uptimeData[0].monitors)}
</ScrollableContainer>
) : (
<Tabs
type="card"
type='card'
collapsible
activeKey={activeUptimeTab}
onChange={setActiveUptimeTab}
size="small"
size='small'
>
{uptimeData.map((group, groupIdx) => (
<TabPane
tab={
<span className="flex items-center gap-2">
<span className='flex items-center gap-2'>
<Gauge size={14} />
{group.categoryName}
<Tag
color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
color={
activeUptimeTab === group.categoryName
? 'red'
: 'grey'
}
size='small'
shape='circle'
>
......@@ -92,7 +107,7 @@ const UptimePanel = ({
itemKey={group.categoryName}
key={groupIdx}
>
<ScrollableContainer maxHeight="21.5rem">
<ScrollableContainer maxHeight='21.5rem'>
{renderMonitorList(group.monitors)}
</ScrollableContainer>
</TabPane>
......@@ -100,10 +115,12 @@ const UptimePanel = ({
</Tabs>
)
) : (
<div className="flex justify-center items-center py-8">
<div className='flex justify-center items-center py-8'>
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
darkModeImage={
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
}
title={t('暂无监控数据')}
description={t('请联系管理员在系统设置中配置Uptime')}
/>
......@@ -114,15 +131,15 @@ const UptimePanel = ({
{/* 图例 */}
{uptimeData.length > 0 && (
<div className="p-3 bg-gray-50 rounded-b-2xl">
<div className="flex flex-wrap gap-3 text-xs justify-center">
<div className='p-3 bg-gray-50 rounded-b-2xl'>
<div className='flex flex-wrap gap-3 text-xs justify-center'>
{uptimeLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div key={index} className='flex items-center gap-1'>
<div
className="w-2 h-2 rounded-full"
className='w-2 h-2 rounded-full'
style={{ backgroundColor: legend.color }}
/>
<span className="text-gray-600">{legend.label}</span>
<span className='text-gray-600'>{legend.label}</span>
</div>
))}
</div>
......
......@@ -41,7 +41,7 @@ import {
FLEX_CENTER_GAP2,
ILLUSTRATION_SIZE,
ANNOUNCEMENT_LEGEND_DATA,
UPTIME_STATUS_MAP
UPTIME_STATUS_MAP,
} from '../../constants/dashboard.constants';
import {
getTrendSpec,
......@@ -49,7 +49,7 @@ import {
handleSpeedTest,
getUptimeStatusColor,
getUptimeStatusText,
renderMonitorList
renderMonitorList,
} from '../../helpers/dashboard';
const Dashboard = () => {
......@@ -70,7 +70,7 @@ const Dashboard = () => {
dashboardData.setPieData,
dashboardData.setLineData,
dashboardData.setModelColors,
dashboardData.t
dashboardData.t,
);
// ========== 统计数据 ==========
......@@ -82,12 +82,12 @@ const Dashboard = () => {
dashboardData.trendData,
dashboardData.performanceMetrics,
dashboardData.navigate,
dashboardData.t
dashboardData.t,
);
// ========== 数据处理 ==========
const initChart = async () => {
await dashboardData.loadQuotaData().then(data => {
await dashboardData.loadQuotaData().then((data) => {
if (data && data.length > 0) {
dashboardCharts.updateChartData(data);
}
......@@ -108,25 +108,30 @@ const Dashboard = () => {
// ========== 数据准备 ==========
const apiInfoData = statusState?.status?.api_info || [];
const announcementData = (statusState?.status?.announcements || []).map(item => {
const announcementData = (statusState?.status?.announcements || []).map(
(item) => {
const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
const absoluteTime = pubDate && !isNaN(pubDate.getTime())
const absoluteTime =
pubDate && !isNaN(pubDate.getTime())
? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
: (item?.publishDate || '');
: item?.publishDate || '';
const relativeTime = getRelativeTime(item.publishDate);
return ({
return {
...item,
time: absoluteTime,
relative: relativeTime
});
});
relative: relativeTime,
};
},
);
const faqData = statusState?.status?.faq || [];
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(([status, info]) => ({
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(
([status, info]) => ({
status: Number(status),
color: info.color,
label: dashboardData.t(info.label)
}));
label: dashboardData.t(info.label),
}),
);
// ========== Effects ==========
useEffect(() => {
......@@ -134,7 +139,7 @@ const Dashboard = () => {
}, []);
return (
<div className="h-full">
<div className='h-full'>
<DashboardHeader
getGreeting={dashboardData.getGreeting}
greetingVisible={dashboardData.greetingVisible}
......@@ -166,8 +171,10 @@ const Dashboard = () => {
/>
{/* API信息和图表面板 */}
<div className="mb-4">
<div className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
<div className='mb-4'>
<div
className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}
>
<ChartsPanel
activeChartTab={dashboardData.activeChartTab}
setActiveChartTab={dashboardData.setActiveChartTab}
......@@ -198,16 +205,18 @@ const Dashboard = () => {
{/* 系统公告和常见问答卡片 */}
{dashboardData.hasInfoPanels && (
<div className="mb-4">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className='mb-4'>
<div className='grid grid-cols-1 lg:grid-cols-4 gap-4'>
{/* 公告卡片 */}
{dashboardData.announcementsEnabled && (
<AnnouncementsPanel
announcementData={announcementData}
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(item => ({
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(
(item) => ({
...item,
label: dashboardData.t(item.label)
}))}
label: dashboardData.t(item.label),
}),
)}
CARD_PROPS={CARD_PROPS}
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
t={dashboardData.t}
......@@ -234,12 +243,19 @@ const Dashboard = () => {
setActiveUptimeTab={dashboardData.setActiveUptimeTab}
loadUptimeData={dashboardData.loadUptimeData}
uptimeLegendData={uptimeLegendData}
renderMonitorList={(monitors) => renderMonitorList(
renderMonitorList={(monitors) =>
renderMonitorList(
monitors,
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
(status) => getUptimeStatusText(status, UPTIME_STATUS_MAP, dashboardData.t),
dashboardData.t
)}
(status) =>
getUptimeStatusText(
status,
UPTIME_STATUS_MAP,
dashboardData.t,
),
dashboardData.t,
)
}
CARD_PROPS={CARD_PROPS}
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
t={dashboardData.t}
......
......@@ -30,12 +30,12 @@ const SearchModal = ({
dataExportDefaultTime,
timeOptions,
handleInputChange,
t
t,
}) => {
const formRef = useRef();
const FORM_FIELD_PROPS = {
className: "w-full mb-2 !rounded-lg",
className: 'w-full mb-2 !rounded-lg',
};
const createFormField = (Component, props) => (
......@@ -54,7 +54,7 @@ const SearchModal = ({
size={isMobile ? 'full-width' : 'small'}
centered
>
<Form ref={formRef} layout='vertical' className="w-full">
<Form ref={formRef} layout='vertical' className='w-full'>
{createFormField(Form.DatePicker, {
field: 'start_timestamp',
label: t('起始时间'),
......@@ -62,7 +62,7 @@ const SearchModal = ({
value: start_timestamp,
type: 'dateTime',
name: 'start_timestamp',
onChange: (value) => handleInputChange(value, 'start_timestamp')
onChange: (value) => handleInputChange(value, 'start_timestamp'),
})}
{createFormField(Form.DatePicker, {
......@@ -72,7 +72,7 @@ const SearchModal = ({
value: end_timestamp,
type: 'dateTime',
name: 'end_timestamp',
onChange: (value) => handleInputChange(value, 'end_timestamp')
onChange: (value) => handleInputChange(value, 'end_timestamp'),
})}
{createFormField(Form.Select, {
......@@ -82,16 +82,18 @@ const SearchModal = ({
placeholder: t('时间粒度'),
name: 'data_export_default_time',
optionList: timeOptions,
onChange: (value) => handleInputChange(value, 'data_export_default_time')
onChange: (value) =>
handleInputChange(value, 'data_export_default_time'),
})}
{isAdminUser && createFormField(Form.Input, {
{isAdminUser &&
createFormField(Form.Input, {
field: 'username',
label: t('用户名称'),
value: username,
placeholder: t('可选值'),
name: 'username',
onChange: (value) => handleInputChange(value, 'username')
onChange: (value) => handleInputChange(value, 'username'),
})}
</Form>
</Modal>
......
......@@ -41,7 +41,7 @@ const ActionButtons = ({
t,
}) => {
return (
<div className="flex items-center gap-2 md:gap-3">
<div className='flex items-center gap-2 md:gap-3'>
<NewYearButton isNewYear={isNewYear} />
<NotificationButton
......@@ -50,11 +50,7 @@ const ActionButtons = ({
t={t}
/>
<ThemeToggle
theme={theme}
onThemeToggle={onThemeToggle}
t={t}
/>
<ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
<LanguageSelector
currentLang={currentLang}
......
......@@ -38,35 +38,35 @@ const HeaderLogo = ({
}
return (
<Link to="/" className="group flex items-center gap-2">
<div className="relative w-8 h-8 md:w-8 md:h-8">
<SkeletonWrapper
loading={isLoading || !logoLoaded}
type="image"
/>
<Link to='/' className='group flex items-center gap-2'>
<div className='relative w-8 h-8 md:w-8 md:h-8'>
<SkeletonWrapper loading={isLoading || !logoLoaded} type='image' />
<img
src={logo}
alt="logo"
className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${(!isLoading && logoLoaded) ? 'opacity-100' : 'opacity-0'}`}
alt='logo'
className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${!isLoading && logoLoaded ? 'opacity-100' : 'opacity-0'}`}
/>
</div>
<div className="hidden md:flex items-center gap-2">
<div className="flex items-center gap-2">
<div className='hidden md:flex items-center gap-2'>
<div className='flex items-center gap-2'>
<SkeletonWrapper
loading={isLoading}
type="title"
type='title'
width={120}
height={24}
>
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
<Typography.Title
heading={4}
className='!text-lg !font-semibold !mb-0'
>
{systemName}
</Typography.Title>
</SkeletonWrapper>
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
<Tag
color={isSelfUseMode ? 'purple' : 'blue'}
className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
size="small"
className='text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm'
size='small'
shape='circle'
>
{isSelfUseMode ? t('自用模式') : t('演示站点')}
......
......@@ -25,21 +25,21 @@ import { CN, GB } from 'country-flag-icons/react/3x2';
const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
return (
<Dropdown
position="bottomRight"
position='bottomRight'
render={
<Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
<Dropdown.Item
onClick={() => onLanguageChange('zh')}
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
>
<CN title="中文" className="!w-5 !h-auto" />
<CN title='中文' className='!w-5 !h-auto' />
<span>中文</span>
</Dropdown.Item>
<Dropdown.Item
onClick={() => onLanguageChange('en')}
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'en' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
>
<GB title="English" className="!w-5 !h-auto" />
<GB title='English' className='!w-5 !h-auto' />
<span>English</span>
</Dropdown.Item>
</Dropdown.Menu>
......@@ -48,9 +48,9 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
<Button
icon={<Languages size={18} />}
aria-label={t('切换语言')}
theme="borderless"
type="tertiary"
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
theme='borderless'
type='tertiary'
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
/>
</Dropdown>
);
......
......@@ -36,13 +36,19 @@ const MobileMenuButton = ({
return (
<Button
icon={
(isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />
(isMobile ? drawerOpen : collapsed) ? (
<IconClose className='text-lg' />
) : (
<IconMenu className='text-lg' />
)
}
aria-label={
(isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')
}
aria-label={(isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')}
onClick={onToggle}
theme="borderless"
type="tertiary"
className="!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700"
theme='borderless'
type='tertiary'
className='!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700'
/>
);
};
......
......@@ -21,21 +21,16 @@ import React from 'react';
import { Link } from 'react-router-dom';
import SkeletonWrapper from './SkeletonWrapper';
const Navigation = ({
mainNavLinks,
isMobile,
isLoading,
userState
}) => {
const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => {
const renderNavLinks = () => {
const baseClasses = 'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';
const baseClasses =
'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';
const hoverClasses = 'hover:text-semi-color-primary';
const spacingClasses = isMobile ? 'p-1' : 'p-2';
const commonLinkClasses = `${baseClasses} ${spacingClasses} ${hoverClasses}`;
return mainNavLinks.map((link) => {
const linkContent = <span>{link.text}</span>;
if (link.isExternal) {
......@@ -58,11 +53,7 @@ const Navigation = ({
}
return (
<Link
key={link.itemKey}
to={targetPath}
className={commonLinkClasses}
>
<Link key={link.itemKey} to={targetPath} className={commonLinkClasses}>
{linkContent}
</Link>
);
......@@ -70,10 +61,10 @@ const Navigation = ({
};
return (
<nav className="flex flex-1 items-center gap-1 lg:gap-2 mx-2 md:mx-4 overflow-x-auto whitespace-nowrap scrollbar-hide">
<nav className='flex flex-1 items-center gap-1 lg:gap-2 mx-2 md:mx-4 overflow-x-auto whitespace-nowrap scrollbar-hide'>
<SkeletonWrapper
loading={isLoading}
type="navigation"
type='navigation'
count={4}
width={60}
height={16}
......
......@@ -36,21 +36,24 @@ const NewYearButton = ({ isNewYear }) => {
return (
<Dropdown
position="bottomRight"
position='bottomRight'
render={
<Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
<Dropdown.Item onClick={handleNewYearClick} className="!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-gray-600">
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
<Dropdown.Item
onClick={handleNewYearClick}
className='!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-gray-600'
>
Happy New Year!!! 🎉
</Dropdown.Item>
</Dropdown.Menu>
}
>
<Button
theme="borderless"
type="tertiary"
icon={<span className="text-xl">🎉</span>}
aria-label="New Year"
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full"
theme='borderless'
type='tertiary'
icon={<span className='text-xl'>🎉</span>}
aria-label='New Year'
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full'
/>
</Dropdown>
);
......
......@@ -26,14 +26,15 @@ const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
icon: <Bell size={18} />,
'aria-label': t('系统公告'),
onClick: onNoticeOpen,
theme: "borderless",
type: "tertiary",
className: "!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2",
theme: 'borderless',
type: 'tertiary',
className:
'!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2',
};
if (unreadCount > 0) {
return (
<Badge count={unreadCount} type="danger" overflowCount={99}>
<Badge count={unreadCount} type='danger' overflowCount={99}>
<Button {...buttonProps} />
</Badge>
);
......
......@@ -62,13 +62,17 @@ const SkeletonWrapper = ({
// 用户区域骨架屏 (头像 + 文本)
const renderUserAreaSkeleton = () => {
return (
<div className={`flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 ${className}`}>
<div
className={`flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 ${className}`}
>
<Skeleton
loading={true}
active
placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
placeholder={
<Skeleton.Avatar active size='extra-small' className='shadow-sm' />
}
/>
<div className="ml-1.5 mr-1">
<div className='ml-1.5 mr-1'>
<Skeleton
loading={true}
active
......@@ -107,12 +111,7 @@ const SkeletonWrapper = ({
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width, height: 24 }}
/>
}
placeholder={<Skeleton.Title active style={{ width, height: 24 }} />}
/>
);
};
......@@ -124,12 +123,7 @@ const SkeletonWrapper = ({
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width, height }}
/>
}
placeholder={<Skeleton.Title active style={{ width, height }} />}
/>
</div>
);
......
......@@ -25,29 +25,32 @@ import { useActualTheme } from '../../../context/Theme';
const ThemeToggle = ({ theme, onThemeToggle, t }) => {
const actualTheme = useActualTheme();
const themeOptions = useMemo(() => ([
const themeOptions = useMemo(
() => [
{
key: 'light',
icon: <Sun size={18} />,
buttonIcon: <Sun size={18} />,
label: t('浅色模式'),
description: t('始终使用浅色主题')
description: t('始终使用浅色主题'),
},
{
key: 'dark',
icon: <Moon size={18} />,
buttonIcon: <Moon size={18} />,
label: t('深色模式'),
description: t('始终使用深色主题')
description: t('始终使用深色主题'),
},
{
key: 'auto',
icon: <Monitor size={18} />,
buttonIcon: <Monitor size={18} />,
label: t('自动模式'),
description: t('跟随系统主题设置')
}
]), [t]);
description: t('跟随系统主题设置'),
},
],
[t],
);
const getItemClassName = (isSelected) =>
isSelected
......@@ -55,13 +58,13 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
: 'hover:!bg-semi-color-fill-1';
const currentButtonIcon = useMemo(() => {
const currentOption = themeOptions.find(option => option.key === theme);
const currentOption = themeOptions.find((option) => option.key === theme);
return currentOption?.buttonIcon || themeOptions[2].buttonIcon;
}, [theme, themeOptions]);
return (
<Dropdown
position="bottomRight"
position='bottomRight'
render={
<Dropdown.Menu>
{themeOptions.map((option) => (
......@@ -71,9 +74,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
onClick={() => onThemeToggle(option.key)}
className={getItemClassName(theme === option.key)}
>
<div className="flex flex-col">
<div className='flex flex-col'>
<span>{option.label}</span>
<span className="text-xs text-semi-color-text-2">
<span className='text-xs text-semi-color-text-2'>
{option.description}
</span>
</div>
......@@ -83,8 +86,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
{theme === 'auto' && (
<>
<Dropdown.Divider />
<div className="px-3 py-2 text-xs text-semi-color-text-2">
{t('当前跟随系统')}{actualTheme === 'dark' ? t('深色') : t('浅色')}
<div className='px-3 py-2 text-xs text-semi-color-text-2'>
{t('当前跟随系统')}
{actualTheme === 'dark' ? t('深色') : t('浅色')}
</div>
</>
)}
......@@ -94,9 +98,9 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
<Button
icon={currentButtonIcon}
aria-label={t('切换主题')}
theme="borderless"
type="tertiary"
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1"
theme='borderless'
type='tertiary'
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
/>
</Dropdown>
);
......
......@@ -19,12 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Link } from 'react-router-dom';
import {
Avatar,
Button,
Dropdown,
Typography,
} from '@douyinfe/semi-ui';
import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
import { ChevronDown } from 'lucide-react';
import {
IconExit,
......@@ -48,7 +43,7 @@ const UserArea = ({
return (
<SkeletonWrapper
loading={true}
type="userArea"
type='userArea'
width={50}
isMobile={isMobile}
/>
......@@ -58,17 +53,20 @@ const UserArea = ({
if (userState.user) {
return (
<Dropdown
position="bottomRight"
position='bottomRight'
render={
<Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
<Dropdown.Item
onClick={() => {
navigate('/console/personal');
}}
className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
>
<div className="flex items-center gap-2">
<IconUserSetting size="small" className="text-gray-500 dark:text-gray-400" />
<div className='flex items-center gap-2'>
<IconUserSetting
size='small'
className='text-gray-500 dark:text-gray-400'
/>
<span>{t('个人设置')}</span>
</div>
</Dropdown.Item>
......@@ -76,10 +74,13 @@ const UserArea = ({
onClick={() => {
navigate('/console/token');
}}
className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
>
<div className="flex items-center gap-2">
<IconKey size="small" className="text-gray-500 dark:text-gray-400" />
<div className='flex items-center gap-2'>
<IconKey
size='small'
className='text-gray-500 dark:text-gray-400'
/>
<span>{t('令牌管理')}</span>
</div>
</Dropdown.Item>
......@@ -87,16 +88,25 @@ const UserArea = ({
onClick={() => {
navigate('/console/topup');
}}
className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
>
<div className="flex items-center gap-2">
<IconCreditCard size="small" className="text-gray-500 dark:text-gray-400" />
<div className='flex items-center gap-2'>
<IconCreditCard
size='small'
className='text-gray-500 dark:text-gray-400'
/>
<span>{t('钱包管理')}</span>
</div>
</Dropdown.Item>
<Dropdown.Item onClick={logout} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white">
<div className="flex items-center gap-2">
<IconExit size="small" className="text-gray-500 dark:text-gray-400" />
<Dropdown.Item
onClick={logout}
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white'
>
<div className='flex items-center gap-2'>
<IconExit
size='small'
className='text-gray-500 dark:text-gray-400'
/>
<span>{t('退出')}</span>
</div>
</Dropdown.Item>
......@@ -104,74 +114,76 @@ const UserArea = ({
}
>
<Button
theme="borderless"
type="tertiary"
className="flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
theme='borderless'
type='tertiary'
className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
>
<Avatar
size="extra-small"
size='extra-small'
color={stringToColor(userState.user.username)}
className="mr-1"
className='mr-1'
>
{userState.user.username[0].toUpperCase()}
</Avatar>
<span className="hidden md:inline">
<Typography.Text className="!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1">
<span className='hidden md:inline'>
<Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
{userState.user.username}
</Typography.Text>
</span>
<ChevronDown size={14} className="text-xs text-semi-color-text-2 dark:text-gray-400" />
<ChevronDown
size={14}
className='text-xs text-semi-color-text-2 dark:text-gray-400'
/>
</Button>
</Dropdown>
);
} else {
const showRegisterButton = !isSelfUseMode;
const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5";
const commonSizingAndLayoutClass =
'flex items-center justify-center !py-[10px] !px-1.5';
const loginButtonSpecificStyling = "!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors";
const loginButtonSpecificStyling =
'!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors';
let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`;
let registerButtonClasses = `${commonSizingAndLayoutClass}`;
const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5";
const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
const loginButtonTextSpanClass =
'!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5';
const registerButtonTextSpanClass = '!text-xs !text-white !p-1.5';
if (showRegisterButton) {
if (isMobile) {
loginButtonClasses += " !rounded-full";
loginButtonClasses += ' !rounded-full';
} else {
loginButtonClasses += " !rounded-l-full !rounded-r-none";
loginButtonClasses += ' !rounded-l-full !rounded-r-none';
}
registerButtonClasses += " !rounded-r-full !rounded-l-none";
registerButtonClasses += ' !rounded-r-full !rounded-l-none';
} else {
loginButtonClasses += " !rounded-full";
loginButtonClasses += ' !rounded-full';
}
return (
<div className="flex items-center">
<Link to="/login" className="flex">
<div className='flex items-center'>
<Link to='/login' className='flex'>
<Button
theme="borderless"
type="tertiary"
theme='borderless'
type='tertiary'
className={loginButtonClasses}
>
<span className={loginButtonTextSpanClass}>
{t('登录')}
</span>
<span className={loginButtonTextSpanClass}>{t('登录')}</span>
</Button>
</Link>
{showRegisterButton && (
<div className="hidden md:block">
<Link to="/register" className="flex -ml-px">
<div className='hidden md:block'>
<Link to='/register' className='flex -ml-px'>
<Button
theme="solid"
type="primary"
theme='solid'
type='primary'
className={registerButtonClasses}
>
<span className={registerButtonTextSpanClass}>
{t('注册')}
</span>
<span className={registerButtonTextSpanClass}>{t('注册')}</span>
</Button>
</Link>
</div>
......
......@@ -63,7 +63,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const { mainNavLinks } = useNavigation(t, docsLink);
return (
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
<header className='text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg'>
<NoticeModal
visible={noticeVisible}
onClose={handleNoticeClose}
......@@ -72,9 +72,9 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
unreadKeys={getUnreadKeys()}
/>
<div className="w-full px-2">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<div className='w-full px-2'>
<div className='flex items-center justify-between h-16'>
<div className='flex items-center'>
<MobileMenuButton
isConsoleRoute={isConsoleRoute}
isMobile={isMobile}
......
......@@ -18,15 +18,31 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useContext, useMemo } from 'react';
import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
import {
Button,
Modal,
Empty,
Tabs,
TabPane,
Timeline,
} from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
import { API, showError, getRelativeTime } from '../../helpers';
import { marked } from 'marked';
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
import {
IllustrationNoContent,
IllustrationNoContentDark,
} from '@douyinfe/semi-illustrations';
import { StatusContext } from '../../context/Status';
import { Bell, Megaphone } from 'lucide-react';
const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadKeys = [] }) => {
const NoticeModal = ({
visible,
onClose,
isMobile,
defaultTab = 'inApp',
unreadKeys = [],
}) => {
const { t } = useTranslation();
const [noticeContent, setNoticeContent] = useState('');
const [loading, setLoading] = useState(false);
......@@ -38,23 +54,25 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
const unreadSet = useMemo(() => new Set(unreadKeys), [unreadKeys]);
const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
const getKeyForItem = (item) =>
`${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
const processedAnnouncements = useMemo(() => {
return (announcements || []).slice(0, 20).map(item => {
return (announcements || []).slice(0, 20).map((item) => {
const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
const absoluteTime = pubDate && !isNaN(pubDate.getTime())
const absoluteTime =
pubDate && !isNaN(pubDate.getTime())
? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
: (item?.publishDate || '');
return ({
: item?.publishDate || '';
return {
key: getKeyForItem(item),
type: item.type || 'default',
time: absoluteTime,
content: item.content,
extra: item.extra,
relative: getRelativeTime(item.publishDate),
isUnread: unreadSet.has(getKeyForItem(item))
});
isUnread: unreadSet.has(getKeyForItem(item)),
};
});
}, [announcements, unreadSet]);
......@@ -100,15 +118,23 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
const renderMarkdownNotice = () => {
if (loading) {
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
return (
<div className='py-12'>
<Empty description={t('加载中...')} />
</div>
);
}
if (!noticeContent) {
return (
<div className="py-12">
<div className='py-12'>
<Empty
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
image={
<IllustrationNoContent style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoContentDark style={{ width: 150, height: 150 }} />
}
description={t('暂无公告')}
/>
</div>
......@@ -118,7 +144,7 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
return (
<div
dangerouslySetInnerHTML={{ __html: noticeContent }}
className="notice-content-scroll max-h-[55vh] overflow-y-auto pr-2"
className='notice-content-scroll max-h-[55vh] overflow-y-auto pr-2'
/>
);
};
......@@ -126,10 +152,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
const renderAnnouncementTimeline = () => {
if (processedAnnouncements.length === 0) {
return (
<div className="py-12">
<div className='py-12'>
<Empty
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
image={
<IllustrationNoContent style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoContentDark style={{ width: 150, height: 150 }} />
}
description={t('暂无系统公告')}
/>
</div>
......@@ -137,8 +167,8 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
}
return (
<div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
<Timeline mode="left">
<div className='max-h-[55vh] overflow-y-auto pr-2 card-content-scroll'>
<Timeline mode='left'>
{processedAnnouncements.map((item, idx) => {
const htmlContent = marked.parse(item.content || '');
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
......@@ -147,12 +177,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
key={idx}
type={item.type}
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
extra={item.extra ? (
extra={
item.extra ? (
<div
className="text-xs text-gray-500"
className='text-xs text-gray-500'
dangerouslySetInnerHTML={{ __html: htmlExtra }}
/>
) : null}
) : null
}
className={item.isUnread ? '' : ''}
>
<div>
......@@ -179,26 +211,40 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK
return (
<Modal
title={
<div className="flex items-center justify-between w-full">
<div className='flex items-center justify-between w-full'>
<span>{t('系统公告')}</span>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
type='button'
>
<TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' />
<TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' />
<Tabs activeKey={activeTab} onChange={setActiveTab} type='button'>
<TabPane
tab={
<span className='flex items-center gap-1'>
<Bell size={14} /> {t('通知')}
</span>
}
itemKey='inApp'
/>
<TabPane
tab={
<span className='flex items-center gap-1'>
<Megaphone size={14} /> {t('系统公告')}
</span>
}
itemKey='system'
/>
</Tabs>
</div>
}
visible={visible}
onCancel={onClose}
footer={(
<div className="flex justify-end">
<Button type='secondary' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
<Button type="primary" onClick={onClose}>{t('关闭公告')}</Button>
footer={
<div className='flex justify-end'>
<Button type='secondary' onClick={handleCloseTodayNotice}>
{t('今日关闭')}
</Button>
<Button type='primary' onClick={onClose}>
{t('关闭公告')}
</Button>
</div>
)}
}
size={isMobile ? 'full-width' : 'large'}
>
{renderBody()}
......
......@@ -27,7 +27,13 @@ import React, { useContext, useEffect, useState } from 'react';
import { useIsMobile } from '../../hooks/common/useIsMobile';
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers';
import {
API,
getLogo,
getSystemName,
showError,
setStatusData,
} from '../../helpers';
import { UserContext } from '../../context/User';
import { StatusContext } from '../../context/Status';
import { useLocation } from 'react-router-dom';
......@@ -42,9 +48,12 @@ const PageLayout = () => {
const { i18n } = useTranslation();
const location = useLocation();
const shouldHideFooter = location.pathname.startsWith('/console') || location.pathname === '/pricing';
const shouldHideFooter =
location.pathname.startsWith('/console') ||
location.pathname === '/pricing';
const shouldInnerPadding = location.pathname.includes('/console') &&
const shouldInnerPadding =
location.pathname.includes('/console') &&
!location.pathname.startsWith('/console/chat') &&
location.pathname !== '/console/playground';
......@@ -120,7 +129,10 @@ const PageLayout = () => {
zIndex: 100,
}}
>
<HeaderBar onMobileMenuToggle={() => setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
<HeaderBar
onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
drawerOpen={drawerOpen}
/>
</Header>
<Layout
style={{
......@@ -142,12 +154,20 @@ const PageLayout = () => {
width: 'var(--sidebar-current-width)',
}}
>
<SiderBar onNavigate={() => { if (isMobile) setDrawerOpen(false); }} />
<SiderBar
onNavigate={() => {
if (isMobile) setDrawerOpen(false);
}}
/>
</Sider>
)}
<Layout
style={{
marginLeft: isMobile ? '0' : showSider ? 'var(--sidebar-current-width)' : '0',
marginLeft: isMobile
? '0'
: showSider
? 'var(--sidebar-current-width)'
: '0',
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
......
......@@ -26,7 +26,10 @@ const SetupCheck = ({ children }) => {
const location = useLocation();
useEffect(() => {
if (statusState?.status?.setup === false && location.pathname !== '/setup') {
if (
statusState?.status?.setup === false &&
location.pathname !== '/setup'
) {
window.location.href = '/setup';
}
}, [statusState?.status?.setup, location.pathname]);
......
......@@ -23,17 +23,9 @@ import { useTranslation } from 'react-i18next';
import { getLucideIcon } from '../../helpers/render';
import { ChevronLeft } from 'lucide-react';
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
import {
isAdmin,
isRoot,
showError
} from '../../helpers';
import {
Nav,
Divider,
Button,
} from '@douyinfe/semi-ui';
import { isAdmin, isRoot, showError } from '../../helpers';
import { Nav, Divider, Button } from '@douyinfe/semi-ui';
const routerMap = {
home: '/',
......@@ -54,7 +46,7 @@ const routerMap = {
personal: '/console/personal',
};
const SiderBar = ({ onNavigate = () => { } }) => {
const SiderBar = ({ onNavigate = () => {} }) => {
const { t } = useTranslation();
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
......@@ -275,14 +267,17 @@ const SiderBar = ({ onNavigate = () => { } }) => {
key={item.itemKey}
itemKey={item.itemKey}
text={
<div className="flex items-center">
<span className="truncate font-medium text-sm" style={{ color: textColor }}>
<div className='flex items-center'>
<span
className='truncate font-medium text-sm'
style={{ color: textColor }}
>
{item.text}
</span>
</div>
}
icon={
<div className="sidebar-icon-container flex-shrink-0">
<div className='sidebar-icon-container flex-shrink-0'>
{getLucideIcon(item.itemKey, isSelected)}
</div>
}
......@@ -302,14 +297,17 @@ const SiderBar = ({ onNavigate = () => { } }) => {
key={item.itemKey}
itemKey={item.itemKey}
text={
<div className="flex items-center">
<span className="truncate font-medium text-sm" style={{ color: textColor }}>
<div className='flex items-center'>
<span
className='truncate font-medium text-sm'
style={{ color: textColor }}
>
{item.text}
</span>
</div>
}
icon={
<div className="sidebar-icon-container flex-shrink-0">
<div className='sidebar-icon-container flex-shrink-0'>
{getLucideIcon(item.itemKey, isSelected)}
</div>
}
......@@ -323,7 +321,10 @@ const SiderBar = ({ onNavigate = () => { } }) => {
key={subItem.itemKey}
itemKey={subItem.itemKey}
text={
<span className="truncate font-medium text-sm" style={{ color: subTextColor }}>
<span
className='truncate font-medium text-sm'
style={{ color: subTextColor }}
>
{subItem.text}
</span>
}
......@@ -339,18 +340,18 @@ const SiderBar = ({ onNavigate = () => { } }) => {
return (
<div
className="sidebar-container"
className='sidebar-container'
style={{ width: 'var(--sidebar-current-width)' }}
>
<Nav
className="sidebar-nav"
className='sidebar-nav'
defaultIsCollapsed={collapsed}
isCollapsed={collapsed}
onCollapseChange={toggleCollapsed}
selectedKeys={selectedKeys}
itemStyle="sidebar-nav-item"
hoverStyle="sidebar-nav-item:hover"
selectedStyle="sidebar-nav-item-selected"
itemStyle='sidebar-nav-item'
hoverStyle='sidebar-nav-item:hover'
selectedStyle='sidebar-nav-item-selected'
renderWrapper={({ itemElement, props }) => {
const to = routerMapState[props.itemKey] || routerMap[props.itemKey];
......@@ -381,27 +382,25 @@ const SiderBar = ({ onNavigate = () => { } }) => {
}}
>
{/* 聊天区域 */}
<div className="sidebar-section">
{!collapsed && (
<div className="sidebar-group-label">{t('聊天')}</div>
)}
<div className='sidebar-section'>
{!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>}
{chatMenuItems.map((item) => renderSubItem(item))}
</div>
{/* 控制台区域 */}
<Divider className="sidebar-divider" />
<Divider className='sidebar-divider' />
<div>
{!collapsed && (
<div className="sidebar-group-label">{t('控制台')}</div>
<div className='sidebar-group-label'>{t('控制台')}</div>
)}
{workspaceItems.map((item) => renderNavItem(item))}
</div>
{/* 个人中心区域 */}
<Divider className="sidebar-divider" />
<Divider className='sidebar-divider' />
<div>
{!collapsed && (
<div className="sidebar-group-label">{t('个人中心')}</div>
<div className='sidebar-group-label'>{t('个人中心')}</div>
)}
{financeItems.map((item) => renderNavItem(item))}
</div>
......@@ -409,10 +408,10 @@ const SiderBar = ({ onNavigate = () => { } }) => {
{/* 管理员区域 - 只在管理员时显示 */}
{isAdmin() && (
<>
<Divider className="sidebar-divider" />
<Divider className='sidebar-divider' />
<div>
{!collapsed && (
<div className="sidebar-group-label">{t('管理员')}</div>
<div className='sidebar-group-label'>{t('管理员')}</div>
)}
{adminItems.map((item) => renderNavItem(item))}
</div>
......@@ -421,22 +420,28 @@ const SiderBar = ({ onNavigate = () => { } }) => {
</Nav>
{/* 底部折叠按钮 */}
<div className="sidebar-collapse-button">
<div className='sidebar-collapse-button'>
<Button
theme="outline"
type="tertiary"
size="small"
theme='outline'
type='tertiary'
size='small'
icon={
<ChevronLeft
size={16}
strokeWidth={2.5}
color="var(--semi-color-text-2)"
style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
color='var(--semi-color-text-2)'
style={{
transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)',
}}
/>
}
onClick={toggleCollapsed}
icononly={collapsed}
style={collapsed ? { padding: '4px', width: '100%' } : { padding: '4px 12px', width: '100%' }}
style={
collapsed
? { padding: '4px', width: '100%' }
: { padding: '4px 12px', width: '100%' }
}
>
{!collapsed ? t('收起侧边栏') : null}
</Button>
......
......@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Card,
Chat,
Typography,
Button,
} from '@douyinfe/semi-ui';
import {
MessageSquare,
Eye,
EyeOff,
} from 'lucide-react';
import { Card, Chat, Typography, Button } from '@douyinfe/semi-ui';
import { MessageSquare, Eye, EyeOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import CustomInputRender from './CustomInputRender';
......@@ -57,37 +48,43 @@ const ChatArea = ({
return (
<Card
className="h-full"
className='h-full'
bordered={false}
bodyStyle={{ padding: 0, height: 'calc(100vh - 66px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
bodyStyle={{
padding: 0,
height: 'calc(100vh - 66px)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* 聊天头部 */}
{styleState.isMobile ? (
<div className="pt-4"></div>
<div className='pt-4'></div>
) : (
<div className="px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center">
<MessageSquare size={20} className="text-white" />
<div className='px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<div className='w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center'>
<MessageSquare size={20} className='text-white' />
</div>
<div>
<Typography.Title heading={5} className="!text-white mb-0">
<Typography.Title heading={5} className='!text-white mb-0'>
{t('AI 对话')}
</Typography.Title>
<Typography.Text className="!text-white/80 text-sm hidden sm:inline">
<Typography.Text className='!text-white/80 text-sm hidden sm:inline'>
{inputs.model || t('选择模型开始对话')}
</Typography.Text>
</div>
</div>
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
<Button
icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
onClick={onToggleDebugPanel}
theme="borderless"
type="primary"
size="small"
className="!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10"
theme='borderless'
type='primary'
size='small'
className='!rounded-lg !text-white/80 hover:!text-white hover:!bg-white/10'
>
{showDebugPanel ? t('隐藏调试') : t('显示调试')}
</Button>
......@@ -97,7 +94,7 @@ const ChatArea = ({
)}
{/* 聊天内容区域 */}
<div className="flex-1 overflow-hidden">
<div className='flex-1 overflow-hidden'>
<Chat
ref={chatRef}
chatBoxRenderConfig={{
......@@ -110,7 +107,7 @@ const ChatArea = ({
style={{
height: '100%',
maxWidth: '100%',
overflow: 'hidden'
overflow: 'hidden',
}}
chats={message}
onMessageSend={onMessageSend}
......@@ -121,7 +118,7 @@ const ChatArea = ({
showStopGenerate
onStopGenerator={onStopGenerator}
onClear={onClearMessages}
className="h-full"
className='h-full'
placeholder={t('请输入您的问题...')}
/>
</div>
......
......@@ -102,15 +102,17 @@ const highlightJson = (str) => {
color = '#569cd6';
}
return `<span style="color: ${color}">${match}</span>`;
}
},
);
};
const isJsonLike = (content, language) => {
if (language === 'json') return true;
const trimmed = content.trim();
return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'));
return (
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'))
);
};
const formatContent = (content) => {
......@@ -148,7 +150,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
const contentMetrics = useMemo(() => {
const length = formattedContent.length;
const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH;
const isVeryLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
const isVeryLarge =
length >
PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH *
PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
return { length, isLarge, isVeryLarge };
}, [formattedContent.length]);
......@@ -156,8 +161,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
if (!contentMetrics.isLarge || isExpanded) {
return formattedContent;
}
return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
'\n\n// ... 内容被截断以提升性能 ...';
return (
formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
'\n\n// ... 内容被截断以提升性能 ...'
);
}, [formattedContent, contentMetrics.isLarge, isExpanded]);
const highlightedContent = useMemo(() => {
......@@ -174,7 +181,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
const handleCopy = useCallback(async () => {
try {
const textToCopy = typeof content === 'object' && content !== null
const textToCopy =
typeof content === 'object' && content !== null
? JSON.stringify(content, null, 2)
: content;
......@@ -205,10 +213,11 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
}, [isExpanded, contentMetrics.isVeryLarge]);
if (!content) {
const placeholderText = {
const placeholderText =
{
preview: t('正在构造请求体预览...'),
request: t('暂无请求数据'),
response: t('暂无响应数据')
response: t('暂无响应数据'),
}[title] || t('暂无数据');
return (
......@@ -222,7 +231,7 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
const contentPadding = contentMetrics.isLarge ? '52px' : '16px';
return (
<div style={codeThemeStyles.container} className="h-full">
<div style={codeThemeStyles.container} className='h-full'>
{/* 性能警告 */}
{contentMetrics.isLarge && (
<div style={codeThemeStyles.performanceWarning}>
......@@ -250,8 +259,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
<Button
icon={<Copy size={14} />}
onClick={handleCopy}
size="small"
theme="borderless"
size='small'
theme='borderless'
style={{
backgroundColor: 'transparent',
border: 'none',
......@@ -268,25 +277,29 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
...codeThemeStyles.content,
paddingTop: contentPadding,
}}
className="model-settings-scroll"
className='model-settings-scroll'
>
{isProcessing ? (
<div style={{
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '200px',
color: '#888'
}}>
<div style={{
color: '#888',
}}
>
<div
style={{
width: '20px',
height: '20px',
border: '2px solid #444',
borderTop: '2px solid #888',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginRight: '8px'
}} />
marginRight: '8px',
}}
/>
{t('正在处理大内容...')}
</div>
) : (
......@@ -296,18 +309,22 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
{/* 展开/收起按钮 */}
{contentMetrics.isLarge && !isProcessing && (
<div style={{
<div
style={{
...codeThemeStyles.actionButton,
bottom: '12px',
left: '50%',
transform: 'translateX(-50%)',
}}>
}}
>
<Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}>
<Button
icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
icon={
isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />
}
onClick={handleToggleExpand}
size="small"
theme="borderless"
size='small'
theme='borderless'
style={{
backgroundColor: 'transparent',
border: 'none',
......@@ -317,8 +334,16 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
>
{isExpanded ? t('收起') : t('展开')}
{!isExpanded && (
<span style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}>
(+{Math.round((contentMetrics.length - PERFORMANCE_CONFIG.PREVIEW_LENGTH) / 1000)}K)
<span
style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}
>
(+
{Math.round(
(contentMetrics.length -
PERFORMANCE_CONFIG.PREVIEW_LENGTH) /
1000,
)}
K)
</span>
)}
</Button>
......
......@@ -18,21 +18,16 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useRef } from 'react';
import {
Button,
Typography,
Toast,
Modal,
Dropdown,
} from '@douyinfe/semi-ui';
import {
Download,
Upload,
RotateCcw,
Settings2,
} from 'lucide-react';
import { Button, Typography, Toast, Modal, Dropdown } from '@douyinfe/semi-ui';
import { Download, Upload, RotateCcw, Settings2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { exportConfig, importConfig, clearConfig, hasStoredConfig, getConfigTimestamp } from './configStorage';
import {
exportConfig,
importConfig,
clearConfig,
hasStoredConfig,
getConfigTimestamp,
} from './configStorage';
const ConfigManager = ({
currentConfig,
......@@ -51,7 +46,10 @@ const ConfigManager = ({
...currentConfig,
timestamp: new Date().toISOString(),
};
localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp));
localStorage.setItem(
'playground_config',
JSON.stringify(configWithTimestamp),
);
exportConfig(currentConfig, messages);
Toast.success({
......@@ -104,7 +102,9 @@ const ConfigManager = ({
const handleReset = () => {
Modal.confirm({
title: t('重置配置'),
content: t('将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?'),
content: t(
'将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?',
),
okText: t('确定重置'),
cancelText: t('取消'),
okButtonProps: {
......@@ -114,7 +114,9 @@ const ConfigManager = ({
// 询问是否同时重置消息
Modal.confirm({
title: t('重置选项'),
content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'),
content: t(
'是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。',
),
okText: t('同时重置消息'),
cancelText: t('仅重置配置'),
okButtonProps: {
......@@ -159,7 +161,7 @@ const ConfigManager = ({
name: 'export',
onClick: handleExport,
children: (
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
<Download size={14} />
{t('导出配置')}
</div>
......@@ -170,7 +172,7 @@ const ConfigManager = ({
name: 'import',
onClick: handleImportClick,
children: (
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
<Upload size={14} />
{t('导入配置')}
</div>
......@@ -184,7 +186,7 @@ const ConfigManager = ({
name: 'reset',
onClick: handleReset,
children: (
<div className="flex items-center gap-2 text-red-600">
<div className='flex items-center gap-2 text-red-600'>
<RotateCcw size={14} />
{t('重置配置')}
</div>
......@@ -197,24 +199,24 @@ const ConfigManager = ({
return (
<>
<Dropdown
trigger="click"
position="bottomLeft"
trigger='click'
position='bottomLeft'
showTick
menu={dropdownItems}
>
<Button
icon={<Settings2 size={14} />}
theme="borderless"
type="tertiary"
size="small"
className="!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50"
theme='borderless'
type='tertiary'
size='small'
className='!rounded-lg !text-gray-600 hover:!text-blue-600 hover:!bg-blue-50'
/>
</Dropdown>
<input
ref={fileInputRef}
type="file"
accept=".json"
type='file'
accept='.json'
onChange={handleFileChange}
style={{ display: 'none' }}
/>
......@@ -224,42 +226,42 @@ const ConfigManager = ({
// 桌面端显示紧凑的按钮组
return (
<div className="space-y-3">
<div className='space-y-3'>
{/* 配置状态信息和重置按钮 */}
<div className="flex items-center justify-between">
<Typography.Text className="text-xs text-gray-500">
<div className='flex items-center justify-between'>
<Typography.Text className='text-xs text-gray-500'>
{getConfigStatus()}
</Typography.Text>
<Button
icon={<RotateCcw size={12} />}
size="small"
theme="borderless"
type="danger"
size='small'
theme='borderless'
type='danger'
onClick={handleReset}
className="!rounded-full !text-xs !px-2"
className='!rounded-full !text-xs !px-2'
/>
</div>
{/* 导出和导入按钮 */}
<div className="flex gap-2">
<div className='flex gap-2'>
<Button
icon={<Download size={12} />}
size="small"
theme="solid"
type="primary"
size='small'
theme='solid'
type='primary'
onClick={handleExport}
className="!rounded-lg flex-1 !text-xs !h-7"
className='!rounded-lg flex-1 !text-xs !h-7'
>
{t('导出')}
</Button>
<Button
icon={<Upload size={12} />}
size="small"
theme="outline"
type="primary"
size='small'
theme='outline'
type='primary'
onClick={handleImportClick}
className="!rounded-lg flex-1 !text-xs !h-7"
className='!rounded-lg flex-1 !text-xs !h-7'
>
{t('导入')}
</Button>
......@@ -267,8 +269,8 @@ const ConfigManager = ({
<input
ref={fileInputRef}
type="file"
accept=".json"
type='file'
accept='.json'
onChange={handleFileChange}
style={{ display: 'none' }}
/>
......
......@@ -21,7 +21,8 @@ import React from 'react';
const CustomInputRender = (props) => {
const { detailProps } = props;
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
detailProps;
// 清空按钮
const styledClearNode = clearContextNode
......@@ -36,7 +37,7 @@ const CustomInputRender = (props) => {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}
},
})
: null;
......@@ -52,21 +53,19 @@ const CustomInputRender = (props) => {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}
},
});
return (
<div className="p-2 sm:p-4">
<div className='p-2 sm:p-4'>
<div
className="flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-xl sm:rounded-2xl shadow-sm hover:shadow-md transition-shadow"
className='flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-xl sm:rounded-2xl shadow-sm hover:shadow-md transition-shadow'
style={{ border: '1px solid var(--semi-color-border)' }}
onClick={onClick}
>
{/* 清空对话按钮 - 左边 */}
{styledClearNode}
<div className="flex-1">
{inputNode}
</div>
<div className='flex-1'>{inputNode}</div>
{/* 发送按钮 - 右边 */}
{styledSendNode}
</div>
......
......@@ -25,13 +25,7 @@ import {
Switch,
Banner,
} from '@douyinfe/semi-ui';
import {
Code,
Edit,
Check,
X,
AlertTriangle,
} from 'lucide-react';
import { Code, Edit, Check, X, AlertTriangle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const CustomRequestEditor = ({
......@@ -48,12 +42,22 @@ const CustomRequestEditor = ({
// 当切换到自定义模式时,用默认payload初始化
useEffect(() => {
if (customRequestMode && (!customRequestBody || customRequestBody.trim() === '')) {
const defaultJson = defaultPayload ? JSON.stringify(defaultPayload, null, 2) : '';
if (
customRequestMode &&
(!customRequestBody || customRequestBody.trim() === '')
) {
const defaultJson = defaultPayload
? JSON.stringify(defaultPayload, null, 2)
: '';
setLocalValue(defaultJson);
onCustomRequestBodyChange(defaultJson);
}
}, [customRequestMode, defaultPayload, customRequestBody, onCustomRequestBodyChange]);
}, [
customRequestMode,
defaultPayload,
customRequestBody,
onCustomRequestBodyChange,
]);
// 同步外部传入的customRequestBody到本地状态
useEffect(() => {
......@@ -113,21 +117,21 @@ const CustomRequestEditor = ({
};
return (
<div className="space-y-4">
<div className='space-y-4'>
{/* 自定义模式开关 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Code size={16} className="text-gray-500" />
<Typography.Text strong className="text-sm">
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Code size={16} className='text-gray-500' />
<Typography.Text strong className='text-sm'>
自定义请求体模式
</Typography.Text>
</div>
<Switch
checked={customRequestMode}
onChange={handleModeToggle}
checkedText="开"
uncheckedText="关"
size="small"
checkedText='开'
uncheckedText='关'
size='small'
/>
</div>
......@@ -135,43 +139,43 @@ const CustomRequestEditor = ({
<>
{/* 提示信息 */}
<Banner
type="warning"
description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
type='warning'
description='启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。'
icon={<AlertTriangle size={16} />}
className="!rounded-lg"
className='!rounded-lg'
closeIcon={null}
/>
{/* JSON编辑器 */}
<div>
<div className="flex items-center justify-between mb-2">
<Typography.Text strong className="text-sm">
<div className='flex items-center justify-between mb-2'>
<Typography.Text strong className='text-sm'>
请求体 JSON
</Typography.Text>
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
{isValid ? (
<div className="flex items-center gap-1 text-green-600">
<div className='flex items-center gap-1 text-green-600'>
<Check size={14} />
<Typography.Text className="text-xs">
<Typography.Text className='text-xs'>
格式正确
</Typography.Text>
</div>
) : (
<div className="flex items-center gap-1 text-red-600">
<div className='flex items-center gap-1 text-red-600'>
<X size={14} />
<Typography.Text className="text-xs">
<Typography.Text className='text-xs'>
格式错误
</Typography.Text>
</div>
)}
<Button
theme="borderless"
type="tertiary"
size="small"
theme='borderless'
type='tertiary'
size='small'
icon={<Edit size={14} />}
onClick={formatJson}
disabled={!isValid}
className="!rounded-lg"
className='!rounded-lg'
>
格式化
</Button>
......@@ -191,12 +195,12 @@ const CustomRequestEditor = ({
/>
{!isValid && errorMessage && (
<Typography.Text type="danger" className="text-xs mt-1 block">
<Typography.Text type='danger' className='text-xs mt-1 block'>
{errorMessage}
</Typography.Text>
)}
<Typography.Text className="text-xs text-gray-500 mt-2 block">
<Typography.Text className='text-xs text-gray-500 mt-2 block'>
请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。
</Typography.Text>
</div>
......
......@@ -26,14 +26,7 @@ import {
Button,
Dropdown,
} from '@douyinfe/semi-ui';
import {
Code,
Zap,
Clock,
X,
Eye,
Send,
} from 'lucide-react';
import { Code, Zap, Clock, X, Eye, Send } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import CodeViewer from './CodeViewer';
......@@ -76,7 +69,7 @@ const DebugPanel = ({
<Dropdown
render={
<Dropdown.Menu>
{items.map(item => {
{items.map((item) => {
return (
<Dropdown.Item
key={item.itemKey}
......@@ -104,21 +97,21 @@ const DebugPanel = ({
return (
<Card
className="h-full flex flex-col"
className='h-full flex flex-col'
bordered={false}
bodyStyle={{
padding: styleState.isMobile ? '16px' : '24px',
height: '100%',
display: 'flex',
flexDirection: 'column'
flexDirection: 'column',
}}
>
<div className="flex items-center justify-between mb-6 flex-shrink-0">
<div className="flex items-center">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3">
<Code size={20} className="text-white" />
<div className='flex items-center justify-between mb-6 flex-shrink-0'>
<div className='flex items-center'>
<div className='w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3'>
<Code size={20} className='text-white' />
</div>
<Typography.Title heading={5} className="mb-0">
<Typography.Title heading={5} className='mb-0'>
{t('调试信息')}
</Typography.Title>
</div>
......@@ -127,75 +120,84 @@ const DebugPanel = ({
<Button
icon={<X size={16} />}
onClick={onCloseDebugPanel}
theme="borderless"
type="tertiary"
size="small"
className="!rounded-lg"
theme='borderless'
type='tertiary'
size='small'
className='!rounded-lg'
/>
)}
</div>
<div className="flex-1 overflow-hidden debug-panel">
<div className='flex-1 overflow-hidden debug-panel'>
<Tabs
renderArrow={renderArrow}
type="card"
type='card'
collapsible
className="h-full"
className='h-full'
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
activeKey={activeKey}
onChange={handleTabChange}
>
<TabPane tab={
<div className="flex items-center gap-2">
<TabPane
tab={
<div className='flex items-center gap-2'>
<Eye size={16} />
{t('预览请求体')}
{customRequestMode && (
<span className="px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full">
<span className='px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full'>
自定义
</span>
)}
</div>
} itemKey="preview">
}
itemKey='preview'
>
<CodeViewer
content={debugData.previewRequest}
title="preview"
language="json"
title='preview'
language='json'
/>
</TabPane>
<TabPane tab={
<div className="flex items-center gap-2">
<TabPane
tab={
<div className='flex items-center gap-2'>
<Send size={16} />
{t('实际请求体')}
</div>
} itemKey="request">
}
itemKey='request'
>
<CodeViewer
content={debugData.request}
title="request"
language="json"
title='request'
language='json'
/>
</TabPane>
<TabPane tab={
<div className="flex items-center gap-2">
<TabPane
tab={
<div className='flex items-center gap-2'>
<Zap size={16} />
{t('响应')}
</div>
} itemKey="response">
}
itemKey='response'
>
<CodeViewer
content={debugData.response}
title="response"
language="json"
title='response'
language='json'
/>
</TabPane>
</Tabs>
</div>
<div className="flex items-center justify-between mt-4 pt-4 flex-shrink-0">
<div className='flex items-center justify-between mt-4 pt-4 flex-shrink-0'>
{(debugData.timestamp || debugData.previewTimestamp) && (
<div className="flex items-center gap-2">
<Clock size={14} className="text-gray-500" />
<Typography.Text className="text-xs text-gray-500">
<div className='flex items-center gap-2'>
<Clock size={14} className='text-gray-500' />
<Typography.Text className='text-xs text-gray-500'>
{activeKey === 'preview' && debugData.previewTimestamp
? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}`
: debugData.timestamp
......
......@@ -19,11 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import {
Settings,
Eye,
EyeOff,
} from 'lucide-react';
import { Settings, Eye, EyeOff } from 'lucide-react';
const FloatingButtons = ({
styleState,
......@@ -55,7 +51,7 @@ const FloatingButtons = ({
onClick={onToggleSettings}
theme='solid'
type='primary'
className="lg:hidden"
className='lg:hidden'
/>
)}
......@@ -64,8 +60,8 @@ const FloatingButtons = ({
<Button
icon={showDebugPanel ? <EyeOff size={18} /> : <Eye size={18} />}
onClick={onToggleDebugPanel}
theme="solid"
type={showDebugPanel ? "danger" : "primary"}
theme='solid'
type={showDebugPanel ? 'danger' : 'primary'}
style={{
position: 'fixed',
right: 16,
......@@ -80,7 +76,7 @@ const FloatingButtons = ({
? 'linear-gradient(to right, #e11d48, #be123c)'
: 'linear-gradient(to right, #4f46e5, #6366f1)',
}}
className="lg:hidden"
className='lg:hidden'
/>
)}
</>
......
......@@ -18,21 +18,17 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Input,
Typography,
Button,
Switch,
} from '@douyinfe/semi-ui';
import { Input, Typography, Button, Switch } from '@douyinfe/semi-ui';
import { IconFile } from '@douyinfe/semi-icons';
import {
FileText,
Plus,
X,
Image,
} from 'lucide-react';
import { FileText, Plus, X, Image } from 'lucide-react';
const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnabledChange, disabled = false }) => {
const ImageUrlInput = ({
imageUrls,
imageEnabled,
onImageUrlsChange,
onImageEnabledChange,
disabled = false,
}) => {
const handleAddImageUrl = () => {
const newUrls = [...imageUrls, ''];
onImageUrlsChange(newUrls);
......@@ -51,75 +47,87 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
return (
<div className={disabled ? 'opacity-50' : ''}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Image size={16} className={imageEnabled && !disabled ? "text-blue-500" : "text-gray-400"} />
<Typography.Text strong className="text-sm">
<div className='flex items-center justify-between mb-2'>
<div className='flex items-center gap-2'>
<Image
size={16}
className={
imageEnabled && !disabled ? 'text-blue-500' : 'text-gray-400'
}
/>
<Typography.Text strong className='text-sm'>
图片地址
</Typography.Text>
{disabled && (
<Typography.Text className="text-xs text-orange-600">
<Typography.Text className='text-xs text-orange-600'>
(已在自定义模式中忽略)
</Typography.Text>
)}
</div>
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
<Switch
checked={imageEnabled}
onChange={onImageEnabledChange}
checkedText="启用"
uncheckedText="停用"
size="small"
className="flex-shrink-0"
checkedText='启用'
uncheckedText='停用'
size='small'
className='flex-shrink-0'
disabled={disabled}
/>
<Button
icon={<Plus size={14} />}
size="small"
theme="solid"
type="primary"
size='small'
theme='solid'
type='primary'
onClick={handleAddImageUrl}
className="!rounded-full !w-4 !h-4 !p-0 !min-w-0"
className='!rounded-full !w-4 !h-4 !p-0 !min-w-0'
disabled={!imageEnabled || disabled}
/>
</div>
</div>
{!imageEnabled ? (
<Typography.Text className="text-xs text-gray-500 mb-2 block">
{disabled ? '图片功能在自定义请求体模式下不可用' : '启用后可添加图片URL进行多模态对话'}
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
{disabled
? '图片功能在自定义请求体模式下不可用'
: '启用后可添加图片URL进行多模态对话'}
</Typography.Text>
) : imageUrls.length === 0 ? (
<Typography.Text className="text-xs text-gray-500 mb-2 block">
{disabled ? '图片功能在自定义请求体模式下不可用' : '点击 + 按钮添加图片URL进行多模态对话'}
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
{disabled
? '图片功能在自定义请求体模式下不可用'
: '点击 + 按钮添加图片URL进行多模态对话'}
</Typography.Text>
) : (
<Typography.Text className="text-xs text-gray-500 mb-2 block">
已添加 {imageUrls.length} 张图片{disabled ? ' (自定义模式下不可用)' : ''}
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
已添加 {imageUrls.length} 张图片
{disabled ? ' (自定义模式下不可用)' : ''}
</Typography.Text>
)}
<div className={`space-y-2 max-h-32 overflow-y-auto image-list-scroll ${!imageEnabled || disabled ? 'opacity-50' : ''}`}>
<div
className={`space-y-2 max-h-32 overflow-y-auto image-list-scroll ${!imageEnabled || disabled ? 'opacity-50' : ''}`}
>
{imageUrls.map((url, index) => (
<div key={index} className="flex items-center gap-2">
<div className="flex-1">
<div key={index} className='flex items-center gap-2'>
<div className='flex-1'>
<Input
placeholder={`https://example.com/image${index + 1}.jpg`}
value={url}
onChange={(value) => handleUpdateImageUrl(index, value)}
className="!rounded-lg"
size="small"
className='!rounded-lg'
size='small'
prefix={<IconFile size='small' />}
disabled={!imageEnabled || disabled}
/>
</div>
<Button
icon={<X size={12} />}
size="small"
theme="borderless"
type="danger"
size='small'
theme='borderless'
type='danger'
onClick={() => handleRemoveImageUrl(index)}
className="!rounded-full !w-6 !h-6 !p-0 !min-w-0 !text-red-500 hover:!bg-red-50 flex-shrink-0"
className='!rounded-full !w-6 !h-6 !p-0 !min-w-0 !text-red-500 hover:!bg-red-50 flex-shrink-0'
disabled={!imageEnabled || disabled}
/>
</div>
......
......@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Button,
Tooltip,
} from '@douyinfe/semi-ui';
import {
RefreshCw,
Copy,
Trash2,
UserCheck,
Edit,
} from 'lucide-react';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { RefreshCw, Copy, Trash2, UserCheck, Edit } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const MessageActions = ({
......@@ -40,23 +31,32 @@ const MessageActions = ({
onRoleToggle,
onMessageEdit,
isAnyMessageGenerating = false,
isEditing = false
isEditing = false,
}) => {
const { t } = useTranslation();
const isLoading = message.status === 'loading' || message.status === 'incomplete';
const isLoading =
message.status === 'loading' || message.status === 'incomplete';
const shouldDisableActions = isAnyMessageGenerating || isEditing;
const canToggleRole = message.role === 'assistant' || message.role === 'system';
const canEdit = !isLoading && message.content && typeof onMessageEdit === 'function' && !isEditing;
const canToggleRole =
message.role === 'assistant' || message.role === 'system';
const canEdit =
!isLoading &&
message.content &&
typeof onMessageEdit === 'function' &&
!isEditing;
return (
<div className="flex items-center gap-0.5">
<div className='flex items-center gap-0.5'>
{!isLoading && (
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')} position="top">
<Tooltip
content={shouldDisableActions ? t('操作暂时被禁用') : t('重试')}
position='top'
>
<Button
theme="borderless"
type="tertiary"
size="small"
theme='borderless'
type='tertiary'
size='small'
icon={<RefreshCw size={styleState.isMobile ? 12 : 14} />}
onClick={() => !shouldDisableActions && onMessageReset(message)}
disabled={shouldDisableActions}
......@@ -67,11 +67,11 @@ const MessageActions = ({
)}
{message.content && (
<Tooltip content={t('复制')} position="top">
<Tooltip content={t('复制')} position='top'>
<Button
theme="borderless"
type="tertiary"
size="small"
theme='borderless'
type='tertiary'
size='small'
icon={<Copy size={styleState.isMobile ? 12 : 14} />}
onClick={() => onMessageCopy(message)}
className={`!rounded-full !text-gray-400 hover:!text-green-600 hover:!bg-green-50 ${styleState.isMobile ? '!w-6 !h-6' : '!w-7 !h-7'} !p-0 transition-all`}
......@@ -81,11 +81,14 @@ const MessageActions = ({
)}
{canEdit && (
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')} position="top">
<Tooltip
content={shouldDisableActions ? t('操作暂时被禁用') : t('编辑')}
position='top'
>
<Button
theme="borderless"
type="tertiary"
size="small"
theme='borderless'
type='tertiary'
size='small'
icon={<Edit size={styleState.isMobile ? 12 : 14} />}
onClick={() => !shouldDisableActions && onMessageEdit(message)}
disabled={shouldDisableActions}
......@@ -104,27 +107,36 @@ const MessageActions = ({
? t('切换为System角色')
: t('切换为Assistant角色')
}
position="top"
position='top'
>
<Button
theme="borderless"
type="tertiary"
size="small"
theme='borderless'
type='tertiary'
size='small'
icon={<UserCheck size={styleState.isMobile ? 12 : 14} />}
onClick={() => !shouldDisableActions && onRoleToggle && onRoleToggle(message)}
onClick={() =>
!shouldDisableActions && onRoleToggle && onRoleToggle(message)
}
disabled={shouldDisableActions}
className={`!rounded-full ${shouldDisableActions ? '!text-gray-300 !cursor-not-allowed' : message.role === 'system' ? '!text-purple-500 hover:!text-purple-700 hover:!bg-purple-50' : '!text-gray-400 hover:!text-purple-600 hover:!bg-purple-50'} ${styleState.isMobile ? '!w-6 !h-6' : '!w-7 !h-7'} !p-0 transition-all`}
aria-label={message.role === 'assistant' ? t('切换为System角色') : t('切换为Assistant角色')}
aria-label={
message.role === 'assistant'
? t('切换为System角色')
: t('切换为Assistant角色')
}
/>
</Tooltip>
)}
{!isLoading && (
<Tooltip content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')} position="top">
<Tooltip
content={shouldDisableActions ? t('操作暂时被禁用') : t('删除')}
position='top'
>
<Button
theme="borderless"
type="tertiary"
size="small"
theme='borderless'
type='tertiary'
size='small'
icon={<Trash2 size={styleState.isMobile ? 12 : 14} />}
onClick={() => !shouldDisableActions && onMessageDelete(message)}
disabled={shouldDisableActions}
......
......@@ -24,23 +24,30 @@ import SettingsPanel from './SettingsPanel';
import DebugPanel from './DebugPanel';
// 优化的消息内容组件
export const OptimizedMessageContent = React.memo(MessageContent, (prevProps, nextProps) => {
export const OptimizedMessageContent = React.memo(
MessageContent,
(prevProps, nextProps) => {
// 只有这些属性变化时才重新渲染
return (
prevProps.message.id === nextProps.message.id &&
prevProps.message.content === nextProps.message.content &&
prevProps.message.status === nextProps.message.status &&
prevProps.message.role === nextProps.message.role &&
prevProps.message.reasoningContent === nextProps.message.reasoningContent &&
prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
prevProps.message.reasoningContent ===
nextProps.message.reasoningContent &&
prevProps.message.isReasoningExpanded ===
nextProps.message.isReasoningExpanded &&
prevProps.isEditing === nextProps.isEditing &&
prevProps.editValue === nextProps.editValue &&
prevProps.styleState.isMobile === nextProps.styleState.isMobile
);
});
},
);
// 优化的消息操作组件
export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, nextProps) => {
export const OptimizedMessageActions = React.memo(
MessageActions,
(prevProps, nextProps) => {
return (
prevProps.message.id === nextProps.message.id &&
prevProps.message.role === nextProps.message.role &&
......@@ -48,32 +55,43 @@ export const OptimizedMessageActions = React.memo(MessageActions, (prevProps, ne
prevProps.isEditing === nextProps.isEditing &&
prevProps.onMessageReset === nextProps.onMessageReset
);
});
},
);
// 优化的设置面板组件
export const OptimizedSettingsPanel = React.memo(SettingsPanel, (prevProps, nextProps) => {
export const OptimizedSettingsPanel = React.memo(
SettingsPanel,
(prevProps, nextProps) => {
return (
JSON.stringify(prevProps.inputs) === JSON.stringify(nextProps.inputs) &&
JSON.stringify(prevProps.parameterEnabled) === JSON.stringify(nextProps.parameterEnabled) &&
JSON.stringify(prevProps.parameterEnabled) ===
JSON.stringify(nextProps.parameterEnabled) &&
JSON.stringify(prevProps.models) === JSON.stringify(nextProps.models) &&
JSON.stringify(prevProps.groups) === JSON.stringify(nextProps.groups) &&
prevProps.customRequestMode === nextProps.customRequestMode &&
prevProps.customRequestBody === nextProps.customRequestBody &&
prevProps.showDebugPanel === nextProps.showDebugPanel &&
prevProps.showSettings === nextProps.showSettings &&
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
JSON.stringify(prevProps.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(
DebugPanel,
(prevProps, nextProps) => {
return (
prevProps.show === nextProps.show &&
prevProps.activeTab === nextProps.activeTab &&
JSON.stringify(prevProps.debugData) === JSON.stringify(nextProps.debugData) &&
JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) &&
JSON.stringify(prevProps.debugData) ===
JSON.stringify(nextProps.debugData) &&
JSON.stringify(prevProps.previewPayload) ===
JSON.stringify(nextProps.previewPayload) &&
prevProps.customRequestMode === nextProps.customRequestMode &&
prevProps.showDebugPanel === nextProps.showDebugPanel
);
});
\ No newline at end of file
},
);
......@@ -18,20 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Card,
Select,
Typography,
Button,
Switch,
} from '@douyinfe/semi-ui';
import {
Sparkles,
Users,
ToggleLeft,
X,
Settings,
} from 'lucide-react';
import { Card, Select, Typography, Button, Switch } from '@douyinfe/semi-ui';
import { Sparkles, Users, ToggleLeft, X, Settings } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { renderGroupOption, selectFilter } from '../../helpers';
import ParameterControl from './ParameterControl';
......@@ -70,22 +58,22 @@ const SettingsPanel = ({
return (
<Card
className="h-full flex flex-col"
className='h-full flex flex-col'
bordered={false}
bodyStyle={{
padding: styleState.isMobile ? '16px' : '24px',
height: '100%',
display: 'flex',
flexDirection: 'column'
flexDirection: 'column',
}}
>
{/* 标题区域 - 与调试面板保持一致 */}
<div className="flex items-center justify-between mb-6 flex-shrink-0">
<div className="flex items-center">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3">
<Settings size={20} className="text-white" />
<div className='flex items-center justify-between mb-6 flex-shrink-0'>
<div className='flex items-center'>
<div className='w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mr-3'>
<Settings size={20} className='text-white' />
</div>
<Typography.Title heading={5} className="mb-0">
<Typography.Title heading={5} className='mb-0'>
{t('模型配置')}
</Typography.Title>
</div>
......@@ -94,17 +82,17 @@ const SettingsPanel = ({
<Button
icon={<X size={16} />}
onClick={onCloseSettings}
theme="borderless"
type="tertiary"
size="small"
className="!rounded-lg"
theme='borderless'
type='tertiary'
size='small'
className='!rounded-lg'
/>
)}
</div>
{/* 移动端配置管理 */}
{styleState.isMobile && (
<div className="mb-4 flex-shrink-0">
<div className='mb-4 flex-shrink-0'>
<ConfigManager
currentConfig={currentConfig}
onConfigImport={onConfigImport}
......@@ -115,7 +103,7 @@ const SettingsPanel = ({
</div>
)}
<div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
<div className='space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll'>
{/* 自定义请求体编辑器 */}
<CustomRequestEditor
customRequestMode={customRequestMode}
......@@ -127,13 +115,13 @@ const SettingsPanel = ({
{/* 分组选择 */}
<div className={customRequestMode ? 'opacity-50' : ''}>
<div className="flex items-center gap-2 mb-2">
<Users size={16} className="text-gray-500" />
<Typography.Text strong className="text-sm">
<div className='flex items-center gap-2 mb-2'>
<Users size={16} className='text-gray-500' />
<Typography.Text strong className='text-sm'>
{t('分组')}
</Typography.Text>
{customRequestMode && (
<Typography.Text className="text-xs text-orange-600">
<Typography.Text className='text-xs text-orange-600'>
(已在自定义模式中忽略)
</Typography.Text>
)}
......@@ -152,20 +140,20 @@ const SettingsPanel = ({
renderOptionItem={renderGroupOption}
style={{ width: '100%' }}
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
className="!rounded-lg"
className='!rounded-lg'
disabled={customRequestMode}
/>
</div>
{/* 模型选择 */}
<div className={customRequestMode ? 'opacity-50' : ''}>
<div className="flex items-center gap-2 mb-2">
<Sparkles size={16} className="text-gray-500" />
<Typography.Text strong className="text-sm">
<div className='flex items-center gap-2 mb-2'>
<Sparkles size={16} className='text-gray-500' />
<Typography.Text strong className='text-sm'>
{t('模型')}
</Typography.Text>
{customRequestMode && (
<Typography.Text className="text-xs text-orange-600">
<Typography.Text className='text-xs text-orange-600'>
(已在自定义模式中忽略)
</Typography.Text>
)}
......@@ -183,7 +171,7 @@ const SettingsPanel = ({
optionList={models}
style={{ width: '100%' }}
dropdownStyle={{ width: '100%', maxWidth: '100%' }}
className="!rounded-lg"
className='!rounded-lg'
disabled={customRequestMode}
/>
</div>
......@@ -194,7 +182,9 @@ const SettingsPanel = ({
imageUrls={inputs.imageUrls}
imageEnabled={inputs.imageEnabled}
onImageUrlsChange={(urls) => onInputChange('imageUrls', urls)}
onImageEnabledChange={(enabled) => onInputChange('imageEnabled', enabled)}
onImageEnabledChange={(enabled) =>
onInputChange('imageEnabled', enabled)
}
disabled={customRequestMode}
/>
</div>
......@@ -212,14 +202,14 @@ const SettingsPanel = ({
{/* 流式输出开关 */}
<div className={customRequestMode ? 'opacity-50' : ''}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ToggleLeft size={16} className="text-gray-500" />
<Typography.Text strong className="text-sm">
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<ToggleLeft size={16} className='text-gray-500' />
<Typography.Text strong className='text-sm'>
流式输出
</Typography.Text>
{customRequestMode && (
<Typography.Text className="text-xs text-orange-600">
<Typography.Text className='text-xs text-orange-600'>
(已在自定义模式中忽略)
</Typography.Text>
)}
......@@ -227,9 +217,9 @@ const SettingsPanel = ({
<Switch
checked={inputs.stream}
onChange={(checked) => onInputChange('stream', checked)}
checkedText="开"
uncheckedText="关"
size="small"
checkedText='开'
uncheckedText='关'
size='small'
disabled={customRequestMode}
/>
</div>
......@@ -238,7 +228,7 @@ const SettingsPanel = ({
{/* 桌面端的配置管理放在底部 */}
{!styleState.isMobile && (
<div className="flex-shrink-0 pt-3">
<div className='flex-shrink-0 pt-3'>
<ConfigManager
currentConfig={currentConfig}
onConfigImport={onConfigImport}
......
......@@ -28,17 +28,25 @@ const ThinkingContent = ({
finalExtractedThinkingContent,
thinkingSource,
styleState,
onToggleReasoningExpansion
onToggleReasoningExpansion,
}) => {
const { t } = useTranslation();
const scrollRef = useRef(null);
const lastContentRef = useRef('');
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
const isThinkingStatus =
message.status === 'loading' || message.status === 'incomplete';
const headerText =
isThinkingStatus && !message.isThinkingComplete
? t('思考中...')
: t('思考过程');
useEffect(() => {
if (scrollRef.current && finalExtractedThinkingContent && message.isReasoningExpanded) {
if (
scrollRef.current &&
finalExtractedThinkingContent &&
message.isReasoningExpanded
) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [finalExtractedThinkingContent, message.isReasoningExpanded]);
......@@ -63,72 +71,100 @@ const ThinkingContent = ({
}
return (
<div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
<div className='rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm'>
<div
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all"
className='flex items-center justify-between p-3 cursor-pointer hover:bg-gradient-to-r hover:from-white/20 hover:to-purple-50/30 transition-all'
style={{
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
position: 'relative'
background:
'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
position: 'relative',
}}
onClick={() => onToggleReasoningExpansion(message.id)}
>
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
<div className='absolute inset-0 overflow-hidden'>
<div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
<div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
</div>
<div className="flex items-center gap-2 sm:gap-4 relative">
<div className="w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center shadow-lg">
<Brain style={{ color: 'white' }} size={styleState.isMobile ? 12 : 16} />
<div className='flex items-center gap-2 sm:gap-4 relative'>
<div className='w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-white/20 flex items-center justify-center shadow-lg'>
<Brain
style={{ color: 'white' }}
size={styleState.isMobile ? 12 : 16}
/>
</div>
<div className="flex flex-col">
<Typography.Text strong style={{ color: 'white' }} className="text-sm sm:text-base">
<div className='flex flex-col'>
<Typography.Text
strong
style={{ color: 'white' }}
className='text-sm sm:text-base'
>
{headerText}
</Typography.Text>
{thinkingSource && (
<Typography.Text style={{ color: 'white' }} className="text-xs mt-0.5 opacity-80 hidden sm:block">
<Typography.Text
style={{ color: 'white' }}
className='text-xs mt-0.5 opacity-80 hidden sm:block'
>
来源: {thinkingSource}
</Typography.Text>
)}
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 relative">
<div className='flex items-center gap-2 sm:gap-3 relative'>
{isThinkingStatus && !message.isThinkingComplete && (
<div className="flex items-center gap-1 sm:gap-2">
<Loader2 style={{ color: 'white' }} className="animate-spin" size={styleState.isMobile ? 14 : 18} />
<Typography.Text style={{ color: 'white' }} className="text-xs sm:text-sm font-medium opacity-90">
<div className='flex items-center gap-1 sm:gap-2'>
<Loader2
style={{ color: 'white' }}
className='animate-spin'
size={styleState.isMobile ? 14 : 18}
/>
<Typography.Text
style={{ color: 'white' }}
className='text-xs sm:text-sm font-medium opacity-90'
>
思考中
</Typography.Text>
</div>
)}
{(!isThinkingStatus || message.isThinkingComplete) && (
<div className="w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center">
{message.isReasoningExpanded ?
<ChevronUp size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} /> :
<ChevronRight size={styleState.isMobile ? 12 : 16} style={{ color: 'white' }} />
}
<div className='w-5 h-5 sm:w-6 sm:h-6 rounded-full bg-white/20 flex items-center justify-center'>
{message.isReasoningExpanded ? (
<ChevronUp
size={styleState.isMobile ? 12 : 16}
style={{ color: 'white' }}
/>
) : (
<ChevronRight
size={styleState.isMobile ? 12 : 16}
style={{ color: 'white' }}
/>
)}
</div>
)}
</div>
</div>
<div
className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
className={`transition-all duration-500 ease-out ${
message.isReasoningExpanded
? 'max-h-96 opacity-100'
: 'max-h-0 opacity-0'
} overflow-hidden bg-gradient-to-br from-purple-50 via-indigo-50 to-violet-50`}
>
{message.isReasoningExpanded && (
<div className="p-3 sm:p-5 pt-2 sm:pt-4">
<div className='p-3 sm:p-5 pt-2 sm:pt-4'>
<div
ref={scrollRef}
className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll"
className='bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll'
style={{
maxHeight: '200px',
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent'
scrollbarColor: 'rgba(0, 0, 0, 0.3) transparent',
}}
>
<div className="prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm">
<div className='prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm'>
<MarkdownRenderer
content={finalExtractedThinkingContent}
className=""
className=''
animated={isThinkingStatus}
previousContentLength={prevLength}
/>
......
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