增加备注

This commit is contained in:
BianLzhaoMin 2025-11-25 17:16:35 +08:00
parent 5ed6e787bc
commit c7ac05a8ac
3 changed files with 947 additions and 871 deletions

View File

@ -1,279 +1,282 @@
<template> <template>
<div class="upload-container"> <div class="upload-container">
<div class="upload-area"> <div class="upload-area">
<div class="upload-tip"> <div class="upload-tip">
<!-- 上传提示或图片预览 --> <!-- 上传提示或图片预览 -->
<div v-if="fileList.length === 0" class="upload-placeholder"> <div v-if="fileList.length === 0" class="upload-placeholder">
<div class="upload-text">上传图片</div> <div class="upload-text">上传图片</div>
</div> </div>
<!-- 文件预览区域 --> <!-- 文件预览区域 -->
<div v-else class="file-preview-container"> <div v-else class="file-preview-container">
<div <div
class="file-item" class="file-item"
v-for="(file, index) in fileList" v-for="(file, index) in fileList"
:key="index" :key="index"
> >
<!-- 图片文件预览 --> <!-- 图片文件预览 -->
<img <img
v-if="isImageFile(file.name)" v-if="isImageFile(file.name)"
:src="file.url" :src="file.url"
alt="" alt=""
class="preview-image" class="preview-image"
> />
<!-- 压缩包文件预览 --> <!-- 压缩包文件预览 -->
<div <div v-else class="zip-preview" :title="file.name">
v-else <i class="el-icon-document"></i>
class="zip-preview" <div class="file-name">{{ file.name }}</div>
:title="file.name" </div>
>
<i class="el-icon-document"></i> <i
<div class="file-name">{{ file.name }}</div> class="el-icon-close delete-icon"
@click="$emit('remove-image', index)"
></i>
</div>
</div>
</div> </div>
<i class="el-icon-close delete-icon" @click="$emit('remove-image', index)"></i> <div class="upload-actions">
</div> <el-upload
action=""
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
accept="image/*,.zip,.rar,.7z"
multiple
:limit="uploadLimit"
:on-exceed="handleExceed"
:show-file-list="false"
>
<el-button type="primary">
<i class="el-icon-upload"></i>
</el-button>
</el-upload>
<el-button
type="primary"
@click="$emit('start-upload')"
:disabled="
fileList.length === 0 || selectedTag.length === 0
"
>
开始标注
</el-button>
</div>
</div> </div>
</div>
<div class="upload-actions">
<el-upload
action=""
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
accept="image/*,.zip,.rar,.7z"
multiple
:limit="uploadLimit"
:on-exceed="handleExceed"
:show-file-list="false"
>
<el-button type="primary">
<i class="el-icon-upload"></i>
</el-button>
</el-upload>
<el-button
type="primary"
@click="$emit('start-upload')"
:disabled="fileList.length === 0 || selectedTag.length === 0"
>
开始标注
</el-button>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "FileUploader", name: 'FileUploader',
props: { props: {
fileList: { fileList: {
type: Array, type: Array,
default: () => [] default: () => [],
},
selectedTag: {
type: Array,
default: () => [],
},
}, },
selectedTag: { computed: {
type: Array, uploadLimit() {
default: () => [] const hasZip = this.fileList.some((file) =>
} this.isCompressedFile(file.name),
}, )
computed: { return hasZip ? 1 : 50
uploadLimit() { },
const hasZip = this.fileList.some(file => this.isCompressedFile(file.name));
return hasZip ? 1 : 50;
}
},
methods: {
isImageFile(filename) {
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const extension = filename.split('.').pop().toLowerCase();
return imageExtensions.includes(extension);
}, },
methods: {
isImageFile(filename) {
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
const extension = filename.split('.').pop().toLowerCase()
return imageExtensions.includes(extension)
},
handleFileChange(file, fileList) { handleFileChange(file, fileList) {
const isZip = this.isCompressedFile(file.name); const isZip = this.isCompressedFile(file.name)
const hasImage = fileList.some(f => this.isImageFile(f.name)); const hasImage = fileList.some((f) => this.isImageFile(f.name))
const hasZip = fileList.some(f => this.isCompressedFile(f.name)); const hasZip = fileList.some((f) => this.isCompressedFile(f.name))
// //
if (isZip && hasImage) { if (isZip && hasImage) {
this.$message.warning('不能同时上传压缩包'); this.$message.warning('不能同时上传压缩包')
return false; return false
} }
if (this.isImageFile(file.name) && hasZip) { if (this.isImageFile(file.name) && hasZip) {
this.$message.warning('不能同时上传图片'); this.$message.warning('不能同时上传图片')
return false; return false
} }
if (file.raw) { if (file.raw) {
const reader = new FileReader(); const reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
file.url = e.target.result; file.url = e.target.result
}; }
reader.readAsDataURL(file.raw); reader.readAsDataURL(file.raw)
} }
// fileListfile.url // fileListfile.url
setTimeout(() => { setTimeout(() => {
this.$emit('file-change', file, fileList); this.$emit('file-change', file, fileList)
}, 100); }, 100)
},
isCompressedFile(filename) {
const compressedExtensions = ['zip', 'rar', '7z']
const extension = filename.split('.').pop().toLowerCase()
return compressedExtensions.includes(extension)
},
handleExceed(files, fileList) {
const hasZip = fileList.some((f) => this.isCompressedFile(f.name))
if (hasZip) {
this.$message.warning('压缩包最多只能上传1个')
} else {
this.$message.warning(`最多只能上传50张图片`)
}
},
}, },
isCompressedFile(filename) {
const compressedExtensions = ['zip', 'rar', '7z'];
const extension = filename.split('.').pop().toLowerCase();
return compressedExtensions.includes(extension);
},
handleExceed(files, fileList) {
const hasZip = fileList.some(f => this.isCompressedFile(f.name));
if (hasZip) {
this.$message.warning('压缩包最多只能上传1个');
} else {
this.$message.warning(`最多只能上传50张图片`);
}
}
}
} }
</script> </script>
<style scoped> <style scoped>
.upload-container { .upload-container {
margin-top: 20px; margin-top: 20px;
text-align: center; text-align: center;
} }
.upload-area { .upload-area {
background-color: #fff; background-color: #fff;
border: 1px solid #dcdfe6; border: 1px solid #dcdfe6;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 0 auto; margin: 0 auto;
height: 220px; height: 220px;
position: relative; position: relative;
} }
.upload-tip { .upload-tip {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
} }
.upload-text { .upload-text {
font-size: 14px; font-size: 14px;
color: #999; color: #999;
margin-bottom: 10px; margin-bottom: 10px;
} }
.upload-actions { .upload-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: 10px;
position: absolute; position: absolute;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
} }
.upload-placeholder { .upload-placeholder {
text-align: center; text-align: center;
color: #999; color: #999;
font-size: 14px; font-size: 14px;
} }
.file-preview-container { .file-preview-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 20px;
padding: 10px 0; padding: 10px 0;
width: 100%; width: 100%;
height: 100px; height: 100px;
overflow-x: auto; overflow-x: auto;
} }
.file-item { .file-item {
position: relative; position: relative;
width: 80px; width: 80px;
height: 80px; height: 80px;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #f5f7fa; background-color: #f5f7fa;
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.file-item:hover { .file-item:hover {
transform: scale(1.05); transform: scale(1.05);
} }
.preview-image { .preview-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
border-radius: 4px; border-radius: 4px;
transition: filter 0.3s ease; transition: filter 0.3s ease;
} }
.preview-image:hover { .preview-image:hover {
filter: brightness(1.1); filter: brightness(1.1);
} }
.zip-preview { .zip-preview {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #666; color: #666;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
} }
.zip-preview .el-icon-document { .zip-preview .el-icon-document {
font-size: 24px; font-size: 24px;
color: #999; color: #999;
margin-bottom: 4px; margin-bottom: 4px;
} }
.file-name { .file-name {
font-size: 10px; font-size: 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 60px; max-width: 60px;
} }
.delete-icon { .delete-icon {
position: absolute; position: absolute;
top: 0px; top: 0px;
right: 0px; right: 0px;
background-color: rgba(255, 0, 0, 0.8); background-color: rgba(255, 0, 0, 0.8);
color: white; color: white;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
transition: all 0.3s; transition: all 0.3s;
z-index: 1; z-index: 1;
} }
.delete-icon:hover { .delete-icon:hover {
background-color: rgba(255, 0, 0, 1); background-color: rgba(255, 0, 0, 1);
transform: scale(1.1); transform: scale(1.1);
} }
</style> </style>

View File

@ -1,118 +1,133 @@
<template> <template>
<div class="history-content"> <div class="history-content">
<!-- 可滚动区域 --> <!-- 可滚动区域 -->
<div class="scrollable-content"> <div class="scrollable-content">
<ImageResults <ImageResults
:upload-info="uploadInfo" :upload-info="uploadInfo"
:image-results="imageResults" :image-results="imageResults"
:grouped-image-results="groupedImageResults" :grouped-image-results="groupedImageResults"
:selected-images="selectedImages" :selected-images="selectedImages"
:is-sure="isSure" :is-sure="isSure"
@hand-click="handleHandClickFromChild" @hand-click="handleHandClickFromChild"
@confirm-results="$emit('confirm-results', $event)" @confirm-results="$emit('confirm-results', $event)"
/> />
</div> </div>
<!-- 固定在底部的区域 --> <!-- 固定在底部的区域 -->
<div class="fixed-bottom-area"> <div class="fixed-bottom-area">
<!-- 标签选择区域 --> <!-- 标签选择区域 -->
<TagSelector <TagSelector
:tags="tags" :tags="tags"
:visible-tags="visibleTags" :visible-tags="visibleTags"
:selected-tag="selectedTag" :selected-tag="selectedTag"
@toggle-tag="$emit('toggle-tag', $event)" @toggle-tag="$emit('toggle-tag', $event)"
@toggle-all-tags="$emit('toggle-all-tags')" @toggle-all-tags="$emit('toggle-all-tags')"
/> />
<!-- 图片上传区域 --> <!-- 输入备注 -->
<FileUploader <el-input
:file-list="fileList" :rows="1"
:selected-tag="selectedTag" placeholder="请输入备注"
@file-change="(file, fileList) => $emit('file-change', file, fileList)" type="textarea"
@remove-image="(index) => $emit('remove-image', index)" v-model="remarkInfo"
@start-upload="$emit('start-upload')" />
/>
<!-- 图片上传区域 -->
<FileUploader
:file-list="fileList"
:selected-tag="selectedTag"
@file-change="
(file, fileList) => $emit('file-change', file, fileList)
"
@remove-image="(index) => $emit('remove-image', index)"
@start-upload="$emit('start-upload')"
/>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
import ImageResults from './ImageResults.vue'; import ImageResults from './ImageResults.vue'
import TagSelector from './TagSelector.vue'; import TagSelector from './TagSelector.vue'
import FileUploader from './FileUploader.vue'; import FileUploader from './FileUploader.vue'
import axios from 'axios' import axios from 'axios'
import {getToken} from '@/utils/auth' import { getToken } from '@/utils/auth'
export default { export default {
name: "HistoryView", name: 'HistoryView',
components: { components: {
ImageResults, ImageResults,
TagSelector, TagSelector,
FileUploader FileUploader,
},
props: {
uploadInfo: {
type: String,
default: ''
}, },
imageResults: { data() {
type: Array, return {
default: () => [] remarkInfo: '', //
}
}, },
groupedImageResults: { props: {
type: Array, uploadInfo: {
default: () => [] type: String,
default: '',
},
imageResults: {
type: Array,
default: () => [],
},
groupedImageResults: {
type: Array,
default: () => [],
},
selectedImages: {
type: Object,
default: () => ({}),
},
tags: {
type: Array,
default: () => [],
},
visibleTags: {
type: Array,
default: () => [],
},
selectedTag: {
type: Array,
default: () => [],
},
fileList: {
type: Array,
default: () => [],
},
isSure: {
type: String,
default: '',
},
}, },
selectedImages: { methods: {
type: Object, handleHandClickFromChild(result, index) {
default: () => ({}) this.$emit('hand-click', result, index)
},
}, },
tags: {
type: Array,
default: () => []
},
visibleTags: {
type: Array,
default: () => []
},
selectedTag: {
type: Array,
default: () => []
},
fileList: {
type: Array,
default: () => []
},
isSure: {
type: String,
default: ''
}
},
methods: {
handleHandClickFromChild(result, index) {
this.$emit('hand-click', result, index);
}
}
} }
</script> </script>
<style scoped> <style scoped>
.history-content { .history-content {
height: 100%; height: 100%;
width: 60%; width: 60%;
margin-left: 22%; margin-left: 22%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.scrollable-content { .scrollable-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.fixed-bottom-area { .fixed-bottom-area {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
padding-top: 20px; padding-top: 20px;
} }
</style> </style>

File diff suppressed because it is too large Load Diff