CocosCreator教程(入门篇)【转】

系列教程
CocosCreator教程(初识篇)
CocosCreator教程(编辑器篇)

一、项目结构

ProjectName(新建项目)
├──assets
├──library
├──local
├──packages
├──settings
├──temp
└──project.json

 

子结构 功能
assets 与资源管理器的内容同步,游戏的核心目录(每个文件都有相应.meta文件)
library 这里文件的结构和资源的格式将被处理成最终游戏发布时需要的形式
local 编辑器使用习惯记录(如:窗体布局)
settings 项目设置
project.json 版本控制,必须与assets共同存在
build 打包导出目录,构建项目时,自动生成

PS:.meta文件——记录某资源在项目中的唯一标识,以及其配置信息,只有在编辑器中对资源做修改,.meta文件才会实时变化。因此,不要在编辑器外,对资源的内容进行操作。

 

二、资源分类

1、场景(scene)

自动释放资源:切换场景后,上一个场景中的资源,从内存中释放。
延迟加载资源:意味着不用等待所有资源加载完毕,才显示场景。(快速切换场景,资源陆续在画面显示)

2、贴图(texture)

普通图,子层为一张spriteFrame。

3、预制(prefab)

创建方式:拖拽场景节点,到资源管理器。

4、图集(atlas)

精灵图,子层为多张spriteFrame。(精灵图合成软件:TexturePacker、Zwoptex)

5、自动图集(auto-atlas)

打包时,将所在目录中的所有碎图,合成为图集。

6、艺术数字(label-atlas)

数字为内容的图集。

7、字体(font)

动态字体:.ttf
位图字体:.fnt + .png(存在于同一目录)

8、粒子(particle)

小型动画

9、声音(audio)

模式:web audio、dom audio

10、骨骼动画(spine / dragonBones)

文件格式 功能
.json 骨骼数据
.png 图集纹理
.txt / .atlas 图集数据

11、瓦片图(tiledMap)

文件格式 功能
.tmx 地图数据
.png 图集纹理
.tsx tileset 数据配置文件

12、文本(text)

13、脚本(script)

14、json

三、资源小知识点

1、跨项目导入导出资源

操作流程:
(1)导出:文件 => 资源导出,选择 .fire场景文件,输出assets目录的 .zip压缩包。
(2)导入:文件 => 资源导入,选择压缩包源路径、解压路径,输出assets目录内容。

2、图像资源自动剪裁

基于size mode,尽量去除spriteFrame无像素的部分,减小图片尺寸。

四、场景小知识点

1、场景中的元素,即是节点,可内嵌组件。

2、坐标系

类别 坐标轴方向
cocos坐标系(世界、本地坐标系) x右、y上、z外
设备屏幕坐标系 x右、y下

3、锚点

作用:用于变换、子节点定位基准。

五、子系统重点

1、渲染系统

对摄像机、渲染组件的了解。

2、UI系统

对widget、layout等UI组件的了解。

3、动画系统

(1)创建动画的基本流程
(2)时间曲线(双击动画线,进入编辑窗口)
(3)事件管理(双击游标、加减按钮控制参数个数)
(4)脚本控制

4、物理系统

碰撞组件(普通碰撞)
(1)editing——是否为编辑模式
(2)regenerate points——计算图形边界,自定生成控制点,数值为控制点的生成密度 / 准确度
(3)ctrl + 点击——删除控制点
(4)组件类型:矩形、圆形、多边形
(5)设置碰撞组(项目 => 项目设置 => 分组设置):
制定分组 => 匹配分组 => 碰撞组件所在节点上,设置所属分组
(6)脚本控制

Box2D物理引擎(高级碰撞)

5、音频系统

(1)audioSource组件
(2)脚本控制

六、脚本开发

1、使用 cc.Class 声明类型

(1)定义 CCClass

var Sprite = cc.Class({
    //...
});

(2)实例化

var obj = new Sprite();

(3)判断类型

cc.log(obj instanceof Sprite);       //使用原生JS的instanceof 

(4)构造函数(ctor)

var Sprite = cc.Class({
    //使用ctor声明构造函数
    ctor: function () {
        cc.log(this instanceof Sprite);
    }
});

(5)实例方法

var Sprite = cc.Class({
    // 声明一个名叫 "print" 的实例方法
    print: function () { }
});

