bug修改
This commit is contained in:
parent
58ace303d2
commit
2ff53cae29
|
|
@ -75,3 +75,12 @@ export function getSampleImageListAPI(params) {
|
|||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 数据管理->样本库管理->获取样本图片列表
|
||||
export function getSampleListByVersionIdAPI(params) {
|
||||
return request({
|
||||
url: '/smartPlatform/data/sample/getSampleListByVersionId',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -447,6 +447,7 @@ export default {
|
|||
},
|
||||
|
||||
// 核心:提交审核结果,适配分类标注的审批参数
|
||||
// 核心:提交审核结果,适配分类标注的审批参数
|
||||
async submitAuditResult(auditStatus) {
|
||||
if (this.isAuditing) return
|
||||
if (!this.currentImage.id) {
|
||||
|
|
@ -474,9 +475,17 @@ export default {
|
|||
|
||||
if (res.code === 200) {
|
||||
this.$message.success(auditStatus === 1 ? '审核通过提交成功!' : '审核不通过提交成功!')
|
||||
|
||||
// ===== 新增核心代码开始 =====
|
||||
await this.loadAuditImages(); // 刷新审批图片列表(等待加载完成)
|
||||
// 刷新后重置索引:取原索引和新列表最大索引的最小值,防止越界
|
||||
this.currentImageIndex = Math.min(this.currentImageIndex, this.imageList.length - 1);
|
||||
// 若刷新后还有图片,重新赋值当前图片并重置视图;无图片则置空
|
||||
if (this.imageList.length === 0) {
|
||||
this.currentImage = {};
|
||||
this.$message.info('暂无剩余审批图片数据');
|
||||
}
|
||||
// 自动切换到下一张(如果有),提升审批效率
|
||||
if (this.currentImageIndex < this.imageList.length - 1) {
|
||||
if (this.imageList.length > 0 && this.currentImageIndex < this.imageList.length - 1) {
|
||||
this.nextImage()
|
||||
}
|
||||
} else {
|
||||
|
|
@ -498,7 +507,7 @@ export default {
|
|||
// 全局容器:适配审批页面的布局
|
||||
.labeling-page-container {
|
||||
width: 100%;
|
||||
height: 92vh;
|
||||
height: 90.5vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
labelingTaskId: this.convertToNumber(decryptWithSM4(this.$route.query.labelingTaskId || '')),
|
||||
labelingTemplate: decryptWithSM4(this.$route.query.labelingTemplate || ''),
|
||||
// 表格配置
|
||||
formLabel,
|
||||
columnsList,
|
||||
|
|
@ -107,12 +108,29 @@ export default {
|
|||
// 查看按钮点击事件(支持选中行/批量查看)
|
||||
handleView() {
|
||||
const labelingTaskIdStr = String(this.labelingTaskId)
|
||||
this.$router.push({
|
||||
name: 'LabelingDetail',
|
||||
query: {
|
||||
labelingTaskId : encryptWithSM4(labelingTaskIdStr)
|
||||
}
|
||||
});
|
||||
console.log('123123123131',this.labelingTemplate)
|
||||
if (this.labelingTemplate === '1' || this.labelingTemplate === '2') {
|
||||
this.$router.push({
|
||||
name: 'ClassificationDetail',
|
||||
query: {
|
||||
labelingTaskId: encryptWithSM4(labelingTaskIdStr),
|
||||
}
|
||||
});
|
||||
} else if (this.labelingTemplate === '3' || this.labelingTemplate === '4' || this.labelingTemplate === '5') {
|
||||
this.$router.push({
|
||||
name: 'LabelingDetail',
|
||||
query: {
|
||||
labelingTaskId: encryptWithSM4(labelingTaskIdStr),
|
||||
}
|
||||
});
|
||||
} else if (this.labelingTemplate === '8' || this.labelingTemplate === '9') {
|
||||
this.$router.push({
|
||||
name: 'OcrLabelingDetail',
|
||||
query: {
|
||||
labelingTaskId: encryptWithSM4(labelingTaskIdStr),
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,12 +139,13 @@ export default {
|
|||
methods: {
|
||||
// 进度列点击跳转处理
|
||||
handleProgressClick(data) {
|
||||
console.log('data',data)
|
||||
const labelingTaskIdStr = String(data.labelingTaskId)
|
||||
this.$router.push({
|
||||
name: 'auditList',
|
||||
query: {
|
||||
labelingTaskId : encryptWithSM4(labelingTaskIdStr)
|
||||
|
||||
labelingTaskId : encryptWithSM4(labelingTaskIdStr),
|
||||
labelingTemplate: encryptWithSM4(data.labelingTemplateValue)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
width="1200px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
class="data-select-dialog"
|
||||
>
|
||||
<div class="data-select-container">
|
||||
<!-- 左侧三层样本树 -->
|
||||
|
|
@ -738,6 +739,7 @@ export default {
|
|||
this.fullScreenImageName = item.fileName || item.label || '样本图片';
|
||||
this.fullScreenImageVisible = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.paddingRight = '17px';
|
||||
},
|
||||
|
||||
/** 关闭全屏预览 */
|
||||
|
|
@ -746,6 +748,7 @@ export default {
|
|||
this.fullScreenImageUrl = "";
|
||||
this.fullScreenImageName = "";
|
||||
document.body.style.overflow = 'auto';
|
||||
document.body.style.paddingRight = '0';
|
||||
},
|
||||
|
||||
/** 页码大小变化 */
|
||||
|
|
@ -799,174 +802,227 @@ export default {
|
|||
.data-select-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden; /* 兜底:禁用根容器滚动 */
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
// 关键:穿透控制el-dialog的所有默认滚动,全局唯一不会污染其他dialog
|
||||
::v-deep .data-select-dialog {
|
||||
.el-dialog__body {
|
||||
padding: 15px 20px !important; /* 微调内边距,减少高度浪费 */
|
||||
height: 700px !important; /* 固定dialog内容区高度,包含标题/内容/按钮 */
|
||||
overflow: hidden !important; /* 彻底禁用dialog默认的内容区滚动,核心修复 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* 主容器 - 核心:移除外层滚动,弹性占满dialog内容区 */
|
||||
.data-select-container {
|
||||
display: flex;
|
||||
height: 600px;
|
||||
gap: 20px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 0; /* 修复1:删掉主容器底部内边距,避免间距放大双横线 */
|
||||
overflow: hidden; /* 禁用自身滚动 */
|
||||
flex: 1; /* 弹性占满dialog body的剩余高度,抵消footer和内边距 */
|
||||
height: 100%; /* 兜底高度 */
|
||||
}
|
||||
|
||||
.sample-tree-panel {
|
||||
width: 250px;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
// 左侧树面板 - 独立滚动,高度100%
|
||||
.sample-tree-panel {
|
||||
width: 250px;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
height: 100%; /* 占满主容器高度 */
|
||||
display: flex;
|
||||
flex-direction: column; /* 弹性列布局,标题固定+树滚动 */
|
||||
overflow: hidden; /* 禁用面板自身滚动 */
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0 0 10px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.tree-empty-tip {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
::v-deep .el-tree {
|
||||
height: calc(100% - 30px);
|
||||
overflow-y: auto;
|
||||
.tree-tooltip {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
// 禁用节点样式(关键:覆盖Element UI默认样式)
|
||||
.el-tree-node.is-disabled {
|
||||
color: #ccc !important;
|
||||
.el-checkbox {
|
||||
pointer-events: none;
|
||||
.el-checkbox__inner {
|
||||
background-color: #f5f7fa !important;
|
||||
border-color: #e4e7ed !important;
|
||||
color: #c0c4cc !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
.el-checkbox__label {
|
||||
color: #ccc !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 确保禁用节点的复选框不可点击
|
||||
.el-tree-node.is-disabled .el-checkbox {
|
||||
display: inline-block !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0 0 10px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
flex-shrink: 0; /* 标题不收缩,固定高度 */
|
||||
}
|
||||
|
||||
.sample-image-panel {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 10px;
|
||||
.tree-empty-tip {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
flex: 1; /* 占满剩余高度,居中显示 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.select-header {
|
||||
::v-deep .el-tree {
|
||||
flex: 1; /* 弹性占满剩余高度 */
|
||||
height: 100%;
|
||||
overflow-y: auto; /* 仅树组件自身滚动,唯一滚动点 */
|
||||
overflow-x: hidden; /* 禁用横向滚动 */
|
||||
.tree-tooltip {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
// 禁用节点样式
|
||||
.el-tree-node.is-disabled {
|
||||
color: #ccc !important;
|
||||
.el-checkbox {
|
||||
pointer-events: none;
|
||||
.el-checkbox__inner {
|
||||
background-color: #f5f7fa !important;
|
||||
border-color: #e4e7ed !important;
|
||||
color: #c0c4cc !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
.el-checkbox__label {
|
||||
color: #ccc !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-tree-node.is-disabled .el-checkbox {
|
||||
display: inline-block !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧图片面板 - 独立滚动,高度100%
|
||||
.sample-image-panel {
|
||||
flex: 1; /* 占满剩余宽度 */
|
||||
height: 100%; /* 占满主容器高度 */
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 0; /* 修复2:删掉面板底部内边距,避免冗余间距 */
|
||||
display: flex;
|
||||
flex-direction: column; /* 弹性列布局:头部+图片滚动+分页固定 */
|
||||
overflow: hidden; /* 禁用面板自身滚动,核心 */
|
||||
|
||||
.select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
flex-shrink: 0; /* 头部固定,不收缩 */
|
||||
}
|
||||
|
||||
.select-count {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.image-empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
flex: 1; /* 占满剩余高度,居中 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
min-height: 100px;
|
||||
flex: 1; /* 弹性占满头部和分页之间的所有高度 */
|
||||
overflow-y: auto; /* 仅图片网格自身滚动,唯一滚动点 */
|
||||
overflow-x: hidden; /* 禁用横向滚动 */
|
||||
padding-right: 6px; /* 为滚动条预留空间,避免内容遮挡 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: calc(33.33% - 10px);
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 280px;
|
||||
|
||||
&:hover {
|
||||
border-color: #1f72ea;
|
||||
box-shadow: 0 2px 8px rgba(31, 114, 234, 0.1);
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
flex: 1;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
.sample-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.image-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
height: 30px;
|
||||
|
||||
.select-count {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
.image-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.image-empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
min-height: 100px;
|
||||
|
||||
.image-item {
|
||||
width: calc(33.33% - 10px);
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
.image-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 280px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
border-color: #1f72ea;
|
||||
box-shadow: 0 2px 8px rgba(31, 114, 234, 0.1);
|
||||
}
|
||||
.preview-btn {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #1f72ea;
|
||||
|
||||
.image-wrapper {
|
||||
flex: 1;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
.sample-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.image-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
height: 30px;
|
||||
|
||||
.image-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.preview-btn {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #1f72ea;
|
||||
|
||||
&:hover {
|
||||
color: #0e5bc8;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: #0e5bc8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页组件固定在面板底部,不滚动
|
||||
::v-deep .el-pagination {
|
||||
flex-shrink: 0; /* 分页固定,不收缩 */
|
||||
margin-top: 15px !important;
|
||||
text-align: right;
|
||||
// 移除分页自身的任何边框,避免冗余
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗底部按钮区 - 核心:只保留这一条顶部边框
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
padding-top: 10px;
|
||||
flex-shrink: 0; /* 按钮区固定,不收缩 */
|
||||
border-top: 1px solid #ebeef5; /* 唯一保留的分隔线 */
|
||||
margin-top: 10px; /* 微调间距,让分隔线和分页的距离更美观 */
|
||||
border-bottom: none !important; /* 兜底:确保无底部边框 */
|
||||
}
|
||||
|
||||
/* 全屏预览样式 */
|
||||
|
|
@ -983,6 +1039,7 @@ export default {
|
|||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; /* 禁用预览弹窗滚动 */
|
||||
}
|
||||
|
||||
.full-screen-image-content {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@
|
|||
type="text"
|
||||
v-hasPermi="['data:dataset:version:add']"
|
||||
class="action-btn version-add-btn"
|
||||
@click="handleVersionAdd(data)"
|
||||
@click="handleExport(data)"
|
||||
>
|
||||
新增版本
|
||||
导出
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
|
|
@ -60,6 +60,7 @@ import TableModel from '@/components/TableModel2'
|
|||
import { columnsList, formLabel } from './config'
|
||||
import { datasetVersionListAPI, delDatasetVersionDataAPI } from '@/api/data/dataset'
|
||||
import { decryptWithSM4, encryptWithSM4 } from '@/utils/sm'
|
||||
import {exportImagesAPI} from "@/api/data/labeling";
|
||||
|
||||
export default {
|
||||
name: 'DatasetVersionList', // 修正组件名,避免冲突
|
||||
|
|
@ -181,8 +182,41 @@ export default {
|
|||
this.queryVersionList()
|
||||
},
|
||||
|
||||
handleExport(row) {
|
||||
this.$message.info(`正在导出数据集:${row.datasetName}`)
|
||||
/** 导出操作 */
|
||||
async handleExport(row) {
|
||||
const datasetVersionId = row.datasetVersionId
|
||||
// 根据选择的格式处理导出
|
||||
try {
|
||||
let blob, fileName;
|
||||
blob = await exportImagesAPI({ datasetVersionId });
|
||||
fileName = `数据集图片.zip`;
|
||||
// 处理文件下载
|
||||
this.downloadFile(blob, fileName);
|
||||
this.$message.success('导出成功,正在下载...');
|
||||
} catch (error) {
|
||||
// 异常处理(兼容blob格式的错误信息)
|
||||
console.error('导出失败:', error);
|
||||
if (error.response?.data instanceof Blob) {
|
||||
const text = await new Response(error.response.data).text();
|
||||
const res = JSON.parse(text);
|
||||
this.$message.error(res.msg || '导出失败,无可用数据');
|
||||
} else {
|
||||
this.$message.error(error.msg || '导出失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
downloadFile(blob, fileName) {
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(new Blob([blob]));
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
// 释放URL对象,清理临时标签
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
},
|
||||
|
||||
handleQuery() {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,11 @@ export default {
|
|||
watch: {
|
||||
isAdd: {
|
||||
handler(newVal) {
|
||||
if (newVal === CONSTANT_PARAMS.type) {
|
||||
console.log('newVal',newVal)
|
||||
console.log('123',CONSTANT_PARAMS.type)
|
||||
|
||||
if (newVal === CONSTANT_PARAMS.type) {
|
||||
console.log('1231')
|
||||
this.initFormData();
|
||||
}else{
|
||||
this.form.labelGroupName = this.rowData.labelGroupName
|
||||
|
|
@ -75,6 +79,7 @@ export default {
|
|||
methods: {
|
||||
/** 初始化表单数据 */
|
||||
initFormData() {
|
||||
console.log('初始化表单数据:', this.rowData);
|
||||
if (this.rowData) {
|
||||
// 编辑模式:填充表单数据
|
||||
this.form = {
|
||||
|
|
@ -229,4 +234,4 @@ export default {
|
|||
::v-deep .el-dialog__footer {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
<script>
|
||||
import TableModel from '@/components/TableModel2'
|
||||
import { columnsList, formLabel } from './config'
|
||||
import { labelListAPI, delLabelAPI } from '@/api/data/label'
|
||||
import { labelListAPI, delDataAPI } from '@/api/data/label'
|
||||
import LabelForm from './LabelForm.vue'
|
||||
|
||||
export default {
|
||||
|
|
@ -101,8 +101,9 @@ export default {
|
|||
/** 修改操作 */
|
||||
handleUpdate(row) {
|
||||
this.rowData = {
|
||||
labelGroupName: this.activeCategoryName,
|
||||
...row
|
||||
...row,
|
||||
labelGroupName: this.activeCategoryName
|
||||
|
||||
}
|
||||
this.title = '编辑标签'
|
||||
this.isAdd = 'edit'
|
||||
|
|
@ -126,7 +127,7 @@ export default {
|
|||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'delete-confirm-dialog'
|
||||
}).then(() => {
|
||||
delLabelAPI(
|
||||
delDataAPI(
|
||||
{
|
||||
labelId: raw.labelId
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
<!-- 保留缩放/全屏/重置 -->
|
||||
<el-button icon="el-icon-zoom-in" @click="zoomIn" title="放大"></el-button>
|
||||
<el-button icon="el-icon-zoom-out" @click="zoomOut" title="缩小"></el-button>
|
||||
<el-button icon="el-icon-full-screen" @click="toggleFullScreen" title="页面内全屏" :class="{ active: isFullScreen }"></el-button>
|
||||
<el-button icon="el-icon-refresh" @click="resetZoom" title="重置视图"></el-button>
|
||||
<!-- <el-button icon="el-icon-full-screen" @click="toggleFullScreen" title="页面内全屏" :class="{ active: isFullScreen }"></el-button>-->
|
||||
<!-- <el-button icon="el-icon-refresh" @click="resetZoom" title="重置视图"></el-button>-->
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
|
|
@ -460,16 +460,16 @@ export default {
|
|||
// 切换图片:核心改造,清空所有残留数据,移除标注相关
|
||||
async switchImage(index) {
|
||||
// 检查是否有未保存的分类
|
||||
if (this.selectedLabelIds.length > 0 || this.isInvalidData) {
|
||||
try {
|
||||
await this.saveLabel() // 静默保存当前分类
|
||||
} catch (err) {
|
||||
const confirm = await this.$confirm('当前图片分类未保存,是否放弃修改并切换?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
if (!confirm) return
|
||||
}
|
||||
}
|
||||
// if (this.selectedLabelIds.length > 0 || this.isInvalidData) {
|
||||
// try {
|
||||
// await this.saveLabel() // 静默保存当前分类
|
||||
// } catch (err) {
|
||||
// const confirm = await this.$confirm('当前图片分类未保存,是否放弃修改并切换?', '提示', {
|
||||
// type: 'warning'
|
||||
// })
|
||||
// if (!confirm) return
|
||||
// }
|
||||
// }
|
||||
|
||||
// 切换图片并**清空所有残留数据**
|
||||
this.currentImageIndex = index
|
||||
|
|
@ -536,6 +536,16 @@ export default {
|
|||
if (res.code === 200) {
|
||||
this.$message.success('分类保存成功!');
|
||||
this.saveCurrentLabelData(); // 更新本地数据
|
||||
// ===== 新增核心代码开始 =====
|
||||
await this.loadTreeData(); // 刷新图片列表(等待加载完成)
|
||||
// 刷新后恢复当前索引,防止越界(比如图片被移走后列表变短)
|
||||
this.currentImageIndex = Math.min(this.currentImageIndex, this.imageList.length - 1);
|
||||
// 重新赋值当前图片
|
||||
this.currentImage = this.imageList[this.currentImageIndex] || {};
|
||||
// 恢复当前图片的选中标签和无效标记,保证页面显示一致
|
||||
this.selectedLabelIds = this.currentImage.selectedLabelIds || [];
|
||||
this.isInvalidData = this.currentImage.isInvalid || false;
|
||||
// ===== 新增核心代码结束 =====
|
||||
return Promise.resolve(res);
|
||||
} else {
|
||||
const errorMsg = '保存失败:' + (res.msg || '接口返回异常');
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@
|
|||
<div
|
||||
class="label-item"
|
||||
v-for="(label, labelIndex) in group.labels"
|
||||
:key="label.id || labelIndex"
|
||||
:key="label.labelId || labelIndex"
|
||||
>
|
||||
<span class="label-name">#{{ labelIndex + 1 }} {{ label.name }} </span>
|
||||
<span class="label-name">#{{ labelIndex + 1 }} {{ label.labelName }} </span>
|
||||
<span class="shape-count" v-if="label.shapeIds && label.shapeIds.length">
|
||||
({{ label.shapeIds.length }}个标注)
|
||||
</span>
|
||||
|
|
@ -109,6 +109,23 @@ export default {
|
|||
name: 'LabelingDetail',
|
||||
data() {
|
||||
return {
|
||||
// 标签绘制配置:复用原标注页的核心配置
|
||||
labelDrawConfig: {
|
||||
baseLineWidth: 3,
|
||||
minLineWidth: 1,
|
||||
maxLineWidth: 6,
|
||||
textFontSize: 16,
|
||||
textPadding: 3,
|
||||
shapeMainColor: {
|
||||
rect: '#00ff00', // 矩形填充主色
|
||||
polygon: '#0099ff', // 多边形填充主色
|
||||
circle: '#ff9900' // 圆形填充主色
|
||||
},
|
||||
selectedColor: '#ff0000', // 选中时所有形状的填充/描边主色
|
||||
bgOpacity: 0.4, // 文字背景透明度
|
||||
shapeFillOpacity: 0.4, // 图形自身填充透明度
|
||||
textColor: "#ffffff"
|
||||
},
|
||||
shapeColorMap: {
|
||||
rect: '#00ff00', // 矩形 - 绿色
|
||||
polygon: '#0099ff', // 多边形 - 蓝色
|
||||
|
|
@ -157,6 +174,58 @@ export default {
|
|||
return isNaN(num) ? '' : num;
|
||||
},
|
||||
|
||||
// 【核心工具方法1】16进制颜色转RGBA(支持#fff/#ffffff格式,添加透明度)
|
||||
hexToRgba(hex, opacity) {
|
||||
const cleanHex = hex.replace('#', '');
|
||||
const fullHex = cleanHex.length === 3
|
||||
? cleanHex.split('').map(c => c + c).join('')
|
||||
: cleanHex;
|
||||
const r = parseInt(fullHex.substring(0, 2), 16);
|
||||
const g = parseInt(fullHex.substring(2, 4), 16);
|
||||
const b = parseInt(fullHex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
},
|
||||
|
||||
// 【核心工具方法2】判断16进制颜色是否为浅色(基于亮度公式,亮度>0.5为浅色)
|
||||
isLightColor(hex) {
|
||||
const cleanHex = hex.replace('#', '');
|
||||
const fullHex = cleanHex.length === 3
|
||||
? cleanHex.split('').map(c => c + c).join('')
|
||||
: cleanHex;
|
||||
const r = parseInt(fullHex.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(fullHex.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(fullHex.substring(4, 6), 16) / 255;
|
||||
// 国际标准相对亮度公式,判断颜色深浅最准确
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b);
|
||||
return luminance > 0.5;
|
||||
},
|
||||
|
||||
// 【核心工具方法3】动态计算线条宽度(适配缩放)
|
||||
getDynamicLineWidth() {
|
||||
const { baseLineWidth, minLineWidth, maxLineWidth } = this.labelDrawConfig
|
||||
// 缩放比例越小,线条越粗(反比例),并限制最大/最小值
|
||||
const dynamicWidth = baseLineWidth / this.zoomScale
|
||||
return Math.max(minLineWidth, Math.min(maxLineWidth, dynamicWidth))
|
||||
},
|
||||
|
||||
// 【核心工具方法4】动态计算标签文字字号(适配缩放)
|
||||
getDynamicFontSize() {
|
||||
const { textFontSize } = this.labelDrawConfig
|
||||
// 文字字号反比例适配,保证缩放后文字大小视觉一致
|
||||
const dynamicSize = textFontSize / this.zoomScale
|
||||
return Math.max(10, Math.min(24, dynamicSize)) // 限制字号范围,避免过大/过小
|
||||
},
|
||||
|
||||
// 【工具方法】根据标签ID查找标签详情
|
||||
findLabelById(labelId) {
|
||||
for (const group of this.labelGroups) {
|
||||
for (const label of group.labels) {
|
||||
if (label.labelId === labelId) return label
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// 加载标签树数据(仅展示)
|
||||
async loadLabelTreeData() {
|
||||
try {
|
||||
|
|
@ -226,7 +295,7 @@ export default {
|
|||
}
|
||||
|
||||
// 生成标注ID
|
||||
const shapeId = `shape_${item.labelingSampleId}_${item.labelId}_${Date.now()}`
|
||||
const shapeId = `shape_${item.labelingSampleId}_${item.id || Date.now()}`
|
||||
|
||||
// 构造标注形状数据
|
||||
const shape = {
|
||||
|
|
@ -357,7 +426,8 @@ export default {
|
|||
} else if (window.AILabel) {
|
||||
instance = window.AILabel({ el: container, image: image })
|
||||
} else {
|
||||
instance = this.createSimpleLabelTool(container, image)
|
||||
// 自研工具传入当前实例,用于调用工具方法
|
||||
instance = this.createSimpleLabelTool(container, image, this)
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
|
|
@ -383,21 +453,21 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// 自研简易标注工具(仅展示,移除所有编辑功能)
|
||||
createSimpleLabelTool(container, image) {
|
||||
// 自研简易标注工具(仅展示,集成标签名称绘制逻辑)
|
||||
createSimpleLabelTool(container, image, parentInstance) {
|
||||
const tool = {
|
||||
el: container,
|
||||
image,
|
||||
shapes: [],
|
||||
selectedShape: null,
|
||||
mainCanvas: null,
|
||||
tempCanvas: null,
|
||||
mainCtx: null,
|
||||
tempCtx: null,
|
||||
zoomScale: 1.0,
|
||||
// 挂载父组件实例,用于调用工具方法和获取配置
|
||||
parent: parentInstance,
|
||||
|
||||
init() {
|
||||
// 主画布(仅展示标注)
|
||||
// 主画布(仅展示标注+标签文字)
|
||||
const mainCanvas = document.createElement('canvas')
|
||||
mainCanvas.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10;width:100%;height:100%;'
|
||||
mainCanvas.width = container.offsetWidth
|
||||
|
|
@ -422,15 +492,6 @@ export default {
|
|||
this.redrawMainCanvas()
|
||||
},
|
||||
|
||||
// 绘制临时形状(空实现,移除编辑功能)
|
||||
drawTempShape() {},
|
||||
|
||||
// 取消绘制(空实现)
|
||||
cancelDrawing() {},
|
||||
|
||||
// 设置绘制类型(空实现)
|
||||
setDrawType() {},
|
||||
|
||||
// 设置缩放
|
||||
setScale(scale) {
|
||||
this.zoomScale = scale
|
||||
|
|
@ -470,12 +531,12 @@ export default {
|
|||
return [...this.shapes]
|
||||
},
|
||||
|
||||
// 重绘画布(仅展示)
|
||||
// 【核心修改】重绘画布(展示标注+居中绘制标签名称)
|
||||
redrawMainCanvas() {
|
||||
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height)
|
||||
this.mainCtx.save()
|
||||
|
||||
// 居中逻辑
|
||||
// 画布居中+缩放逻辑(原有保留)
|
||||
this.mainCtx.translate(
|
||||
this.el.offsetWidth / 2,
|
||||
this.el.offsetHeight / 2
|
||||
|
|
@ -486,7 +547,7 @@ export default {
|
|||
-this.image.naturalWidth / 2,
|
||||
-this.image.naturalHeight / 2
|
||||
)
|
||||
// 绘制图片
|
||||
// 绘制底图
|
||||
this.mainCtx.drawImage(
|
||||
this.image,
|
||||
0, 0,
|
||||
|
|
@ -495,24 +556,34 @@ export default {
|
|||
)
|
||||
}
|
||||
|
||||
// 绘制所有标注(仅展示)
|
||||
// 获取动态配置(适配缩放)
|
||||
const dynamicLineWidth = this.parent.getDynamicLineWidth()
|
||||
const dynamicFontSize = this.parent.getDynamicFontSize()
|
||||
const { textPadding, bgOpacity, shapeFillOpacity, shapeMainColor } = this.parent.labelDrawConfig
|
||||
|
||||
// 绘制所有标注+标签文字
|
||||
this.shapes.forEach(s => {
|
||||
// 1. 获取标注基础信息
|
||||
const shapeColor = s.color || shapeMainColor[s.type] || '#000000'
|
||||
const labelId = s.labelId || ''
|
||||
const label = this.parent.findLabelById(labelId)
|
||||
|
||||
// 2. 设置标注绘制样式
|
||||
this.mainCtx.lineWidth = dynamicLineWidth
|
||||
this.mainCtx.strokeStyle = shapeColor
|
||||
this.mainCtx.fillStyle = this.parent.hexToRgba(shapeColor, shapeFillOpacity)
|
||||
|
||||
// 3. 绘制标注图形(原有逻辑保留,样式统一)
|
||||
if (s.type === 'rect') {
|
||||
this.mainCtx.strokeStyle = s.color || '#00ff00'
|
||||
this.mainCtx.fillStyle = 'rgba(0,255,0,0.1)'
|
||||
this.mainCtx.strokeRect(s.x, s.y, s.width, s.height)
|
||||
this.mainCtx.fillRect(s.x, s.y, s.width, s.height)
|
||||
} else if (s.type === 'circle') {
|
||||
this.mainCtx.strokeStyle = s.color || '#ff9900'
|
||||
this.mainCtx.fillStyle = 'rgba(255,153,0,0.1)'
|
||||
this.mainCtx.beginPath()
|
||||
this.mainCtx.arc(s.x, s.y, s.radius, 0, Math.PI * 2)
|
||||
this.mainCtx.closePath()
|
||||
this.mainCtx.fill()
|
||||
this.mainCtx.stroke()
|
||||
} else if (s.type === 'polygon') {
|
||||
this.mainCtx.strokeStyle = s.color || '#0099ff'
|
||||
this.mainCtx.fillStyle = 'rgba(0,153,255,0.1)'
|
||||
this.mainCtx.beginPath()
|
||||
s.points.forEach((point, idx) => {
|
||||
idx === 0 ? this.mainCtx.moveTo(point.x, point.y) : this.mainCtx.lineTo(point.x, point.y)
|
||||
|
|
@ -521,15 +592,53 @@ export default {
|
|||
this.mainCtx.fill()
|
||||
this.mainCtx.stroke()
|
||||
}
|
||||
|
||||
// 4. 绘制标签名称(核心逻辑:无标签则不绘制)
|
||||
if (!label) return
|
||||
// 计算标注图形的正中心坐标(不同形状通用)
|
||||
let textX, textY
|
||||
if (s.type === 'rect') {
|
||||
textX = s.x + s.width / 2
|
||||
textY = s.y + s.height / 2
|
||||
} else if (s.type === 'circle') {
|
||||
textX = s.x
|
||||
textY = s.y
|
||||
} else if (s.type === 'polygon') {
|
||||
const totalX = s.points.reduce((sum, p) => sum + p.x, 0)
|
||||
const totalY = s.points.reduce((sum, p) => sum + p.y, 0)
|
||||
textX = totalX / s.points.length
|
||||
textY = totalY / s.points.length
|
||||
}
|
||||
|
||||
// 5. 设置文字基础样式
|
||||
this.mainCtx.font = `${dynamicFontSize}px Microsoft YaHei`
|
||||
this.mainCtx.textAlign = 'center' // 水平居中
|
||||
this.mainCtx.textBaseline = 'middle' // 垂直居中
|
||||
|
||||
// 6. 智能计算文字背景和文字颜色(适配不同标注颜色)
|
||||
const bgRgba = this.parent.hexToRgba(shapeColor, bgOpacity)
|
||||
const textColor = this.parent.isLightColor(shapeColor) ? '#000000' : '#ffffff'
|
||||
|
||||
// 7. 绘制文字背景框(适配文字宽度,居中展示)
|
||||
const textWidth = this.mainCtx.measureText(label.labelName).width
|
||||
const textHeight = dynamicFontSize
|
||||
this.mainCtx.fillStyle = bgRgba
|
||||
this.mainCtx.fillRect(
|
||||
textX - (textWidth + 2 * textPadding) / 2,
|
||||
textY - (textHeight + 2 * textPadding) / 2,
|
||||
textWidth + 2 * textPadding,
|
||||
textHeight + 2 * textPadding
|
||||
)
|
||||
|
||||
// 8. 绘制标签文字
|
||||
this.mainCtx.fillStyle = textColor
|
||||
this.mainCtx.fillText(label.labelName, textX, textY)
|
||||
})
|
||||
|
||||
this.mainCtx.restore()
|
||||
},
|
||||
|
||||
// 事件绑定(空实现)
|
||||
on() {},
|
||||
|
||||
// 形状数据操作(仅读取)
|
||||
// 形状数据操作(绑定labelId/color等信息)
|
||||
setShapeData(id, data) {
|
||||
const shape = this.shapes.find(s => s.id === id)
|
||||
if (shape) Object.assign(shape, data)
|
||||
|
|
@ -559,7 +668,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// 加载标注数据(仅展示)
|
||||
// 加载标注数据(仅展示,绑定labelId用于绘制标签)
|
||||
loadLabelData() {
|
||||
if (!this.ailabelInstance || !this.currentImage) return
|
||||
|
||||
|
|
@ -579,6 +688,7 @@ export default {
|
|||
shapes.forEach(shapeData => {
|
||||
try {
|
||||
let shapeId = ''
|
||||
// 根据标注类型绘制图形
|
||||
if (shapeData.type === 'rect' && shapeData.x !== undefined) {
|
||||
shapeId = this.ailabelInstance.drawRect?.(shapeData.x, shapeData.y, shapeData.width, shapeData.height) || shapeData.id
|
||||
} else if (shapeData.type === 'polygon' && shapeData.points && shapeData.points.length >= 3) {
|
||||
|
|
@ -587,10 +697,11 @@ export default {
|
|||
shapeId = this.ailabelInstance.drawCircle?.(shapeData.x, shapeData.y, shapeData.radius) || shapeData.id
|
||||
}
|
||||
|
||||
// 绑定标注样式和数据
|
||||
// 【核心】绑定标注的labelId和color(用于绘制标签名称)
|
||||
if (shapeId) {
|
||||
this.ailabelInstance.setShapeData?.(shapeId, {
|
||||
color: shapeData.color || this.shapeColorMap[shapeData.type]
|
||||
labelId: shapeData.labelId, // 绑定标签ID,用于查找标签名称
|
||||
color: shapeData.color || this.shapeColorMap[shapeData.type] // 绑定标注颜色
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -663,7 +774,6 @@ export default {
|
|||
handleShapeDeleted() {},
|
||||
selectLabel() {},
|
||||
findShapeIdsByLabelId() { return [] },
|
||||
findLabelById() { return null },
|
||||
removeLabelById() {},
|
||||
confirmAddLabel() {},
|
||||
deleteSelected() {},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<el-button plain size="mini" type="success" icon="el-icon-download" v-hasPermi="['system:user:import']"
|
||||
@click="handleModelExport">
|
||||
导入模板
|
||||
模板下载
|
||||
</el-button>
|
||||
|
||||
<el-button plain size="mini" type="warning" icon="el-icon-upload2" v-hasPermi="['system:user:import']"
|
||||
|
|
|
|||
Loading…
Reference in New Issue