基于Vue的验证码实现
一、验证码核心实现
创建slide-verify.vue,代码如下:
<template><divclass="slide-verify":style="{ width: w + 'px' }"id="slideVerify"onselectstart="return false;"><!-- 图片加载遮蔽罩 --><div :class="{ 'slider-verify-loading': loadBlock }"></div><canvas :width="w" :height="h" ref="canvas"></canvas><div v-if="show" @click="refresh" class="slide-verify-refresh-icon"></div><canvas:width="w":height="h"ref="block"class="slide-verify-block"></canvas><!-- container --><divclass="slide-verify-slider":class="{'container-active': containerActive,'container-success': containerSuccess,'container-fail': containerFail}"><div class="slide-verify-slider-mask" :style="{ width: sliderMaskWidth }"><!-- slider --><div@mousedown="sliderDown"@touchstart="touchStartEvent"@touchmove="handleMoveEvent($event, 'touch')"@touchend="handleMoveEndEvent($event, 'touch')"class="slide-verify-slider-mask-item":style="{ left: sliderLeft }"><div class="slide-verify-slider-mask-item-icon"></div></div></div><span class="slide-verify-slider-text">{{ sliderText }}</span></div></div>
</template>
<script>
const PI = Math.PI;function sum(x, y) {return x + y;
}function square(x) {return x * x;
}
export default {name: "SlideVerify",props: {// block lengthl: {type: Number,default: 42},// block radiusr: {type: Number,default: 10},// canvas widthw: {type: Number,default: 310},// canvas heighth: {type: Number,default: 155},sliderText: {type: String,default: "Slide filled right"},accuracy: {type: Number,default: 5 // 若为 -1 则不进行机器判断},show: {type: Boolean,default: true},imgs: {type: Array,default: () => []}},data() {return {containerActive: false, // container active classcontainerSuccess: false, // container success classcontainerFail: false, // container fail classcanvasCtx: null,blockCtx: null,block: null,block_x: undefined, // container random positionblock_y: undefined,L: this.l + this.r * 2 + 3, // block real lengthimg: undefined,originX: undefined,originY: undefined,isMouseDown: false,trail: [],sliderLeft: 0, // block right offsetsliderMaskWidth: 0, // mask width,success: false, // Bug Fixes 修复了验证成功后还能滑动loadBlock: true, // Features 图片加载提示,防止图片没加载完就开始验证timestamp: null};},mounted() {this.init();},methods: {init() {this.initDom();this.initImg();this.bindEvents();},initDom() {this.block = this.$refs.block;this.canvasCtx = this.$refs.canvas.getContext("2d");this.blockCtx = this.block.getContext("2d");},initImg() {const img = this.createImg(() => {// 图片加载完关闭遮蔽罩this.loadBlock = false;this.drawBlock();this.canvasCtx.drawImage(img, 0, 0, this.w, this.h);this.blockCtx.drawImage(img, 0, 0, this.w, this.h);let { block_x: x, block_y: y, r, L } = this;let _y = y - r * 2 - 1;let ImageData = this.blockCtx.getImageData(x, _y, L, L);this.block.width = L;this.blockCtx.putImageData(ImageData, 0, _y);});this.img = img;},drawBlock() {this.block_x = this.getRandomNumberByRange(this.L + 10,this.w - (this.L + 10));this.block_y = this.getRandomNumberByRange(10 + this.r * 2,this.h - (this.L + 10));this.draw(this.canvasCtx, this.block_x, this.block_y, "fill");this.draw(this.blockCtx, this.block_x, this.block_y, "clip");},draw(ctx, x, y, operation) {let { l, r } = this;ctx.beginPath();ctx.moveTo(x, y);ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);ctx.lineTo(x + l, y);ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);ctx.lineTo(x + l, y + l);ctx.lineTo(x, y + l);ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);ctx.lineTo(x, y);ctx.lineWidth = 2;ctx.fillStyle = "rgba(255, 255, 255, 0.7)";ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";ctx.stroke();ctx[operation]();// Bug Fixes 修复了火狐和ie显示问题ctx.globalCompositeOperation = "destination-over";},createImg(onload) {const img = document.createElement("img");img.crossOrigin = "Anonymous";img.onload = onload;img.onerror = () => {img.src = this.getRandomImg();};img.src = this.getRandomImg();return img;},// 随机生成img srcgetRandomImg() {// return require('../assets/img.jpg')const len = this.imgs.length;return len > 0? this.imgs[this.getRandomNumberByRange(0, len - 1)]: // "https://bing.ioliu.cn/v1/rand?w=300&h=150";"https://source.unsplash.com/300x150/?book,library";// "https://api.dujin.org/pic/fengjing";},getRandomNumberByRange(start, end) {return Math.round(Math.random() * (end - start) + start);},refresh() {this.reset();this.$emit("refresh");},sliderDown(event) {if (this.success) return;this.originX = event.clientX;this.originY = event.clientY;this.isMouseDown = true;this.timestamp = +new Date();},touchStartEvent(e) {if (this.success) return;e.preventDefault();this.originX = e.changedTouches[0].pageX;this.originY = e.changedTouches[0].pageY;this.isMouseDown = true;this.timestamp = +new Date();},bindEvents() {document.addEventListener("mousemove", this.handleMoveEvent);document.addEventListener("mouseup", this.handleMoveEndEvent);},// 处理函数抽离handleMoveEvent: throttle(function(e, type = "mouse") {if (!this.isMouseDown) return false;const moveX =type === "mouse"? e.clientX - this.originX: e.changedTouches[0].pageX - this.originX;const moveY =type === "mouse"? e.clientY - this.originY: e.changedTouches[0].pageY - this.originY;if (moveX < 0 || moveX + 38 >= this.w) return false;this.sliderLeft = moveX + "px";let blockLeft = ((this.w - 40 - 20) / (this.w - 40)) * moveX;this.block.style.left = blockLeft + "px";this.containerActive = true; // add activethis.sliderMaskWidth = moveX + "px";this.trail.push(moveY);}),handleMoveEndEvent(e, type = "mouse") {if (!this.isMouseDown) return false;this.isMouseDown = false;if ((type === "mouse" && e.clientX === this.originX) ||(type === "touch" && e.changedTouches[0].pageX === this.originX))return false;this.containerActive = false; // remove activethis.timestamp = +new Date() - this.timestamp;const { spliced, TuringTest } = this.verify();if (spliced) {if (this.accuracy === -1) {this.containerSuccess = true;this.success = true;this.$emit("success", this.timestamp);return;}if (TuringTest) {// succthis.containerSuccess = true;this.success = true;this.$emit("success", this.timestamp);} else {this.containerFail = true;this.$emit("again");}} else {this.containerFail = true;this.$emit("fail");setTimeout(() => {this.reset();}, 1000);}},verify() {const arr = this.trail; // drag y move distanceconst average = arr.reduce(sum) / arr.length; // averageconst deviations = arr.map(x => x - average); // deviation arrayconst stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); // standard deviationconst left = parseInt(this.block.style.left);const accuracy =this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy;return {spliced: Math.abs(left - this.block_x) <= accuracy,TuringTest: average !== stddev // equal => not person operate};},reset() {this.success = false;this.containerActive = false;this.containerSuccess = false;this.containerFail = false;this.sliderLeft = 0;this.block.style.left = 0;this.sliderMaskWidth = 0;// canvaslet { w, h } = this;this.canvasCtx.clearRect(0, 0, w, h);this.blockCtx.clearRect(0, 0, w, h);this.block.width = w;// generate imgthis.img.src = this.getRandomImg();this.$emit("fulfilled");}},destroyed() {document.removeEventListener("mousemove", this.handleMoveEvent);document.removeEventListener("mouseup", this.handleMoveEndEvent);}
};function throttle(fn,interval = 50,options = { leading: true, trailing: true }
) {const { leading, trailing, resultCallback } = options;let lastTime = 0;let timer = null;const _throttle = function(...args) {return new Promise((resolve, reject) => {const nowTime = new Date().getTime();if (!lastTime && !leading) lastTime = nowTime;const remainTime = interval - (nowTime - lastTime);if (remainTime <= 0) {if (timer) {clearTimeout(timer);timer = null;}const result = fn.apply(this, args);if (resultCallback) resultCallback(result);resolve(result);lastTime = nowTime;return;}if (trailing && !timer) {timer = setTimeout(() => {timer = null;lastTime = !leading ? 0 : new Date().getTime();const result = fn.apply(this, args);if (resultCallback) resultCallback(result);resolve(result);}, remainTime);}});};_throttle.cancel = function() {if (timer) clearTimeout(timer);timer = null;lastTime = 0;};return _throttle;
}
</script>
<style scoped>
.slide-verify {position: relative;
}/* 图片加载样式 */
.slider-verify-loading {position: absolute;top: 0;right: 0;left: 0;bottom: 0;background: rgba(255, 255, 255, 0.9);z-index: 999;animation: loading 1.5s infinite;
}@keyframes loading {0% {opacity: 0.7;}100% {opacity: 9;}
}.slide-verify-block {position: absolute;left: 0;top: 0;
}.slide-verify-refresh-icon {position: absolute;right: 0;top: 0;width: 34px;height: 34px;cursor: pointer;background: url("../assets/icon_light.png") 0 -437px;background-size: 34px 471px;
}.slide-verify-slider {position: relative;text-align: center;width: 100%;height: 40px;line-height: 40px;margin-top: 15px;background: #f7f9fa;color: #45494c;border: 1px solid #e4e7eb;
}.slide-verify-slider-mask {position: absolute;left: 0;top: 0;height: 40px;border: 0 solid #1991fa;background: #d1e9fe;
}.slide-verify-slider-mask-item {position: absolute;top: 0;left: 0;width: 40px;height: 40px;background: #fff;box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);cursor: pointer;transition: background 0.2s linear;
}.slide-verify-slider-mask-item:hover {background: #1991fa;
}.slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {background-position: 0 -13px;
}.slide-verify-slider-mask-item-icon {position: absolute;top: 15px;left: 13px;width: 14px;height: 12px;background: url("../assets/icon_light.png") 0 -26px;background-size: 34px 471px;
}
.container-active .slide-verify-slider-mask-item {height: 38px;top: -1px;border: 1px solid #1991fa;
}.container-active .slide-verify-slider-mask {height: 38px;border-width: 1px;
}.container-success .slide-verify-slider-mask-item {height: 38px;top: -1px;border: 1px solid #52ccba;background-color: #52ccba !important;
}.container-success .slide-verify-slider-mask {height: 38px;border: 1px solid #52ccba;background-color: #d2f4ef;
}.container-success .slide-verify-slider-mask-item-icon {background-position: 0 0 !important;
}.container-fail .slide-verify-slider-mask-item {height: 38px;top: -1px;border: 1px solid #f57a7a;background-color: #f57a7a !important;
}.container-fail .slide-verify-slider-mask {height: 38px;border: 1px solid #f57a7a;background-color: #fce1e1;
}.container-fail .slide-verify-slider-mask-item-icon {top: 14px;background-position: 0 -82px !important;
}.container-active .slide-verify-slider-text,
.container-success .slide-verify-slider-text,
.container-fail .slide-verify-slider-text {display: none;
}
</style>
基于slide-verify.vue创建组件,index.js代码如下:
import SlideVerify from './slide-verify.vue'const plugins = {install(Vue) {Vue.component(SlideVerify.name, SlideVerify)}
}if (typeof window !== 'undefined' && window.Vue) {window.Vue.use(SlideVerify)
}export default plugins
二、在项目中使用验证码
在main.js中应用:
import { createApp } from 'vue'
import App from './App.vue'
// 导入验证码
import SlideVerify from './xxx/index'const vue = createApp(App)
vue.use(SlideVerify) // 在vue中使用
vue.mount('#app')
准备验证码图片:

其中icon_light.png图标图片如下,其他img.jpg图片可以是任意图片。
![]()
在vue中使用验证码,App.vue代码如下:
<template><div id="app"><slide-verify ref="slideBlock" @success="onSuccess" @again="onAgain" @fulfilled="onFulfilled" @fail="onFail"@refresh="onRefresh" :slider-text="text" :imgs="imgs" :accuracy="accuracy"></slide-verify><div>{{ msg }}</div><button class="btn" @click="handleClick">在父组件点击刷新</button></div>
</template><script setup>
import img0 from './assets/img.jpg';
import img1 from './assets/img1.jpg';
import img2 from './assets/img2.jpg';
import img3 from './assets/img3.jpg';
import img4 from './assets/img4.jpg';
import img5 from './assets/img5.jpg';
import {ref} from "vue";const slideBlock = ref();
const msg = ref('');
const text = ref('向右滑动->');
const imgs = ref([img0,img1,img2,img3,img4,img5,
]);
const accuracy = ref(1); // 精确度小,可允许的误差范围小;为1时,则表示滑块要与凹槽完全重叠,才能验证成功。默认值为5const onSuccess = (times) => {console.log('验证通过');msg.value = `login success, 耗时${(times / 1000).toFixed(1)}s`;
};
const onFail = () => {console.log('验证不通过');msg.value = ''
};
const onRefresh = () => {console.log('点击了刷新小图标');msg.value = ''
};
const onFulfilled = () => {console.log('刷新成功啦!');
};
const onAgain = () => {console.log('检测到非人为操作的哦!');msg.value = 'try again';// 刷新handleClick();
};
const handleClick = () => {slideBlock.value.reset();msg.value = ''
}</script><style scoped>
#app {font-family: 'Avenir', Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;color: #2c3e50;margin-top: 60px;
}.btn {margin-top: 20px;outline: 0;border: none;padding: 8px 15px;border-radius: 5px;color: #fff;background-color: #1890ff;cursor: pointer;
}.btn:active {box-shadow: 1px 5px 0 rgba(0, 0, 0, 0.1) inset;
}
</style>
运行项目查看:

