1
This commit is contained in:
@@ -4,6 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { EditPen, Loading, Picture, Plus, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import request from '../api/request'
|
||||
import FalseTargets from './FalseTargets.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@@ -45,9 +46,22 @@ const trainingIds = reactive(new Set()) // 训练发起中的数据集 id(请
|
||||
// ---------- 负样本库(技术设计.md「负样本库」):source=negative 特殊数据集,
|
||||
// 统一背景样本训练打包时混入全部物种数据集;图片无标注/审核流程,仅上传/删除 ----------
|
||||
|
||||
const pageTab = ref('datasets') // datasets 数据集卡片 | negatives 负样本库
|
||||
const pageTab = ref('datasets') // datasets 数据集卡片 | negatives 负样本库 | falseTargets 假目标上报
|
||||
const negLib = ref(null) // 负样本库数据集记录 {id, name, imageCount}
|
||||
const negList = ref([]) // 图片列表 [{id, filename, url}]
|
||||
|
||||
// ---------- 假目标上报 tab(2026-09-08):审核列表内嵌 FalseTargets 组件,radio 带待审数 ----------
|
||||
const ftRef = ref(null)
|
||||
const ftPendingCount = ref(0)
|
||||
|
||||
function loadFtPendingCount() {
|
||||
request
|
||||
.get('/false-targets', { params: { status: 'pending', page: 1, size: 1 } })
|
||||
.then((data) => {
|
||||
ftPendingCount.value = data.total || 0
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
const negLoading = ref(false)
|
||||
const negSelected = ref(new Set()) // 勾选的图片 id
|
||||
// 分页与详情页同款(客户端分页;每页条数 localStorage 持久化)
|
||||
@@ -114,6 +128,9 @@ function switchTab(tab) {
|
||||
pageTab.value = tab
|
||||
if (tab === 'negatives') {
|
||||
loadNegatives().then(loadNegGenTask)
|
||||
} else if (tab === 'falseTargets') {
|
||||
loadFtPendingCount()
|
||||
ftRef.value?.load()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +576,7 @@ function cancelTrain(row, t) {
|
||||
let trainPoll = null
|
||||
onMounted(() => {
|
||||
load()
|
||||
loadFtPendingCount()
|
||||
trainPoll = setInterval(() => {
|
||||
if (list.value.some((r) => trList(r).some((t) => t.status === 'running'))) load()
|
||||
}, 10000)
|
||||
@@ -572,12 +590,13 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<!-- 训练数据双 tab(2026-09-07):数据集 = 物种数据集卡片;负样本 = 统一背景样本库
|
||||
(无标注,训练打包时自动混入全部数据集) -->
|
||||
<!-- 训练数据三 tab:数据集 = 物种数据集卡片;负样本 = 统一背景样本库
|
||||
(无标注,训练打包时自动混入全部数据集);假目标上报 = App 误报回流待审 -->
|
||||
<div class="page-tabs">
|
||||
<el-radio-group :model-value="pageTab" size="small" @update:model-value="switchTab">
|
||||
<el-radio-button value="datasets">数据集</el-radio-button>
|
||||
<el-radio-button value="negatives">负样本{{ negLib ? ` ${negLib.imageCount}` : '' }}</el-radio-button>
|
||||
<el-radio-button value="falseTargets">假目标上报{{ ftPendingCount ? ` ${ftPendingCount}` : '' }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
@@ -681,7 +700,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<!-- 负样本 tab:与数据集详情页同款卡片网格(点击放大/多选删除/同款分页);仅 AI 生成,无本地上传 -->
|
||||
<template v-else>
|
||||
<template v-else-if="pageTab === 'negatives'">
|
||||
<div class="toolbar">
|
||||
<div class="neg-tip">
|
||||
负样本 = 不含目标动物的背景图(内置场景池 AI 生成 + 真实随手拍)。训练打包时统一混入全部物种数据集
|
||||
@@ -738,6 +757,9 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 假目标上报 tab:审核通过迁移入负样本库当背景图;列表/审核在 FalseTargets 组件 -->
|
||||
<FalseTargets v-if="pageTab === 'falseTargets'" ref="ftRef" />
|
||||
|
||||
<!-- 负样本批量生成:内置场景池 + RF-DETR 空检自动剔除,进度在负样本 tab 内展示 -->
|
||||
<el-dialog v-model="negGenVisible" title="AI 生成负样本" width="min(480px, 94vw)">
|
||||
<el-form label-width="90px">
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../api/request'
|
||||
|
||||
// 假目标上报审核(数据训练页第三个 tab):App 识别端误报一键回流(整帧 + 检测框快照);
|
||||
// 通过 = 整图迁移入负样本库当背景图训练;拒绝 = 删文件。
|
||||
// defineExpose(load) 供父页 tab 切换时刷新与待审数统计
|
||||
|
||||
const filter = reactive({
|
||||
phoneNum: '',
|
||||
status: 'pending',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
|
||||
const statusMap = {
|
||||
pending: { text: '待审核', type: 'warning' },
|
||||
approved: { text: '已通过', type: 'success' },
|
||||
}
|
||||
|
||||
// <img> 无法带自定义请求头,token 以 query 参数携带(与 Datasets.vue 同约定)
|
||||
function imgUrl(row) {
|
||||
const u = `/api/v1/admin/false-targets/image?id=${row.id}`
|
||||
return `${location.origin}${u}&token=${encodeURIComponent(localStorage.getItem('adminToken') || '')}`
|
||||
}
|
||||
|
||||
function boxText(row) {
|
||||
if (!row.boxes || row.boxes.length === 0) return '-'
|
||||
return row.boxes
|
||||
.map((b) => `${b.label || (b.class === 0 ? '目标' : '疑似')} ${(b.score * 100).toFixed(0)}%`)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
function load() {
|
||||
loading.value = true
|
||||
const params = { page: page.value, size: size.value }
|
||||
if (filter.phoneNum) params.phoneNum = filter.phoneNum
|
||||
if (filter.status) params.status = filter.status
|
||||
request
|
||||
.get('/false-targets', { params })
|
||||
.then((data) => {
|
||||
list.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
filter.phoneNum = ''
|
||||
filter.status = ''
|
||||
search()
|
||||
}
|
||||
|
||||
function review(row, approve) {
|
||||
const tip = approve
|
||||
? '通过后该图将迁入负样本库参与训练,确认通过?'
|
||||
: '拒绝后该上报记录及图片将被删除,确认拒绝?'
|
||||
ElMessageBox.confirm(tip, '审核确认', { type: 'warning' })
|
||||
.then(() =>
|
||||
request.post('/false-targets/review', { ids: [row.id], approve }).then(() => {
|
||||
ElMessage.success(approve ? '已通过,图片已入负样本库' : '已拒绝')
|
||||
load()
|
||||
}),
|
||||
)
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
defineExpose({ load })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="filter" inline @submit.prevent="search">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="filter.phoneNum" placeholder="精确匹配" clearable style="width: 200px" @keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filter.status" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v.text" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe>
|
||||
<el-table-column label="上报画面" width="170">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="imgUrl(row)"
|
||||
:preview-src-list="[imgUrl(row)]"
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
style="width: 120px; height: 68px; border-radius: 4px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phoneNum" label="上报用户" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="species" label="来源模型" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="检测框" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ boxText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(statusMap[row.status] || {}).type || 'info'" size="small">{{ (statusMap[row.status] || {}).text || row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上报时间" width="170">
|
||||
<template #default="{ row }">{{ row.createdAt || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核时间" width="170">
|
||||
<template #default="{ row }">{{ row.reviewedAt || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button type="success" size="small" @click="review(row, true)">通过</el-button>
|
||||
<el-button type="danger" size="small" @click="review(row, false)">拒绝</el-button>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pager"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@change="load"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pager {
|
||||
margin-top: 14px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user