0
点赞
收藏
分享

微信扫一扫

贪吃蛇JS实现(超详细,万字解析版)

ixiaoyang8 2022-01-11 阅读 65

前言

小伙伴们好呀,本人这次呕心沥血为大家带来一个做了绝对收获满满的项目。
分析到位,也是第一次写这么长的文章,十分期望大家的反馈✨✨

本文是对B站尚硅谷TypeScript贪吃蛇项目的完整解析。
原视频对TypeScript讲的很好,很建议有时间去观看。对于时间不充裕的小伙伴
此文的目的就是让大家以少的时间得到大的收获。
项目环境搭建是一个难点,小伙伴们看不明白可以视频研究,保证视频中会提到相关知识点。因为这对于代码的编写过程起到较大的帮助,但是不会影响到最终代码的产出。(意思就是看不懂直接拿我整合好的代码往上扔就行)。
可以直接评论区留言,或者私信我直接发你完整项目方便对照研究。

目录

1.准备阶段:

编写程序用的是VSCode。
需要大致了解基本的html,css,javascript相关知识。
对Typescript,node.js有初步理解。
掌握预处理器less
打包工具webpack整理代码
该项目不会对过旧浏览器进行兼容,尽量使用chrome浏览器验证效果

2.项目分析:

总共有三个类对象:蛇,食物,场景。并设置控制模块
要准备4个js子模块,最后在设置一个index模块做整合

在这里插入图片描述

Sound.js是我设置的BGM,大家也可以自己设置一个作为点缀

1.场景模块需求

样式和结构:
1.具有长和宽的容器,容器内分成蛇移动的场景记分牌上下两个板块
2.蛇移动的场景设置边界线
3.记分牌记录蛇吃到食物的分数以及等级(等级越高蛇速度越快)
逻辑:
1.蛇吃到食物分数涨一分,每涨一定分数等级提高一级
2.等级设有上限
3.等级和蛇移动速度有关

2.食物模块需求

样式和结构:处于蛇移动的场景中
逻辑:
1.开局自动生成在场景中任意位置
2.蛇吃掉以后消失并刷新在新的位置

3.蛇模块需求

样式和结构:
1.蛇整体在移动的场景中
2.蛇分成蛇头和蛇身体两个部分
逻辑:
1.开局只有蛇头,蛇身体每吃一个食物涨一节
2.蛇头碰到自己身体或者场景模块中的边界线游戏结束
3.蛇持续前进只能改变方向,不能停下。

4.控制模块需求

逻辑:
1.按任意键开始游戏
2.只能通过四个方向键改变蛇前进的方向
3.能检测蛇死亡,蛇是否吃到食物
4.控制分数和等级是否相应增长,食物是否刷新等逻辑

以上是大致要实现的需求,作者这里给出一些优化方向,各位可自行实现:
1.蛇,食物,场景,页面自身的美化
2.加入暂停功能,
3.加入BGM,音效等
4.场景复杂度提升:如随机生成地刺,动态调整场景大小等
5.按空格建加速功能
6.加入自动重开功能
7.随着分数的提高出现鼓励文字,死亡出现游戏结束文字样式
8.改成吃豆人

3.项目搭建

1.TS转JS的配置文件

 //拥有了该文件,一个tsc指令即可直接编译所有ts文件,该文件就是ts的配置文件
    //extends定义继承哪个
    //路径:**表示任意目录*表示任意文件
{
  //include(指定哪些ts文件需要被编译)
  //路径:**表示任意目录,*表示任意文件
    "include":[
        "./'你要被编译的ts的文件根目录路径'/**/*"
    ],
    //编译器的选项
    "compilerOptions":{
        //指定要使用的模块化的规范
        "module":"ES2015",
        //target:用来被编译的ES的版本
        "target":"ES2015",
        //严格模式开启
        "strict":true,
        //错误代码不进行编译
        "noEmitOnError":false
    }
}

要点:
1.我们要在项目中创建一个名称为tsconfig.json的文件作为配置文件
2.其作用是当我们以集成终端形式打开项目并输入tsc指令时,将项目下我们所有的ts文件转换成js文件。
3.该文件主要是指定一些ts转译js的规范
4.如果不写任何内容,光创建这个文件也可以,因为这个文件不创建就无法使用tsc命令。
5.tsc命令只能编译一次, tsc -w表示持续监视,后续会自动编译
6.使用webpack不必用tsc命令进行编译。下面会讲到

