Clarify subscription quota display
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user