diff --git a/web/src/App.jsx b/web/src/App.jsx index b57da89..8743d54 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -18,6 +18,38 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { lazy, Suspense, useContext, useMemo } from 'react'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, info: null }; + } + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + componentDidCatch(error, info) { + this.setState({ info }); + console.error('ErrorBoundary caught:', error, info); + } + render() { + if (this.state.hasError) { + return ( +
+

页面崩溃: {this.state.error?.message}

+
+ Stack +
{this.state.error?.stack}
+
+
+ Component Stack +
{this.state.info?.componentStack}
+
+
+ ); + } + return this.props.children; + } +} import { Route, Routes, useLocation, useParams } from 'react-router-dom'; import Loading from './components/common/ui/Loading'; import User from './pages/User'; @@ -352,9 +384,11 @@ function App() { path='/console/personal' element={ - } key={location.pathname}> - - + + } key={location.pathname}> + + + } /> diff --git a/web/src/components/dashboard/index.jsx b/web/src/components/dashboard/index.jsx index c897bd0..5ce3d4d 100644 --- a/web/src/components/dashboard/index.jsx +++ b/web/src/components/dashboard/index.jsx @@ -50,8 +50,7 @@ const Dashboard = () => { const { unreadCount } = useUserMessageUnreadCount(user); return ( -
-
+
{}} onRefresh={dashboard.refresh} @@ -60,7 +59,7 @@ const Dashboard = () => { unreadCount={unreadCount} /> -
+
{/* 第一行:5 个核心指标卡 */} @@ -97,7 +96,6 @@ const Dashboard = () => {
{getSystemName()} · 2026
-
); }; diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx index de95f51..f5b7132 100644 --- a/web/src/components/layout/SiderBar.jsx +++ b/web/src/components/layout/SiderBar.jsx @@ -52,6 +52,7 @@ const routerMap = { 'model-heat': '/console/model-heat', playground: '/console/playground', benchmarks: '/benchmarks', + 'api-tester': '/console/api-tester', personal: '/console/personal', supplier: null, distributor: '/console/distributor/admin', @@ -90,6 +91,11 @@ const SiderBar = ({ onNavigate = () => {} }) => { itemKey: 'benchmarks', to: '/benchmarks', }, + { + text: t('API 测速'), + itemKey: 'api-tester', + to: '/api-tester', + }, { text: t('数据看板'), itemKey: 'detail', diff --git a/web/src/components/settings/personal/components/TwoFASetting.jsx b/web/src/components/settings/personal/components/TwoFASetting.jsx index e0fb09f..d2c66cc 100644 --- a/web/src/components/settings/personal/components/TwoFASetting.jsx +++ b/web/src/components/settings/personal/components/TwoFASetting.jsx @@ -35,6 +35,7 @@ import { IconAlertTriangle, IconRefresh, IconCopy, + IconShield, } from '@douyinfe/semi-icons'; import { ScanFace } from 'lucide-react'; import React, { useEffect, useState } from 'react'; diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index f728cc4..05e1743 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/web/src/components/topup/RechargeCard.jsx @@ -1,32 +1,27 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { - Avatar, Typography, Tag, - Card, Button, Banner, Skeleton, - Form, - Space, - Row, - Col, Spin, Tooltip, - Tabs, - TabPane, + Input, + InputNumber, } from '@douyinfe/semi-ui'; import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; import { CreditCard, - Coins, - Wallet, - BarChart2, - TrendingUp, + WalletCards, + Activity, + ChartColumn, Receipt, - Sparkles, + Gift, + DollarSign, + Ticket, + ChevronRight, } from 'lucide-react'; -import { IconGift } from '@douyinfe/semi-icons'; import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime'; import { getCurrencyConfig } from '../../helpers/render'; import SubscriptionPlansCard from './SubscriptionPlansCard'; @@ -83,591 +78,476 @@ const RechargeCard = ({ allSubscriptions = [], reloadSubscriptionSelf, }) => { - const onlineFormApiRef = useRef(null); - const redeemFormApiRef = useRef(null); - const initialTabSetRef = useRef(false); - const showAmountSkeleton = useMinimumLoadingTime(amountLoading); const [activeTab, setActiveTab] = useState('topup'); + const [customAmount, setCustomAmount] = useState(10); + const showAmountSkeleton = useMinimumLoadingTime(amountLoading); const shouldShowSubscription = !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(() => { - if (initialTabSetRef.current) return; if (subscriptionLoading) return; - setActiveTab(shouldShowSubscription ? 'subscription' : 'topup'); - initialTabSetRef.current = true; + if (shouldShowSubscription) { + setActiveTab('subscription'); + } }, [shouldShowSubscription, subscriptionLoading]); - useEffect(() => { - if (!shouldShowSubscription && activeTab !== 'topup') { - setActiveTab('topup'); - } - }, [shouldShowSubscription, activeTab]); + const isAnyPayEnabled = + enableOnlineTopUp || + enableStripeTopUp || + enableCreemTopUp || + enableAlipayTopUp || + enableWechatPayTopUp || + enableWaffoTopUp; + + const tabs = [ + { 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 = ( - - {/* 账户统计 */} -
-
-
-
- -
-
-
- {t('账户统计')} -
-
- 实时余额与消费概览 -
-
-
-
-
- {/* 当前余额 —— Hero 数字 */} -
-
- - {t('当前余额')} -
-
- {renderQuota(userState?.user?.quota)} -
-
- 账户可用额度 · 可继续调用 API -
-
- - {/* 累计消费 */} -
-
- - {t('累计消费')} -
-
- {renderQuota(userState?.user?.used_quota)} -
-
- 历史总消费 -
-
- - {/* 平均单次消费 */} -
-
- - {t('平均单次')} -
-
- {userState?.user?.used_quota && userState?.user?.request_count - ? renderQuota( - Math.round( - Number(userState.user.used_quota) / - Number(userState.user.request_count), - ), - ) - : '—'} -
-
- 单次调用平均花费 -
-
-
-
- {/* 在线充值表单 */} - {statusLoading ? ( -
- -
- ) : enableOnlineTopUp || - enableStripeTopUp || - enableCreemTopUp || - enableAlipayTopUp || - enableWechatPayTopUp || - enableWaffoTopUp ? ( -
(onlineFormApiRef.current = api)} - initValues={{ topUpCount: topUpCount }} - > -
- {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp || enableAlipayTopUp || enableWechatPayTopUp) && ( - - - { - 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={ - - } - > - - {t('实付金额:')} - - {renderAmount()} - - - - } - style={{ width: '100%' }} - /> - - {payMethods && - payMethods.filter((m) => m.type !== 'waffo' && m.type !== 'custom1').length > 0 && ( - - - - {payMethods - .filter((m) => m.type !== 'waffo' && m.type !== 'custom1') - .map((payMethod) => { - const minTopupVal = - Number(payMethod.min_topup) || 0; - const isStripe = payMethod.type === 'stripe'; - const isAlipay = payMethod.type === 'alipay'; - const isWechat = payMethod.type === 'wxpay'; - const disabled = - (!enableOnlineTopUp && !isStripe && !isAlipay && !isWechat) || - (!enableStripeTopUp && isStripe) || - (!enableAlipayTopUp && isAlipay) || - (!enableWechatPayTopUp && isWechat) || - minTopupVal > Number(topUpCount || 0); - - const buttonEl = ( - - ); - - return disabled && - minTopupVal > Number(topUpCount || 0) ? ( - - {buttonEl} - - ) : ( - - {buttonEl} - - ); - })} - - - - )} - - )} - - {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp || enableAlipayTopUp || enableWechatPayTopUp) && ( - - {t('选择充值额度')} - {(() => { - const { symbol, rate, type } = getCurrencyConfig(); - if (type === 'USD') return null; - - return ( - - (1 USD = {rate.toFixed(2)} {symbol}) - - ); - })()} -
- } - > -
- {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 ( -
{ - onlineFormApiRef.current?.setValue( - 'topUpCount', - preset.value, - ); - if (onPresetCardClick) { - onPresetCardClick(preset); - } else { - selectPresetAmount(preset); - } - }} - > - -
- - - {formatLargeNumber(displayValue)} - {hasDiscount && ( - - {t('折').includes('off') - ? ( - (1 - parseFloat(discount)) * - 100 - ).toFixed(1) - : (discount * 10).toFixed(1)} - {t('折')} - - )} - -
- {t('实付')} {symbol} - {displayActualPay.toFixed(2)} - {hasDiscount && displaySave > 0.005 && ( - <> - ,{t('节省')} {symbol} - {displaySave.toFixed(2)} - - )} -
-
-
-
- ); - })} -
- - )} - - {/* Waffo 充值区域 */} - {enableWaffoTopUp && - waffoPayMethods && - waffoPayMethods.length > 0 && ( - - - {waffoPayMethods.map((method, index) => ( - - ))} - - - )} - - {/* Creem 充值区域 */} - {enableCreemTopUp && creemProducts.length > 0 && ( - -
- {creemProducts.map((product, index) => ( - creemPreTopUp(product)} - className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300' - bodyStyle={{ textAlign: 'center', padding: '16px' }} - > -
- {product.name} -
-
- {t('充值额度')}: {product.quota} -
-
- {product.currency === 'EUR' ? '€' : 'USD'} - {product.price} -
-
- ))} -
-
- )} -
- - ) : ( - - )} - - {/* 兑换码充值 */} -
-
-
- +
+ {/* 左侧:充值卡片 */} +
+ {/* 卡片头部 */} +
+
+
-
- {t('兑换码充值')} +
+ {t('账户充值')}
-
- {t('输入兑换码直接到账,无需在线支付')} +
+ {t('选择金额和支付方式完成充值')}
-
(redeemFormApiRef.current = api)} - initValues={{ redemptionCode: redemptionCode }} - > - setRedemptionCode(value)} - prefix={} - suffix={ -
- -
- } - showClear - style={{ width: '100%' }} - extraText={ - topUpLink && ( - - {t('在找兑换码?')} - - {t('购买兑换码')} - - - ) - } - /> - + +
+ {isAnyPayEnabled ? ( + <> + {/* 预设额度 */} +
+
+ + {t('充值金额')} + + + USD + +
+
+ {presetAmounts.map((preset, index) => { + const discount = getDiscountForAmount(preset.value); + const rmbAmount = calcRmb(preset.value * discount); + const isSelected = selectedPreset === preset.value; + return ( + + ); + })} +
+
+ + {/* 自定义金额 */} +
+
+ + {t('自定义金额')} + +
+
+
+ + $ + + +
+
+ {t('应付')} + + ¥{calcRmb(customAmount * getDiscountForAmount(customAmount)).toFixed(0)} + +
+
+
+ + {/* 支付方式 */} +
+
+ + {t('支付方式')} + +
+
+ {payMethods + .filter((m) => m.type !== 'waffo') + .map((payMethod) => { + const minTopupVal = Number(payMethod.min_topup) || 0; + const isStripe = payMethod.type === 'stripe'; + const isAlipay = payMethod.type === 'alipay'; + const isWechat = payMethod.type === 'wxpay'; + const disabled = + (!enableOnlineTopUp && !isStripe && !isAlipay && !isWechat) || + (!enableStripeTopUp && isStripe) || + (!enableAlipayTopUp && isAlipay) || + (!enableWechatPayTopUp && isWechat) || + minTopupVal > Number(topUpCount || 0); + + const getIcon = () => { + if (isAlipay) + return ; + if (isWechat) + return ; + if (isStripe) + return ; + return ; + }; + + const btn = ( + + ); + + if (disabled && minTopupVal > Number(topUpCount || 0)) { + return ( + + {btn} + + ); + } + return {btn}; + })} + + {enableWaffoTopUp && + waffoPayMethods && + waffoPayMethods.length > 0 && + waffoPayMethods.map((method, index) => ( + + ))} + + {enableCreemTopUp && + creemProducts.length > 0 && + creemProducts.map((product, index) => ( + + ))} +
+
+ + ) : ( + + )} +
- + + {/* 右侧:兑换码 + 账单入口 */} +
+
+
+
+ +
+
+
+ {t('兑换码')} +
+
+ {t('输入兑换码获取额度')} +
+
+
+
+ + + {topUpLink && ( + + )} +
+
+ + {/* 账单入口 */} + +
+
); return ( - - {/* 卡片头部 */} -
-
-
- -
-
-
- {t('账户充值')} +
+ {/* 统计卡片 */} +
+ {statCards.map((card, i) => ( +
+
+ + + {card.label} +
-
- {t('多种充值方式,安全便捷')} +
+ {statusLoading ? ( + + ) : card.raw ? ( + card.value + ) : ( + renderQuota(card.value) + )} +
+
+ {card.sublabel}
-
- + ))}
- {shouldShowSubscription ? ( - - - - {t('订阅套餐')} -
- } - itemKey='subscription' - > -
- -
- - - - {t('额度充值')} -
- } - itemKey='topup' - > -
{topupContent}
- - + {/* 标签切换 */} + {shouldShowSubscription && ( +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ )} + + {/* 主体内容 */} + {statusLoading ? ( +
+ +
+ ) : shouldShowSubscription && activeTab === 'subscription' ? ( + ) : ( topupContent )} - +
); }; diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index ebe02de..6373e7b 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -864,7 +864,7 @@ const TopUp = () => { }; return ( -
+
{/* 划转模态框 */} { }; return ( -
-
-
+
+
+
{/* 页面标题区 */}

API 快速测试

-

+

当前协议:{format === 'anthropic' ? 'Anthropic Messages' : 'OpenAI Chat Completions'} · 请求地址 {finalEndpoint}

@@ -330,14 +330,14 @@ const ApiTester = () => {
{/* 第一行:API Key / 测试目标 / 模型 */} -
+
{/* API Key 输入卡片 */} -
+
-
API Key
+
API Key
{ {showKey ? : }
-
+
{user?.token ? '已自动填充当前登录用户的 Token' : '请填写你的 API Key'}
{/* 测试目标选择卡片 */} -
+
-
+
测试目标
@@ -408,18 +408,18 @@ const ApiTester = () => {
-
+
我的目标
{customScenes.length === 0 ? ( -
+
调整完地址后,点右上「保存当前」即可收藏到本地
) : ( @@ -431,7 +431,7 @@ const ApiTester = () => { > @@ -452,14 +452,14 @@ const ApiTester = () => { })()}
-
@@ -480,29 +480,29 @@ const ApiTester = () => {
)} -
+
实际请求: {finalEndpoint}
{/* 模型选择卡片 */} -
+
-
模型
+
模型
setModel(e.target.value)} placeholder='例如 gpt-4o-mini / claude-3-5-sonnet ...' - className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]' + className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]' /> {format === 'anthropic' && (
-
)} @@ -519,7 +519,7 @@ const ApiTester = () => {
{/* 第二行:请求参数 + 响应结果 */} -
+
{/* 请求参数卡片 */}
@@ -527,7 +527,7 @@ const ApiTester = () => {
-
+
请求参数
@@ -547,7 +547,7 @@ const ApiTester = () => {
-