Skip to content

Commit 38e6aba

Browse files
committed
feat: FaImageUpload 组件增加入场、出场、排序动画
1 parent 1c81115 commit 38e6aba

4 files changed

Lines changed: 98 additions & 7 deletions

File tree

apps/example/src/types/components.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,4 @@ declare global {
185185
const FaTooltip: typeof import('@fantastic-admin/components')['FaTooltip']
186186
const FaTree: typeof import('@fantastic-admin/components')['FaTree']
187187
const FaTrend: typeof import('@fantastic-admin/components')['FaTrend']
188-
}
188+
}

apps/example/src/views/component_example/image_upload/_demo2.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function handleSuccess() {
1010
<FaImageUpload
1111
v-model="files"
1212
action="/fake/upload"
13-
:after-upload="(response) => response.data.url"
13+
:after-upload="(response) => `${response.data.url}?fake=${Math.random()}`"
1414
:max="2"
1515
@on-success="handleSuccess"
1616
/>

apps/example/src/views/component_example/image_upload/_demo3.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function handleSuccess() {
1010
<FaImageUpload
1111
v-model="files"
1212
action="/fake/upload"
13-
:after-upload="(response) => response.data.url"
13+
:after-upload="(response) => `${response.data.url}?fake=${Math.random()}`"
1414
:width="200"
1515
:height="130"
1616
:dimension="{ width: 400, height: 260 }"

packages/components/src/image-upload/index.vue

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,15 @@ const emits = defineEmits<{
4949
5050
const images = defineModel<string[]>('modelValue', { required: true })
5151
52+
interface ImageItem {
53+
key: string
54+
src: string
55+
}
56+
5257
const activeUploadCount = ref(0)
5358
const uploadProgress = ref(0)
59+
const imageItemSeed = shallowRef(0)
60+
const imageItems = ref<ImageItem[]>([])
5461
const containerRef = useTemplateRef<HTMLElement>('containerRef')
5562
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
5663
const 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+
66102
function 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

Comments
 (0)