mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-26 08:02:08 +00:00
Merge pull request #2573 from wucm667/feat/redeem-code-expiry
feat(redeem): 兑换码支持设置使用有效期
This commit is contained in:
@@ -60,6 +60,7 @@ export async function getById(id: number): Promise<RedeemCode> {
|
||||
* @param value - Value of the code
|
||||
* @param groupId - Group ID (required for subscription type)
|
||||
* @param validityDays - Validity days (for subscription type)
|
||||
* @param expiresInDays - Days before the code itself expires
|
||||
* @returns Array of generated redeem codes
|
||||
*/
|
||||
export async function generate(
|
||||
@@ -67,7 +68,8 @@ export async function generate(
|
||||
type: RedeemCodeType,
|
||||
value: number,
|
||||
groupId?: number | null,
|
||||
validityDays?: number
|
||||
validityDays?: number,
|
||||
expiresInDays?: number | null
|
||||
): Promise<RedeemCode[]> {
|
||||
const payload: GenerateRedeemCodesRequest = {
|
||||
count,
|
||||
@@ -82,6 +84,9 @@ export async function generate(
|
||||
payload.validity_days = validityDays
|
||||
}
|
||||
}
|
||||
if (expiresInDays && expiresInDays > 0) {
|
||||
payload.expires_in_days = expiresInDays
|
||||
}
|
||||
|
||||
const { data } = await apiClient.post<RedeemCode[]>('/admin/redeem-codes/generate', payload)
|
||||
return data
|
||||
|
||||
@@ -4115,6 +4115,7 @@ export default {
|
||||
status: 'Status',
|
||||
usedBy: 'Used By',
|
||||
usedAt: 'Used At',
|
||||
expiresAt: 'Expires At',
|
||||
actions: 'Actions'
|
||||
},
|
||||
userPrefix: 'User #{id}',
|
||||
@@ -4160,6 +4161,12 @@ export default {
|
||||
selectGroup: 'Select Group',
|
||||
selectGroupPlaceholder: 'Choose a subscription group',
|
||||
validityDays: 'Validity Days',
|
||||
codeExpiry: 'Code Expiry',
|
||||
neverExpires: 'Never expires',
|
||||
expiryPresetDays: '{days} days',
|
||||
customExpiry: 'Custom',
|
||||
customExpiryDays: 'Custom days',
|
||||
expiryDaysRequired: 'Please enter a valid expiry day count',
|
||||
groupRequired: 'Please select a subscription group',
|
||||
days: ' days',
|
||||
status: {
|
||||
|
||||
@@ -4229,6 +4229,7 @@ export default {
|
||||
status: '状态',
|
||||
usedBy: '使用者',
|
||||
usedAt: '使用时间',
|
||||
expiresAt: '过期时间',
|
||||
createdAt: '创建时间',
|
||||
actions: '操作'
|
||||
},
|
||||
@@ -4278,6 +4279,12 @@ export default {
|
||||
selectGroup: '选择分组',
|
||||
selectGroupPlaceholder: '选择订阅分组',
|
||||
validityDays: '有效天数',
|
||||
codeExpiry: '兑换码过期',
|
||||
neverExpires: '永不过期',
|
||||
expiryPresetDays: '{days} 天',
|
||||
customExpiry: '自定义',
|
||||
customExpiryDays: '自定义天数',
|
||||
expiryDaysRequired: '请输入有效的过期天数',
|
||||
groupRequired: '请选择订阅分组',
|
||||
days: '天',
|
||||
status: {
|
||||
|
||||
@@ -1293,6 +1293,7 @@ export interface RedeemCode {
|
||||
used_by: number | null
|
||||
used_at: string | null
|
||||
created_at: string
|
||||
expires_at?: string | null
|
||||
updated_at?: string
|
||||
group_id?: number | null // 订阅类型专用
|
||||
validity_days?: number // 订阅类型专用
|
||||
@@ -1306,6 +1307,8 @@ export interface GenerateRedeemCodesRequest {
|
||||
value: number
|
||||
group_id?: number | null // 订阅类型专用
|
||||
validity_days?: number // 订阅类型专用
|
||||
expires_at?: string | null
|
||||
expires_in_days?: number
|
||||
}
|
||||
|
||||
export interface RedeemCodeRequest {
|
||||
|
||||
@@ -137,6 +137,19 @@
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-expires_at="{ value, row }">
|
||||
<span
|
||||
:class="[
|
||||
'text-sm',
|
||||
row.status === 'expired'
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-gray-500 dark:text-dark-400'
|
||||
]"
|
||||
>
|
||||
{{ value ? formatDateTime(value) : t('admin.redeem.neverExpires') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@@ -287,6 +300,35 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.redeem.codeExpiry') }}</label>
|
||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-5">
|
||||
<button
|
||||
v-for="option in redeemCodeExpiryOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
@click="generateForm.expiry_option = option.value"
|
||||
:class="[
|
||||
'rounded-lg border px-3 py-2 text-sm transition-colors',
|
||||
generateForm.expiry_option === option.value
|
||||
? 'border-primary-500 bg-primary-50 text-primary-700 dark:border-primary-400 dark:bg-primary-900/20 dark:text-primary-300'
|
||||
: 'border-gray-200 text-gray-700 hover:bg-gray-50 dark:border-dark-600 dark:text-gray-300 dark:hover:bg-dark-700'
|
||||
]"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-if="generateForm.expiry_option === 'custom'"
|
||||
v-model.number="generateForm.custom_expiry_days"
|
||||
type="number"
|
||||
min="1"
|
||||
max="3650"
|
||||
required
|
||||
class="input mt-2"
|
||||
:placeholder="t('admin.redeem.customExpiryDays')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.redeem.count') }}</label>
|
||||
<input
|
||||
@@ -504,6 +546,7 @@ const columns = computed<Column[]>(() => [
|
||||
{ key: 'status', label: t('admin.redeem.columns.status'), sortable: true },
|
||||
{ key: 'used_by', label: t('admin.redeem.columns.usedBy') },
|
||||
{ key: 'used_at', label: t('admin.redeem.columns.usedAt'), sortable: true },
|
||||
{ key: 'expires_at', label: t('admin.redeem.columns.expiresAt'), sortable: true },
|
||||
{ key: 'actions', label: t('admin.redeem.columns.actions') }
|
||||
])
|
||||
|
||||
@@ -555,12 +598,24 @@ const showDeleteUnusedDialog = ref(false)
|
||||
const deletingCode = ref<RedeemCode | null>(null)
|
||||
const copiedCode = ref<string | null>(null)
|
||||
|
||||
type RedeemCodeExpiryOption = 'never' | '1' | '3' | '7' | 'custom'
|
||||
|
||||
const redeemCodeExpiryOptions = computed<{ value: RedeemCodeExpiryOption; label: string }[]>(() => [
|
||||
{ value: 'never', label: t('admin.redeem.neverExpires') },
|
||||
{ value: '1', label: t('admin.redeem.expiryPresetDays', { days: 1 }) },
|
||||
{ value: '3', label: t('admin.redeem.expiryPresetDays', { days: 3 }) },
|
||||
{ value: '7', label: t('admin.redeem.expiryPresetDays', { days: 7 }) },
|
||||
{ value: 'custom', label: t('admin.redeem.customExpiry') }
|
||||
])
|
||||
|
||||
const generateForm = reactive({
|
||||
type: 'balance' as RedeemCodeType,
|
||||
value: 10,
|
||||
count: 1,
|
||||
group_id: null as number | null,
|
||||
validity_days: 30
|
||||
validity_days: 30,
|
||||
expiry_option: 'never' as RedeemCodeExpiryOption,
|
||||
custom_expiry_days: 7
|
||||
})
|
||||
|
||||
// 监听类型变化,邀请码类型时自动设置 value 为 0
|
||||
@@ -650,6 +705,22 @@ const handleSort = (key: string, order: 'asc' | 'desc') => {
|
||||
loadCodes()
|
||||
}
|
||||
|
||||
const getRedeemCodeExpiresInDays = () => {
|
||||
if (generateForm.expiry_option === 'never') {
|
||||
return undefined
|
||||
}
|
||||
if (generateForm.expiry_option === 'custom') {
|
||||
if (
|
||||
!Number.isFinite(generateForm.custom_expiry_days) ||
|
||||
generateForm.custom_expiry_days < 1
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return Math.floor(generateForm.custom_expiry_days)
|
||||
}
|
||||
return Number(generateForm.expiry_option)
|
||||
}
|
||||
|
||||
const handleGenerateCodes = async () => {
|
||||
// 订阅类型必须选择分组
|
||||
if (generateForm.type === 'subscription' && !generateForm.group_id) {
|
||||
@@ -657,6 +728,12 @@ const handleGenerateCodes = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const expiresInDays = getRedeemCodeExpiresInDays()
|
||||
if (expiresInDays === null) {
|
||||
appStore.showError(t('admin.redeem.expiryDaysRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
generating.value = true
|
||||
try {
|
||||
const result = await adminAPI.redeem.generate(
|
||||
@@ -664,7 +741,8 @@ const handleGenerateCodes = async () => {
|
||||
generateForm.type,
|
||||
generateForm.value,
|
||||
generateForm.type === 'subscription' ? generateForm.group_id : undefined,
|
||||
generateForm.type === 'subscription' ? generateForm.validity_days : undefined
|
||||
generateForm.type === 'subscription' ? generateForm.validity_days : undefined,
|
||||
expiresInDays
|
||||
)
|
||||
showGenerateDialog.value = false
|
||||
generatedCodes.value = result
|
||||
@@ -672,6 +750,8 @@ const handleGenerateCodes = async () => {
|
||||
// 重置表单
|
||||
generateForm.group_id = null
|
||||
generateForm.validity_days = 30
|
||||
generateForm.expiry_option = 'never'
|
||||
generateForm.custom_expiry_days = 7
|
||||
loadCodes()
|
||||
} catch (error: any) {
|
||||
appStore.showError(error.response?.data?.detail || t('admin.redeem.failedToGenerate'))
|
||||
|
||||
Reference in New Issue
Block a user