2026-05-15 10:51:06 +08:00
|
|
|
|
<template>
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<div class="ads-compliance-tencent">
|
|
|
|
|
|
<el-card shadow="hover" class="main-card">
|
|
|
|
|
|
<!-- Tabs -->
|
|
|
|
|
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="main-tabs">
|
|
|
|
|
|
<el-tab-pane label="图片素材" name="image">
|
|
|
|
|
|
<!-- 图片素材内容区域 -->
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<!-- 工具栏区域 -->
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
<!-- 左侧:筛选表单 -->
|
|
|
|
|
|
<el-form :model="imageFilters" :inline="true" class="search-form">
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-select v-model="imageFilters.status" placeholder="全部" clearable style="width: 120px">
|
|
|
|
|
|
<el-option label="全部" value=""></el-option>
|
|
|
|
|
|
<el-option label="待校验" value="PENDING"></el-option>
|
|
|
|
|
|
<el-option label="送检中" value="SUBMITTING"></el-option>
|
|
|
|
|
|
<el-option label="校验通过" value="VERIFIED"></el-option>
|
|
|
|
|
|
<el-option label="校验不通过" value="REJECTED"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="账户ID">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="imageFilters.accountId"
|
|
|
|
|
|
placeholder="账户ID"
|
|
|
|
|
|
style="width: 150px"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@keyup.enter="searchImage"
|
|
|
|
|
|
></el-input>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" @click="searchImage">搜索</el-button>
|
|
|
|
|
|
<el-button @click="resetImageFilter">重置</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:操作按钮 -->
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<el-button type="success" @click="batchVerifyImage" :loading="batchLoading"> 批量校验图片 </el-button>
|
|
|
|
|
|
<el-button type="primary" plain @click="pollImageResults" :loading="pollLoading">
|
|
|
|
|
|
<el-icon><Refresh /></el-icon> 刷新检测结果
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="warning" plain @click="exportImageUrls">
|
|
|
|
|
|
<el-icon><Download /></el-icon> 导出失败素材
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<!-- 表格区域 -->
|
|
|
|
|
|
<div class="table-wrapper">
|
|
|
|
|
|
<el-table :data="imageList" border style="width: 100%" v-loading="imageLoading">
|
|
|
|
|
|
<el-table-column prop="accountId" label="账户ID" width="120"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="imageUsage" label="用途" width="120"></el-table-column>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
<el-table-column label="预览" width="180">
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="scope.row.previewUrl"
|
|
|
|
|
|
:src="scope.row.previewUrl"
|
|
|
|
|
|
class="media-preview"
|
|
|
|
|
|
@click="previewMedia(scope.row.previewUrl, 'image')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="verifyStatus" label="校验状态" width="120">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
|
|
|
|
|
|
{{ getStatusText(scope.row.verifyStatus) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
<el-table-column label="操作" width="160" fixed="right">
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-button size="small" type="text" @click="verifyImage(scope.row.imageId)">送检</el-button>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
<el-button size="small" type="text" @click="viewLog(scope.row)">查看日志</el-button>
|
2026-05-15 13:07:38 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<!-- 分页区域 -->
|
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
@current-change="handleImagePageChange"
|
|
|
|
|
|
v-model:current-page="imagePage"
|
|
|
|
|
|
:page-size="imagePageSize"
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:total="imageTotal"
|
|
|
|
|
|
>
|
|
|
|
|
|
</el-pagination>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-15 13:07:38 +08:00
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="视频素材" name="video">
|
|
|
|
|
|
<!-- 视频素材内容区域 -->
|
|
|
|
|
|
<div class="tab-content">
|
|
|
|
|
|
<!-- 工具栏区域 -->
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
<!-- 左侧:筛选表单 -->
|
|
|
|
|
|
<el-form :model="videoFilters" :inline="true" class="search-form">
|
|
|
|
|
|
<el-form-item label="状态">
|
|
|
|
|
|
<el-select v-model="videoFilters.status" placeholder="全部" clearable style="width: 120px">
|
|
|
|
|
|
<el-option label="全部" value=""></el-option>
|
|
|
|
|
|
<el-option label="待校验" value="PENDING"></el-option>
|
|
|
|
|
|
<el-option label="送检中" value="SUBMITTING"></el-option>
|
|
|
|
|
|
<el-option label="校验通过" value="VERIFIED"></el-option>
|
|
|
|
|
|
<el-option label="校验不通过" value="REJECTED"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="账户ID">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="videoFilters.accountId"
|
|
|
|
|
|
placeholder="账户ID"
|
|
|
|
|
|
style="width: 150px"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
@keyup.enter="searchVideo"
|
|
|
|
|
|
></el-input>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" @click="searchVideo">搜索</el-button>
|
|
|
|
|
|
<el-button @click="resetVideoFilter">重置</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:操作按钮 -->
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<el-button type="success" @click="batchVerifyVideo" :loading="batchLoading"> 批量校验视频 </el-button>
|
|
|
|
|
|
<el-button type="primary" plain @click="pollVideoResults" :loading="pollLoading">
|
|
|
|
|
|
<el-icon><Refresh /></el-icon> 刷新检测结果
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="warning" plain @click="exportVideoUrls">
|
|
|
|
|
|
<el-icon><Download /></el-icon> 导出失败素材
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<!-- 表格区域 -->
|
|
|
|
|
|
<div class="table-wrapper">
|
|
|
|
|
|
<el-table :data="videoList" border style="width: 100%" v-loading="videoLoading">
|
|
|
|
|
|
<el-table-column prop="accountId" label="账户ID" width="120"></el-table-column>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
<el-table-column label="预览" width="180">
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<div class="video-preview" @click="previewMedia(scope.row.previewUrl, 'video')">
|
|
|
|
|
|
<el-icon><VideoPlay style="font-size: 32px" /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="verifyStatus" label="校验状态" width="120">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
|
|
|
|
|
|
{{ getStatusText(scope.row.verifyStatus) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
<el-table-column label="操作" width="160" fixed="right">
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-button size="small" type="text" @click="verifyVideo(scope.row.videoId)">送检</el-button>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
<el-button size="small" type="text" @click="viewLog(scope.row)">查看日志</el-button>
|
2026-05-15 13:07:38 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
<!-- 分页区域 -->
|
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
@current-change="handleVideoPageChange"
|
|
|
|
|
|
v-model:current-page="videoPage"
|
|
|
|
|
|
:page-size="videoPageSize"
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:total="videoTotal"
|
|
|
|
|
|
>
|
|
|
|
|
|
</el-pagination>
|
|
|
|
|
|
</div>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
</div>
|
2026-05-15 13:07:38 +08:00
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</el-card>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 预览对话框 -->
|
|
|
|
|
|
<el-dialog title="媒体预览" v-model="previewVisible" width="60%">
|
|
|
|
|
|
<div style="text-align: center">
|
|
|
|
|
|
<img v-if="previewType === 'image'" :src="previewUrl" style="max-width: 100%" />
|
|
|
|
|
|
<video v-if="previewType === 'video'" :src="previewUrl" controls style="max-width: 100%"></video>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
2026-05-15 13:15:24 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 日志对话框 -->
|
|
|
|
|
|
<el-dialog title="校验日志" v-model="logVisible" width="60%">
|
|
|
|
|
|
<el-table :data="currentLogList" border style="width: 100%">
|
|
|
|
|
|
<el-table-column prop="time" label="时间" width="180"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="type" label="类型" width="120">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<span :class="'table-status status-' + (scope.row.type || 'info').toLowerCase()">
|
|
|
|
|
|
{{ getLogTypeText(scope.row.type) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="message" label="日志内容"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div v-if="!currentLogList.length" style="text-align: center; color: #909399; padding: 40px">暂无日志记录</div>
|
|
|
|
|
|
</el-dialog>
|
2026-05-15 10:51:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, reactive, onMounted } from 'vue';
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
|
import { Refresh, Download, VideoPlay } from '@element-plus/icons-vue';
|
|
|
|
|
|
import {
|
|
|
|
|
|
getImageStats,
|
|
|
|
|
|
getVideoStats,
|
|
|
|
|
|
getImageList,
|
|
|
|
|
|
getVideoList,
|
|
|
|
|
|
manualVerifyImage,
|
|
|
|
|
|
manualVerifyVideo,
|
|
|
|
|
|
pollImageResults as pollImageResultsApi,
|
|
|
|
|
|
pollVideoResults as pollVideoResultsApi,
|
|
|
|
|
|
batchVerifyImage as batchVerifyImageApi,
|
|
|
|
|
|
batchVerifyVideo as batchVerifyVideoApi,
|
|
|
|
|
|
} from '/@/api/ads/compliance/tencent/materialVerify';
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式状态
|
|
|
|
|
|
const activeTab = ref('image');
|
|
|
|
|
|
const pollLoading = ref(false);
|
|
|
|
|
|
const batchLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 图片统计
|
|
|
|
|
|
const imageStats = reactive({ pending: 0, verified: 0, rejected: 0 });
|
|
|
|
|
|
// 视频统计
|
|
|
|
|
|
const videoStats = reactive({ pending: 0, verified: 0, rejected: 0 });
|
|
|
|
|
|
|
|
|
|
|
|
// 图片列表
|
|
|
|
|
|
const imageList = ref<any[]>([]);
|
|
|
|
|
|
const imageLoading = ref(false);
|
|
|
|
|
|
const imagePage = ref(1);
|
|
|
|
|
|
const imagePageSize = ref(20);
|
|
|
|
|
|
const imageTotal = ref(0);
|
|
|
|
|
|
const imageFilters = reactive({ status: '', accountId: '' });
|
|
|
|
|
|
|
|
|
|
|
|
// 视频列表
|
|
|
|
|
|
const videoList = ref<any[]>([]);
|
|
|
|
|
|
const videoLoading = ref(false);
|
|
|
|
|
|
const videoPage = ref(1);
|
|
|
|
|
|
const videoPageSize = ref(20);
|
|
|
|
|
|
const videoTotal = ref(0);
|
|
|
|
|
|
const videoFilters = reactive({ status: '', accountId: '' });
|
|
|
|
|
|
|
|
|
|
|
|
// 对话框
|
|
|
|
|
|
const previewVisible = ref(false);
|
|
|
|
|
|
const previewUrl = ref('');
|
|
|
|
|
|
const previewType = ref('image');
|
|
|
|
|
|
|
2026-05-15 13:15:24 +08:00
|
|
|
|
// 日志对话框
|
|
|
|
|
|
const logVisible = ref(false);
|
|
|
|
|
|
const currentLogList = ref<any[]>([]);
|
|
|
|
|
|
|
2026-05-15 10:51:06 +08:00
|
|
|
|
// 加载统计
|
|
|
|
|
|
const loadStats = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [imgRes, vidRes] = await Promise.all([getImageStats(), getVideoStats()]);
|
|
|
|
|
|
const imgStats = imgRes.data || {};
|
|
|
|
|
|
const vidStats = vidRes.data || {};
|
|
|
|
|
|
Object.assign(imageStats, {
|
|
|
|
|
|
pending: imgStats.pending || 0,
|
|
|
|
|
|
verified: imgStats.verified || 0,
|
|
|
|
|
|
rejected: imgStats.rejected || 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
Object.assign(videoStats, {
|
|
|
|
|
|
pending: vidStats.pending || 0,
|
|
|
|
|
|
verified: vidStats.verified || 0,
|
|
|
|
|
|
rejected: vidStats.rejected || 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (err) {
|
2026-05-15 13:07:38 +08:00
|
|
|
|
ElMessage.error('加载统计失败');
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 加载图片列表
|
|
|
|
|
|
const loadImageList = () => {
|
|
|
|
|
|
imageLoading.value = true;
|
|
|
|
|
|
getImageList({
|
|
|
|
|
|
page: imagePage.value,
|
|
|
|
|
|
pageSize: imagePageSize.value,
|
|
|
|
|
|
status: imageFilters.status,
|
|
|
|
|
|
accountId: imageFilters.accountId,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
imageList.value = res.data?.list || [];
|
|
|
|
|
|
imageTotal.value = res.data?.total || 0;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
ElMessage.error('加载图片列表失败');
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
imageLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 加载视频列表
|
|
|
|
|
|
const loadVideoList = () => {
|
|
|
|
|
|
videoLoading.value = true;
|
|
|
|
|
|
getVideoList({
|
|
|
|
|
|
page: videoPage.value,
|
|
|
|
|
|
pageSize: videoPageSize.value,
|
|
|
|
|
|
status: videoFilters.status,
|
|
|
|
|
|
accountId: videoFilters.accountId,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
videoList.value = res.data?.list || [];
|
|
|
|
|
|
videoTotal.value = res.data?.total || 0;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
ElMessage.error('加载视频列表失败');
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
videoLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Tab 切换
|
|
|
|
|
|
const handleTabClick = (tab: any) => {
|
|
|
|
|
|
if (tab.name === 'image') {
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
} else if (tab.name === 'video') {
|
|
|
|
|
|
loadVideoList();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索
|
|
|
|
|
|
const searchImage = () => {
|
|
|
|
|
|
imagePage.value = 1;
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const searchVideo = () => {
|
|
|
|
|
|
videoPage.value = 1;
|
|
|
|
|
|
loadVideoList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 重置筛选
|
|
|
|
|
|
const resetImageFilter = () => {
|
|
|
|
|
|
Object.assign(imageFilters, { status: '', accountId: '' });
|
|
|
|
|
|
searchImage();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetVideoFilter = () => {
|
|
|
|
|
|
Object.assign(videoFilters, { status: '', accountId: '' });
|
|
|
|
|
|
searchVideo();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 分页
|
|
|
|
|
|
const handleImagePageChange = (page: number) => {
|
|
|
|
|
|
imagePage.value = page;
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleVideoPageChange = (page: number) => {
|
|
|
|
|
|
videoPage.value = page;
|
|
|
|
|
|
loadVideoList();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 手动送检
|
|
|
|
|
|
const verifyImage = (imageId: string) => {
|
|
|
|
|
|
ElMessageBox.confirm('确认提交图片 ' + imageId + ' 进行校验?', '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
manualVerifyImage({ materialId: imageId })
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
ElMessage.success('提交成功');
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const verifyVideo = (videoId: string) => {
|
|
|
|
|
|
ElMessageBox.confirm('确认提交视频 ' + videoId + ' 进行校验?', '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
manualVerifyVideo({ materialId: videoId })
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
ElMessage.success('提交成功');
|
|
|
|
|
|
loadVideoList();
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新检测结果
|
|
|
|
|
|
const pollImageResults = () => {
|
|
|
|
|
|
pollLoading.value = true;
|
|
|
|
|
|
pollImageResultsApi()
|
|
|
|
|
|
.then((res: any) => {
|
|
|
|
|
|
ElMessage.success(res?.msg || '刷新完成');
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
pollLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const pollVideoResults = () => {
|
|
|
|
|
|
pollLoading.value = true;
|
|
|
|
|
|
pollVideoResultsApi()
|
|
|
|
|
|
.then((res: any) => {
|
|
|
|
|
|
ElMessage.success(res?.msg || '刷新完成');
|
|
|
|
|
|
loadVideoList();
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
pollLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-15 11:22:30 +08:00
|
|
|
|
// 导出失败素材
|
2026-05-15 10:51:06 +08:00
|
|
|
|
const exportImageUrls = () => {
|
2026-05-15 11:22:30 +08:00
|
|
|
|
const failedImages = imageList.value.filter((item) => item.verifyStatus === 'REJECTED');
|
|
|
|
|
|
if (!failedImages.length) {
|
|
|
|
|
|
ElMessage.warning('没有失败的图片素材');
|
2026-05-15 10:51:06 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const rows = [['ID', '图片ID', '账户ID', '用途', '预览URL', '状态', '描述']];
|
2026-05-15 11:22:30 +08:00
|
|
|
|
failedImages.forEach((item) => {
|
2026-05-15 10:51:06 +08:00
|
|
|
|
rows.push([
|
|
|
|
|
|
item.id,
|
|
|
|
|
|
item.imageId,
|
|
|
|
|
|
item.accountId,
|
|
|
|
|
|
item.imageUsage,
|
|
|
|
|
|
item.previewUrl || '-',
|
|
|
|
|
|
getStatusText(item.verifyStatus),
|
|
|
|
|
|
item.description || '-',
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
2026-05-15 11:22:30 +08:00
|
|
|
|
downloadCsv('图片失败素材.csv', rows);
|
2026-05-15 10:51:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const exportVideoUrls = () => {
|
2026-05-15 11:22:30 +08:00
|
|
|
|
const failedVideos = videoList.value.filter((item) => item.verifyStatus === 'REJECTED');
|
|
|
|
|
|
if (!failedVideos.length) {
|
|
|
|
|
|
ElMessage.warning('没有失败的视频素材');
|
2026-05-15 10:51:06 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const rows = [['ID', '视频ID', '账户ID', '预览URL', '状态', '描述']];
|
2026-05-15 11:22:30 +08:00
|
|
|
|
failedVideos.forEach((item) => {
|
2026-05-15 10:51:06 +08:00
|
|
|
|
rows.push([item.id, item.videoId, item.accountId, item.previewUrl || '-', getStatusText(item.verifyStatus), item.description || '-']);
|
|
|
|
|
|
});
|
2026-05-15 11:22:30 +08:00
|
|
|
|
downloadCsv('视频失败素材.csv', rows);
|
2026-05-15 10:51:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const downloadCsv = (filename: string, rows: string[][]) => {
|
|
|
|
|
|
const csv = rows.map((r) => r.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
|
|
|
|
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = URL.createObjectURL(blob);
|
|
|
|
|
|
link.download = filename;
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
URL.revokeObjectURL(link.href);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 批量送检
|
|
|
|
|
|
const batchVerifyImage = () => {
|
|
|
|
|
|
ElMessageBox.confirm('确认批量校验待处理的图片?', '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
batchLoading.value = true;
|
|
|
|
|
|
batchVerifyImageApi()
|
|
|
|
|
|
.then((res: any) => {
|
|
|
|
|
|
ElMessage.success(res?.msg || '批量校验完成');
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
batchLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const batchVerifyVideo = () => {
|
|
|
|
|
|
ElMessageBox.confirm('确认批量校验待处理的视频?', '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
batchLoading.value = true;
|
|
|
|
|
|
batchVerifyVideoApi()
|
|
|
|
|
|
.then((res: any) => {
|
|
|
|
|
|
ElMessage.success(res?.msg || '批量校验完成');
|
|
|
|
|
|
loadVideoList();
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
batchLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 预览
|
|
|
|
|
|
const previewMedia = (url: string, type: string) => {
|
|
|
|
|
|
if (!url) {
|
|
|
|
|
|
ElMessage.warning('无预览地址');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
previewUrl.value = url;
|
|
|
|
|
|
previewType.value = type;
|
|
|
|
|
|
previewVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 工具方法
|
|
|
|
|
|
const getStatusText = (status: string) => {
|
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
|
PENDING: '待校验',
|
|
|
|
|
|
SUBMITTING: '送检中',
|
|
|
|
|
|
VERIFIED: '校验通过',
|
|
|
|
|
|
REJECTED: '校验不通过',
|
|
|
|
|
|
};
|
|
|
|
|
|
return map[status] || status || '待校验';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-15 13:15:24 +08:00
|
|
|
|
// 查看日志
|
|
|
|
|
|
const viewLog = (row: any) => {
|
|
|
|
|
|
// 模拟日志数据
|
|
|
|
|
|
currentLogList.value = [
|
|
|
|
|
|
{ time: new Date().toLocaleString('zh-CN'), type: 'INFO', message: '素材已创建' },
|
|
|
|
|
|
{ time: new Date().toLocaleString('zh-CN'), type: 'INFO', message: '开始校验流程' },
|
|
|
|
|
|
{ time: new Date().toLocaleString('zh-CN'), type: 'SUCCESS', message: `校验完成,结果:${getStatusText(row.verifyStatus)}` },
|
|
|
|
|
|
];
|
|
|
|
|
|
logVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取日志类型文本
|
|
|
|
|
|
const getLogTypeText = (type: string) => {
|
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
|
INFO: '信息',
|
|
|
|
|
|
SUCCESS: '成功',
|
|
|
|
|
|
WARN: '警告',
|
|
|
|
|
|
ERROR: '错误',
|
|
|
|
|
|
};
|
|
|
|
|
|
return map[type] || type || '信息';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-15 10:51:06 +08:00
|
|
|
|
// 组件挂载时加载数据
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadStats();
|
|
|
|
|
|
loadImageList();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.ads-compliance-tencent {
|
|
|
|
|
|
padding: 24px;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
min-height: calc(100vh - 60px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.main-card {
|
2026-05-15 10:51:06 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
2026-05-15 13:07:38 +08:00
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.card-header {
|
2026-05-15 10:51:06 +08:00
|
|
|
|
display: flex;
|
2026-05-15 13:07:38 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.card-description {
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin: 12px 0 20px 0;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
|
border-bottom: 1px solid #ebeeef;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.main-tabs {
|
|
|
|
|
|
margin-top: 16px;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.tab-content {
|
|
|
|
|
|
padding: 20px 0;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 16px;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.search-form {
|
2026-05-15 10:51:06 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-05-15 13:07:38 +08:00
|
|
|
|
}
|
2026-05-15 10:51:06 +08:00
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.search-form :deep(.el-form-item) {
|
|
|
|
|
|
margin-right: 16px;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-05-15 10:51:06 +08:00
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.search-form :deep(.el-form-item__label) {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
padding-right: 8px;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.search-form :deep(.el-button) {
|
|
|
|
|
|
margin-left: 8px;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.action-buttons {
|
2026-05-15 10:51:06 +08:00
|
|
|
|
display: flex;
|
2026-05-15 13:07:38 +08:00
|
|
|
|
gap: 12px;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.action-buttons :deep(.el-button) {
|
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
2026-05-15 10:51:06 +08:00
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.table-wrapper {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
overflow: hidden;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.table-wrapper :deep(.el-table) {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-wrapper :deep(.el-table__header th) {
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #606266;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-status {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
&.status-pending {
|
|
|
|
|
|
background: #fdf6ec;
|
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-submitting {
|
|
|
|
|
|
background: #e8f4fd;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-verified {
|
|
|
|
|
|
background: #f0f9eb;
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-rejected {
|
|
|
|
|
|
background: #fef0f0;
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
2026-05-15 13:15:24 +08:00
|
|
|
|
|
|
|
|
|
|
&.status-info {
|
|
|
|
|
|
background: #e8f4fd;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-success {
|
|
|
|
|
|
background: #f0f9eb;
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-warn {
|
|
|
|
|
|
background: #fdf6ec;
|
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-error {
|
|
|
|
|
|
background: #fef0f0;
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 13:07:38 +08:00
|
|
|
|
.pagination-container {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-preview {
|
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
|
max-height: 150px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.video-preview {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
height: 120px;
|
|
|
|
|
|
background: #000;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.description-text {
|
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
2026-05-15 13:07:38 +08:00
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
margin: 0;
|
2026-05-15 10:51:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|