🎋 Image Choose Component

    111

示例

预览 👀

效果和传统的组件差不多,用到了 startViewTransition 和 sortablejs
前者用于实现动画,后者用于实现拖拽排序

startViewTransition

link 🔗

and

然后,早期看到过很多博客的主题交替做的十分不错,像这种

其本质上是 css 动画和 startViewTransition 结合所达到的效果
但是 startViewTransition 能干的事还不止于此,具体可以在这里看到

link 🔗

组件定义

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 😋

消息盒子
# 您需要首次评论以获取消息 #
# 您需要首次评论以获取消息 #

只显示最新10条未读和已读信息