2.准备Node.js

在node.js官网上下载Node.js放入和项目相同的硬盘中,
效果如下:在这里插入图片描述
在这里插入图片描述
并建议node_modules文件夹复制一份放入项目中
要点:
1.我们需要通过node.js下载webpack以及其它模块
2.国内下载可能网速缓慢,可以尝试使用一些镜像服务器下载。

3.package.json包配置文件

使用webpack打包工具同样需要名为package.json的文件,进行打包的配置。
如告诉该项目需要哪些依赖。
首先

{
    "name": "snake",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack",
        "start": "node E:/VScode/贪吃蛇项目/list/bundle.js"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "express": "^4.17.1"
    },
    # 下列显示的是开发依赖
    "devDependencies": {
    	# css-loader用于整合css和webpack
        "css-loader": "^6.5.1",
        "html-webpack-plugin": "^4.5.0",
        # style-loader用于整合css和webpack
        "style-loader": "^3.3.1",
        # ts-loader用于整合typescript和webpack
        "ts-loader": "^9.2.6",
        # ts核心包
        "typescript": "^4.4.4",
        # 下载完后可以在集成终端中使用webpack的命令
        "webpack": "^5.64.0",
        "webpack-cli": "^4.9.0"
    }
}

要点:
1.确保你的Node.js安装好了
2.在Vscode中右键本项目选择在集成终端中打开并输入代码npm init -y 进行项目初始化,一般这个时候就会在你的项目中生成一个初步的package.json文件,然后我们进一步完善
3.该json文件是不能写注释的,粘贴代码时,请删去我的注释
4.在集成终端中输入指令npm i -D webpack webpack-cli typescript ts-loader用来下载相关依赖(如果可以看见package.json的depDependencies中更新了你下载的依赖表示下载成功)。i表示install下载的意思,-D意思是下载的作为依赖使用
5.继续输入指令npm i -D css-loader 等依赖,这些后面都有用
6.请注意上述代码中scripts中的"build": "webpack"键值对,这个设置说明我们可以用npm run build的代码来启用webpack打包工具(详情见视频09的15分钟到20分钟)

4.webpack.config.js打包工具配置

// 引入一个包
const path = require('path')
//引入html插件
const HTMLWebpackPlugin = require('html-webpack-plugin')
//webpack中所有的配置信息都要写在module.exports中
module.exports = {
    //指定入口文件
    entry:"./src/index.ts",
    
    
    
    //指定打包文件所在的目录
    output:{
        //指定打包文件的目录
        path:path.resolve("E:/VScode/贪吃蛇项目/list"),
        //filename打包后文件的名字一般叫bundle.js
        filename:"bundle.js",

    },
    //指定webpack打包时要使用模块
    module:{
        //指定要加载的数据
        rules:[
            {
                //test指定规则生效的文件 即所有以.ts结尾的文件
                test:/\.ts$/,
                use:'ts-loader',
                //指定要排除的文件
                exclude:/node-modules/
            },
            {
                //匹配哪些文件
                test: /\.css$/,
                //使用哪些loader进行处理
                use:[
     //use数组中的执行顺序是从后往前依次执行
     //创建一个style标签将js中的样式资源插入进行,添加到head中生效
                    'style-loader',
      //将css文件变成commonjs模块加载js中,里面内容是样式字符串
                    'css-loader'
                ]
            },
            
        ]
        
    },
    plugins:[
        new HTMLWebpackPlugin({
                template:'./src/index.html'
            }) ,
          ],
    //告诉webpack哪些文件可以作为模块被引入
    resolve:{
        extensions:['.ts','.js']
    },
    mode:'development'
}

要点:
1.最后是关于webpack打包工具的相关配置,同样命名一个webpack.config.js
的文件并粘贴上述代码
2.由于webpack相关使用不是本文重点,不过我在几个重要的配置上做了注释,感兴趣的小伙伴可以参考视频09-11
3.output中放入的是打包输出后的文件

