Industrial-image-management.../src/components/ImageImport/index.vue

692 lines
17 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-import">
<a-modal
v-model:visible="visible"
title="导入图像"
width="600px"
:confirm-loading="importing"
@ok="handleImport"
@cancel="handleCancel"
>
<div class="import-content">
<!-- 选择项目和组件 -->
<a-form :model="form" layout="vertical">
<a-form-item label="图像来源" required>
<a-select
v-model="form.imageSource"
:options="imageSourceOptions"
placeholder="请选择图像来源"
:loading="loadingImageSources"
/>
</a-form-item>
<a-form-item label="目标项目" required>
<a-tree-select
v-model="form.projectId"
:data="projectTree"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
placeholder="请选择项目"
tree-checkable
:tree-check-strictly="true"
@change="onProjectChange"
/>
</a-form-item>
<a-form-item label="目标组件">
<a-select
v-model="form.componentId"
:options="componentOptions"
placeholder="请选择组件(可选)"
allow-clear
/>
</a-form-item>
<a-form-item label="上传用户">
<a-input v-model="form.uploadUser" placeholder="请输入上传用户" />
</a-form-item>
<a-form-item label="位置信息">
<a-row :gutter="16">
<a-col :span="8">
<a-input v-model="form.altitude" placeholder="海拔" />
</a-col>
<a-col :span="8">
<a-input v-model="form.latitude" placeholder="纬度" />
</a-col>
<a-col :span="8">
<a-input v-model="form.longitude" placeholder="经度" />
</a-col>
</a-row>
</a-form-item>
<a-form-item label="导入设置">
<a-checkbox-group v-model="form.settings">
<a-checkbox value="autoAnnotate">导入后自动标注</a-checkbox>
<a-checkbox value="overwrite">覆盖同名文件</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item v-if="form.settings.includes('autoAnnotate')" label="标注类型">
<a-select
v-model="form.annotationTypes"
:options="defectTypeOptions"
placeholder="选择要自动标注的缺陷类型"
multiple
/>
</a-form-item>
</a-form>
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
ref="uploadRef"
:custom-request="() => {}"
:show-file-list="false"
accept="image/*"
multiple
@change="handleFileChange"
>
<template #upload-button>
<div class="upload-area">
<div class="upload-drag-icon">
<IconUpload size="48" />
</div>
<div class="upload-text">
<p>点击或拖拽图像文件到此区域</p>
<p class="upload-hint">支持 JPG、PNG、JPEG 格式,可同时选择多个文件</p>
</div>
</div>
</template>
</a-upload>
</div>
<!-- 文件列表 -->
<div v-if="fileList.length > 0" class="file-list">
<div class="list-header">
<h4>待导入文件 ({{ fileList.length }})</h4>
<a-button type="text" @click="clearFiles">
<template #icon>
<IconDelete />
</template>
清空
</a-button>
</div>
<div class="list-content">
<div
v-for="(file, index) in fileList"
:key="index"
class="file-item"
>
<div class="file-info">
<div class="file-preview">
<img :src="file.preview" alt="preview" />
</div>
<div class="file-details">
<div class="file-name">{{ file.name }}</div>
<div class="file-size">{{ formatFileSize(file.size) }}</div>
</div>
</div>
<div class="file-actions">
<a-button
type="text"
size="small"
@click="removeFile(index)"
>
<template #icon>
<IconClose />
</template>
</a-button>
</div>
</div>
</div>
</div>
<!-- 导入进度 -->
<div v-if="importing" class="import-progress">
<a-progress
:percent="importProgress"
:status="importStatus"
size="large"
/>
<p class="progress-text">{{ progressText }}</p>
</div>
<!-- 导入结果 -->
<div v-if="importResult" class="import-result">
<a-alert
:type="importResult.failed.length > 0 ? 'warning' : 'success'"
:title="getResultTitle()"
:description="getResultDescription()"
show-icon
/>
<div v-if="importResult.failed.length > 0" class="result-details">
<h4>失败文件列表:</h4>
<div class="failed-list">
<div
v-for="failed in importResult.failed"
:key="failed.filename"
class="failed-item"
>
<span class="filename">{{ failed.filename }}</span>
<span class="error">{{ failed.error }}</span>
</div>
</div>
</div>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconClose,
IconDelete,
IconUpload,
} from '@arco-design/web-vue/es/icon'
import {
getImageSources,
getProjectTree,
importImages,
} from '@/apis/industrial-image'
import type {
ImageImportParams,
IndustrialImage,
ProjectTreeNode,
} from '@/apis/industrial-image/type'
interface Props {
visible: boolean
}
interface Emits {
(e: 'update:visible', visible: boolean): void
(e: 'importSuccess', result: { success: IndustrialImage[], failed: any[] }): void
}
interface FileItem {
file: File
name: string
size: number
preview: string
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 响应式数据
const importing = ref(false)
const importProgress = ref(0)
const importStatus = ref<'normal' | 'success' | 'warning' | 'danger'>('normal')
const progressText = ref('')
const importResult = ref<{ success: IndustrialImage[], failed: any[] } | null>(null)
const form = ref({
imageSource: '',
projectId: '',
componentId: '',
uploadUser: '',
altitude: '',
latitude: '',
longitude: '',
settings: [] as string[],
annotationTypes: [] as string[],
})
const fileList = ref<FileItem[]>([])
const projectTree = ref<ProjectTreeNode[]>([])
const defectTypes = ref<Array<{ id: string, name: string, description?: string, color?: string }>>([])
const imageSources = ref<Array<{ id: string, name: string, code: string }>>([])
const loadingImageSources = ref(false)
// 计算属性
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
})
const componentOptions = computed(() => {
const findComponents = (nodes: ProjectTreeNode[]): Array<{ label: string, value: string }> => {
const options: Array<{ label: string, value: string }> = []
nodes.forEach((node) => {
if (node.type === 'component' || node.type === 'blade' || node.type === 'tower') {
options.push({
label: node.name,
value: node.id,
})
}
if (node.children) {
options.push(...findComponents(node.children))
}
})
return options
}
return form.value.projectId ? findComponents(projectTree.value) : []
})
const defectTypeOptions = computed(() => {
return defectTypes.value.map((type) => ({
label: type.name,
value: type.id,
}))
})
const imageSourceOptions = computed(() => {
return imageSources.value.map((source) => ({
label: source.name,
value: source.code,
}))
})
// 方法
const loadProjectTree = async () => {
try {
const res = await getProjectTree()
projectTree.value = res.data
} catch (error) {
console.error('加载项目树失败:', error)
}
}
// const loadDefectTypes = async () => {
// try {
// const res = await getDefectTypes()
// defectTypes.value = res.data
// } catch (error) {
// console.error('加载缺陷类型失败:', error)
// }
// }
const loadImageSources = async () => {
loadingImageSources.value = true
try {
const res = await getImageSources()
if (res.data && res.data.length > 0 && res.data[0].data) {
imageSources.value = res.data[0].data
if (imageSources.value.length > 0) {
form.value.imageSource = imageSources.value[0].code
}
}
} catch (error) {
console.error('加载图像来源失败:', error)
} finally {
loadingImageSources.value = false
}
}
const onProjectChange = (value: string) => {
form.value.componentId = ''
}
const handleFileChange = (fileList: any) => {
const files = Array.from(fileList.target?.files || []) as File[]
files.forEach((file) => {
if (!file.type.startsWith('image/')) {
Message.warning(`文件 ${file.name} 不是图像文件`)
return
}
if (file.size > 10 * 1024 * 1024) { // 10MB
Message.warning(`文件 ${file.name} 大小超过10MB`)
return
}
const reader = new FileReader()
reader.onload = (e) => {
const fileItem: FileItem = {
file,
name: file.name,
size: file.size,
preview: e.target?.result as string,
}
fileList.value.push(fileItem)
}
reader.readAsDataURL(file)
})
}
const removeFile = (index: number) => {
fileList.value.splice(index, 1)
}
const clearFiles = () => {
fileList.value = []
}
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
}
const handleImport = async () => {
if (!form.value.imageSource) {
Message.warning('请选择图像来源')
return
}
if (!form.value.projectId) {
Message.warning('请选择目标项目')
return
}
if (fileList.value.length === 0) {
Message.warning('请选择要导入的图像文件')
return
}
importing.value = true
importProgress.value = 0
importStatus.value = 'normal'
progressText.value = '正在导入图像...'
importResult.value = null
try {
const files = fileList.value.map((item) => item.file)
const params: ImageImportParams = {
imageSource: form.value.imageSource,
projectId: form.value.projectId,
componentId: form.value.componentId || undefined,
uploadUser: form.value.uploadUser || undefined,
altitude: form.value.altitude || undefined,
latitude: form.value.latitude || undefined,
longitude: form.value.longitude || undefined,
autoAnnotate: form.value.settings.includes('autoAnnotate'),
annotationTypes: form.value.settings.includes('autoAnnotate') ? form.value.annotationTypes : undefined,
}
// 模拟进度更新
const progressInterval = setInterval(() => {
if (importProgress.value < 90) {
importProgress.value += 10
}
}, 200)
const res = await importImages(files, params)
clearInterval(progressInterval)
importProgress.value = 100
importStatus.value = 'success'
progressText.value = '导入完成!'
// 注意真实API返回的数据结构可能不同需要根据实际情况调整
const mockResult = {
success: files.map((file, index) => ({
id: `img_${Date.now()}_${index}`,
name: file.name,
path: `uploaded/${file.name}`,
size: file.size,
type: file.type,
projectId: form.value.projectId,
componentId: form.value.componentId,
createTime: new Date().toISOString(),
})),
failed: [],
}
importResult.value = mockResult
emit('importSuccess', mockResult)
Message.success(`成功导入 ${files.length} 个图像文件`)
} catch (error) {
console.error('导入失败:', error)
importProgress.value = 100
importStatus.value = 'danger'
progressText.value = '导入失败!'
Message.error('导入图像失败')
} finally {
importing.value = false
}
}
const handleCancel = () => {
if (importing.value) {
Message.warning('正在导入中,请稍后再试')
return
}
resetForm()
visible.value = false
}
const resetForm = () => {
form.value = {
imageSource: '',
projectId: '',
componentId: '',
uploadUser: '',
altitude: '',
latitude: '',
longitude: '',
settings: [],
annotationTypes: [],
}
fileList.value = []
importResult.value = null
importProgress.value = 0
importStatus.value = 'normal'
progressText.value = ''
}
const getResultTitle = () => {
if (!importResult.value) return ''
const { success, failed } = importResult.value
if (failed.length === 0) {
return `导入成功!共导入 ${success.length} 个图像文件`
} else {
return `导入完成!成功 ${success.length} 个,失败 ${failed.length} 个`
}
}
const getResultDescription = () => {
if (!importResult.value) return ''
const { success, failed } = importResult.value
if (failed.length === 0) {
return '所有图像文件都已成功导入到指定项目中'
} else {
return `部分文件导入失败,请检查失败原因并重新导入`
}
}
// 监听器
watch(visible, (newValue) => {
if (newValue) {
loadProjectTree()
// loadDefectTypes()
loadImageSources()
} else {
resetForm()
}
})
</script>
<style scoped lang="scss">
.image-import {
.import-content {
max-height: 70vh;
overflow-y: auto;
}
.upload-section {
margin: 20px 0;
}
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 120px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
transition: all 0.3s;
cursor: pointer;
&:hover {
border-color: #1890ff;
background: #f0f8ff;
}
}
.upload-drag-icon {
margin-bottom: 16px;
color: #999;
}
.upload-text {
text-align: center;
p {
margin: 0;
&.upload-hint {
margin-top: 8px;
font-size: 12px;
color: #999;
}
}
}
.file-list {
margin-top: 20px;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
h4 {
margin: 0;
font-size: 14px;
font-weight: 500;
}
}
.list-content {
max-height: 300px;
overflow-y: auto;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.file-info {
display: flex;
align-items: center;
flex: 1;
}
.file-preview {
width: 40px;
height: 40px;
margin-right: 12px;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e8e8e8;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.file-details {
flex: 1;
}
.file-name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.file-size {
font-size: 12px;
color: #999;
}
.import-progress {
margin-top: 20px;
text-align: center;
}
.progress-text {
margin-top: 8px;
font-size: 14px;
color: #666;
}
.import-result {
margin-top: 20px;
}
.result-details {
margin-top: 16px;
h4 {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
}
.failed-list {
max-height: 150px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 6px;
}
.failed-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.filename {
font-size: 12px;
color: #333;
}
.error {
font-size: 12px;
color: #ff4d4f;
}
}
</style>