App.vue
<template>
<div>
<d-puzzle v-bind="puzzleConfig[level]" :key="level" @nextLevel="goToNextLevel" @imgWHConfirm="imgWHConfirm"/>
</div>
</template>
<script>
import DPuzzle from './Puzzle'
export default {
components: {
DPuzzle
},
data() {
return {
level: 0,
puzzleConfig: [
{id:1,row: 2, col: 2, img: './images/1.jpeg'},
{row: 3, col: 3, img: './images/2.jpeg'},
{row: 3, col: 3, img: './images/4.jpeg'},
{row: 2, col: 2, img: './images/5.jpeg'},
{row: 2, col: 2, img: './images/6.jpg'},
{row: 3, col: 3, img: './images/3.jpeg'},
]
}
},
methods: {
goToNextLevel() {
if (++this.level === this.puzzleConfig.length){
let answerFlag = confirm("恭喜您,已经通过全部关卡,要重新开始嘛?")
if (answerFlag){
this.level = 0;
}
}
},
imgWHConfirm(){
setTimeout(() => {
let answerFlag = confirm("检测到该图片宽高不一致,会影响您的游戏体验,是否跳过该关卡?")
if (answerFlag) {
this.goToNextLevel();
}
},500);
}
}
}
</script>
<style>
</style>
Puzzle.vue
<template>
<div class="puzzle" :style="{width: width+'px',height: height+'px'}">
<div class="puzzle__block"
v-for="(item,index) in blockPoints" :key="item.id"
@click="handleClick"
:ref="index === blockPoints.length - 1 ? 'empty' : 'block'"
:class="index === blockPoints.length - 1 ? 'empty-block' : 'img-block'"
:data-correctX="correctPoints[index].x"
:data-correctY="correctPoints[index].y"
: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`,
backgroundSize: `${width}px ${height}px`,
}"></div>
</div>
</template>
<script>
export default {
props: {
width: {
type: Number,
default: 500
},
height: {
type: Number,
default: 500
},
row: {
type: Number,
default: 3
},
col: {
type: Number,
default: 3
},
img: {
type: String,
required: true
}
},
created() {
this.getImgWidthHeight(this.img).then(res => {
if (res.success) {
let wh = res.data;
const {width, height} = wh;
if (width != height) {
this.$emit('imgWHConfirm');
}
} else {
alert(res.msg);
}
})
},
computed: {
blockWidth() {
return this.width / this.col;
},
blockHeight() {
return this.height / this.row;
},
correctPoints() {
const {row, col, blockWidth, blockHeight} = this;
let arr = [];
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
arr.push({
id: this.randomStr(2) + Math.random() + +new Date(),
x: j * blockWidth,
y: i * blockHeight
})
}
}
return arr;
},
blockPoints() {
let points = this.correctPoints;
let length = points.length;
let lastEle = points[length - 1];
const copyOfPoints = [...points];
copyOfPoints.length = length - 1;
let shuffledPoints = copyOfPoints.sort(() => Math.random() - 0.5);
shuffledPoints.push(lastEle);
return shuffledPoints;
}
},
data() {
return {
randomSeed: ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm',
'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
isStopPlay: false
}
},
methods: {
async getImgWidthHeight(src) {
let result = {};
function imgLoad(src, result) {
let img = new Image();
img.src = src;
return new Promise(resolve => {
if (img.complete) {
result.success = true;
result.data = {
width: img.width,
height: img.height
}
resolve();
} else {
let timeOut = setTimeout(() => {
result.success = false;
result.msg = "图片加载失败";
resolve();
}, 2000);//2000ms可以认为是超时时间
img.onload = function () {
result.success = true;
result.data = {
width: img.width,
height: img.height
}
window.clearTimeout(timeOut);
resolve();
}
}
})
}
await imgLoad(src, result);
return result;
},
randomStr(len) {
let arr = this.randomSeed;
let result = '';
for (let i = 0; i < len; i++) {
result += arr[Math.floor(Math.random() * arr.length)];
}
return result;
},
handleClick(e) {
const blockDom = e.target;
const emptyDom = this.$refs.empty[0];
const {left, top} = blockDom.style;
if (!this.isAdjacent(blockDom, emptyDom)) {
return;
}
blockDom.style.left = emptyDom.style.left;
blockDom.style.top = emptyDom.style.top;
emptyDom.style.left = left;
emptyDom.style.top = top;
if (this.isVictory()) {
this.winGame(emptyDom);
}
},
isAdjacent(blockDom, emptyDom) {
const {left: blockLeft, top: blockTop} = blockDom.style;
const {left: emptyLeft, top: emptyTop} = emptyDom.style;
const {blockWidth, blockHeight} = this;
const xDis = Math.floor(Math.abs(parseFloat(blockLeft) - parseFloat(emptyLeft)));
const yDis = Math.floor(Math.abs(parseFloat(blockTop) - parseFloat(emptyTop)));
return (blockLeft === emptyLeft && yDis === parseInt(blockWidth))
|| (blockTop === emptyTop && xDis === parseInt(blockHeight));
},
isVictory() {
const blockDomArr = this.$refs.block;
return blockDomArr.every(dom => {
const {left: domLeft, top: domTop} = dom.style;
const {correctx: correctX, correcty: correctY} = dom.dataset;
return parseInt(domLeft) === parseInt(correctX) && parseInt(domTop) === parseInt(correctY);
})
},
winGame(emptyDom) {
emptyDom.style.opacity = "1";
setTimeout(() => {
alert("恭喜您,拼图成功!");
setTimeout(() => {
const answerFlag = confirm("要玩下一关嘛?")
if (answerFlag) {
this.$emit("nextLevel");
} else {
this.isStopPlay = true;
}
}, 300);
}, 500);
}
}
}
</script>
<style>
.puzzle {
position: relative;
border: 2px solid #ccc;
}
.puzzle__block {
box-sizing: border-box;
position: absolute;
transition: all .3s;
border: 1px solid #fff;
}
.puzzle__block.img-block {
cursor: pointer;
}
.puzzle__block.empty-block {
opacity: 0;
}
</style>
源码和图片:
https://gitee.com/luminescent-acridine/vue-puzzle-game.git