至此,TypeScript编译配置,Node.js配置和webpack配置完毕,可以正式撸代码啦!O(∩_∩)O

4.CSS,HTML搭建

1.HTML

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     //这里引入下面的css
    <link rel="stylesheet" href="./style/index.css">
    <title>贪吃蛇</title>
</head>
<body>
    <!--游戏主容器-->
    <div id="main">
        <p>按下任意方向键开始游戏</p>
        <!--设置游戏的舞台-->
        <div id="stage">
            <!--设置蛇-->
            <div id="snake">
                <!--snake 的身体各个部分-->
                <div id="head"></div>
            </div>
        <!--设置食物-->
        <div id="food">
            <!--添加四个div设置食物样式-->
            <div></div>
            <div></div>
            <div></div>
            <div></div>
        </div>
        </div>
        <!--设置游戏的积分盘-->
        <div id="score-panel">
            <div>
                SCORE: <span id="score">0</span>
            </div>
            <div>
                LEVEL: <span id="level">1</span>
            </div>
        </div>
    </div>
    // 以下为作者添加的BGM代码
    <audio autoplay="autopaly" loop="loop" id="audios">
        <source src="./bgm.mp3" type="audio/mp3" />
    </audio>
</body>
</html>

层级顺序:
主容器 => 蛇 =>蛇头,蛇身
      =>食物
       =>积分盘=>积分,等级

2.CSS

1.首先要记得清除浏览器默认样式,这个网上一抓一大把。
2.本来是用less预处理器编写的然后转译成CSS,这里就直接给转译好的CSS
3.样式方面只要注意长宽是10px的倍数,因为后面蛇一次的位移格数默认是10px。
4.此CSS样式中默认开局蛇头居中,食物位置固定

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font: bold 20px "Comic Sans MS";
  position: absolute;
  display: flex;
  flex-direction: column;
  background-color: red;
  flex: 1;
  width: 100%;
  height: 100%;
  # 背景图片任意选择图源
  background: url('bg.jpg') no-repeat;
  background-size: 100% 100%;
  background-attachment: fixed;
}
#main {
  width: 360px;
  height: 420px;
  background-color: #b7d4a8;
  margin: 100px auto;
  border: 10px solid black;
  border-radius: 40px;
  display: flex;
  flex-flow: column;
  align-items: center;
  justify-content: space-around;
}
#main #stage {
  width: 304px;
  height: 304px;
  border: 2px solid black;
  position: relative;
}
#main #stage #food {
  width: 10px;
  height: 10px;
  background-color: black;
  left: 100px;
  top: 100px;
  position: absolute;
  display: flex;
  flex-flow: row wrap;
  justify-content: space-between;
  align-content: space-between;
}
#main #stage #food > div {
  width: 4px;
  height: 4px;
  border: 1px solid #b7d4a8;
}
#main #stage #snake > div {
  width: 10px;
  height: 10px;
  background-color: #fff;
  border: 1px solid #b7d4a8;
  position: absolute;
  left: 140px;
  top: 140px;
}
#main #stage #snake #head {
  background-color: black;
}
#main #score-panel {
  width: 250px;
  display: flex;
  justify-content: space-between;
}

此时,页面基本样式已经搭建完毕如下图:

在这里插入图片描述

5.JavaScript注入灵魂

现在只要我们照着项目分析中的需求分别对几个模块的逻辑编写代码即可。
不过首先我们要用ts语言更加严谨的编写。
首先我们还要理解class 类名{} 是构建一个对象模板,该模板中我们能够编写一些属性和方法,这样别的js文件调用该模板对象时就能创建一个符合模板规范的对象。可以类比构造函数和创建实例对象

1.食物模块

//定义食物类Food作为模板对象
class Food{
//定义元素类型为HTMLElement,这样就具备了一些特性
    element: HTMLElement;
    
