js_image_annotation_web/src/views/imageCaptioning/image-captioning/components/ImageResults.vue

1362 lines
45 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="image-results-container">
<!-- 按下标分组显示 -->
<template v-if="groupedImageResults && groupedImageResults.length > 0">
<div
v-for="(group, groupIndex) in groupedImageResults"
:key="group.index"
class="group-section"
>
<!-- 每组的上传信息提示 -->
<div class="upload-info" v-if="isEvaluate == '0'">
<img
src="@/assets/images/imageCaptioning/photography.png"
alt=""
class="icon-photography"
/>
<span>
第{{ group.index + 1 }}组 - {{ group.operaName }}:
上传{{ group.totalCount }}张图片,可识别标注{{
group.recognizedCount
}}张,
{{
group.unrecognizedCount
}}张无法识别标注。已识别标注图片如下,请确认。
</span>
</div>
<!-- 每组的图片展示 -->
<div class="image-results-grid">
<div
v-for="(result, imageIndex) in group.images"
:key="imageIndex"
:class="
isEvaluate == '1'
? 'image-item-evaluate'
: 'image-item'
"
>
<el-image
:class="
isEvaluate == '1'
? 'result-image-evaluate'
: 'result-image'
"
:src="result.url"
fit="contain"
@click="onClickImage(result, group.isSure)"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
<!-- 右上角的hand图标 -->
<img
v-if="
isGroupImageSelected(groupIndex, imageIndex) &&
isEvaluate == '0'
"
src="@/assets/images/imageCaptioning/hand-yellow.png"
alt="selected"
class="icon-hand"
@click="
handleGroupImageClick(
groupIndex,
imageIndex,
group.isSure,
)
"
/>
<img
v-else-if="
!isGroupImageSelected(groupIndex, imageIndex) &&
isEvaluate == '0'
"
src="@/assets/images/imageCaptioning/hand.png"
alt="hand"
class="icon-hand"
@click="
handleGroupImageClick(
groupIndex,
imageIndex,
group.isSure,
)
"
/>
<div class="card-content" v-if="isEvaluate == '1'">
<div class="card-footer">
<div class="person-info">
<span class="name"
>综合得分:{{ overallScore }}</span
>
<span class="name">{{ dictValue }}</span>
<span class="date">{{ contentImage }}</span>
</div>
<div class="tags">
<div class="tag-row">
<span
class="tag-item"
style="padding-left: 16px"
>清晰度: {{ clarity }}分</span
>
<span
class="tag-item"
style="padding-left: 16px"
>干净度: {{ cleanliness }}分</span
>
<span class="tag-item"
>压缩痕迹:
{{ compressMarks }}分</span
>
</div>
<div class="tag-row">
<span class="tag-item"
>明暗均衡: {{ balance }}分</span
>
<span class="tag-item"
>整体观感: {{ impression }}分</span
>
<span class="tag-item"
>细节体验: {{ detail }}分</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 每组的操作按钮 -->
<div class="action-buttons" v-if="isEvaluate == '0'">
<el-button
type="primary"
@click="confirmGroup(groupIndex, group.id)"
:disabled="group.isSure === '1'"
>
{{ group.isSure === '1' ? '已确认' : '确认' }}
</el-button>
</div>
</div>
</template>
<!-- 保持原有的单一展示方式 -->
<template v-else>
<!-- 上传信息提示 -->
<div class="upload-info" v-if="isEvaluate == '0'">
<img
src="@/assets/images/imageCaptioning/photography.png"
alt=""
class="icon-photography"
/>
<span>{{ uploadInfo }}</span>
</div>
<!-- 图片识别结果展示 -->
<div class="image-results-grid">
<div
v-for="(result, index) in imageResults"
:key="index"
:class="
isEvaluate == '1' ? 'image-item-evaluate' : 'image-item'
"
>
<ImagePreview
:class="
isEvaluate == '1'
? 'result-image-evaluate'
: 'result-image'
"
:src="result.url"
@click.native="onClickImage(result)"
/>
<!-- 右上角的hand图标 -->
<img
v-if="selectedImages[index] && isEvaluate == '0'"
src="@/assets/images/imageCaptioning/hand-yellow.png"
alt="selected"
class="icon-hand"
@click="handleImageClick(index, isSure)"
/>
<img
v-else-if="!selectedImages[index] && isEvaluate == '0'"
src="@/assets/images/imageCaptioning/hand.png"
alt="hand"
class="icon-hand"
@click="handleImageClick(index, isSure)"
/>
<div class="card-content" v-if="isEvaluate == '1'">
<div class="card-footer">
<div class="person-info">
<span class="name"
>综合得分:{{ overallScore }}</span
>
<span class="name">{{ dictValue }}</span>
<span class="date">{{ contentImage }}</span>
</div>
<div class="tags">
<div class="tag-row">
<span
class="tag-item"
style="padding-left: 16px"
>清晰度: {{ clarity }}分</span
>
<span
class="tag-item"
style="padding-left: 16px"
>干净度: {{ cleanliness }}分</span
>
<span class="tag-item"
>压缩痕迹: {{ compressMarks }}分</span
>
</div>
<div class="tag-row">
<span class="tag-item"
>明暗均衡: {{ balance }}分</span
>
<span class="tag-item"
>整体观感: {{ impression }}分</span
>
<span class="tag-item"
>细节体验: {{ detail }}分</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons" v-if="isEvaluate == '0'">
<el-button
type="primary"
@click="$emit('confirm-results')"
:disabled="isSure === '1'"
>
{{ isSure === '1' ? '已确认' : '确认' }}
</el-button>
</div>
</template>
<!-- 图片显示弹框 -->
<el-dialog
width="70%"
:visible.sync="imageDialogVisible"
:before-close="handleCloseImageDialog"
>
<template #title>
<span>{{ isSure == 1 ? '图片查看' : '图片标注' }}</span>
</template>
<div v-if="currentImage" class="annotation-dialog">
<div class="annotation-preview">
<div
v-if="isReAnnotating"
class="annotation-canvas-wrapper"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px',
}"
>
<canvas ref="imageCanvas" class="image-canvas"></canvas>
<canvas
ref="drawCanvas"
class="draw-canvas"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
></canvas>
</div>
<img
v-else
:src="imgSrc"
alt="preview"
class="annotation-preview-image"
/>
</div>
<div v-if="isReAnnotating" class="annotation-type">
<span class="label">标注类型:</span>
<el-radio-group v-model="annotationType" size="small">
<el-radio-button
v-for="option in annotationTypeOptions"
:key="option.value"
:label="option.value"
>
{{ option.label }}
</el-radio-button>
</el-radio-group>
<!-- 增加一个tags标签新增 每次可以新增一个 -->
<div>
<el-tag
:key="tag"
closable
v-for="tag in newTagsList"
:disable-transitions="false"
@close="handleClose(tag)"
>
{{ tag }}
</el-tag>
<el-input
class="input-new-tag"
v-if="inputVisible"
v-model="inputValue"
ref="saveTagInput"
size="small"
@blur="handleInputConfirm"
>
</el-input>
<el-button
v-else
class="button-new-tag"
size="small"
@click="showInput"
>+ 其他</el-button
>
</div>
</div>
<div class="annotation-info">
<div class="annotation-meta">
<p v-if="isReAnnotating">
图片尺寸:{{ originalSize.width }} x
{{ originalSize.height }}
</p>
<p v-if="annotationCoords">
当前标注:左上:({{ annotationCoords.x1 }},
{{ annotationCoords.y1 }}) ,右下:({{
annotationCoords.x2
}}, {{ annotationCoords.y2 }})
</p>
<p v-if="!annotationCoords && isReAnnotating">
请先选择标注类型,然后拖拽绘制标注框
</p>
</div>
<div class="annotation-buttons">
<el-button
v-if="!isReAnnotating && isSure != 1"
type="primary"
size="small"
@click="startReAnnotation"
>
重新标注
</el-button>
<template v-if="isReAnnotating && isSure != 1">
<el-button size="small" @click="cancelReAnnotation">
取消标注
</el-button>
<el-button
type="success"
size="small"
:disabled="!annotationCoords || !annotationType"
@click="saveTemporaryAnnotation"
>
临时确定
</el-button>
<el-button
type="primary"
size="small"
:disabled="annotations.length === 0"
@click="submitAllAnnotations"
>
提交全部
</el-button>
</template>
</div>
</div>
<!-- 标注列表 -->
<div
v-if="isReAnnotating && annotations.length > 0"
class="annotation-list"
>
<div class="annotation-list-title"
>已标注列表({{ annotations.length }}</div
>
<div class="annotation-list-content">
<div
v-for="(item, index) in annotations"
:key="index"
class="annotation-list-item"
>
<span class="annotation-item-info">
<span class="annotation-item-type">{{
getTypeLabel(item.type)
}}</span>
<span class="annotation-item-coords"
>({{ item.coords.x1 }},
{{ item.coords.y1 }}) - ({{
item.coords.x2
}}, {{ item.coords.y2 }})</span
>
</span>
<el-button
type="text"
size="mini"
icon="el-icon-delete"
@click="removeAnnotation(index)"
>
删除
</el-button>
</div>
</div>
</div>
</div>
<div v-else class="annotation-empty">请先选择图片</div>
</el-dialog>
</div>
</template>
<script>
import {
getSelectedAPI,
updateImageAnnotationAPI,
} from '@/api/imageCaptioning/imageCaptioning'
export default {
name: 'ImageResults',
props: {
uploadInfo: {
type: String,
default: '',
},
imageResults: {
type: Array,
default: () => [],
},
groupedImageResults: {
type: Array,
default: () => [],
},
selectedImages: {
type: Object,
default: () => ({}),
},
isEvaluate: {
type: String,
default: '0',
},
isSure: {
type: String,
default: '',
},
},
data() {
return {
imageDialogVisible: false,
currentImage: null,
canvasWidth: 720,
canvasHeight: 480,
isReAnnotating: false,
originalSize: {
width: 0,
height: 0,
},
scaleInfo: {
scale: 1,
offsetX: 0,
offsetY: 0,
},
isDrawing: false,
startPoint: null,
currentRect: null,
annotationCoords: null,
annotationType: '',
annotations: [],
annotationTypeOptions: [
// { label: '人物', value: 'person' },
// { label: '物体', value: 'object' },
// { label: '场景', value: 'scene' },
],
imgSrc: '',
isSure: null,
newTagsList: [],
inputVisible: false,
inputValue: '',
}
},
created() {
this.getTags()
},
methods: {
isGroupImageSelected(groupIndex, imageIndex) {
// 根据分组索引和图片索引判断是否选中
const key = `${groupIndex}-${imageIndex}`
return !!this.selectedImages[key]
},
handleGroupImageClick(groupIndex, imageIndex, groupIsSure) {
// 如果该组已确认,则不允许更改选择
if (groupIsSure === '1') {
this.$message.warning('该组已确认,无法更改选择')
return
}
const key = `${groupIndex}-${imageIndex}`
this.$emit('hand-click', { groupIndex, imageIndex, key })
},
handleImageClick(index, isSure) {
// 如果已确认,则不允许更改选择
if (isSure === '1') {
this.$message.warning('已确认,无法更改选择')
return
}
this.$emit('hand-click', { index, isSingle: true })
},
confirmGroup(groupIndex, id) {
console.log('confirmGroup', id)
// 可以在这里处理特定组的确认逻辑
this.$emit('confirm-results', id)
},
// 点击图片显示弹框
onClickImage(image, isSure = null) {
console.log('image', image)
if (!image || !image.url) {
return
}
this.isSure = isSure
this.currentImage = image
this.imgSrc = image.url
this.imageDialogVisible = true
this.annotationCoords = null
this.annotationType = ''
this.annotations = []
this.isReAnnotating = false
},
// 关闭图片显示弹框
handleCloseImageDialog() {
this.imageDialogVisible = false
this.resetAnnotation()
},
startReAnnotation() {
if (this.isSure == 1) {
this.$message.warning('该图片已确认,无法重新标注')
return
}
this.isReAnnotating = true
this.annotationCoords = null
this.annotationType = ''
this.imgSrc = this.currentImage.originUrl
this.annotations = []
this.$nextTick(() => {
this.renderBaseImage()
})
},
cancelReAnnotation() {
this.isReAnnotating = false
// 清空所有状态
this.annotationCoords = null
this.annotationType = ''
this.annotations = []
this.currentRect = null
this.isDrawing = false
this.startPoint = null
this.imgSrc = this.currentImage.url
// 清空画布
this.clearDrawing()
},
renderBaseImage() {
const imageCanvas = this.$refs.imageCanvas
const drawCanvas = this.$refs.drawCanvas
if (
!this.isReAnnotating ||
!imageCanvas ||
!drawCanvas ||
!this.currentImage
) {
return
}
imageCanvas.width = this.canvasWidth
imageCanvas.height = this.canvasHeight
drawCanvas.width = this.canvasWidth
drawCanvas.height = this.canvasHeight
const ctx = imageCanvas.getContext('2d')
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const { naturalWidth, naturalHeight } = img
this.originalSize = {
width: naturalWidth,
height: naturalHeight,
}
const scale = Math.min(
this.canvasWidth / naturalWidth,
this.canvasHeight / naturalHeight,
)
const drawWidth = naturalWidth * scale
const drawHeight = naturalHeight * scale
const offsetX = (this.canvasWidth - drawWidth) / 2
const offsetY = (this.canvasHeight - drawHeight) / 2
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight)
this.scaleInfo = { scale, offsetX, offsetY }
this.clearDrawing()
// 重新绘制已保存的标注
this.$nextTick(() => {
this.drawCurrentRect()
})
}
img.src = this.imgSrc
},
clearDrawing() {
const drawCanvas = this.$refs.drawCanvas
if (!drawCanvas) return
const drawCtx = drawCanvas.getContext('2d')
drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height)
},
handleMouseDown(event) {
if (!this.currentImage || !this.annotationType) {
if (!this.annotationType) {
this.$message.warning('请先选择标注类型')
}
return
}
const position = this.getCanvasPosition(event)
this.isDrawing = true
this.startPoint = position
this.currentRect = { ...position, width: 0, height: 0 }
},
handleMouseMove(event) {
if (!this.isDrawing || !this.currentRect) return
const position = this.getCanvasPosition(event)
this.currentRect.width = position.x - this.startPoint.x
this.currentRect.height = position.y - this.startPoint.y
this.drawCurrentRect()
},
handleMouseUp() {
if (!this.isDrawing || !this.currentRect) return
this.isDrawing = false
this.annotationCoords = this.calculateOriginalCoords(
this.currentRect,
)
},
handleMouseLeave() {
if (!this.isDrawing) return
this.isDrawing = false
this.drawCurrentRect()
},
getCanvasPosition(event) {
const canvas = this.$refs.drawCanvas
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
return { x, y }
},
drawCurrentRect() {
const drawCanvas = this.$refs.drawCanvas
if (!drawCanvas) return
const ctx = drawCanvas.getContext('2d')
ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height)
// 绘制已保存的标注框
this.annotations.forEach((annotation) => {
const rect = this.convertToCanvasCoords(annotation.coords)
ctx.strokeStyle = '#ff0000'
ctx.lineWidth = 2
ctx.setLineDash([])
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)
})
// 绘制当前正在绘制的标注框
if (this.currentRect) {
ctx.strokeStyle = '#409eff' // 红线
ctx.lineWidth = 2
ctx.setLineDash([6, 4])
ctx.strokeRect(
this.currentRect.x,
this.currentRect.y,
this.currentRect.width,
this.currentRect.height,
)
ctx.setLineDash([])
}
},
calculateOriginalCoords(rect) {
const { scale, offsetX, offsetY } = this.scaleInfo
if (!scale || !this.originalSize.width) return null
const normalize = (value, offset) => (value - offset) / scale
let x1 = Math.min(rect.x, rect.x + rect.width)
let y1 = Math.min(rect.y, rect.y + rect.height)
let x2 = Math.max(rect.x, rect.x + rect.width)
let y2 = Math.max(rect.y, rect.y + rect.height)
x1 = this.clampCoordinate(
normalize(x1, offsetX),
0,
this.originalSize.width,
)
y1 = this.clampCoordinate(
normalize(y1, offsetY),
0,
this.originalSize.height,
)
x2 = this.clampCoordinate(
normalize(x2, offsetX),
0,
this.originalSize.width,
)
y2 = this.clampCoordinate(
normalize(y2, offsetY),
0,
this.originalSize.height,
)
return {
x1: Math.round(x1),
y1: Math.round(y1),
x2: Math.round(x2),
y2: Math.round(y2),
}
},
clampCoordinate(value, min, max) {
if (Number.isNaN(value)) return min
return Math.max(min, Math.min(max, value))
},
convertToCanvasCoords(coords) {
const { scale, offsetX, offsetY } = this.scaleInfo
if (!scale) return { x: 0, y: 0, width: 0, height: 0 }
const x1 = coords.x1 * scale + offsetX
const y1 = coords.y1 * scale + offsetY
const x2 = coords.x2 * scale + offsetX
const y2 = coords.y2 * scale + offsetY
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
}
},
saveTemporaryAnnotation() {
if (!this.annotationCoords || !this.annotationType) {
this.$message.warning('请先完成标注并选择类型')
return
}
this.annotations.push({
coords: { ...this.annotationCoords },
type: this.annotationType,
typeName: this.getTypeLabel(this.annotationType),
})
this.annotationCoords = null
this.annotationType = ''
this.clearDrawing()
this.$nextTick(() => {
this.drawCurrentRect()
})
this.$message.success('标注已保存')
},
removeAnnotation(index) {
this.annotations.splice(index, 1)
// 清空当前正在绘制的标注
this.currentRect = null
this.annotationCoords = null
this.isDrawing = false
// 重新绘制画布(只绘制剩余的已保存标注)
this.$nextTick(() => {
this.clearDrawing()
this.drawCurrentRect()
})
// this.$message.success('标注已删除')
},
getTypeLabel(value) {
const option = this.annotationTypeOptions.find(
(opt) => opt.value === value,
)
return option ? option.label : value
},
async submitAllAnnotations() {
if (this.annotations.length === 0) {
this.$message.warning('请至少添加一个标注')
return
}
if (!this.currentImage) {
this.$message.warning('图片信息缺失')
return
}
try {
// 生成带标注的图片base64
const imageCanvas = this.$refs.imageCanvas
const drawCanvas = this.$refs.drawCanvas
if (!imageCanvas || !drawCanvas) {
this.$message.error('无法获取画布')
return
}
// 创建一个临时canvas用于合成最终图片
const finalCanvas = document.createElement('canvas')
finalCanvas.width = this.originalSize.width
finalCanvas.height = this.originalSize.height
const finalCtx = finalCanvas.getContext('2d')
// 根据图片尺寸计算缩放比例,确保在不同分辨率下都能清晰显示
// 使用图片的平均尺寸作为基准,计算缩放因子
const avgSize =
(this.originalSize.width + this.originalSize.height) / 2
const baseSize = 1000 // 基准尺寸1000px
// 使用对数缩放,避免过大图片时缩放因子过大
// 缩放因子范围0.8 - 3.0
const scaleFactor = Math.max(
0.8,
Math.min(3.0, Math.sqrt(avgSize / baseSize)),
)
// 根据缩放因子动态计算线条宽度和字体大小
const baseLineWidth = 1.5
const lineWidth = baseLineWidth * scaleFactor
const baseFontSize = 12
const fontSize = baseFontSize * scaleFactor
const baseMargin = 2
const margin = baseMargin * scaleFactor
const basePadding = 2
const padding = basePadding * scaleFactor
// 加载原始图片
const img = new Image()
img.crossOrigin = 'Anonymous'
await new Promise((resolve, reject) => {
img.onload = () => {
// 绘制原始图片
finalCtx.drawImage(
img,
0,
0,
this.originalSize.width,
this.originalSize.height,
)
// 绘制所有标注框
this.annotations.forEach((annotation) => {
const { coords, typeName } = annotation
// 绘制标注框
finalCtx.strokeStyle = '#03FB02'
finalCtx.lineWidth = lineWidth
finalCtx.strokeRect(
coords.x1,
coords.y1,
coords.x2 - coords.x1,
coords.y2 - coords.y1,
)
// 绘制类型名称标签
if (typeName) {
// 设置字体
finalCtx.font = `${fontSize}px Arial`
finalCtx.textBaseline = 'top'
// 测量文字宽度
const textMetrics =
finalCtx.measureText(typeName)
const textWidth = textMetrics.width
const textHeight = fontSize
// 标签位置(标注框左上角 + margin
const labelX = coords.x1 + margin
const labelY = coords.y1 + margin
// 绘制背景矩形
finalCtx.fillStyle = '#01CB04'
finalCtx.fillRect(
labelX,
labelY,
textWidth + padding * 2,
textHeight + padding * 2,
)
// 绘制文字
finalCtx.fillStyle = '#ffffff'
finalCtx.fillText(
typeName,
labelX + padding,
labelY + padding,
)
}
})
resolve()
}
img.onerror = reject
img.src = this.imgSrc
})
// 转换为base64
const base64Image = finalCanvas.toDataURL('image/png')
// 提交数据
// this.$emit('manual-annotation', {
// image: this.currentImage,
// annotations: this.annotations,
// base64Image: base64Image,
// })
console.log(
'base64Image',
base64Image,
'annotations',
this.annotations,
'image',
this.currentImage,
)
// 根据this.annotations 把重复的标签去重
const tagList = new Set(this.annotations.map((e) => e.typeName))
console.log('tagList', tagList)
// 组装参数
const params = {
id: this.currentImage.id,
bast64: base64Image,
jsonData: JSON.stringify(
this.annotations.map((e) => {
return {
class: e.typeName,
confidence: 0,
bbox: {
x1: e.coords.x1,
y1: e.coords.y1,
x2: e.coords.x2,
y2: e.coords.y2,
},
}
}),
),
// 获取标签去重后的数据Set转数组后join
type: Array.from(tagList).join(','),
}
console.log('params', params)
const res = await updateImageAnnotationAPI(params)
console.log('res', res)
if (res.code === 200) {
this.$message.success('标注已提交')
this.cancelReAnnotation()
this.imageDialogVisible = false
this.$emit('update-image-annotation')
}
// this.$message.success('标注已提交')
// 重置状态
} catch (error) {
console.error('提交标注失败:', error)
this.$message.error('提交标注失败,请重试')
}
},
resetAnnotation() {
this.currentImage = null
this.annotationCoords = null
this.annotationType = ''
this.annotations = []
this.isReAnnotating = false
this.originalSize = { width: 0, height: 0 }
this.scaleInfo = { scale: 1, offsetX: 0, offsetY: 0 }
this.isDrawing = false
this.startPoint = null
this.currentRect = null
},
// 获取标签
getTags() {
getSelectedAPI().then((res) => {
// console.log('res标签', res)
this.annotationTypeOptions = res.data.map((item) => ({
label: item.name,
value: item.id,
}))
})
},
handleClose(tag) {
this.newTagsList.splice(this.newTagsList.indexOf(tag), 1)
// 根据value删除annotationTypeOptions中的数据
this.annotationTypeOptions = this.annotationTypeOptions.filter(
(item) => item.value !== tag,
)
},
showInput() {
this.inputVisible = true
this.$nextTick((_) => {
this.$refs.saveTagInput.$refs.input.focus()
})
},
handleInputConfirm() {
let inputValue = this.inputValue
if (!inputValue) {
this.inputVisible = false
return
}
// 根据inputValue在annotationTypeOptions中查找是否存在
const index = this.annotationTypeOptions.findIndex(
(item) => item.value === inputValue,
)
if (index === -1) {
this.newTagsList.push(inputValue)
this.annotationTypeOptions.push({
label: inputValue,
value: inputValue,
})
} else {
this.$message.warning('标签已存在')
return
}
// if (inputValue) {
// this.newTagsList.push(inputValue)
// this.annotationTypeOptions.push({
// label: inputValue,
// value: inputValue,
// })
// }
this.inputVisible = false
this.inp
},
},
}
</script>
<style scoped lang="scss">
.upload-info {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: 16px;
color: #54646c;
font-weight: 600;
}
.icon-photography {
width: 47px;
height: 47px;
margin-right: 8px;
vertical-align: middle;
}
.image-results-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.image-item {
position: relative;
width: 100%;
height: 260px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-item-evaluate {
position: relative;
width: 100%;
height: 360px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.result-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.result-image-evaluate {
width: 100%;
height: 260px;
object-fit: contain;
}
.annotation-dialog {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.annotation-preview {
width: 100%;
display: flex;
justify-content: center;
}
.annotation-canvas-wrapper {
position: relative;
border: 1px dashed #dcdfe6;
border-radius: 8px;
background: #000;
}
.image-canvas,
.draw-canvas {
position: absolute;
top: 0;
left: 0;
}
.image-canvas {
z-index: 1;
}
.draw-canvas {
z-index: 2;
cursor: crosshair;
}
.annotation-preview-image {
max-width: 100%;
max-height: 520px;
border-radius: 8px;
}
.annotation-type {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.annotation-type .label {
font-weight: 500;
color: #333;
white-space: nowrap;
}
.annotation-info {
width: 100%;
display: flex;
align-items: flex-start;
justify-content: space-between;
color: #333;
gap: 16px;
}
.annotation-meta p {
margin: 0 0 6px 0;
}
.annotation-buttons {
display: flex;
gap: 8px;
}
.annotation-empty {
text-align: center;
color: #999;
padding: 30px 0;
}
.annotation-list {
width: 100%;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #f5f7fa;
}
.annotation-list-title {
padding: 12px 16px;
background: #e4e7ed;
font-weight: 500;
color: #333;
border-bottom: 1px solid #e4e7ed;
}
.annotation-list-content {
max-height: 200px;
overflow-y: auto;
}
.annotation-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid #e4e7ed;
transition: background-color 0.2s;
}
.annotation-list-item:last-child {
border-bottom: none;
}
.annotation-list-item:hover {
background-color: #ecf5ff;
}
.annotation-item-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.annotation-item-type {
padding: 2px 8px;
background: #409eff;
color: #fff;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.annotation-item-coords {
color: #606266;
font-size: 13px;
font-family: monospace;
}
.icon-hand {
position: absolute;
top: 25px;
right: 25px;
width: 30px;
height: 30px;
cursor: pointer;
border-radius: 50%;
z-index: 1;
}
.icon-hand:hover {
opacity: 0.8;
}
.action-buttons {
text-align: right;
margin-bottom: 20px;
}
.card-content {
width: 100%;
display: flex;
flex-direction: column;
font-size: 14px;
}
.card-footer {
display: flex;
flex-direction: column;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
min-height: 24px;
}
.person-info {
display: flex;
justify-content: space-between;
margin: 10px 18px 0 18px;
align-items: center;
}
.name {
font-weight: bold;
color: #333;
}
.date {
color: #666;
/*font-size: 12px;*/
}
.tags {
display: flex;
flex-direction: column;
margin: 10px 18px 0 18px;
gap: 6px;
}
.tag-row {
display: table;
width: 100%;
table-layout: fixed;
margin: 3px 0 0 0;
}
.tag-item {
display: table-cell;
width: 33.33%;
text-align: left;
font-size: 12px;
color: #333;
white-space: nowrap;
padding: 0 4px;
}
.action-buttons {
display: flex;
justify-content: flex-end;
margin: 10px 18px 10px 18px;
}
el-image {
::v-deep .el-image__inner {
object-fit: contain;
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
::v-deep .image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 30px;
}
}
.input-new-tag {
width: 90px;
margin-left: 10px;
vertical-align: bottom;
}
.el-tag + .el-tag {
margin-left: 10px;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
</style>