2026-05-15 10:28:17 +08:00
|
|
|
|
<!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">
|
2026-05-15 14:01:15 +08:00
|
|
|
|
<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>
|
2026-05-15 10:28:17 +08:00
|
|
|
|
</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">
|
2026-05-15 14:01:15 +08:00
|
|
|
|
<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>
|
2026-05-15 10:28:17 +08:00
|
|
|
|
</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: [],
|
2026-05-15 14:01:15 +08:00
|
|
|
|
batchLoading: false,
|
|
|
|
|
|
// 账户列表(下拉筛选用)
|
|
|
|
|
|
accounts: []
|
2026-05-15 10:28:17 +08:00
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
|
|
|
|
|
this.loadStats();
|
|
|
|
|
|
this.loadImageList();
|
2026-05-15 14:01:15 +08:00
|
|
|
|
this.loadAccounts();
|
2026-05-15 10:28:17 +08:00
|
|
|
|
},
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-15 14:01:15 +08:00
|
|
|
|
// 加载账户列表(下拉筛选用)
|
|
|
|
|
|
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 : '';
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-15 10:28:17 +08:00
|
|
|
|
// 图片列表
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-15 14:01:15 +08:00
|
|
|
|
// 导出(不通过数据,含失败原因)
|
2026-05-15 10:28:17 +08:00
|
|
|
|
exportImageUrls() {
|
2026-05-15 14:01:15 +08:00
|
|
|
|
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));
|
2026-05-15 10:28:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
exportVideoUrls() {
|
2026-05-15 14:01:15 +08:00
|
|
|
|
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));
|
2026-05-15 10:28:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
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>
|