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:
1 parent
ea006f6202
commit
e5ad9ac438
13 files changed
+53
-43
No files matched your search
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "会话已过期,请重新登录",
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user