diff --git a/web/src/components/topup/SubscriptionPlansCard.jsx b/web/src/components/topup/SubscriptionPlansCard.jsx index a619c745..49518ff1 100644 --- a/web/src/components/topup/SubscriptionPlansCard.jsx +++ b/web/src/components/topup/SubscriptionPlansCard.jsx @@ -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 = ({ )} + {quotaMeta.recurring && ( +
+ {t('\u672c\u5468\u671f\u989d\u5ea6')}?{renderQuota(quotaMeta.cycleAmount)} ? {' '} + {t('\u9884\u8ba1\u7d2f\u8ba1\u989d\u5ea6')}?{renderQuota(quotaMeta.estimatedTotalAmount)} + {plan ? ( + + ? {t('\u6309')} {formatSubscriptionResetPeriod(plan, t)} {t('\u91cd\u7f6e\u4f30\u7b97')} + + ) : null} +
+ )} {!isLast && } ); @@ -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, diff --git a/web/src/components/topup/modals/SubscriptionPurchaseModal.jsx b/web/src/components/topup/modals/SubscriptionPurchaseModal.jsx index 8bd861ee..47ba1361 100644 --- a/web/src/components/topup/modals/SubscriptionPurchaseModal.jsx +++ b/web/src/components/topup/modals/SubscriptionPurchaseModal.jsx @@ -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 = ({ )} + {recurringQuota && ( + <> +
+ + {t('\u6bcf\u5468\u671f\u989d\u5ea6')}? + + + {renderQuota(totalAmount)} + +
+
+ + {t('\u9884\u8ba1\u7d2f\u8ba1\u989d\u5ea6')}? + + + + {renderQuota(estimatedTotalAmount)} + + +
+ + )} {plan?.upgrade_group ? (
diff --git a/web/src/helpers/subscriptionFormat.js b/web/src/helpers/subscriptionFormat.js index 6e49a839..a7679a75 100644 --- a/web/src/helpers/subscriptionFormat.js +++ b/web/src/helpers/subscriptionFormat.js @@ -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; +}