| | |
| | | <!-- 添加导入组件 --> |
| | | <BatchImport ref="batchImport" :show-type-selector="false" @import="handleImportFiles" /> |
| | | <!-- 模型训练弹窗 --> |
| | | <el-dialog title="模型训练" :visible.sync="trainDialogVisible" width="372px" top="10vh"> |
| | | <el-dialog class="dialog1" title="模型训练" :visible.sync="trainDialogVisible" width="372px" top="10vh"> |
| | | <div class="info-label">样本信息</div> |
| | | <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> |
| | | <el-button type="primary" @click="startTraining"> |
| | | <i class="el-icon-time"></i> |
| | | 开始训练</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 批量标注弹窗 --> |
| | | <el-dialog title="批量标注" :visible.sync="batchLabelDialogVisible" width="472px" top="10vh"> |
| | | <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"> |
| | | 正确 |
| | |
| | | <!-- 选择时段 --> |
| | | <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> |
| | | |
| | | <!-- 分类 --> |
| | |
| | | @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" |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import _ from 'lodash'; // 用于防抖 |
| | | import BatchImport from './batchImport'; |
| | | import imageCard from './imageCard'; |
| | | import Pagination from '@/components/rightPagination'; |
| | |
| | | name: 'ImageGallery', |
| | | data() { |
| | | return { |
| | | cardWidth: 'calc(25% - 20px)', // 默认4列布局 |
| | | minCardWidth: 300, // 卡片最小宽度 |
| | | margin: 20, // 卡片间距 |
| | | trainDialogVisible: false, // 模型训练弹窗可见性 |
| | | batchLabelDialogVisible: false, // 批量标注弹窗可见性 |
| | | // 模型训练弹窗数据 |
| | |
| | | trainId: 0, |
| | | totalCount: 0, // 总数据量 |
| | | currentPage: 1, // 当前页码 |
| | | pageSize: 12, // 每页数量 |
| | | pageSize: 15, // 每页数量 |
| | | tableData: [], // 表格数据 |
| | | // 是否批量模式 |
| | | isBatchMode: false, |
| | |
| | | filter: { |
| | | cameraName: '', |
| | | timeRange: ['', ''], |
| | | category: 'all' |
| | | 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 } |
| | | ], |
| | | |
| | | // 图片数据 |
| | |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.calculateCardWidth(); |
| | | // 添加防抖的resize监听 |
| | | window.addEventListener('resize', _.debounce(this.calculateCardWidth, 100)); |
| | | }, |
| | | beforeDestroy() { |
| | | window.removeEventListener('resize', this.calculateCardWidth); |
| | | }, |
| | | methods: { |
| | | // 新增卡片宽度计算方法 |
| | | 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; |
| | |
| | | let formData = new FormData(); |
| | | // 2. 添加实际文件内容到FormData |
| | | files.forEach(item => { |
| | | formData.append('file', item); |
| | | formData.append('file', item); |
| | | }); |
| | | formData.append('tagId', this.trainId); |
| | | formData.append('status', type === 3 ? 0 : type); |
| | | |
| | | // console.log(formData) |
| | | |
| | | // console.log(formData) |
| | | // // 模拟上传请求(实际应调用API) |
| | | let rspc = await uploadDataTrainTags(formData) |
| | | if (rspc && rspc.status === 200) { |
| | |
| | | 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) { |
| | |
| | | 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 |
| | |
| | | // 处理搜索 |
| | | handleSearch() { |
| | | // console.log('执行搜索:', this.filter); |
| | | // console.log('trainId:', this.trainId); |
| | | // console.log('filter:', this.filter.cameraName); |
| | | this.currentPage = 1, // 当前页码 |
| | | // 这里可以添加实际的搜索逻辑 |
| | | this.fetchTableData({ tagId: this.trainId }) |
| | | this.fetchTableData() |
| | | }, |
| | | |
| | | // 重置筛选条件 |
| | |
| | | this.filter = { |
| | | cameraName: '', |
| | | timeRange: ['', ''], |
| | | category: 'all' |
| | | category: -1 |
| | | }; |
| | | console.log('已重置筛选条件'); |
| | | }, |
| | |
| | | openTrainDialog() { |
| | | // 此处应调用API获取实际的样本数量 |
| | | // 这里使用示例数据 |
| | | this.positiveCount = 100; |
| | | this.negativeCount = 10; |
| | | // this.trainDialogVisible = true; |
| | | 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: '模型训练中...' }); |
| | | // this.$loading({ text: '模型训练中...' }); |
| | | // 调用实际的训练API |
| | | // await startModelTraining({ |
| | | // positive: this.positiveCount, |
| | |
| | | // }); |
| | | |
| | | // 模拟API延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 2000)); |
| | | // await new Promise(resolve => setTimeout(resolve, 2000)); |
| | | |
| | | this.$message.success('模型训练已开始'); |
| | | // this.$message.success('模型训练已开始'); |
| | | this.trainDialogVisible = false; |
| | | this.$message.error(`功能暂未实现`); |
| | | } catch (error) { |
| | | this.$message.error(`训练失败: ${error.message}`); |
| | | } finally { |
| | |
| | | </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; |
| | |
| | | gap: 10px; |
| | | } |
| | | |
| | | /* 弹窗样式 */ |
| | | /* 标题左对齐 */ |
| | | ::v-deep .el-dialog__header { |
| | | text-align: left; |
| | |
| | | padding-bottom: 15px; |
| | | } |
| | | |
| | | /* 模型训练弹窗内容 */ |
| | | .sample-info { |
| | | padding: 20px; |
| | | /* 模型训练弹窗样式 */ |
| | | .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; |
| | | } |
| | | } |
| | | |
| | | .info-label { |
| | | font-weight: bold; |
| | | margin-bottom: 12px; |
| | | color: #606266; |
| | | } |
| | | /* 批量惭怍弹窗样式 */ |
| | | .dialog2 { |
| | | |
| | | .sample-count { |
| | | padding: 8px 0; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | /* 批量标注弹窗内容 */ |
| | | .label-options { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | /* 批量标注弹窗内容 */ |
| | | .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 { |
| | | 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:hover { |
| | | border-color: #409eff; |
| | | color: #409eff; |
| | | } |
| | | .label-option.active { |
| | | border-color: #409eff; |
| | | background-color: #ecf5ff; |
| | | 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; */ |
| | | /* 弹窗底部按钮 */ |
| | | .dialog-footer { |
| | | /* display: flex; */ |
| | | text-align: center; |
| | | /* justify-content: space-between; */ |
| | | /* padding: 10px 20px; */ |
| | | /* border-top: 1px solid #e6ebf5; */ |
| | | } |
| | | } |
| | | </style> |