拼图游戏逻辑分析和源码分享(复制黏贴到本地就能玩)

前言

最近看到一篇博客,实现了拼图游戏并分享了源码。我感觉很好玩,就拿来改了改,实现了一个自己的版本。核心逻辑没有变,主要是优化了样式和交互,增加了些游戏的趣味性。

原文代码实现是用原生 js 面向对象的方式写的,这让习惯了使用 vue 语法糖的我,再次感到了用原生 js 好麻烦。好在,最近也是在重温原生js,打算再把基础学扎实一点,所以我遵循了原作者,也先用原生 js 做优化,后面还是打算改写为 vue 版本,是为了可以打包为移动端的 App,这样就可以在手机上玩了,哈哈。想拼什么图,就拼什么图,完美。

参考博文

https://blog.csdn.net/dkm123456/article/details/111991734#comments_15924279

拼图逻辑分析

1、一张图分割成几块,怎么分割?

定义构造函数,初始化数据。

function Jigsaw(row,boxWidth){
  this.row=row;
  this.itemWidth= boxWidth/this.row;
  this.fragment=[];//拼图碎片的dom数组
  this.originalKeys=[];//拼图碎片的下标,记录最初的正确顺序,方便后面对照拼图是否正确完成。
  this.keys=[];//拼图碎片的下标,游戏开始时会被打乱顺序。
  this.len=this.row*this.row;
  this.init();
}

拼图的尺寸是固定的,难度系数,确定了一张图分割为横纵的多少块,这样每块的宽高也就可以计算了。

分割使用背景图 background 属性,定位呈现整张图的固定部分区域。

background-position 属性设置背景图像的起始位置。

以 900px 的拼图大小分割为 3*3 的拼图碎片为例

(x , y)

x 随着每次换行 归零

y 随着每次换行 自增

游戏初始化

  //初始化
  init:function(){
    var fragment=dom.createDocumentFragment();
    var url = imgView.src;
    for(var i=0;i<this.len;i++){
        var div=dom.createElement('div');  
        div.style.cssText=`
          background:url(${url}) no-repeat -${(i%this.row)*this.itemWidth}px -${Math.floor(i/this.row)*this.itemWidth}px;
          height:${this.itemWidth}px;
          width:${this.itemWidth}px;
        `;

        this.fragment.push(div);//每个拼图碎片,都对应唯一的keys[i] 和 originalKeys[i]
        this.keys.push(i);
        this.originalKeys.push(i);
        fragment.appendChild(div);
    }
    box.innerHTML="";
    box.appendChild(fragment);
  },

完整的拼图就是多个应用了背景图属性的 div 组合而成的。

2、随机位置实现

随机位置实现,首先要打乱拼图碎片的下标数组。

arrayObject.sort(sortby)

排序函数的巧妙使用,用来打乱拼图碎片的下标数组。

sort 一般用于给乱序的数组排序,这里反其道用之,给正序的数组打乱顺序。参数 a b是数组中的元素。新的排序 依据 排序函数的返回值 来决定 a 和 b 在数组中的前后位置。

this.keys.sort(function(a,b){

      console.log('a = '+a+' , b = '+b)

      //return a-b

      //return Math.random()>0.5?1:-1;

})

乱序

this.keys.sort(function(a,b){

      console.log("a = "+a+' , b = '+b)

      return Math.random()>0.5?1:-1;

})

经测试,这里 sort()不传参数 a,b 也是可以的,应该是sort()方法在源码实现时,做了默认参数处理。

根据拼图碎片的随机下标,获取拼图碎片的随机 dom ,然后依次给拼图碎片应用绝对定位。

以 900px 的拼图大小分割为 3*3 的拼图碎片为例(原理同上使用背景图定位,所以后面代码实现也可以同上优化)

(x , y)

x 随着每次换行 归零

y 随着每次换行 自增

最外层相对定位,拼图碎片绝对定位。

this.item 是存储的拼图碎片 element 数组

也是在此时,给每个拼图碎片绑定鼠标事件。

start:function(){
    //随机位置
    this.keys.sort(function(a,b){
      return Math.random()>0.5?1:-1;
    })

    var keys = this.keys;//随机打乱的拼图碎片下标
    var colNum=0;
    var rowNum=0;

    box.innerHTML="";

    for(var i=0;i<keys.length;i++){
      if(i>0){
        if(i%this.row===0){
          rowNum++;
          colNum=0;
        }
      }
    
      var item = this.fragment[keys[i]];
          item.style.position='absolute';
          item.style.left=`${colNum++*this.itemWidth}px`;
          item.style.top=`${rowNum*this.itemWidth}px`;
          item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 index
          item.key=keys[i];//key::item数组元素在 keys 数组中对应的值
      
      this.drag(item);
      box.appendChild(item);
    }
  },

