This commit is contained in:
@@ -2,11 +2,16 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
batchUpdateAdminRedemptionCodes,
|
||||
createAdminRedemptionCodes,
|
||||
deleteAdminRedemptionCode,
|
||||
listAdminPlans,
|
||||
listAdminRedemptionCodes,
|
||||
updateAdminRedemptionCode,
|
||||
type AdminPlanView,
|
||||
type AdminRedemptionBatchAction,
|
||||
type AdminRedemptionCodeFilters,
|
||||
type AdminRedemptionCodeStatus,
|
||||
type AdminRedemptionCodeView,
|
||||
type GeneratedRedemptionCode,
|
||||
type RedemptionKind,
|
||||
@@ -17,11 +22,25 @@ import { useAuthStore } from '@/stores/auth'
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const actionMessage = ref<string | null>(null)
|
||||
const plans = ref<AdminPlanView[]>([])
|
||||
const codes = ref<AdminRedemptionCodeView[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const limit = 50
|
||||
const limit = ref(50)
|
||||
|
||||
const filterForm = ref<{
|
||||
keyword: string
|
||||
status: '' | AdminRedemptionCodeStatus
|
||||
benefit_kind: '' | RedemptionKind
|
||||
plan_id: string
|
||||
}>({
|
||||
keyword: '',
|
||||
status: '',
|
||||
benefit_kind: '',
|
||||
plan_id: '',
|
||||
})
|
||||
const activeFilters = ref<AdminRedemptionCodeFilters>({})
|
||||
|
||||
const form = ref({
|
||||
benefit_kind: 'plan' as RedemptionKind,
|
||||
@@ -36,11 +55,21 @@ const generating = ref(false)
|
||||
const generated = ref<GeneratedRedemptionCode[]>([])
|
||||
const generateMessage = ref<string | null>(null)
|
||||
const generateError = ref<string | null>(null)
|
||||
const generatedCopyMessage = ref<string | null>(null)
|
||||
const updatingId = ref<string | null>(null)
|
||||
const deletingId = ref<string | null>(null)
|
||||
const batchBusy = ref(false)
|
||||
const selectedIds = ref<string[]>([])
|
||||
const copiedId = ref<string | null>(null)
|
||||
const copyMessage = ref<string | null>(null)
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / limit)))
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / limit.value)))
|
||||
const activePlans = computed(() => plans.value.filter((plan) => plan.is_active))
|
||||
const generatedText = computed(() => generated.value.map((item) => item.code).join('\n'))
|
||||
const selectedCount = computed(() => selectedIds.value.length)
|
||||
const allPageSelected = computed(
|
||||
() => codes.value.length > 0 && codes.value.every((item) => selectedIds.value.includes(item.id)),
|
||||
)
|
||||
|
||||
function errorText(err: unknown, fallback: string) {
|
||||
return err instanceof ApiError ? `[${err.code}] ${err.message}` : fallback
|
||||
@@ -50,17 +79,18 @@ async function load(targetPage = page.value) {
|
||||
if (!auth.token) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
selectedIds.value = []
|
||||
try {
|
||||
const [planResp, codeResp] = await Promise.all([
|
||||
listAdminPlans(auth.token),
|
||||
listAdminRedemptionCodes(auth.token, targetPage, limit),
|
||||
listAdminRedemptionCodes(auth.token, targetPage, limit.value, activeFilters.value),
|
||||
])
|
||||
plans.value = planResp.plans.filter((plan) => plan.is_active)
|
||||
plans.value = planResp.plans
|
||||
codes.value = codeResp.codes
|
||||
total.value = codeResp.total
|
||||
page.value = codeResp.page
|
||||
if (!form.value.plan_id && plans.value.length > 0) {
|
||||
form.value.plan_id = plans.value[0]?.id ?? ''
|
||||
if (!activePlans.value.some((plan) => plan.id === form.value.plan_id)) {
|
||||
form.value.plan_id = activePlans.value[0]?.id ?? ''
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = errorText(err, '加载兑换码失败')
|
||||
@@ -69,12 +99,30 @@ async function load(targetPage = page.value) {
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
activeFilters.value = {
|
||||
status: filterForm.value.status || undefined,
|
||||
benefit_kind: filterForm.value.benefit_kind || undefined,
|
||||
plan_id:
|
||||
filterForm.value.benefit_kind === 'units' ? undefined : filterForm.value.plan_id || undefined,
|
||||
keyword: filterForm.value.keyword.trim() || undefined,
|
||||
}
|
||||
await load(1)
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
filterForm.value = { keyword: '', status: '', benefit_kind: '', plan_id: '' }
|
||||
activeFilters.value = {}
|
||||
await load(1)
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
if (!auth.token) return
|
||||
generating.value = true
|
||||
generated.value = []
|
||||
generateMessage.value = null
|
||||
generateError.value = null
|
||||
generatedCopyMessage.value = null
|
||||
copyMessage.value = null
|
||||
try {
|
||||
const payload: {
|
||||
@@ -106,13 +154,48 @@ async function generate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyText(text: string, successMessage: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return successMessage
|
||||
} catch {
|
||||
return '自动复制失败,请手动选择文本'
|
||||
}
|
||||
}
|
||||
|
||||
async function copyGenerated() {
|
||||
if (!generatedText.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(generatedText.value)
|
||||
copyMessage.value = '已复制全部兑换码'
|
||||
} catch {
|
||||
copyMessage.value = '自动复制失败,请手动选择文本'
|
||||
generatedCopyMessage.value = await copyText(generatedText.value, '已复制本次生成的全部兑换码')
|
||||
}
|
||||
|
||||
async function copyCode(item: AdminRedemptionCodeView) {
|
||||
if (!item.code) return
|
||||
copyMessage.value = await copyText(item.code, '兑换码已复制')
|
||||
copiedId.value = item.id
|
||||
window.setTimeout(() => {
|
||||
if (copiedId.value === item.id) copiedId.value = null
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
async function copySelected() {
|
||||
const selected = new Set(selectedIds.value)
|
||||
const fullCodes = codes.value
|
||||
.filter((item) => selected.has(item.id) && item.code)
|
||||
.map((item) => item.code as string)
|
||||
if (fullCodes.length === 0) {
|
||||
copyMessage.value = '所选记录均为无法恢复完整内容的历史兑换码'
|
||||
return
|
||||
}
|
||||
copyMessage.value = await copyText(
|
||||
fullCodes.join('\n'),
|
||||
`已复制 ${fullCodes.length} 个兑换码${fullCodes.length < selectedCount.value ? ',历史码已跳过' : ''}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function reloadAfterMutation() {
|
||||
await load(page.value)
|
||||
if (codes.value.length === 0 && page.value > 1) {
|
||||
await load(page.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +203,11 @@ async function toggleCode(item: AdminRedemptionCodeView) {
|
||||
if (!auth.token || item.redeemed_at) return
|
||||
updatingId.value = item.id
|
||||
error.value = null
|
||||
actionMessage.value = null
|
||||
try {
|
||||
await updateAdminRedemptionCode(auth.token, item.id, !item.is_active)
|
||||
item.is_active = !item.is_active
|
||||
const resp = await updateAdminRedemptionCode(auth.token, item.id, !item.is_active)
|
||||
actionMessage.value = resp.message
|
||||
await reloadAfterMutation()
|
||||
} catch (err) {
|
||||
error.value = errorText(err, '更新兑换码失败')
|
||||
} finally {
|
||||
@@ -130,6 +215,50 @@ async function toggleCode(item: AdminRedemptionCodeView) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCode(item: AdminRedemptionCodeView) {
|
||||
if (!auth.token || item.redeemed_at) return
|
||||
const displayCode = item.code ?? item.code_hint
|
||||
if (!window.confirm(`确定删除兑换码 ${displayCode} 吗?删除后无法恢复。`)) return
|
||||
deletingId.value = item.id
|
||||
error.value = null
|
||||
actionMessage.value = null
|
||||
try {
|
||||
const resp = await deleteAdminRedemptionCode(auth.token, item.id)
|
||||
actionMessage.value = resp.message
|
||||
await reloadAfterMutation()
|
||||
} catch (err) {
|
||||
error.value = errorText(err, '删除兑换码失败')
|
||||
} finally {
|
||||
deletingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function runBatch(action: AdminRedemptionBatchAction) {
|
||||
if (!auth.token || selectedIds.value.length === 0) return
|
||||
if (
|
||||
action === 'delete' &&
|
||||
!window.confirm(`确定批量删除选中的 ${selectedIds.value.length} 条记录吗?已兑换记录会自动跳过。`)
|
||||
) {
|
||||
return
|
||||
}
|
||||
batchBusy.value = true
|
||||
error.value = null
|
||||
actionMessage.value = null
|
||||
try {
|
||||
const resp = await batchUpdateAdminRedemptionCodes(auth.token, selectedIds.value, action)
|
||||
actionMessage.value = resp.message
|
||||
await reloadAfterMutation()
|
||||
} catch (err) {
|
||||
error.value = errorText(err, '批量操作失败')
|
||||
} finally {
|
||||
batchBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function togglePageSelection() {
|
||||
selectedIds.value = allPageSelected.value ? [] : codes.value.map((item) => item.id)
|
||||
}
|
||||
|
||||
function statusLabel(item: AdminRedemptionCodeView) {
|
||||
if (item.redeemed_at) return '已兑换'
|
||||
if (!item.is_active) return '已停用'
|
||||
@@ -137,6 +266,14 @@ function statusLabel(item: AdminRedemptionCodeView) {
|
||||
return '待兑换'
|
||||
}
|
||||
|
||||
function statusClass(item: AdminRedemptionCodeView) {
|
||||
const status = statusLabel(item)
|
||||
if (status === '待兑换') return 'bg-emerald-50 text-emerald-700'
|
||||
if (status === '已兑换') return 'bg-sky-50 text-sky-700'
|
||||
if (status === '已过期') return 'bg-amber-50 text-amber-700'
|
||||
return 'bg-slate-100 text-slate-600'
|
||||
}
|
||||
|
||||
function benefitLabel(item: AdminRedemptionCodeView) {
|
||||
return item.benefit_kind === 'plan' ? item.plan_name ?? '未知套餐' : `${item.units ?? 0} 次`
|
||||
}
|
||||
@@ -148,12 +285,15 @@ onMounted(() => load())
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-lg font-semibold text-slate-900">兑换码</h2>
|
||||
<p class="text-sm text-slate-600">生成套餐卡或限时次数卡。完整兑换码只在生成后显示一次。</p>
|
||||
<p class="text-sm text-slate-600">新兑换码会加密保存,可随时查看、复制、筛选和批量管理。</p>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-if="actionMessage" class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900">
|
||||
{{ actionMessage }}
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-5">
|
||||
<div class="text-sm font-medium text-slate-900">生成兑换码</div>
|
||||
@@ -168,7 +308,7 @@ onMounted(() => load())
|
||||
<label v-if="form.benefit_kind === 'plan'" class="space-y-1">
|
||||
<div class="text-xs font-medium text-slate-600">对应套餐</div>
|
||||
<select v-model="form.plan_id" class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm" required>
|
||||
<option v-for="plan in plans" :key="plan.id" :value="plan.id">{{ plan.name }}</option>
|
||||
<option v-for="plan in activePlans" :key="plan.id" :value="plan.id">{{ plan.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-else class="space-y-1">
|
||||
@@ -197,57 +337,145 @@ onMounted(() => load())
|
||||
</button>
|
||||
<div v-if="generateError" class="mt-3 rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-900">{{ generateError }}</div>
|
||||
|
||||
<div v-if="generated.length > 0" class="mt-4 rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<div v-if="generated.length > 0" class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-amber-950">{{ generateMessage }}</div>
|
||||
<div class="text-xs text-amber-800">离开或刷新页面后无法再次查看完整兑换码。</div>
|
||||
<div class="text-sm font-medium text-emerald-950">{{ generateMessage }}</div>
|
||||
<div class="text-xs text-emerald-800">下方便于本次批量复制,之后仍可在兑换码记录中查看。</div>
|
||||
</div>
|
||||
<button type="button" class="rounded-md border border-amber-300 bg-white px-3 py-1.5 text-xs text-amber-900" @click="copyGenerated">复制全部</button>
|
||||
<button type="button" class="rounded-md border border-emerald-300 bg-white px-3 py-1.5 text-xs text-emerald-900" @click="copyGenerated">复制全部</button>
|
||||
</div>
|
||||
<textarea :value="generatedText" readonly class="mt-3 h-36 w-full rounded-md border border-amber-200 bg-white px-3 py-2 font-mono text-sm text-slate-800"></textarea>
|
||||
<div v-if="copyMessage" class="mt-2 text-xs text-amber-900">{{ copyMessage }}</div>
|
||||
<textarea :value="generatedText" readonly class="mt-3 h-36 w-full rounded-md border border-emerald-200 bg-white px-3 py-2 font-mono text-sm text-slate-800"></textarea>
|
||||
<div v-if="generatedCopyMessage" class="mt-2 text-xs text-emerald-900">{{ generatedCopyMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-medium text-slate-900">兑换码记录({{ total }})</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-slate-900">兑换码记录({{ total }})</div>
|
||||
<div class="mt-1 text-xs text-slate-500">旧版本生成的兑换码没有保存完整内容,只能继续显示脱敏标识。</div>
|
||||
</div>
|
||||
<button type="button" class="text-xs text-indigo-600 hover:text-indigo-700" @click="load(page)">刷新</button>
|
||||
</div>
|
||||
|
||||
<form class="mt-4 grid grid-cols-1 gap-3 rounded-lg bg-slate-50 p-4 md:grid-cols-5" @submit.prevent="applyFilters">
|
||||
<label class="space-y-1 md:col-span-2">
|
||||
<div class="text-xs font-medium text-slate-600">关键词</div>
|
||||
<input v-model="filterForm.keyword" type="search" maxlength="100" class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm" placeholder="完整码、标识、备注或兑换人" />
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<div class="text-xs font-medium text-slate-600">状态</div>
|
||||
<select v-model="filterForm.status" class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm">
|
||||
<option value="">全部状态</option>
|
||||
<option value="available">待兑换</option>
|
||||
<option value="redeemed">已兑换</option>
|
||||
<option value="disabled">已停用</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<div class="text-xs font-medium text-slate-600">类型</div>
|
||||
<select v-model="filterForm.benefit_kind" class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm">
|
||||
<option value="">全部类型</option>
|
||||
<option value="plan">套餐卡</option>
|
||||
<option value="units">次数卡</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<div class="text-xs font-medium text-slate-600">套餐</div>
|
||||
<select v-model="filterForm.plan_id" :disabled="filterForm.benefit_kind === 'units'" class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm disabled:bg-slate-100">
|
||||
<option value="">全部套餐</option>
|
||||
<option v-for="plan in plans" :key="plan.id" :value="plan.id">{{ plan.name }}{{ plan.is_active ? '' : '(已停用)' }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-end gap-2 md:col-span-5">
|
||||
<button type="submit" class="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800">查询</button>
|
||||
<button type="button" class="rounded-md border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700" @click="resetFilters">重置</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="selectedCount > 0" class="mt-4 flex flex-wrap items-center gap-2 rounded-lg border border-indigo-100 bg-indigo-50 px-3 py-2">
|
||||
<span class="mr-2 text-xs font-medium text-indigo-900">已选择 {{ selectedCount }} 条</span>
|
||||
<button type="button" class="rounded bg-white px-3 py-1.5 text-xs text-indigo-700" :disabled="batchBusy" @click="copySelected">复制完整码</button>
|
||||
<button type="button" class="rounded bg-white px-3 py-1.5 text-xs text-emerald-700 disabled:opacity-50" :disabled="batchBusy" @click="runBatch('enable')">批量启用</button>
|
||||
<button type="button" class="rounded bg-white px-3 py-1.5 text-xs text-amber-700 disabled:opacity-50" :disabled="batchBusy" @click="runBatch('disable')">批量停用</button>
|
||||
<button type="button" class="rounded bg-white px-3 py-1.5 text-xs text-rose-700 disabled:opacity-50" :disabled="batchBusy" @click="runBatch('delete')">批量删除</button>
|
||||
<button type="button" class="ml-auto text-xs text-indigo-600" :disabled="batchBusy" @click="selectedIds = []">取消选择</button>
|
||||
</div>
|
||||
<div v-if="copyMessage" class="mt-3 text-xs text-slate-600">{{ copyMessage }}</div>
|
||||
|
||||
<div v-if="loading" class="mt-4 text-sm text-slate-600">加载中…</div>
|
||||
<div v-else-if="codes.length === 0" class="mt-4 text-sm text-slate-600">暂无兑换码</div>
|
||||
<div v-else-if="codes.length === 0" class="mt-4 text-sm text-slate-600">没有符合条件的兑换码</div>
|
||||
<div v-else class="mt-4 overflow-auto">
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<table class="min-w-[1180px] text-left text-sm">
|
||||
<thead class="text-xs text-slate-500">
|
||||
<tr>
|
||||
<th class="py-2 pr-4">标识</th><th class="py-2 pr-4">权益</th><th class="py-2 pr-4">时长</th>
|
||||
<th class="py-2 pr-4">状态</th><th class="py-2 pr-4">兑换人</th><th class="py-2 pr-4">截止时间</th>
|
||||
<th class="w-10 py-2 pr-3">
|
||||
<input type="checkbox" :checked="allPageSelected" aria-label="选择本页兑换码" @change="togglePageSelection" />
|
||||
</th>
|
||||
<th class="py-2 pr-4">兑换码</th>
|
||||
<th class="py-2 pr-4">权益</th>
|
||||
<th class="py-2 pr-4">时长</th>
|
||||
<th class="py-2 pr-4">状态</th>
|
||||
<th class="py-2 pr-4">兑换人</th>
|
||||
<th class="py-2 pr-4">生成时间</th>
|
||||
<th class="py-2 pr-4">截止时间</th>
|
||||
<th class="py-2 pr-4">备注</th>
|
||||
<th class="py-2 pr-4">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-slate-700">
|
||||
<tr v-for="item in codes" :key="item.id" class="border-t border-slate-100">
|
||||
<td class="py-2 pr-4 font-mono text-xs">{{ item.code_hint }}</td>
|
||||
<td class="py-2 pr-4">{{ benefitLabel(item) }}</td>
|
||||
<td class="py-2 pr-4">{{ item.duration_days }} 天</td>
|
||||
<td class="py-2 pr-4">{{ statusLabel(item) }}</td>
|
||||
<td class="py-2 pr-4">{{ item.redeemed_username ?? '—' }}</td>
|
||||
<td class="py-2 pr-4">{{ item.redeem_before ? new Date(item.redeem_before).toLocaleString() : '不限' }}</td>
|
||||
<td class="py-2 pr-4">
|
||||
<button v-if="!item.redeemed_at" type="button" class="text-xs text-indigo-600 disabled:opacity-50" :disabled="updatingId === item.id" @click="toggleCode(item)">
|
||||
{{ item.is_active ? '停用' : '启用' }}
|
||||
</button>
|
||||
<span v-else class="text-xs text-slate-400">不可修改</span>
|
||||
<tr v-for="item in codes" :key="item.id" class="border-t border-slate-100 align-top">
|
||||
<td class="py-3 pr-3">
|
||||
<input v-model="selectedIds" type="checkbox" :value="item.id" :aria-label="`选择 ${item.code ?? item.code_hint}`" />
|
||||
</td>
|
||||
<td class="py-3 pr-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="whitespace-nowrap rounded bg-slate-100 px-2 py-1 text-xs text-slate-800">{{ item.code ?? item.code_hint }}</code>
|
||||
<button v-if="item.code" type="button" class="text-xs text-indigo-600" @click="copyCode(item)">{{ copiedId === item.id ? '已复制' : '复制' }}</button>
|
||||
</div>
|
||||
<div v-if="!item.code" class="mt-1 text-[11px] text-amber-600">历史码无法恢复完整内容</div>
|
||||
</td>
|
||||
<td class="py-3 pr-4">{{ benefitLabel(item) }}</td>
|
||||
<td class="py-3 pr-4">{{ item.duration_days }} 天</td>
|
||||
<td class="py-3 pr-4">
|
||||
<span class="rounded-full px-2 py-1 text-xs" :class="statusClass(item)">{{ statusLabel(item) }}</span>
|
||||
</td>
|
||||
<td class="py-3 pr-4">{{ item.redeemed_username ?? '—' }}</td>
|
||||
<td class="whitespace-nowrap py-3 pr-4 text-xs">{{ new Date(item.created_at).toLocaleString() }}</td>
|
||||
<td class="whitespace-nowrap py-3 pr-4 text-xs">{{ item.redeem_before ? new Date(item.redeem_before).toLocaleString() : '不限' }}</td>
|
||||
<td class="max-w-48 py-3 pr-4 text-xs text-slate-500">{{ item.note ?? '—' }}</td>
|
||||
<td class="py-3 pr-4">
|
||||
<div v-if="!item.redeemed_at" class="flex items-center gap-3 whitespace-nowrap">
|
||||
<button type="button" class="text-xs text-indigo-600 disabled:opacity-50" :disabled="updatingId === item.id || deletingId === item.id" @click="toggleCode(item)">
|
||||
{{ item.is_active ? '停用' : '启用' }}
|
||||
</button>
|
||||
<button type="button" class="text-xs text-rose-600 disabled:opacity-50" :disabled="updatingId === item.id || deletingId === item.id" @click="deleteCode(item)">
|
||||
{{ deletingId === item.id ? '删除中…' : '删除' }}
|
||||
</button>
|
||||
</div>
|
||||
<span v-else class="text-xs text-slate-400">保留记录</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-between text-xs text-slate-500">
|
||||
<button type="button" class="rounded border border-slate-200 px-3 py-1.5 disabled:opacity-40" :disabled="page <= 1 || loading" @click="load(page - 1)">上一页</button>
|
||||
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
||||
<button type="button" class="rounded border border-slate-200 px-3 py-1.5 disabled:opacity-40" :disabled="page >= totalPages || loading" @click="load(page + 1)">下一页</button>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-between gap-3 text-xs text-slate-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>每页</span>
|
||||
<select v-model.number="limit" class="rounded border border-slate-200 bg-white px-2 py-1" @change="load(1)">
|
||||
<option :value="20">20</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
</select>
|
||||
<span>条</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" class="rounded border border-slate-200 px-3 py-1.5 disabled:opacity-40" :disabled="page <= 1 || loading" @click="load(page - 1)">上一页</button>
|
||||
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
||||
<button type="button" class="rounded border border-slate-200 px-3 py-1.5 disabled:opacity-40" :disabled="page >= totalPages || loading" @click="load(page + 1)">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user