重构充值页面和统一控制台页面风格

- 充值页面:全新设计余额概览、预设金额卡片、支付方式、兑换码区域
- 控制台主页:移除灰色背景,统一页面边距和宽度
- API测速页:移至控制台下,调整字体大小和间距
- 侧边栏:新增API测速菜单入口
- 修复个人设置页TwoFASetting组件缺少IconShield导入
- 多个页面统一顶部间距和背景样式
This commit is contained in:
wangxiaoji 2026-07-01 14:13:15 +08:00
parent 44356571a6
commit ac1120fc33
8 changed files with 540 additions and 619 deletions

40
web/src/App.jsx vendored
View File

@ -18,6 +18,38 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { lazy, Suspense, useContext, useMemo } from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, info: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
this.setState({ info });
console.error('ErrorBoundary caught:', error, info);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 40, color: 'red', fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
<h2>页面崩溃: {this.state.error?.message}</h2>
<details>
<summary>Stack</summary>
<pre>{this.state.error?.stack}</pre>
</details>
<details>
<summary>Component Stack</summary>
<pre>{this.state.info?.componentStack}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
import { Route, Routes, useLocation, useParams } from 'react-router-dom';
import Loading from './components/common/ui/Loading';
import User from './pages/User';
@ -352,9 +384,11 @@ function App() {
path='/console/personal'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PersonalSetting />
</Suspense>
<ErrorBoundary>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PersonalSetting />
</Suspense>
</ErrorBoundary>
</PrivateRoute>
}
/>

View File

