一起学Vue自定义组件之拼图小游戏

通过学习Vue自定义组件,可以开发一些小功能,自娱自乐,巩固学习的基础知识,本文以一个简单的拼图小游戏的例子,简述Vue自定义组件的开发,调用等基本流程,仅供学习分享使用,如有不足之处,还请指正。

涉及知识点

关于Vue组件的基础知识,前篇已有介绍,本例涉及知识点如下:

  • 拼图游戏,只有相邻的元素才可以交换位置,那如何判断两个元素相邻,方法如下:
    • 左右相邻:y轴坐标相同,x轴相减的绝对值等于一个元素的宽度。
    • 上下相邻:x轴坐标相同,y轴相减的绝对值等于一个元素的高度。
  • 如何判断拼图中的可以与之交换位置的空白,方法如下:
    • 通过ref引用属性,将空白属性,定义为empty,其他定义为block,以便区分。
  • 如何将一张图放到每一个元素上,并只显示一块内容,方法如下:
    • 将背景图的位置和元素的坐标起始位置关联起来,即将图片的向左上方平移即可。
  • 元素之间的切换平滑过渡。在本例中,通过css样式设置,所有元素的移动都在0.3s内完成,达到平滑过渡的效果。

示例效果图

本例中拼图游戏一共分5关,分别是3*3,4*4等,难度逐级增加,所用图片的均是500px*500px大小,如下图所示:

当拼图完成时,询问是否进行下一关,如下所示:

下一关,效果如下所示:

其他效果图类似,只是分的行和列递增,拼图难度增加,但是处理逻辑都是相同的。

核心源码

关于Puzzle.vue源码,如下所示:

模板部分(template),主要是元素的布局,本例采用v-for动态加载,如下所示:

<template>
  <div class="puzzle" :style="{width:width+'px',height:height+'px'}">
    <div
      v-for="(item,index) in blockPoints"
      :key="item.id"
      :style="{width:blockWidth+'px',
        height:blockHeight+'px',
        left:item.x+'px',top:item.y+'px',
        backgroundImage:`url(${img})`,
        backgroundPosition:`-${correctPoints[index].x}px -${correctPoints[index].y}px`,
        opacity: index===blockPoints.length-1 && 0 }"
      v-on:click="handleClick"
      class="puzzle__block"
      :ref="index===blockPoints.length-1?'empty':'block'"
      :data-correctX="correctPoints[index].x"
      :data-correctY="correctPoints[index].y"
    ></div>
  </div>
</template>

脚本部分(Script),主要用于逻辑的校验和判断,如下所示:

