feat(marketing): add ad placement management UI

- CRUD for ad placements (rewarded video, banner, interstitial)
- Integrated into marketing management page with table and dialog

Made-with: Cursor
This commit is contained in:
nepiedg
2026-04-04 04:02:17 +08:00
parent d62c51f140
commit 66713b110f
2 changed files with 196 additions and 2 deletions
+30
View File
@@ -84,3 +84,33 @@ export function deleteMarketingTemplate(id) {
method: 'delete' method: 'delete'
}) })
} }
export function getAdPlacements() {
return request({
url: '/api/admin/marketing/ad-placements',
method: 'get'
})
}
export function createAdPlacement(data) {
return request({
url: '/api/admin/marketing/ad-placements',
method: 'post',
data
})
}
export function updateAdPlacement(id, data) {
return request({
url: `/api/admin/marketing/ad-placements/${id}`,
method: 'put',
data
})
}
export function deleteAdPlacement(id) {
return request({
url: `/api/admin/marketing/ad-placements/${id}`,
method: 'delete'
})
}
+166 -2
View File
@@ -152,6 +152,84 @@
</div> </div>
</el-card> </el-card>
<!-- 广告位管理 -->
<el-card class="section-card">
<template #header>
<div class="header-row">
<span>广告位管理</span>
<el-button type="primary" @click="openAdDialog()">新增广告位</el-button>
</div>
</template>
<el-table v-loading="adsLoading" :data="adPlacements" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="广告位名称" min-width="140" />
<el-table-column prop="ad_type" label="广告类型" width="140">
<template #default="{ row }">
<el-tag size="small" :type="row.ad_type === 'rewarded_video' ? 'warning' : 'info'">
{{ { rewarded_video: '激励视频', banner: 'Banner', interstitial: '插屏' }[row.ad_type] || row.ad_type }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ad_unit_id" label="广告单元 ID" min-width="200">
<template #default="{ row }">
<span>{{ row.ad_unit_id || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="mini_program_id" label="小程序 ID" width="100" />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="备注" min-width="140" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openAdDialog(row)">编辑</el-button>
<el-popconfirm title="确认删除该广告位?" @confirm="handleDeleteAd(row.id)">
<template #reference>
<el-button link type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 广告位弹窗 -->
<el-dialog v-model="adDialogVisible" :title="adForm.id ? '编辑广告位' : '新增广告位'" width="520px">
<el-form label-width="110px">
<el-form-item label="广告位名称" required>
<el-input v-model="adForm.name" placeholder="如: 保存营销图" />
</el-form-item>
<el-form-item label="广告类型" required>
<el-select v-model="adForm.ad_type" style="width: 100%">
<el-option label="激励视频" value="rewarded_video" />
<el-option label="Banner" value="banner" />
<el-option label="插屏" value="interstitial" />
</el-select>
</el-form-item>
<el-form-item label="广告单元 ID">
<el-input v-model="adForm.ad_unit_id" placeholder="在微信后台申请后填入" />
</el-form-item>
<el-form-item label="小程序 ID" required>
<el-input-number v-model="adForm.mini_program_id" :min="1" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="adForm.statusBool" active-text="启用" inactive-text="禁用" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="adForm.description" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="adDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="adSaving" @click="saveAd">保存</el-button>
</template>
</el-dialog>
<el-dialog <el-dialog
v-model="categoryDialogVisible" v-model="categoryDialogVisible"
:title="categoryForm.id ? '编辑分类' : '新增分类'" :title="categoryForm.id ? '编辑分类' : '新增分类'"
@@ -250,7 +328,11 @@ import {
getMarketingStats, getMarketingStats,
getMarketingTemplates, getMarketingTemplates,
updateMarketingCategory, updateMarketingCategory,
updateMarketingTemplate updateMarketingTemplate,
getAdPlacements,
createAdPlacement,
updateAdPlacement,
deleteAdPlacement
} from '../../api/marketing' } from '../../api/marketing'
const errorMessage = ref('') const errorMessage = ref('')
@@ -300,6 +382,20 @@ const templateForm = reactive({
statusBool: true statusBool: true
}) })
const adPlacements = ref([])
const adsLoading = ref(false)
const adDialogVisible = ref(false)
const adSaving = ref(false)
const adForm = reactive({
id: null,
name: '',
ad_type: 'rewarded_video',
ad_unit_id: '',
mini_program_id: 3,
statusBool: true,
description: ''
})
const parseDownloads = (data, key) => { const parseDownloads = (data, key) => {
const candidates = [ const candidates = [
data?.[key], data?.[key],
@@ -369,9 +465,77 @@ const loadStats = async () => {
} }
} }
const loadAdPlacements = async () => {
adsLoading.value = true
try {
const res = await getAdPlacements()
adPlacements.value = res.data || []
} catch (error) {
errorMessage.value = '加载广告位失败'
} finally {
adsLoading.value = false
}
}
const openAdDialog = (row) => {
if (!row) {
adForm.id = null
adForm.name = ''
adForm.ad_type = 'rewarded_video'
adForm.ad_unit_id = ''
adForm.mini_program_id = 3
adForm.statusBool = true
adForm.description = ''
} else {
adForm.id = row.id
adForm.name = row.name || ''
adForm.ad_type = row.ad_type || 'rewarded_video'
adForm.ad_unit_id = row.ad_unit_id || ''
adForm.mini_program_id = row.mini_program_id || 3
adForm.statusBool = row.status === 1
adForm.description = row.description || ''
}
adDialogVisible.value = true
}
const saveAd = async () => {
if (!adForm.name.trim()) {
ElMessage.warning('请填写广告位名称')
return
}
adSaving.value = true
try {
const payload = {
name: adForm.name.trim(),
ad_type: adForm.ad_type,
ad_unit_id: adForm.ad_unit_id.trim(),
mini_program_id: adForm.mini_program_id,
status: adForm.statusBool ? 1 : 0,
description: adForm.description.trim()
}
if (adForm.id) {
await updateAdPlacement(adForm.id, payload)
ElMessage.success('广告位更新成功')
} else {
await createAdPlacement(payload)
ElMessage.success('广告位创建成功')
}
adDialogVisible.value = false
await loadAdPlacements()
} finally {
adSaving.value = false
}
}
const handleDeleteAd = async (id) => {
await deleteAdPlacement(id)
ElMessage.success('广告位删除成功')
await loadAdPlacements()
}
const loadAll = async () => { const loadAll = async () => {
errorMessage.value = '' errorMessage.value = ''
await Promise.all([loadCategories(), loadTemplates(), loadTemplateCount(), loadStats()]) await Promise.all([loadCategories(), loadTemplates(), loadTemplateCount(), loadStats(), loadAdPlacements()])
} }
const resetCategoryForm = () => { const resetCategoryForm = () => {