@ -50,8 +50,7 @@ const Dashboard = () => {
const { unreadCount } = useUserMessageUnreadCount(user);
return (
<div className='min-h-full bg-slate-50 pb-16 text-slate-900 dark:bg-neutral-950 dark:text-white'>
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
<div className='flex flex-col gap-6 pb-8'>
<TopBar
onSearch={() => {}}
onRefresh={dashboard.refresh}
@ -60,7 +59,7 @@ const Dashboard = () => {
unreadCount={unreadCount}
/>
<div className='space-y-6 pt-8 pb-8'>
<div className='space-y-6 pb-8'>
<Hero user={user} />
{/* 第一行5 个核心指标卡 */}
@ -97,7 +96,6 @@ const Dashboard = () => {
<div className='flex items-center justify-center border-t border-slate-200 py-6 text-xs text-slate-400 dark:border-white/10 dark:text-white/30'>
<span>{getSystemName()} · 2026</span>
</div>
</div>
</div>
);
};

View File

@ -52,6 +52,7 @@ const routerMap = {
'model-heat': '/console/model-heat',
playground: '/console/playground',
benchmarks: '/benchmarks',
'api-tester': '/console/api-tester',
personal: '/console/personal',
supplier: null,
distributor: '/console/distributor/admin',
@ -90,6 +91,11 @@ const SiderBar = ({ onNavigate = () => {} }) => {
itemKey: 'benchmarks',
to: '/benchmarks',
},
{
text: t('API 测速'),
itemKey: 'api-tester',
to: '/api-tester',
},
{
text: t('数据看板'),
itemKey: 'detail',

View File

@ -35,6 +35,7 @@ import {
IconAlertTriangle,
IconRefresh,
IconCopy,
IconShield,
} from '@douyinfe/semi-icons';
import { ScanFace } from 'lucide-react';
import React, { useEffect, useState } from 'react';

File diff suppressed because it is too large Load Diff

View File

@ -864,7 +864,7 @@ const TopUp = () => {
};
return (
<div className='mx-auto w-full max-w-[1400px] px-4 pb-16 sm:px-6 lg:px-10'>
<div className='mx-auto w-full max-w-[1400px] px-4 pb-16 pt-20 sm:px-6 lg:px-10'>
{/* 划转模态框 */}
<TransferModal
t={t}

View File

@ -308,16 +308,16 @@ const ApiTester = () => {
};
return (
<div className='min-h-full bg-slate-50 pb-16 text-slate-900 dark:bg-neutral-950 dark:text-white'>
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
<div className='space-y-6 pt-8 pb-8'>
<div className='min-h-full pb-16 text-slate-900 dark:text-white'>
<div className='mx-auto w-full max-w-[1400px] px-4 pt-20 sm:px-6 lg:px-10'>
<div className='space-y-8 pb-8'>
{/* 页面标题区 */}
<div className='flex flex-wrap items-end justify-between gap-4'>
<div>
<h1 className='text-3xl font-bold tracking-tight text-slate-900 md:text-4xl dark:text-white'>
API 快速测试
</h1>
<p className='mt-2 text-sm text-slate-500 dark:text-white/50'>
<p className='mt-2 text-xs text-slate-500 dark:text-white/50'>
当前协议{format === 'anthropic' ? 'Anthropic Messages' : 'OpenAI Chat Completions'} · 请求地址 {finalEndpoint}
</p>
</div>
@ -330,14 +330,14 @@ const ApiTester = () => {
</div>
{/* 第一行API Key / 测试目标 / 模型 */}
<div className='grid grid-cols-1 gap-5 lg:grid-cols-3'>
<div className='grid grid-cols-1 gap-6 lg:grid-cols-3'>
{/* API Key 输入卡片 */}
<div className='rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/10 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='mb-3 flex items-center gap-2'>
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<KeyRound size={15} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>API Key</div>
<div className='text-xs font-semibold text-slate-900 dark:text-white'>API Key</div>
</div>
<div className='relative'>
<input
@ -354,25 +354,25 @@ const ApiTester = () => {
{showKey ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
<div className='mt-2 text-[11px] text-slate-400 dark:text-white/40'>
<div className='mt-2 text-xs text-slate-400 dark:text-white/40'>
{user?.token ? '已自动填充当前登录用户的 Token' : '请填写你的 API Key'}
</div>
</div>
{/* 测试目标选择卡片 */}
<div className='rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/10 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='mb-3 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<Globe size={15} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
<div className='text-xs font-semibold text-slate-900 dark:text-white'>
测试目标
</div>
</div>
<button
onClick={() => setShowCustomBase((v) => !v)}
className='inline-flex items-center gap-1 text-[11px] text-slate-400 transition-colors hover:text-slate-700 dark:text-white/40 dark:hover:text-white'
className='inline-flex items-center gap-1 text-xs text-slate-400 transition-colors hover:text-slate-700 dark:text-white/40 dark:hover:text-white'
title='切换自定义域名'
>
{showCustomBase ? '收起域名' : '换个平台'}
@ -386,10 +386,10 @@ const ApiTester = () => {
return acc;
}, {});
return (
<div className='space-y-2.5'>
<div className='space-y-3'>
{Object.entries(grouped).map(([group, items]) => (
<div key={group}>
<div className='mb-1.5 text-[10px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
<div className='mb-1.5 text-[11px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
{group}
</div>
<div className='flex flex-wrap gap-1.5'>
@ -397,7 +397,7 @@ const ApiTester = () => {
<button
key={s.id}
onClick={() => applyScene(s)}
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-[11px] font-medium text-slate-600 transition hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:border-blue-400 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:border-blue-400 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
>
<s.Icon size={11} /> {s.label}
</button>
@ -408,18 +408,18 @@ const ApiTester = () => {
<div className='border-t border-slate-100 pt-2.5 dark:border-white/5'>
<div className='mb-1.5 flex items-center justify-between'>
<div className='text-[10px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
<div className='text-[11px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
我的目标
</div>
<button
onClick={saveCurrentAsCustom}
className='inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[10px] font-medium text-slate-600 transition hover:border-blue-400 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:text-blue-400'
className='inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-600 transition hover:border-blue-400 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:text-blue-400'
>
<Plus size={10} /> 保存当前
</button>
</div>
{customScenes.length === 0 ? (
<div className='text-[11px] text-slate-400 dark:text-white/40'>
<div className='text-xs text-slate-400 dark:text-white/40'>
调整完地址后点右上保存当前即可收藏到本地
</div>
) : (
@ -431,7 +431,7 @@ const ApiTester = () => {
>
<button
onClick={() => applyScene(s)}
className='inline-flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-medium text-slate-600 transition hover:bg-blue-50 hover:text-blue-600 dark:text-white/70 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
className='inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:bg-blue-50 hover:text-blue-600 dark:text-white/70 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
>
<Bookmark size={11} /> {s.label}
</button>
@ -452,14 +452,14 @@ const ApiTester = () => {
})()}
<div className='mt-3'>
<label className='mb-1.5 block text-[11px] font-medium text-slate-500 dark:text-white/50'>
<label className='mb-1.5 block text-xs font-medium text-slate-500 dark:text-white/50'>
完整请求地址
</label>
<input
value={isAbsoluteEndpoint ? endpointPath : `${endpointBase}${endpointPath}`}
onChange={(e) => applyFullUrl(e.target.value)}
placeholder='https://你的平台域名/v1/chat/completions'
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-[11px] text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
/>
</div>
@ -480,29 +480,29 @@ const ApiTester = () => {
</div>
)}
<div className='mt-3 flex items-center gap-1.5 truncate font-mono text-[11px] text-slate-400 dark:text-white/40'>
<div className='mt-3 flex items-center gap-1.5 truncate font-mono text-xs text-slate-400 dark:text-white/40'>
<span className='shrink-0'>实际请求</span>
<span className='truncate'>{finalEndpoint}</span>
</div>
</div>
{/* 模型选择卡片 */}
<div className='rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/10 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='mb-3 flex items-center gap-2'>
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<Cpu size={15} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>模型</div>
<div className='text-xs font-semibold text-slate-900 dark:text-white'>模型</div>
</div>
<input
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder='例如 gpt-4o-mini / claude-3-5-sonnet ...'
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
/>
{format === 'anthropic' && (
<div className='mt-3'>
<label className='mb-1.5 block text-[11px] font-medium text-slate-500 dark:text-white/50'>
<label className='mb-1.5 block text-xs font-medium text-slate-500 dark:text-white/50'>
max_tokensAnthropic 必填
</label>
<input
@ -511,7 +511,7 @@ const ApiTester = () => {
max={8192}
value={maxTokens}
onChange={(e) => setMaxTokens(Number(e.target.value) || 1)}
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
/>
</div>
)}
@ -519,7 +519,7 @@ const ApiTester = () => {
</div>
{/* 第二行:请求参数 + 响应结果 */}
<div className='grid grid-cols-1 gap-5 lg:grid-cols-2'>
<div className='grid grid-cols-1 gap-6 lg:grid-cols-2'>
{/* 请求参数卡片 */}
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
<div className='mb-4 flex items-center justify-between'>
@ -527,7 +527,7 @@ const ApiTester = () => {
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<MessageSquare size={15} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
<div className='text-xs font-semibold text-slate-900 dark:text-white'>
请求参数
</div>
</div>
@ -547,7 +547,7 @@ const ApiTester = () => {
<div className='space-y-4'>
<div>
<label className='mb-1.5 block text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
<label className='mb-1.5 block text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
{format === 'anthropic' ? 'system顶层字段' : '系统提示'}
</label>
<textarea
@ -555,12 +555,12 @@ const ApiTester = () => {
onChange={(e) => setSystemPrompt(e.target.value)}
rows={2}
placeholder={format === 'anthropic' ? '可选Anthropic 顶层 system 字段' : '(可选)设置模型角色'}
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
/>
</div>
<div>
<label className='mb-1.5 block text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
<label className='mb-1.5 block text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
用户提示 <span className='text-red-500'>*</span>
</label>
<textarea
@ -568,7 +568,7 @@ const ApiTester = () => {
onChange={(e) => setPrompt(e.target.value)}
rows={5}
placeholder='输入你想让模型回答的内容'
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
/>
</div>
</div>
@ -581,12 +581,12 @@ const ApiTester = () => {
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<Zap size={15} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
<div className='text-xs font-semibold text-slate-900 dark:text-white'>
响应结果
</div>
{status && (
<span
className={`ml-2 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-medium ${
className={`ml-2 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${
status.ok
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400'
: 'bg-red-50 text-red-600 dark:bg-red-500/10 dark:text-red-400'
@ -607,7 +607,7 @@ const ApiTester = () => {
navigator.clipboard?.writeText(response);
Toast.success('已复制响应');
}}
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-[11px] font-medium text-slate-600 transition-colors hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:border-white/20 dark:hover:text-white'
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:border-white/20 dark:hover:text-white'
>
<Copy size={11} /> 复制
</button>
@ -621,7 +621,7 @@ const ApiTester = () => {
请求中...
</div>
) : response ? (
<pre className='max-h-[480px] overflow-auto p-4 font-mono text-[11px] leading-relaxed text-slate-800 dark:text-white/80'>
<pre className='max-h-[480px] overflow-auto p-4 font-mono text-xs leading-relaxed text-slate-800 dark:text-white/80'>
{response}
</pre>
) : (

View File

@ -21,8 +21,10 @@ import React from 'react';
import Dashboard from '../../components/dashboard';
const Detail = () => (
<div className='mt-[60px] w-full max-w-[1600px] mx-auto px-3 sm:px-4 lg:px-6 pb-6'>
<Dashboard />
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
<div className='w-full max-w-[1400px] px-4 sm:px-6 lg:px-10 ml-4 lg:ml-10'>
<Dashboard />
</div>
</div>
);