重构充值页面和统一控制台页面风格
- 充值页面:全新设计余额概览、预设金额卡片、支付方式、兑换码区域 - 控制台主页:移除灰色背景,统一页面边距和宽度 - API测速页:移至控制台下,调整字体大小和间距 - 侧边栏:新增API测速菜单入口 - 修复个人设置页TwoFASetting组件缺少IconShield导入 - 多个页面统一顶部间距和背景样式
This commit is contained in:
parent
44356571a6
commit
ac1120fc33
|
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
|
||||||
// 数量保持USD,价格从CNY转USD
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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_tokens(Anthropic 必填)
|
max_tokens(Anthropic 必填)
|
||||||
</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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue