feat(subscription): 支持调整订阅时长(延长/缩短)

- 将"延长订阅"功能改为"调整订阅",支持正数延长、负数缩短
- 后端验证:调整天数范围 -36500 到 36500,缩短后剩余天数必须 > 0
- 前端同步更新界面文案和验证逻辑

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Wen
2026-01-20 00:11:30 +08:00
parent e7bc62500b
commit cc07a0e295
5 changed files with 80 additions and 33 deletions

View File

@@ -950,7 +950,7 @@ export default {
title: 'Subscription Management',
description: 'Manage user subscriptions and quota limits',
assignSubscription: 'Assign Subscription',
extendSubscription: 'Extend Subscription',
adjustSubscription: 'Adjust Subscription',
revokeSubscription: 'Revoke Subscription',
allStatus: 'All Status',
allGroups: 'All Groups',
@@ -965,6 +965,7 @@ export default {
resetInHoursMinutes: 'Resets in {hours}h {minutes}m',
resetInDaysHours: 'Resets in {days}d {hours}h',
daysRemaining: 'days remaining',
remainingDays: 'Remaining days',
noExpiration: 'No expiration',
status: {
active: 'Active',
@@ -983,28 +984,32 @@ export default {
user: 'User',
group: 'Subscription Group',
validityDays: 'Validity (Days)',
extendDays: 'Extend by (Days)'
adjustDays: 'Adjust by (Days)'
},
selectUser: 'Select a user',
selectGroup: 'Select a subscription group',
groupHint: 'Only groups with subscription billing type are shown',
validityHint: 'Number of days the subscription will be valid',
extendingFor: 'Extending subscription for',
adjustingFor: 'Adjusting subscription for',
currentExpiration: 'Current expiration',
adjustDaysPlaceholder: 'Positive to extend, negative to shorten',
adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)',
assign: 'Assign',
assigning: 'Assigning...',
extend: 'Extend',
extending: 'Extending...',
adjust: 'Adjust',
adjusting: 'Adjusting...',
revoke: 'Revoke',
noSubscriptionsYet: 'No subscriptions yet',
assignFirstSubscription: 'Assign a subscription to get started.',
subscriptionAssigned: 'Subscription assigned successfully',
subscriptionExtended: 'Subscription extended successfully',
subscriptionAdjusted: 'Subscription adjusted successfully',
subscriptionRevoked: 'Subscription revoked successfully',
failedToLoad: 'Failed to load subscriptions',
failedToAssign: 'Failed to assign subscription',
failedToExtend: 'Failed to extend subscription',
failedToAdjust: 'Failed to adjust subscription',
failedToRevoke: 'Failed to revoke subscription',
adjustWouldExpire: 'Remaining days after adjustment must be greater than 0',
adjustOutOfRange: 'Adjustment days must be between -36500 and 36500',
pleaseSelectUser: 'Please select a user',
pleaseSelectGroup: 'Please select a group',
validityDaysRequired: 'Please enter a valid number of days (at least 1)',

View File

@@ -1025,7 +1025,7 @@ export default {
title: '订阅管理',
description: '管理用户订阅和配额限制',
assignSubscription: '分配订阅',
extendSubscription: '延长订阅',
adjustSubscription: '调整订阅',
revokeSubscription: '撤销订阅',
allStatus: '全部状态',
allGroups: '全部分组',
@@ -1040,6 +1040,7 @@ export default {
resetInHoursMinutes: '{hours} 小时 {minutes} 分钟后重置',
resetInDaysHours: '{days} 天 {hours} 小时后重置',
daysRemaining: '天剩余',
remainingDays: '剩余天数',
noExpiration: '无过期时间',
status: {
active: '生效中',
@@ -1058,28 +1059,32 @@ export default {
user: '用户',
group: '订阅分组',
validityDays: '有效期(天)',
extendDays: '延长天数'
adjustDays: '调整天数'
},
selectUser: '选择用户',
selectGroup: '选择订阅分组',
groupHint: '仅显示订阅计费类型的分组',
validityHint: '订阅的有效天数',
extendingFor: '为以下用户延长订阅',
adjustingFor: '为以下用户调整订阅',
currentExpiration: '当前到期时间',
adjustDaysPlaceholder: '正数延长,负数缩短',
adjustHint: '输入正数延长订阅负数缩短订阅缩短后剩余天数需大于0',
assign: '分配',
assigning: '分配中...',
extend: '延长',
extending: '延长中...',
adjust: '调整',
adjusting: '调整中...',
revoke: '撤销',
noSubscriptionsYet: '暂无订阅',
assignFirstSubscription: '分配一个订阅以开始使用。',
subscriptionAssigned: '订阅分配成功',
subscriptionExtended: '订阅延长成功',
subscriptionAdjusted: '订阅调整成功',
subscriptionRevoked: '订阅撤销成功',
failedToLoad: '加载订阅列表失败',
failedToAssign: '分配订阅失败',
failedToExtend: '延长订阅失败',
failedToAdjust: '调整订阅失败',
failedToRevoke: '撤销订阅失败',
adjustWouldExpire: '调整后剩余天数必须大于0',
adjustOutOfRange: '调整天数必须在 -36500 到 36500 之间',
pleaseSelectUser: '请选择用户',
pleaseSelectGroup: '请选择分组',
validityDaysRequired: '请输入有效的天数至少1天',

View File

@@ -359,10 +359,10 @@
<button
v-if="row.status === 'active'"
@click="handleExtend(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-green-50 hover:text-green-600 dark:hover:bg-green-900/20 dark:hover:text-green-400"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
>
<Icon name="clock" size="sm" />
<span class="text-xs">{{ t('admin.subscriptions.extend') }}</span>
<Icon name="calendar" size="sm" />
<span class="text-xs">{{ t('admin.subscriptions.adjust') }}</span>
</button>
<button
v-if="row.status === 'active'"
@@ -512,10 +512,10 @@
</template>
</BaseDialog>
<!-- Extend Subscription Modal -->
<!-- Adjust Subscription Modal -->
<BaseDialog
:show="showExtendModal"
:title="t('admin.subscriptions.extendSubscription')"
:title="t('admin.subscriptions.adjustSubscription')"
width="narrow"
@close="closeExtendModal"
>
@@ -527,7 +527,7 @@
>
<div class="rounded-lg bg-gray-50 p-4 dark:bg-dark-700">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ t('admin.subscriptions.extendingFor') }}
{{ t('admin.subscriptions.adjustingFor') }}
<span class="font-medium text-gray-900 dark:text-white">{{
extendingSubscription.user?.email
}}</span>
@@ -542,10 +542,25 @@
}}
</span>
</p>
<p v-if="extendingSubscription.expires_at" class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ t('admin.subscriptions.remainingDays') }}:
<span class="font-medium text-gray-900 dark:text-white">
{{ getDaysRemaining(extendingSubscription.expires_at) ?? 0 }}
</span>
</p>
</div>
<div>
<label class="input-label">{{ t('admin.subscriptions.form.extendDays') }}</label>
<input v-model.number="extendForm.days" type="number" min="1" required class="input" />
<label class="input-label">{{ t('admin.subscriptions.form.adjustDays') }}</label>
<div class="flex items-center gap-2">
<input
v-model.number="extendForm.days"
type="number"
required
class="input text-center"
:placeholder="t('admin.subscriptions.adjustDaysPlaceholder')"
/>
</div>
<p class="input-hint">{{ t('admin.subscriptions.adjustHint') }}</p>
</div>
</form>
<template #footer>
@@ -559,7 +574,7 @@
:disabled="submitting"
class="btn btn-primary"
>
{{ submitting ? t('admin.subscriptions.extending') : t('admin.subscriptions.extend') }}
{{ submitting ? t('admin.subscriptions.adjusting') : t('admin.subscriptions.adjust') }}
</button>
</div>
</template>
@@ -1000,17 +1015,27 @@ const closeExtendModal = () => {
const handleExtendSubscription = async () => {
if (!extendingSubscription.value) return
// 前端验证:调整后剩余天数必须 > 0
if (extendingSubscription.value.expires_at) {
const currentDaysRemaining = getDaysRemaining(extendingSubscription.value.expires_at) ?? 0
const newDaysRemaining = currentDaysRemaining + extendForm.days
if (newDaysRemaining <= 0) {
appStore.showError(t('admin.subscriptions.adjustWouldExpire'))
return
}
}
submitting.value = true
try {
await adminAPI.subscriptions.extend(extendingSubscription.value.id, {
days: extendForm.days
})
appStore.showSuccess(t('admin.subscriptions.subscriptionExtended'))
appStore.showSuccess(t('admin.subscriptions.subscriptionAdjusted'))
closeExtendModal()
loadSubscriptions()
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.subscriptions.failedToExtend'))
console.error('Error extending subscription:', error)
appStore.showError(error.response?.data?.detail || t('admin.subscriptions.failedToAdjust'))
console.error('Error adjusting subscription:', error)
} finally {
submitting.value = false
}