js_image_annotation_web/src/views/imageCaptioning/image-captioning/index.vue

692 lines
26 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-captioning-container">
<!-- 左侧导航栏 -->
<Sidebar
:is-sidebar-visible="isSidebarVisible"
:label-list="labelList"
:selected-item="selectedItem"
:current-index="1"
@toggle-sidebar="toggleSidebar"
@create-new-label="createNewLabel"
@select-item="selectItem"
/>
<!-- 主内容区域 -->
<div class="main-content" v-if="showNewAnnotation">
<!-- 主内容区域-新增的 -->
<div class="add-content">
<!-- 标题 -->
<div class="welcome-message"> 我是你的图像标注助理 </div>
<!-- 标签选择区域 -->
<TagSelector
:tags="tags"
:visible-tags="visibleTags"
:selected-tag="selectedTag"
@toggle-tag="toggleTag"
@toggle-all-tags="toggleAllTags"
/>
<!-- 输入备注 -->
<el-input
:rows="1"
placeholder="请输入备注"
type="textarea"
v-model="remarkInfo"
/>
<!-- 图片上传区域 -->
<FileUploader
:file-list="fileList"
:selected-tag="selectedTag"
@file-change="handleFileChange"
@remove-image="removeImage"
@start-upload="startUpload"
/>
</div>
</div>
<!-- 主内容区域-历史 -->
<div class="main-content" v-if="showNewAnnotationAdd">
<HistoryView
:upload-info="uploadInfo"
:image-results="imageResults"
:grouped-image-results="groupedImageResults"
:selected-images="selectedImages"
:tags="tags"
:visible-tags="visibleTags"
:selected-tag="selectedTag"
:file-list="fileList"
:is-sure="isSure"
@hand-click="handleHandClick"
@confirm-results="confirmResults"
@toggle-tag="toggleTag"
@toggle-all-tags="toggleAllTags"
@file-change="handleFileChange"
@remove-image="removeImage"
@start-upload="startUpload"
@update-image-annotation="updateImageAnnotation"
/>
</div>
</div>
</template>
<script>
import {
getSelectedAPI,
getImageListAPI,
getImageListDetailsAPI,
addImageInfoAPI,
updateImageSureAPI,
} from '@/api/imageCaptioning/imageCaptioning'
import Sidebar from './components/Sidebar'
import TagSelector from './components/TagSelector'
import FileUploader from './components/FileUploader'
import HistoryView from './components/HistoryView'
export default {
name: 'imageCaptioning',
components: {
Sidebar,
TagSelector,
FileUploader,
HistoryView,
},
data() {
return {
showNewAnnotation: true, // 控制新建标注界面的显示状态
showNewAnnotationAdd: false, // 控制历史界面的显示状态
tags: [], // 标签数据
visibleTags: [], // 默认显示前3个
selectedTag: [], // 当前选中的标签
showAllTags: false, // 控制是否显示所有标签
// 导航栏数据
labelList: [],
selectedItem: null,
// 文件上传相关
fileList: [],
showImageResults: false,
uploadInfo: '',
// 图片识别结果
imageResults: [],
selectedImages: {}, // 用于跟踪选中的图片
isSidebarVisible: true, // 控制左侧导航栏显示/隐藏
isSure: '', //小图标,确定按钮 0-未确定 1-已确定
addId: '', //新增id
groupedImageResults: [], // 新增分组数据
remarkInfo: '', // 备注信息
}
},
created() {
// 获取标签列表数据
this.getSelectedAPI()
//查询左侧历史记录
this.getImageListAPI()
},
methods: {
async getSelectedAPI() {
try {
const res = await getSelectedAPI()
// 判断返回码和数据
if (res.code === 200 && res.data && Array.isArray(res.data)) {
// 将返回的数据转换为包含 id 和 name 的对象数组
this.tags = res.data.map((item) => ({
id: item.id,
name: item.name,
}))
this.visibleTags = this.tags.slice(0, 3)
} else {
console.error('获取标签列表失败:', res)
this.tags = []
}
} catch (error) {
console.error('获取标签列表异常:', error)
this.tags = []
}
},
async getImageListAPI() {
try {
const res = await getImageListAPI({ operaType: 1 })
// 判断返回码和数据
if (res.code === 200 && res.data && Array.isArray(res.data)) {
this.labelList = res.data.map((item) => ({
id: item.id,
date: item.operaTime,
name: item.operaName,
}))
}
} catch (error) {
console.error('获取列表异常:', error)
}
},
createNewLabel() {
this.selectedItem = null
this.fileList = []
this.selectedTag = []
this.showImageResults = false
this.uploadInfo = ''
this.imageResults = []
this.selectedImages = {}
this.showNewAnnotation = true
this.showNewAnnotationAdd = false
this.addId = ''
},
startUpload(remarkInfo = '') {
console.log('开始上传')
if (this.selectedTag.length === 0) {
this.$message.warning('请选择需要标注的内容标签')
return
}
// 标签处理
const selectedTagIds = this.selectedTag
.map((tag) => tag.id)
.join(',')
const selectedTagNames = this.selectedTag
.map((tag) => tag.name)
.join(',')
console.log('选中的标签IDs:', selectedTagIds)
console.log('选中的标签名称:', selectedTagNames)
if (this.fileList.length === 0) {
this.$message.warning('请上传图片')
return
}
// 初始化 selectedImages
this.selectedImages = {}
// 显示加载状态
const loading = this.$loading({
lock: true,
text: '正在上传和识别图片...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
})
// 保存原始文件列表用于显示结果
const originalFileList = [...this.fileList]
// 清空上传预览区域
this.fileList = []
// 创建 FormData 对象
const formData = new FormData()
// 添加文件
originalFileList.forEach((file) => {
formData.append('files', file.raw) // 使用 raw 字段获取原始文件对象
})
// formData.append('param', selectedTagNames);
// formData.append('id', this.addId);
// 修改参数处理方式与之前的params 保持一致
const params = {
param: selectedTagNames,
remark: remarkInfo || this.remarkInfo,
id: this.addId,
}
formData.append('params', JSON.stringify(params))
//调用上传的接口传递文件列表和标签IDsname
addImageInfoAPI(formData)
.then((res) => {
loading.close()
if (
res.code === 200 &&
res.data &&
Array.isArray(res.data)
) {
// 处理成功响应 - 获取全部记录
const records = res.data
this.addId = records[0]?.operaId || ''
console.log('this.addId:', this.addId)
// 按下标分组存储数据
this.groupedImageResults = records.map(
(record, index) => ({
index: index, // 下标
operaName: record.operaName,
images: record.fileVoList.map((item) => ({
url: item.bjUrl || '未找到图片地址',
originUrl: item.imageUrl,
id: item.imageId || item.id,
name: item.originalName,
contentImage: item.contentImage,
fileSize: item.fileSize,
operId: record.id,
})),
totalCount:
record.imageNum || originalFileList.length,
recognizedCount: record.bzNum || 0,
unrecognizedCount: record.wbzNum || 0,
isSure: record.isSure,
operId: record.operaId,
id: record.id,
}),
)
// 初始化选中状态 - 根据 isActive 字段
this.initializeSelectedImages(records)
// this.isSure = record.isSure;
// 刷新左侧标签文本并选中对应标签
if (records.length > 0) {
this.refreshSidebarLabel(
records[0].operaName,
records[0].id,
)
}
this.showImageResults = true
this.showNewAnnotation = false
this.showNewAnnotationAdd = true
this.$message.success('上传和识别完成')
} else {
this.$message.error('上传失败')
}
})
.catch((error) => {
loading.close()
console.error('上传异常:', error)
this.$message.error('上传过程中出现错误')
})
},
refreshSidebarLabel(operaName, recordId) {
this.getImageListAPI()
setTimeout(() => {
const index = this.labelList.findIndex(
(item) => item.id === recordId,
)
if (index !== -1) {
this.labelList[index].name = operaName
}
// 选中对应的标签
this.selectedItem = recordId
}, 500) // 延迟确保数据加载完成
},
selectItem(item) {
this.selectedItem = item.id
// 这里可以添加加载历史记录数据的逻辑
this.loadRecordData(item.id)
},
loadRecordData(recordId) {
// 模拟加载历史记录数据
const record = this.labelList.find((r) => r.id === recordId)
if (record) {
this.showNewAnnotation = false
this.showNewAnnotationAdd = true
// 这里可以添加实际的数据加载逻辑
this.fileList = [] // 清空文件列表
this.selectedTag = [] // 清空标签选择
this.showImageResults = true // 显示识别结果
this.selectedImages = {} // 初始化选中状态
// 生成数据
this.generateTestData(recordId)
}
},
toggleTag(tag) {
const index = this.selectedTag.findIndex(
(item) => item.id === tag.id,
)
if (index === -1) {
this.selectedTag.push(tag)
} else {
this.selectedTag.splice(index, 1)
}
if (!this.showAllTags) {
this.updateVisibleTagsInCollapsedState()
}
},
updateVisibleTagsInCollapsedState() {
const selectedTags = this.selectedTag || []
const defaultTags = this.tags.slice(0, 3)
if (selectedTags.length > 0) {
if (selectedTags.length >= 3) {
this.visibleTags = selectedTags.slice(0, 3)
} else {
const tags = defaultTags
.filter(
(tag) =>
!selectedTags.some(
(selected) => selected.id === tag.id,
),
)
.slice(0, 3 - selectedTags.length)
this.visibleTags = [...selectedTags, ...tags]
}
} else {
this.visibleTags = defaultTags
}
},
toggleAllTags() {
this.showAllTags = !this.showAllTags
if (this.showAllTags) {
this.visibleTags = this.tags
} else {
const selectedTags = this.selectedTag || []
const defaultTags = this.tags.slice(0, 3)
if (selectedTags.length > 0) {
if (selectedTags.length <= 3) {
const allTags = [...selectedTags, ...defaultTags]
const uniqueTags = allTags.filter(
(tag, index, self) =>
index ===
self.findIndex((t) => t.id === tag.id),
)
this.visibleTags = uniqueTags.slice(0, 3)
} else {
this.visibleTags = selectedTags
}
} else {
this.visibleTags = defaultTags
}
}
},
handleFileChange(file, fileList) {
if (file.raw) {
const reader = new FileReader()
reader.onload = (e) => {
file.url = e.target.result
}
reader.readAsDataURL(file.raw)
}
// 延迟设置fileList确保file.url已正确设置
setTimeout(() => {
this.fileList = fileList
}, 100)
},
removeImage(index) {
this.fileList.splice(index, 1)
this.$message.success('已删除文件')
},
// 处理手图标点击事件
// 处理手图标点击事件
handleHandClick(result, index) {
// 判断是分组模式还是普通模式
if (
typeof result === 'object' &&
result.hasOwnProperty('groupIndex') &&
result.hasOwnProperty('imageIndex')
) {
// 分组模式
const { groupIndex, imageIndex, key } = result
console.log('点击了分组图片:', groupIndex, imageIndex)
// 检查是否已有其他未确认分组的选中项
const hasOtherUnconfirmedGroupSelection = Object.keys(
this.selectedImages,
).some((k) => {
if (k.includes('-')) {
const [otherGroupIndex, otherImageIndex] = k
.split('-')
.map(Number)
// 获取其他分组的信息
const otherGroup =
this.groupedImageResults[otherGroupIndex]
// 只有当其他分组未确认且与当前分组不同时,才限制选择
if (
otherGroup &&
otherGroup.isSure !== '1' &&
otherGroupIndex !== groupIndex &&
this.selectedImages[k]
) {
return true
}
}
return false
})
// 如果已有其他未确认分组的选中项,提示用户
if (hasOtherUnconfirmedGroupSelection) {
this.$message.warning(
'当前版本不支持跨组选择,请先取消其他组的选择',
)
return
}
if (this.selectedImages[key]) {
console.log('取消选中分组图片:', groupIndex, imageIndex)
this.$set(this.selectedImages, key, false)
} else {
console.log('选中分组图片:', groupIndex, imageIndex)
this.$set(this.selectedImages, key, true)
}
} else if (
typeof result === 'object' &&
result.hasOwnProperty('index') &&
result.isSingle
) {
// 普通模式
const { index } = result
console.log('点击了图片:', index)
if (this.selectedImages[index]) {
console.log('取消选中:', index)
this.$set(this.selectedImages, index, false)
} else {
console.log('选中:', index)
this.$set(this.selectedImages, index, true)
}
} else {
// 兼容旧的处理方式
console.log(result)
console.log('点击了图片:', index)
if (this.selectedImages[index]) {
console.log('取消选中:', index)
this.$set(this.selectedImages, index, false)
} else {
console.log('选中:', index)
this.$set(this.selectedImages, index, true)
}
}
},
async generateTestData(recordId) {
try {
const res = await getImageListDetailsAPI({
id: recordId,
operaType: 1,
})
if (res.code === 200 && res.data && Array.isArray(res.data)) {
this.addId = res.data[0].operaId
console.log('addId:', this.addId)
// 按下标分组存储数据
this.groupedImageResults = res.data.map(
(record, index) => ({
index: index, // 下标
operaName: record.operaName,
images: record.fileVoList.map((item) => ({
url: item.bjUrl,
originUrl: item.imageUrl,
id: item.imageId || item.id,
name: item.originalName,
contentImage: item.contentImage,
fileSize: item.fileSize,
operId: record.id,
})),
totalCount: record.imageNum || 0,
recognizedCount: record.bzNum || 0,
unrecognizedCount: record.wbzNum || 0,
isSure: record.isSure,
operId: record.operaId,
id: record.id,
remark: record.remark,
}),
)
// 初始化选中状态 - 根据 isActive 字段
this.initializeSelectedImages(res.data)
// 设置第一条记录的状态用于界面显示
this.isSure = res.data[0]?.isSure || ''
}
} catch (error) {
console.error('获取列表异常:', error)
}
},
// 获取选中图片的ID和operId
getSelectedImageInfo() {
const selectedImageInfo = []
// 遍历选中的图片
for (let key in this.selectedImages) {
if (this.selectedImages[key]) {
// 判断是分组模式还是普通模式
if (key.includes('-')) {
// 分组模式: key格式为 "groupIndex-imageIndex"
const [groupIndex, imageIndex] = key
.split('-')
.map(Number)
const group = this.groupedImageResults[groupIndex]
if (group && group.images[imageIndex]) {
selectedImageInfo.push({
imageId: group.images[imageIndex].id,
operId: group.operId,
imageName: group.images[imageIndex].name,
})
}
} else {
// 普通模式: key是数字索引
const index = parseInt(key)
if (this.imageResults[index]) {
selectedImageInfo.push({
imageId: this.imageResults[index].id,
operId: this.imageResults[index].operId,
imageName: this.imageResults[index].name,
})
}
}
}
}
return selectedImageInfo
},
confirmResults(id) {
console.log('确认结果', id)
const selectedInfo = this.getSelectedImageInfo()
if (selectedInfo.length === 0) {
// this.$message.warning('请至少选择一张图片');
// return;
this.handleConfirm(this.addId, null, id)
} else {
// 获取图片ID数组
const imageIds = selectedInfo.map((item) => item.imageId)
// 获取operId假设所有选中图片属于同一组使用第一个
const operId = selectedInfo[0].operId
console.log('选中的图片IDs:', imageIds)
console.log('操作ID:', operId)
// 调用确认接口
this.handleConfirm(operId, imageIds, id)
}
},
async handleConfirm(operId, imageIds, id) {
try {
// 调用后端接口
const res = await updateImageSureAPI({
operaId: operId,
imageId: imageIds,
id: id,
})
if (res.code === 200) {
this.$message.success('确认成功')
// 使用 nextTick 确保 DOM 更新后再延迟
this.$nextTick(() => {
setTimeout(async () => {
await this.generateTestData(this.addId)
}, 8000) // 延迟0.8秒
})
} else {
this.$message.error('确认失败')
}
} catch (error) {
console.error('确认异常:', error)
this.$message.error('确认过程中出现错误')
}
},
toggleSidebar() {
this.isSidebarVisible = !this.isSidebarVisible
},
initializeSelectedImages(records) {
// 清空当前选中状态
this.selectedImages = {}
// 遍历所有分组和图片
records.forEach((record, groupIndex) => {
if (record.fileVoList && Array.isArray(record.fileVoList)) {
record.fileVoList.forEach((fileItem, imageIndex) => {
// 如果 isActive 为 "1",则默认选中
if (fileItem.isActive === '1') {
const key = `${groupIndex}-${imageIndex}`
this.$set(this.selectedImages, key, true)
}
})
}
})
},
updateImageAnnotation() {
this.generateTestData(this.selectedItem)
},
},
}
</script>
<style scoped>
.image-captioning-container {
display: flex;
overflow: hidden;
height: calc(100vh - 84px);
width: 100%;
background: url('../../../assets/images/imageCaptioning/login-bg-background.png')
no-repeat center center;
background-size: 100% 100%;
}
.main-content {
flex: 1;
padding: 40px;
overflow-y: auto;
}
.add-content {
height: 55%;
width: 60%;
margin-top: 15%;
margin-left: 22%;
}
.welcome-message {
text-align: center;
font-size: 20px;
margin-bottom: 50px;
font-weight: 600;
color: #334249;
font-style: normal;
text-transform: none;
}
</style>