Fix dist-site pricing topup and review UX

This commit is contained in:
abingyyds committed 2026-04-11 09:25:55 +08:00
1 parent 58a18a2991
commit d6b0d65ce2
5 files changed
+92 -30

No files matched your search

+5 -1
View File
@@ -197,6 +197,8 @@
"pricing.model": "Model",
"pricing.inputPrice": "Input / 1M tokens",
"pricing.outputPrice": "Output / 1M tokens",
"pricing.perCall": "Per call",
"pricing.perCallUnit": "call",
"pricing.status": "Status",
"pricing.online": "Online",
"pricing.unknown": "Unknown",
@@ -246,7 +248,9 @@
"topup.onlineTopup": "Online Top Up",
"topup.selectAmount": "Select Amount",
"topup.customAmount": "Custom Amount",
"topup.amountPlaceholder": "Enter amount (min ${{min}})",
"topup.customAmountHint": "Custom amounts are automatically aligned to payable tiers so the entered and charged amounts stay consistent.",
"topup.rechargeAmountLabel": "Recharge Amount",
"topup.amountPlaceholder": "Enter amount (min {{min}})",
"topup.paymentMethod": "Payment Method",
"topup.processing": "Processing...",
"topup.calculating": "Calculating...",
+5 -1
View File
@@ -197,6 +197,8 @@
"pricing.model": "模型",
"pricing.inputPrice": "输入 / 百万 token",
"pricing.outputPrice": "输出 / 百万 token",
"pricing.perCall": "按次计费",
"pricing.perCallUnit": "次",
"pricing.status": "状态",
"pricing.online": "在线",
"pricing.unknown": "未知",
@@ -246,7 +248,9 @@
"topup.onlineTopup": "在线充值",
"topup.selectAmount": "选择金额",
"topup.customAmount": "自定义金额",
"topup.amountPlaceholder": "输入金额(最低 ${{min}})",
"topup.customAmountHint": "自定义金额会自动对齐到可支付档位,避免输入金额和实付金额不一致。",
"topup.rechargeAmountLabel": "充值金额",
"topup.amountPlaceholder": "输入金额(最低 {{min}})",
"topup.paymentMethod": "支付方式",
"topup.processing": "处理中...",
"topup.calculating": "计算中...",
+19 -4
View File
@@ -45,11 +45,26 @@ export default function Pricing() {
(m.display_name || m.model_name || '').toLowerCase().includes(q)
);
}
// Sort by input_price ascending
list = [...list].sort((a, b) => (Number(a.input_price) || 0) - (Number(b.input_price) || 0));
list = [...list].sort((a, b) => {
if (!!a.is_per_call !== !!b.is_per_call) {
return a.is_per_call ? 1 : -1;
}
if (a.is_per_call) {
return (Number(a.fixed_price) || 0) - (Number(b.fixed_price) || 0);
}
return (Number(a.input_price) || 0) - (Number(b.input_price) || 0);
});
return list;
}, [enabledModels, vendor, search]);
const formatTokenPrice = (price) =>
price != null ? `${symbol}${(Number(price) * 1000 * rate).toFixed(4)}` : '-';
const formatPerCallPrice = (price) =>
price != null
? `${symbol}${(Number(price) * rate).toFixed(4)}/${t('pricing.perCallUnit')}`
: '-';
if (loading) {
return (
<div className="flex items-center justify-center min-h-[50vh]">
@@ -134,10 +149,10 @@ export default function Pricing() {
<span className="font-mono text-page">{m.display_name || m.model_name}</span>
</td>
<td className="px-5 py-3.5 text-right font-mono text-page-label">
{m.input_price != null ? `${symbol}${(Number(m.input_price) * 1000 * rate).toFixed(4)}` : '-'}
{m.is_per_call ? t('pricing.perCall') : formatTokenPrice(m.input_price)}
</td>
<td className="px-5 py-3.5 text-right font-mono text-page-label">
{m.output_price != null ? `${symbol}${(Number(m.output_price) * 1000 * rate).toFixed(4)}` : '-'}
{m.is_per_call ? formatPerCallPrice(m.fixed_price) : formatTokenPrice(m.output_price)}
</td>
<td className="px-5 py-3.5 text-center">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs border ${
+61 -22
View File
@@ -28,6 +28,7 @@ export default function Topup() {
// Online topup
const [amount, setAmount] = useState('');
const [displayAmount, setDisplayAmount] = useState('');
const [selectedPreset, setSelectedPreset] = useState(null);
const [payAmount, setPayAmount] = useState(null);
const [amountLoading, setAmountLoading] = useState(false);
@@ -50,6 +51,14 @@ export default function Topup() {
const enableTopup = site?.enable_topup && topupInfo;
const topupConfig = site?.topup_config;
const presetAmounts = topupInfo?.amount_options || [1, 5, 10, 20, 50, 100];
const minTopup = topupInfo?.min_topup || 1;
const payMethods = topupInfo?.pay_methods || [];
const enableOnline = topupInfo?.enable_online_topup;
const enableStripe = topupInfo?.enable_stripe_topup;
const enableCreem = topupInfo?.enable_creem_topup;
const enableCrypto = topupInfo?.enable_crypto_topup;
const hasAnyPayment = enableOnline || enableStripe || enableCreem || enableCrypto;
const loadData = useCallback(async () => {
setLoading(true);
@@ -71,6 +80,22 @@ export default function Topup() {
const requestCount = usage?.request_count ?? user?.request_count ?? 0;
const balanceDollars = quota / Q * rate;
const formatCurrencyAmount = useCallback((value) => {
if (value === '' || value == null || Number.isNaN(Number(value))) return '';
return Number(value).toFixed(2).replace(/\.?0+$/, '');
}, []);
const toDisplayAmount = useCallback((quotaAmount) => {
if (quotaAmount === '' || quotaAmount == null) return '';
return formatCurrencyAmount(Number(quotaAmount) * rate);
}, [formatCurrencyAmount, rate]);
const toQuotaAmount = useCallback((currencyAmount) => {
const numeric = Number.parseFloat(currencyAmount);
if (!Number.isFinite(numeric) || numeric <= 0) return '';
return Math.max(minTopup, Math.round(numeric / rate));
}, [minTopup, rate]);
// Redeem
const handleRedeem = async (e) => {
e.preventDefault();
@@ -103,7 +128,8 @@ export default function Topup() {
// Select preset
const handlePreset = (val) => {
setSelectedPreset(val);
setAmount(val);
setAmount(String(val));
setDisplayAmount(toDisplayAmount(val));
calcAmount(val);
};
@@ -291,15 +317,6 @@ export default function Topup() {
setHistoryLoading(false);
};
const presetAmounts = topupInfo?.amount_options || [1, 5, 10, 20, 50, 100];
const minTopup = topupInfo?.min_topup || 1;
const payMethods = topupInfo?.pay_methods || [];
const enableOnline = topupInfo?.enable_online_topup;
const enableStripe = topupInfo?.enable_stripe_topup;
const enableCreem = topupInfo?.enable_creem_topup;
const enableCrypto = topupInfo?.enable_crypto_topup;
const hasAnyPayment = enableOnline || enableStripe || enableCreem || enableCrypto;
// Filter pay methods: EPay methods + Stripe-based methods go in the main payment buttons
// Creem and Crypto get their own sections
const epayAndStripeMethods = payMethods.filter(
@@ -373,7 +390,7 @@ export default function Topup() {
: 'glass-sm text-page-label hover:text-page hover:bg-page-surface-hover'
}`}
>
{symbol}{Math.round(val * rate)}
{symbol}{formatCurrencyAmount(val * rate)}
</button>
))}
</div>
@@ -385,28 +402,50 @@ export default function Topup() {
<div className="flex gap-3">
<input
type="number"
value={amount ? Math.round(amount * rate) : ''}
value={displayAmount}
onChange={(e) => {
const cnyVal = e.target.value;
const usdVal = cnyVal ? Math.max(1, Math.ceil(cnyVal / rate)) : '';
setAmount(usdVal);
const currentValue = e.target.value;
setDisplayAmount(currentValue);
setSelectedPreset(null);
const quotaAmount = toQuotaAmount(currentValue);
setAmount(quotaAmount === '' ? '' : String(quotaAmount));
calcAmount(quotaAmount);
}}
onBlur={(e) => {
const cnyVal = e.target.value;
const usdVal = cnyVal ? Math.max(1, Math.ceil(cnyVal / rate)) : '';
calcAmount(usdVal);
const quotaAmount = toQuotaAmount(e.target.value);
if (quotaAmount === '') {
setDisplayAmount('');
setAmount('');
setPayAmount(null);
return;
}
setAmount(String(quotaAmount));
setDisplayAmount(toDisplayAmount(quotaAmount));
calcAmount(quotaAmount);
}}
min={Math.round(minTopup * rate)}
placeholder={t('topup.amountPlaceholder', { min: Math.round(minTopup * rate) })}
min={minTopup * rate}
step="0.01"
placeholder={t('topup.amountPlaceholder', { min: formatCurrencyAmount(minTopup * rate) })}
className="input flex-1"
/>
</div>
<p className="text-xs text-page-muted mt-2">
{t('topup.customAmountHint')}
</p>
{amountLoading ? (
<p className="text-xs text-page-muted mt-2">{t('topup.calculating')}</p>
) : payAmount ? (
<p className="text-xs text-page-secondary mt-2">
{t('topup.payAmountLabel')}: <span className="text-page-success font-medium">¥{payAmount}</span>
<div className="mt-2 space-y-1 text-xs text-page-secondary">
<p>
{t('topup.rechargeAmountLabel')}: <span className="text-page font-medium">{symbol}{displayAmount || toDisplayAmount(amount)}</span>
</p>
<p>
{t('topup.payAmountLabel')}: <span className="text-page-success font-medium">{symbol}{payAmount}</span>
</p>
</div>
) : amount ? (
<p className="text-xs text-page-muted mt-2">
{t('topup.rechargeAmountLabel')}: {symbol}{displayAmount || toDisplayAmount(amount)}
</p>
) : null}
</div>