复用 admin.explore.write、POST/PUT+审计、C端 GET /cards/decks、 H5 无 active 空态/失败回退;migration 000054。无牌面内容编辑。 ExploreConfig Loop STOP,禁自动 ECR-046。 Co-authored-by: Cursor <cursoragent@cursor.com>
166 lines
4.8 KiB
Vue
166 lines
4.8 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
import { adminApi } from '@/api/client'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
type Row = Awaited<ReturnType<typeof adminApi.imageCardDecks>>['items'][number]
|
|
|
|
const auth = useAuthStore()
|
|
const canWrite = () => auth.can('admin.explore.write')
|
|
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const items = ref<Row[]>([])
|
|
const selected = ref<Row | null>(null)
|
|
const saving = ref(false)
|
|
const formErr = ref('')
|
|
const form = reactive({
|
|
code: '',
|
|
title: '',
|
|
active: true,
|
|
})
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const res = await adminApi.imageCardDecks()
|
|
items.value = res.items || []
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '加载失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function resetForm() {
|
|
form.code = ''
|
|
form.title = ''
|
|
form.active = true
|
|
selected.value = null
|
|
formErr.value = ''
|
|
}
|
|
|
|
async function openRow(id: string) {
|
|
formErr.value = ''
|
|
try {
|
|
const row = await adminApi.imageCardDeck(id)
|
|
selected.value = row
|
|
form.code = row.code
|
|
form.title = row.title
|
|
form.active = row.active
|
|
} catch {
|
|
selected.value = null
|
|
}
|
|
}
|
|
|
|
function payload() {
|
|
return {
|
|
code: form.code.trim(),
|
|
title: form.title.trim(),
|
|
active: form.active,
|
|
}
|
|
}
|
|
|
|
async function createRow() {
|
|
if (!canWrite()) return
|
|
saving.value = true
|
|
formErr.value = ''
|
|
try {
|
|
const row = await adminApi.createImageCardDeck(payload())
|
|
await load()
|
|
await openRow(row.id)
|
|
} catch (e) {
|
|
formErr.value = e instanceof Error ? e.message : '创建失败'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function saveRow() {
|
|
if (!selected.value || !canWrite()) return
|
|
saving.value = true
|
|
formErr.value = ''
|
|
try {
|
|
const row = await adminApi.updateImageCardDeck(selected.value.id, payload())
|
|
selected.value = row
|
|
await load()
|
|
} catch (e) {
|
|
formErr.value = e instanceof Error ? e.message : '保存失败'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleActive() {
|
|
if (!selected.value || !canWrite()) return
|
|
form.active = !form.active
|
|
await saveRow()
|
|
}
|
|
|
|
onMounted(() => {
|
|
void load()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<section>
|
|
<h1>意象牌组</h1>
|
|
<p class="muted">ImageCardDeck 写面 · 下架仅 active=false · ECR-045</p>
|
|
<p v-if="!canWrite()" class="err">需要 admin.explore.write 才可写</p>
|
|
<p v-if="loading" class="muted">加载中…</p>
|
|
<p v-else-if="error" class="err">{{ error }}</p>
|
|
<div v-else class="layout">
|
|
<div class="card">
|
|
<h2>列表</h2>
|
|
<button v-if="canWrite()" class="btn" type="button" @click="resetForm">新建</button>
|
|
<p v-if="!items.length" class="muted">暂无</p>
|
|
<table v-else>
|
|
<thead>
|
|
<tr><th>代码</th><th>标题</th><th>状态</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="b in items" :key="b.id">
|
|
<td><code>{{ b.code }}</code></td>
|
|
<td>{{ b.title }}</td>
|
|
<td>{{ b.active ? '启用' : '停用' }}</td>
|
|
<td><button class="btn" type="button" @click="openRow(b.id)">编辑</button></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h2>{{ selected ? '编辑' : '新建' }}</h2>
|
|
<label>代码 <input v-model="form.code" :disabled="!!selected?.system || !canWrite()" /></label>
|
|
<label>标题 <input v-model="form.title" :disabled="!canWrite()" /></label>
|
|
<label class="check"><input v-model="form.active" type="checkbox" :disabled="!canWrite()" /> 启用</label>
|
|
<p v-if="formErr" class="err">{{ formErr }}</p>
|
|
<div v-if="canWrite()" class="actions">
|
|
<button v-if="!selected" class="btn primary" type="button" :disabled="saving" @click="createRow">
|
|
创建
|
|
</button>
|
|
<template v-else>
|
|
<button class="btn primary" type="button" :disabled="saving" @click="saveRow">保存</button>
|
|
<button class="btn" type="button" :disabled="saving" @click="toggleActive">
|
|
{{ form.active ? '下架' : '上架' }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</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; }
|
|
.layout { display: grid; grid-template-columns: 1.2fr 1fr; gap: 1rem; margin-top: 1rem; }
|
|
code { font-size: 0.8rem; }
|
|
.card label { display: block; margin: 0.4rem 0; font-size: 0.9rem; }
|
|
.card input { width: 100%; margin-top: 0.2rem; }
|
|
.check { display: flex; align-items: center; gap: 0.4rem; }
|
|
.actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
|
|
.btn.primary { font-weight: 600; }
|
|
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
|
</style>
|