sd
2025-08-05 72b025d6b43271ae88541ea23c92070b3b2acc96
模型训练-批量批注、批量删除以及批量导入功能实现
1个文件已添加
3个文件已修改
771 ■■■■■ 已修改文件
src/api/modelTuning.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/modelTuning/components/batchImport.vue 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/modelTuning/components/imageCard.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/modelTuning/components/rightCardList.vue 403 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/modelTuning.ts
@@ -50,3 +50,24 @@
    params: query
  });
};
export const batchUpdateTrainStatus = (params: any) => {
  return request({
    url: "/api-v1/v1/train/batchUpdate ",
    method: "put",
    data: params
  });
};
export const batchDeleteTrains = (query: any) => {
  return request({
    url: "/api-v1/v1/train/batchDelete",
    method: "delete",
    params: query
  });
};
export const uploadDataTrainTags = (params: any) => {
  return request({
    url: "/api-v1/v1/train/uploadData",
    method: "post",
    data: params
  });
};
src/pages/modelTuning/components/batchImport.vue
New file
@@ -0,0 +1,308 @@
<template>
  <el-dialog :title="`导入${importLabel}`" :visible.sync="visible" width="600px">
    <div class="batch-import-container">
      <!-- 导入类型选择 -->
      <el-form label-width="100px" v-if="showTypeSelector">
        <el-form-item label="样本类型">
          <el-radio-group v-model="importType">
            <el-radio :label="1">正样本</el-radio>
            <el-radio :label="2">负样本</el-radio>
            <el-radio :label="3">待标记样本</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <!-- 上传区域 -->
      <div class="upload-area" @dragover.prevent @drop="onDrop">
        <div class="upload-content">
          <i class="el-icon-upload"></i>
          <div class="upload-text">
            <p>将图片拖放到此处,或</p>
            <el-button type="primary">选择图片</el-button>
          </div>
          <p class="upload-hint">支持JPG/PNG格式,最多100张图片,单张最大5MB</p>
        </div>
        <input
          type="file"
          ref="fileInput"
          class="file-input"
          multiple
          accept="image/*"
          @change="handleFileChange"
        >
      </div>
      <!-- 图片预览区 -->
      <div class="image-preview">
        <div class="preview-header">
          <span>已选择图片 ({{ fileList.length }})</span>
          <el-button
            type="text"
            :disabled="fileList.length === 0"
            @click="fileList = []"
          >
            清空
          </el-button>
        </div>
        <div class="preview-content">
          <div
            class="preview-item"
            v-for="(file, index) in fileList"
            :key="index"
          >
            <img :src="getPreviewUrl(file)" alt="预览图">
            <div class="image-info">
              <span class="file-name">{{ file.name }}</span>
              <span class="file-size">{{ formatSize(file.size) }}</span>
            </div>
            <i
              class="el-icon-delete"
              title="删除"
              @click="removeFile(index)"
            ></i>
          </div>
          <div class="empty-hint" v-if="fileList.length === 0">
            暂未选择图片
          </div>
        </div>
      </div>
    </div>
    <div slot="footer" class="dialog-footer">
      <el-button @click="visible = false">取 消</el-button>
      <el-button
        type="primary"
        :disabled="fileList.length === 0"
        @click="submitImport"
      >
        导 入
      </el-button>
    </div>
  </el-dialog>
