vue3+ts+vuedraggable,实现拖拽版的羊了个羊

815 阅读2分钟

最近刚好了解低代码平台,顺手写了一个拖拽版羊了个羊。第二关写的有点难了,也是卡自己的bug才过关了

一、实现拖拽效果

利用vuedraggable实现拖拽效果,官方文档:github.com/SortableJS/…

主要props:

  • group 保持一致既可相互拖拽
  • sort 表示在组内能否拖拽改变顺序

拖拽demo:

<template>
  <div>
    <!-- 第一个数据组 -->
    <draggable class="group" :list="arr1" item-key="index" group="person" :sort="false">
      <template #item="{ element }">
        <div>{{element}}</div>
      </template>
    </draggable>
    <!-- 第二个数据组 -->
    <draggable class="group" :list="arr2" group="person" @change="endDrag" item-key="index" :sort="false">
      <template #item="{ element }">
        <div>{{element}}</div>
      </template>
    </draggable>
  </div>
</template>
<script lang="ts">
  import { defineComponent, reactive, toRefs } from 'vue'
  import draggable from 'vuedraggable'
  interface Date {
    arr1: string[]
    arr2: string[]
  }
  export default defineComponent({
    components: { draggable },
    setup() {
      const state = reactive<Date>({
        arr1: ['张三', '李四', '王五', '赵六'],
        arr2: [],
      })
      // arr2的数据发生变化时的回调
      const endDrag = () => {
        console.log('arr1拖到arr2,拖拽完成')
      }
      return {
        ...toRefs(state),
        endDrag
      }
    }
  })
</script>

效果:

二、生成icon数据

  • 生成icon需要3个相同的才能被消除
  • 根据光卡不同,生成icon数量不同
  • 每个icon的位置是随机的
// 定义一个icon的接口类型
interface oneIcon {
  id: number
  icon: string
  path: string
  x: number
  y: number
  pos: number
  isCover: boolean
}
// 获取icon图片
const getIconPath = (icon: string): string => {
  return require(`@/assets/img/${icon}.png`)
}
// 获取随机数字
const getNumber = function (min: number, max: number): number {
  return Math.round(Math.random() * (max - min) + min)
}
// 生成数据,参数为关卡
export const getList = (level: number) => {
  const list: Array<oneIcon> = []
  const iconList: string[] = ['fruit1', 'fruit2', 'fruit3', 'fruit4', 'fruit5', 'fruit6', 'fruit7', 'fruit8', 'fruit9', 'fruit10']
  let i = level === 1 ? 3 : 6 // 需要是3的倍数才能消除,第一关不重复
  while (i--) {
    // 根据关卡不同,选择不同数量的icon,这里采用指数,使关卡难度陡增
    iconList.slice(0, 3 ** (level ** 2)).forEach(icon => {
      list.push({
        id: Math.random(), // 随机一个id
        icon,// icon名字,后续用于消除对比
        path: getIconPath(icon),// 图片的src
        // 以下数据可根据实际情况设置,我的每个icon大小是50x50,容器大小是250x250
        x: getNumber(0, 4 * 50),    // left,x轴上打乱些,可以增加难度
        y: getNumber(0, 4 * 2) * 25, // top,让y轴上排列整齐些,结果均是:0,25,50,75...
        pos: getNumber(0, 4), // z-index
        isCover: false // 是否被覆盖了,默认都没有被覆盖
      })
    })
  }
  return list
}

三、设置遮挡效果

  • 通过设置每个icon上的isCover,控制遮挡效果,被覆盖的元素不能拖拽
  • 通过碰撞算法检测是否有遮挡
  • 通过数组的index和pos确定哪一个元素在上或下

1. 检查是否覆盖

// 检测是否覆盖
export const checkCover = (list: Array<oneIcon>): void => {
  const len = list.length
  // 先将所有的icon设置为不覆盖
  list.forEach(item => {
    item.isCover = false
  })
  // 双层循环,两两对比
  for (let i = 0; i < len - 1; i++) {
    const curcat = list[i]
    for (let j = i + 1; j < len; j++) {
      const otherCat = list[j]
      // 50是icon的大小
      if (!(curcat.x >= otherCat.x + 50 || otherCat.x >= curcat.x + 50 || curcat.y >= otherCat.y + 50 || otherCat.y >= curcat.y + 50)) {
        // 有重叠
        if (curcat.pos > otherCat.pos) {
          // 判断z-index,较小的被覆盖
          otherCat.isCover = true
        } else {
          // curcat的index小于otherCat的index,所以curcat被覆盖
          curcat.isCover = true
        }
      }
    }
  }
}

2. 设置被覆盖的元素不能被拖拽

  • filter过滤不能被拖拽的元素
<!-- 被覆盖的不能拖拽 -->
<draggable
  :list="iconArray"
  item-key="id"
  group="icon"
  :sort="false"
  filter=".cover"   
>
  <template #item="{ element }">
    <div
      :style="{
        top: element.y + 'px',
        left: element.x + 'px',
        zIndex: element.pos
      }"
      :class="{ cover: element.isCover }"
    >
			<!-- 被覆盖增加一个半透明蒙层 -->
      <div class="iscover" v-if="element.isCover"></div>
      <img :src="element.path" alt="" />
    </div>
  </template>
</draggable>

四、消除

  • 监听每次槽内的数据变化,执行消除回调
  • 判断数组相邻的三个元素是否相等,进行消除

1. 消除回调

export const mergeCat = (list: Array<oneIcon>): void => {
  let i = 0
  while (i < list.length - 2) {
    // 判断相邻三个元素是否相同
    if (list[i].icon === list[i + 1].icon && list[i + 1].icon === list[i + 2].icon) {
      list.splice(i, 3)
    } else {
      i++
    }
  }
}

2. 监听数据变化

  • @change="endDrag",监听变化
<draggable
  class="res-group"
  :list="iconArray2"
  group="icon"
  @change="endDrag"
  item-key="id"
  :sort="false"
>
  <template #item="{ element }">
    <div class="item">
      <img :src="element.path" alt="" />
    </div>
  </template>
</draggable>
...
const endDrag = () => {
  // 重新计算元素是否覆盖
  checkCover(state.iconArray)
  // 槽内元素小于3,不进行消除
  const len = state.iconArray2.length
  if (len < 3) return
  // 消除
  mergeIcon(state.iconArray2)
  // 槽内元素大于7,游戏结束
  if (state.iconArray2.length >= 7) {
    context.emit('gameOver')
  }
  // 容器内没有元素了,过关了
  if (state.iconArray.length === 0) {
    context.emit('complete')
  }
}

五、完整代码

github.com/Abner105/ca…