💸 chore: Align subscription pricing display with global currency settings

Unify subscription price rendering to use the site-wide currency symbol/rate on the wallet and admin views.
Make subscription plan currency read-only in the editor and force USD on create/update to avoid drift.
Use global currency display type when creating Creem checkout payloads.
This commit is contained in:
t0ng7u
2026-01-31 13:41:55 +08:00
parent 354da6ea6b
commit 28c5feb570
7 changed files with 39 additions and 30 deletions

View File

@@ -19,6 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Button, Modal, Space, Tag } from '@douyinfe/semi-ui';
import { convertUSDToCurrency } from '../../helpers/render';
const quotaTypeLabel = (quotaType) => (quotaType === 1 ? '按次' : '按量');
@@ -48,8 +49,8 @@ const renderPlanTitle = (text, record) => {
);
};
const renderPrice = (text, record) => {
return `${record?.plan?.currency || 'USD'} ${Number(text || 0).toFixed(2)}`;
const renderPrice = (text) => {
return convertUSDToCurrency(Number(text || 0), 2);
};
const renderDuration = (text, record, t) => {

View File

@@ -114,7 +114,7 @@ const AddEditSubscriptionModal = ({
title: p.title || '',
subtitle: p.subtitle || '',
price_amount: Number(p.price_amount || 0),
currency: p.currency || 'USD',
currency: 'USD',
duration_unit: p.duration_unit || 'month',
duration_value: Number(p.duration_value || 1),
custom_seconds: Number(p.custom_seconds || 0),
@@ -274,6 +274,7 @@ const AddEditSubscriptionModal = ({
plan: {
...values,
price_amount: Number(values.price_amount || 0),
currency: 'USD',
duration_value: Number(values.duration_value || 0),
custom_seconds: Number(values.custom_seconds || 0),
quota_reset_period: values.quota_reset_period || 'never',
@@ -467,15 +468,12 @@ const AddEditSubscriptionModal = ({
</Col>
<Col span={12}>
<Form.Select
<Form.Input
field='currency'
label={t('币种')}
rules={[{ required: true }]}
>
<Select.Option value='USD'>USD</Select.Option>
<Select.Option value='EUR'>EUR</Select.Option>
<Select.Option value='CNY'>CNY</Select.Option>
</Form.Select>
disabled
extraText={t('由全站货币展示设置统一控制')}
/>
</Col>
<Col span={12}>

View File

@@ -35,6 +35,7 @@ import {
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import { API, showError, showSuccess } from '../../../../helpers';
import { convertUSDToCurrency } from '../../../../helpers/render';
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
import CardTable from '../../../common/ui/CardTable';
@@ -104,7 +105,10 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
const planOptions = useMemo(() => {
return (plans || []).map((p) => ({
label: `${p?.plan?.title || ''} (${p?.plan?.currency || 'USD'} ${Number(p?.plan?.price_amount || 0)})`,
label: `${p?.plan?.title || ''} (${convertUSDToCurrency(
Number(p?.plan?.price_amount || 0),
2,
)})`,
value: p?.plan?.id,
}));
}, [plans]);

View File

@@ -30,6 +30,7 @@ import {
Typography,
} from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../helpers';
import { getCurrencyConfig } from '../../helpers/render';
import { CalendarClock, Check, Crown, RefreshCw, Sparkles } from 'lucide-react';
import SubscriptionPurchaseModal from './modals/SubscriptionPurchaseModal';
@@ -99,12 +100,6 @@ function submitEpayForm({ url, params }) {
document.body.removeChild(form);
}
// 获取货币符号
function getCurrencySymbol(currency) {
const symbols = { USD: '$', EUR: '€', CNY: '¥', GBP: '£', JPY: '¥' };
return symbols[currency] || currency + ' ';
}
const SubscriptionPlansCard = ({
t,
loading = false,
@@ -457,8 +452,11 @@ const SubscriptionPlansCard = ({
{plans.map((p, index) => {
const plan = p?.plan;
const planItems = p?.items || [];
const currency = getCurrencySymbol(plan?.currency || 'USD');
const { symbol, rate } = getCurrencyConfig();
const price = Number(plan?.price_amount || 0);
const displayPrice = (price * rate).toFixed(
price % 1 === 0 ? 0 : 2,
);
const isPopular = index === 0 && plans.length > 1;
return (
@@ -504,10 +502,10 @@ const SubscriptionPlansCard = ({
<div className='text-center py-2'>
<div className='flex items-baseline justify-center'>
<span className='text-xl font-bold text-purple-600'>
{currency}
{symbol}
</span>
<span className='text-3xl font-bold text-purple-600'>
{price.toFixed(price % 1 === 0 ? 0 : 2)}
{displayPrice}
</span>
</div>
<div className='text-sm text-gray-500 mt-1'>

View File

@@ -31,6 +31,7 @@ import {
import { Crown, CalendarClock, Package, Check } from 'lucide-react';
import { SiStripe } from 'react-icons/si';
import { IconCreditCard } from '@douyinfe/semi-icons';
import { getCurrencyConfig } from '../../../helpers/render';
const { Text } = Typography;
@@ -70,12 +71,6 @@ function formatResetPeriod(plan, t) {
return t('不重置');
}
// 获取货币符号
function getCurrencySymbol(currency) {
const symbols = { USD: '$', EUR: '€', CNY: '¥', GBP: '£', JPY: '¥' };
return symbols[currency] || currency + ' ';
}
const SubscriptionPurchaseModal = ({
t,
visible,
@@ -94,8 +89,9 @@ const SubscriptionPurchaseModal = ({
}) => {
const plan = selectedPlan?.plan;
const items = selectedPlan?.items || [];
const currency = plan ? getCurrencySymbol(plan.currency || 'USD') : '$';
const { symbol, rate } = getCurrencyConfig();
const price = plan ? Number(plan.price_amount || 0) : 0;
const displayPrice = (price * rate).toFixed(price % 1 === 0 ? 0 : 2);
// 只有当管理员开启支付网关 AND 套餐配置了对应的支付ID时才显示
const hasStripe = enableStripeTopUp && !!plan?.stripe_price_id;
const hasCreem = enableCreemTopUp && !!plan?.creem_product_id;
@@ -170,8 +166,8 @@ const SubscriptionPurchaseModal = ({
{t('应付金额')}
</Text>
<Text strong className='text-xl text-purple-600'>
{currency}
{price.toFixed(price % 1 === 0 ? 0 : 2)}
{symbol}
{displayPrice}
</Text>
</div>
</div>