(6)继承(extends)

// 父类
var Shape = cc.Class();
// 子类
var Rect = cc.Class({
    //使用 extends 实现继承
    extends: Shape
});

(7)父构造函数

var Shape = cc.Class({
    ctor: function () {
        cc.log("Shape");    // 实例化时,父构造函数会自动调用,
    }
});
 
var Rect = cc.Class({
    extends: Shape
});
 
var Square = cc.Class({
    extends: Rect,
    ctor: function () {
        cc.log("Square");   // 再调用子构造函数
    }
});
 
var square = new Square();

(8)完整声明属性

//简单类型声明
properties: {
    score: {
        //这几个参数分别指定了 score 的默认值为 0,在 属性检查器 里,其属性名将显示为:“Score (player)”,并且当鼠标移到参数上时,显示对应的 Tooltip。
        default: 0,
        displayName: "Score (player)",
        tooltip: "The score of player",
    }
}
 
//数组声明
properties: {
    names: {
        default: [],
        type: [cc.String]   // 用 type 指定数组的每个元素都是字符串类型
    },
 
    enemies: {
        default: [],
        type: [cc.Node]     // type 同样写成数组,提高代码可读性
    },
}
 
//get/set 声明
properties: {
    width: {
        get: function () {
            return this._width;
        },
        set: function (value) {
            this._width = value;
        }
    }
}

properties常用参数

参数 作用
default 默认值
type 限定属性的数据类型
visible 若为false,则不在属性检查器面板中显示该属性
serializable 若为false,则不序列化(保存)该属性
displayName 在属性检查器面板中,显示成指定名字
tooltip 在属性检查器面板中,添加属性的Tooltip

2、访问节点和组件

(1)获得组件所在的节点

this.node

(2)获得其它组件

this.getComponent(组件名)

(3)获得其它节点及其组件

// Player.js
cc.Class({
    extends: cc.Component,
    properties: {
        player: {
            default: null,
            type: cc.Node
        }
    }
});
 
//如果你将属性的 type 声明为 Player 组件,当你拖动节点 "Player Node" 到 属性检查器,player 属性就会被设置为这个节点里面的 Player 组件
// Cannon.js
var Player = require("Player");
cc.Class({
    extends: cc.Component,
    properties: {
        // 声明 player 属性,这次直接是组件类型
        player: {
            default: null,
            type: Player
        }
    }
});
 
//查找子节点
//返回子节点数组
this.node.children
//返回对应的子节点
this.node.getChildByName(子节点名);
//查找后代节点
cc.find(子节点/.../后代节点, this.node);
//全局查找节点
cc.find(场景/节点/节点/...);

(4)访问已有变量里的值(通过模块访问

//专门开设一个中介模块,导出接口;在其他模块进行节点、组件、属性的操作
// Global.js
module.exports = {
    backNode: null,
    backLabel: null,
};
 
// Back.js
var Global = require("Global");
cc.Class({
    extends: cc.Component,
    onLoad: function () {
        Global.backNode = this.node;
        Global.backLabel = this.getComponent(cc.Label);
    }
});
 
// AnyScript.js
var Global = require("Global");
cc.Class({
    extends: cc.Component,
    start: function () {
        var text = "Back";
        Global.backLabel.string = text;
    }
});

3、常用节点和组件接口

(1)节点状态和层级操作

//激活/关闭节点
this.node.active = true;
this.node.active = false;
 
//更改节点的父节点
this.node.parent = parentNode;
 
//索引节点的子节点
//返回子节点数组
this.node.children
//返回子节点数量
this.node.childrenCount

(2)更改节点的变换(位置、旋转、缩放、尺寸)

//更改节点位置
//分别对 x 轴和 y 轴坐标赋值
this.node.x = 100;
this.node.y = 50;
//使用setPosition方法
this.node.setPosition(100, 50);
this.node.setPosition(cc.v2(100, 50));
//设置position变量
this.node.position = cc.v2(100, 50);
 
//更改节点旋转
this.node.rotation = 90;
this.node.setRotation(90);
 
//更改节点缩放
this.node.scaleX = 2;
this.node.scaleY = 2;
this.node.setScale(2);
this.node.setScale(2, 2);
 
//更改节点尺寸
this.node.setContentSize(100, 100);
this.node.setContentSize(cc.size(100, 100));
this.node.width = 100;
this.node.height = 100;
 
//更改节点锚点位置
this.node.anchorX = 1;
this.node.anchorY = 0;
this.node.setAnchorPoint(1, 0);

(3)颜色和不透明度

//设置颜色
mySprite.node.color = cc.Color.RED;
//设置不透明度
mySprite.node.opacity = 128;

4)常用组件接口
cc.Component 是所有组件的基类,任何组件都包括如下的常见接口:

