@@ -49,8 +49,15 @@ const emits = defineEmits<{
4949
5050const images = defineModel <string []>(' modelValue' , { required: true })
5151
52+ interface ImageItem {
53+ key: string
54+ src: string
55+ }
56+
5257const activeUploadCount = ref (0 )
5358const uploadProgress = ref (0 )
59+ const imageItemSeed = shallowRef (0 )
60+ const imageItems = ref <ImageItem []>([])
5461const containerRef = useTemplateRef <HTMLElement >(' containerRef' )
5562const fileInputRef = useTemplateRef <HTMLInputElement >(' fileInputRef' )
5663const isHoveringContainer = ref (false )
@@ -63,6 +70,35 @@ const canHandlePaste = computed(() =>
6370 && (isHoveringContainer .value || isContainerFocused .value ),
6471)
6572
73+ watch (images , (currentImages ) => {
74+ const reusableItems = new Map <string , ImageItem []>()
75+ imageItems .value .forEach ((item ) => {
76+ const itemsForSrc = reusableItems .get (item .src )
77+ if (itemsForSrc ) {
78+ itemsForSrc .push (item )
79+ }
80+ else {
81+ reusableItems .set (item .src , [item ])
82+ }
83+ })
84+
85+ imageItems .value = currentImages .map ((src ) => {
86+ const reusableItem = reusableItems .get (src )?.shift ()
87+ if (reusableItem ) {
88+ return reusableItem
89+ }
90+
91+ imageItemSeed .value += 1
92+ return {
93+ key: ` fa-image-upload-item-${imageItemSeed .value } ` ,
94+ src ,
95+ }
96+ })
97+ }, {
98+ deep: true ,
99+ immediate: true ,
100+ })
101+
66102function normalizeExt(ext ? : string ) {
67103 if (! ext ) {
68104 return ' '
@@ -231,6 +267,20 @@ function onMove(index: number, direction: 'forward' | 'backward') {
231267 images .value [index ] = images .value .splice (index + 1 , 1 , images .value [index ])[0 ]
232268 }
233269}
270+
271+ function onBeforeLeave(el : Element ) {
272+ if (! (el instanceof HTMLElement ) || ! (el .parentElement instanceof HTMLElement )) {
273+ return
274+ }
275+
276+ const elementRect = el .getBoundingClientRect ()
277+ const listRect = el .parentElement .getBoundingClientRect ()
278+
279+ el .style .left = ` ${elementRect .left - listRect .left }px `
280+ el .style .top = ` ${elementRect .top - listRect .top }px `
281+ el .style .width = ` ${elementRect .width }px `
282+ el .style .height = ` ${elementRect .height }px `
283+ }
234284 </script >
235285
236286<template >
@@ -243,14 +293,19 @@ function onMove(index: number, direction: 'forward' | 'backward') {
243293 @focusin =" onContainerFocusIn"
244294 @focusout =" onContainerFocusOut"
245295 >
246- <div class =" flex flex-wrap gap-2" >
296+ <TransitionGroup
297+ tag =" div"
298+ name =" fa-image-upload-list"
299+ class =" fa-image-upload-list flex flex-wrap gap-2"
300+ @before-leave =" onBeforeLeave"
301+ >
247302 <div
248- v-for =" (img , index) in images " :key =" img " class =" group/image-upload border rounded-lg flex items-center justify-center relative overflow-hidden" :style =" {
303+ v-for =" (item , index) in imageItems " :key =" item.key " class =" fa-image-upload-item group/image-upload border rounded-lg flex items-center justify-center relative overflow-hidden" :style =" {
249304 width: `${props.width}px`,
250305 height: `${props.height}px`,
251306 }"
252307 >
253- <img :src =" img " class =" h-full w-full object-contain" >
308+ <img :src =" item.src " class =" h-full w-full object-contain" >
254309 <div v-if =" !props.disabled" class =" text-white p-2 rounded-lg bg-black/50 opacity-0 grid grid-cols-2 transition-opacity inset-0 place-items-center absolute group-hover/image-upload:opacity-100" >
255310 <div class =" opacity-60 flex-center cursor-pointer transition-all hover:(opacity-100 scale-110)" @click.prevent =" onPreview(index)" >
256311 <FaIcon name =" i-icon-park-outline:preview-open" class =" size-6" />
@@ -300,7 +355,7 @@ function onMove(index: number, direction: 'forward' | 'backward') {
300355 </template >
301356 <input ref =" fileInputRef" type =" file" accept =" image/*" :multiple =" props.multiple" class =" hidden" @change =" onSelectFile" >
302357 </button >
303- </div >
358+ </TransitionGroup >
304359 <div v-if =" !props.hideTips" class =" text-xs text-card-foreground/50 flex flex-wrap gap-1 empty:hidden" >
305360 <div v-if =" props.dimension" class =" after:content-[';_'] last:after:content-empty" >
306361 建议尺寸为 {{ props.dimension.width }}*{{ props.dimension.height }}
@@ -320,3 +375,39 @@ function onMove(index: number, direction: 'forward' | 'backward') {
320375 </div >
321376 </div >
322377</template >
378+
379+ <style scoped>
380+ .fa-image-upload-list {
381+ --fa-image-upload-ease : cubic-bezier (0.22 , 1 , 0.36 , 1 );
382+
383+ position : relative ;
384+ }
385+
386+ .fa-image-upload-item {
387+ transform-origin : center ;
388+ }
389+
390+ .fa-image-upload-list-enter-active ,
391+ .fa-image-upload-list-leave-active {
392+ transition :
393+ opacity 180ms var (--fa-image-upload-ease ),
394+ transform 240ms var (--fa-image-upload-ease );
395+ }
396+
397+ .fa-image-upload-list-move {
398+ transition : transform 240ms var (--fa-image-upload-ease );
399+ will-change : transform;
400+ }
401+
402+ .fa-image-upload-list-enter-from ,
403+ .fa-image-upload-list-leave-to {
404+ opacity : 0 ;
405+ transform : scale (0.94 );
406+ }
407+
408+ .fa-image-upload-list-leave-active {
409+ position : absolute ;
410+ z-index : 2 ;
411+ pointer-events : none ;
412+ }
413+ </style >
0 commit comments