| | |
| | | <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-date-picker style="width: 256px;" v-model="filter.timeRange" type="daterange" |
| | | value-format="yyyy-MM-dd" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /> |
| | | value-format="yyyy-MM-dd" range-separator="至" start-placeholder="开始日期" |
| | | end-placeholder="结束日期" /> |
| | | </el-form-item> |
| | | |
| | | <!-- 分类 --> |
| | |
| | | <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"> |
| | |
| | | @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 class="image-grid" ref="imageGrid"> |
| | | <div v-for="(item, index) in galleryItems" :key="index" class="image-card-wrapper" |
| | | :style="{ width: cardWidth}"> |
| | | <image-card :item="item" :is-batch-mode="isBatchMode" @delete-details="handleDeleteDetails" |
| | | @status-change="handleStatusChange" @card-click="handleCardClick1" |
| | | @toggle-select="toggleSelect(index)" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 分页组件 --> |
| | | <Pagination :total="totalCount" :current-page.sync="currentPage" :page-size.sync="pageSize" |
| | | @pagination-change="fetchTableData" /> |
| | | @pagination-change="paginationChange" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import _ from 'lodash'; // 用于防抖 |
| | | import BatchImport from './batchImport'; |
| | | import imageCard from './imageCard'; |
| | | import Pagination from '@/components/rightPagination'; |
| | | import { getTrainTags, getTrains, updateTrainStatus, deleteTrains } from "@/api/modelTuning"; |
| | | import { getTrains, updateTrainStatus, deleteTrains, batchUpdateTrainStatus, batchDeleteTrains, uploadDataTrainTags } from "@/api/modelTuning"; |
| | | export default { |
| | | components: { |
| | | imageCard, |
| | | Pagination, |
| | | BatchImport |
| | | |
| | | }, |
| | | name: 'ImageGallery', |
| | | props: { |
| | | trainId: { |
| | | type: Number |
| | | }, |
| | | }, |
| | | data() { |
| | | return { |
| | | cardWidth: 'calc(25% - 20px)', // 默认4列布局 |
| | | minCardWidth: 300, // 卡片最小宽度 |
| | | margin: 20, // 卡片间距 |
| | | trainDialogVisible: false, // 模型训练弹窗可见性 |
| | | batchLabelDialogVisible: false, // 批量标注弹窗可见性 |
| | | // 模型训练弹窗数据 |
| | | positiveCount: 0, // 正样本数量 |
| | | negativeCount: 0, // 负样本数量 |
| | | // 批量标注状态 |
| | | batchLabelStatus: 0, // 默认选择"不确定" |
| | | isIndeterminate: false, |
| | | selectAll: false, |
| | | pagination: {}, |
| | | trainId: 0, |
| | | totalCount: 0, // 总数据量 |
| | | currentPage: 1, // 当前页码 |
| | | pageSize: 10, // 每页数量 |
| | | pageSize: 15, // 每页数量 |
| | | tableData: [], // 表格数据 |
| | | // 是否批量模式 |
| | | isBatchMode: false, |
| | |
| | | // 筛选条件 |
| | | filter: { |
| | | cameraName: '', |
| | | timeRange: ['2025-06-22', '2025-06-28'], |
| | | category: 'all' |
| | | timeRange: ['', ''], |
| | | category: -1 |
| | | }, |
| | | |
| | | // 分类选项 |
| | | categories: [ |
| | | { label: '全部', value: 'all' }, |
| | | { label: '正确', value: 'correct' }, |
| | | { label: '错误', value: 'incorrect' }, |
| | | { label: '不确定', value: 'unknown' } |
| | | { label: '全部', value: -1 }, |
| | | { label: '正确', value: 1 }, |
| | | { label: '错误', value: 2 }, |
| | | { label: '不确定', value: 0 } |
| | | ], |
| | | |
| | | // 图片数据 |
| | | galleryItems: [ |
| | | { |
| | | id: 1, |
| | | image: "@/assets/img/样本图.png", |
| | | date: '2025-07-01 10:00:09', |
| | | camera: '大门口检测摄像头', |
| | | status: 'correct' |
| | | }, |
| | | { |
| | | id: 2, |
| | | image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%23f0f7ff'/%3E%3Crect x='50' y='30' width='200' height='140' rx='5' ry='5' fill='%23d5e8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Cpath d='M100,60 L200,60 M100,80 L200,80 M100,100 L200,100 M100,120 L200,120 M100,140 L200,140' stroke='%23409EFF' stroke-width='1'/%3E%3Cpath d='M80,80 L120,120 M80,120 L120,80' stroke='%23FF4D4F' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E", |
| | | date: '2025-07-01 10:00:09', |
| | | camera: '大门口检测摄像头', |
| | | status: 'incorrect' |
| | | }, |
| | | { |
| | | id: 3, |
| | | image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%23f0f7ff'/%3E%3Crect x='50' y='30' width='200' height='140' rx='5' ry='5' fill='%23d5e8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Cellipse cx='150' cy='100' rx='70' ry='40' stroke='%23409EFF' stroke-width='1' fill='none'/%3E%3Ccircle cx='130' cy='80' r='5' fill='%23409EFF'/%3E%3Ccircle cx='170' cy='80' r='5' fill='%23409EFF'/%3E%3Cpath d='M150,100 Q160,120 140,120' stroke='%23409EFF' stroke-width='1' fill='none'/%3E%3C/svg%3E", |
| | | date: '2025-07-01 10:00:09', |
| | | camera: '大门口检测摄像头', |
| | | status: 'correct' |
| | | }, |
| | | { |
| | | id: 4, |
| | | image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%23f0f7ff'/%3E%3Crect x='50' y='30' width='200' height='140' rx='5' ry='5' fill='%23d5e8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Cpath d='M80,80 L220,80 M150,50 L150,110' stroke='%23409EFF' stroke-width='1'/%3E%3Ccircle cx='150' cy='80' r='40' stroke='%23409EFF' stroke-width='1' fill='none'/%3E%3C/svg%3E", |
| | | date: '2025-07-01 10:00:09', |
| | | camera: '大门口检测摄像头', |
| | | status: 'unknown' |
| | | }, |
| | | { |
| | | id: 5, |
| | | image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%23f0f7ff'/%3E%3Crect x='50' y='30' width='200' height='140' rx='5' ry='5' fill='%23d5e8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Cpath d='M100,60 L200,60 M100,80 L200,80 M100,100 L200,100 M100,120 L200,120' stroke='%23409EFF' stroke-width='1'/%3E%3Cpath d='M120,100 A20,20 0 1,1 180,100 A20,20 0 1,1 120,100' stroke='%23409EFF' stroke-width='1' fill='none'/%3E%3C/svg%3E", |
| | | date: '2025-07-01 10:00:09', |
| | | camera: '大门口检测摄像头', |
| | | status: 'correct' |
| | | }, |
| | | { |
| | | id: 6, |
| | | image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%23f0f7ff'/%3E%3Crect x='50' y='30' width='200' height='140' rx='5' ry='5' fill='%23d5e8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Crect x='70' y='60' width='60' height='40' rx='3' ry='3' fill='%23a0c8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Crect x='170' y='60' width='60' height='40' rx='3' ry='3' fill='%23a0c8ff' stroke='%23409EFF' stroke-width='1'/%3E%3Crect x='120' y='120' width='60' height='20' rx='2' ry='2' fill='%23a0c8ff' stroke='%23409EFF' stroke-width='1'/%3E%3C/svg%3E", |
| | | date: '2025-07-01 10:00:09', |
| | | camera: '大门口检测摄像头', |
| | | status: 'incorrect' |
| | | } |
| | | ] |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.fastFetchTableData() |
| | | this.calculateCardWidth(); |
| | | // 添加防抖的resize监听 |
| | | window.addEventListener('resize', _.debounce(this.calculateCardWidth, 100)); |
| | | }, |
| | | beforeDestroy() { |
| | | window.removeEventListener('resize', this.calculateCardWidth); |
| | | }, |
| | | methods: { |
| | | async fastFetchTableData() { |
| | | let rspc = await getTrainTags(); |
| | | if (rspc && rspc.status === 200) { |
| | | this.fetchTableData({ |
| | | tagId: rspc.data.list[0].id, |
| | | page: this.currentPage, |
| | | limit: this.pageSize |
| | | }) |
| | | // 新增卡片宽度计算方法 |
| | | calculateCardWidth() { |
| | | if (this.$refs.imageGrid) { |
| | | const containerWidth = this.$refs.imageGrid.clientWidth; |
| | | // 计算每行可以放置的卡片数量 |
| | | const cardsPerRow = Math.floor(containerWidth / (this.minCardWidth + this.margin)); |
| | | const n = Math.max(3, cardsPerRow); // 至少1列 |
| | | // 设置卡片宽度公式 |
| | | this.cardWidth = `calc(${100 / n}% - ${this.margin}px)`; |
| | | } |
| | | }, |
| | | // 打开导入对话框 |
| | | 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) { |
| | | // yourApi.getData(params).then(res => { |
| | | // this.tableData = res.data.list |
| | | // this.totalCount = res.data.total |
| | | // }) |
| | | // console.info(this.currentPage) |
| | | this.galleryItems = [] |
| | | let rspc = await getTrains(params); |
| | | // 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) { |
| | | 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 = 20 |
| | | 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) { |
| | |
| | | type: 'success', |
| | | message: '成功' |
| | | }); |
| | | this.fetchSelectData() |
| | | // 删除成功后自动修正页码 |
| | | if (this.galleryItems && this.galleryItems.length === 1 && this.currentPage > 1) { |
| | | this.currentPage -= 1 |
| | | } |
| | | this.fetchTableData() |
| | | } else { |
| | | this.$message({ |
| | | type: 'error', |
| | |
| | | }, |
| | | //修改图片状态 |
| | | async handleStatusChange(parm) { |
| | | console.log('修改状态', parm); |
| | | // console.log('修改状态', parm); |
| | | let rspc = await updateTrainStatus(parm); |
| | | if (rspc && rspc.status === 200) { |
| | | this.$message({ |
| | | type: 'success', |
| | | message: '成功' |
| | | }); |
| | | this.fetchTableData({ |
| | | tagid: this.trainId, |
| | | page: this.currentPage, |
| | | limit: this.pageSize |
| | | }) |
| | | |
| | | // 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', |
| | |
| | | // 处理搜索 |
| | | handleSearch() { |
| | | // console.log('执行搜索:', this.filter); |
| | | // console.log('trainId:', this.trainId); |
| | | // 这里可以添加实际的搜索逻辑 |
| | | this.fetchTableData({ tagId: this.trainId }) |
| | | // console.log('filter:', this.filter.cameraName); |
| | | this.currentPage = 1, // 当前页码 |
| | | // 这里可以添加实际的搜索逻辑 |
| | | this.fetchTableData() |
| | | }, |
| | | |
| | | // 重置筛选条件 |
| | |
| | | this.filter = { |
| | | cameraName: '', |
| | | timeRange: ['', ''], |
| | | category: 'all' |
| | | category: -1 |
| | | }; |
| | | console.log('已重置筛选条件'); |
| | | }, |
| | | // 进入批量模式 |
| | | 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 = 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 |
| | | // }); |
| | | |
| | | this.$message({ |
| | | message: `已对${selectedItems.length}个项执行批量操作`, |
| | | type: 'success' |
| | | // 模拟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(() => { |
| | | // 用户取消删除 |
| | | }); |
| | | |
| | | this.exitBatchMode(); |
| | | }, |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* 新增图片网格布局样式 */ |
| | | .image-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | margin: -10px; |
| | | min-width: 1000px; |
| | | /* 负边距抵消包裹元素的边距 */ |
| | | /* width: 100%; */ |
| | | } |
| | | |
| | | .image-card-wrapper { |
| | | margin: 10px; |
| | | /* 设置卡片间距 */ |
| | | box-sizing: border-box; |
| | | /* transition: width 0.3s ease; */ |
| | | /* 添加平滑过渡效果 */ |
| | | } |
| | | |
| | | .image-gallery { |
| | | min-width: 1265px; |
| | | background-color: #ffffff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | |
| | | |
| | | /* 筛选区域样式 */ |
| | | .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; |
| | | } |
| | | |
| | | /* 模型训练弹窗样式 */ |
| | | .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> |