接口 作用
this.node 该组件所属的节点实例
this.enabled 是否每帧执行该组件的 update 方法,同时也用来控制渲染组件是否显示
update(dt) 作为组件的成员方法,在组件的 enabled 属性为 true 时,其中的代码会每帧执行
onLoad() 组件所在节点进行初始化时(节点添加到节点树时)执行
start() 会在该组件第一次 update 之前执行,通常用于需要在所有组件的 onLoad 初始化完毕后执行的逻辑

4、生命周期

函数名 描述
onLoad 在节点首次激活时触发,或者所在节点被激活的情况下触发
start 在组件首次激活前
update 动画更新前
lateUpdate 动画更新后
onEnable 当组件的 enabled 属性从 false 变为 true 时,或者所在节点的 active 属性从 false 变为 true 时(倘若节点首次被创建且 enabled 为 true,则会在 onLoad 之后,start 之前被调用)
onDisable 当组件的 enabled 属性从 true 变为 false 时,或者所在节点的 active 属性从 true 变为 false 时
onDestroy 当组件或者所在节点调用了 destroy()时

5、创建和销毁节点

(1)创建新节点

cc.Class({
  extends: cc.Component,
  properties: {
    sprite: {
      default: null,
      type: cc.SpriteFrame,
    },
  },
  start: function () {
    //动态创建节点,并将它加入到场景中
    var node = new cc.Node('Sprite');
    var sp = node.addComponent(cc.Sprite);
    sp.spriteFrame = this.sprite;
    node.parent = this.node;
  }
});

(2)克隆已有节点

//
cc.Class({
  extends: cc.Component,
  properties: {
    target: {
      default: null,
      type: cc.Node,
    },
  },
  start: function () {
    //克隆场景中的已有节点
    var scene = cc.director.getScene();
    var node = cc.instantiate(this.target);
    node.parent = scene;
    node.setPosition(0, 0);
  }
});

(3)创建预制节点

//
cc.Class({
  extends: cc.Component,
  properties: {
    target: {
      default: null,
      type: cc.Prefab,    //预制
    },
  },
  start: function () {
    var scene = cc.director.getScene();
    var node = cc.instantiate(this.target);
    node.parent = scene;
    node.setPosition(0, 0);
  }
});

(4)销毁节点

//
cc.Class({
  extends: cc.Component,
  properties: {
    target: cc.Node,
  },
  start: function () {
    // 5 秒后销毁目标节点
    //销毁节点并不会立刻被移除,而是在当前帧逻辑更新结束后,统一执行
    setTimeout(function () {
      this.target.destroy();
    }.bind(this), 5000);
  },
  update: function (dt) {
    //判断当前节点是否已经被销毁
    if (cc.isValid(this.target)) {
      this.target.rotation += dt * 10.0;
    }
  }
});

PS:不要使用removeFromParent去销毁节点。
原因:调用一个节点的 removeFromParent 后,它不一定就能完全从内存中释放,因为有可能由于一些逻辑上的问题,导致程序中仍然引用到了这个对象。

6、加载和切换场景

(1)加载和切换

//从当前场景,切换到MyScene场景
cc.director.loadScene("MyScene");

(2)通过常驻节点,进行场景资源管理和参数传递

//常驻节点:不随场景切换,而自动销毁,为所有场景提供持久性信息
//设置常驻节点
cc.game.addPersistRootNode(myNode);
//取消常驻节点,还原为一般场景节点
cc.game.removePersistRootNode(myNode);

(3)场景加载回调

//fn:加载MyScene场景时触发
cc.director.loadScene("MyScene", fn);

(4)预加载场景

//后台预加载场景
cc.director.preloadScene("MyScene", fn);
//有需要时,手动加载该场景
cc.director.loadScene("MyScene", fn);

7、获取和加载资源

(1)资源属性的声明