相关文章:
基于Vue的验证码实现
一、验证码核心实现 创建slide-verify.vue,代码如下: <template><divclass"slide-verify":style"{ width: w px }"id"slideVerify"onselectstart"return false;"><!-- 图片加载遮蔽罩 -->&…...
P4【力扣217,389,496】【数据结构】【哈希表】C++版
【217】存在重复元素 给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。 示例 1: 输入:nums [1,2,3,1] 输出:true 示例 2:…...
PE文件(六)新增节-添加代码作业
一.手动新增节添加代码 1.当预备条件都满足,节表结尾没有相关数据时: 现在我们将ipmsg.exe用winhex打开,在节的最后新增一个节用于存放我们要增加的数据 注意:飞鸽的文件对齐和内存对齐是一致的 先判断节表末尾到第一个节之间…...
ICRA 2024: NVIDIA 联合多伦多大学、加州大学伯克利分校、苏黎世联邦理工学院等研究人员开发了精细操作的手术机器人
英伟达(NVIDIA)正与学术研究人员合作,研究手术机器人。 NVIDIA 联合多伦多大学、加州大学伯克利分校、苏黎世联邦理工学院和佐治亚理工学院的研究人员开发了 ORBIT-Surgical,一个训练机器人的模拟框架,可以提高手术团…...
探索Go语言的原子操作秘籍:sync/atomic.Value全解析
引言 在并发编程的世界里,数据的一致性和线程安全是永恒的话题。Go语言以其独特的并发模型——goroutine和channel,简化了并发编程的复杂性。然而,在某些场景下,我们仍然需要一种机制来保证操作的原子性。这就是sync/atomic.V…...
【java深入学习第3章】利用 Spring Boot 和 Screw 快速生成数据库设计文档
免费多模型AI网站,支持豆包、GPT-4o、谷歌Gemini等AI模型,无限制使用,快去白嫖👉海鲸AI🔥🔥🔥 在开发过程中,数据库设计文档是非常重要的,它可以帮助开发者理解数据库结构࿰…...
继“三级淋巴结”之后,再看看“单细胞”如何与AI结合【医学AI|顶刊速递|05-25】
小罗碎碎念 24-05-25文献速递 今天想和大家分享的是肿瘤治疗领域的另一个热点——单细胞技术,我们一起来看看,最新出炉的顶刊,是如何把AI与单细胞结合起来的。 另外,今天是周末,所以会有两篇文章——一篇文献速递&…...
[图解]产品经理创新之阿布思考法
0 00:00:00,000 --> 00:00:01,900 那刚才我们讲到了 1 00:00:02,730 --> 00:00:03,746 业务序列图 2 00:00:03,746 --> 00:00:04,560 然后怎么 3 00:00:05,530 --> 00:00:06,963 画现状,怎么改进 4 00:00:06,963 --> 00:00:09,012 然后改进的模式…...
Proteus仿真小技巧(隔空连线)
用了好几天Proteus了.总结一下使用的小技巧. 目录 一.隔空连线 1.打开添加网络标号 2.输入网络标号 二.常用元件 三.运行仿真 四.总结 一.隔空连线 引出一条线,并在末尾点一下. 1.打开添加网络标号 选择添加网络标号, 也可以先点击按钮,再去选择线(注意不要点端口) 2.…...
抖音极速版:抖音轻量精简版本,新人享大福利
和快手一样,抖音也有自己的极速版,可视作抖音的轻量精简版,更专注于刷视频看广告赚钱,收益比抖音要高,可玩性更佳。 抖音极速版简介 抖音极速版是一个提供短视频创业和收益任务的平台,用户可以通过观看广…...
leetCode-hot100-数组专题之双指针
数组双指针专题 1.同向双指针1.1例题26.删除有序数组中的重复项27.移除元素80.删除有序数组中的重复项 Ⅱ 2.相向双指针2.1例题11.盛最多水的容器42.接雨水581.最短无序连续子数组 双指针在算法题中很常见,下面总结双指针在数组中的一些应用,主要分为两类…...
完成商品SPU管理页面
文章目录 1.引入前端界面1.将前端界面放到commodity下2.创建菜单3.进入前端项目,使用npm添加依赖1.根目录下输入2.报错 chromedriver2.27.2的问题3.点击链接下载压缩包,然后使用下面的命令安装4.再次安装 pubsub-js 成功5.在main.js中引入这个组件 4.修改…...
Ansible实战YAML语言完成apache的部署,配置,启动全过程
🏡作者主页:点击! 🏝️Ansible专栏:点击! ⏰️创作时间:2024年5月24日15点59分 目录 💯趣站推荐💯 🎊前言 ✨️YAML语言回顾 🎆1.编写YAML文…...
深入探索微软Edge:新一代浏览器的演进与创新
在数字时代的浪潮中,浏览器已不再只是简单的网页访问工具,而是成为了连接信息、服务与用户之间的重要桥梁。微软Edge作为微软公司推出的一款全新的浏览器,不仅承载着微软在互联网领域的最新愿景,还融合了多项前沿技术,…...
k8s使用Volcano调度gpu
k8s部署 https://www.yangxingzhen.com/9817.html cri-dockerd安装 https://zhuanlan.zhihu.com/p/632861515 安装nvidia-container-runtime https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html 安装k8s-device-plugin https://…...
x的平方根-力扣
本题想到使用二分法不断逼近一个区间,直到最后趋近于x,从而求得解。注意的点,一开始使用 if(mid * mid < x) 进行判断时,会出现越界,原因是输入一个很大的数是,超过int表示的范围,继而修改为…...
hot100 -- 回溯(上)
目录 🍞科普 🌼全排列 AC DFS 🚩子集 AC DFS 🎂电话号码的字母组合 AC DFS 🌼组合总和 AC DFS 🍞科普 忘记 dfs 的,先看看这个👇 DFS(深度优先搜索…...
5.24数据库作业
考虑如下关系模式R(A,B.C.D,E,F)上的函数依赖集F: {A→BCD,BC→DE,B→D,D→A} 1、计算B的闭包。 2、(使用Armstrong公理)证明AF是超码。 3、计算上述函数依赖集F的正则覆盖;给出你的推导的步骤并解释。 4、基于正则覆盖࿰…...
go-zero 实战(5)
引入Prometheus 用 Prometheus 监控应用 1. 用 docker 启动 Prometheus 编辑配置位置,我将 prometheus.yaml 和 targets.json 文件放在了 /opt/prometheus/conf目录下 prometheus.yaml global:scrape_interval: 15s # 抓取间隔evaluation_interval: 15s # 评估…...
Python异常处理:打造你的代码防弹衣!
Hi,我是阿佑,上文咱们讲到——揭秘Python的魔法:装饰器的超能力大揭秘 ♂️✨,阿佑将带领大家通过精准捕获异常、使用with语句和上下文管理器、以及异常链等高级技巧来增强代码的健壮性。就像为代码穿上防弹衣,保护它…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
逻辑回归:给不确定性划界的分类大师
想象你是一名医生。面对患者的检查报告(肿瘤大小、血液指标),你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...
ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
【VLNs篇】07:NavRL—在动态环境中学习安全飞行
项目内容论文标题NavRL: 在动态环境中学习安全飞行 (NavRL: Learning Safe Flight in Dynamic Environments)核心问题解决无人机在包含静态和动态障碍物的复杂环境中进行安全、高效自主导航的挑战,克服传统方法和现有强化学习方法的局限性。核心算法基于近端策略优化…...
论文阅读笔记——Muffin: Testing Deep Learning Libraries via Neural Architecture Fuzzing
Muffin 论文 现有方法 CRADLE 和 LEMON,依赖模型推理阶段输出进行差分测试,但在训练阶段是不可行的,因为训练阶段直到最后才有固定输出,中间过程是不断变化的。API 库覆盖低,因为各个 API 都是在各种具体场景下使用。…...
ubuntu系统文件误删(/lib/x86_64-linux-gnu/libc.so.6)修复方案 [成功解决]
报错信息:libc.so.6: cannot open shared object file: No such file or directory: #ls, ln, sudo...命令都不能用 error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory重启后报错信息&…...
鸿蒙(HarmonyOS5)实现跳一跳小游戏
下面我将介绍如何使用鸿蒙的ArkUI框架,实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...
