一起学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动态加载,如下所示:

 1 <template>
 2   <div class="puzzle" :style="{width:width+'px',height:height+'px'}">
 3     <div
 4       v-for="(item,index) in blockPoints"
 5       :key="item.id"
 6       :style="{width:blockWidth+'px',
 7         height:blockHeight+'px',
 8         left:item.x+'px',top:item.y+'px',
 9         backgroundImage:`url(${img})`,
10         backgroundPosition:`-${correctPoints[index].x}px -${correctPoints[index].y}px`,
11         opacity: index===blockPoints.length-1 && 0 }"
12       v-on:click="handleClick"
13       class="puzzle__block"
14       :ref="index===blockPoints.length-1?'empty':'block'"
15       :data-correctX="correctPoints[index].x"
16       :data-correctY="correctPoints[index].y"
17     ></div>
18   </div>
19 </template>
View Code

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

  1 <script>
  2 export default {
  3   props: {
  4     img: {
  5       // 图片路径
  6       type: String,
  7       required: true,
  8     },
  9     width: {
 10       // 图片总宽度
 11       type: Number,
 12       default: 500,
 13     },
 14     height: {
 15       // 图片总高度
 16       type: Number,
 17       default: 500,
 18     },
 19     row: {
 20       // 行数
 21       type: Number,
 22       default: 3,
 23     },
 24     col: {
 25       // 列数
 26       type: Number,
 27       default: 3,
 28     },
 29   },
 30   data() {
 31     return {
 32       status: {
 33         type: String,
 34         default: "进行中......",
 35       },
 36     };
 37   },
 38   methods: {
 39     handleClick(e) {
 40       const blockDom = e.target;
 41       const empthDom = this.$refs.empty[0];
 42       const { left, top } = blockDom.style;
 43       if (!this.isAdjacent(blockDom, empthDom)) {
 44         return;
 45       }
 46       //交换元素
 47       blockDom.style.left = empthDom.style.left;
 48       blockDom.style.top = empthDom.style.top;
 49       empthDom.style.left = left;
 50       empthDom.style.top = top;
 51       const winFlag = this.winCheck();
 52       if (winFlag) {
 53         //   console.log('success');
 54         this.winGame(empthDom);
 55       }
 56     },
 57     isAdjacent(blockDom, empthDom) {
 58       // 判断是否相邻
 59       const { left: blockLeft, top: blockTop, width, height } = blockDom.style;
 60       const { left: emptyLeft, top: emptyTop } = empthDom.style;
 61       const xDis = Math.floor(
 62         Math.abs(parseFloat(blockLeft) - parseFloat(emptyLeft))
 63       );
 64       const yDis = Math.floor(
 65         Math.abs(parseFloat(blockTop) - parseFloat(emptyTop))
 66       );
 67       const flag =
 68         (blockLeft === emptyLeft && yDis === parseInt(height)) ||
 69         (blockTop === emptyTop && xDis === parseInt(width));
 70       console.log(flag);
 71       return flag;
 72     },
 73     winCheck() {
 74       // 判断是否完成
 75       const blockDomArr = this.$refs.block;
 76       return blockDomArr.every((dom) => {
 77         const { left: domLeft, top: domTop } = dom.style;
 78         const { correctx: correctX, correcty: correctY } = dom.dataset;
 79         const flag =
 80           parseInt(domLeft) === parseInt(correctX) &&
 81           parseInt(domTop) === parseInt(correctY);
 82         return flag;
 83       });
 84       // console.log(blockDomArr.length);
 85     },
 86     winGame(empthDom) {
 87       //通关
 88       setTimeout(() => {
 89         this.status = "胜利";
 90         alert("恭喜通关");
 91         empthDom.style.opacity = 1;
 92         this.$emit("getStatus");
 93         setTimeout(() => {
 94           this.goToNextLevel();
 95         }, 300);
 96       }, 300);
 97     },
 98     goToNextLevel() {
 99       const answerFlag = window.confirm("现在进行下一关么?");
100       if (answerFlag) {
101         this.status = "进行中......";
102         this.$emit("next");
103       }
104     },
105   },
106   computed: {
107     blockWidth() {
108       return this.width / this.col;
109     },
110     blockHeight() {
111       return this.height / this.row;
112     },
113     correctPoints() {
114       const { row, col, blockWidth, blockHeight } = this;
115       const arr = [];
116       for (let i = 0; i < row; i++) {
117         for (let j = 0; j < col; j++) {
118           arr.push({
119             x: j * blockWidth,
120             y: i * blockHeight,
121             id: new Date().getTime() + Math.random() * 100,
122           });
123         }
124       }
125       return arr;
126     },
127     blockPoints() {
128       const points = this.correctPoints;
129       const length = points.length; //数组的长度
130       const lastEle = points[length - 1]; //最后一个元素
131       const newArr = [...points];
132       newArr.length = length - 1;
133       //打乱顺序
134       newArr.sort(() => Math.random() - 0.5);
135       newArr.push(lastEle);
136       return newArr;
137     },
138   },
139 };
140 </script>
View Code

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

 1 <style>
 2 .puzzle {
 3   box-sizing: content-box;
 4   border: 2px solid #cccccc;
 5   position: relative;
 6 }
 7 .puzzle__block {
 8   border: 1px solid #ffffff;
 9   box-sizing: border-box;
10   /* background-color: rebeccapurple; */
11   position: absolute;
12   transition: all 0.3s;
13 }
14 </style>
View Code

 拼图组件的调用App.vue

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

 1 <script>
 2 import puzzle from "./Puzzle";
 3 export default {
 4   components: {
 5     puzzle,
 6   },
 7   data() {
 8     return {
 9       level: 0,
10       puzzleConfig: [
11         { img: "./img/001.jpg", row: 3, col: 3 },
12         { img: "./img/002.jpg", row: 4, col: 4 },
13         { img: "./img/003.jpg", row: 5, col: 5 },
14         { img: "./img/004.jpg", row: 6, col: 6 },
15         { img: "./img/005.jpg", row: 7, col: 7 },
16       ],
17       status: "进行中......",
18     };
19   },
20   methods: {
21     handleNext() {
22       console.log("next");
23       this.status = this.$refs.dpuzzle.status;
24       this.level++;
25       if (this.level == this.puzzleConfig.length - 1) {
26         const answerFlag = window.confirm("已经是最后一关了,需要重新开始么?");
27         if (answerFlag) {
28           this.level = 0;
29         }
30       }
31     },
32     getStatus() {
33       this.status = this.$refs.dpuzzle.status;
34     },
35   },
36 };
37 </script>
View Code

组件的调用,如下所示:

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

 注意事项:

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

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

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

源码链接

备注

浪淘沙令·帘外雨潺潺

 

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

帘外雨潺潺,春意阑珊。

罗衾不耐五更寒。

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

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

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

posted @ 2020-08-27 23:41  老码识途呀  阅读(1310)  评论(0编辑  收藏  举报