// NewScript.js
cc.Class({
    extends: cc.Component,
    properties: {
        //所有继承自 cc.Asset 的类型都统称资源,如 cc.Texture2D, cc.SpriteFrame, cc.AnimationClip, cc.Prefab 等
        texture: {
            default: null,
            type: cc.Texture2D
        },
        spriteFrame: {
            default: null,
            type: cc.SpriteFrame
        }
    }
});

(2)静态加载(在属性检查器里设置资源)

// NewScript.js
onLoad: function () {
    //拖拽资源管理器的资源,到属性检查器的脚本组件中,即可在脚本里拿到设置好的资源
    var spriteFrame = this.spriteFrame;
    var texture = this.texture;
    spriteFrame.setTexture(texture);
}

(3)动态加载

//动态加载的资源,需要存放于assets的子目录resources中
 
//加载单个资源
//cc.loader.loadRes(resources的相对路径, 类型(可选), 回调函数)
//加载Prefab资源
cc.loader.loadRes("test assets/prefab", function (err, prefab) {
    var newNode = cc.instantiate(prefab);
    cc.director.getScene().addChild(newNode);
});
//加载SpriteFrame
var self = this;
cc.loader.loadRes("test assets/image", cc.SpriteFrame, function (err, spriteFrame) {
    self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});
 
//批量加载资源
//cc.loader.loadResDir(resources的相对路径, 类型(可选), 回调函数)
//加载test assets目录下所有资源
cc.loader.loadResDir("test assets", function (err, assets) {
    // ...
});
//加载test assets目录下所有SpriteFrame,并且获取它们的路径
cc.loader.loadResDir("test assets", cc.SpriteFrame, function (err, assets, urls) {
    // ...
});
 
//资源浅释放
//cc.loader.releaseRes(resources的相对路径, 类型(可选))
cc.loader.releaseRes("test assets/image", cc.SpriteFrame);
cc.loader.releaseRes("test assets/anim");
//cc.loader.releaseAsset(组件名)
cc.loader.releaseAsset(spriteFrame);
 
// 资源深释放,释放一个资源以及所有它依赖的资源
var deps = cc.loader.getDependsRecursively('prefabs/sample');

(4)加载远程资源和设备资源

//加载远程资源
//远程 url 带图片后缀名
var remoteUrl = "http://unknown.org/someres.png";
cc.loader.load(remoteUrl, function (err, texture) {
    //...
});
//远程 url 不带图片后缀名,此时必须指定远程图片文件的类型
remoteUrl = "http://unknown.org/emoji?id=124982374";
cc.loader.load({url: remoteUrl, type: 'png'}, function () {
    //...
});
 
//加载设备资源
//用绝对路径加载设备存储内的资源,比如相册
var absolutePath = "/dara/data/some/path/to/image.png"
cc.loader.load(absolutePath, function () {
    //...
});

加载限制:
1、原生平台远程加载不支持图片文件以外类型的资源。
2、这种加载方式只支持图片、声音、文本等原生资源类型,不支持SpriteFrame、SpriteAtlas、Tilemap等资源的直接加载和解析。(需要后续版本中的AssetBundle支持)
3、Web端的远程加载受到浏览器的CORS跨域策略限制,如果对方服务器禁止跨域访问,那么会加载失败,而且由于WebGL安全策略的限制,即便对方服务器允许http请求成功之后也无法渲染。

(5)资源的依赖和释放

// 直接释放某个贴图
cc.loader.release(texture);
// 释放一个 prefab 以及所有它依赖的资源
var deps = cc.loader.getDependsRecursively('prefabs/sample');
cc.loader.release(deps);
// 如果在这个 prefab 中有一些和场景其他部分共享的资源,你不希望它们被释放,可以将这个资源从依赖列表中删除
var deps = cc.loader.getDependsRecursively('prefabs/sample');
var index = deps.indexOf(texture2d._uuid);
if (index !== -1)
    deps.splice(index, 1);
cc.loader.release(deps);

8、监听和发射事件

(1)监听事件

//target是可选参数,用于绑定响应函数的调用者
//boolean是可选参数,默认为false,表示冒泡流
this.node.on(event, fn, target, boolean);

(2)关闭监听

this.node.off(event, fn, target, boolean);

(3)发射事件

//为事件函数,提供参数,最多5个
this.node.emit(event, arg1, arg2, arg3);

