feat(subscription): 支持恢复已撤销订阅

This commit is contained in:
deqiying
2026-07-01 22:19:21 +08:00
parent 7dc7cfce1d
commit b26dcc3da2
17 changed files with 382 additions and 19 deletions
+12 -1
View File
@@ -24,7 +24,7 @@ export async function list(
page: number = 1,
pageSize: number = 20,
filters?: {
status?: 'active' | 'expired' | 'revoked'
status?: 'active' | 'expired' | 'revoked' | 'suspended'
user_id?: number
group_id?: number
platform?: string
@@ -121,6 +121,16 @@ export async function revoke(id: number): Promise<{ message: string }> {
return data
}
/**
* Restore revoked subscription
* @param id - Subscription ID
* @returns Restored subscription
*/
export async function restore(id: number): Promise<UserSubscription> {
const { data } = await apiClient.post<UserSubscription>(`/admin/subscriptions/${id}/restore`)
return data
}
/**
* Reset daily, weekly, and/or monthly usage quota for a subscription
* @param id - Subscription ID
@@ -188,6 +198,7 @@ export const subscriptionsAPI = {
bulkAssign,
extend,
revoke,
restore,
resetQuota,
listByGroup,
listByUser
+9 -2
View File
@@ -2977,6 +2977,7 @@ export default {
assignSubscription: 'Assign Subscription',
adjustSubscription: 'Adjust Subscription',
revokeSubscription: 'Revoke Subscription',
restoreSubscription: 'Restore Subscription',
allStatus: 'All Status',
allGroups: 'All Groups',
allPlatforms: 'All Platforms',
@@ -2999,7 +3000,8 @@ export default {
status: {
active: 'Active',
expired: 'Expired',
revoked: 'Revoked'
revoked: 'Revoked',
suspended: 'Suspended'
},
columns: {
user: 'User',
@@ -3028,6 +3030,7 @@ export default {
adjust: 'Adjust',
adjusting: 'Adjusting...',
revoke: 'Revoke',
restore: 'Restore',
resetQuota: 'Reset Quota',
resetQuotaTitle: 'Reset Usage Quota',
resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.",
@@ -3038,17 +3041,21 @@ export default {
subscriptionAssigned: 'Subscription assigned successfully',
subscriptionAdjusted: 'Subscription adjusted successfully',
subscriptionRevoked: 'Subscription revoked successfully',
subscriptionRestored: 'Subscription restored successfully',
failedToLoad: 'Failed to load subscriptions',
failedToAssign: 'Failed to assign subscription',
failedToAdjust: 'Failed to adjust subscription',
failedToRevoke: 'Failed to revoke subscription',
failedToRestore: 'Failed to restore 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)',
revokeConfirm:
"Are you sure you want to revoke the subscription for '{user}'? This action cannot be undone.",
"Are you sure you want to revoke the subscription for '{user}'? You can restore it later from the revoked list.",
restoreConfirm:
"Restore the subscription for '{user}'? If the original subscription has expired, it will be restored as expired.",
guide: {
title: 'Subscription Management Guide',
subtitle: 'Subscription mode lets you assign time-based usage quotas to users, with daily/weekly/monthly limits. Follow these steps to get started.',
+8 -2
View File
@@ -3054,6 +3054,7 @@ export default {
assignSubscription: '分配订阅',
adjustSubscription: '调整订阅',
revokeSubscription: '撤销订阅',
restoreSubscription: '恢复订阅',
allStatus: '全部状态',
allGroups: '全部分组',
allPlatforms: '全部平台',
@@ -3076,7 +3077,8 @@ export default {
status: {
active: '生效中',
expired: '已过期',
revoked: '已撤销'
revoked: '已撤销',
suspended: '已暂停'
},
columns: {
user: '用户',
@@ -3105,6 +3107,7 @@ export default {
adjust: '调整',
adjusting: '调整中...',
revoke: '撤销',
restore: '恢复',
resetQuota: '重置配额',
resetQuotaTitle: '重置用量配额',
resetQuotaConfirm: "确定要重置 '{user}' 的每日、每周和每月用量配额吗?用量将归零并从今天开始重新计算。",
@@ -3115,16 +3118,19 @@ export default {
subscriptionAssigned: '订阅分配成功',
subscriptionAdjusted: '订阅调整成功',
subscriptionRevoked: '订阅撤销成功',
subscriptionRestored: '订阅已恢复',
failedToLoad: '加载订阅列表失败',
failedToAssign: '分配订阅失败',
failedToAdjust: '调整订阅失败',
failedToRevoke: '撤销订阅失败',
failedToRestore: '恢复订阅失败',
adjustWouldExpire: '调整后剩余天数必须大于0',
adjustOutOfRange: '调整天数必须在 -36500 到 36500 之间',
pleaseSelectUser: '请选择用户',
pleaseSelectGroup: '请选择分组',
validityDaysRequired: '请输入有效的天数(至少1天)',
revokeConfirm: "确定要撤销 '{user}' 的订阅吗?此操作无法撤销。",
revokeConfirm: "确定要撤销 '{user}' 的订阅吗?可稍后在已撤销列表中恢复。",
restoreConfirm: "确定要恢复 '{user}' 的订阅吗?如果原订阅已过期,恢复后将显示为已过期。",
guide: {
title: '订阅管理教程',
subtitle: '订阅模式允许你按时间周期为用户分配使用额度,支持日/周/月配额限制。按照以下步骤即可完成配置。',
+1 -1
View File
@@ -1602,7 +1602,7 @@ export interface UserSubscription {
id: number
user_id: number
group_id: number
status: 'active' | 'expired' | 'revoked'
status: 'active' | 'expired' | 'revoked' | 'suspended'
starts_at: string
daily_usage_usd: number
weekly_usage_usd: number
@@ -404,6 +404,14 @@
<Icon name="ban" size="sm" />
<span class="text-xs">{{ t('admin.subscriptions.revoke') }}</span>
</button>
<button
v-if="row.status === 'revoked'"
@click="handleRestore(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"
>
<Icon name="refresh" size="sm" />
<span class="text-xs">{{ t('admin.subscriptions.restore') }}</span>
</button>
</div>
</template>
@@ -645,6 +653,17 @@
@cancel="showRevokeDialog = false"
/>
<!-- Restore Confirmation Dialog -->
<ConfirmDialog
:show="showRestoreDialog"
:title="t('admin.subscriptions.restoreSubscription')"
:message="t('admin.subscriptions.restoreConfirm', { user: restoringSubscription?.user?.email })"
:confirm-text="t('admin.subscriptions.restore')"
:cancel-text="t('common.cancel')"
@confirm="confirmRestore"
@cancel="showRestoreDialog = false"
/>
<!-- Reset Quota Confirmation Dialog -->
<ConfirmDialog
:show="showResetQuotaConfirm"
@@ -940,12 +959,14 @@ const pagination = reactive({
const showAssignModal = ref(false)
const showExtendModal = ref(false)
const showRevokeDialog = ref(false)
const showRestoreDialog = ref(false)
const showResetQuotaConfirm = ref(false)
const submitting = ref(false)
const resettingSubscription = ref<UserSubscription | null>(null)
const resettingQuota = ref(false)
const extendingSubscription = ref<UserSubscription | null>(null)
const revokingSubscription = ref<UserSubscription | null>(null)
const restoringSubscription = ref<UserSubscription | null>(null)
const assignForm = reactive({
user_id: null as number | null,
@@ -1261,6 +1282,26 @@ const confirmRevoke = async () => {
}
}
const handleRestore = (subscription: UserSubscription) => {
restoringSubscription.value = subscription
showRestoreDialog.value = true
}
const confirmRestore = async () => {
if (!restoringSubscription.value) return
try {
await adminAPI.subscriptions.restore(restoringSubscription.value.id)
appStore.showSuccess(t('admin.subscriptions.subscriptionRestored'))
showRestoreDialog.value = false
restoringSubscription.value = null
loadSubscriptions()
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.subscriptions.failedToRestore'))
console.error('Error restoring subscription:', error)
}
}
const handleResetQuota = (subscription: UserSubscription) => {
resettingSubscription.value = subscription
showResetQuotaConfirm.value = true