<script>
export default {
  props: {
    img: {
      // 图片路径
      type: String,
      required: true,
    },
    width: {
      // 图片总宽度
      type: Number,
      default: 500,
    },
    height: {
      // 图片总高度
      type: Number,
      default: 500,
    },
    row: {
      // 行数
      type: Number,
      default: 3,
    },
    col: {
      // 列数
      type: Number,
      default: 3,
    },
  },
  data() {
    return {
      status: {
        type: String,
        default: "进行中......",
      },
    };
  },
  methods: {
    handleClick(e) {
      const blockDom = e.target;
      const empthDom = this.$refs.empty[0];
      const { left, top } = blockDom.style;
      if (!this.isAdjacent(blockDom, empthDom)) {
        return;
      }
      //交换元素
      blockDom.style.left = empthDom.style.left;
      blockDom.style.top = empthDom.style.top;
      empthDom.style.left = left;
      empthDom.style.top = top;
      const winFlag = this.winCheck();
      if (winFlag) {
        //   console.log('success');
        this.winGame(empthDom);
      }
    },
    isAdjacent(blockDom, empthDom) {
      // 判断是否相邻
      const { left: blockLeft, top: blockTop, width, height } = blockDom.style;
      const { left: emptyLeft, top: emptyTop } = empthDom.style;
      const xDis = Math.floor(
        Math.abs(parseFloat(blockLeft) - parseFloat(emptyLeft))
      );
      const yDis = Math.floor(
        Math.abs(parseFloat(blockTop) - parseFloat(emptyTop))
      );
      const flag =
        (blockLeft === emptyLeft && yDis === parseInt(height)) ||
        (blockTop === emptyTop && xDis === parseInt(width));
      console.log(flag);
      return flag;
    },
    winCheck() {
      // 判断是否完成
      const blockDomArr = this.$refs.block;
      return blockDomArr.every((dom) => {
        const { left: domLeft, top: domTop } = dom.style;
        const { correctx: correctX, correcty: correctY } = dom.dataset;
        const flag =
          parseInt(domLeft) === parseInt(correctX) &&
          parseInt(domTop) === parseInt(correctY);
        return flag;
      });
      // console.log(blockDomArr.length);
    },
    winGame(empthDom) {
      //通关
      setTimeout(() => {
        this.status = "胜利";
        alert("恭喜通关");
        empthDom.style.opacity = 1;
        this.$emit("getStatus");
        setTimeout(() => {
          this.goToNextLevel();
        }, 300);
      }, 300);
    },
    goToNextLevel() {
      const answerFlag = window.confirm("现在进行下一关么?");
      if (answerFlag) {
        this.status = "进行中......";
        this.$emit("next");
      }
    },
  },
  computed: {
    blockWidth() {
      return this.width / this.col;
    },
    blockHeight() {
      return this.height / this.row;
    },
    correctPoints() {
      const { row, col, blockWidth, blockHeight } = this;
      const arr = [];
      for (let i = 0; i < row; i++) {
        for (let j = 0; j < col; j++) {
          arr.push({
            x: j * blockWidth,
            y: i * blockHeight,
            id: new Date().getTime() + Math.random() * 100,
          });
        }
      }
      return arr;
    },
    blockPoints() {
      const points = this.correctPoints;
      const length = points.length; //数组的长度
      const lastEle = points[length - 1]; //最后一个元素
      const newArr = [...points];
      newArr.length = length - 1;
      //打乱顺序
      newArr.sort(() => Math.random() - 0.5);
      newArr.push(lastEle);
      return newArr;
    },
  },
};
</script>

样式部分(Style),主要用于外观样式的设置,如下所示:

<style>
.puzzle {
  box-sizing: content-box;
  border: 2px solid #cccccc;
  position: relative;
}
.puzzle__block {
  border: 1px solid #ffffff;
  box-sizing: border-box;
  /* background-color: rebeccapurple; */
  position: absolute;
  transition: all 0.3s;
}
</style>

 拼图组件的调用App.vue

首先组件需要引入和注册,采用使用,如下所示:

<script>
import puzzle from "./Puzzle";
export default {
  components: {
    puzzle,
  },
  data() {
    return {
      level: 0,
      puzzleConfig: [
        { img: "./img/001.jpg", row: 3, col: 3 },
        { img: "./img/002.jpg", row: 4, col: 4 },
        { img: "./img/003.jpg", row: 5, col: 5 },
        { img: "./img/004.jpg", row: 6, col: 6 },
        { img: "./img/005.jpg", row: 7, col: 7 },
      ],
      status: "进行中......",
    };
  },
  methods: {
    handleNext() {
      console.log("next");
      this.status = this.$refs.dpuzzle.status;
      this.level++;
      if (this.level == this.puzzleConfig.length - 1) {
        const answerFlag = window.confirm("已经是最后一关了,需要重新开始么?");
        if (answerFlag) {
          this.level = 0;
        }
      }
    },
    getStatus() {
      this.status = this.$refs.dpuzzle.status;
    },
  },
};
</script>

组件的调用,如下所示:

<template>
  <div>
    <h3>[拼图游戏]当前是第{{level+1}}关,当前状态[{{status}}]</h3>
    <puzzle ref="dpuzzle" @getStatus="getStatus" @next="handleNext" v-bind="puzzleConfig[level]" />
    <!-- <button @click="handleNext" style="width:20px,height:20px" value="下一关">下一关</button> -->
  </div>
</template>

 注意事项:

如果获取组件内部的元素的值,在组件调用时采用ref属性,然后获取组件内的data属性值。

组件内如果调用父组件的方法,本文采用触发注册事件的方式this.$emit("next");

如果需要学习参考源码的朋友,可以点击源码链接进行下载。

源码链接

备注

浪淘沙令·帘外雨潺潺

作者:李煜【五代十国南唐后主】

帘外雨潺潺,春意阑珊。

罗衾不耐五更寒。

梦里不知身是客,一晌贪欢。

独自莫凭栏,无限江山,别时容易见时难。

流水落花春去也,天上人间。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老码识途呀

写作不易,多谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值