(4)派送事件

//grandson.js
//升级版的on,冒泡到的节点,全部注册事件
this.node.dispatchEvent( new cc.Event.EventCustom('foobar', true) );
 
//father.js
//在指定的上级节点中,注册相同的事件,阻止事件冒泡,手动停止派送
this.node.on('foobar', function (event) {
  event.stopPropagation();
});

(5)事件对象(回调参数的event对象)

API 名 类型 意义
type String 事件的类型(事件名)
target cc.Node 接收到事件的原始对象
currentTarget cc.Node 接收到事件的当前对象,事件在冒泡阶段当前对象可能与原始对象不同
getType Function 获取事件的类型
stopPropagation Function 停止冒泡阶段,事件将不会继续向父节点传递,当前节点的剩余监听器仍然会接收到事件
stopPropagationImmediate Function 立即停止事件的传递,事件将不会传给父节点以及当前节点的剩余监听器
getCurrentTarget Function 获取当前接收到事件的目标节点
detail Function 自定义事件的信息(属于 cc.Event.EventCustom)
setUserData Function 设置自定义事件的信息(属于 cc.Event.EventCustom)
getUserData Function 获取自定义事件的信息(属于 cc.Event.EventCustom)

9、节点系统事件

(1)鼠标事件类型和事件对象

枚举对象定义 对应的事件名 事件触发的时机
cc.Node.EventType.MOUSE_DOWN mousedown 当鼠标在目标节点区域按下时触发一次
cc.Node.EventType.MOUSE_ENTER mouseenter 当鼠标移入目标节点区域时,不论是否按下
cc.Node.EventType.MOUSE_MOVE mousemove 当鼠标在目标节点在目标节点区域中移动时,不论是否按下
cc.Node.EventType.MOUSE_LEAVE mouseleave 当鼠标移出目标节点区域时,不论是否按下
cc.Node.EventType.MOUSE_UP mouseup 当鼠标从按下状态松开时触发一次
cc.Node.EventType.MOUSE_WHEEL mousewheel 当鼠标滚轮滚动时

 

函数名 返回值类型 意义
getScrollY Number 获取滚轮滚动的 Y 轴距离,只有滚动时才有效
getLocation Object 获取鼠标位置对象,对象包含 x 和 y 属性
getLocationX Number 获取鼠标的 X 轴位置
getLocationY Number 获取鼠标的 Y 轴位置
getPreviousLocation Object 获取鼠标事件上次触发时的位置对象,对象包含 x 和 y 属性
getDelta Object 获取鼠标距离上一次事件移动的距离对象,对象包含 x 和 y 属性
getButton Number cc.Event.EventMouse.BUTTON_LEFT或cc.Event.EventMouse.BUTTON_RIGHT或cc.Event.EventMouse.BUTTON_MIDDLE

(2)触摸事件类型和事件对象

枚举对象定义 对应的事件名 事件触发的时机
cc.Node.EventType.TOUCH_START touchstart 当手指触点落在目标节点区域内时
cc.Node.EventType.TOUCH_MOVE touchmove 当手指在屏幕上目标节点区域内移动时
cc.Node.EventType.TOUCH_END touchend 当手指在目标节点区域内离开屏幕时
cc.Node.EventType.TOUCH_CANCEL touchcancel 当手指在目标节点区域外离开屏幕时

 

API 名 类型 意义
touch cc.Touch 与当前事件关联的触点对象
getID Number 获取触点的 ID,用于多点触摸的逻辑判断
getLocation Object 获取触点位置对象,对象包含 x 和 y 属性
getLocationX Number 获取触点的 X 轴位置
getLocationY Number 获取触点的 Y 轴位置
getPreviousLocation Object 获取触点上一次触发事件时的位置对象,对象包含 x 和 y 属性
getStartLocation Object 获取触点初始时的位置对象,对象包含 x 和 y 属性
getDelta Object 获取触点距离上一次事件移动的距离对象,对象包含 x 和 y 属性

(3)其它事件

枚举对象定义 对应的事件名 事件触发的时机
position-changed 当位置属性修改时
rotation-changed 当旋转属性修改时
scale-changed 当缩放属性修改时
size-changed 当宽高属性修改时
anchor-changed 当锚点属性修改时

PS:枚举对象定义、事件名等价,在回调参数中,作用相同。

