feat(ECR-015): RedemptionCode 兑换码并 Closed
批次生成/作废、C 端兑码延长会员;admin-h5 /codes。 Loop continuous。Next:ECR-016 UserIntelligence。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { adminApi } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const msg = ref('')
|
||||
const label = ref('batch')
|
||||
const plan = ref('month')
|
||||
const qty = ref(5)
|
||||
const batches = ref<Array<{ id: string; label: string; plan_code: string; quantity: number; created_at: string }>>([])
|
||||
const codes = ref<Array<{ id: string; code: string; status: string }>>([])
|
||||
const activeBatch = ref('')
|
||||
|
||||
async function loadBatches() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await adminApi.redemptionBatches()
|
||||
batches.value = res.items || []
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createBatch() {
|
||||
msg.value = ''
|
||||
try {
|
||||
const res = await adminApi.createRedemptionBatch({
|
||||
label: label.value,
|
||||
plan_code: plan.value,
|
||||
quantity: Number(qty.value),
|
||||
})
|
||||
msg.value = `已生成 ${res.codes.length} 个码`
|
||||
activeBatch.value = res.batch.id
|
||||
codes.value = res.codes
|
||||
await loadBatches()
|
||||
} catch (e) {
|
||||
msg.value = e instanceof Error ? e.message : '生成失败'
|
||||
}
|
||||
}
|
||||
|
||||
async function openBatch(id: string) {
|
||||
activeBatch.value = id
|
||||
const res = await adminApi.redemptionCodes(id)
|
||||
codes.value = res.items || []
|
||||
}
|
||||
|
||||
async function disable(id: string) {
|
||||
await adminApi.disableRedemptionCode(id)
|
||||
if (activeBatch.value) await openBatch(activeBatch.value)
|
||||
}
|
||||
|
||||
onMounted(loadBatches)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h1>兑换码</h1>
|
||||
<p class="muted">批量生成会员兑换码(禁真支付)。</p>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
<p v-if="msg" class="muted">{{ msg }}</p>
|
||||
|
||||
<div v-if="auth.can('admin.membership.codes.write')" class="card block">
|
||||
<h2>生成批次</h2>
|
||||
<div class="row">
|
||||
<label>标签 <input v-model="label" /></label>
|
||||
<label>套餐
|
||||
<select v-model="plan">
|
||||
<option value="month">month</option>
|
||||
<option value="quarter">quarter</option>
|
||||
<option value="year">year</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>数量 <input v-model.number="qty" type="number" min="1" max="100" /></label>
|
||||
<button class="btn" type="button" @click="createBatch">生成</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card block">
|
||||
<h2>批次</h2>
|
||||
<p v-if="loading" class="muted">加载中…</p>
|
||||
<ul v-else>
|
||||
<li v-for="b in batches" :key="b.id">
|
||||
<button class="link" type="button" @click="openBatch(b.id)">
|
||||
{{ b.label }} · {{ b.plan_code }} × {{ b.quantity }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="codes.length" class="card block">
|
||||
<h2>码列表</h2>
|
||||
<table>
|
||||
<thead><tr><th>码</th><th>状态</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="c in codes" :key="c.id">
|
||||
<td><code>{{ c.code }}</code></td>
|
||||
<td>{{ c.status }}</td>
|
||||
<td>
|
||||
<button
|
||||
v-if="c.status === 'unused' && auth.can('admin.membership.codes.write')"
|
||||
class="btn ghost"
|
||||
type="button"
|
||||
@click="disable(c.id)"
|
||||
>
|
||||
作废
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 { margin: 0 0 0.35rem; font-size: 1.35rem; }
|
||||
h2 { margin: 0 0 0.6rem; font-size: 1.05rem; }
|
||||
.block { margin-bottom: 1rem; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: end; }
|
||||
label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.8rem; color: var(--muted); }
|
||||
input, select { border: 1px solid var(--line); border-radius: 8px; padding: 0.45rem 0.6rem; }
|
||||
.link { background: none; border: 0; color: var(--accent); cursor: pointer; padding: 0.2rem 0; }
|
||||
ul { margin: 0; padding-left: 1rem; }
|
||||
code { font-size: 0.85rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user