<el-icon class="fz" v-if="oneiSub === 0">
<StarFilled :color="colors[0]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 1">
<Aim :color="colors[1]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 2">
<Grid :color="colors[2]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 3">
<HelpFilled :color="colors[3]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 4">
<Star :color="colors[4]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 5">
<Menu :color="colors[5]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 6">
<Camera :color="colors[6]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 7">
<Bicycle :color="colors[7]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 8">
<IceTea :color="colors[8]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 9">
<ColdDrink :color="colors[9]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 10">
<CoffeeCup :color="colors[10]" />
</el-icon>
</div>
</div>
</el-col>
</el-row>
</template>
</div>
<div class="sheep-footer flex-center">
<div v-for="(ii, k) in footerList" :key="'ii' + k" class="sheep-footer-items">
<el-icon class="fz" v-if="ii === 0">
<StarFilled :color="colors[0]" />
</el-icon>
<el-icon class="fz" v-if="ii === 1">
<Aim :color="colors[1]" />
</el-icon>
<el-icon class="fz" v-if="ii === 2">
<Grid :color="colors[2]" />
</el-icon>
<el-icon class="fz" v-if="ii === 3">
<HelpFilled :color="colors[3]" />
</el-icon>
<el-icon class="fz" v-if="ii === 4">
<Star :color="colors[4]" />
</el-icon>
<el-icon class="fz" v-if="ii === 5">
<Menu :color="colors[5]" />
</el-icon>
<el-icon class="fz" v-if="ii === 6">
<Camera :color="colors[6]" />
</el-icon>
<el-icon class="fz" v-if="ii === 7">
<Bicycle :color="colors[7]" />
</el-icon>
<el-icon class="fz" v-if="ii === 8">
<IceTea :color="colors[8]" />
</el-icon>
<el-icon class="fz" v-if="ii === 9">
<ColdDrink :color="colors[9]" />
</el-icon>
<el-icon class="fz" v-if="ii === 10">
<CoffeeCup :color="colors[10]" />
</el-icon>
<div class="boom-class" v-if="ii === 'boom'">💥</div>
</div>
</div>
.el-row {
// margin-top: 3rem;
height: 28%;
}
.fz {
font-size: 3rem;
border: 1px solid #dfe5f9;
// box-shadow: 2px 2px 10px #f3f6fe;
background: #f3f6fe;
border-radius: 5px;
}
.pic-list {
position: relative;
width: 100%;
height: 100%;
&-item {
position: absolute;
left: 10vw;
cursor: pointer;
transition: all 0.3s;
&:nth-child(1n) {
top: calc(var(--i) * 1.5rem);
}
&.true {
box-shadow: 0 -55px 0 0 #dfe5f9 inset;
}
// &:nth-child(even) {
// top: 2rem;
// }
}
}
.sheep-main {
flex: 1;
&-wrap {
height: calc(100% - 80px);
}
}
.sheep-footer {
height: 80px;
width: 100%;
// border: 2px solid #298df9;
border: 2px solid #778899;
background: #010206;
.sheep-footer-items {
height: 80px;
width: calc(100% / 7);
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
.boom-class {
font-size: 3rem;
animation: myMove 3s ease-in-out infinite;
}
@keyframes myMove {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
// border-right: 1px solid #dfe5f9;
}
}
## 3.核心逻辑分步骤详解
>
> import { ref, type Ref } from "vue";
>
>
> import { ElMessage, ElMessageBox } from "element-plus";
>
>
> import { useSheepStore } from "@/stores/sheep";
>
>
> // 关卡数据
>
>
> import data from "./data.json";
>
>
> // 颜色
>
>
> import constants from "./constants";
>
>
> // pinia 控制关卡
>
>
> const store = useSheepStore();
>
>
>
首先引入data.json数据是渲染中间的页面内容,即是:

中间的就叫卡片区域吧,卡片分为半个遮挡和整个遮挡,在data数据里面配置:
>
> "full": true
>
>
>
默认是半个遮挡,配置了"full": true就表示这块的卡片是全遮挡的效果:
>
> :style="!onei.full ? `--i:${onekSub}` : `--i:0`"
>
>
> :class="onei.full && onei.oneSub.length > 1 ? 'true' : ''" :key="'i' + onekSub"
>
>
>
css: 使用了var的变量形式,来控制是否需要top下移,&.true来控制是否有下一级的卡片的样式
>
> &:nth-child(1n) {
>
>
> top: calc(var(--i) \* 1.5rem);
>
>
> }
>
>
>
> &.true {
>
>
> box-shadow: 0 -55px 0 0 #dfe5f9 inset;
>
>
> }
>
>
>
data.json里面的数据oneSub的选值范围是:0-10
这和dom渲染层的息息相关:卡片使用的是简单的icon也可以是其他类型的元素,你觉得好看即可。
<el-icon class="fz" v-if="oneiSub === 0">
<StarFilled :color="colors[0]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 1">
<Aim :color="colors[1]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 2">
<Grid :color="colors[2]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 3">
<HelpFilled :color="colors[3]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 4">
<Star :color="colors[4]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 5">
<Menu :color="colors[5]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 6">
<Camera :color="colors[6]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 7">
<Bicycle :color="colors[7]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 8">
<IceTea :color="colors[8]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 9">
<ColdDrink :color="colors[9]" />
</el-icon>
<el-icon class="fz" v-if="oneiSub === 10">
<CoffeeCup :color="colors[10]" />
</el-icon>
这里只提供11中卡片的效果,可以扩展添加,需要修改代码。
接下来是:
>
> // 七个槽位
>
>
> // const footerList = ref([0, 1, 2, 3, 4, 5, 6]);
>
>
> const footerList: Ref<Array<any> | [any]> = ref([]);
>
>
>
> const colors = ref(constants.colors);
>
>
>
> // 关卡响应式
>
>
> const totalList: Ref<Array<any> | [any]> = ref([]);
>
>
> totalList.value = data["list1"]; // 默认第一关
>
>
>
> // 控制动画效果结束才能点击
>
>
> const isNotClick = ref(false);
>
>
>
>
7个槽位在底部需要变化展示,做成响应式。totalList是动态变化的卡片数据集。totalList.value = data["list1"] ,默认第一关。爆炸💥的电话效果有延迟,需要控制在结束之后才能进行卡片的点击。
然后就是核心的卡片点击事件,需要做哪些逻辑控制呢?先看源代码,已经提前做了备注:
// 点击控制事件
function handleClick(
i: number,
k: number,
onei: { oneSub: string | Array },
onek: number,
oneiSub: Array,
onekSub: number
) {
console.log(i, k, onei, onek, oneiSub, onekSub, “测试”);
if (isNotClick.value) {
return false;
}
// 内层不能点击
if (onekSub !== onei.oneSub.length - 1) {
return false;
}
// 前置点击如果槽位满了还没有消除完
fullFun()
// 关卡的消除
let tempList = fixFun(k, onekSub, onek, oneiSub)
// 消除动作 和 添加爆炸效果
if (footerList.value.length > 2) {
isNotClick.value = true
const { list, flag } = eliminationFunction(footerList.value)
footerList.value = list;
if (flag) {
footerList.value = addBoomFunction(footerList.value);
}
setTimeout(() => {
const { list, flag } = eliminationFunction(footerList.value)
footerList.value = list;
isNotClick.value = false
}, 1000);
// 进入下一关
nextFun(tempList)
}
// 挑战失败
failFun(tempList)
console.log(footerList, tempList, “tempList”);
}
首先是函数的签名,接受最上层级的i对象,k索引,然后是中层的onei对象,onek索引,最后是父级的oneiSub对象,onekSub索引。判断条件需要前置,判断能否点击isNotClick,内层不能点击
>
> if (isNotClick.value) {
>
>
> return false;
>
>
> }
>
>
>
// 前置点击如果槽位满了还没有消除完
fullFun()函数判断如果槽位满了还没有消除完,就是挑战失败
function fullFun() {
if (footerList.value.length === 7) {
ElMessage.closeAll();
ElMessageBox.alert("挑战失败,点击确定返回!", "Warning", {
confirmButtonText: "确定",
type: "warning",
showClose: false,
}).then(() => {
location.reload();
});
return false;
}
}
如何添加爆炸💥效果:
思路是在三个相同消除之后添加,添加在totalList数据之中 ,效果展示完成之后立即进行totalList数据重置操作。
// 关卡的消除
let tempList = fixFun(k, onekSub, onek, oneiSub)
// 消除动作 和 添加爆炸效果
if (footerList.value.length > 2) {
isNotClick.value = true
const { list, flag } = eliminationFunction(footerList.value)
footerList.value = list;
if (flag) {
footerList.value = addBoomFunction(footerList.value);
}
setTimeout(() => {
const { list, flag } = eliminationFunction(footerList.value)
footerList.value = list;
isNotClick.value = false
}, 1000);
// 进入下一关
nextFun(tempList)
}
css 添加的方法:
.boom-class {
font-size: 3rem;
animation: myMove 3s ease-in-out infinite;
}
@keyframes myMove {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
消除函数eliminationFunction逻辑的控制,flag用来进行是否成功消除:
// 消除函数
function eliminationFunction(list: any[]) {
let flag: boolean = false;
for (let k = 0; k < list.length - 2; k++) {
const temp = list;
const arr = temp.slice(k, k + 3);
console.log(k, arr);
if (arr[0] === arr[1] && arr[1] === arr[2] && arr[0] === arr[2]) {
list.splice(k + 2);
list.splice(k + 1);
list.splice(k, 1);
flag = true
break;
}
}
return { list, flag };
}
添加addBoomFunction爆炸函数:
// 实现爆炸💥效果
function addBoomFunction(list: any[]) {
const temp = JSON.parse(JSON.stringify([…list, …[‘boom’, ‘boom’, ‘boom’]]))
return temp;
}
挑战失败如何判断呢?
//fail
function failFun(tempList: any[]) {
setTimeout(() => {
if (footerList.value.length > 0 && !jugeList(tempList)) {
ElMessage.closeAll();
ElMessageBox.alert("挑战失败,点击确定返回!", "Warning", {
confirmButtonText: "确定",
type: "warning",
showClose: false,
}).then(() => {
location.reload();
});
return false;
}
}, 1002)
}
jugeList函数是对目前存在的卡片集合进行长度判断,如何卡片不存在,但是槽位的数据不为空的情况下,说明没有消除完,就判断要重新开始挑战:
// 判断是否过关
function jugeList(list: any[]) {
let temp: any = [];
list?.forEach((oeni: { one: any }) => {
oeni?.one?.forEach((sub: { oneSub: any }) => {
temp = […temp, …sub.oneSub];
});
});
return temp.length;
}
最后是挑战成功就可以进行下一关:
// next
function nextFun(tempList: any[]) {
setTimeout(() => {
if (!footerList.value.length && !jugeList(tempList)) {
// debugger
ElMessage.closeAll();
ElMessage.success(“恭喜您,挑战成功!进入下一关”);
store.step++;
const inStep: string = “list” + (store.step + 1);
totalList.value = JSON.parse(JSON.stringify(data))[inStep];
footerList.value = [];
}
}, 1001)
}
如何卡片不存在,但是槽位的数据为空的情况下,说明消除完了,就可以进入下一关进行挑战,难度也将升级!
## 4.总结
最近是由于玩了羊了个羊的小程序,有所感悟,思考了这个游戏的整体的玩法,如何去操作,然后想到了可以实现一个前端网页版本的羊了个羊,这里面有一些自己的设计思考是很重要的,花了一个星期左右来实现,中间遇到了如何消除,如何控制挑战失败,成功的问题,并且一一解决了,可以想到如果前端来做这个游戏怎么在最优的方案上,书写可以扩展的dom,来适配很多不同的关卡的元素或者是我们需要什么样的数据结构,方便后续的关卡的升级。这里解决的方案是配合json,数据是数组嵌套类型,元素是需要循环来调用的,什么类型的卡片是需要提前有个范围的,这样是可扩展的。最后的操作,或者撤销,恢复等操作(这里没有实现)本质上也是对于数据的操作。终而言之:数据驱动页面,才是我们追求的。最后,各位同学一起多思考一下背后的实现,让我们用技术来创作更多有趣的事情吧~❤️
>
> 个人主页:[KinHKin(五年前端)的博客\_CSDN博客-vue,css,中秋活动领域博主](https://bbs.csdn.net/topics/618166371)的博客_CSDN博客-vue,css,中秋活动领域博主")
>
>
> 在线演示:[KinHKin](https://bbs.csdn.net/topics/618166371)
>
### 结尾
正式学习前端大概 3 年多了,很早就想整理这个书单了,因为常常会有朋友问,前端该如何学习,学习前端该看哪些书,我就讲讲我学习的道路中看的一些书,虽然整理的书不多,但是每一本都是那种看一本就秒不绝口的感觉。
以下大部分是我看过的,或者说身边的人推荐的书籍,每一本我都有些相关的推荐语,如果你有看到更好的书欢迎推荐呀。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
配合json,数据是数组嵌套类型,元素是需要循环来调用的,什么类型的卡片是需要提前有个范围的,这样是可扩展的。最后的操作,或者撤销,恢复等操作(这里没有实现)本质上也是对于数据的操作。终而言之:数据驱动页面,才是我们追求的。最后,各位同学一起多思考一下背后的实现,让我们用技术来创作更多有趣的事情吧~❤️
>
> 个人主页:[KinHKin(五年前端)的博客\_CSDN博客-vue,css,中秋活动领域博主](https://bbs.csdn.net/topics/618166371)的博客_CSDN博客-vue,css,中秋活动领域博主")
>
>
> 在线演示:[KinHKin](https://bbs.csdn.net/topics/618166371)
>
### 结尾
正式学习前端大概 3 年多了,很早就想整理这个书单了,因为常常会有朋友问,前端该如何学习,学习前端该看哪些书,我就讲讲我学习的道路中看的一些书,虽然整理的书不多,但是每一本都是那种看一本就秒不绝口的感觉。
以下大部分是我看过的,或者说身边的人推荐的书籍,每一本我都有些相关的推荐语,如果你有看到更好的书欢迎推荐呀。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
