<template>
|
<div class="image-gallery">
|
<!-- 添加导入组件 -->
|
<BatchImport ref="batchImport" :show-type-selector="false" @import="handleImportFiles" />
|
<!-- 模型训练弹窗 -->
|
<el-dialog class="dialog1" title="模型训练" :visible.sync="trainDialogVisible" width="372px" top="10vh">
|
<div class="info-label">样本信息</div>
|
<div class="sample-info">
|
<div class="sample-count">正样本数量:{{ positiveCount }}</div>
|
<div class="sample-count">负样本数量:{{ negativeCount }}</div>
|
</div>
|
<div slot="footer" class="dialog-footer">
|
<el-button @click="trainDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="startTraining">
|
<i class="el-icon-time"></i>
|
开始训练</el-button>
|
</div>
|
</el-dialog>
|
|
<!-- 批量标注弹窗 -->
|
<el-dialog class="dialog2" title="批量标注" :visible.sync="batchLabelDialogVisible" width="472px" top="10vh">
|
<div class="label-options">
|
<div class="label-option" :class="{ active: batchLabelStatus === 1 }" @click="batchLabelStatus = 1">
|
正确
|
</div>
|
<div class="label-option" :class="{ active: batchLabelStatus === 2 }" @click="batchLabelStatus = 2">
|
错误
|
</div>
|
<div class="label-option" :class="{ active: batchLabelStatus === 0 }" @click="batchLabelStatus = 0">
|
不确定
|
</div>
|
</div>
|
<div slot="footer" class="dialog-footer">
|
<el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
|
<el-button @click="batchLabelDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="confirmBatchLabeling">确定</el-button>
|
</div>
|
</el-dialog>
|
<!-- 顶部筛选区域 -->
|
<div class="filter-section">
|
<el-form :inline="true" class="filter-form">
|
<!-- 摄像机名称 -->
|
<el-form-item label="">
|
<el-input style="width: 256px;" v-model="filter.cameraName" placeholder="请输入摄像机名称" clearable />
|
</el-form-item>
|
|
<!-- 选择时段 -->
|
<el-form-item label="选择时段">
|
<el-date-picker style="width: 256px;" v-model="filter.timeRange" type="daterange"
|
value-format="yyyy-MM-dd hh:mm:ss" range-separator="至" start-placeholder="开始日期"
|
end-placeholder="结束日期" />
|
</el-form-item>
|
|
<!-- 分类 -->
|
<el-form-item label="分类">
|
<el-select style="width: 91px;" v-model="filter.category" placeholder="全部">
|
<el-option v-for="item in categories" :key="item.value" :label="item.label"
|
:value="item.value" />
|
</el-select>
|
</el-form-item>
|
|
<!-- 操作按钮 -->
|
<el-form-item>
|
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
</el-form-item>
|
|
<!-- 右侧功能按钮 -->
|
<div class="action-buttons">
|
<el-button v-if="!isBatchMode" type="primary" class="action-btn" @click="enterBatchMode">
|
批量标注
|
</el-button>
|
|
<el-dropdown>
|
<el-button type="primary" class="action-btn">
|
<i class="el-icon-download"></i> 导入
|
</el-button>
|
<el-dropdown-menu slot="dropdown" v-if="!this.trainId == 0">
|
<el-dropdown-item @click.native="openImportDialog(1)">正样本</el-dropdown-item>
|
<el-dropdown-item @click.native="openImportDialog(2)">负样本</el-dropdown-item>
|
<el-dropdown-item @click.native="openImportDialog(3)">待标记样本</el-dropdown-item>
|
</el-dropdown-menu>
|
</el-dropdown>
|
<el-button type="primary" @click="openTrainDialog" class="action-btn">
|
模型训练
|
</el-button>
|
</div>
|
</el-form>
|
</div>
|
|
<!-- 批量操作控制栏 -->
|
<div class="batch-controls" v-if="isBatchMode">
|
<div class="select-all">
|
<el-checkbox :indeterminate="isIndeterminate" v-model="selectAll"
|
@change="toggleSelectAll">全选</el-checkbox>
|
</div>
|
<!-- <div>已选择 {{ selectedCount }} 个项目</div> -->
|
<div class="batch-actions">
|
<el-button type="primary" @click="confirmBatch">确定</el-button>
|
<el-button @click="exitBatchMode">取消</el-button>
|
</div>
|
</div>
|
<!-- 图片展示区域
|
@status-change="handleStatusChange" @show-details="showImageDetails"
|
@edit-annotation="editAnnotation" @download="downloadImage"-->
|
<div class="gallery-section">
|
<el-row :gutter="20" class="image-grid">
|
<image-card v-for="(item, index) in galleryItems" :key="index" :item="item" :is-batch-mode="isBatchMode"
|
@delete-details="handleDeleteDetails" @status-change="handleStatusChange"
|
@card-click="handleCardClick1" @toggle-select="toggleSelect(index)" />
|
</el-row>
|
</div>
|
<!-- 分页组件 -->
|
<Pagination :total="totalCount" :current-page.sync="currentPage" :page-size.sync="pageSize"
|
@pagination-change="paginationChange" />
|
</div>
|
</template>
|
|
<script>
|
import BatchImport from './batchImport';
|
import imageCard from './imageCard';
|
import Pagination from '@/components/rightPagination';
|
import { getTrains, updateTrainStatus, deleteTrains, batchUpdateTrainStatus, batchDeleteTrains, uploadDataTrainTags } from "@/api/modelTuning";
|
export default {
|
components: {
|
imageCard,
|
Pagination,
|
BatchImport
|
|
},
|
name: 'ImageGallery',
|
data() {
|
return {
|
trainDialogVisible: false, // 模型训练弹窗可见性
|
batchLabelDialogVisible: false, // 批量标注弹窗可见性
|
// 模型训练弹窗数据
|
positiveCount: 0, // 正样本数量
|
negativeCount: 0, // 负样本数量
|
// 批量标注状态
|
batchLabelStatus: 0, // 默认选择"不确定"
|
isIndeterminate: false,
|
selectAll: false,
|
pagination: {},
|
trainId: 0,
|
totalCount: 0, // 总数据量
|
currentPage: 1, // 当前页码
|
pageSize: 12, // 每页数量
|
tableData: [], // 表格数据
|
// 是否批量模式
|
isBatchMode: false,
|
batchSelected: [], // 已选择的卡片索引
|
// 筛选条件
|
filter: {
|
cameraName: '',
|
timeRange: ['', ''],
|
category: -1
|
},
|
|
// 分类选项
|
categories: [
|
{ label: '全部', value: -1 },
|
{ label: '正确', value: 1 },
|
{ label: '错误', value: 2 },
|
{ label: '不确定', value: 0 }
|
],
|
|
// 图片数据
|
galleryItems: [
|
]
|
}
|
},
|
mounted() {
|
},
|
methods: {
|
// 打开导入对话框
|
openImportDialog(type) {
|
this.$refs.batchImport.presetType = type;
|
this.$refs.batchImport.open();
|
},
|
// 处理导入的文件
|
async handleImportFiles({ type, files }) {
|
try {
|
this.$loading({ text: `导入${this.getTypeName(type)}中...` });
|
// 1. 创建FormData用于文件上传
|
let formData = new FormData();
|
// 2. 添加实际文件内容到FormData
|
files.forEach(item => {
|
formData.append('file', item);
|
});
|
formData.append('tagId', this.trainId);
|
formData.append('status', type === 3 ? 0 : type);
|
|
// console.log(formData)
|
// // 模拟上传请求(实际应调用API)
|
let rspc = await uploadDataTrainTags(formData)
|
if (rspc && rspc.status === 200) {
|
this.$message({
|
type: 'success',
|
message: '成功'
|
});
|
this.fetchTableData()
|
} else {
|
this.$message({
|
type: 'error',
|
message: rspc.msg
|
});
|
|
}
|
} catch (error) {
|
this.$message.error(`导入失败: ${error.message}`);
|
} finally {
|
this.$loading().close();
|
}
|
},
|
getTypeName(type) {
|
return {
|
1: '正样本',
|
2: '负样本',
|
3: '待标记样本'
|
}[type];
|
},
|
// 模拟文件上传函数
|
async uploadFile(file, type) {
|
// 在实际应用中,这里应该是一个API调用
|
return new Promise((resolve) => {
|
const formData = new FormData();
|
formData.append('file', file);
|
formData.append('type', type);
|
formData.append('trainId', this.trainId);
|
|
// 调用上传API,例如:
|
// await uploadSample(formData);
|
resolve();
|
});
|
},
|
async paginationChange(params) {
|
this.currentPage = params.page
|
this.pageSize = params.pageSize
|
await this.fetchTableData()
|
},
|
async changeTrainId(trainId) {
|
// console.info(trainId)
|
this.trainId = trainId
|
this.isBatchMode = false
|
await this.fetchTableData()
|
},
|
// 获取表格数据方法
|
async fetchTableData(params) {
|
// console.info(this.currentPage)
|
this.galleryItems = []
|
// params.tagId = this.trainId
|
let rspc = await getTrains({
|
tagId: this.trainId,
|
page: this.currentPage,
|
pageSize: this.pageSize,
|
startTime: this.filter.timeRange[0] ? this.filter.timeRange[0] + " 00:00:00" : "",
|
endTime: this.filter.timeRange[1] ? this.filter.timeRange[1] + " 23:23:59" : "",
|
searchName: this.filter.cameraName,
|
status: this.filter.category
|
});
|
if (rspc && rspc.status === 200) {
|
if (rspc.data.list) {
|
this.galleryItems = rspc.data.list.map(item => ({
|
...item,
|
selected: false // 确保每个卡片都有初始值
|
}));
|
}
|
}
|
// console.log('trainId:', this.trainId);
|
this.totalCount = rspc.data.pagination.total
|
// 更新分页数据前先校验当前页码
|
const totalPage = rspc.data.pagination.totalPage
|
const currentPage = this.currentPage > totalPage
|
? totalPage
|
: rspc.data.pagination.page
|
this.currentPage = currentPage,
|
this.totalPage = totalPage
|
},
|
//删除
|
async handleDeleteDetails(item) {
|
// console.log('删除', item);
|
this.$confirm('此操作将永久删除数据, 是否继续?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}).then(async () => {
|
let rspc = await deleteTrains({
|
trainId: item.trainId,
|
});
|
if (rspc && rspc.status === 200) {
|
this.$message({
|
type: 'success',
|
message: '成功'
|
});
|
// 删除成功后自动修正页码
|
if (this.galleryItems && this.galleryItems.length === 1 && this.currentPage > 1) {
|
this.currentPage -= 1
|
}
|
this.fetchTableData()
|
} else {
|
this.$message({
|
type: 'error',
|
message: rspc.msg
|
});
|
|
}
|
}).catch(() => {
|
console.info("取消")
|
});
|
},
|
//修改图片状态
|
async handleStatusChange(parm) {
|
// console.log('修改状态', parm);
|
let rspc = await updateTrainStatus(parm);
|
if (rspc && rspc.status === 200) {
|
this.$message({
|
type: 'success',
|
message: '成功'
|
});
|
|
// for (let i = 0; i < this.galleryItems.length; i++) {
|
// if (parm.trainId === this.galleryItems[i].trainId) {
|
// this.galleryItems[i].status = parm.status
|
// }
|
// }
|
this.fetchTableData()
|
} else {
|
this.$message({
|
type: 'error',
|
message: rspc.msg
|
});
|
|
}
|
},
|
//详情
|
handleCardClick1() {
|
console.log('执行详情');
|
},
|
// 处理搜索
|
handleSearch() {
|
// console.log('执行搜索:', this.filter);
|
// console.log('filter:', this.filter.cameraName);
|
this.currentPage = 1, // 当前页码
|
// 这里可以添加实际的搜索逻辑
|
this.fetchTableData()
|
},
|
|
// 重置筛选条件
|
handleReset() {
|
this.filter = {
|
cameraName: '',
|
timeRange: ['', ''],
|
category: -1
|
};
|
console.log('已重置筛选条件');
|
},
|
// 进入批量模式
|
enterBatchMode() {
|
this.isBatchMode = true;
|
this.selectAll = false;
|
this.isIndeterminate = false;
|
this.batchSelected = [];
|
|
// 清除已选状态
|
this.galleryItems.forEach(item => {
|
item.selected = false;
|
});
|
},
|
|
// 退出批量模式
|
exitBatchMode() {
|
this.isBatchMode = false;
|
this.selectAll = false;
|
this.isIndeterminate = false;
|
this.batchSelected = [];
|
|
// 清除已选状态
|
this.galleryItems.forEach(item => {
|
if (item.hasOwnProperty('selected')) {
|
item.selected = false;
|
}
|
});
|
},
|
toggleSelect(index) {
|
if (!this.isBatchMode) return;
|
|
const isCurrentlySelected = this.galleryItems[index].selected;
|
|
// 更新选中状态
|
this.$set(this.galleryItems[index], 'selected', !isCurrentlySelected);
|
|
// 更新batchSelected数组
|
if (!isCurrentlySelected) {
|
// 添加选择
|
this.batchSelected.push(index);
|
} else {
|
// 移除选择
|
const position = this.batchSelected.indexOf(index);
|
if (position !== -1) {
|
this.batchSelected.splice(position, 1);
|
}
|
}
|
this.checkAllSelected();
|
},
|
checkAllSelected() {
|
this.selectAll = this.batchSelected.length === this.galleryItems.length;
|
this.isIndeterminate = this.batchSelected.length > 0 && this.batchSelected.length < this.galleryItems.length;
|
// console.info("batchSelected.length="+this.batchSelected.length)
|
// console.info("galleryItems.length="+this.galleryItems.length)
|
},
|
// 全选/取消全选
|
toggleSelectAll() {
|
const allSelected = this.selectAll;
|
this.batchSelected = [];
|
|
this.galleryItems.forEach((_, index) => {
|
this.$set(this.galleryItems[index], 'selected', allSelected);
|
if (allSelected) {
|
this.batchSelected.push(index);
|
}
|
});
|
this.isIndeterminate = this.batchSelected.length > 0 && this.batchSelected.length < this.galleryItems.length;
|
},
|
|
// 确认批量操作
|
confirmBatch() {
|
this.openBatchLabelDialog()
|
},
|
// 打开模型训练弹窗
|
openTrainDialog() {
|
// 此处应调用API获取实际的样本数量
|
// 这里使用示例数据
|
this.positiveCount = this.galleryItems.filter(item => item.status === 1).length;
|
this.negativeCount = this.galleryItems.filter(item => item.status === 2).length;
|
this.trainDialogVisible = true;
|
},
|
// 开始训练
|
async startTraining() {
|
try {
|
// this.$loading({ text: '模型训练中...' });
|
// 调用实际的训练API
|
// await startModelTraining({
|
// positive: this.positiveCount,
|
// negative: this.negativeCount
|
// });
|
|
// 模拟API延迟
|
// await new Promise(resolve => setTimeout(resolve, 2000));
|
|
// this.$message.success('模型训练已开始');
|
this.trainDialogVisible = false;
|
this.$message.error(`功能暂未实现`);
|
} catch (error) {
|
this.$message.error(`训练失败: ${error.message}`);
|
} finally {
|
this.$loading().close();
|
}
|
},
|
// 打开批量标注弹窗
|
openBatchLabelDialog() {
|
if (this.batchSelected.length === 0) {
|
this.$message.warning('请先选择图片进行标注');
|
return;
|
}
|
this.batchLabelStatus = 0; // 重置为不确定状态
|
this.batchLabelDialogVisible = true;
|
},
|
// 确认批量标注
|
async confirmBatchLabeling() {
|
if (this.batchSelected.length === 0) {
|
this.$message.warning('请先选择图片');
|
return;
|
}
|
|
try {
|
// this.$loading({ text: '批量标注中...' });
|
|
const selectedItems = this.galleryItems.filter(item => item.selected);
|
let ids = []
|
for (let i = 0; i < selectedItems.length; i++) {
|
ids.push(selectedItems[i].trainId)
|
}
|
|
// 调用批量更新状态的API
|
let rspc = await batchUpdateTrainStatus({
|
ids: ids,
|
status: this.batchLabelStatus
|
})
|
if (rspc && rspc.status === 200) {
|
this.$message.success(`已成功标注${selectedItems.length}个数据`);
|
this.batchLabelDialogVisible = false;
|
this.exitBatchMode(); // 退出批量模式
|
this.fetchTableData(); // 刷新数据
|
} else {
|
this.$message.error(`标注失败: ${rspc.msg}`);
|
}
|
} catch (error) {
|
this.$message.error(`标注失败: ${error.message}`);
|
} finally {
|
this.$loading().close();
|
}
|
},
|
// 批量删除
|
async handleBatchDelete() {
|
if (this.batchSelected.length === 0) {
|
this.$message.warning('请先选择图片');
|
return;
|
}
|
|
this.$confirm(`确定要删除选中的${this.batchSelected.length}个数据吗?`, '警告', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}).then(async () => {
|
try {
|
// this.$loading({ text: '删除中...' });
|
|
// 调用批量删除API
|
let rspc = await batchDeleteTrains({
|
ids: ids
|
})
|
if (rspc && rspc.status === 200) {
|
this.$message.success(`已成功删除${this.batchSelected.length}个数据`);
|
this.batchLabelDialogVisible = false;
|
this.exitBatchMode(); // 退出批量模式
|
this.fetchTableData(); // 刷新数据
|
} else {
|
this.$message.error(`删除失败: ${rspc.msg}`);
|
}
|
} catch (error) {
|
this.$message.error(`删除失败: ${error.message}`);
|
} finally {
|
this.$loading().close();
|
}
|
}).catch(() => {
|
// 用户取消删除
|
});
|
},
|
}
|
}
|
</script>
|
|
<style scoped>
|
.image-gallery {
|
background-color: #ffffff;
|
padding: 20px;
|
border-radius: 4px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif;
|
}
|
|
/* 筛选区域样式 */
|
.filter-section {
|
/* padding: 20px; */
|
/* background-color: #f5f7fa; */
|
border-radius: 4px;
|
margin-bottom: 20px;
|
}
|
|
.filter-form {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: center;
|
}
|
|
.action-buttons {
|
display: flex;
|
margin-left: auto;
|
gap: 10px;
|
margin-bottom: 20px;
|
}
|
|
.action-btn {
|
padding: 10px 20px;
|
}
|
|
::v-deep .el-form-item {
|
margin-bottom: 18px;
|
margin-right: 20px;
|
}
|
|
::v-deep .el-form-item__label {
|
font-weight: bold;
|
color: #606266;
|
}
|
|
/* 图片展示区域 */
|
.gallery-section {
|
margin-top: 20px;
|
}
|
|
.image-card {
|
margin-bottom: 20px;
|
border-radius: 4px;
|
overflow: hidden;
|
background-color: #f9fbfd;
|
border: 1px solid #e6ebf5;
|
transition: all 0.3s ease;
|
}
|
|
.batch-controls {
|
display: flex;
|
align-items: center;
|
background: #fff;
|
/* border-bottom: 1px solid #e6ebf5; */
|
padding: 5px;
|
margin-bottom: 20px;
|
margin-top: -10px;
|
/* border-radius: 4px; */
|
/* box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); */
|
|
}
|
|
.select-all {
|
margin-right: 15px;
|
}
|
|
.batch-actions {
|
margin-left: 100px;
|
display: flex;
|
gap: 10px;
|
}
|
|
/* 标题左对齐 */
|
::v-deep .el-dialog__header {
|
text-align: left;
|
}
|
|
::v-deep .el-dialog__title {
|
font-weight: bold;
|
font-size: 16px;
|
}
|
|
::v-deep .el-dialog__body {
|
padding-top: 20px;
|
padding-bottom: 15px;
|
}
|
|
/* 模型训练弹窗样式 */
|
.dialog1 {
|
.info-label {
|
font-weight: bold;
|
font-size: 20px;
|
margin-bottom: 15px;
|
color: black;
|
text-align: left;
|
}
|
|
.sample-info {
|
display: flex;
|
justify-content: space-between;
|
/* padding: 0 10px; */
|
margin-bottom: 20px;
|
}
|
|
.sample-count {
|
/* font-weight: 600; */
|
font-size: 15px;
|
color: #333;
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: flex-end;
|
padding: 10px 0;
|
}
|
}
|
|
/* 批量惭怍弹窗样式 */
|
.dialog2 {
|
/* 批量标注弹窗内容 */
|
.label-options {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.label-option {
|
width: 100%;
|
padding: 15px 20px;
|
margin: 8px 0;
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
cursor: pointer;
|
transition: all 0.3s;
|
text-align: center;
|
box-sizing: border-box;
|
}
|
|
.label-option:hover {
|
border-color: #409eff;
|
color: #409eff;
|
}
|
|
.label-option.active {
|
border-color: #409eff;
|
background-color: #ecf5ff;
|
color: #409eff;
|
}
|
|
/* 弹窗底部按钮 */
|
.dialog-footer {
|
/* display: flex; */
|
text-align: center;
|
/* justify-content: space-between; */
|
/* padding: 10px 20px; */
|
/* border-top: 1px solid #e6ebf5; */
|
}
|
}
|
</style>
|