start 方法 可以优化

start:function(){
    //随机位置
    this.keys.sort(function(a,b){
      return Math.random()>0.5?1:-1;
    })

    var keys = this.keys;//随机打乱的拼图碎片下标
    box.innerHTML="";

    //js % 模运算
    //余数指整数除法中被除数未被除尽部分,且余数的取值范围为0到除数之间(不包括除数)的整数。 [1]  例如:27除以6,商数为4,余数为3。
    //一个数除以另一个数,要是比另一个数小的话,商为0,余数就是它自己。 [1]  例如:1除以2,商数为0,余数为1;2除以3,商数为0,余数为2。

    for(var i=0;i<keys.length;i++){
      var item = this.fragment[keys[i]];
          item.style.position='absolute';
          item.style.left=`${(i%this.row)*this.itemWidth}px`;
          item.style.top=`${Math.floor(i/this.row)*this.itemWidth}px`;
          item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 index
          item.key=keys[i];//key::item数组元素在 keys 数组中对应的值
      
      this.drag(item);
      box.appendChild(item);
    }
},

3、鼠标右键的点击问题

拼图交互,设置为鼠标左键选中,右键取消选中。

鼠标右键和系统事件冲突。解决方法是自定义鼠标右键 oncontextmenu 事件。

自定义鼠标右键 oncontextmenu 事件

//设置拖动
  drag:function(item){
    var me=this;//构造函数 Jigsaw
    item.onmousedown=function(e){
      var e = e||window.event;
      //this 当前点击的拼图碎片
      var target = me.findTarget(this);
      if(e.button===0){//左键(键值0)
        if(target){
          //前面已有一块拼图碎片,处于已点击 active 状态
          me.exchange(this,target);
          //单纯点击不增加步数,只有交换后才增加步数。
          //重点在于 在 findTarget 方法中,排除两次点击相同的情况
          steps++;
          stepMsg.innerHTML=steps;
        }else{
          this.moveFlag=true;
          this.className='active';
        }
      }
      e.preventDefault && e.preventDefault();
    }

    //右键(键值2)取消拼图选中
    //oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。
    //注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。
    item.oncontextmenu = function(e) {
      var e = e||window.event;
      //findTarget 传参 undefined 避开同次点击判断的限制
      //取消 active 一般都是同拼图碎片 点击
      var target = me.findTarget();
      if(target){
        target.className='';
        target.moveFlag=false;
      }
      e.preventDefault && e.preventDefault();
    }
  }

4、两块拼图位置交换

exchange:function(from,target){
    var fromLeft = from.style.left;
    var fromTop = from.style.top;
    var fromPos = from.pos;
    var fromKey = from.key;
    var targetLeft = target.style.left;
    var targetTop = target.style.top;
    var targetPos = target.pos;//pos:item数组元素在 keys 数组中对应的下标 index
    var targetKey = target.key;//key::item数组元素在 keys 数组中对应的值
    from.style.left=targetLeft;
    from.style.top=targetTop;
    from.pos=targetPos;
    target.style.left=fromLeft;
    target.style.top=fromTop;
    target.pos=fromPos;

    //交换过后取消 active
    target.className='';
    target.moveFlag=false;

    this.keys.splice(fromPos,1,targetKey);
    this.keys.splice(targetPos,1,fromKey);

    //如果相等表示已经找好了
    if(this.diff(this.originalKeys,this.keys)){
      this.tips();
      gameMsg.innerHTML="太棒了,成功了!";
      //关闭计时器
      clearInterval(timer);
    }else{
      //提示未完成,继续加油
      //随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)
      var i=Math.floor(Math.random()*10);//0~9
      gameMsg.innerHTML=gameMsgArr[i]
    }
},

exchange 方法可以优化

