Files
cid/resource/frontend/material-verify.html

1012 lines
48 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>素材送检状态管理</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
}
.app-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.header h1 {
color: #409EFF;
font-size: 24px;
margin-bottom: 10px;
}
.header p {
color: #909399;
font-size: 14px;
}
.stats-card {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-item {
background: #fff;
padding: 20px;
border-radius: 8px;
flex: 1;
text-align: center;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.stat-item.pending { border-left: 4px solid #E6A23C; }
.stat-item.verified { border-left: 4px solid #67C23A; }
.stat-item.rejected { border-left: 4px solid #F56C6C; }
.stat-value {
font-size: 32px;
font-weight: bold;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 8px;
}
.stat-breakdown {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed #ebeef5;
}
.stat-breakdown-item {
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
font-size: 13px;
line-height: 24px;
}
.stat-breakdown-item .type-tag {
display: inline-block;
padding: 1px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: normal;
min-width: 28px;
}
.stat-breakdown-item .type-tag.image {
background: #ecf5ff;
color: #409EFF;
}
.stat-breakdown-item .type-tag.video {
background: #fdf6ec;
color: #E6A23C;
}
.stat-breakdown-item .num {
font-weight: bold;
font-size: 14px;
}
.card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.filter-bar {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-item label {
color: #606266;
font-size: 14px;
}
.action-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.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; }
.pagination {
margin-top: 20px;
text-align: right;
}
.log-detail {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}
.log-detail pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: Monaco, Consolas, monospace;
font-size: 12px;
}
.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;
}
.tab-container {
margin-top: 20px;
}
.description-text {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<!-- 头部 -->
<div class="header">
<h1>素材送检状态管理</h1>
<p>腾讯图片/视频素材自动校验系统 - 基于易盾内容安全检测</p>
</div>
<!-- 统计卡片 -->
<div class="stats-card">
<div class="stat-item pending">
<div class="stat-value">{{ imageStats.pending + videoStats.pending || 0 }}</div>
<div class="stat-label">待校验</div>
<div class="stat-breakdown">
<div class="stat-breakdown-item">
<span class="type-tag image"></span>
<span>图片: </span>
<span class="num">{{ imageStats.pending || 0 }}</span>
</div>
<div class="stat-breakdown-item">
<span class="type-tag video"></span>
<span>视频: </span>
<span class="num">{{ videoStats.pending || 0 }}</span>
</div>
</div>
</div>
<div class="stat-item verified">
<div class="stat-value">{{ imageStats.verified + videoStats.verified || 0 }}</div>
<div class="stat-label">校验通过</div>
<div class="stat-breakdown">
<div class="stat-breakdown-item">
<span class="type-tag image"></span>
<span>图片: </span>
<span class="num">{{ imageStats.verified || 0 }}</span>
</div>
<div class="stat-breakdown-item">
<span class="type-tag video"></span>
<span>视频: </span>
<span class="num">{{ videoStats.verified || 0 }}</span>
</div>
</div>
</div>
<div class="stat-item rejected">
<div class="stat-value">{{ imageStats.rejected + videoStats.rejected || 0 }}</div>
<div class="stat-label">校验不通过</div>
<div class="stat-breakdown">
<div class="stat-breakdown-item">
<span class="type-tag image"></span>
<span>图片: </span>
<span class="num">{{ imageStats.rejected || 0 }}</span>
</div>
<div class="stat-breakdown-item">
<span class="type-tag video"></span>
<span>视频: </span>
<span class="num">{{ videoStats.rejected || 0 }}</span>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="图片素材" name="image">
<div class="card">
<!-- 筛选 -->
<div class="filter-bar">
<div class="filter-item">
<label>状态:</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>
</div>
<div class="filter-item">
<label>账户:</label>
<el-select v-model="imageFilters.accountId" placeholder="全部账户" clearable filterable style="width: 200px;" @change="searchImage">
<el-option
v-for="item in accounts"
:key="item.accountId"
:label="item.accountId + ' - ' + item.corporationName"
:value="item.accountId">
</el-option>
</el-select>
</div>
<el-button type="primary" @click="searchImage">搜索</el-button>
<el-button @click="resetImageFilter">重置</el-button>
</div>
<!-- 操作按钮 -->
<div class="action-bar">
<el-button type="success" @click="batchVerifyImage" :loading="batchLoading">
批量校验图片
</el-button>
<el-button type="primary" plain @click="pollImageResults" :loading="pollLoading">
<i class="el-icon-refresh"></i> 刷新检测结果
</el-button>
<el-button type="warning" plain @click="exportImageUrls">
<i class="el-icon-download"></i> 导出
</el-button>
</div>
<!-- 表格 -->
<el-table :data="imageList" border style="width: 100%;" v-loading="imageLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="imageId" label="图片ID" width="180"></el-table-column>
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
<el-table-column prop="imageUsage" label="用途" width="100"></el-table-column>
<el-table-column label="预览" width="120">
<template slot-scope="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="110">
<template slot-scope="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="150">
<template slot-scope="scope">
<span class="description-text" :title="scope.row.description">{{ scope.row.description || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="showLogs('IMAGE', scope.row.imageId)">查看日志</el-button>
<el-button size="mini" type="text" @click="verifyImage(scope.row.imageId)">送检</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
@current-change="handleImagePageChange"
:current-page="imagePage"
:page-size="imagePageSize"
layout="total, prev, pager, next"
:total="imageTotal">
</el-pagination>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="视频素材" name="video">
<div class="card">
<!-- 筛选 -->
<div class="filter-bar">
<div class="filter-item">
<label>状态:</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>
</div>
<div class="filter-item">
<label>账户:</label>
<el-select v-model="videoFilters.accountId" placeholder="全部账户" clearable filterable style="width: 200px;" @change="searchVideo">
<el-option
v-for="item in accounts"
:key="item.accountId"
:label="item.accountId + ' - ' + item.corporationName"
:value="item.accountId">
</el-option>
</el-select>
</div>
<el-button type="primary" @click="searchVideo">搜索</el-button>
<el-button @click="resetVideoFilter">重置</el-button>
</div>
<!-- 操作按钮 -->
<div class="action-bar">
<el-button type="success" @click="batchVerifyVideo" :loading="batchLoading">
批量校验视频
</el-button>
<el-button type="primary" plain @click="pollVideoResults" :loading="pollLoading">
<i class="el-icon-refresh"></i> 刷新检测结果
</el-button>
<el-button type="warning" plain @click="exportVideoUrls">
<i class="el-icon-download"></i> 导出
</el-button>
</div>
<!-- 表格 -->
<el-table :data="videoList" border style="width: 100%;" v-loading="videoLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="videoId" label="视频ID" width="180"></el-table-column>
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
<el-table-column label="预览" width="120">
<template slot-scope="scope">
<div class="video-preview" @click="previewMedia(scope.row.previewUrl, 'video')">
<i class="el-icon-video-play" style="font-size: 32px;"></i>
</div>
</template>
</el-table-column>
<el-table-column prop="verifyStatus" label="校验状态" width="110">
<template slot-scope="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="150">
<template slot-scope="scope">
<span class="description-text" :title="scope.row.description">{{ scope.row.description || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="showLogs('VIDEO', scope.row.videoId)">查看日志</el-button>
<el-button size="mini" type="text" @click="verifyVideo(scope.row.videoId)">送检</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
@current-change="handleVideoPageChange"
:current-page="videoPage"
:page-size="videoPageSize"
layout="total, prev, pager, next"
:total="videoTotal">
</el-pagination>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="校验日志" name="log">
<div class="card">
<!-- 筛选 -->
<div class="filter-bar">
<div class="filter-item">
<label>素材类型:</label>
<el-select v-model="logFilters.materialType" placeholder="全部" clearable style="width: 120px;">
<el-option label="全部" value=""></el-option>
<el-option label="图片" value="IMAGE"></el-option>
<el-option label="视频" value="VIDEO"></el-option>
</el-select>
</div>
<div class="filter-item">
<label>校验状态:</label>
<el-select v-model="logFilters.verifyStatus" placeholder="全部" clearable style="width: 120px;">
<el-option label="全部" value=""></el-option>
<el-option label="待校验" value="PENDING"></el-option>
<el-option label="校验通过" value="VERIFIED"></el-option>
<el-option label="校验不通过" value="REJECTED"></el-option>
</el-select>
</div>
<div class="filter-item">
<label>素材ID:</label>
<el-input v-model="logFilters.materialId" placeholder="素材ID" style="width: 150px;" clearable></el-input>
</div>
<el-button type="primary" @click="searchLog">搜索</el-button>
<el-button @click="resetLogFilter">重置</el-button>
</div>
<!-- 表格 -->
<el-table :data="logList" border style="width: 100%;" v-loading="logLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="materialType" label="类型" width="80">
<template slot-scope="scope">
{{ scope.row.materialType === 'IMAGE' ? '图片' : '视频' }}
</template>
</el-table-column>
<el-table-column prop="materialId" label="素材ID" width="180"></el-table-column>
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
<el-table-column prop="taskId" label="任务ID" width="180"></el-table-column>
<el-table-column prop="verifyStatus" label="校验状态" width="110">
<template slot-scope="scope">
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
{{ getStatusText(scope.row.verifyStatus) }}
</span>
</template>
</el-table-column>
<el-table-column prop="suggestion" label="建议" width="80">
<template slot-scope="scope">
{{ getSuggestionText(scope.row.suggestion) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="160">
<template slot-scope="scope">
{{ formatTime(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="showLogDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
@current-change="handleLogPageChange"
:current-page="logPage"
:page-size="logPageSize"
layout="total, prev, pager, next"
:total="logTotal">
</el-pagination>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 日志详情对话框 -->
<el-dialog title="校验日志详情" :visible.sync="logDialogVisible" width="800px">
<div v-if="currentLog">
<el-descriptions :column="2" border>
<el-descriptions-item label="日志ID">{{ currentLog.id }}</el-descriptions-item>
<el-descriptions-item label="素材类型">{{ currentLog.materialType === 'IMAGE' ? '图片' : '视频' }}</el-descriptions-item>
<el-descriptions-item label="素材ID">{{ currentLog.materialId }}</el-descriptions-item>
<el-descriptions-item label="账户ID">{{ currentLog.accountId }}</el-descriptions-item>
<el-descriptions-item label="任务ID">{{ currentLog.taskId || '-' }}</el-descriptions-item>
<el-descriptions-item label="校验状态">
<span :class="'table-status status-' + (currentLog.verifyStatus || 'pending').toLowerCase()">
{{ getStatusText(currentLog.verifyStatus) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="处置建议">{{ getSuggestionText(currentLog.suggestion) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(currentLog.createdAt) }}</el-descriptions-item>
</el-descriptions>
<h4 style="margin-top: 20px;">请求参数:</h4>
<div class="log-detail">
<pre>{{ currentLog.requestParams || '无' }}</pre>
</div>
<h4>响应结果:</h4>
<div class="log-detail">
<pre>{{ currentLog.responseResult || '无' }}</pre>
</div>
<h4>错误信息:</h4>
<div class="log-detail" v-if="currentLog.errorMsg">
<pre style="color: #F56C6C;">{{ currentLog.errorMsg }}</pre>
</div>
<div class="log-detail" v-else>
<pre></pre>
</div>
</div>
</el-dialog>
<!-- 预览对话框 -->
<el-dialog title="媒体预览" :visible.sync="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>
<!-- 日志列表对话框 -->
<el-dialog title="校验日志" :visible.sync="logsDialogVisible" width="900px">
<el-table :data="materialLogs" border style="width: 100%;" size="small">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="taskId" label="任务ID" width="180"></el-table-column>
<el-table-column prop="verifyStatus" label="状态" width="100">
<template slot-scope="scope">
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
{{ getStatusText(scope.row.verifyStatus) }}
</span>
</template>
</el-table-column>
<el-table-column prop="suggestion" label="建议" width="80">
<template slot-scope="scope">
{{ getSuggestionText(scope.row.suggestion) }}
</template>
</el-table-column>
<el-table-column prop="errorMsg" label="错误信息" show-overflow-tooltip></el-table-column>
<el-table-column prop="createdAt" label="时间" width="160">
<template slot-scope="scope">
{{ formatTime(scope.row.createdAt) }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js"></script>
<script>
// API 基础地址
const API_BASE = 'http://localhost:3001';
new Vue({
el: '#app',
data: {
activeTab: 'image',
// 轮询加载
pollLoading: false,
// 图片统计
imageStats: { pending: 0, verified: 0, rejected: 0 },
// 视频统计
videoStats: { pending: 0, verified: 0, rejected: 0 },
// 图片
imageList: [],
imageLoading: false,
imagePage: 1,
imagePageSize: 20,
imageTotal: 0,
imageFilters: { status: '', accountId: '' },
// 视频
videoList: [],
videoLoading: false,
videoPage: 1,
videoPageSize: 20,
videoTotal: 0,
videoFilters: { status: '', accountId: '' },
// 日志
logList: [],
logLoading: false,
logPage: 1,
logPageSize: 20,
logTotal: 0,
logFilters: { materialType: '', verifyStatus: '', materialId: '' },
// 对话框
logDialogVisible: false,
previewVisible: false,
logsDialogVisible: false,
currentLog: null,
previewUrl: '',
previewType: 'image',
materialLogs: [],
batchLoading: false,
// 账户列表(下拉筛选用)
accounts: []
},
mounted() {
this.loadStats();
this.loadImageList();
this.loadAccounts();
},
methods: {
// API 请求
apiGet(url, params = {}) {
return axios.get(API_BASE + url, { params });
},
apiPost(url, data = {}) {
return axios.post(API_BASE + url, data);
},
// 加载统计(图片和视频分开存储)
loadStats() {
Promise.all([
this.apiGet('/material/verify/controller/stats-image'),
this.apiGet('/material/verify/controller/stats-video')
]).then(results => {
const imgStats = results[0].data.data || {};
const vidStats = results[1].data.data || {};
this.imageStats = {
pending: imgStats.pending || 0,
verified: imgStats.verified || 0,
rejected: imgStats.rejected || 0
};
this.videoStats = {
pending: vidStats.pending || 0,
verified: vidStats.verified || 0,
rejected: vidStats.rejected || 0
};
}).catch(err => {
console.error('加载统计失败', err);
});
},
// 加载账户列表(下拉筛选用)
loadAccounts() {
this.apiPost('/material/verify/controller/list-accounts', {}).then(res => {
this.accounts = res.data.data?.list || [];
}).catch(err => {
console.error('加载账户列表失败', err);
});
},
// 获取账户名称(显示用)
getAccountName(accountId) {
const account = this.accounts.find(a => a.accountId === accountId);
return account ? account.corporationName : '';
},
// 图片列表
loadImageList() {
this.imageLoading = true;
const params = {
page: this.imagePage,
pageSize: this.imagePageSize,
status: this.imageFilters.status,
accountId: this.imageFilters.accountId
};
this.apiGet('/material/verify/controller/list-image', params).then(res => {
this.imageList = res.data.data?.list || [];
this.imageTotal = res.data.data?.total || 0;
}).catch(err => {
this.$message.error('加载图片列表失败');
console.error(err);
}).finally(() => {
this.imageLoading = false;
});
},
// 视频列表
loadVideoList() {
this.videoLoading = true;
const params = {
page: this.videoPage,
pageSize: this.videoPageSize,
status: this.videoFilters.status,
accountId: this.videoFilters.accountId
};
this.apiGet('/material/verify/controller/list-video', params).then(res => {
this.videoList = res.data.data?.list || [];
this.videoTotal = res.data.data?.total || 0;
}).catch(err => {
this.$message.error('加载视频列表失败');
console.error(err);
}).finally(() => {
this.videoLoading = false;
});
},
// 日志列表
loadLogList() {
this.logLoading = true;
const params = {
page: this.logPage,
pageSize: this.logPageSize,
materialType: this.logFilters.materialType,
verifyStatus: this.logFilters.verifyStatus,
materialId: this.logFilters.materialId
};
this.apiGet('/material/verify/controller/list-log', params).then(res => {
this.logList = res.data.data?.list || [];
this.logTotal = res.data.data?.total || 0;
}).catch(err => {
this.$message.error('加载日志列表失败');
console.error(err);
}).finally(() => {
this.logLoading = false;
});
},
// Tab 切换
handleTabClick(tab) {
if (tab.name === 'image') {
this.loadImageList();
} else if (tab.name === 'video') {
this.loadVideoList();
} else if (tab.name === 'log') {
this.loadLogList();
}
},
// 搜索
searchImage() {
this.imagePage = 1;
this.loadImageList();
},
searchVideo() {
this.videoPage = 1;
this.loadVideoList();
},
searchLog() {
this.logPage = 1;
this.loadLogList();
},
// 重置筛选
resetImageFilter() {
this.imageFilters = { status: '', accountId: '' };
this.searchImage();
},
resetVideoFilter() {
this.videoFilters = { status: '', accountId: '' };
this.searchVideo();
},
resetLogFilter() {
this.logFilters = { materialType: '', verifyStatus: '', materialId: '' };
this.searchLog();
},
// 分页
handleImagePageChange(page) {
this.imagePage = page;
this.loadImageList();
},
handleVideoPageChange(page) {
this.videoPage = page;
this.loadVideoList();
},
handleLogPageChange(page) {
this.logPage = page;
this.loadLogList();
},
// 手动送检
verifyImage(imageId) {
this.$confirm('确认提交图片 ' + imageId + ' 进行校验?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.apiPost('/material/verify/controller/manual-verify-image', { materialId: imageId }).then(res => {
this.$message.success('提交成功');
this.loadImageList();
this.loadStats();
}).catch(err => {
this.$message.error('提交失败: ' + (err.response?.data?.msg || err.message));
});
}).catch(() => {});
},
verifyVideo(videoId) {
this.$confirm('确认提交视频 ' + videoId + ' 进行校验?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.apiPost('/material/verify/controller/manual-verify-video', { materialId: videoId }).then(res => {
this.$message.success('提交成功');
this.loadVideoList();
this.loadStats();
}).catch(err => {
this.$message.error('提交失败: ' + (err.response?.data?.msg || err.message));
});
}).catch(() => {});
},
// 刷新检测结果(轮询)
pollImageResults() {
this.pollLoading = true;
this.apiPost('/yidun/callback/controller/poll-image-results').then(res => {
const d = res.data;
this.$message.success(d.msg || '刷新完成');
this.loadImageList();
this.loadStats();
}).catch(err => {
this.$message.error('刷新失败: ' + (err.response?.data?.msg || err.message));
}).finally(() => {
this.pollLoading = false;
});
},
pollVideoResults() {
this.pollLoading = true;
this.apiPost('/yidun/callback/controller/poll-video-results').then(res => {
const d = res.data;
this.$message.success(d.msg || '刷新完成');
this.loadVideoList();
this.loadStats();
}).catch(err => {
this.$message.error('刷新失败: ' + (err.response?.data?.msg || err.message));
}).finally(() => {
this.pollLoading = false;
});
},
// 导出(不通过数据,含失败原因)
exportImageUrls() {
this.apiPost('/material/verify/controller/export-rejected', { materialType: 'IMAGE' }).then(res => {
const items = res.data.data?.items || [];
if (!items.length) {
this.$message.warning('没有不通过的图片数据');
return;
}
const rows = [['序号', '账户(名称)', '预览URL', '描述', '失败原因', '检测时间']];
items.forEach((item, index) => {
const accountLabel = item.corporationName ? item.accountId + ' - ' + item.corporationName : item.accountId;
rows.push([
index + 1, accountLabel,
item.previewUrl || '-', item.description || '-',
item.errorMsg || '-', item.createdAt || '-'
]);
});
this.downloadCsv('图片不通过数据.csv', rows);
this.$message.success('导出成功,共 ' + items.length + ' 条');
}).catch(err => {
this.$message.error('导出失败: ' + (err.response?.data?.msg || err.message));
});
},
exportVideoUrls() {
this.apiPost('/material/verify/controller/export-rejected', { materialType: 'VIDEO' }).then(res => {
const items = res.data.data?.items || [];
if (!items.length) {
this.$message.warning('没有不通过的视频数据');
return;
}
const rows = [['序号', '账户(名称)', '预览URL', '描述', '失败原因', '检测时间']];
items.forEach((item, index) => {
const accountLabel = item.corporationName ? item.accountId + ' - '+ item.corporationName : item.accountId;
rows.push([
index + 1, accountLabel,
item.previewUrl || '-', item.description || '-',
item.errorMsg || '-', item.createdAt || '-'
]);
});
this.downloadCsv('视频不通过数据.csv', rows);
this.$message.success('导出成功,共 ' + items.length + ' 条');
}).catch(err => {
this.$message.error('导出失败: ' + (err.response?.data?.msg || err.message));
});
},
downloadCsv(filename, rows) {
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);
},
// 批量送检
batchVerifyImage() {
this.$confirm('确认批量校验待处理的图片?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.batchLoading = true;
this.apiPost('/material/verify/controller/batch-verify-image', {}).then(res => {
this.$message.success(res.data.msg || '批量校验完成');
this.loadImageList();
this.loadStats();
}).catch(err => {
this.$message.error('批量校验失败: ' + (err.response?.data?.msg || err.message));
}).finally(() => {
this.batchLoading = false;
});
}).catch(() => {});
},
batchVerifyVideo() {
this.$confirm('确认批量校验待处理的视频?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.batchLoading = true;
this.apiPost('/material/verify/controller/batch-verify-video', {}).then(res => {
this.$message.success(res.data.msg || '批量校验完成');
this.loadVideoList();
this.loadStats();
}).catch(err => {
this.$message.error('批量校验失败: ' + (err.response?.data?.msg || err.message));
}).finally(() => {
this.batchLoading = false;
});
}).catch(() => {});
},
// 查看日志
showLogs(materialType, materialId) {
this.apiGet('/material/verify/controller/list-log', {
materialType: materialType,
materialId: materialId,
pageSize: 100
}).then(res => {
// 后端返回格式为 { list: [...], total: xxx }
this.materialLogs = (res.data.data && res.data.data.list) || [];
this.logsDialogVisible = true;
}).catch(err => {
this.$message.error('加载日志失败');
});
},
// 日志详情
showLogDetail(log) {
this.currentLog = log;
this.logDialogVisible = true;
},
// 预览
previewMedia(url, type) {
if (!url) {
this.$message.warning('无预览地址');
return;
}
this.previewUrl = url;
this.previewType = type;
this.previewVisible = true;
},
// 工具方法
getStatusText(status) {
const map = {
'PENDING': '待校验',
'SUBMITTING': '送检中',
'VERIFIED': '校验通过',
'REJECTED': '校验不通过'
};
return map[status] || status || '待校验';
},
getSuggestionText(suggestion) {
const map = {
0: '通过',
1: '嫌疑',
2: '不通过'
};
return map[suggestion] || '-';
},
formatTime(timeStr) {
if (!timeStr) return '-';
// 后端 gf gtime.Time 输出北京时间但不含时区(如 "2026-05-15 09:35:07"
// 手动补上 +08:00 时区new Date() 才能正确解析
if (typeof timeStr === 'string' && !/Z|[+-]\d{2}:\d{2}$/i.test(timeStr)) {
timeStr = timeStr.replace(' ', 'T') + '+08:00';
}
const date = new Date(timeStr);
if (isNaN(date.getTime())) return timeStr;
return date.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
}
});
</script>
</body>
</html>