示例
效果和传统的组件差不多,用到了 startViewTransition
和 sortablejs
前者用于实现动画,后者用于实现拖拽排序
startViewTransition
and
然后,早期看到过很多博客的主题交替做的十分不错,像这种
其本质上是 css
动画和 startViewTransition
结合所达到的效果
但是 startViewTransition
能干的事还不止于此,具体可以在这里看到
组件定义
index.ts
import { h } from 'vue'
import Component from './component.vue'
type DefineProps = {
limit?: number
module?: string
modelValue?: string[]
'onUpdate:modelValue'?: (val: string[]) => void
}
export default (params: DefineProps) => {
return h(Component, params)
}
component.vue
<template>
<div class="flex gap-[10px] flex-wrap" ref="parent">
<div
id="image_choose_button"
style="view-transition-name: item-choose-btn"
v-if="!$props.readonly && (!$props.limit || images.length < $props.limit)"
class="h-[100px] w-[100px] bg-gray-500/10 relative rounded-[4px] grid place-content-center"
>
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512" fill="rgb(107 114 128 / 0.3)">
<path
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM232 344V280H168c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H280v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"
/>
</svg>
<input type="file" class="absolute inset-0 opacity-0" accept="image/*" @change="choose" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import ImageViewer from '@/components/ImageViewer'
import Sortable from 'sortablejs'
const props = defineProps<{
modelValue?: string[]
limit?: number
module?: string
move?: boolean
readonly?: boolean
}>()
const emits = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
let sortable: Sortable | null = null
const parent = ref()
const loading = ref<boolean>(false)
const images = ref<string[]>(props.modelValue || [])
const childArr: HTMLElement[] = []
const choose = (e: Event) => {
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
loading.value = true
const url = URL.createObjectURL(file)
images.value = [...images.value, url]
const item = createItem(url)
childArr.push(item)
emits('update:modelValue', images.value)
document.startViewTransition(() => {
parent.value?.insertBefore(item, parent.value.lastChild)
})
loading.value = false
}
const remove = (url: string) => {
images.value = images.value.filter((item) => item !== url)
emits('update:modelValue', images.value)
}
const createSortable = (el: HTMLElement) => {
sortable = Sortable.create(el, {
animation: 300,
filter: '#image_choose_button',
onMove: function (evt) {
return evt.related.id !== 'image_choose_button'
},
onEnd: function (evt) {
const values = sortable?.toArray() || []
values.pop()
images.value = values
emits('update:modelValue', images.value)
},
})
}
const childrenHandle = () => {
images.value.forEach((url, index) => {
const item = createItem(url, index)
childArr.push(item)
})
parent.value?.prepend(...childArr)
}
const generateName = (): string => {
const prefix = Math.floor(Math.random() * 100000)
.toString()
.padStart(5, '0')
const suffix = Date.now().toString().slice(-5)
return prefix + suffix
}
const createItem = (url: string, index: number = childArr.length + 1) => {
const div = document.createElement('div')
div.className = 'h-[100px] w-[100px] bg-gray-500/10 relative rounded-[4px] overflow-hidden border'
div.dataset.id = url
div.style.setProperty('view-transition-name', `item-${generateName()}`)
const img = document.createElement('img')
img.src = url
img.className = 'h-full w-full object-cover'
img.onclick = () => ImageViewer.show(url)
div.appendChild(img)
const btn = document.createElement('div')
btn.className = 'absolute right-[5px] top-[5px] flex items-center justify-center drop-shadow-lg text-white'
btn.addEventListener('click', () => {
document.startViewTransition(() => {
div.remove()
})
remove(url)
})
const svg = `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512" fill="#FFFFFF"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/></svg>`
btn.innerHTML = svg
div.appendChild(btn)
return div
}
onMounted(() => {
if (props.move) {
const el = parent.value as HTMLElement
createSortable(el)
}
childrenHandle()
})
</script>
这个组件其实也纠结了一番,sortablejs
这个库是有提供 vue
组件的,但是用起来效果并不好,于是就引入了原库,不过原库和 vue
配合使用时也存在一些问题,因为其拖拽会直接改变 dom
结构,一番折腾下来并无卵用,索性直接用了原生js 😋
空空如也!