refactor: 合并 NSFW 数据到 media_info 表

This commit is contained in:
2025-07-26 21:05:51 +08:00
parent 95e6297076
commit 59b0206cf1
12 changed files with 159 additions and 463 deletions
+53
View File
@@ -25,6 +25,9 @@
<el-select v-model="query.format" placeholder="格式" clearable filterable size="small" style="width: 120px" @change="handleFilterChange">
<el-option v-for="f in formatOptions" :key="f" :label="f" :value="f"/>
</el-select>
<el-select v-model="query.nsfwOnly" placeholder="色情" clearable size="small" style="width: 110px" @change="handleFilterChange">
<el-option label="仅色情" :value="true"/>
</el-select>
<el-input v-model="query.keyword" placeholder="搜索文件名" size="small" clearable style="width: 180px" @keyup.enter.native="handleFilterChange" @clear="handleFilterChange">
<i slot="prefix" class="el-input__icon el-icon-search"/>
</el-input>
@@ -32,6 +35,21 @@
</div>
<el-table :data="list" v-loading="loading" border stripe @sort-change="handleSortChange">
<el-table-column label="预览" width="80" align="center">
<template slot-scope="scope">
<el-image
v-if="isImage(scope.row.mediaType)"
:src="getPreviewUrl(scope.row.filePath)"
:preview-src-list="[getPreviewUrl(scope.row.filePath)]"
style="width: 60px; height: 60px"
fit="cover">
<div slot="error" class="image-error">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
<span v-else class="muted">-</span>
</template>
</el-table-column>
<el-table-column prop="fileName" label="文件名" min-width="100" show-overflow-tooltip>
<template slot-scope="scope">
<span class="file-name" :title="scope.row.filePath">{{ scope.row.fileName }}</span>
@@ -60,6 +78,17 @@
<span v-else class="muted">-</span>
</template>
</el-table-column>
<el-table-column label="色情检测" width="140" align="center">
<template slot-scope="scope">
<div v-if="scope.row.nsfwScore > 0">
<el-tag :type="scope.row.nsfwScore >= scope.row.nsfwThreshold ? 'danger' : 'warning'" size="small">
{{ (scope.row.nsfwScore * 100).toFixed(1) }}%
</el-tag>
<div class="nsfw-threshold">阈值: {{ (scope.row.nsfwThreshold * 100).toFixed(0) }}%</div>
</div>
<span v-else class="muted">-</span>
</template>
</el-table-column>
<el-table-column prop="codec" label="编码" width="120" align="center">
<template slot-scope="scope">
<span class="muted" v-if="!scope.row.codec">-</span>
@@ -134,6 +163,7 @@ export default {
resolution: '',
codec: '',
format: '',
nsfwOnly: null,
keyword: '',
sortField: '',
sortOrder: ''
@@ -196,6 +226,12 @@ export default {
typeTag(type) {
return {image: '', video: 'primary', audio: 'warning'}[type] || 'info'
},
isImage(type) {
return type === 'image'
},
getPreviewUrl(filePath) {
return '/api/media/preview?path=' + encodeURIComponent(filePath)
},
formatTime(value) {
if (!value) return '-'
const d = new Date(value)
@@ -226,6 +262,23 @@ export default {
color: #c0c4cc;
}
.image-error {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background: #f5f7fa;
color: #c0c4cc;
font-size: 20px;
}
.nsfw-threshold {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.pagination {
margin-top: 16px;
text-align: right;
-198
View File
@@ -1,198 +0,0 @@
<template>
<div class="page-card">
<div class="toolbar">
<span class="title">色情识别</span>
<div class="toolbar-right">
<span class="hint" v-if="!nsfwEnabled">识别功能未开启:请在 application.properties 设 nsfw.enabled=true 并重新扫描</span>
<span class="hint" v-else-if="!nsfwDetector">识别已配置开启,但最近一次扫描时检测服务没起来:请确认本机已安装 python 和 nudenet,再重新扫描</span>
<el-button type="primary" size="small" icon="el-icon-refresh" @click="loadList">刷新</el-button>
</div>
</div>
<div class="tip-bar">
<i class="el-icon-view"></i>
这是最近一次开启色情识别的扫描判出的图片,按检测分从高到低排列。点缩略图可放大预览,用来核对识别结果准不准。
</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column label="预览" width="110" align="center">
<template slot-scope="scope">
<el-image
class="thumb"
:src="previewUrl(scope.row.path)"
:preview-src-list="[previewUrl(scope.row.path)]"
fit="cover"
lazy
style="width: 84px; height: 84px; border-radius: 6px; border: 1px solid #e6efff">
<div slot="error" class="thumb-error">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</template>
</el-table-column>
<el-table-column prop="fileName" label="文件名" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">
<span class="file-name" :title="scope.row.path">{{ scope.row.fileName }}</span>
</template>
</el-table-column>
<el-table-column label="检测分" width="160" align="center">
<template slot-scope="scope">
<span class="score" :class="scoreTag(scope.row)">{{ (scope.row.score * 100).toFixed(1) }}%</span>
</template>
</el-table-column>
<el-table-column label="阈值" width="100" align="center">
<template slot-scope="scope">
<span class="muted">{{ (scope.row.threshold * 100).toFixed(1) }}%</span>
</template>
</el-table-column>
<el-table-column label="完整路径" min-width="240" show-overflow-tooltip>
<template slot-scope="scope">
<span class="path-text">{{ scope.row.path }}</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="检测时间" width="180" align="center">
<template slot-scope="scope">
<span class="muted">{{ scope.row.createdAt }}</span>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="!loading && list.length === 0"
:description="emptyDesc"/>
</div>
</template>
<script>
import request from '../utils/request'
/**
* 色情识别页面。
* 展示最近一次开启色情识别的扫描判出的图片:检测分 + 完整路径 + 图片预览。
* 数据来自 /api/nsfw/list,预览走 /api/nsfw/preview(后端按路径读本地文件)。
*/
export default {
name: 'NsfwList',
data() {
return {
loading: false,
list: [],
nsfwEnabled: true, // 配置里是否开了 nsfw.enabled
nsfwDetector: true, // 最近一次扫描时检测服务是否真的起来了
nsfwConfigured: true // 上两者都满足才算"识别可用"
}
},
computed: {
// 空列表时的提示文案:识别可用说明是"没命中",否则是"没开启/没起来"
emptyDesc() {
if (this.nsfwConfigured) {
return '识别已开启,最近一次扫描没有判出色情图片。'
}
return '暂无记录。开启 nsfw.enabled 并确保检测服务可用后重新扫描,判为色情的图片会出现在这里。'
}
},
mounted() {
this.loadList()
},
methods: {
async loadList() {
this.loading = true
try {
const res = await request.get('/api/nsfw/list')
this.list = res.data.list || []
// 后端真实上报:配置是否开启、检测服务上次是否真的起来了
this.nsfwEnabled = res.data.nsfwEnabled !== false
this.nsfwDetector = res.data.nsfwDetector === true
this.nsfwConfigured = this.nsfwEnabled && this.nsfwDetector
} finally {
this.loading = false
}
},
previewUrl(path) {
if (!path) return ''
return '/api/nsfw/preview?path=' + encodeURIComponent(path)
},
// 检测分越接近 1 越"实锤":>=90% 红色,>=75% 橙色,否则灰色
scoreTag(row) {
if (row.score >= 0.9) return 'score-high'
if (row.score >= 0.75) return 'score-mid'
return ''
}
}
}
</script>
<style scoped>
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.hint {
color: #e6a23c;
font-size: 12px;
}
.tip-bar {
display: flex;
align-items: center;
gap: 8px;
background: #f8fbff;
border: 1px solid #e6efff;
border-radius: 8px;
padding: 10px 14px;
color: #4a5a78;
font-size: 13px;
margin-bottom: 16px;
}
.tip-bar .el-icon-view {
color: #1677ff;
flex-shrink: 0;
}
.thumb {
display: block;
cursor: zoom-in;
}
.thumb-error {
width: 84px;
height: 84px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #c0c4cc;
font-size: 26px;
border-radius: 6px;
}
.file-name {
color: #1f3a6b;
}
.path-text {
color: #606266;
font-size: 12px;
word-break: break-all;
}
.score {
font-weight: 700;
color: #909399;
}
.score-high {
color: #f56c6c;
}
.score-mid {
color: #e6a23c;
}
.muted {
color: #c0c4cc;
}
</style>