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

- 充值页面:全新设计余额概览、预设金额卡片、支付方式、兑换码区域
- 控制台主页:移除灰色背景,统一页面边距和宽度
- 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

34
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'; 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 { Route, Routes, useLocation, useParams } from 'react-router-dom';
import Loading from './components/common/ui/Loading'; import Loading from './components/common/ui/Loading';
import User from './pages/User'; import User from './pages/User';
@ -352,9 +384,11 @@ function App() {
path='/console/personal' path='/console/personal'
element={ element={
<PrivateRoute> <PrivateRoute>
<ErrorBoundary>
<Suspense fallback={<Loading></Loading>} key={location.pathname}> <Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PersonalSetting /> <PersonalSetting />
</Suspense> </Suspense>
</ErrorBoundary>
</PrivateRoute> </PrivateRoute>
} }
/> />

View File

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

View File

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

View File

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

View File

@ -1,32 +1,27 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Avatar,
Typography, Typography,
Tag, Tag,
Card,
Button, Button,
Banner, Banner,
Skeleton, Skeleton,
Form,
Space,
Row,
Col,
Spin, Spin,
Tooltip, Tooltip,
Tabs, Input,
TabPane, InputNumber,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
import { import {
CreditCard, CreditCard,
Coins, WalletCards,
Wallet, Activity,
BarChart2, ChartColumn,
TrendingUp,
Receipt, Receipt,
Sparkles, Gift,
DollarSign,
Ticket,
ChevronRight,
} from 'lucide-react'; } from 'lucide-react';
import { IconGift } from '@douyinfe/semi-icons';
import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime'; import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime';
import { getCurrencyConfig } from '../../helpers/render'; import { getCurrencyConfig } from '../../helpers/render';
import SubscriptionPlansCard from './SubscriptionPlansCard'; import SubscriptionPlansCard from './SubscriptionPlansCard';
@ -83,186 +78,208 @@ const RechargeCard = ({
allSubscriptions = [], allSubscriptions = [],
reloadSubscriptionSelf, reloadSubscriptionSelf,
}) => { }) => {
const onlineFormApiRef = useRef(null);
const redeemFormApiRef = useRef(null);
const initialTabSetRef = useRef(false);
const showAmountSkeleton = useMinimumLoadingTime(amountLoading);
const [activeTab, setActiveTab] = useState('topup'); const [activeTab, setActiveTab] = useState('topup');
const [customAmount, setCustomAmount] = useState(10);
const showAmountSkeleton = useMinimumLoadingTime(amountLoading);
const shouldShowSubscription = const shouldShowSubscription =
!subscriptionLoading && subscriptionPlans.length > 0; !subscriptionLoading && subscriptionPlans.length > 0;
const statusStr = localStorage.getItem('status');
let usdRate = 7;
try {
if (statusStr) {
const s = JSON.parse(statusStr);
usdRate = s?.usd_exchange_rate || 7;
}
} catch (e) {}
const { symbol, rate, type: currencyType } = getCurrencyConfig();
const calcRmb = (usdAmount) => {
return usdAmount * (priceRatio || 1);
};
const getDiscountForAmount = (val) => {
const preset = presetAmounts.find((p) => p.value === val);
if (preset?.discount) return preset.discount;
if (topupInfo?.discount?.[val]) return topupInfo.discount[val];
return 1.0;
};
const handleCustomAmountChange = (val) => {
const num = Number(val);
if (!num || num <= 0) return;
setCustomAmount(num);
setTopUpCount(num);
setSelectedPreset(null);
getAmount(num);
};
const handlePresetClick = (preset) => {
const discount = getDiscountForAmount(preset.value);
const num = preset.value * discount;
setCustomAmount(preset.value);
if (onPresetCardClick) {
onPresetCardClick(preset);
} else {
selectPresetAmount(preset);
}
};
useEffect(() => { useEffect(() => {
if (initialTabSetRef.current) return;
if (subscriptionLoading) return; if (subscriptionLoading) return;
setActiveTab(shouldShowSubscription ? 'subscription' : 'topup'); if (shouldShowSubscription) {
initialTabSetRef.current = true; setActiveTab('subscription');
}
}, [shouldShowSubscription, subscriptionLoading]); }, [shouldShowSubscription, subscriptionLoading]);
useEffect(() => { const isAnyPayEnabled =
if (!shouldShowSubscription && activeTab !== 'topup') { enableOnlineTopUp ||
setActiveTab('topup');
}
}, [shouldShowSubscription, activeTab]);
const topupContent = (
<Space vertical style={{ width: '100%' }}>
{/* 账户统计 */}
<div className='rounded-3xl border border-slate-200 bg-white p-10 transition-shadow hover:shadow-lg dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/30'>
<div className='mb-8 flex items-center justify-between'>
<div className='flex items-center gap-4'>
<div className='flex h-14 w-14 items-center justify-center rounded-2xl bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'>
<BarChart2 size={24} />
</div>
<div>
<div className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
{t('账户统计')}
</div>
<div className='mt-1 text-xs text-slate-400 tabular-nums dark:text-white/40'>
实时余额与消费概览
</div>
</div>
</div>
</div>
<div className='grid grid-cols-1 gap-8 border-t border-slate-100 pt-8 dark:border-white/5 md:grid-cols-[1.8fr_1fr_1fr] md:items-end'>
{/* 当前余额 —— Hero 数字 */}
<div className='min-w-0'>
<div className='flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400'>
<Wallet size={14} />
{t('当前余额')}
</div>
<div className='mt-3 font-mono text-5xl font-bold leading-none tabular-nums text-emerald-600 md:text-6xl dark:text-emerald-400'>
{renderQuota(userState?.user?.quota)}
</div>
<div className='mt-3 text-xs text-slate-400 dark:text-white/40'>
账户可用额度 · 可继续调用 API
</div>
</div>
{/* 累计消费 */}
<div className='min-w-0 md:border-l md:border-slate-100 md:pl-8 md:dark:border-white/5'>
<div className='flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-white/40'>
<TrendingUp size={14} />
{t('累计消费')}
</div>
<div className='mt-3 font-mono text-4xl font-semibold leading-none tabular-nums text-slate-900 md:text-5xl dark:text-white'>
{renderQuota(userState?.user?.used_quota)}
</div>
<div className='mt-3 text-xs text-slate-400 dark:text-white/40'>
历史总消费
</div>
</div>
{/* 平均单次消费 */}
<div className='min-w-0 md:border-l md:border-slate-100 md:pl-8 md:dark:border-white/5'>
<div className='flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-white/40'>
<Receipt size={14} />
{t('平均单次')}
</div>
<div className='mt-3 font-mono text-4xl font-semibold leading-none tabular-nums text-slate-900 md:text-5xl dark:text-white'>
{userState?.user?.used_quota && userState?.user?.request_count
? renderQuota(
Math.round(
Number(userState.user.used_quota) /
Number(userState.user.request_count),
),
)
: '—'}
</div>
<div className='mt-3 text-xs text-slate-400 dark:text-white/40'>
单次调用平均花费
</div>
</div>
</div>
</div>
{/* 在线充值表单 */}
{statusLoading ? (
<div className='py-8 flex justify-center'>
<Spin size='large' />
</div>
) : enableOnlineTopUp ||
enableStripeTopUp || enableStripeTopUp ||
enableCreemTopUp || enableCreemTopUp ||
enableAlipayTopUp || enableAlipayTopUp ||
enableWechatPayTopUp || enableWechatPayTopUp ||
enableWaffoTopUp ? ( enableWaffoTopUp;
<Form
getFormApi={(api) => (onlineFormApiRef.current = api)} const tabs = [
initValues={{ topUpCount: topUpCount }} { key: 'topup', label: t('充值'), desc: t('灵活付费') },
{ key: 'subscription', label: t('订阅'), desc: t('享更低单价') },
];
//
const statCards = [
{
key: 'balance',
icon: WalletCards,
label: t('当前余额'),
value: userState?.user?.quota,
sublabel: t('剩余配额'),
highlight: true,
},
{
key: 'usage',
icon: ChartColumn,
label: t('总用量'),
value: userState?.user?.used_quota,
sublabel: t('总消耗额度'),
},
{
key: 'requests',
icon: Activity,
label: t('API 请求'),
value: userState?.user?.request_count || 0,
sublabel: t('总请求数'),
raw: true,
},
];
const topupContent = (
<div className='grid gap-5 lg:grid-cols-[1fr_380px] lg:items-start'>
{/* 左侧:充值卡片 */}
<div className='rounded-2xl border border-slate-200 bg-white dark:border-white/10 dark:bg-white/[0.02]'>
{/* 卡片头部 */}
<div className='flex items-center gap-3 border-b border-slate-100 px-5 py-4 dark:border-white/5'>
<div className='flex h-9 w-9 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400'>
<DollarSign className='h-4 w-4' />
</div>
<div>
<div className='text-base font-semibold text-slate-900 dark:text-white'>
{t('账户充值')}
</div>
<div className='text-xs text-slate-400 dark:text-white/40'>
{t('选择金额和支付方式完成充值')}
</div>
</div>
</div>
<div className='space-y-5 p-5'>
{isAnyPayEnabled ? (
<>
{/* 预设额度 */}
<section>
<div className='mb-3 flex items-center justify-between'>
<span className='text-sm font-medium text-slate-700 dark:text-slate-300'>
{t('充值金额')}
</span>
<span className='text-xs text-slate-400 dark:text-white/40'>
USD
</span>
</div>
<div className='grid grid-cols-3 gap-2'>
{presetAmounts.map((preset, index) => {
const discount = getDiscountForAmount(preset.value);
const rmbAmount = calcRmb(preset.value * discount);
const isSelected = selectedPreset === preset.value;
return (
<button
key={index}
type='button'
onClick={() => handlePresetClick(preset)}
className={`group relative flex flex-col items-center rounded-xl border-2 px-3 py-3 transition-all duration-200 ${
isSelected
? 'border-emerald-500 bg-emerald-50/50 dark:border-emerald-400 dark:bg-emerald-500/10'
: 'border-slate-100 bg-white hover:border-slate-300 hover:shadow-sm dark:border-white/5 dark:bg-transparent dark:hover:border-white/20'
}`}
> >
<div className='space-y-6'> {discount < 1.0 && (
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp || enableAlipayTopUp || enableWechatPayTopUp) && ( <span className='absolute -top-2 right-2 rounded-full bg-emerald-500 px-2 py-0.5 text-[10px] font-bold text-white'>
<Row gutter={12}> {(discount * 10).toFixed(1)}{t('折')}
<Col xs={24} sm={24} md={24} lg={10} xl={10}> </span>
<Form.InputNumber )}
field='topUpCount' <span className={`text-xl font-bold tracking-tight ${isSelected ? 'text-emerald-700 dark:text-emerald-400' : 'text-slate-900 dark:text-white'}`}>
label={t('充值数量')} ${formatLargeNumber(preset.value)}
disabled={ </span>
!enableOnlineTopUp && <span className='mt-1 text-xs text-slate-400 dark:text-white/40'>
!enableStripeTopUp && ¥{rmbAmount.toFixed(0)}
!enableWaffoTopUp && </span>
!enableAlipayTopUp && </button>
!enableWechatPayTopUp );
} })}
placeholder={ </div>
t('充值数量,最低 ') + renderTopUpCount(minTopUp) </section>
}
value={topUpCount} {/* 自定义金额 */}
<section>
<div className='mb-3'>
<span className='text-sm font-medium text-slate-700 dark:text-slate-300'>
{t('自定义金额')}
</span>
</div>
<div className='flex items-center gap-3'>
<div className='relative flex-1'>
<span className='pointer-events-none absolute inset-y-0 left-3 flex items-center text-sm font-medium text-slate-400'>
$
</span>
<InputNumber
min={minTopUp} min={minTopUp}
max={999999999} max={999999999}
step={1} value={customAmount}
precision={0} onChange={handleCustomAmountChange}
onChange={async (value) => { className='!h-10 !rounded-xl !pl-7 !text-base'
if (value && value >= 1) {
setTopUpCount(value);
setSelectedPreset(null);
await getAmount(value);
}
}}
onBlur={(e) => {
const value = parseInt(e.target.value);
if (!value || value < 1) {
setTopUpCount(1);
getAmount(1);
}
}}
formatter={(value) => (value ? `${value}` : '')}
parser={(value) =>
value ? parseInt(value.replace(/[^\d]/g, '')) : 0
}
extraText={
<Skeleton
loading={showAmountSkeleton}
active
placeholder={
<Skeleton.Title
style={{
width: 120,
height: 20,
borderRadius: 6,
}}
/>
}
>
<Text type='secondary' className='text-red-600'>
{t('实付金额:')}
<span style={{ color: 'red' }}>
{renderAmount()}
</span>
</Text>
</Skeleton>
}
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
</Col> </div>
{payMethods && <div className='flex h-10 items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-4 dark:border-white/10 dark:bg-white/5'>
payMethods.filter((m) => m.type !== 'waffo' && m.type !== 'custom1').length > 0 && ( <span className='text-xs text-slate-400'>{t('应付')}</span>
<Col xs={24} sm={24} md={24} lg={14} xl={14}> <span className='text-sm font-semibold text-slate-700 dark:text-white'>
<Form.Slot label={t('选择支付方式')}> ¥{calcRmb(customAmount * getDiscountForAmount(customAmount)).toFixed(0)}
<Space wrap> </span>
</div>
</div>
</section>
{/* 支付方式 */}
<section>
<div className='mb-3'>
<span className='text-sm font-medium text-slate-700 dark:text-slate-300'>
{t('支付方式')}
</span>
</div>
<div className='flex flex-wrap gap-2'>
{payMethods {payMethods
.filter((m) => m.type !== 'waffo' && m.type !== 'custom1') .filter((m) => m.type !== 'waffo')
.map((payMethod) => { .map((payMethod) => {
const minTopupVal = const minTopupVal = Number(payMethod.min_topup) || 0;
Number(payMethod.min_topup) || 0;
const isStripe = payMethod.type === 'stripe'; const isStripe = payMethod.type === 'stripe';
const isAlipay = payMethod.type === 'alipay'; const isAlipay = payMethod.type === 'alipay';
const isWechat = payMethod.type === 'wxpay'; const isWechat = payMethod.type === 'wxpay';
@ -273,200 +290,50 @@ const RechargeCard = ({
(!enableWechatPayTopUp && isWechat) || (!enableWechatPayTopUp && isWechat) ||
minTopupVal > Number(topUpCount || 0); minTopupVal > Number(topUpCount || 0);
const buttonEl = ( const getIcon = () => {
if (isAlipay)
return <SiAlipay size={18} color='#1677FF' />;
if (isWechat)
return <SiWechat size={18} color='#07C160' />;
if (isStripe)
return <SiStripe size={18} color='#635BFF' />;
return <CreditCard size={18} />;
};
const btn = (
<Button <Button
key={payMethod.type} key={payMethod.type}
theme={payWay === payMethod.type ? 'solid' : 'outline'} theme='outline'
type={payWay === payMethod.type ? 'primary' : 'tertiary'} type='tertiary'
onClick={() => preTopUp(payMethod.type)} onClick={() => preTopUp(payMethod.type)}
disabled={disabled} disabled={disabled}
loading={ loading={paymentLoading && payWay === payMethod.type}
paymentLoading && icon={getIcon()}
payWay === payMethod.type className='!h-10 !rounded-xl !px-5 !text-sm'
}
icon={
payMethod.type === 'alipay' ? (
<SiAlipay size={18} color={payWay === payMethod.type ? 'white' : '#1677FF'} />
) : payMethod.type === 'wxpay' ? (
<SiWechat size={18} color={payWay === payMethod.type ? 'white' : '#07C160'} />
) : payMethod.type === 'stripe' ? (
<SiStripe size={18} color={payWay === payMethod.type ? 'white' : '#635BFF'} />
) : (
<CreditCard
size={18}
color={
payMethod.color ||
'var(--semi-color-text-2)'
}
/>
)
}
className='!rounded-lg !px-4 !py-2'
> >
{payMethod.name} {payMethod.name}
</Button> </Button>
); );
return disabled && if (disabled && minTopupVal > Number(topUpCount || 0)) {
minTopupVal > Number(topUpCount || 0) ? ( return (
<Tooltip <Tooltip
content={ content={t('此支付方式最低充值金额为') + ' ' + minTopupVal}
t('此支付方式最低充值金额为') +
' ' +
minTopupVal
}
key={payMethod.type} key={payMethod.type}
> >
{buttonEl} {btn}
</Tooltip> </Tooltip>
) : (
<React.Fragment key={payMethod.type}>
{buttonEl}
</React.Fragment>
); );
}
return <React.Fragment key={payMethod.type}>{btn}</React.Fragment>;
})} })}
</Space>
</Form.Slot>
</Col>
)}
</Row>
)}
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp || enableAlipayTopUp || enableWechatPayTopUp) && (
<Form.Slot
label={
<div className='flex items-center gap-2'>
<span>{t('选择充值额度')}</span>
{(() => {
const { symbol, rate, type } = getCurrencyConfig();
if (type === 'USD') return null;
return (
<span
style={{
color: 'var(--semi-color-text-2)',
fontSize: '12px',
fontWeight: 'normal',
}}
>
(1 USD = {rate.toFixed(2)} {symbol})
</span>
);
})()}
</div>
}
>
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'>
{presetAmounts.map((preset, index) => {
const discount =
preset.discount ||
topupInfo?.discount?.[preset.value] ||
1.0;
const originalPrice = preset.value * priceRatio;
const discountedPrice = originalPrice * discount;
const hasDiscount = discount < 1.0;
const actualPay = discountedPrice;
const save = originalPrice - discountedPrice;
//
const { symbol, rate, type } = getCurrencyConfig();
const statusStr = localStorage.getItem('status');
let usdRate = 7; // CNY
try {
if (statusStr) {
const s = JSON.parse(statusStr);
usdRate = s?.usd_exchange_rate || 7;
}
} catch (e) {}
//
let displayValue = preset.value;
let displayActualPay = actualPay;
let displaySave = save;
if (type === 'USD') {
// USDCNYUSD
displayActualPay = actualPay / usdRate;
displaySave = save / usdRate;
} else if (type === 'CUSTOM') {
//
displayActualPay = (actualPay / usdRate) * rate;
displaySave = (save / usdRate) * rate;
}
return (
<div
key={index}
className='preset-amount-card'
onClick={() => {
onlineFormApiRef.current?.setValue(
'topUpCount',
preset.value,
);
if (onPresetCardClick) {
onPresetCardClick(preset);
} else {
selectPresetAmount(preset);
}
}}
>
<Card
className='preset-amount-card__inner'
bodyStyle={{ padding: '12px' }}
>
<div style={{ textAlign: 'center' }}>
<Typography.Title
heading={6}
style={{ margin: '0 0 8px 0' }}
>
<Coins size={18} />
{formatLargeNumber(displayValue)}
{hasDiscount && (
<Tag style={{ marginLeft: 4 }} color='green'>
{t('折').includes('off')
? (
(1 - parseFloat(discount)) *
100
).toFixed(1)
: (discount * 10).toFixed(1)}
{t('折')}
</Tag>
)}
</Typography.Title>
<div
style={{
color: 'var(--semi-color-text-2)',
fontSize: '12px',
margin: '4px 0',
}}
>
{t('实付')} {symbol}
{displayActualPay.toFixed(2)}
{hasDiscount && displaySave > 0.005 && (
<>
{t('节省')} {symbol}
{displaySave.toFixed(2)}
</>
)}
</div>
</div>
</Card>
</div>
);
})}
</div>
</Form.Slot>
)}
{/* Waffo 充值区域 */}
{enableWaffoTopUp && {enableWaffoTopUp &&
waffoPayMethods && waffoPayMethods &&
waffoPayMethods.length > 0 && ( waffoPayMethods.length > 0 &&
<Form.Slot label={t('Waffo 充值')}> waffoPayMethods.map((method, index) => (
<Space wrap>
{waffoPayMethods.map((method, index) => (
<Button <Button
key={index} key={`waffo-${index}`}
theme='outline' theme='outline'
type='tertiary' type='tertiary'
onClick={() => waffoTopUp(index)} onClick={() => waffoTopUp(index)}
@ -476,165 +343,192 @@ const RechargeCard = ({
<img <img
src={method.icon} src={method.icon}
alt={method.name} alt={method.name}
style={{ style={{ width: 18, height: 18, objectFit: 'contain' }}
width: 36,
height: 36,
objectFit: 'contain',
}}
/> />
) : ( ) : (
<CreditCard <CreditCard size={18} />
size={18}
color='var(--semi-color-text-2)'
/>
) )
} }
className='!rounded-lg !px-4 !py-2' className='!h-10 !rounded-xl !px-5 !text-sm'
> >
{method.name} {method.name}
</Button> </Button>
))} ))}
</Space>
</Form.Slot>
)}
{/* Creem 充值区域 */} {enableCreemTopUp &&
{enableCreemTopUp && creemProducts.length > 0 && ( creemProducts.length > 0 &&
<Form.Slot label={t('Creem 充值')}> creemProducts.map((product, index) => (
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3'> <Button
{creemProducts.map((product, index) => ( key={`creem-${index}`}
<Card theme='outline'
key={index} type='tertiary'
onClick={() => creemPreTopUp(product)} onClick={() => creemPreTopUp(product)}
className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300' className='!h-10 !rounded-xl !px-5 !text-sm'
bodyStyle={{ textAlign: 'center', padding: '16px' }}
> >
<div className='font-medium text-lg mb-2'> {product.name} - {product.currency === 'EUR' ? '€' : '$'}
{product.name}
</div>
<div className='text-sm text-gray-600 mb-2'>
{t('充值额度')}: {product.quota}
</div>
<div className='text-lg font-semibold text-blue-600'>
{product.currency === 'EUR' ? '€' : 'USD'}
{product.price} {product.price}
</div> </Button>
</Card>
))} ))}
</div> </div>
</Form.Slot> </section>
)} </>
</div>
</Form>
) : ( ) : (
<Banner <Banner
type='info' type='info'
description={t( description={t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
'管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。',
)}
className='!rounded-xl' className='!rounded-xl'
closeIcon={null} closeIcon={null}
/> />
)} )}
</div>
</div>
{/* 兑换码充值 */} {/* 右侧:兑换码 + 账单入口 */}
<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='space-y-4'>
<div className='mb-4 flex items-center gap-3'> <div className='rounded-2xl border border-slate-200 bg-white dark:border-white/10 dark:bg-white/[0.02]'>
<div className='flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400'> <div className='flex items-center gap-3 border-b border-slate-100 px-5 py-4 dark:border-white/5'>
<IconGift size={18} /> <div className='flex h-9 w-9 items-center justify-center rounded-xl bg-amber-50 text-amber-600 dark:bg-amber-500/10 dark:text-amber-400'>
<Gift className='h-4 w-4' />
</div> </div>
<div> <div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'> <div className='text-base font-semibold text-slate-900 dark:text-white'>
{t('兑换码充值')} {t('兑换码')}
</div> </div>
<div className='mt-0.5 text-[11px] text-slate-400 dark:text-white/40'> <div className='text-xs text-slate-400 dark:text-white/40'>
{t('输入兑换码直接到账,无需在线支付')} {t('输入兑换码获取额度')}
</div> </div>
</div> </div>
</div> </div>
<Form <div className='space-y-3 p-5'>
getFormApi={(api) => (redeemFormApiRef.current = api)} <Input
initValues={{ redemptionCode: redemptionCode }}
>
<Form.Input
field='redemptionCode'
noLabel={true}
placeholder={t('请输入兑换码')} placeholder={t('请输入兑换码')}
value={redemptionCode} value={redemptionCode}
onChange={(value) => setRedemptionCode(value)} onChange={setRedemptionCode}
prefix={<IconGift />} className='!h-10 !rounded-xl'
suffix={ />
<div className='flex items-center gap-2'>
<Button <Button
type='primary' type='primary'
theme='solid' theme='solid'
onClick={topUp} onClick={topUp}
loading={isSubmitting} loading={isSubmitting}
disabled={!redemptionCode}
className='!h-10 !w-full !rounded-xl !bg-emerald-600 hover:!bg-emerald-700'
> >
{t('兑换额度')} {t('立即兑换')}
</Button> </Button>
</div> {topUpLink && (
} <button
showClear
style={{ width: '100%' }}
extraText={
topUpLink && (
<Text type='tertiary'>
{t('在找兑换码?')}
<Text
type='secondary'
underline
className='cursor-pointer'
onClick={openTopUpLink} onClick={openTopUpLink}
className='flex w-full items-center justify-center gap-1 text-xs text-slate-400 transition-colors hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70'
> >
<Ticket className='h-3 w-3' />
{t('购买兑换码')} {t('购买兑换码')}
</Text> <ChevronRight className='h-3 w-3' />
</Text> </button>
) )}
} </div>
/> </div>
</Form>
{/* 账单入口 */}
<button
onClick={onOpenHistory}
className='flex w-full items-center justify-between rounded-2xl border border-slate-200 bg-white px-5 py-4 transition-all hover:border-slate-300 hover:shadow-sm dark:border-white/10 dark:bg-white/[0.02] dark:hover:border-white/20'
>
<div className='flex items-center gap-3'>
<div className='flex h-9 w-9 items-center justify-center rounded-xl bg-slate-50 text-slate-500 dark:bg-white/5 dark:text-white/60'>
<Receipt className='h-4 w-4' />
</div>
<div className='text-left'>
<div className='text-sm font-medium text-slate-700 dark:text-slate-300'>
{t('充值记录')}
</div>
<div className='text-xs text-slate-400 dark:text-white/40'>
{t('查看历史账单明细')}
</div>
</div>
</div>
<ChevronRight className='h-4 w-4 text-slate-300 dark:text-white/20' />
</button>
</div>
</div> </div>
</Space>
); );
return ( return (
<Card className='!rounded-2xl shadow-sm border-0'> <div className='flex w-full flex-col gap-5'>
{/* 卡片头部 */} {/* 统计卡片 */}
<div className='mb-6 flex flex-wrap items-center justify-between gap-3'> <div className='grid grid-cols-3 gap-0 overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-white/10 dark:bg-white/[0.02]'>
<div className='flex items-center gap-3'> {statCards.map((card, i) => (
<div className='flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'> <div
<CreditCard size={18} /> key={card.key}
</div> className={`relative flex flex-col px-5 py-4 ${
<div> i < 2 ? 'border-r border-slate-100 dark:border-white/5' : ''
<div className='text-base font-semibold tracking-tight text-slate-900 dark:text-white'> }`}
{t('账户充值')}
</div>
<div className='mt-0.5 text-[11px] text-slate-400 dark:text-white/40'>
{t('多种充值方式,安全便捷')}
</div>
</div>
</div>
<button
onClick={onOpenHistory}
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-4 py-2 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/70 dark:hover:border-white/20 dark:hover:text-white'
> >
<Receipt size={13} /> {t('账单')} <div className='flex items-center gap-2'>
</button> <card.icon
className={`h-3.5 w-3.5 ${
card.highlight
? 'text-emerald-500 dark:text-emerald-400'
: 'text-slate-400 dark:text-white/40'
}`}
/>
<span className='text-xs font-medium text-slate-400 dark:text-white/40'>
{card.label}
</span>
</div>
<div
className={`mt-2 font-mono text-2xl font-bold tabular-nums ${
card.highlight
? 'text-emerald-600 dark:text-emerald-400'
: 'text-slate-900 dark:text-white'
}`}
>
{statusLoading ? (
<Skeleton.Title style={{ width: 60, height: 28, margin: 0 }} />
) : card.raw ? (
card.value
) : (
renderQuota(card.value)
)}
</div>
<div className='mt-1 text-xs text-slate-400 dark:text-white/40'>
{card.sublabel}
</div>
</div>
))}
</div> </div>
{shouldShowSubscription ? ( {/* 标签切换 */}
<Tabs type='card' activeKey={activeTab} onChange={setActiveTab}> {shouldShowSubscription && (
<TabPane <div className='flex justify-start'>
tab={ <div className='inline-flex items-center rounded-xl bg-slate-100 p-1 dark:bg-white/5'>
<div className='flex items-center gap-2'> {tabs.map((tab) => (
<Sparkles size={16} /> <button
{t('订阅套餐')} key={tab.key}
</div> type='button'
} onClick={() => setActiveTab(tab.key)}
itemKey='subscription' className={`flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-all duration-200 ${
activeTab === tab.key
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white'
: 'text-slate-500 hover:text-slate-700 dark:text-white/40 dark:hover:text-white/70'
}`}
> >
<div className='py-2'> {tab.label}
<span className='text-xs font-normal text-slate-400 dark:text-white/40'>
{tab.desc}
</span>
</button>
))}
</div>
</div>
)}
{/* 主体内容 */}
{statusLoading ? (
<div className='flex items-center justify-center py-16'>
<Spin size='large' />
</div>
) : shouldShowSubscription && activeTab === 'subscription' ? (
<SubscriptionPlansCard <SubscriptionPlansCard
t={t} t={t}
loading={subscriptionLoading} loading={subscriptionLoading}
@ -650,24 +544,10 @@ const RechargeCard = ({
reloadSubscriptionSelf={reloadSubscriptionSelf} reloadSubscriptionSelf={reloadSubscriptionSelf}
withCard={false} withCard={false}
/> />
</div>
</TabPane>
<TabPane
tab={
<div className='flex items-center gap-2'>
<Wallet size={16} />
{t('额度充值')}
</div>
}
itemKey='topup'
>
<div className='py-2'>{topupContent}</div>
</TabPane>
</Tabs>
) : ( ) : (
topupContent topupContent
)} )}
</Card> </div>
); );
}; };

View File

@ -864,7 +864,7 @@ const TopUp = () => {
}; };
return ( 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 <TransferModal
t={t} t={t}

View File

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

View File

@ -21,9 +21,11 @@ import React from 'react';
import Dashboard from '../../components/dashboard'; import Dashboard from '../../components/dashboard';
const Detail = () => ( const Detail = () => (
<div className='mt-[60px] w-full max-w-[1600px] mx-auto px-3 sm:px-4 lg:px-6 pb-6'> <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 /> <Dashboard />
</div> </div>
</div>
); );
export default Detail; export default Detail;