//交换两个div位置 既要交换视图上的位置,也要交换在 keys数组 中的位置。
  exchange:function(from,target){
    //解构语法交换两个变量的值
    [from.style.left,target.style.left]=[target.style.left,from.style.left];
    [from.style.top,target.style.top]=[target.style.top,from.style.top];
    [from.pos,target.pos]=[target.pos,from.pos];

    //交换过后取消 active
    target.className='';
    target.moveFlag=false;
    
    //交换它们在this.keys中的位置

    this.keys.splice(target.pos,1,target.key);
    this.keys.splice(from.pos,1,from.key);

    //如果相等表示已经找好了
    if(this.diff(this.originalKeys,this.keys)){
      this.tips();
      gameMsg.innerHTML="太棒了,成功了!";
      //关闭计时器
      clearInterval(timer);
    }else{
      //提示未完成,继续加油
      //随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)
      var i=Math.floor(Math.random()*10);//0~9
      gameMsg.innerHTML=gameMsgArr[i]
    }
  },

 5、是否完成拼图判断。

  //比较 this.originalKeys , this.keys 两个数组,如果一模一样 证明顺序对了		
  diff:function(a,b){
    var me=this;
    //Array.isArray() 用于确定传递的值是否是一个 Array
    var isArrayA = Array.isArray(a);
    var isArrayB = Array.isArray(b);
    if (isArrayA && isArrayB) {//如果都是数组
      return a.every(function (item, index) {//用every和递归来比对a数组和b数组的每个元素,并返回
        return me.diff(item, b[index]);
      })
    }else{
      return String(a) === String(b)
    }
  },

6、总结

相较原文

1、增加了步数统计(有点问题待优化,单纯点击鼠标不应该增加步数)。

2、增加了文字提示(只要拼图没完成,就会随机文字提示,如果有音效就更棒了)。

3、增加了耗时统计,这样就知道拼图的快慢了。

4、顺序图片,改为随机图片,也是为了好玩,图库里的图片越多越好玩,有的简单,有的复杂。

5、难度设置由纵横两个维度,统一为一个维度。这样是为了保证方形的拼图,因为我觉得方形的图片好看。也是为了简化代码逻辑和视图呈现的好看。

6、原文拼图默认宽高是 600*600,我觉得有点小,改成了 900*900。所以,准备的拼图都是900*900的方图。如果是其他尺寸的图片,则要么拼不全,要么有空白。

7、增加了拼图的分割线。

8、部分代码优化。

我的实现

界面预览

源码分享

 
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;">
<style>
div.game{
  display: flex;
  flex-direction: row;
  justify-content: center;
}

div.left{
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 50px;
  background-color: #f7f7f7;
  margin-right:10px;
  padding:20px;
}

/* 预览图片 */
.imgView{
  width: 300px;
  box-sizing: content-box;
  border: 8px solid #bc6e42;
}

.input{ 
  -webkit-appearance: none;
  background-color: #fff;
  background-image: none;
  border-radius: 4px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  color: #606266;
  display: inline-block;
  font-size: inherit;
  height: 40px;
  line-height: 40px;
  outline: none;
  padding: 0 15px;
  transition: border-color .2s cubic-bezier(.645,.045,.355,1);
  width: 234px;
}

div.btns{
  width: 100%;
  text-align: center;
  border-top:1px solid #999;
  margin-top:30px;
  padding:10px 0;
}

button{
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  outline: none;
  margin: 0 10px;
  transition: .1s;
  font-weight: 500;
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 4px;
}

div.right{
  background-color: #f7f7f7;
  padding:20px;
  margin-left:10px;
}

div.message{
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  text-align: center;
  line-height: 60px;
  font-weight: bold;
}

div.message div{
  margin:0 10px;
}

div.message img{
  width:24px;
}

div.message span{
  padding:0 16px;
  color:#F56200;
}

/* 拼图盒子 */
#box{	
  width: 900px;
  height: 900px;
  box-sizing: content-box;
  position: relative;
}

#box div{
  box-sizing: border-box;
  border: 1px dotted #fff;
  float:left;
  cursor:move;
}

/* 消息盒子 */
#box p{
  box-sizing: border-box;
  line-height:900px;
  text-align:center;
  font-size:60px;
  color:#F56200;
  margin:0;
}

.active{
  border:8px solid #bc6e42 !important;
}
</style>
</head>

<body>
  <div class="container">
    <div class="game">
      <div class="left">
        <div>随机图片:<span id="imgNum">图1</span></div>
        <div><img class="imgView" src='./images/1.jpg'/></div>
        <div>难度系数:<input type='number' id='difficulty' class='input' min=3 max=9 value='3' onchange="gameInit()"></div>
        <div class="btns">
          <button onclick='imgChange()'>换一张图</button>
          <button onclick='startGame()'>开始游戏</button>
        </div>
      </div>

      <div class="right">
        <div class="message">
          <div><img src="./images/step.png" alt="img"><span id='stepMsg'>0</span></div>
          <div><img src="./images/msg.png" alt="img"><span id='gameMsg'>无</span></div>
          <div><img src="./images/time.png" alt="img"><span id='timeMsg'>0</span></div>
        </div>
        <div id='box'></div>
      </div>
    </div>
  </div>
