|
|
@@ -0,0 +1,434 @@
|
|
|
+<template>
|
|
|
+ <div class="space-y-6">
|
|
|
+ <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
|
+ <div class="border-b border-gray-200 pb-4 mb-6">
|
|
|
+ <h2 class="text-xl font-semibold text-gray-800 flex items-center gap-2">
|
|
|
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
|
+ </svg>
|
|
|
+ 广告计划创意列表
|
|
|
+ </h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 筛选条件 -->
|
|
|
+ <div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
|
+ <el-form :inline="true" :model="queryForm" class="flex flex-wrap gap-4">
|
|
|
+ <el-form-item label="配置ID">
|
|
|
+ <el-input v-model="queryForm.accountId" placeholder="请输入账户ID" clearable style="width: 150px"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="计划ID">
|
|
|
+ <el-input v-model="queryForm.adgroupId" placeholder="请输入计划ID" clearable style="width: 150px"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="计划ID">
|
|
|
+ <el-input v-model="queryForm.creativeId" placeholder="请输入计划ID" clearable style="width: 150px"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="MD5">
|
|
|
+ <el-input v-model="queryForm.md5" placeholder="请输入MD5" clearable style="width: 200px"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-select v-model="queryForm.status" placeholder="全部状态" clearable style="width: 150px">
|
|
|
+ <el-option label="待执行" :value="1"></el-option>
|
|
|
+ <el-option label="执行中" :value="2"></el-option>
|
|
|
+ <el-option label="失败" :value="3"></el-option>
|
|
|
+ <el-option label="已结束" :value="4"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleQuery">查询</el-button>
|
|
|
+ <el-button @click="handleReset">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 创意列表表格 -->
|
|
|
+ <el-table
|
|
|
+ :data="creativeList"
|
|
|
+ style="width: 100%"
|
|
|
+ v-loading="loading"
|
|
|
+ stripe
|
|
|
+ :header-cell-style="{ background: '#f9fafb', color: '#374151', fontWeight: '600' }"
|
|
|
+ >
|
|
|
+ <el-table-column prop="id" label="ID" width="80"></el-table-column>
|
|
|
+ <el-table-column prop="accountId" label="账户ID" width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.accountId }}
|
|
|
+ <el-button v-if="row.accountId > 0" size="small" link @click="goToTencentByAccountId(row)">
|
|
|
+ <el-icon color="#409efc" class="no-inherit">
|
|
|
+ <Share />
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="adgroupId" label="广告组ID" width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.adgroupId }}
|
|
|
+ <el-button v-if="row.adgroupId > 0" size="small" link @click="goToTencentByAdgroupId(row)">
|
|
|
+ <el-icon color="#409efc" class="no-inherit">
|
|
|
+ <Share />
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="creativeId" label="创意ID" width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.creativeId }}
|
|
|
+ <el-button v-if="row.creativeId > 0" size="small" link @click="gotToTencentCreative(row)">
|
|
|
+ <el-icon color="#409efc" class="no-inherit">
|
|
|
+ <Share />
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="creativeName" label="创意名称" min-width="200" show-overflow-tooltip></el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="创意文件" width="120">
|
|
|
+ <template #default="{ row: creative }">
|
|
|
+ <el-image
|
|
|
+ v-if="creative.creativeFileType === 'image' && creative.creativeFileUrl"
|
|
|
+ :src="creative.creativeFileUrl"
|
|
|
+ :preview-src-list="[creative.creativeFileUrl]"
|
|
|
+ :preview-teleported="true"
|
|
|
+ style="width: 60px; height: 60px"
|
|
|
+ class="cursor-pointer rounded"
|
|
|
+ />
|
|
|
+ <video v-else-if="creative.creativeFileType === 'video' && creative.creativeFileUrl" controls>
|
|
|
+ <source :src="creative.creativeFileUrl" type="video/mp4">
|
|
|
+ </video>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="creativeFileMd5" label="文件MD5" width="150" show-overflow-tooltip></el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="status" label="状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getStatusType(row.status)" size="small">
|
|
|
+ {{ getStatusLabel(row.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="approvalStatus" label="审核状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getApprovalStatusType(row.approvalStatus)" size="small">
|
|
|
+ {{ getApprovalStatusLabel(row.approvalStatus) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="viewCount" label="曝光次数" width="120"></el-table-column>
|
|
|
+ <el-table-column prop="validClickCount" label="点击次数" width="120"></el-table-column>
|
|
|
+ <el-table-column prop="ctr" label="点击率" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.ctr ? (row.ctr * 100).toFixed(2) + '%' : '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="cost" label="花费(分)" width="120"></el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="createTime" label="创建时间" width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ formatDateTime(row.createTime) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="mt-6 flex justify-end">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.page"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="pagination.total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 详情对话框 -->
|
|
|
+ <el-dialog v-model="detailDialogVisible" title="创意详情" width="700px">
|
|
|
+ <el-descriptions :column="2" border v-if="currentCreative">
|
|
|
+ <el-descriptions-item label="创意ID">{{ currentCreative.creativeId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创意名称">{{ currentCreative.creativeName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="计划ID">{{ currentCreative.adPlanId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="广告组ID">{{ currentCreative.adgroupId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="账户ID">{{ currentCreative.accountId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="配置ID">{{ currentCreative.adPlanConfId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <el-tag :type="getStatusType(currentCreative.status)" size="small">
|
|
|
+ {{ getStatusLabel(currentCreative.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="审核状态">
|
|
|
+ <el-tag :type="getApprovalStatusType(currentCreative.approvalStatus)" size="small">
|
|
|
+ {{ currentCreative.approvalStatus || '-' }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="曝光次数">{{ currentCreative.viewCount || 0 }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="点击次数">{{ currentCreative.validClickCount || 0 }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="点击率">{{ currentCreative.ctr ? (currentCreative.ctr * 100).toFixed(2) + '%' : '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="花费">{{ currentCreative.cost ? (currentCreative.cost / 100).toFixed(2) + ' 元' : '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="转化成本">{{ currentCreative.conversionsCost ? (currentCreative.conversionsCost / 100).toFixed(2) + ' 元' : '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="文件MD5">{{ currentCreative.creativeFileMd5 || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间" :span="2">{{ formatDateTime(currentCreative.createTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="更新时间" :span="2">{{ formatDateTime(currentCreative.updateTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="失败原因" :span="2" v-if="currentCreative.failReason">
|
|
|
+ <span class="text-red-500">{{ currentCreative.failReason }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="detailDialogVisible = false">关闭</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 图片预览对话框 -->
|
|
|
+ <el-dialog v-model="previewVisible" title="图片预览" width="800px">
|
|
|
+ <div class="flex justify-center">
|
|
|
+ <img :src="previewUrl" alt="预览" style="max-width: 100%; max-height: 600px;">
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+defineOptions({
|
|
|
+ name: 'AdPlanCreative'
|
|
|
+})
|
|
|
+
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { getCreativeList, getCreativeDetail, deleteCreative } from '@/api/adPlanCreative'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+
|
|
|
+// 查询表单
|
|
|
+const queryForm = reactive({
|
|
|
+ accountId: '',
|
|
|
+ adgroupId: '',
|
|
|
+ creativeId: '',
|
|
|
+ md5: '',
|
|
|
+ status: 4
|
|
|
+})
|
|
|
+
|
|
|
+// 创意列表
|
|
|
+const creativeList = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+
|
|
|
+// 分页
|
|
|
+const pagination = reactive({
|
|
|
+ page: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ total: 0
|
|
|
+})
|
|
|
+
|
|
|
+// 详情对话框
|
|
|
+const detailDialogVisible = ref(false)
|
|
|
+const currentCreative = ref(null)
|
|
|
+
|
|
|
+// 预览
|
|
|
+const previewVisible = ref(false)
|
|
|
+const previewUrl = ref('')
|
|
|
+
|
|
|
+// 获取状态标签
|
|
|
+const getStatusLabel = (status) => {
|
|
|
+ const labels = {
|
|
|
+ 1: '待执行',
|
|
|
+ 2: '执行中',
|
|
|
+ 3: '失败',
|
|
|
+ 4: '已结束'
|
|
|
+ }
|
|
|
+ return labels[status] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+// 获取状态类型
|
|
|
+const getStatusType = (status) => {
|
|
|
+ const types = {
|
|
|
+ 1: 'info',
|
|
|
+ 2: 'warning',
|
|
|
+ 3: 'danger',
|
|
|
+ 4: 'success'
|
|
|
+ }
|
|
|
+ return types[status] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ *
|
|
|
+ * @param status ] * CREATIVE_SET_APPROVAL_STATUS_UNKNOWN, // 未知
|
|
|
+ * CREATIVE_SET_APPROVAL_STATUS_PENDING, // 审核中
|
|
|
+ * CREATIVE_SET_APPROVAL_STATUS_NORMAL, // 投放中
|
|
|
+ * CREATIVE_SET_APPROVAL_STATUS_DENIED, // 审核拒绝
|
|
|
+ * CREATIVE_SET_APPROVAL_STATUS_PARTIAL_NORMAL, // 部分投放中
|
|
|
+ */
|
|
|
+// 获取审核状态类型
|
|
|
+const getApprovalStatusType = (status) => {
|
|
|
+ switch (status) {
|
|
|
+ case 'CREATIVE_SET_APPROVAL_STATUS_UNKNOWN': return 'info'
|
|
|
+ case 'CREATIVE_SET_APPROVAL_STATUS_PENDING': return 'warning'
|
|
|
+ case 'CREATIVE_SET_APPROVAL_STATUS_NORMAL': return 'success'
|
|
|
+ case 'CREATIVE_SET_APPROVAL_STATUS_DENIED': return 'danger'
|
|
|
+ case 'CREATIVE_SET_APPROVAL_STATUS_PARTIAL_NORMAL': return 'success'
|
|
|
+ default: return 'info'
|
|
|
+ }
|
|
|
+}
|
|
|
+// 获取审核状态标签
|
|
|
+const getApprovalStatusLabel = (status) => {
|
|
|
+ const labels = {
|
|
|
+ 'CREATIVE_SET_APPROVAL_STATUS_UNKNOWN': '未知',
|
|
|
+ 'CREATIVE_SET_APPROVAL_STATUS_PENDING': '审核中',
|
|
|
+ 'CREATIVE_SET_APPROVAL_STATUS_NORMAL': '投放中',
|
|
|
+ 'CREATIVE_SET_APPROVAL_STATUS_DENIED': '审核拒绝',
|
|
|
+ 'CREATIVE_SET_APPROVAL_STATUS_PARTIAL_NORMAL': '部分投放中'
|
|
|
+ }
|
|
|
+ return labels[status] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+const goToTencentByAccountId = (row) => {
|
|
|
+ const date = dayjs().format('YYYY-MM-DD')
|
|
|
+ window.open(`https://ad.qq.com/atlas/${row.accountId}/admanage/index?start_date=${date}&end_date=${date}&tab=adgroup&query={%22operation_status%22:[%22CALCULATE_STATUS_EXCLUDE_DEL%22],%22system_status%22:[]}`, '_blank')
|
|
|
+}
|
|
|
+
|
|
|
+const goToTencentByAdgroupId = (row) => {
|
|
|
+ const date = dayjs().format('YYYY-MM-DD')
|
|
|
+ window.open(`https://ad.qq.com/atlas/${row.accountId}/admanage/index?start_date=${date}&end_date=${date}&tab=adgroup&query={%22operation_status%22:[%22CALCULATE_STATUS_EXCLUDE_DEL%22],%22system_status%22:[],%22search_name%22:%22${row.adgroupId}%22}`, '_blank')
|
|
|
+}
|
|
|
+
|
|
|
+const gotToTencentCreative = (row) => {
|
|
|
+ window.open(`https://ad.qq.com/atlas/${row.accountId}/admanage/index?tab=dynamic_creative&query={%22operation_status%22:[],%22created_time%22:%22-89%22,%22fuzzy_name%22:%22${row.creativeId}%22}`, '_blank')
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化日期时间
|
|
|
+const formatDateTime = (dateTime) => {
|
|
|
+ if (!dateTime) return '-'
|
|
|
+ return dateTime.replace('T', ' ')
|
|
|
+}
|
|
|
+
|
|
|
+// 加载创意列表
|
|
|
+const loadCreativeList = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ pageNum: pagination.page,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ adPlanConfId: queryForm.adPlanConfId || undefined,
|
|
|
+ planId: queryForm.planId || undefined,
|
|
|
+ md5: queryForm.md5 || undefined,
|
|
|
+ status: queryForm.status || undefined
|
|
|
+ }
|
|
|
+ const res = await getCreativeList(params)
|
|
|
+ if (res.success) {
|
|
|
+ creativeList.value = res.data.records || []
|
|
|
+ pagination.total = res.data.total || 0
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '查询失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('查询失败:', error)
|
|
|
+ ElMessage.error('查询失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查询
|
|
|
+const handleQuery = () => {
|
|
|
+ pagination.page = 1
|
|
|
+ loadCreativeList()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置查询
|
|
|
+const handleReset = () => {
|
|
|
+ queryForm.adPlanConfId = ''
|
|
|
+ queryForm.planId = ''
|
|
|
+ queryForm.md5 = ''
|
|
|
+ queryForm.status = null
|
|
|
+ handleQuery()
|
|
|
+}
|
|
|
+
|
|
|
+// 查看详情
|
|
|
+const handleViewDetail = async (row) => {
|
|
|
+ try {
|
|
|
+ const res = await getCreativeDetail(row.id)
|
|
|
+ if (res.success) {
|
|
|
+ currentCreative.value = res.data
|
|
|
+ detailDialogVisible.value = true
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '查询详情失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('查询详情失败:', error)
|
|
|
+ ElMessage.error('查询详情失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除创意
|
|
|
+const handleDelete = (row) => {
|
|
|
+ ElMessageBox.confirm('确定要删除该创意吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await deleteCreative(row.id)
|
|
|
+ if (res.success) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ loadCreativeList()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除失败:', error)
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 预览文件
|
|
|
+const previewFile = (url) => {
|
|
|
+ if (!url) return
|
|
|
+ if (url.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
|
|
|
+ previewUrl.value = url
|
|
|
+ previewVisible.value = true
|
|
|
+ } else {
|
|
|
+ window.open(url, '_blank')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 分页大小改变
|
|
|
+const handleSizeChange = (size) => {
|
|
|
+ pagination.pageSize = size
|
|
|
+ pagination.page = 1
|
|
|
+ loadCreativeList()
|
|
|
+}
|
|
|
+
|
|
|
+// 当前页改变
|
|
|
+const handleCurrentChange = (page) => {
|
|
|
+ pagination.page = page
|
|
|
+ loadCreativeList()
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化
|
|
|
+onMounted(() => {
|
|
|
+ // 从路由参数获取MD5筛选条件
|
|
|
+ if (route.query.md5) {
|
|
|
+ queryForm.md5 = route.query.md5
|
|
|
+ }
|
|
|
+ loadCreativeList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+:deep(.el-descriptions__label) {
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+</style>
|