    constructor(){
//我们根据ID属性值去网页中拿div
//这里加!是因为我们百分之百能够在html中获取到food
        this.element = document.getElementById('food')!;
        this.element.style.left = Math.round(Math.random()*29) * 10 + 'px';
        this.element.style.top = Math.round(Math.random()*29) * 10 + 'px'
    }
    //定义一个获取食物X轴坐标的方法
    get X(){
        return this.element.offsetLeft
    }
    //定义一个获取食物Y轴坐标的方法
    get Y(){
        return this.element.offsetTop
    }
    //定义一个方法能够修改食物的位置
    change(){
 //生成一个随机的位置
 //Math.random()会生成一个0-1的随机数,但是不包含0和1 
 //Math.round()方法表示四舍五入
        let left = Math.round(Math.random()*29) * 10;
        let top = Math.round(Math.random()*29) * 10;
        this.element.style.left = left + 'px'
        this.element.style.top = top + 'px'
    }
}
export default Food;//暴露Food这个类

要点:
1.由于在配置typescript时我们设置了strict模式,因此
this.element = document.getElementById('food')!中如果我们不加!会让程序不确定我们是否一定会获取到food而报错
2.这个之后不会再提,constructor方法中的所有代码都会在该模板对象被实例化的时候执行一次,也就是说,我们运用了随机数random在页面刚刚刷新的时候的食物不会像css中定义的那样处在固定位置,而是一开始就调用了这里的代码进行了一次随机刷新。让游戏更真一点。
3.准备了get()方法可以在控制模块中随时获取food的具体定位
4.change()方法为随机刷新一次food
5.这个之后不会再提,export default Food代码绝对要加在最后。为的是把food这个模板对象暴露给全局,否则别的模块引用都引用不了

2.计分板模块

//定义表示记分牌的类
class ScorePanel{
    score = 0;
    level = 1;
    scoreEle:HTMLElement;
    levelEle:HTMLElement;
    //设置变量限制等级
    maxLevel :number;
    //设置一个变量表示多少分时升级
    upScore:number;
    constructor(maxLevel:number = 10,upScore:number = 10){
        this.scoreEle = document.getElementById('score')!;
        this.levelEle = document.getElementById('level')!;
        this.maxLevel = maxLevel;
        this.upScore = upScore;
    }
    //设置一个加分的方法
    addScore(){
        //使分数自增
        this.scoreEle.innerHTML = ++this.score + ''//需要字符串形式
        //判断分数是多少
        if(this.score % 3 == 0){
            this.levelUp()
        }
    }
    levelUp(){
        //还要设计一个等级上限
        if(this.level < this.maxLevel){
            this.levelEle.innerHTML = ++this.level + ''
        }
    }
}
export default ScorePanel;//暴露ScorePanel这个类

要点:
1.需要两个方法,调用就能提升分数以及等级
2.我们设定了等级的上限不能超过十级,大家可自凭喜好修改
3.如果能大致明白food模块,计分板模块不难明白

3.蛇模块

这是逻辑最复杂的模块,但是大部分都打上了注释,搞定它,就离成功不远啦!

