Clarify subscription quota display

This commit is contained in:
2026-04-09 09:49:44 +08:00
parent de5791834f
commit 183e95468c
3 changed files with 180 additions and 3 deletions
@@ -37,6 +37,8 @@ import SubscriptionPurchaseModal from './modals/SubscriptionPurchaseModal';
import {
formatSubscriptionDuration,
formatSubscriptionResetPeriod,
getResetCycleCount,
getSubscriptionEndDate,
} from '../../helpers/subscriptionFormat';
const { Text } = Typography;
@@ -232,6 +234,16 @@ const SubscriptionPlansCard = ({
return map;
}, [plans]);
const planMap = useMemo(() => {
const map = new Map();
(plans || []).forEach((p) => {
const plan = p?.plan;
if (!plan?.id) return;
map.set(plan.id, plan);
});
return map;
}, [plans]);
const getPlanPurchaseCount = (planId) =>
planPurchaseCountMap.get(planId) || 0;
@@ -251,6 +263,27 @@ const SubscriptionPlansCard = ({
return Math.round((used / total) * 100);
};
const getPlanQuotaMetaForDisplay = (plan, startDate = new Date()) => {
const totalAmount = Number(plan?.total_amount || 0);
const resetPeriod = plan?.quota_reset_period || 'never';
const recurring = totalAmount > 0 && resetPeriod !== 'never';
if (!recurring) return { recurring: false, cycleAmount: totalAmount, estimatedTotalAmount: totalAmount };
const endDate = getSubscriptionEndDate(startDate, plan);
const cycles = endDate ? getResetCycleCount(plan, startDate, endDate) : 1;
return { recurring: true, cycleAmount: totalAmount, estimatedTotalAmount: totalAmount * Math.max(1, cycles) };
};
const getSubscriptionQuotaMeta = (subscription, plan) => {
const totalAmount = Number(subscription?.amount_total || 0);
const resetPeriod = plan?.quota_reset_period || 'never';
const recurring = totalAmount > 0 && resetPeriod !== 'never';
if (!recurring) return { recurring: false, cycleAmount: totalAmount, estimatedTotalAmount: totalAmount };
const startDate = new Date(Number(subscription?.start_time || 0) * 1000);
const endDate = new Date(Number(subscription?.end_time || 0) * 1000);
const cycles = getResetCycleCount(plan, startDate, endDate);
return { recurring: true, cycleAmount: totalAmount, estimatedTotalAmount: totalAmount * Math.max(1, cycles) };
};
const cardContent = (
<>
{/* 卡片头部 */}
@@ -387,6 +420,8 @@ const SubscriptionPlansCard = ({
totalAmount > 0
? Math.max(0, totalAmount - usedAmount)
: 0;
const plan = planMap.get(subscription?.plan_id);
const quotaMeta = getSubscriptionQuotaMeta(subscription, plan);
const planTitle =
planTitleMap.get(subscription?.plan_id) || '';
const remainDays = getRemainingDays(sub);
@@ -463,6 +498,17 @@ const SubscriptionPlansCard = ({
</span>
)}
</div>
{quotaMeta.recurring && (
<div className='text-xs text-gray-500 mb-2'>
{t('\u672c\u5468\u671f\u989d\u5ea6')}?{renderQuota(quotaMeta.cycleAmount)} ? {' '}
{t('\u9884\u8ba1\u7d2f\u8ba1\u989d\u5ea6')}?{renderQuota(quotaMeta.estimatedTotalAmount)}
{plan ? (
<span className='ml-2'>
? {t('\u6309')} {formatSubscriptionResetPeriod(plan, t)} {t('\u91cd\u7f6e\u4f30\u7b97')}
</span>
) : null}
</div>
)}
{!isLast && <Divider margin={12} />}
</div>
);
@@ -482,6 +528,7 @@ const SubscriptionPlansCard = ({
{plans.map((p, index) => {
const plan = p?.plan;
const totalAmount = Number(plan?.total_amount || 0);
const quotaMeta = getPlanQuotaMetaForDisplay(plan);
const { symbol, rate } = getCurrencyConfig();
const price = Number(plan?.price_amount || 0);
const convertedPrice = price * rate;
@@ -493,8 +540,16 @@ const SubscriptionPlansCard = ({
const limitLabel = limit > 0 ? `${t('限购')} ${limit}` : null;
const totalLabel =
totalAmount > 0
? `${t('总额度')}: ${renderQuota(totalAmount)}`
: `${t('总额度')}: ${t('不限')}`;
? `${t('\u603b\u989d\u5ea6')}: ${renderQuota(totalAmount)}`
: `${t('\u603b\u989d\u5ea6')}: ${t('\u4e0d\u9650')}`;
const periodQuotaLabel =
quotaMeta.recurring && quotaMeta.cycleAmount > 0
? `${t('\u6bcf\u5468\u671f\u989d\u5ea6')}: ${renderQuota(quotaMeta.cycleAmount)}`
: null;
const estimatedTotalLabel =
quotaMeta.recurring && quotaMeta.estimatedTotalAmount > 0
? `${t('\u9884\u8ba1\u7d2f\u8ba1\u989d\u5ea6')}: ${renderQuota(quotaMeta.estimatedTotalAmount)}`
: null;
const upgradeLabel = plan?.upgrade_group
? `${t('升级分组')}: ${plan.upgrade_group}`
: null;
@@ -507,6 +562,18 @@ const SubscriptionPlansCard = ({
label: `${t('有效期')}: ${formatSubscriptionDuration(plan, t)}`,
},
resetLabel ? { label: resetLabel } : null,
periodQuotaLabel
? {
label: periodQuotaLabel,
tooltip: `${t('\u5f53\u524d\u6bcf\u6b21\u91cd\u7f6e\u540e\u53ef\u7528\u989d\u5ea6')}?${renderQuota(quotaMeta.cycleAmount)}`,
}
: null,
estimatedTotalLabel
? {
label: estimatedTotalLabel,
tooltip: t('\u6309\u5f53\u524d\u6709\u6548\u671f\u548c\u91cd\u7f6e\u5468\u671f\u4f30\u7b97\uff0c\u4e0d\u7ed3\u8f6c\u672a\u7528\u989d\u5ea6'),
}
: null,
totalAmount > 0
? {
label: totalLabel,
@@ -36,6 +36,8 @@ import { getCurrencyConfig } from '../../../helpers/render';
import {
formatSubscriptionDuration,
formatSubscriptionResetPeriod,
getResetCycleCount,
getSubscriptionEndDate,
} from '../../../helpers/subscriptionFormat';
const { Text } = Typography;
@@ -59,6 +61,17 @@ const SubscriptionPurchaseModal = ({
}) => {
const plan = selectedPlan?.plan;
const totalAmount = Number(plan?.total_amount || 0);
const resetPeriod = plan?.quota_reset_period || 'never';
const recurringQuota = totalAmount > 0 && resetPeriod !== 'never';
const estimatedCycles = (() => {
if (!recurringQuota) return 1;
const startDate = new Date();
const endDate = getSubscriptionEndDate(startDate, plan);
return endDate ? getResetCycleCount(plan, startDate, endDate) : 1;
})();
const estimatedTotalAmount = recurringQuota
? totalAmount * Math.max(1, estimatedCycles)
: totalAmount;
const { symbol, rate } = getCurrencyConfig();
const price = plan ? Number(plan.price_amount || 0) : 0;
const convertedPrice = price * rate;
@@ -146,6 +159,28 @@ const SubscriptionPurchaseModal = ({
)}
</div>
</div>
{recurringQuota && (
<>
<div className='flex justify-between items-center'>
<Text strong className='text-slate-700 dark:text-slate-200'>
{t('\u6bcf\u5468\u671f\u989d\u5ea6')}?
</Text>
<Text className='text-slate-900 dark:text-slate-100'>
{renderQuota(totalAmount)}
</Text>
</div>
<div className='flex justify-between items-center'>
<Text strong className='text-slate-700 dark:text-slate-200'>
{t('\u9884\u8ba1\u7d2f\u8ba1\u989d\u5ea6')}?
</Text>
<Tooltip content={t('\u6309\u5f53\u524d\u6709\u6548\u671f\u548c\u91cd\u7f6e\u5468\u671f\u4f30\u7b97\uff0c\u4e0d\u7ed3\u8f6c\u672a\u7528\u989d\u5ea6')}>
<Text className='text-slate-900 dark:text-slate-100'>
{renderQuota(estimatedTotalAmount)}
</Text>
</Tooltip>
</div>
</>
)}
{plan?.upgrade_group ? (
<div className='flex justify-between items-center'>
<Text strong className='text-slate-700 dark:text-slate-200'>
+76 -1
View File
@@ -9,9 +9,10 @@ export function formatSubscriptionDuration(plan, t) {
custom: t('自定义'),
};
if (unit === 'custom') {
const seconds = plan?.custom_seconds || 0;
const seconds = Number(plan?.custom_seconds || 0);
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
return `${seconds} ${t('秒')}`;
}
return `${value} ${unitLabels[unit] || unit}`;
@@ -32,3 +33,77 @@ export function formatSubscriptionResetPeriod(plan, t) {
}
return t('不重置');
}
export function getSubscriptionEndDate(startDate, plan) {
if (!startDate || !plan) return null;
const start = new Date(startDate);
if (Number.isNaN(start.getTime())) return null;
const unit = plan?.duration_unit || 'month';
const value = Number(plan?.duration_value || 1);
const customSeconds = Number(plan?.custom_seconds || 0);
const end = new Date(start.getTime());
switch (unit) {
case 'year':
end.setFullYear(end.getFullYear() + value);
break;
case 'month':
end.setMonth(end.getMonth() + value);
break;
case 'day':
end.setDate(end.getDate() + value);
break;
case 'hour':
end.setHours(end.getHours() + value);
break;
case 'custom':
end.setSeconds(end.getSeconds() + customSeconds);
break;
default:
return null;
}
return end;
}
export function getResetCycleCount(plan, startDate, endDate) {
if (!plan || !startDate || !endDate) return 1;
const period = plan?.quota_reset_period || 'never';
if (period === 'never') return 1;
const start = new Date(startDate);
const end = new Date(endDate);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return 1;
if (end <= start) return 1;
const nextReset = (current) => {
const next = new Date(current.getTime());
switch (period) {
case 'daily':
next.setDate(next.getDate() + 1);
return next;
case 'weekly':
next.setDate(next.getDate() + 7);
return next;
case 'monthly':
next.setMonth(next.getMonth() + 1);
return next;
case 'custom': {
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
if (seconds <= 0) return null;
next.setSeconds(next.getSeconds() + seconds);
return next;
}
default:
return null;
}
};
let cycles = 1;
let current = start;
for (let i = 0; i < 10000; i += 1) {
const next = nextReset(current);
if (!next || next >= end) break;
cycles += 1;
current = next;
}
return cycles;
}