Merge pull request #2557 from Arron196/fix/issue-2542-daily-card-expiry-mode

fix: 修复日卡跨日重复刷新额度
This commit is contained in:
Wesley Liddick
2026-05-19 14:53:22 +08:00
committed by GitHub
11 changed files with 451 additions and 78 deletions
+38 -16
View File
@@ -246,7 +246,7 @@
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ formatResetTime(row.daily_window_start, 'daily') }}</span>
<span>{{ formatDailyUsageWindow(row) }}</span>
</div>
</div>
@@ -758,6 +758,7 @@ import Select from '@/components/common/Select.vue'
import GroupBadge from '@/components/common/GroupBadge.vue'
import GroupOptionItem from '@/components/common/GroupOptionItem.vue'
import Icon from '@/components/icons/Icon.vue'
import { getRemainingDurationParts, isOneTimeDailyQuota, type RemainingDurationParts } from '@/utils/subscriptionQuota'
const { t } = useI18n()
const appStore = useAppStore()
@@ -1313,8 +1314,41 @@ const getProgressClass = (used: number | null | undefined, limit: number | null)
return 'bg-green-500'
}
const formatResetDuration = (parts: RemainingDurationParts): string => {
if (parts.days > 0) {
return t('admin.subscriptions.resetInDaysHours', { days: parts.days, hours: parts.hours })
}
if (parts.hours > 0) {
return t('admin.subscriptions.resetInHoursMinutes', { hours: parts.hours, minutes: parts.minutes })
}
return t('admin.subscriptions.resetInMinutes', { minutes: parts.minutes })
}
const formatQuotaEndDuration = (parts: RemainingDurationParts): string => {
if (parts.days > 0) {
return t('admin.subscriptions.quotaEndsInDaysHours', { days: parts.days, hours: parts.hours })
}
if (parts.hours > 0) {
return t('admin.subscriptions.quotaEndsInHoursMinutes', { hours: parts.hours, minutes: parts.minutes })
}
return t('admin.subscriptions.quotaEndsInMinutes', { minutes: parts.minutes })
}
const formatDailyUsageWindow = (subscription: UserSubscription): string => {
if (isOneTimeDailyQuota(subscription) && subscription.expires_at) {
const parts = getRemainingDurationParts(subscription.expires_at)
return parts ? formatQuotaEndDuration(parts) : t('admin.subscriptions.windowNotActive')
}
return formatResetTime(subscription.daily_window_start, 'daily')
}
// Format reset time based on window start and period type
const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'monthly'): string => {
const formatResetTime = (windowStart: string | null, period: 'daily' | 'weekly' | 'monthly'): string => {
if (!windowStart) return t('admin.subscriptions.windowNotActive')
const start = new Date(windowStart)
@@ -1334,21 +1368,9 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
break
}
const diffMs = resetTime.getTime() - now.getTime()
if (diffMs <= 0) return t('admin.subscriptions.windowNotActive')
const parts = getRemainingDurationParts(resetTime, now)
const diffSeconds = Math.floor(diffMs / 1000)
const days = Math.floor(diffSeconds / 86400)
const hours = Math.floor((diffSeconds % 86400) / 3600)
const minutes = Math.floor((diffSeconds % 3600) / 60)
if (days > 0) {
return t('admin.subscriptions.resetInDaysHours', { days, hours })
} else if (hours > 0) {
return t('admin.subscriptions.resetInHoursMinutes', { hours, minutes })
} else {
return t('admin.subscriptions.resetInMinutes', { minutes })
}
return parts ? formatResetDuration(parts) : t('admin.subscriptions.windowNotActive')
}
// Handle click outside to close dropdowns
+28 -23
View File
@@ -127,11 +127,7 @@
v-if="subscription.daily_window_start"
class="text-xs text-gray-500 dark:text-dark-400"
>
{{
t('userSubscriptions.resetIn', {
time: formatResetTime(subscription.daily_window_start, 24)
})
}}
{{ formatDailyUsageWindow(subscription) }}
</p>
</div>
@@ -256,6 +252,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
import Icon from '@/components/icons/Icon.vue'
import { formatDateOnly } from '@/utils/format'
import { platformBorderClass, platformBadgeClass, platformButtonClass, platformLabel } from '@/utils/platformColors'
import { getRemainingDurationParts, isOneTimeDailyQuota, type RemainingDurationParts } from '@/utils/subscriptionQuota'
function platformAccentDotClass(p: string): string {
switch (p) {
@@ -334,30 +331,38 @@ function getExpirationClass(expiresAt: string): string {
return 'text-gray-700 dark:text-gray-300'
}
function formatDurationParts(parts: RemainingDurationParts): string {
if (parts.days > 0) {
return `${parts.days}d ${parts.hours}h`
}
if (parts.hours > 0) {
return `${parts.hours}h ${parts.minutes}m`
}
return `${parts.minutes}m`
}
function formatDailyUsageWindow(subscription: UserSubscription): string {
if (isOneTimeDailyQuota(subscription) && subscription.expires_at) {
const parts = getRemainingDurationParts(subscription.expires_at)
if (!parts) return t('userSubscriptions.windowNotActive')
return t('userSubscriptions.quotaEndsIn', { time: formatDurationParts(parts) })
}
return t('userSubscriptions.resetIn', {
time: formatResetTime(subscription.daily_window_start, 24)
})
}
function formatResetTime(windowStart: string | null, windowHours: number): string {
if (!windowStart) return t('userSubscriptions.windowNotActive')
const start = new Date(windowStart)
const end = new Date(start.getTime() + windowHours * 60 * 60 * 1000)
const now = new Date()
const diff = end.getTime() - now.getTime()
const parts = getRemainingDurationParts(end)
if (diff <= 0) return t('userSubscriptions.windowNotActive')
const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
if (hours > 24) {
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
return `${days}d ${remainingHours}h`
}
if (hours > 0) {
return `${hours}h ${minutes}m`
}
return `${minutes}m`
return parts ? formatDurationParts(parts) : t('userSubscriptions.windowNotActive')
}
onMounted(() => {