|
|
|
@ -95,6 +95,8 @@
|
|
|
|
v-for="(annotation, index) in annotations"
|
|
|
|
v-for="(annotation, index) in annotations"
|
|
|
|
:key="annotation.id"
|
|
|
|
:key="annotation.id"
|
|
|
|
class="annotation-item"
|
|
|
|
class="annotation-item"
|
|
|
|
|
|
|
|
:class="{ 'selected': selectedAnnotationId === annotation.id }"
|
|
|
|
|
|
|
|
@click="selectAnnotationById(annotation, index)"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<div class="annotation-header">
|
|
|
|
<div class="annotation-header">
|
|
|
|
<el-select
|
|
|
|
<el-select
|
|
|
|
@ -102,6 +104,7 @@
|
|
|
|
placeholder="选择类型"
|
|
|
|
placeholder="选择类型"
|
|
|
|
size="small"
|
|
|
|
size="small"
|
|
|
|
@change="(value) => setAnnotationType(index, value)"
|
|
|
|
@change="(value) => setAnnotationType(index, value)"
|
|
|
|
|
|
|
|
@click.stop
|
|
|
|
class="type-select"
|
|
|
|
class="type-select"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<el-option
|
|
|
|
<el-option
|
|
|
|
@ -115,7 +118,7 @@
|
|
|
|
link
|
|
|
|
link
|
|
|
|
type="danger"
|
|
|
|
type="danger"
|
|
|
|
size="small"
|
|
|
|
size="small"
|
|
|
|
@click="deleteAnnotation(annotation, index)"
|
|
|
|
@click.stop="deleteAnnotation(annotation, index)"
|
|
|
|
class="delete-annotation"
|
|
|
|
class="delete-annotation"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<Icon icon="ep:close" />
|
|
|
|
<Icon icon="ep:close" />
|
|
|
|
@ -236,30 +239,83 @@
|
|
|
|
<el-aside class="right-container" width="320px">
|
|
|
|
<el-aside class="right-container" width="320px">
|
|
|
|
<el-collapse v-model="activeRightPanels" accordion class="custom-collapse">
|
|
|
|
<el-collapse v-model="activeRightPanels" accordion class="custom-collapse">
|
|
|
|
<!-- 图片选择面板 -->
|
|
|
|
<!-- 图片选择面板 -->
|
|
|
|
<el-collapse-item title="图片选择" name="images">
|
|
|
|
<el-collapse-item name="images">
|
|
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
|
|
<span class="panel-title">图片选择</span>
|
|
|
|
|
|
|
|
<el-progress
|
|
|
|
|
|
|
|
type="circle"
|
|
|
|
|
|
|
|
:percentage="annotationProgress"
|
|
|
|
|
|
|
|
:width="36"
|
|
|
|
|
|
|
|
:stroke-width="3"
|
|
|
|
|
|
|
|
:show-text="false"
|
|
|
|
|
|
|
|
class="title-progress"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<span class="progress-text-small">{{ annotationProgress }}%</span>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
<el-scrollbar class="image-scrollbar">
|
|
|
|
<el-scrollbar class="image-scrollbar">
|
|
|
|
<div class="image-grid">
|
|
|
|
<!-- 待标注图片 -->
|
|
|
|
<div
|
|
|
|
<div v-if="unannotatedImages.length > 0" class="image-section">
|
|
|
|
v-for="image in imageList"
|
|
|
|
<div class="section-label" @click="unannotatedCollapsed = !unannotatedCollapsed">
|
|
|
|
:key="image.id"
|
|
|
|
<span class="label-text">待标注 ({{ unannotatedImages.length }})</span>
|
|
|
|
class="image-item"
|
|
|
|
<Icon :icon="unannotatedCollapsed ? 'ep:caret-right' : 'ep:caret-bottom'" class="collapse-icon" />
|
|
|
|
:class="{ active: currentImage && currentImage.id === image.id }"
|
|
|
|
</div>
|
|
|
|
@click="selectImage(image)"
|
|
|
|
<div v-show="!unannotatedCollapsed" class="image-grid">
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
<div class="image-wrapper">
|
|
|
|
v-for="image in unannotatedImages"
|
|
|
|
<img
|
|
|
|
:key="image.id"
|
|
|
|
:src="image.path"
|
|
|
|
class="image-item unannotated"
|
|
|
|
:alt="image.name"
|
|
|
|
:class="{ active: currentImage && currentImage.id === image.id }"
|
|
|
|
class="thumbnail"
|
|
|
|
@click="selectImage(image)"
|
|
|
|
@error="handleImageError"
|
|
|
|
>
|
|
|
|
/>
|
|
|
|
<div class="image-wrapper">
|
|
|
|
<div v-if="image.status === 1" class="status-indicator completed">
|
|
|
|
<img
|
|
|
|
<Icon icon="ep:check" />
|
|
|
|
:src="image.path"
|
|
|
|
|
|
|
|
:alt="image.name"
|
|
|
|
|
|
|
|
class="thumbnail"
|
|
|
|
|
|
|
|
@error="handleImageError"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div class="status-indicator pending">
|
|
|
|
|
|
|
|
<Icon icon="ep:more" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="image-name">{{ image.name }}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="image-name">{{ image.name }}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 已标注图片 -->
|
|
|
|
|
|
|
|
<div v-if="annotatedImages.length > 0" class="image-section">
|
|
|
|
|
|
|
|
<div class="section-label" @click="annotatedCollapsed = !annotatedCollapsed">
|
|
|
|
|
|
|
|
<span class="label-text">已标注 ({{ annotatedImages.length }})</span>
|
|
|
|
|
|
|
|
<Icon :icon="annotatedCollapsed ? 'ep:caret-right' : 'ep:caret-bottom'" class="collapse-icon" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-show="!annotatedCollapsed" class="image-grid">
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
v-for="image in annotatedImages"
|
|
|
|
|
|
|
|
:key="image.id"
|
|
|
|
|
|
|
|
class="image-item annotated"
|
|
|
|
|
|
|
|
:class="{ active: currentImage && currentImage.id === image.id }"
|
|
|
|
|
|
|
|
@click="selectImage(image)"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div class="image-wrapper">
|
|
|
|
|
|
|
|
<img
|
|
|
|
|
|
|
|
:src="image.path"
|
|
|
|
|
|
|
|
:alt="image.name"
|
|
|
|
|
|
|
|
class="thumbnail"
|
|
|
|
|
|
|
|
@error="handleImageError"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div class="status-indicator completed">
|
|
|
|
|
|
|
|
<Icon icon="ep:check" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="image-name">{{ image.name }}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="imageList.length === 0" class="empty-images">
|
|
|
|
|
|
|
|
<el-empty description="暂无图片" :image-size="60" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</el-scrollbar>
|
|
|
|
</el-scrollbar>
|
|
|
|
</el-collapse-item>
|
|
|
|
</el-collapse-item>
|
|
|
|
</el-collapse>
|
|
|
|
</el-collapse>
|
|
|
|
@ -450,18 +506,32 @@ const initAnnotorious = async () => {
|
|
|
|
// 删除标注
|
|
|
|
// 删除标注
|
|
|
|
const deleteAnnotation = async (annotation: any, index: number) => {
|
|
|
|
const deleteAnnotation = async (annotation: any, index: number) => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 清除选中状态
|
|
|
|
|
|
|
|
selectedAnnotationId.value = null;
|
|
|
|
|
|
|
|
selectedAnnoId.value = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 从 Annotorious 中删除标注
|
|
|
|
// 从 Annotorious 中删除标注
|
|
|
|
if (annoInstance && annotation.annotationData) {
|
|
|
|
if (annoInstance) {
|
|
|
|
annoInstance.removeAnnotation(annotation.annotationData);
|
|
|
|
// 优先使用 annotationData.id
|
|
|
|
|
|
|
|
const annoId = annotation.annotationData?.id || annotation.id;
|
|
|
|
|
|
|
|
if (annoId) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
annoInstance.removeAnnotation(annoId);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.warn('从 Annotorious 删除标注失败:', e);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从本地标注列表中删除
|
|
|
|
// 从本地标注列表中删除(支持通过 id 或 annotationData.id 匹配)
|
|
|
|
annotations.value.splice(index, 1);
|
|
|
|
const idx = annotations.value.findIndex(a =>
|
|
|
|
|
|
|
|
a.id === annotation.id || (annotation.annotationData && a.annotationData?.id === annotation.annotationData.id)
|
|
|
|
// 如果需要,可以调用后端API删除标注
|
|
|
|
);
|
|
|
|
// await MarkApi.deleteMark(annotation.id);
|
|
|
|
if (idx !== -1) {
|
|
|
|
|
|
|
|
annotations.value.splice(idx, 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('删除成功');
|
|
|
|
console.log('删除成功, 剩余标注:', annotations.value.length);
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('删除标注失败:', error);
|
|
|
|
console.error('删除标注失败:', error);
|
|
|
|
alert('删除失败');
|
|
|
|
alert('删除失败');
|
|
|
|
@ -612,11 +682,30 @@ const imageList = ref<Image[]>([])
|
|
|
|
const annotationTypes = ref<AnnotationType[]>([])
|
|
|
|
const annotationTypes = ref<AnnotationType[]>([])
|
|
|
|
const annotations = ref<Annotation[]>([])
|
|
|
|
const annotations = ref<Annotation[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性:待标注和已标注的图片
|
|
|
|
|
|
|
|
const unannotatedImages = computed(() => imageList.value.filter(img => img.status !== 1))
|
|
|
|
|
|
|
|
const annotatedImages = computed(() => imageList.value.filter(img => img.status === 1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 图片选择区域折叠状态
|
|
|
|
|
|
|
|
const unannotatedCollapsed = ref(false)
|
|
|
|
|
|
|
|
const annotatedCollapsed = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 标注进度
|
|
|
|
|
|
|
|
const annotationProgress = computed(() => {
|
|
|
|
|
|
|
|
if (imageList.value.length === 0) return 0
|
|
|
|
|
|
|
|
return Math.round((imageList.value.filter(img => img.status === 1).length / imageList.value.length) * 100)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 分类模式相关数据
|
|
|
|
// 分类模式相关数据
|
|
|
|
const selectedClassificationType = ref<number | null>(null)
|
|
|
|
const selectedClassificationType = ref<number | null>(null)
|
|
|
|
const classificationAnnotations = ref<{ [key: number]: number }>({}) // 图片ID -> 类型ID的映射
|
|
|
|
const classificationAnnotations = ref<{ [key: number]: number }>({}) // 图片ID -> 类型ID的映射
|
|
|
|
const globalClassificationType = ref<number | null>(null) // 全局分类类型,切换图片时保持
|
|
|
|
const globalClassificationType = ref<number | null>(null) // 全局分类类型,切换图片时保持
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 标注选中状态
|
|
|
|
|
|
|
|
const selectedAnnotationId = ref<number | null>(null)
|
|
|
|
|
|
|
|
const selectedAnnoId = ref<string | null>(null) // 当前选中的 Annotorious 标注ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 添加类型对话框相关
|
|
|
|
// 添加类型对话框相关
|
|
|
|
const addTypeDialogVisible = ref(false)
|
|
|
|
const addTypeDialogVisible = ref(false)
|
|
|
|
@ -627,6 +716,7 @@ const newTypeForm = reactive({
|
|
|
|
})
|
|
|
|
})
|
|
|
|
const typeFormRef = ref()
|
|
|
|
const typeFormRef = ref()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 表单验证规则
|
|
|
|
// 表单验证规则
|
|
|
|
const typeFormRules = {
|
|
|
|
const typeFormRules = {
|
|
|
|
name: [
|
|
|
|
name: [
|
|
|
|
@ -720,6 +810,21 @@ const updateImageStatus = async (imageId: number, status: number) => {
|
|
|
|
console.error('更新图片状态失败:', error)
|
|
|
|
console.error('更新图片状态失败:', error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 检查并更新项目状态
|
|
|
|
|
|
|
|
const checkAndUpdateProjectStatus = async () => {
|
|
|
|
|
|
|
|
// 检查是否所有图片都已标注完成
|
|
|
|
|
|
|
|
const allAnnotated = imageList.value.every(img => img.status === 1)
|
|
|
|
|
|
|
|
if (allAnnotated && currentProject.value) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await MarkApi.updateProjectStatus({ id: currentProject.value.id, status: 2 })
|
|
|
|
|
|
|
|
console.log('项目已全部标注完成,状态已更新为2')
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('更新项目状态失败:', error)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取图片列表
|
|
|
|
// 获取图片列表
|
|
|
|
const getImageList = async (projectId: number) => {
|
|
|
|
const getImageList = async (projectId: number) => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
@ -798,6 +903,9 @@ const handleClassificationChange = async (typeId: number) => {
|
|
|
|
// 调用API更新图片状态
|
|
|
|
// 调用API更新图片状态
|
|
|
|
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
|
|
|
|
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 检查并更新项目状态
|
|
|
|
|
|
|
|
await checkAndUpdateProjectStatus();
|
|
|
|
|
|
|
|
|
|
|
|
console.log('分类标注保存成功');
|
|
|
|
console.log('分类标注保存成功');
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('保存分类标注失败:', error);
|
|
|
|
console.error('保存分类标注失败:', error);
|
|
|
|
@ -875,6 +983,8 @@ const selectImage = async (image: Image) => {
|
|
|
|
// 调用API更新图片状态
|
|
|
|
// 调用API更新图片状态
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
|
|
|
|
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
|
|
|
|
|
|
|
|
// 检查并更新项目状态
|
|
|
|
|
|
|
|
await checkAndUpdateProjectStatus();
|
|
|
|
} catch (updateError) {
|
|
|
|
} catch (updateError) {
|
|
|
|
console.error('更新图片状态失败:', updateError);
|
|
|
|
console.error('更新图片状态失败:', updateError);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -916,6 +1026,8 @@ const selectImage = async (image: Image) => {
|
|
|
|
imageInList.status = 1;
|
|
|
|
imageInList.status = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
|
|
|
|
await MarkApi.updateImageStatus({ id: currentImage.value.id, status: 1 });
|
|
|
|
|
|
|
|
// 检查并更新项目状态
|
|
|
|
|
|
|
|
await checkAndUpdateProjectStatus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('保存分类标注失败:', error);
|
|
|
|
console.error('保存分类标注失败:', error);
|
|
|
|
@ -941,6 +1053,8 @@ const selectImage = async (image: Image) => {
|
|
|
|
if (currentProject.value.type === '1') {
|
|
|
|
if (currentProject.value.type === '1') {
|
|
|
|
// 检测模式:初始化 annotorious 并加载标注
|
|
|
|
// 检测模式:初始化 annotorious 并加载标注
|
|
|
|
annotations.value = [];
|
|
|
|
annotations.value = [];
|
|
|
|
|
|
|
|
selectedAnnotationId.value = null; // 清除选中状态
|
|
|
|
|
|
|
|
selectedAnnoId.value = null; // 清除选中状态
|
|
|
|
await initAnnotorious();
|
|
|
|
await initAnnotorious();
|
|
|
|
} else if (currentProject.value.type === '2') {
|
|
|
|
} else if (currentProject.value.type === '2') {
|
|
|
|
// 分类模式:加载已有分类标注
|
|
|
|
// 分类模式:加载已有分类标注
|
|
|
|
@ -955,6 +1069,10 @@ const selectImage = async (image: Image) => {
|
|
|
|
// 修改 handleAnnotationCreated 函数
|
|
|
|
// 修改 handleAnnotationCreated 函数
|
|
|
|
const handleAnnotationCreated = (annotation: any) => {
|
|
|
|
const handleAnnotationCreated = (annotation: any) => {
|
|
|
|
console.log('创建标注:', annotation);
|
|
|
|
console.log('创建标注:', annotation);
|
|
|
|
|
|
|
|
// 创建新标注后,设置新的选中状态
|
|
|
|
|
|
|
|
selectedAnnoId.value = annotation.id;
|
|
|
|
|
|
|
|
selectedAnnotationId.value = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
// 解析标注位置信息
|
|
|
|
// 解析标注位置信息
|
|
|
|
const positionInfo = parseAnnotationPosition(annotation);
|
|
|
|
const positionInfo = parseAnnotationPosition(annotation);
|
|
|
|
console.log(positionInfo);
|
|
|
|
console.log(positionInfo);
|
|
|
|
@ -1024,11 +1142,13 @@ const handleAnnotationCreated = (annotation: any) => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
|
|
|
|
const isSelected = selectedAnnoId.value === annotationItem.id;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
fill: colorValue,
|
|
|
|
fill: colorValue,
|
|
|
|
fillOpacity: 0.25,
|
|
|
|
fillOpacity: isSelected ? 0.4 : 0.25,
|
|
|
|
stroke: colorValue,
|
|
|
|
stroke: isSelected ? '#409EFF' : colorValue,
|
|
|
|
strokeWidth: 2,
|
|
|
|
strokeWidth: isSelected ? 3 : 2,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
@ -1074,7 +1194,114 @@ const handleAnnotationDeleted = (annotation: any) => {
|
|
|
|
// 处理点击标注事件
|
|
|
|
// 处理点击标注事件
|
|
|
|
const handleClickAnnotation = (annotation: any, shape: any, sel: any) => {
|
|
|
|
const handleClickAnnotation = (annotation: any, shape: any, sel: any) => {
|
|
|
|
console.log('点击标注:', annotation);
|
|
|
|
console.log('点击标注:', annotation);
|
|
|
|
// 可以在这里实现标注选中后的操作,如高亮显示等
|
|
|
|
// 设置选中状态,高亮左侧对应项
|
|
|
|
|
|
|
|
if (annotation && annotation.id) {
|
|
|
|
|
|
|
|
// 记录当前选中的 Annotorious 标注 ID
|
|
|
|
|
|
|
|
selectedAnnoId.value = annotation.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 通过 annotationData.id 查找左侧对应的标注
|
|
|
|
|
|
|
|
const matchedAnnotation = annotations.value.find((item: Annotation) =>
|
|
|
|
|
|
|
|
item.annotationData && item.annotationData.id === annotation.id
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (matchedAnnotation) {
|
|
|
|
|
|
|
|
selectedAnnotationId.value = matchedAnnotation.id;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 如果没找到匹配的,使用 annotation.id 作为后备
|
|
|
|
|
|
|
|
selectedAnnotationId.value = annotation.id;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动到对应的左侧标注项
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
const selectedItem = document.querySelector('.annotation-item.selected');
|
|
|
|
|
|
|
|
if (selectedItem) {
|
|
|
|
|
|
|
|
selectedItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 手动刷新样式以显示选中效果
|
|
|
|
|
|
|
|
if (annoInstance) {
|
|
|
|
|
|
|
|
annoInstance.setStyle((annotationItem: any, state: any) => {
|
|
|
|
|
|
|
|
if (annotationItem.bodies && annotationItem.bodies.length > 0) {
|
|
|
|
|
|
|
|
let color = annotationItem.bodies[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 修复可能损坏的颜色数据结构
|
|
|
|
|
|
|
|
if (typeof color === 'object' && color !== null) {
|
|
|
|
|
|
|
|
if (!Array.isArray(color)) {
|
|
|
|
|
|
|
|
const keys = Object.keys(color).filter(k => k !== 'annotation').sort((a, b) => parseInt(a) - parseInt(b));
|
|
|
|
|
|
|
|
color = keys.map(k => color[k]).join('');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
|
|
|
|
const isSelected = selectedAnnoId.value === annotationItem.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
fill: colorValue,
|
|
|
|
|
|
|
|
fillOpacity: isSelected ? 0.4 : 0.25,
|
|
|
|
|
|
|
|
stroke: isSelected ? '#409EFF' : colorValue,
|
|
|
|
|
|
|
|
strokeWidth: isSelected ? 3 : 2,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 点击左侧标注列表选中对应的标注
|
|
|
|
|
|
|
|
const selectAnnotationById = (annotation: Annotation, index: number) => {
|
|
|
|
|
|
|
|
selectedAnnotationId.value = annotation.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 Annotorious 标注 ID
|
|
|
|
|
|
|
|
const annoId = annotation.annotationData?.id || null;
|
|
|
|
|
|
|
|
selectedAnnoId.value = annoId;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 通过刷新样式来高亮对应的标注
|
|
|
|
|
|
|
|
if (annoInstance) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
annoInstance.setStyle((annotationItem: any, state: any) => {
|
|
|
|
|
|
|
|
if (annotationItem.bodies && annotationItem.bodies.length > 0) {
|
|
|
|
|
|
|
|
let color = annotationItem.bodies[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 修复可能损坏的颜色数据结构
|
|
|
|
|
|
|
|
if (typeof color === 'object' && color !== null) {
|
|
|
|
|
|
|
|
if (!Array.isArray(color)) {
|
|
|
|
|
|
|
|
const keys = Object.keys(color).filter(k => k !== 'annotation').sort((a, b) => parseInt(a) - parseInt(b));
|
|
|
|
|
|
|
|
color = keys.map(k => color[k]).join('');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
|
|
|
|
const isSelected = selectedAnnoId.value === annotationItem.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
fill: colorValue,
|
|
|
|
|
|
|
|
fillOpacity: isSelected ? 0.4 : 0.25,
|
|
|
|
|
|
|
|
stroke: isSelected ? '#409EFF' : colorValue,
|
|
|
|
|
|
|
|
strokeWidth: isSelected ? 3 : 2,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('高亮标注:', annoId);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.error('高亮标注失败:', error);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log('选中左侧标注:', annotation);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 触发标注选中效果(备用方法)
|
|
|
|
|
|
|
|
const triggerAnnotationSelection = (annotationData: any) => {
|
|
|
|
|
|
|
|
if (!annoInstance || !annotationData) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 手动触发点击事件
|
|
|
|
|
|
|
|
const selector = document.querySelector(`[data-id="${annotationData.id}"]`);
|
|
|
|
|
|
|
|
if (selector) {
|
|
|
|
|
|
|
|
selector.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理选择变化事件
|
|
|
|
// 处理选择变化事件
|
|
|
|
@ -1212,11 +1439,13 @@ const setAnnotationType = (annotationIndex: number, typeId: number) => {
|
|
|
|
const finalColor = typeof color === 'string' ? color : color.toString();
|
|
|
|
const finalColor = typeof color === 'string' ? color : color.toString();
|
|
|
|
annoData.bodies[0] = finalColor;
|
|
|
|
annoData.bodies[0] = finalColor;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isSelected = selectedAnnoId.value === annoData.id;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
fill: finalColor,
|
|
|
|
fill: finalColor,
|
|
|
|
fillOpacity: 0.25,
|
|
|
|
fillOpacity: isSelected ? 0.4 : 0.25,
|
|
|
|
stroke: finalColor,
|
|
|
|
stroke: isSelected ? '#409EFF' : finalColor,
|
|
|
|
strokeWidth: 2,
|
|
|
|
strokeWidth: isSelected ? 3 : 2,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1238,15 +1467,38 @@ const setAnnotationType = (annotationIndex: number, typeId: number) => {
|
|
|
|
// 替换 selectAnnotationOnImage 函数
|
|
|
|
// 替换 selectAnnotationOnImage 函数
|
|
|
|
const selectAnnotationOnImage = (annotation: any) => {
|
|
|
|
const selectAnnotationOnImage = (annotation: any) => {
|
|
|
|
if (annoInstance && annotation.annotationData) {
|
|
|
|
if (annoInstance && annotation.annotationData) {
|
|
|
|
// 使用 Annotorious 的正确 API 来选中标注
|
|
|
|
// 更新选中状态
|
|
|
|
|
|
|
|
selectedAnnoId.value = annotation.annotationData.id;
|
|
|
|
|
|
|
|
selectedAnnotationId.value = annotation.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新样式以显示高亮
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// 先清除当前选择
|
|
|
|
annoInstance.setStyle((annotationItem: any, state: any) => {
|
|
|
|
annoInstance.clearSelection();
|
|
|
|
if (annotationItem.bodies && annotationItem.bodies.length > 0) {
|
|
|
|
// 选中指定的标注
|
|
|
|
let color = annotationItem.bodies[0];
|
|
|
|
annoInstance.selectAnnotation(annotation.annotationData.id);
|
|
|
|
|
|
|
|
console.log('选中标注:', annotation);
|
|
|
|
if (typeof color === 'object' && color !== null) {
|
|
|
|
|
|
|
|
if (!Array.isArray(color)) {
|
|
|
|
|
|
|
|
const keys = Object.keys(color).filter(k => k !== 'annotation').sort((a, b) => parseInt(a) - parseInt(b));
|
|
|
|
|
|
|
|
color = keys.map(k => color[k]).join('');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
|
|
|
|
const isSelected = selectedAnnoId.value === annotationItem.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
fill: colorValue,
|
|
|
|
|
|
|
|
fillOpacity: isSelected ? 0.4 : 0.25,
|
|
|
|
|
|
|
|
stroke: isSelected ? '#409EFF' : colorValue,
|
|
|
|
|
|
|
|
strokeWidth: isSelected ? 3 : 2,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('高亮标注:', annotation);
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.error('选中标注失败:', error);
|
|
|
|
console.error('高亮标注失败:', error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -1404,11 +1656,13 @@ const loadExistingAnnotations = () => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
const colorValue = typeof color === 'string' ? color : color.toString();
|
|
|
|
|
|
|
|
const isSelected = selectedAnnoId.value === annotationItem.id;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
fill: colorValue,
|
|
|
|
fill: colorValue,
|
|
|
|
fillOpacity: 0.25,
|
|
|
|
fillOpacity: isSelected ? 0.4 : 0.25,
|
|
|
|
stroke: colorValue,
|
|
|
|
stroke: isSelected ? '#409EFF' : colorValue,
|
|
|
|
strokeWidth: 2,
|
|
|
|
strokeWidth: isSelected ? 3 : 2,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
@ -1758,33 +2012,41 @@ onUnmounted(() => {
|
|
|
|
.annotation-list {
|
|
|
|
.annotation-list {
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: 12px;
|
|
|
|
gap: 8px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.annotation-item {
|
|
|
|
.annotation-item {
|
|
|
|
background: #f8f9fa;
|
|
|
|
background: #f8f9fa;
|
|
|
|
border-radius: 8px;
|
|
|
|
border-radius: 6px;
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
padding: 12px;
|
|
|
|
padding: 8px 10px;
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
width:90%;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
&:hover {
|
|
|
|
background: #e9ecef;
|
|
|
|
background: #e9ecef;
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.annotation-header {
|
|
|
|
.annotation-header {
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
align-items: center;
|
|
|
|
gap: 8px;
|
|
|
|
gap: 6px;
|
|
|
|
margin-bottom: 8px;
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
.type-select {
|
|
|
|
.type-select {
|
|
|
|
flex: 1;
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
:deep(.el-input__wrapper) {
|
|
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
:deep(.el-input__inner) {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delete-annotation {
|
|
|
|
.delete-annotation {
|
|
|
|
color: #f56c6c;
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
&:hover {
|
|
|
|
background: rgba(245, 108, 108, 0.1);
|
|
|
|
background: rgba(245, 108, 108, 0.1);
|
|
|
|
@ -1795,20 +2057,32 @@ onUnmounted(() => {
|
|
|
|
.annotation-coords {
|
|
|
|
.annotation-coords {
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: 4px;
|
|
|
|
gap: 2px;
|
|
|
|
|
|
|
|
|
|
|
|
.coord-item {
|
|
|
|
.coord-item {
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
align-items: center;
|
|
|
|
gap: 6px;
|
|
|
|
gap: 4px;
|
|
|
|
font-size: 12px;
|
|
|
|
font-size: 11px;
|
|
|
|
color: #606266;
|
|
|
|
color: #606266;
|
|
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
|
|
|
|
|
|
.el-icon {
|
|
|
|
.el-icon {
|
|
|
|
color: #909399;
|
|
|
|
color: #909399;
|
|
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 选中状态样式
|
|
|
|
|
|
|
|
&.selected {
|
|
|
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
|
|
|
|
|
|
|
transform: translateX(4px);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 进度信息
|
|
|
|
// 进度信息
|
|
|
|
@ -1824,17 +2098,56 @@ onUnmounted(() => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 图片分组样式
|
|
|
|
|
|
|
|
.image-section {
|
|
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.section-label {
|
|
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
|
padding-bottom: 6px;
|
|
|
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.label-text {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.collapse-icon {
|
|
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
|
|
transition: transform 0.3s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:hover .collapse-icon {
|
|
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.empty-images {
|
|
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 图片网格
|
|
|
|
// 图片网格
|
|
|
|
.image-grid {
|
|
|
|
.image-grid {
|
|
|
|
display: grid;
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
gap: 12px;
|
|
|
|
gap: 10px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-item {
|
|
|
|
.image-item {
|
|
|
|
border: 2px solid #e9ecef;
|
|
|
|
border: 2px solid #e9ecef;
|
|
|
|
border-radius: 8px;
|
|
|
|
border-radius: 8px;
|
|
|
|
padding: 8px;
|
|
|
|
padding: 6px;
|
|
|
|
cursor: pointer;
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
background: white;
|
|
|
|
background: white;
|
|
|
|
@ -1842,19 +2155,32 @@ onUnmounted(() => {
|
|
|
|
&:hover {
|
|
|
|
&:hover {
|
|
|
|
border-color: #409eff;
|
|
|
|
border-color: #409eff;
|
|
|
|
transform: translateY(-2px);
|
|
|
|
transform: translateY(-2px);
|
|
|
|
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.2);
|
|
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
&.active {
|
|
|
|
border-color: #409eff;
|
|
|
|
border-color: #409eff;
|
|
|
|
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1) 0%, rgba(64, 158, 255, 0.05) 100%);
|
|
|
|
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1) 0%, rgba(64, 158, 255, 0.05) 100%);
|
|
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.annotated,
|
|
|
|
|
|
|
|
&.unannotated {
|
|
|
|
|
|
|
|
border-left-color: #409eff;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.annotated:not(.active) {
|
|
|
|
|
|
|
|
border-left: 3px solid #67c23a;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.unannotated:not(.active) {
|
|
|
|
|
|
|
|
border-left: 3px solid #e6a23c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-wrapper {
|
|
|
|
.image-wrapper {
|
|
|
|
position: relative;
|
|
|
|
position: relative;
|
|
|
|
width: 100%;
|
|
|
|
width: 100%;
|
|
|
|
height: 100px;
|
|
|
|
height: 80px;
|
|
|
|
overflow: hidden;
|
|
|
|
overflow: hidden;
|
|
|
|
border-radius: 6px;
|
|
|
|
border-radius: 6px;
|
|
|
|
background: #f5f7fa;
|
|
|
|
background: #f5f7fa;
|
|
|
|
@ -1872,28 +2198,32 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
|
|
|
|
.status-indicator {
|
|
|
|
.status-indicator {
|
|
|
|
position: absolute;
|
|
|
|
position: absolute;
|
|
|
|
bottom: 6px;
|
|
|
|
bottom: 4px;
|
|
|
|
right: 6px;
|
|
|
|
right: 4px;
|
|
|
|
width: 24px;
|
|
|
|
width: 20px;
|
|
|
|
height: 24px;
|
|
|
|
height: 20px;
|
|
|
|
border-radius: 50%;
|
|
|
|
border-radius: 50%;
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
justify-content: center;
|
|
|
|
color: white;
|
|
|
|
color: white;
|
|
|
|
font-size: 14px;
|
|
|
|
font-size: 12px;
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
&.completed {
|
|
|
|
&.completed {
|
|
|
|
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
|
|
|
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.pending {
|
|
|
|
|
|
|
|
background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-name {
|
|
|
|
.image-name {
|
|
|
|
text-align: center;
|
|
|
|
text-align: center;
|
|
|
|
margin-top: 8px;
|
|
|
|
margin-top: 6px;
|
|
|
|
font-size: 12px;
|
|
|
|
font-size: 11px;
|
|
|
|
font-weight: 500;
|
|
|
|
font-weight: 500;
|
|
|
|
color: #303133;
|
|
|
|
color: #303133;
|
|
|
|
overflow: hidden;
|
|
|
|
overflow: hidden;
|
|
|
|
@ -1904,6 +2234,13 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 图片面板包装器
|
|
|
|
|
|
|
|
.image-panel-wrapper {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动条样式
|
|
|
|
// 滚动条样式
|
|
|
|
.left-scrollbar {
|
|
|
|
.left-scrollbar {
|
|
|
|
height: 60vh;
|
|
|
|
height: 60vh;
|
|
|
|
@ -1924,21 +2261,52 @@ onUnmounted(() => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 面板标题样式
|
|
|
|
|
|
|
|
.panel-title {
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.title-progress {
|
|
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-progress-circle__track) {
|
|
|
|
|
|
|
|
stroke: #e4e7ed;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-progress-circle__path) {
|
|
|
|
|
|
|
|
stroke: #67c23a;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.progress-text-small {
|
|
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-scrollbar {
|
|
|
|
.image-scrollbar {
|
|
|
|
height: calc(100vh - 200px);
|
|
|
|
height: calc(100vh - 250px);
|
|
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-scrollbar__bar) {
|
|
|
|
/* 自定义滚动条样式 */
|
|
|
|
&.is-vertical {
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
width: 6px;
|
|
|
|
width: 8px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-scrollbar__thumb {
|
|
|
|
&::-webkit-scrollbar-track {
|
|
|
|
background: rgba(144, 147, 153, 0.3);
|
|
|
|
background: #f1f1f1;
|
|
|
|
border-radius: 3px;
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
background: rgba(144, 147, 153, 0.5);
|
|
|
|
background: #c1c1c1;
|
|
|
|
}
|
|
|
|
border-radius: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
|
|
background: #a1a1a1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|