</body>

	

<script>
//全局变量
var isFirst=true;//页面第一次加载
var dom=document;
var timer;
var timeMsg=dom.getElementById("timeMsg");
var stepMsg = dom.getElementById("stepMsg");
var gameMsg = dom.getElementById("gameMsg");
var gameMsgArr=[
  '继续加油!',
  '保持冷静!',
  '马上就要完成了!',
  '快成功了!',
  '还差一点儿!',
  '加油啊',
  '继续保持',
  '行不行啊',
  '吁。。。',
  '快点快点'
];
var box=dom.getElementById("box");
var imgView=dom.getElementsByClassName("imgView")[0]; 
var game;
var steps=0;

function Jigsaw(row,boxWidth){
  this.row=row;
  this.itemWidth= boxWidth/this.row;
  this.fragment=[];//拼图碎片的dom数组
  this.originalKeys=[];//拼图碎片的下标,记录最初的正确顺序,方便后面对照拼图是否正确完成。
  this.keys=[];//拼图碎片的下标,游戏开始时会被打乱顺序。
  this.len=this.row*this.row;
  this.init();
}

Jigsaw.prototype={
  //初始化
  init:function(){
    var fragment=dom.createDocumentFragment();
    var url = imgView.src;
    for(var i=0;i<this.len;i++){
        var div=dom.createElement('div');  
        div.style.cssText=`
          background:url(${url}) no-repeat -${(i%this.row)*this.itemWidth}px -${Math.floor(i/this.row)*this.itemWidth}px;
          height:${this.itemWidth}px;
          width:${this.itemWidth}px;
        `;

        this.fragment.push(div);//每个拼图碎片,都对应唯一的keys[i] 和 originalKeys[i]
        this.keys.push(i);
        this.originalKeys.push(i);
        fragment.appendChild(div);
    }
    box.innerHTML="";
    box.appendChild(fragment);
  },

  start:function(){
    //随机位置
    //sort() 方法用于对数组的元素进行排序。
    //返回值 对数组的引用。请注意,数组在原数组上进行排序,不生成副本。
    //如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。
    //如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:
    // return a-b
    // 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
    // 若 a 等于 b,则返回 0。
    // 若 a 大于 b,则返回一个大于 0 的值。
    // 这里没有用 return a-b ,用的是 return Math.random()>0.5?1:-1; 则 传入的 a,b 根据这个结果排序。

    this.keys.sort(function(a,b){
      return Math.random()>0.5?1:-1;
    })

    var keys = this.keys;//随机打乱的拼图碎片下标
    box.innerHTML="";

    //js % 模运算
    //余数指整数除法中被除数未被除尽部分,且余数的取值范围为0到除数之间(不包括除数)的整数。 [1]  例如:27除以6,商数为4,余数为3。
    //一个数除以另一个数,要是比另一个数小的话,商为0,余数就是它自己。 [1]  例如:1除以2,商数为0,余数为1;2除以3,商数为0,余数为2。

    for(var i=0;i<keys.length;i++){
      var item = this.fragment[keys[i]];
          item.style.position='absolute';
          item.style.left=`${(i%this.row)*this.itemWidth}px`;
          item.style.top=`${Math.floor(i/this.row)*this.itemWidth}px`;
          item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 index
          item.key=keys[i];//key::item数组元素在 keys 数组中对应的值
      
      this.drag(item);
      box.appendChild(item);
    }
  },

  //寻找 active moveFlag = true 
  findTarget:function(current){
    //从列表中查找是否已经有选择的目标了 
    for(var i=0;i<this.fragment.length;i++){
      //排除两次点击的一样
      if(current!==this.fragment[i]&&this.fragment[i].moveFlag){
        return this.fragment[i];
      }
    }

    // findTarget 使用 find 始终返回 undefined ,猜测可能是因为 this.item 数组元素为 element dom 对象,所以不能用 find 方法。
    // find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。
    // find() 方法为数组中的每个元素都调用一次函数执行:
    // 当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。
    // 如果没有符合条件的元素返回 undefined

    // this.fragment.find(item=>{
    //   return current!==item&&item.moveFlag;
    // })
  },

  //交换两个div位置 既要交换视图上的位置,也要交换在 keys数组 中的位置。
  exchange:function(from,target){
    //解构语法交换两个变量的值
    [from.style.left,target.style.left]=[target.style.left,from.style.left];
    [from.style.top,target.style.top]=[target.style.top,from.style.top];
    [from.pos,target.pos]=[target.pos,from.pos];

    //交换过后取消 active
    target.className='';
    target.moveFlag=false;
    
    //交换它们在this.keys中的位置
    //arrayObject.splice(index,howmany,item1,.....,itemX)
    //返回值:由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。
    //index	必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
    // howmany	必需。要删除的项目数量。如果设置为 0,则不会删除项目。
    // item1, ..., itemX	可选。向数组添加的新项目。

    this.keys.splice(target.pos,1,target.key);
    this.keys.splice(from.pos,1,from.key);

    //如果相等表示已经找好了
    if(this.diff(this.originalKeys,this.keys)){
      this.tips();
      gameMsg.innerHTML="太棒了,成功了!";
      //关闭计时器
      clearInterval(timer);
    }else{
      //提示未完成,继续加油
      //随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)
      var i=Math.floor(Math.random()*10);//0~9
      gameMsg.innerHTML=gameMsgArr[i]
    }
  },

  //完成后倒计时的提示
  tips:function(){
    setTimeout(function(){
      var suc=dom.createElement('p');
      suc.innerHTML='Bingo!';
      box.innerHTML="";
      box.appendChild(suc);
    },500)
  },

  //比较 this.originalKeys , this.keys 两个数组,如果一模一样 证明顺序对了		
  diff:function(a,b){
    var me=this;
    //Array.isArray() 用于确定传递的值是否是一个 Array
    var isArrayA = Array.isArray(a);
    var isArrayB = Array.isArray(b);
    if (isArrayA && isArrayB) {//如果都是数组
      return a.every(function (item, index) {//用every和递归来比对a数组和b数组的每个元素,并返回
        return me.diff(item, b[index]);
      })
    }else{
      return String(a) === String(b)
    }
  },

  //设置拖动
  drag:function(item){
    var me=this;//构造函数 Jigsaw
    item.onmousedown=function(e){
      var e = e||window.event;
      //this 当前点击的拼图碎片
      var target = me.findTarget(this);
      if(e.button===0){//左键(键值0)
        if(target){
          //前面已有一块拼图碎片,处于已点击 active 状态
          me.exchange(this,target);
          //单纯点击不增加步数,只有交换后才增加步数。
          //重点在于 在 findTarget 方法中,排除两次点击相同的情况
          steps++;
          stepMsg.innerHTML=steps;
        }else{
          this.moveFlag=true;
          this.className='active';
        }
      }
      e.preventDefault && e.preventDefault();
    }

    //右键(键值2)取消拼图选中
    //oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。
    //注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。
    item.oncontextmenu = function(e) {
      var e = e||window.event;
      //findTarget 传参 undefined 避开同次点击判断的限制
      //取消 active 一般都是同拼图碎片 点击
      var target = me.findTarget();
      if(target){
        target.className='';
        target.moveFlag=false;
      }
      e.preventDefault && e.preventDefault();
    }
  }
}