class Snake{
    head:HTMLElement;
//蛇的身体,包括蛇头,HTMLCollection的特性是会实施刷新,
//如果添加新元素,它会自动获取新元素
    bodies:HTMLCollection;
//获取蛇的容器
    element:HTMLElement
//获取p
    p:HTMLElement
    constructor(){
 //querySelctor只会取一个,所以把第一个div当蛇头
 //as HEMLElement类型断言因为querySelector获取到的是元素
 //要断言HTMLElement
        this.head = document.querySelector('#snake > div') as HTMLElement;
        this.bodies = document.getElementById('snake')!.getElementsByTagName('div');
        this.element = document.getElementById('snake')!
        this.p = document.querySelector('#main > p')!
    };
     //获取蛇头的坐标
    get X(){
        return this.head.offsetLeft;
    }
    get Y(){
        return this.head.offsetTop;
    }
    set X(value:number){
 //优化:因为本游戏蛇的移动一次只会改变x,y中的一个值
 //所以可以做一个判断机制,如果值和之前一样就不重新改变CSS样式了
        if(this.X === value){
            return
        }
       
// X的合法值范围在0-290px之间(蛇自身占据stage的10px)
        if(value < 0 || value > 290){
            //说明蛇撞墙了,蛇就死了
            throw console.error("蛇死了");
        }
        
//判断蛇是否掉头,通过和蛇的第一节身体的坐标进行比对
//同时前提要有第一节身体
        if(this.bodies[1]&&(this.bodies[1] as HTMLElement).offsetLeft === value){
            if(value>this.X){
                value = this.X - 10
            }else{
                value = this.X + 10
            }
        }
        this.movebody()
        this.head.style.left = value + 'px'
        //检查有没有撞到自己,必须在坐标发生变化以后检查
        this.checkHeadBody()
    }
    
      
    set Y(value:number){
        if(this.Y === value){
            return
        }
        if(value < 0 || value > 290){
            //说明蛇撞墙了,蛇就死了
            throw console.error("蛇死了");
        }
//判断蛇是否掉头,通过和蛇的第一节身体的坐标进行比对,同时前提要有第一节身体
          if(this.bodies[1]&&(this.bodies[1] as HTMLElement).offsetTop === value){
            //如果发生错误,让蛇向反方向移动,即不会让蛇180°掉头
            if(value>this.Y){
                value = this.Y - 10
            }else{
                value = this.Y + 10
            }
        }
        this.movebody()
        this.head.style.top = value + 'px'
        //检查有没有撞到自己,必须在坐标发生变化以后检查
        this.checkHeadBody()
    }
    //蛇增加身体的方法头的div后面继续加div
    addBody(){
//向Element中添加div insertAdjacentHTML方法两个参数
//第一个是添加HTML标签的位置,第二个是输入的HTML代码
        this.element.insertAdjacentHTML("beforeend","<div></div>")
    }
    //添加一个蛇身体移动的方法
    movebody(){
//将后面的身体设置成前面身体的位置
//并且从尾巴上开始设置避免前面的先移动导致后面身体不知道到底是哪个位置
        //遍历从后往前遍历
        for(let i = this.bodies.length-1;i>0;i--){
            //获取前面身体的位置
            let X = (this.bodies[i-1] as HTMLElement).offsetLeft;
            let Y = (this.bodies[i-1]as HTMLElement).offsetTop;
            //将值设置到当前身体上
            (this.bodies[i] as HTMLElement).style.left = X + 'px';
            (this.bodies[i] as HTMLElement).style.top = Y + 'px' 
        }
    }
    //检查头和身体有没有相撞
    checkHeadBody(){
        //获取所有的身体,检查其是否和蛇头的坐标发生重叠
        for(let i=1;i<this.bodies.length;i++){
            if(this.X === (this.bodies[i] as HTMLElement).offsetLeft && this.Y === (this.bodies[i] as HTMLElement).offsetTop ){
                throw new Error('撞到自己了')
            }
        }
    }
}
export default Snake;

要点:
1.首先它自身只添加了三个功能函数addbody,movebody,和checkbody
2.movebody为什么要从蛇尾开始移动,确实以一般人的观念蛇的确从头开始移动,然而在代码中蛇头先移动会发生什么?没错,蛇后面的身体很难去判断自己的下一步的移动到底是蛇头移动前的位置还是蛇头移动后的位置。
因此,最好的方法就是从不动的蛇尾开始判断前一个身体的坐标,在配合更改自己的坐标即可。
3.为什么get,set,判断蛇是否死亡机制以及之后的蛇移动的代码一定要写在constructor()函数中而不是写在外面?
在后面还有一个控制模块中
首先利用get()方法获得蛇头坐标,当蛇头移动一次以后,立刻刷新后的蛇头坐标反馈给蛇对象
蛇这个对象更新以后constructor代码就会执行一遍,执行过程中首先蛇头的坐标用set()函数重新设置,然后蛇的movebody函数就会执行一次。最后对蛇进行判断死没死。
这样一次代码就执行完成啦。此时整条蛇都前进了一次。然后我们通过定时器定个时间不断让蛇移动就可以了。

4.控制器模块

//引入其它类才能控制
import Snake from "./Snake";
import Food from "./food";
import ScorePanel from "./ScorePanel";
//游戏控制器,控制其它所有类
class GameControl{
    //蛇类
    snake:Snake;
    //食物类
    food:Food;
    //分数类
    scorePanel:ScorePanel;
    //创建一个属性来存储蛇的移动方向,默认设置为空字符串
    direction:string = '';
    //创建一个属性用来记录游戏是否结束
    islive = true
     //创建一个键盘按下的响应函数,这样最方便修改
    keydoenHandler(event:KeyboardEvent){
         //修改direction的属性
        this.direction = event.key;
    }
    