</template>
<script>
export default {
  name: 'BatchImport',
  props: {
    // 是否显示类型选择器(用于区分导入按钮和下拉导入)
    showTypeSelector: {
      type: Boolean,
      default: true
    },
    // 预设的导入类型(1-正样本、2-负样本、3-待标记)
    presetType: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      visible: false,
      importType: this.presetType || 1,
      fileList: []
    };
  },
  computed: {
    importLabel() {
      const labels = {1: '正样本', 2: '负样本', 3: '待标记样本'};
      return this.presetType ? labels[this.presetType] : labels[this.importType];
    }
  },
  methods: {
    open() {
      this.visible = true;
      this.fileList = [];
      if (!this.presetType) this.importType = 1;
    },
    close() {
      this.visible = false;
    },
    onDrop(e) {
      e.preventDefault();
      this.addFiles(e.dataTransfer.files);
    },
    handleFileChange(e) {
      this.addFiles(e.target.files);
      // 重置input以便可以再次选择相同文件
      e.target.value = null;
    },
    addFiles(fileList) {
      const files = Array.from(fileList);
      // 过滤非图片文件
      const validFiles = files.filter(file => file.type.startsWith('image/'));
      // 限制文件数量 (最多100张)
      if (this.fileList.length + validFiles.length > 100) {
        this.$message.warning('最多支持100张图片导入');
        return;
      }
      validFiles.forEach(file => {
        // 检查文件大小 (5MB以内)
        if (file.size > 5 * 1024 * 1024) {
          this.$message.warning(`文件 ${file.name} 超过5MB限制`);
          return;
        }
        this.fileList.push(file);
      });
    },
    getPreviewUrl(file) {
      return URL.createObjectURL(file);
    },
    formatSize(size) {
      if (size < 1024) return size + ' B';
      if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
      return (size / (1024 * 1024)).toFixed(1) + ' MB';
    },
    removeFile(index) {
      this.fileList.splice(index, 1);
    },
    submitImport() {
      const finalImportType = this.presetType || this.importType;
      const fileNames = this.fileList.map(f => f.name);
      this.$emit('import', {
        type: finalImportType,
        files: this.fileList
      });
    //   this.$message.success(`成功导入 ${fileNames.length} 张图片`);
      this.close();
    }
  }
};
</script>
<style scoped>
.batch-import-container {
  padding: 10px;
}
.upload-area {
  position: relative;
  border: 2px dashed #dcdfe6;
  border-radius: 6px;
  padding: 20px;
  text-align: center;
  margin-bottom: 20px;
  cursor: pointer;
  transition: border-color 0.3s;
}
.upload-area:hover {
  border-color: #409EFF;
}
.upload-content {
  pointer-events: none;
}
.upload-content i {
  font-size: 48px;
  color: #c0c4cc;
  margin-bottom: 15px;
}
.upload-text {
  margin-bottom: 10px;
}
.upload-hint {
  color: #909399;
  font-size: 12px;
  margin-top: 10px;
}
.file-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.image-preview {
  border: 1px solid #ebeef5;
  border-radius: 4px;
  max-height: 300px;
  overflow-y: auto;
}
.preview-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 15px;
  background-color: #f5f7fa;
  border-bottom: 1px solid #ebeef5;
}
.preview-content {
  padding: 10px;
}
.preview-item {
  position: relative;
  display: flex;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #f1f1f1;
}
.preview-item:hover {
  background-color: #f8f8f8;
}
.preview-item img {
  width: 50px;
  height: 50px;
  object-fit: cover;
  border-radius: 4px;
  margin-right: 10px;
}
.image-info {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.file-name {
  font-size: 14px;
  margin-bottom: 5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 380px;
}
.file-size {
  color: #909399;
  font-size: 12px;
}
.preview-item .el-icon-delete {
  color: #f56c6c;
  font-size: 16px;
  cursor: pointer;
}
.empty-hint {
  text-align: center;
  padding: 30px;
  color: #909399;
}
</style>
src/pages/modelTuning/components/imageCard.vue
@@ -2,13 +2,14 @@
    <el-col :xs="24" :sm="12" :md="8" :lg="6" class="image-card-container">
        <div class="image-card" :class="{ 'batch-selected': isBatchMode && selected }" @click="handleCardClick">
            <!-- 多选框 (仅在批量模式下显示) -->
            <div class="checkbox-wrapper" v-if="isBatchMode">
                <el-checkbox v-model="localSelected" @click.stop @change="emitSelection" />
            </div>
            <!-- <div class="checkbox-wrapper" v-if="isBatchMode">
            </div> -->
            <el-checkbox class="checkbox-wrapper" v-if="isBatchMode" v-model="localSelected" @click.stop
                @change="emitSelection" />
            <!-- 图片容器 -->
            <div class="image-container">
                <!-- <img :src="item.imagePath" class="gallery-image" alt="监控截图" /> -->
                <el-image :src="item.imagePath" class="gallery-image" fit="scale-down"/>
                <el-image :src="item.imagePath" class="gallery-image" fit="scale-down" />
            </div>
            <!-- 卡片操作按钮 -->
@@ -65,8 +66,18 @@
        }
    },
    watch: {
        selected(newVal) {
            this.localSelected = newVal;
        selected: {
            immediate: true,
            handler(newVal) {
                this.localSelected = newVal;
            }
        },
        // 添加对item.selected的深度监听
        'item.selected': {
            immediate: true,
            handler(newVal) {
                this.localSelected = newVal;
            }
        }
    },
    methods: {
@@ -82,19 +93,21 @@
        handleCardClick() {
            // 批量模式下点击卡片触发选择
            if (this.isBatchMode) {
                this.localSelected = !this.localSelected;
                this.emitSelection();
            } else {
                this.$emit('card-click', this.item);
            }
        },
        emitSelection() {
            // console.info(this.selected)
            this.localSelected = !this.localSelected;
            this.$emit('toggle-select');
        },
        // 更改状态
        changeStatus(status) {
            this.$emit('status-change', { trainId: this.item.trainId, status });
            if (!this.isBatchMode) {
                this.$emit('status-change', { trainId: this.item.trainId, status });
            }
        },
        // 删除
@@ -248,10 +261,14 @@
}
.checkbox-wrapper {
    width: 18px;
    /* height: 18px; */
    color: #FFFFFF;
    position: absolute;
    top: 10px;
    left: 20px;
    /* top: 10px;
    left: 20px; */
    margin-top: 10px;
    margin-left: 10px;
    z-index: 10;
}
</style>
src/pages/modelTuning/components/rightCardList.vue
@@ -1,5 +1,39 @@
<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">
@@ -38,13 +72,13 @@
                        <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>
@@ -54,7 +88,8 @@
        <!-- 批量操作控制栏 -->
        <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">
@@ -79,20 +114,31 @@
</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,     // 每页数量
@@ -123,6 +169,67 @@
    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
@@ -131,11 +238,12 @@
        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({
@@ -144,15 +252,20 @@
                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
        },
@@ -190,13 +303,18 @@
        },
        //修改图片状态
        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({
@@ -230,20 +348,9 @@
        },
        // 进入批量模式
        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 = [];
            // 清除已选状态
@@ -251,37 +358,174 @@
                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();
        },
    }
}
@@ -298,8 +542,8 @@
/* 筛选区域样式 */
.filter-section {
    padding: 20px;
    background-color: #f5f7fa;
    /* padding: 20px; */
    /* background-color: #f5f7fa; */
    border-radius: 4px;
    margin-bottom: 20px;
}
@@ -349,11 +593,12 @@
    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); */
}
@@ -366,4 +611,74 @@
    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>