10、全局系统事件

//全局系统事件的类型
cc.SystemEvent.EventType.KEY_DOWN    //键盘按下
cc.SystemEvent.EventType.KEY_UP    //键盘释放
cc.SystemEvent.EventType.DEVICEMOTION    //设备重力传感
 
//绑定、解除全局系统事件
cc.systemEvent.on(event, fn, target, boolean);
cc.systemEvent.off(event, fn, target, boolean);

11、动作系统(变换系统)

(1)动作控制

// 执行动作
node.runAction(action);
// 停止一个动作
node.stopAction(action);
// 停止所有动作
node.stopAllActions();
 
// 给 action 设置 tag
var ACTION_TAG = 1;
action.setTag(ACTION_TAG);
// 通过 tag 获取 action
node.getActionByTag(ACTION_TAG);
// 通过 tag 停止一个动作
node.stopActionByTag(ACTION_TAG);

(2)容器动作

//顺序执行
cc.sequence(action1, action2, ...);
//并发执行
cc.spawn(action1, action2, ...);
//指定次数,重复执行
cc.repeat(action, times)
//无限次数,重复执行
cc.repeatForever(action)
//改变动作速度倍率,再执行
cc.speed(action, rate)

(3)即时动作

cc.show()    //立即显示
cc.hide()    //立即隐藏

(4)时间间隔动作

cc.moveTo()    //移动到目标位置
cc.rotateTo()    //旋转到目标角度
cc.scaleTo()    //将节点大小缩放到指定的倍数

(5)动作回调

var finished = cc.callFunc(fn, target, arg);

(6)缓动动作

var action = cc.scaleTo(0.5, 2, 2);
//使用easeIn曲线,丰富动作表现
action.easing(cc.easeIn(3.0));

PS:可以使用缓动系统,代替动作系统。(缓动系统的API更简约)

12、计时器

//interval:以秒为单位的时间间隔
//repeat:重复次数
//delay:开始延时
this.schedule(fn, interval, repeat, delay)
this.unschedule(fn)

13、脚本执行顺序

editor: {
        //executionOrder越小,该组件相对其它组件就会越先执行(默认为0)
        //executionOrder只对 onLoad, onEnable, start, update 和 lateUpdate 有效,对 onDisable 和 onDestroy 无效
        executionOrder: 1
    }

14、标准网络接口

(1)XMLHttpRequest——短连接
(2)WebSocket——长连接

15、对象池

对象池的概念
在同一场景中,需要多次进行节点的生成、消失时,假如直接进行创建、销毁的操作,就会很浪费性能。因此,使用对象池,存储需要消失的节点,释放需要生成的节点,达到节点回收利用的目的。

工作流程
(1)初始化对象池

properties: {
    enemyPrefab: cc.Prefab    //准备预制资源
},
onLoad: function () {
    this.enemyPool = new cc.NodePool();
    let initCount = 5;
    for (let i = 0; i < initCount; ++i) {
        let enemy = cc.instantiate(this.enemyPrefab); // 创建节点
        this.enemyPool.put(enemy); // 通过 put 接口放入对象池
    }
}

(2)从对象池请求对象

createEnemy: function (parentNode) {
    let enemy = null;
    if (this.enemyPool.size() > 0) { // 通过 size 接口判断对象池中是否有空闲的对象
        enemy = this.enemyPool.get();
    } else { // 如果没有空闲对象,也就是对象池中备用对象不够时,我们就用 cc.instantiate 重新创建
        enemy = cc.instantiate(this.enemyPrefab);
    }
    enemy.parent = parentNode; // 将生成的敌人加入节点树
    enemy.getComponent('Enemy').init(); //接下来就可以调用 enemy 身上的脚本进行初始化
}

(3)将对象返回对象池

onEnemyKilled: function (enemy) {
    // enemy 应该是一个 cc.Node
    this.enemyPool.put(enemy); // 和初始化时的方法一样,将节点放进对象池,这个方法会同时调用节点的 removeFromParent
}

清除对象池

//手动清空对象池,销毁其中缓存的所有节点
myPool.clear();

七、发布游戏

文章转自:https://blog.csdn.net/ccnu027cs/article/details/101070069

posted @ 2020-03-12 11:18  小猿笔记  阅读(3205)  评论(0编辑  收藏  举报