chore: establish V1.1 recovery baseline

This commit is contained in:
2026-09-09 22:05:00 +08:00
commit b46b48eb33
947 changed files with 155471 additions and 0 deletions
@@ -0,0 +1,144 @@
<template>
<div class="banner-management">
<el-card>
<template #header>
<div class="card-header">
<span>首页 Banner 管理</span>
<el-button type="primary" @click="openCreate">新建 Banner</el-button>
</div>
</template>
<el-table :data="banners" v-loading="loading" stripe>
<el-table-column prop="title" label="标题" width="150" />
<el-table-column prop="target_type" label="跳转类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ bannerTargetTypeLabel(row.target_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="audience_scope" label="展示范围" width="120">
<template #default="{ row }">
<el-tag size="small" :type="audienceScopeTag(row.audience_scope)">{{ audienceScopeLabel(row.audience_scope) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort_order" label="排序" width="80" />
<el-table-column label="时间范围" width="200">
<template #default="{ row }">
<span>{{ formatDateTime(row.start_at) }} ~ {{ formatDateTime(row.end_at) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="warning" @click="toggleStatus(row)">
{{ row.status === 'active' ? '下架' : '上架' }}
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新建/编辑 Dialog -->
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑 Banner' : '新建 Banner'" width="600px">
<el-form :model="form" label-width="100px">
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
<el-form-item label="副标题"><el-input v-model="form.subtitle" /></el-form-item>
<el-form-item label="跳转类型">
<el-select v-model="form.target_type">
<el-option label="公益活动" value="activity" />
<el-option label="公益红娘" value="matchmaker" />
<el-option label="婚恋咨询" value="counseling" />
<el-option label="婚前辅导" value="premarital" />
<el-option label="善缘服务站" value="service_location" />
<el-option label="文章内容" value="article" />
<el-option label="内部页面" value="internal_page" />
<el-option label="无" value="none" />
</el-select>
</el-form-item>
<el-form-item label="目标值"><el-input v-model="form.target_value" placeholder="活动ID/文章ID等" /></el-form-item>
<el-form-item label="展示范围">
<el-select v-model="form.audience_scope">
<el-option label="全部用户" value="all" />
<el-option label="未注册" value="onboarding" />
<el-option label="正在寻找" value="searching" />
<el-option label="初步了解" value="getting_to_know" />
<el-option label="成功牵手" value="successful" />
</el-select>
</el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort_order" :min="0" /></el-form-item>
<el-form-item label="展示开始"><el-date-picker v-model="form.startAt" type="datetime" /></el-form-item>
<el-form-item label="展示结束"><el-date-picker v-model="form.endAt" type="datetime" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveBanner">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { get, post, put } from '@/api/client'
import { ElMessage } from 'element-plus'
const loading = ref(false)
const banners = ref<any[]>([])
const dialogVisible = ref(false)
const editingId = ref<string | null>(null)
const form = ref({ title: '', subtitle: '', target_type: 'none', target_value: '', audience_scope: 'all', sort_order: 0, start_at: '', end_at: '' })
const TARGET_TYPE_LABEL: Record<string, string> = { activity: '活动', matchmaker: '红娘', counseling: '咨询', premarital: '婚前辅导', service_location: '服务站', article: '文章', internal_page: '内部页', none: '无' }
const AUDIENCE_SCOPE_LABEL: Record<string, string> = { all: '全部', onboarding: '未注册', searching: '寻找中', getting_to_know: '初步了解', successful: '成功牵手' }
const STATUS_LABEL: Record<string, string> = { draft: '草稿', scheduled: '定时', active: '生效中', inactive: '已下架', expired: '已过期' }
const STATUS_TAG: Record<string, string> = { draft: 'info', scheduled: 'warning', active: 'success', inactive: '', expired: 'danger' }
function bannerTargetTypeLabel(t: string) { return TARGET_TYPE_LABEL[t] || t }
function audienceScopeLabel(s: string) { return AUDIENCE_SCOPE_LABEL[s] || s }
function audienceScopeTag(s: string) { return s === 'all' ? 'success' : 'info' }
function statusLabel(s: string) { return STATUS_LABEL[s] || s }
function statusTag(s: string) { return STATUS_TAG[s] || 'info' }
function formatDateTime(d: string) { return d ? new Date(d).toLocaleString('zh-CN') : '-' }
async function loadBanners() {
loading.value = true
try {
const res: any = await get('/home/banners')
banners.value = res.data || []
} catch (e) { console.error(e) } finally { loading.value = false }
}
function openCreate() { editingId.value = null; Object.assign(form.value, { title: '', subtitle: '', target_type: 'none', target_value: '', audience_scope: 'all', sort_order: 0, start_at: '', end_at: '' }); dialogVisible.value = true }
function openEdit(row: any) { editingId.value = row.id; Object.assign(form.value, { title: row.title, subtitle: row.subtitle, target_type: row.target_type, target_value: row.target_value, audience_scope: row.audience_scope, sort_order: row.sort_order, start_at: row.start_at, end_at: row.end_at }); dialogVisible.value = true }
async function saveBanner() {
try {
if (editingId.value) {
await put(`/home/banners/${editingId.value}`, form.value)
ElMessage.success('更新成功')
} else {
await post('/home/banners', form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
loadBanners()
} catch (e) { ElMessage.error('操作失败') }
}
async function toggleStatus(row: any) {
try {
const newStatus = row.status === 'active' ? 'inactive' : 'active'
await put(`/home/banners/${row.id}`, { ...row, status: newStatus })
await loadBanners()
} catch (e) { ElMessage.error('操作失败') }
}
onMounted(loadBanners)
</script>
<style scoped>
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>