331 lines
7.2 KiB
Vue
331 lines
7.2 KiB
Vue
<template>
|
|
<div class="video-image-uploader">
|
|
<div class="image-list">
|
|
<div
|
|
v-for="(item, index) in localPreviewList"
|
|
:key="item.uid"
|
|
class="image-item"
|
|
>
|
|
<img :src="item.url" class="uploaded-image" :alt="getFrameLabel(index)" />
|
|
<div class="frame-label">{{ getFrameLabel(index) }}</div>
|
|
<div class="delete-icon" @click.stop="handleDelete(index)">
|
|
<i-ep-close />
|
|
</div>
|
|
</div>
|
|
<template v-for="i in maxImages" :key="i">
|
|
<div
|
|
v-if="localPreviewList.length < maxImages && !localPreviewList[i - 1]"
|
|
class="upload-trigger"
|
|
@click="triggerUpload(i - 1)"
|
|
>
|
|
<i-ep-plus color="#333333" />
|
|
<div class="upload-text">{{ getUploadText(i) }}</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<el-upload
|
|
v-for="i in maxImages"
|
|
v-show="false"
|
|
:key="i"
|
|
:ref="el => setUploadRef(el, i - 1)"
|
|
:action="uploadurl"
|
|
:limit="1"
|
|
:before-upload="beforeUpload"
|
|
:on-success="(response, uploadFile) => handleSuccess(response, uploadFile, i - 1)"
|
|
:on-error="handleError"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { genFileId } from 'element-plus'
|
|
|
|
const props = defineProps({
|
|
modelType: {
|
|
type: String,
|
|
default: 'text'
|
|
},
|
|
imagesCount: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
modelValue: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
|
|
const uploadRefs = ref([])
|
|
const imageList = ref([...props.modelValue])
|
|
const localPreviewList = ref([...props.modelValue])
|
|
const isUploading = ref(false)
|
|
|
|
const showEndFrame = computed(() => props.modelType === 'image' && props.imagesCount === 2)
|
|
const maxImages = computed(() => props.modelType === 'image' ? (showEndFrame.value ? 2 : 1) : 1)
|
|
|
|
const setUploadRef = (el, index) => {
|
|
if (el) {
|
|
uploadRefs.value[index] = el
|
|
}
|
|
}
|
|
|
|
const getFrameLabel = (index) => {
|
|
if (props.modelType === 'digitalHuman') {
|
|
return ''
|
|
}
|
|
return index === 0 ? '首帧' : '尾帧'
|
|
}
|
|
|
|
const getUploadText = (i) => {
|
|
if (props.modelType === 'digitalHuman') {
|
|
return '参考内容'
|
|
}
|
|
return i === 1 ? '首帧' : '尾帧'
|
|
}
|
|
|
|
watch(() => props.modelValue, async (newVal) => {
|
|
if (isUploading.value) {
|
|
return
|
|
}
|
|
|
|
imageList.value = [...newVal]
|
|
|
|
const newPreviewList = []
|
|
for (const img of newVal) {
|
|
let previewImg = { ...img }
|
|
if (img.url && !img.url.startsWith('blob:')) {
|
|
try {
|
|
const response = await fetch(img.url)
|
|
const blob = await response.blob()
|
|
previewImg = {
|
|
...img,
|
|
url: URL.createObjectURL(blob),
|
|
serverUrl: img.url
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create blob URL:', error)
|
|
previewImg = {
|
|
...img,
|
|
serverUrl: img.url
|
|
}
|
|
}
|
|
}
|
|
newPreviewList.push(previewImg)
|
|
}
|
|
localPreviewList.value = newPreviewList
|
|
}, { deep: true, immediate: true })
|
|
|
|
const triggerUpload = (index) => {
|
|
if (uploadRefs.value[index]) {
|
|
uploadRefs.value[index].$el.querySelector('input').click()
|
|
}
|
|
}
|
|
|
|
const beforeUpload = (rawFile) => {
|
|
const allowedTypes = ['image/jpeg', 'image/png']
|
|
|
|
if (!allowedTypes.includes(rawFile.type)) {
|
|
ElMessage.error('不支持的文件类型,请上传 JPG、PNG 格式的图片')
|
|
return false
|
|
}
|
|
|
|
if (rawFile.size / 1024 / 1024 > 10) {
|
|
ElMessage.error('图片大小不能超过 10MB')
|
|
return false
|
|
}
|
|
}
|
|
|
|
const handleSuccess = (response, uploadFile, index) => {
|
|
ElMessage.success('上传成功')
|
|
|
|
isUploading.value = true
|
|
|
|
const localUrl = URL.createObjectURL(uploadFile.raw)
|
|
|
|
const newImage = {
|
|
uid: uploadFile.uid,
|
|
url: response.url
|
|
}
|
|
|
|
if (imageList.value[index]) {
|
|
const previewItem = localPreviewList.value[index]
|
|
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(previewItem.url)
|
|
}
|
|
imageList.value[index] = newImage
|
|
localPreviewList.value[index] = {
|
|
uid: uploadFile.uid,
|
|
url: localUrl,
|
|
serverUrl: response.url
|
|
}
|
|
} else {
|
|
imageList.value[index] = newImage
|
|
localPreviewList.value[index] = {
|
|
uid: uploadFile.uid,
|
|
url: localUrl,
|
|
serverUrl: response.url
|
|
}
|
|
}
|
|
|
|
emit('update:modelValue', [...imageList.value])
|
|
|
|
nextTick(() => {
|
|
isUploading.value = false
|
|
})
|
|
}
|
|
|
|
const handleError = () => {
|
|
ElMessage.error('上传失败,请重新选择图片')
|
|
}
|
|
|
|
const handleDelete = (index) => {
|
|
const previewItem = localPreviewList.value[index]
|
|
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(previewItem.url)
|
|
}
|
|
|
|
localPreviewList.value.splice(index, 1)
|
|
imageList.value.splice(index, 1)
|
|
emit('update:modelValue', [...imageList.value])
|
|
}
|
|
|
|
const handleSelect = async (url, index = 0) => {
|
|
const initialFile = await fetch(url)
|
|
const blob = await initialFile.blob()
|
|
const file = new File([blob], `selected_image_${Date.now()}.jpg`, { type: blob.type })
|
|
file.uid = genFileId()
|
|
|
|
if (uploadRefs.value[index]) {
|
|
uploadRefs.value[index].handleStart(file)
|
|
uploadRefs.value[index].submit()
|
|
}
|
|
}
|
|
|
|
defineExpose({
|
|
handleSelect
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.video-image-uploader {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.image-list {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.image-item {
|
|
position: relative;
|
|
width: 56px;
|
|
height: 56px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
|
|
&:hover {
|
|
transform: scale(1.1);
|
|
z-index: 10;
|
|
|
|
.delete-icon {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.uploaded-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
transition: all 0.3s ease;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.frame-label {
|
|
position: absolute;
|
|
bottom: 4px;
|
|
right: 4px;
|
|
width: 32px;
|
|
height: 20px;
|
|
background: rgba(0, 15, 51, 0.8);
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
z-index: 5;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.delete-icon {
|
|
position: absolute;
|
|
top: -6px;
|
|
right: -6px;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: #ef4444;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: all 0.3s ease;
|
|
z-index: 20;
|
|
|
|
&:hover {
|
|
background: #dc2626;
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
}
|
|
|
|
.upload-trigger {
|
|
width: 56px;
|
|
height: 56px;
|
|
background: rgba(0, 15, 51, 0.04);
|
|
backdrop-filter: blur(5px);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
cursor: pointer;
|
|
color: #9ca3af;
|
|
font-size: 15px;
|
|
transition: all 0.3s ease;
|
|
|
|
span {
|
|
color: #333;
|
|
font-family: "Microsoft YaHei";
|
|
font-size: 12px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
}
|
|
|
|
&:hover {
|
|
background-color: #F0F1F2;
|
|
border-color: #626aef;
|
|
transform: scale(1.05);
|
|
}
|
|
}
|
|
|
|
.upload-text {
|
|
color: #666;
|
|
font-family: "Microsoft YaHei";
|
|
font-size: 12px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
}
|
|
</style>
|