Merge pull request #2573 from wucm667/feat/redeem-code-expiry

feat(redeem): 兑换码支持设置使用有效期
This commit is contained in:
Wesley Liddick
2026-05-19 16:25:12 +08:00
committed by GitHub
27 changed files with 676 additions and 38 deletions
+82 -2
View File
@@ -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'))