529 lines
13 KiB
Vue
529 lines
13 KiB
Vue
<template>
|
|
<div class="industrial-image-list" :class="{ 'collapsed': isCollapsed }">
|
|
<div class="header-actions" v-if="!isCollapsed">
|
|
<slot name="header-left">
|
|
<a-button v-if="showImportButton" type="primary" @click="handleImportImages">
|
|
<template #icon><icon-upload /></template>
|
|
导入图像
|
|
</a-button>
|
|
</slot>
|
|
<div class="search-bar" v-if="showSearch">
|
|
<a-input-search
|
|
v-model="searchKeyword"
|
|
placeholder="输入关键字搜索"
|
|
allow-clear
|
|
@search="handleSearch"
|
|
@clear="handleSearchClear"
|
|
/>
|
|
</div>
|
|
<slot name="header-right"></slot>
|
|
<div class="collapse-button">
|
|
<a-button
|
|
type="text"
|
|
@click="toggleCollapse"
|
|
>
|
|
<template #icon>
|
|
<icon-up />
|
|
</template>
|
|
收起
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-grid" v-show="!isCollapsed">
|
|
<div v-if="imageList.length === 0" class="empty-data">
|
|
<icon-image class="empty-icon" />
|
|
<p>{{ emptyText }}</p>
|
|
</div>
|
|
|
|
<div v-else class="image-thumbnails">
|
|
<div
|
|
v-for="image in imageList"
|
|
:key="image.imageId"
|
|
class="thumbnail-item"
|
|
:class="{ active: selectedImageId === image.imageId }"
|
|
@click="handleImageSelect(image)"
|
|
>
|
|
<div class="thumbnail-image">
|
|
<img
|
|
:src="getImageUrl(image.imagePath)"
|
|
:alt="image.imageName"
|
|
@error="handleImageError"
|
|
@load="handleImageLoad"
|
|
/>
|
|
<div class="image-placeholder" v-if="!image.imagePath">
|
|
<icon-image />
|
|
<span>暂无图像</span>
|
|
</div>
|
|
<div class="thumbnail-overlay">
|
|
<div class="image-info">
|
|
<p class="image-name">{{ image.imageName }}</p>
|
|
<p class="image-size">{{ formatFileSize(image.size || 0) }}</p>
|
|
</div>
|
|
<div class="image-actions">
|
|
<a-button v-if="showPreviewAction" type="text" size="small" @click.stop="handleImagePreview(image)">
|
|
<icon-eye />
|
|
</a-button>
|
|
<a-button v-if="showProcessAction" type="text" size="small" @click.stop="handleImageProcess(image)">
|
|
<icon-settings />
|
|
</a-button>
|
|
<a-button v-if="showDeleteAction" type="text" size="small" status="danger" @click.stop="handleImageDelete(image)">
|
|
<icon-delete />
|
|
</a-button>
|
|
<slot name="item-actions" :image="image"></slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="thumbnail-info">
|
|
<p class="thumbnail-name">{{ image.imageName }}</p>
|
|
<div class="thumbnail-meta">
|
|
<span class="image-type">{{ image.imageType?.toUpperCase() || 'IMAGE' }}</span>
|
|
<span v-if="image.defectCount" class="defect-count">缺陷: {{ image.defectCount }}</span>
|
|
<slot name="item-meta" :image="image"></slot>
|
|
</div>
|
|
<div class="thumbnail-extra" v-if="image.partName || image.shootingTime">
|
|
<span v-if="image.partName" class="part-name">{{ image.partName }}</span>
|
|
<span v-if="image.shootingTime" class="capture-time">{{ formatTime(image.shootingTime) }}</span>
|
|
</div>
|
|
<slot name="item-extra" :image="image"></slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 收起状态下的展开按钮 -->
|
|
<div v-if="isCollapsed" class="expand-button-container">
|
|
<a-button
|
|
type="primary"
|
|
@click="toggleCollapse"
|
|
>
|
|
<template #icon>
|
|
<icon-down />
|
|
</template>
|
|
展开图像列表
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import {
|
|
IconUpload,
|
|
IconImage,
|
|
IconEye,
|
|
IconSettings,
|
|
IconDelete,
|
|
IconUp,
|
|
IconDown
|
|
} from '@arco-design/web-vue/es/icon'
|
|
|
|
export interface IndustrialImage {
|
|
imageId: string
|
|
imageName: string
|
|
imagePath?: string
|
|
imageType?: string
|
|
size?: number
|
|
defectCount?: number
|
|
partName?: string
|
|
shootingTime?: string
|
|
[key: string]: any // 允许其他属性
|
|
}
|
|
|
|
const props = defineProps({
|
|
imageList: {
|
|
type: Array as () => IndustrialImage[],
|
|
default: () => []
|
|
},
|
|
selectedImageId: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
baseUrl: {
|
|
type: String,
|
|
default: 'http://pms.dtyx.net:9158'
|
|
},
|
|
emptyText: {
|
|
type: String,
|
|
default: '暂无图像数据'
|
|
},
|
|
showImportButton: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showSearch: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showPreviewAction: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showProcessAction: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showDeleteAction: {
|
|
type: Boolean,
|
|
default: true
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
importImages: []
|
|
imageSelect: [image: IndustrialImage]
|
|
imagePreview: [image: IndustrialImage]
|
|
imageProcess: [image: IndustrialImage]
|
|
imageDelete: [image: IndustrialImage]
|
|
search: [keyword: string]
|
|
collapsedChange: [collapsed: boolean]
|
|
}>()
|
|
|
|
const searchKeyword = ref('')
|
|
const isCollapsed = ref(false)
|
|
|
|
const handleImportImages = () => {
|
|
emit('importImages')
|
|
}
|
|
|
|
const handleImageSelect = (image: IndustrialImage) => {
|
|
emit('imageSelect', image)
|
|
}
|
|
|
|
const handleImagePreview = (image: IndustrialImage) => {
|
|
emit('imagePreview', image)
|
|
}
|
|
|
|
const handleImageProcess = (image: IndustrialImage) => {
|
|
emit('imageProcess', image)
|
|
}
|
|
|
|
const handleImageDelete = (image: IndustrialImage) => {
|
|
emit('imageDelete', image)
|
|
}
|
|
|
|
const handleSearch = () => {
|
|
emit('search', searchKeyword.value)
|
|
}
|
|
|
|
const handleSearchClear = () => {
|
|
searchKeyword.value = ''
|
|
emit('search', '')
|
|
}
|
|
|
|
const toggleCollapse = () => {
|
|
isCollapsed.value = !isCollapsed.value
|
|
emit('collapsedChange', isCollapsed.value)
|
|
}
|
|
|
|
// 图片加载处理
|
|
const handleImageLoad = (event: Event) => {
|
|
const img = event.target as HTMLImageElement
|
|
img.style.display = 'block'
|
|
}
|
|
|
|
const handleImageError = (event: Event) => {
|
|
const img = event.target as HTMLImageElement
|
|
img.style.display = 'none'
|
|
console.warn('图片加载失败:', img.src)
|
|
}
|
|
|
|
// 获取完整的图片URL
|
|
const getImageUrl = (imagePath?: string): string => {
|
|
if (!imagePath) return ''
|
|
if (imagePath.startsWith('http')) return imagePath
|
|
return `${props.baseUrl}${imagePath}`
|
|
}
|
|
|
|
// 格式化文件大小
|
|
const formatFileSize = (size: number): string => {
|
|
if (size < 1024) return `${size} B`
|
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
|
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
|
}
|
|
|
|
// 格式化时间
|
|
const formatTime = (timeString: string): string => {
|
|
if (!timeString) return ''
|
|
try {
|
|
const date = new Date(timeString)
|
|
return date.toLocaleDateString('zh-CN', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
} catch {
|
|
return timeString
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.industrial-image-list {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: white;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
transition: height 0.3s ease;
|
|
position: relative;
|
|
|
|
&.collapsed {
|
|
flex: 0 0 auto;
|
|
height: auto;
|
|
min-height: 0;
|
|
max-height: 0;
|
|
overflow: visible;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
padding: 10px 16px;
|
|
justify-content: space-between;
|
|
background-color: #ffffff;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: all 0.3s ease;
|
|
|
|
.search-bar {
|
|
width: 300px;
|
|
}
|
|
|
|
.collapse-button {
|
|
margin-left: auto;
|
|
}
|
|
}
|
|
|
|
/* 收起状态下的展开按钮容器 */
|
|
.expand-button-container {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 100;
|
|
}
|
|
|
|
.image-grid {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
height: calc(100% - 60px); /* 减去header-actions的高度 */
|
|
|
|
// 美化滚动条
|
|
&::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
|
|
&:hover {
|
|
background: #94a3b8;
|
|
}
|
|
}
|
|
|
|
.empty-data {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 200px;
|
|
color: #6b7280;
|
|
|
|
.empty-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
p {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
.image-thumbnails {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
|
|
.thumbnail-item {
|
|
cursor: pointer;
|
|
border: 2px solid transparent;
|
|
border-radius: 8px;
|
|
transition: all 0.3s ease;
|
|
|
|
&:hover {
|
|
border-color: #3b82f6;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px -8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
&.active {
|
|
border-color: #3b82f6;
|
|
background: #f0f9ff;
|
|
}
|
|
|
|
.thumbnail-image {
|
|
position: relative;
|
|
aspect-ratio: 4/3;
|
|
overflow: hidden;
|
|
border-radius: 6px;
|
|
background: #f8f9fa;
|
|
|
|
img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.image-placeholder {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f8f9fa;
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
|
|
.arco-icon {
|
|
font-size: 24px;
|
|
margin-bottom: 8px;
|
|
}
|
|
}
|
|
|
|
.thumbnail-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.7) 100%);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
padding: 12px;
|
|
color: white;
|
|
|
|
.image-info {
|
|
.image-name {
|
|
margin: 0 0 4px 0;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.image-size {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
opacity: 0.9;
|
|
}
|
|
}
|
|
|
|
.image-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
|
|
.arco-btn {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
color: white;
|
|
backdrop-filter: blur(4px);
|
|
|
|
&:hover {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
color: white;
|
|
}
|
|
|
|
&.arco-btn-status-danger:hover {
|
|
background: rgba(239, 68, 68, 0.8);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&:hover .thumbnail-overlay {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.thumbnail-info {
|
|
padding: 8px;
|
|
|
|
.thumbnail-name {
|
|
margin: 0 0 4px 0;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #1f2937;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.thumbnail-meta {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
|
|
.image-type {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
background: #f3f4f6;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.defect-count {
|
|
font-size: 12px;
|
|
color: #dc2626;
|
|
background: #fee2e2;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
|
|
.thumbnail-extra {
|
|
margin-top: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
|
|
.part-name {
|
|
font-size: 11px;
|
|
color: #059669;
|
|
background: #ecfdf5;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.capture-time {
|
|
font-size: 10px;
|
|
color: #6b7280;
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style> |