feat: add per-distributor currency display setting (USD/CNY)

Distributors can now choose between USD ($) and CNY (¥) for frontend
price and balance display. Only affects presentation — backend amounts
remain in USD internally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
abingyydsandClaude Sonnet 4.6 committed 2026-04-17 14:04:56 +08:00
1 parent ea006f6202
commit e5ad9ac438
13 files changed
+53 -43

No files matched your search

+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Sans+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<title>AI Platform</title>
<script type="module" crossorigin src="/assets/index-BY6FzEaC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B3G1pDJs.css">
<script type="module" crossorigin src="/assets/index-By8pKJte.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DJ3hXS61.css">
</head>
<body class="antialiased">
<script>
+9 -1
View File
@@ -72,6 +72,7 @@ export function useCurrency() {
const symbol = currency?.symbol || '¥';
const rate = currency?.exchange_rate || 7;
const code = currency?.code || 'CNY';
const usdRate = currency?.usd_exchange_rate || 7;
const fmt = (usdValue, decimals = 4) => {
if (usdValue == null || isNaN(usdValue)) return '-';
@@ -79,5 +80,12 @@ export function useCurrency() {
return symbol + converted.toFixed(decimals);
};
return { symbol, rate, code, fmt };
const fmtCNY = (cnyValue, decimals = 2) => {
if (cnyValue == null || isNaN(cnyValue)) return '-';
const v = Number(cnyValue);
const converted = code === 'CNY' ? v : v / usdRate;
return symbol + converted.toFixed(decimals);
};
return { symbol, rate, code, fmt, fmtCNY, usdRate };
}
+5 -5
View File
@@ -111,7 +111,7 @@
"subDist.paymentMethod": "Payment Method",
"subDist.chain": "Blockchain",
"subDist.token": "Token",
"subDist.payAndOpen": "Launch Now ¥{{price}}",
"subDist.payAndOpen": "Launch Now {{symbol}}{{price}}",
"subDist.currentUserHint": "Current paying account: {{user}}, payment method: {{method}}",
"subDist.postPayHint": "After payment, your management access will be activated automatically. If login is required, continue with the same account to finish setup.",
"subDist.cryptoTitle": "Crypto Order Created",
@@ -248,7 +248,7 @@
"packages.noPackages": "No packages available at the moment.",
"packages.checkPricing": "Check model pricing",
"packages.daysAccess": "{{count}} days access",
"packages.creditIncluded": "¥{{amount}} credit included",
"packages.creditIncluded": "{{symbol}}{{amount}} credit included",
"packages.requestsMin": "{{count}} requests/min",
"packages.allModels": "All models access",
"packages.openaiApi": "OpenAI-compatible API",
@@ -256,7 +256,7 @@
"packages.signUpToSubscribe": "Sign Up to Subscribe",
"packages.processing": "Processing...",
"packages.confirmTitle": "Confirm Subscription",
"packages.confirmDesc": "Subscribe to {{name}} for ${{price}}?",
"packages.confirmDesc": "Subscribe to {{name}} for {{price}}?",
"packages.yourBalance": "Your balance:",
"packages.insufficientBalance": "Insufficient balance. Please top up before subscribing.",
"packages.confirm": "Confirm",
@@ -272,8 +272,8 @@
"packages.resetDaily": "Resets daily",
"packages.resetWeekly": "Resets weekly",
"packages.resetMonthly": "Resets monthly",
"packages.periodicQuota": "¥{{amount}} {{period}} quota",
"packages.subscriptionInfo": "This is a subscription ({{period}}), valid for {{days}} days. Each period grants ¥{{amount}} quota. Unused quota expires.",
"packages.periodicQuota": "{{symbol}}{{amount}} {{period}} quota",
"packages.subscriptionInfo": "This is a subscription ({{period}}), valid for {{days}} days. Each period grants {{symbol}}{{amount}} quota. Unused quota expires.",
"packages.officialEquiv": "Equivalent to ${{amount}} at {{model}} official pricing",
"common.sessionExpired": "Session expired, please log in again",
+5 -5
View File
@@ -111,7 +111,7 @@
"subDist.paymentMethod": "支付方式",
"subDist.chain": "区块链",
"subDist.token": "代币",
"subDist.payAndOpen": "立即开通 ¥{{price}}",
"subDist.payAndOpen": "立即开通 {{symbol}}{{price}}",
"subDist.currentUserHint": "当前支付账号:{{user}},支付方式:{{method}}",
"subDist.postPayHint": "支付完成后系统会自动为您开通管理权限;如需补登录,请使用同一账号继续完成初始化。",
"subDist.cryptoTitle": "加密货币订单已创建",
@@ -248,7 +248,7 @@
"packages.noPackages": "暂无可用套餐。",
"packages.checkPricing": "查看模型定价",
"packages.daysAccess": "{{count}} 天有效期",
"packages.creditIncluded": "含 ¥{{amount}} 额度",
"packages.creditIncluded": "含 {{symbol}}{{amount}} 额度",
"packages.requestsMin": "{{count}} 次请求/分钟",
"packages.allModels": "全部模型可用",
"packages.openaiApi": "OpenAI 兼容 API",
@@ -256,7 +256,7 @@
"packages.signUpToSubscribe": "注册后订阅",
"packages.processing": "处理中...",
"packages.confirmTitle": "确认订阅",
"packages.confirmDesc": "订阅 {{name}},价格 ${{price}}?",
"packages.confirmDesc": "订阅 {{name}},价格 {{price}}?",
"packages.yourBalance": "您的余额:",
"packages.insufficientBalance": "余额不足,请先充值。",
"packages.confirm": "确认",
@@ -272,8 +272,8 @@
"packages.resetDaily": "每日重置",
"packages.resetWeekly": "每周重置",
"packages.resetMonthly": "每月重置",
"packages.periodicQuota": "{{period}}额度 ¥{{amount}}",
"packages.subscriptionInfo": "📋 此套餐为周期订阅({{period}}),有效期 {{days}} 天。每个周期获得 ¥{{amount}} 额度,未用完作废。",
"packages.periodicQuota": "{{period}}额度 {{symbol}}{{amount}}",
"packages.subscriptionInfo": "📋 此套餐为周期订阅({{period}}),有效期 {{days}} 天。每个周期获得 {{symbol}}{{amount}} 额度,未用完作废。",
"packages.officialEquiv": "相当于 {{model}} 官方 ${{amount}} 额度",
"common.sessionExpired": "会话已过期,请重新登录",
+7 -6
View File
@@ -25,7 +25,7 @@ export default function Packages() {
const { t } = useTranslation();
const { user, refreshUser } = useAuth();
const navigate = useNavigate();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [packages, setPackages] = useState([]);
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
@@ -209,9 +209,9 @@ export default function Packages() {
{/* Price */}
<div className="mb-6">
<div className="flex items-baseline gap-2">
<span className="text-4xl font-bold text-page">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-4xl font-bold text-page">{fmtCNY(pkg.price)}</span>
{pkg.original_price > 0 && pkg.original_price > pkg.price && (
<span className="text-lg text-page-muted line-through">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-lg text-page-muted line-through">{fmtCNY(pkg.original_price)}</span>
)}
</div>
{pkg.duration > 0 && (
@@ -239,8 +239,8 @@ export default function Packages() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{isSubscription
? t('packages.periodicQuota', { amount: (pkg.quota_amount / Q * rate).toFixed(2), period: getResetLabel(resetPeriod) })
: t('packages.creditIncluded', { amount: (pkg.quota_amount / Q * rate).toFixed(2) })
? t('packages.periodicQuota', { symbol, amount: (pkg.quota_amount / Q * rate).toFixed(2), period: getResetLabel(resetPeriod) })
: t('packages.creditIncluded', { symbol, amount: (pkg.quota_amount / Q * rate).toFixed(2) })
}
</li>
)}
@@ -292,12 +292,13 @@ export default function Packages() {
<div className="glass rounded-2xl p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
<h2 className="text-lg font-semibold text-page mb-3">{t('packages.confirmTitle')}</h2>
<p className="text-sm text-page-secondary mb-2">
{t('packages.confirmDesc', { name: confirmPkg.name, price: pkgPrice.toFixed(2) })}
{t('packages.confirmDesc', { name: confirmPkg.name, price: fmtCNY(pkgPrice) })}
</p>
{isSubscription && (
<div className="bg-purple-500/10 border border-purple-500/20 rounded-xl p-3 mb-3">
<p className="text-xs text-page-info">
{t('packages.subscriptionInfo', {
symbol,
period: getResetLabel(resetPeriod),
days: confirmPkg.duration || 30,
amount: (confirmPkg.quota_amount / Q * rate).toFixed(2),
+4 -3
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import toast from 'react-hot-toast';
import { createSubDistributorOrder, getSubDistributorInfo } from '../api';
import { useAuth } from '../context/AuthContext';
import { useSite } from '../context/SiteContext';
import { useSite, useCurrency } from '../context/SiteContext';
function submitEpayForm(resData) {
const params = resData.data;
@@ -33,6 +33,7 @@ export default function SubDistributor() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, fmtCNY } = useCurrency();
const [loading, setLoading] = useState(true);
const [subInfo, setSubInfo] = useState(null);
const [submitting, setSubmitting] = useState(false);
@@ -216,7 +217,7 @@ export default function SubDistributor() {
<button type="submit" disabled={submitting} className="btn-primary w-full justify-center flex items-center gap-2">
{submitting && <span className="w-4 h-4 rounded-full border-2 border-white/30 border-t-white animate-spin" />}
{t('subDist.payAndOpen', { price: Number(subInfo?.price || 0).toFixed(2) })}
{t('subDist.payAndOpen', { symbol, price: Number(subInfo?.price || 0).toFixed(2) })}
</button>
<p className="text-xs text-page-muted">
@@ -247,7 +248,7 @@ export default function SubDistributor() {
<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-page-secondary">{t('subDist.openPrice')}</span>
<span className="text-2xl font-bold text-page">¥{Number(subInfo?.price || 0).toFixed(2)}</span>
<span className="text-2xl font-bold text-page">{fmtCNY(subInfo?.price || 0)}</span>
</div>
<p className="text-sm text-page-secondary leading-6">{t('subDist.priceSummary')}</p>
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function ClaudeHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -160,9 +160,9 @@ export default function ClaudeHome() {
<h3 className="text-base font-semibold text-[#3D3024]">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-[#8B7D6E] mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-[#3D3024]">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-[#3D3024]">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-[#8B7D6E] line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-[#8B7D6E] line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-[#8B7D6E] mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function CleanHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -173,9 +173,9 @@ export default function CleanHome() {
<h3 className="text-base font-semibold text-gray-900">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-gray-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-gray-900">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-gray-900">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-gray-500 line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-gray-500 line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-gray-500 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function CorporateHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -172,9 +172,9 @@ export default function CorporateHome() {
<h3 className="text-base font-semibold text-slate-900">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-slate-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-slate-900">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-slate-900">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-slate-500 line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-slate-500 line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-slate-500 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function DarkHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -163,9 +163,9 @@ export default function DarkHome() {
<h3 className="text-base font-semibold text-white">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-white">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-white">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-600 line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-neutral-600 line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function DefaultHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -172,9 +172,9 @@ export default function DefaultHome() {
<h3 className="text-base font-semibold text-white">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-white">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-white">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-600 line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-neutral-600 line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function MinimalHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -155,9 +155,9 @@ export default function MinimalHome() {
<h3 className="text-base font-semibold text-white">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-white">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-white">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-600 line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-neutral-600 line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>
+3 -3
View File
@@ -13,7 +13,7 @@ export default function StarterHome() {
const { t } = useTranslation();
const { user } = useAuth();
const { site } = useSite();
const { symbol, rate } = useCurrency();
const { symbol, rate, fmtCNY } = useCurrency();
const [models, setModels] = useState([]);
const [packages, setPackages] = useState([]);
@@ -178,9 +178,9 @@ export default function StarterHome() {
<h3 className="text-base font-semibold text-white">{pkg.name}</h3>
{pkg.description && <p className="text-sm text-neutral-500 mt-1">{pkg.description}</p>}
<div className="mt-auto pt-6">
<span className="text-3xl font-bold text-white">{symbol}{Number(pkg.price).toFixed(2)}</span>
<span className="text-3xl font-bold text-white">{fmtCNY(pkg.price)}</span>
{pkg.original_price > pkg.price && (
<span className="text-sm text-neutral-600 line-through ml-2">{symbol}{Number(pkg.original_price).toFixed(2)}</span>
<span className="text-sm text-neutral-600 line-through ml-2">{fmtCNY(pkg.original_price)}</span>
)}
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
</div>