| | |
| | | <template> |
| | | <div class="image-gallery"> |
| | | <!-- 添加导入组件 --> |
| | | <BatchImport ref="batchImport" :show-type-selector="false" @import="handleImportFiles" /> |
| | | <!-- 模型训练弹窗 --> |
| | | <el-dialog title="模型训练" :visible.sync="trainDialogVisible" width="372px" top="10vh"> |
| | | <div class="sample-info"> |
| | | <div class="info-label">样本信息</div> |
| | | <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">开始训练</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 批量标注弹窗 --> |
| | | <el-dialog 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-button type="primary" class="action-btn"> |
| | | <i class="el-icon-download"></i> 导入 |
| | | </el-button> |
| | | <el-dropdown-menu slot="dropdown"> |
| | | <el-dropdown-item>正样本</el-dropdown-item> |
| | | <el-dropdown-item>负样本</el-dropdown-item> |
| | | <el-dropdown-item>待标记样本</el-dropdown-item> |
| | | <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" class="action-btn"> |
| | | <el-button type="primary" @click="openTrainDialog" class="action-btn"> |
| | | 模型训练 |
| | | </el-button> |
| | | </div> |
| | |
| | | <!-- 批量操作控制栏 --> |
| | | <div class="batch-controls" v-if="isBatchMode"> |
| | | <div class="select-all"> |
| | | <el-checkbox v-model="selectAll" @change="toggleSelectAll">全选</el-checkbox> |
| | | <el-checkbox :indeterminate="isIndeterminate" v-model="selectAll" |
| | | @change="toggleSelectAll">全选</el-checkbox> |
| | | </div> |
| | | <!-- <div>已选择 {{ selectedCount }} 个项目</div> --> |
| | | <div class="batch-actions"> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import BatchImport from './batchImport'; |
| | | import imageCard from './imageCard'; |
| | | import Pagination from '@/components/rightPagination'; |
| | | import { getTrains, updateTrainStatus, deleteTrains } from "@/api/modelTuning"; |
| | | 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: null, |
| | | trainId: 0, |
| | | totalCount: 0, // 总数据量 |
| | | currentPage: 1, // 当前页码 |
| | | pageSize: 12, // 每页数量 |
| | |
| | | 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 |
| | |
| | | async changeTrainId(trainId) { |
| | | // console.info(trainId) |
| | | this.trainId = trainId |
| | | this.isBatchMode = false |
| | | await this.fetchTableData() |
| | | }, |
| | | // 获取表格数据方法 |
| | | async fetchTableData(params) { |
| | | console.info(this.currentPage) |
| | | // console.info(this.currentPage) |
| | | this.galleryItems = [] |
| | | // params.tagId = this.trainId |
| | | let rspc = await getTrains({ |
| | |
| | | pageSize: this.pageSize, |
| | | }); |
| | | if (rspc && rspc.status === 200) { |
| | | this.galleryItems = rspc.data.list; |
| | | 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 = res.data.pagination.totalPage |
| | | const totalPage = rspc.data.pagination.totalPage |
| | | const currentPage = this.currentPage > totalPage |
| | | ? totalPage |
| | | : res.data.pagination.page |
| | | : rspc.data.pagination.page |
| | | this.currentPage = currentPage, |
| | | this.totalPage = totalPage |
| | | }, |
| | |
| | | }, |
| | | //修改图片状态 |
| | | async handleStatusChange(parm) { |
| | | console.log('修改状态', 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({ |
| | |
| | | }, |
| | | // 进入批量模式 |
| | | enterBatchMode() { |
| | | // this.isBatchMode = true; |
| | | // this.selectAll = false; |
| | | // this.batchSelected = []; |
| | | |
| | | // // 清除已选状态 |
| | | // this.galleryItems.forEach(item => { |
| | | // item.selected = false; |
| | | // }); |
| | | }, |
| | | |
| | | // 退出批量模式 |
| | | exitBatchMode() { |
| | | this.isBatchMode = false; |
| | | this.isBatchMode = true; |
| | | this.selectAll = false; |
| | | this.isIndeterminate = false; |
| | | this.batchSelected = []; |
| | | |
| | | // 清除已选状态 |
| | |
| | | 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 position = this.batchSelected.indexOf(index); |
| | | if (position === -1) { |
| | | const isCurrentlySelected = this.galleryItems[index].selected; |
| | | |
| | | // 更新选中状态 |
| | | this.$set(this.galleryItems[index], 'selected', !isCurrentlySelected); |
| | | |
| | | // 更新batchSelected数组 |
| | | if (!isCurrentlySelected) { |
| | | // 添加选择 |
| | | this.batchSelected.push(index); |
| | | } else { |
| | | this.batchSelected.splice(position, 1); |
| | | // 移除选择 |
| | | const position = this.batchSelected.indexOf(index); |
| | | if (position !== -1) { |
| | | this.batchSelected.splice(position, 1); |
| | | } |
| | | } |
| | | this.checkAllSelected(); |
| | | }, |
| | | checkAllSelected() { |
| | | this.isAllSelected = this.batchSelected.length === this.galleryItems.length; |
| | | 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() { |
| | | this.galleryItems.forEach(item => { |
| | | item.selected = this.selectAll; |
| | | 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() { |
| | | const selectedItems = this.galleryItems.filter(item => item.selected); |
| | | this.openBatchLabelDialog() |
| | | }, |
| | | // 打开模型训练弹窗 |
| | | openTrainDialog() { |
| | | // 此处应调用API获取实际的样本数量 |
| | | // 这里使用示例数据 |
| | | this.positiveCount = 100; |
| | | this.negativeCount = 10; |
| | | // this.trainDialogVisible = true; |
| | | }, |
| | | // 开始训练 |
| | | async startTraining() { |
| | | try { |
| | | this.$loading({ text: '模型训练中...' }); |
| | | // 调用实际的训练API |
| | | // await startModelTraining({ |
| | | // positive: this.positiveCount, |
| | | // negative: this.negativeCount |
| | | // }); |
| | | |
| | | this.$message({ |
| | | message: `已对${selectedItems.length}个项执行批量操作`, |
| | | type: 'success' |
| | | // 模拟API延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 2000)); |
| | | |
| | | this.$message.success('模型训练已开始'); |
| | | this.trainDialogVisible = false; |
| | | } 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(() => { |
| | | // 用户取消删除 |
| | | }); |
| | | |
| | | this.exitBatchMode(); |
| | | }, |
| | | } |
| | | } |
| | |
| | | |
| | | /* 筛选区域样式 */ |
| | | .filter-section { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | /* padding: 20px; */ |
| | | /* background-color: #f5f7fa; */ |
| | | border-radius: 4px; |
| | | margin-bottom: 20px; |
| | | } |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | background: #fff; |
| | | border-bottom: 1px solid #e6ebf5; |
| | | padding: 10px 20px; |
| | | /* border-bottom: 1px solid #e6ebf5; */ |
| | | padding: 5px; |
| | | margin-bottom: 20px; |
| | | border-radius: 4px; |
| | | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); |
| | | margin-top: -10px; |
| | | /* border-radius: 4px; */ |
| | | /* box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); */ |
| | | |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | |
| | | /* 模型训练弹窗内容 */ |
| | | .sample-info { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .info-label { |
| | | font-weight: bold; |
| | | margin-bottom: 12px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .sample-count { |
| | | padding: 8px 0; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | /* 批量标注弹窗内容 */ |
| | | .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> |