    //定义一个方法用来检测蛇是否吃到食物
    checkEat(x:any,y:any){
        if(x === this.food.X && y === this.food.Y){
            console.log('蛇吃到食物了')
            //改变食物位置
            this.food.change();
            //分数增加一分
            this.scorePanel.addScore();
            //蛇的身体增加一节
            this.snake.addBody()
        }
    }
    
    //该函数用来控制蛇移动
    run(){
        //根据方向this.direction来改变蛇的位置
        //向上/下top增/减,向左/右,left增/减
        //通过赋值我们自己设置snake属性里的x,y属性来获取蛇现在的坐标
        let x = this.snake.X
        let y = this.snake.Y
        //判断蛇头的坐标是否和食物相同
        this.checkEat(x,y)
        //switch来判断传递进来的参数从而移动蛇,
        switch(this.direction){
            case "ArrowUp":
                y -= 10 ;
                break;
            case "ArrowDown":
                y += 10;
                break;
            case "ArrowLeft":
                x -= 10;
                break;
            case "ArrowRight":
                x += 10;
                break;
        }
        
        try{
            //修改完的值再回过来赋值给蛇,snake中的set方法会改变CSS样式
            this.snake.X = x
            this.snake.Y = y
        }catch(e){
            //进入catch,说明捕获到异常,游戏结束,弹出一个提示信息
            this.snake.p.innerHTML = '真可惜,蛇死了'

            this.islive = false
        }
 //开启一个定时器定时调用run函数自己达到一个持续自动移动的效果
 //因为是document调用函数,这里要用bind把this绑定的对象还原成gamecontrol
 //自动判断游戏是否结束
        this.islive && setTimeout(this.run.bind(this), 200-(this.scorePanel.level-1)*20);
    }
    changep(){
        this.snake.p.innerHTML = '游戏开始!'
    }
    //一旦调用会自动执行的constructor方法
    constructor(){
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel();
        this.init()
    }
    //游戏的初始化方法,调用游戏开始
    init(){
//绑定键盘按键按下的事件,但是会导致this的对象指向document,因为是它调用的
//bind就是创建一个新函数,传入的this绑定的是当初所属的GameControl,而不是document        		    
		document.addEventListener('keydown',this.keydoenHandler.bind(this));
        document.addEventListener('keydown',this.changep.bind(this));
        this.run()
    }

}
export default GameControl

要点:
1.添加了4个新方法:检查食物是否被吃到,run()键盘控制蛇头移动,将页面中的文字改成游戏开始(点缀作用)以及init()按下任意按键开始游戏。
2.init中的this对象指代到底是谁请各位自行钻研,此外视频中讲的比较清楚

5.index

import "./style/index.css"
import Food from "./modules/food"
import ScorePanel from "./modules/ScorePanel"
import Snake from "./modules/Snake"
import GameControl from "./modules/GameControl"
// 将以下代码添加到js入口函数内即可
document.addEventListener('keydown', function() {
    let a:any = document.getElementById('audios') 
    a.play()
})
//不要忘记调用
new GameControl()

对于上述的几个模块进行一个最终汇总

6.收尾

对着项目右键以集成终端形式打开并输入指令npm run build然后回车,
目的是用webpack的指令让其打包并输出到我们想要的地方。
出现下图所示基本就是整合成功了。
在这里插入图片描述
webpack会帮你把所有的js模块整合到一起然后自动给你的html文件引用它。如下所示:list为我打包输出的地方
在这里插入图片描述
我们用浏览器打开html文件就能开始玩啦!!
在这里插入图片描述
还是第一次写这么长的文章。都划到这里了,看在这么辛苦码字的份上各位小伙伴小小点个赞不过分吧!!!
(~ ̄▽ ̄)~

往期文章同样精彩哦

HTML相关知识点整理

CSS相关知识点整理(超详解高度塌陷)

JavaScript基本知识整理(循序渐进,有条不紊)

JavaScript基本知识点补充01(详细解释何为原型对象)

为什么jQuery在前端中很经典?时代之光)

举报

相关推荐

0 条评论