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

692 lines
17 KiB
Vue
Raw Normal View History

2025-07-30 09:13:52 +08:00
<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>
2025-07-30 09:13:52 +08:00
<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>
2025-07-30 09:13:52 +08:00
<a-form-item label="目标组件">
<a-select
v-model="form.componentId"
:options="componentOptions"
placeholder="请选择组件(可选)"
allow-clear
/>
</a-form-item>
2025-07-30 09:13:52 +08:00
<a-form-item label="上传用户">
<a-input v-model="form.uploadUser" placeholder="请输入上传用户" />
</a-form-item>
2025-07-30 09:13:52 +08:00
<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>
2025-07-30 09:13:52 +08:00
<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="标注类型">
2025-07-30 09:13:52 +08:00
<a-select
v-model="form.annotationTypes"
:options="defectTypeOptions"
placeholder="选择要自动标注的缺陷类型"
multiple
/>
</a-form-item>
</a-form>
2025-07-30 09:13:52 +08:00
<!-- 文件上传区域 -->
<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>
2025-07-30 09:13:52 +08:00
<div class="upload-text">
<p>点击或拖拽图像文件到此区域</p>
<p class="upload-hint">支持 JPGPNGJPEG 格式可同时选择多个文件</p>
</div>
</div>
</template>
</a-upload>
</div>
2025-07-30 09:13:52 +08:00
<!-- 文件列表 -->
<div v-if="fileList.length > 0" class="file-list">
2025-07-30 09:13:52 +08:00
<div class="list-header">
<h4>待导入文件 ({{ fileList.length }})</h4>
<a-button type="text" @click="clearFiles">
<template #icon>
<IconDelete />
2025-07-30 09:13:52 +08:00
</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 />
2025-07-30 09:13:52 +08:00
</template>
</a-button>
</div>
</div>
</div>
</div>
2025-07-30 09:13:52 +08:00
<!-- 导入进度 -->
<div v-if="importing" class="import-progress">
2025-07-30 09:13:52 +08:00
<a-progress
:percent="importProgress"
:status="importStatus"
size="large"
/>
<p class="progress-text">{{ progressText }}</p>
</div>
2025-07-30 09:13:52 +08:00
<!-- 导入结果 -->
<div v-if="importResult" class="import-result">
2025-07-30 09:13:52 +08:00
<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">
2025-07-30 09:13:52 +08:00
<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'
2025-07-30 09:13:52 +08:00
import { Message } from '@arco-design/web-vue'
import {
IconClose,
IconDelete,
IconUpload,
2025-07-30 09:13:52 +08:00
} from '@arco-design/web-vue/es/icon'
import {
getImageSources,
getProjectTree,
importImages,
2025-07-30 09:13:52 +08:00
} from '@/apis/industrial-image'
import type {
ImageImportParams,
2025-07-30 09:13:52 +08:00
IndustrialImage,
ProjectTreeNode,
2025-07-30 09:13:52 +08:00
} 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[],
2025-07-30 09:13:52 +08:00
})
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 }>>([])
2025-07-30 09:13:52 +08:00
const loadingImageSources = ref(false)
// 计算属性
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value),
2025-07-30 09:13:52 +08:00
})
const componentOptions = computed(() => {
const findComponents = (nodes: ProjectTreeNode[]): Array<{ label: string, value: string }> => {
const options: Array<{ label: string, value: string }> = []
nodes.forEach((node) => {
2025-07-30 09:13:52 +08:00
if (node.type === 'component' || node.type === 'blade' || node.type === 'tower') {
options.push({
label: node.name,
value: node.id,
2025-07-30 09:13:52 +08:00
})
}
if (node.children) {
options.push(...findComponents(node.children))
}
})
2025-07-30 09:13:52 +08:00
return options
}
2025-07-30 09:13:52 +08:00
return form.value.projectId ? findComponents(projectTree.value) : []
})
const defectTypeOptions = computed(() => {
return defectTypes.value.map((type) => ({
2025-07-30 09:13:52 +08:00
label: type.name,
value: type.id,
2025-07-30 09:13:52 +08:00
}))
})
const imageSourceOptions = computed(() => {
return imageSources.value.map((source) => ({
2025-07-30 09:13:52 +08:00
label: source.name,
value: source.code,
2025-07-30 09:13:52 +08:00
}))
})
// 方法
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) => {
2025-07-30 09:13:52 +08:00
if (!file.type.startsWith('image/')) {
Message.warning(`文件 ${file.name} 不是图像文件`)
return
}
2025-07-30 09:13:52 +08:00
if (file.size > 10 * 1024 * 1024) { // 10MB
Message.warning(`文件 ${file.name} 大小超过10MB`)
return
}
2025-07-30 09:13:52 +08:00
const reader = new FileReader()
reader.onload = (e) => {
const fileItem: FileItem = {
file,
name: file.name,
size: file.size,
preview: e.target?.result as string,
2025-07-30 09:13:52 +08:00
}
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]}`
2025-07-30 09:13:52 +08:00
}
const handleImport = async () => {
if (!form.value.imageSource) {
Message.warning('请选择图像来源')
return
}
2025-07-30 09:13:52 +08:00
if (!form.value.projectId) {
Message.warning('请选择目标项目')
return
}
2025-07-30 09:13:52 +08:00
if (fileList.value.length === 0) {
Message.warning('请选择要导入的图像文件')
return
}
2025-07-30 09:13:52 +08:00
importing.value = true
importProgress.value = 0
importStatus.value = 'normal'
progressText.value = '正在导入图像...'
importResult.value = null
2025-07-30 09:13:52 +08:00
try {
const files = fileList.value.map((item) => item.file)
2025-07-30 09:13:52 +08:00
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,
2025-07-30 09:13:52 +08:00
}
2025-07-30 09:13:52 +08:00
// 模拟进度更新
const progressInterval = setInterval(() => {
if (importProgress.value < 90) {
importProgress.value += 10
}
}, 200)
2025-07-30 09:13:52 +08:00
const res = await importImages(files, params)
2025-07-30 09:13:52 +08:00
clearInterval(progressInterval)
importProgress.value = 100
importStatus.value = 'success'
progressText.value = '导入完成!'
2025-07-30 09:13:52 +08:00
// 注意真实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(),
2025-07-30 09:13:52 +08:00
})),
failed: [],
2025-07-30 09:13:52 +08:00
}
2025-07-30 09:13:52 +08:00
importResult.value = mockResult
emit('importSuccess', mockResult)
2025-07-30 09:13:52 +08:00
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
}
2025-07-30 09:13:52 +08:00
resetForm()
visible.value = false
}
const resetForm = () => {
form.value = {
imageSource: '',
projectId: '',
componentId: '',
uploadUser: '',
altitude: '',
latitude: '',
longitude: '',
settings: [],
annotationTypes: [],
2025-07-30 09:13:52 +08:00
}
fileList.value = []
importResult.value = null
importProgress.value = 0
importStatus.value = 'normal'
progressText.value = ''
}
const getResultTitle = () => {
if (!importResult.value) return ''
2025-07-30 09:13:52 +08:00
const { success, failed } = importResult.value
if (failed.length === 0) {
return `导入成功!共导入 ${success.length} 个图像文件`
} else {
return `导入完成!成功 ${success.length} 个,失败 ${failed.length}`
}
}
const getResultDescription = () => {
if (!importResult.value) return ''
2025-07-30 09:13:52 +08:00
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;
}
2025-07-30 09:13:52 +08:00
.upload-section {
margin: 20px 0;
}
2025-07-30 09:13:52 +08:00
.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;
2025-07-30 09:13:52 +08:00
&:hover {
border-color: #1890ff;
background: #f0f8ff;
}
}
2025-07-30 09:13:52 +08:00
.upload-drag-icon {
margin-bottom: 16px;
color: #999;
}
2025-07-30 09:13:52 +08:00
.upload-text {
text-align: center;
2025-07-30 09:13:52 +08:00
p {
margin: 0;
2025-07-30 09:13:52 +08:00
&.upload-hint {
margin-top: 8px;
font-size: 12px;
color: #999;
}
}
}
2025-07-30 09:13:52 +08:00
.file-list {
margin-top: 20px;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
}
2025-07-30 09:13:52 +08:00
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
2025-07-30 09:13:52 +08:00
h4 {
margin: 0;
font-size: 14px;
font-weight: 500;
}
}
2025-07-30 09:13:52 +08:00
.list-content {
max-height: 300px;
overflow-y: auto;
}
2025-07-30 09:13:52 +08:00
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
2025-07-30 09:13:52 +08:00
&:last-child {
border-bottom: none;
}
}
2025-07-30 09:13:52 +08:00
.file-info {
display: flex;
align-items: center;
flex: 1;
}
2025-07-30 09:13:52 +08:00
.file-preview {
width: 40px;
height: 40px;
margin-right: 12px;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e8e8e8;
2025-07-30 09:13:52 +08:00
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
2025-07-30 09:13:52 +08:00
.file-details {
flex: 1;
}
2025-07-30 09:13:52 +08:00
.file-name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
2025-07-30 09:13:52 +08:00
.file-size {
font-size: 12px;
color: #999;
}
2025-07-30 09:13:52 +08:00
.import-progress {
margin-top: 20px;
text-align: center;
}
2025-07-30 09:13:52 +08:00
.progress-text {
margin-top: 8px;
font-size: 14px;
color: #666;
}
2025-07-30 09:13:52 +08:00
.import-result {
margin-top: 20px;
}
2025-07-30 09:13:52 +08:00
.result-details {
margin-top: 16px;
2025-07-30 09:13:52 +08:00
h4 {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
}
2025-07-30 09:13:52 +08:00
.failed-list {
max-height: 150px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 6px;
}
2025-07-30 09:13:52 +08:00
.failed-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
2025-07-30 09:13:52 +08:00
&:last-child {
border-bottom: none;
}
}
2025-07-30 09:13:52 +08:00
.filename {
font-size: 12px;
color: #333;
}
2025-07-30 09:13:52 +08:00
.error {
font-size: 12px;
color: #ff4d4f;
}
}
</style>