//页面初始化
function gameInit(){
  //关闭计时器
  if(!timer){
    timer=null;
  }else{
    //关闭计时器
      clearInterval(timer);
  }

  if(isFirst){//只执行一次 只在页面第一次初始化时执行
    isFirst=false;
    //随机的一张图
    var index=Math.floor(Math.random()*10)+1;
    imgView.src='./images/'+index+'.jpg';
    var imgNum=dom.getElementById("imgNum");
    imgNum.innerHTML="图"+index;
  }
  
  steps=0;
  stepMsg.innerHTML=0;
  timeMsg.innerHTML=0;
  gameMsg.innerHTML='无';
  var difficulty=dom.getElementById("difficulty").value;
  game = new Jigsaw(difficulty,900);
}

//开始游戏
function startGame(){
  game.start();	
  timeStart();
}

//随机的一张图
function imgChange(){
  isFirst=true;
  gameInit();
}

function timeStart(){
  //防止多次点击开始游戏启动多个计时器
  //关闭计时器
  if(!timer){
    timer=null;
  }else{
    //关闭计时器
      clearInterval(timer);
  }

  var time=0;
  timer=setInterval(function(){
    timeMsg.innerHTML=time++;
  })
}

//页面初始化
gameInit();	

</script>
</html>

 

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Irene1991

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值