js向上向下取整_JavaScript(13) JS 实现动画效果

基本思路

  • 使用 setInterval 把一个动作分解开, 分成步骤来完成.

 <div class="box">div><style>    .box {        position: absolute;        left: 0;        top: 0;        width: 200px;        height: 200px;        background-color: red;    }style><script>    var box = document.querySelector('.box');    var timerId = setInterval(function () {        var left = box.offsetLeft;        left += 5;        box.style.left = left + 'px';    }, 20);script>

停止动画

  • 满足一定条件清除定时器即可

当元素向右移动 500px 就停止

 ... // html css 和上面相同.<script>    var box = document.querySelector('.box');    var timerId = setInterval(function () {        var left = box.offsetLeft;        if (left < 500) {            left += 5;            box.style.left = left + 'px';        } else {            // 元素到达指定位置, 就停止            clearInterval(timerId);        }    }, 20);script>

封装动画函数

可以把刚才的代码封装到专门的函数中.

  • animateRight 向右移动

  • animateLeft 向左移动

  • 同理, 也可以实现向上移动和向下移动(只是操作的元素坐标不同).

  • 同理, 还可以实现任意方向移动(水平和垂直两个坐标叠加移动).

 // 水平向右移动// elem 表示要移动的元素. (必须要带有定位)// distance 表示要移动的距离. // 15ms 作为周期, 意味着大概 1s 是 66 帧function animateRight(elem, distance) {    var srcPos = elem.offsetLeft;    var destPos = srcPos + distance;    var timerId = setInterval(function () {        var curPos = elem.offsetLeft;        if (curPos >= destPos) {            // 到达指定位置了, 不需要再移动了            clearInterval(timerId);        } else {            elem.style.left = curPos + 1 + 'px';        }    }, 15);}// 水平向左移动// 和 animateRight 相比, 三个地方有差别:function animateLeft(elem, distance) {    var srcPos = elem.offsetLeft;    // 差别1    var destPos = srcPos - distance;    var timerId = setInterval(function () {        var curPos = elem.offsetLeft;        // 差别2        if (curPos <= destPos) {            // 到达指定位置了, 不需要再移动了            clearInterval(timerId);        } else {            // 差别3            elem.style.left = curPos - 1 + 'px';        }    }, 15);}

这样后面使用的时候就方便了.

 <button id='btn1'>box1走起button><button id='btn2'>box2走起button><div class="box1">box1div><div class="box2">box2div><style>    .box1 {        position: absolute;        left: 0;        top: 100px;        width: 200px;        height: 200px;        background-color: red;    }    .box2 {        position: absolute;        left: 400px;        top: 400px;        width: 200px;        height: 200px;        background-color: red;    }style><script>    var btn1 = document.querySelector('#btn1');    var box1 = document.querySelector('.box1');    btn1.onclick = function () {        animateRight(box1, 500);    }    var btn2 = document.querySelector('#btn2');    var box2 = document.querySelector('.box2');    btn2.onclick = function () {        animateLeft(box2, 200);    }script>

一点问题: 此时发现, 快速点击按钮的时候, 会给元素设定多个定时器, 导致多个动画叠加, 元素移动很快.

修改方案1: 在设定新定时器判定之前是否有定时器, 如果之前有就不设定新的定时器

 function animateRight(elem, distance) {    var srcPos = elem.offsetLeft;    var destPos = srcPos + distance;    // 先判定之前是否有定时器.     if (elem.timerId) {        return;    }    // 再设定新的定时器    elem.timerId = setInterval(function () {        var curPos = elem.offsetLeft;        if (curPos >= destPos) {            // 到达指定位置了, 不需要再移动了            clearInterval(elem.timerId);            elem.timerId = null;        } else {            elem.style.left = curPos + 1 + 'px';        }    }, 15);}

修改方案2: 在设定新定时器时直接取消旧定时器, 然后设置新的.

 function animateRight(elem, distance) {    var srcPos = elem.offsetLeft;    var destPos = srcPos + distance;    // 先清空元素原有的定时器(把定时器的身份标识保存到 elem 中)    clearInterval(elem.timerId);    // 再设定新的定时器    elem.timerId = setInterval(function () {        var curPos = elem.offsetLeft;        if (curPos >= destPos) {            // 到达指定位置了, 不需要再移动了            clearInterval(elem.timerId);            elem.timerId = null;        } else {            elem.style.left = curPos + 1 + 'px';        }    }, 15);}

两种策略都很有用, 需要根据实际情况决定使用哪种方式更合理.

注意: 当前动画函数的参数为移动的距离. 实际使用的时候也可能需要参数为目标位置.

缓动动画

平时见到的比较多的动画效果, 就是缓动动画. 开始移动速度快, 后来越来越慢.

公式: 移动的步长 = (目标位置 - 现在位置) / 10. 定时器的周期建议是 15ms (此时意味着 FPS 为 66 ).

 function animateRight(elem, distance) {    var srcPos = elem.offsetLeft;    var destPos = srcPos + distance;    if (elem.timerId) {        return;    }    elem.timerId = setInterval(function () {        var curPos = elem.offsetLeft;        if (curPos >= destPos) {            clearInterval(elem.timerId);            elem.timerId = null;        } else {            // 如果没有向上取整, 移动的距离不再精确, 误差 <10px            var step = Math.ceil((destPos - curPos) / 10);            elem.style.left = curPos + step + 'px';        }    }, 15);}

注意,

  • 计算公式中的除数不一定是 10. 可以通过该参数灵活调整动画的速度. (数值越大, 则动画越慢)

  • 上面的步长需要取整, 否则元素到达的位置不一定精确.

  • 取整的时候要注意, 如果步长结果是正值, 则需要向上取整; 如果是负值, 则需要向下取整. 总之就是往绝对值大的方向取整.

给动画函数添加回调

很多时候我们需要当动画结束时能再执行一些动作. 此时就可以给上面的动画函数增加一个回调函数作为参数.

 function animateRight(elem, distance, callback) {    var srcPos = elem.offsetLeft;    var destPos = srcPos + distance;    if (elem.timerId) {        return;    }    elem.timerId = setInterval(function () {        var curPos = elem.offsetLeft;        if (curPos >= destPos) {            clearInterval(elem.timerId);            elem.timerId = null;            // 动画结束时调用回调            if (callback) {                callback();            }        } else {            var step = Math.ceil((destPos - curPos) / 20);            elem.style.left = curPos + step + 'px';        }    }, 15);}

此时可以借助这个回调完成一些功能. 例如动画结束后提示 "动画结束"

点击开始动画

代码示例: 下拉菜单

预期效果

1) 初始情况下, 只显示一个导航

e0847ecb3bcb17612d824f4190f0f22b.png

2) 点击查看详情, 则从上向下弹出一个菜单, 弹出完毕时将 "查看详情" 修改为 "隐藏详情"

098604ee48cff9550ee9e4348171431f.png

实现布局&样式
查看详情
前端JavaC++
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.nav {
position: relative;
width: 100%;
height: 50px;
font-size: 20px;
line-height: 50px;
color: #fff;
background-color: #333;
padding-left: 20px;
}
.item {
width: 100px;
height: 50px;
text-align: center;
cursor: pointer;
}
.nav ul {
position: absolute;
left: 20px;
/* 50px 为显示菜单; -100px 为隐藏菜单 */
/* top: 50px; */
top: -100px;
z-index: -1;
width: 100px;
height: 150px;
background-color: purple;
list-style: none;
}
.nav ul>li {
height: 50px;
padding-left: 0.5em;
cursor: pointer;
}
.nav ul>li:hover {
background-color: orange;
}
实现垂直动画函数

代码思路和之前的 animateLeft / animateRight 一致. 只不过把上下移动的功能集成在一个函数中了.

注意:

  • 要想实现上下都能移动, 需要在判定元素是否到达目标位置时, 使用 == 判定(而不是 <=   >=)

  • 向下移动时, distance 是正值, 需要使用 Math.ceil 向上取整; 向上移动时, distance 是负值, 需要使用 Math.floor 向下取整. (往绝对值大的方向取整). 不取整就会导致最终元素的位置出现误差.

// 动画函数, 垂直方向移动
// distance 为正, 表示向下移动. distance 为负, 表示向上移动
function animateY(elem, distance, callback) {
// 1. 计算目标位置
var destPos = elem.offsetTop + distance;
// 2. 判定当前是否已经有定时器, 如果已经有定时器, 就不再搞新的
if (elem.timerId) {
return;
}
// 3. 设置新定时器
elem.timerId = setInterval(function () {
var curPos = elem.offsetTop;
// 要想实现上下都能移动, 此处需要使用 ==.
// 否则就需要拆成两个函数, 分别是 >= 和 <=
if (curPos == destPos) {
// 4. 元素到达目标位置, 拆除定时器, 并调用回调函数
clearInterval(elem.timerId);
elem.timerId = null;
if (callback) {
callback();
}
} else {
// 5. 元素未到达目标位置
// a) 先计算要移动的步长(缓动动画的公式)
var step = (destPos - curPos) / 10;
// b) 根据 step 的正负, 决定是向上取整还是向下取整(往绝对值大的方向取整)
step = step > 0 ? Math.ceil(step) : Math.floor(step);
// c) 根据步长, 修改元素位置
elem.style.top = curPos + step + 'px';
}
}, 15);
}
调用垂直动画函数
// 给下拉菜单添加上动画效果
var item = document.querySelector('.item');
var ul = document.querySelector('.nav>ul');
item.addEventListener('click', function () {
if (this.innerHTML == '查看详情') {
// 弹出菜单
animateY(ul, 150, function () {
item.innerHTML = '隐藏详情';
});
} else {
// 收缩菜单
animateY(ul, -150, function () {
item.innerHTML = '查看详情';
});
}
});

代码示例: 滚动到顶部

点击 "回到顶部" 按钮, 带有动画效果的滚动到页面最顶端.

  • 核心函数 window.scroll (参数为要滚动的目标位置的 x, y 坐标)

594b78f18eee6c8ee4115c1d424b4e26.png

实现基本布局
回到顶部
body {
height: 2000px;
}
button {
position: fixed;
bottom: 20px;
right: 20px;
}
.section {
height: 400px;
width: 800px;
background-color: red;
margin: 10px auto;
}
实现点击按钮滚动
  • 使用 window.scroll 函数进行滚动

var button = document.querySelector('button');
button.addEventListener('click', function () {
window.scroll(0, 0);
});

这种方式滚动就是一下就滚上去了. 接下来需要添加动画效果

实现滚动动画函数

基于上面的 animateY 函数进行调整.

function animateY(elem, distance, callback) {
var destPos = document.documentElement.scrollTop + distance;
clearInterval(elem.timerId);
elem.timerId = setInterval(function () {
// [1] 通过 scrollTop 获取到当前滚动的位置
var curPos = document.documentElement.scrollTop;
if (curPos == destPos) {
clearInterval(elem.timerId);
elem.timerId = null;
if (callback) {
callback();
}
} else {
var step = (destPos - curPos) / 10;
step = step > 0 ? Math.ceil(step) : Math.floor(step);
// [2] 使用 window.scroll 来滚动页面
window.scroll(0, curPos + step);
}
}, 15);
}
调用动画函数
var button = document.querySelector('button');
button.addEventListener('click', function () {
// window.scroll(0, 0);
animateY(window, -document.documentElement.scrollTop);
});

代码示例: 轮播图

功能
  • 鼠标经过轮播图, 显示左右按钮, 鼠标离开则隐藏.

  • 点击小圆圈, 切换到指定的轮播图.

  • 点击右侧按钮, 则往右显示一张轮播图; 点击左侧按钮, 则往左显示一张轮播图.

  • 鼠标离开轮播图, 每隔一定时间自动播放下一张. 鼠标经过轮播图, 轮播图取消自动轮播.

  • 轮播图播放时下方的小圆圈自动随之变化.

实现布局&样式
>
<
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.carousel {
/* 保证子元素的定位在父元素之内. 子绝父相 */
position: relative;
width: 520px;
height: 280px;
margin: 100px auto;
/* 调试阶段可以不加这个 */
overflow: hidden;
}
.carousel ul {
/* 一定要给 ul 加定位, 否则无法移动 */
position: absolute;
left: 0;
top: 0;
list-style: none;
width: 1000%;
}
.carousel ul>li {
float: left;
}
.carousel .left-arrow,
.carousel .right-arrow {
/* 尺寸位置 */
position: absolute;
width: 25px;
height: 40px;
top: 50%;
margin-top: -20px;
/* 字体颜色 */
font-size: 22px;
line-height: 40px;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
}
.carousel .left-arrow {
left: 0;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
text-align: left;
}
.carousel .right-arrow {
right: 0;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
text-align: right;
}
.carousel ol {
position: absolute;
/* 宽度最好不要写死. 能更好适应内部的 li 的个数 */
/* width: 90px; */
height: 18px;
bottom: 14px;
left: 50%;
margin-left: -45px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 9px;
list-style: none;
}
.carousel ol>li {
float: left;
width: 10px;
height: 10px;
margin: 4px;
background-color: #fff;
border-radius: 50%;
}
.carousel .current {
background-color: #ff5000;
}
初始代码

使用 load 事件包裹代码, 保证代码在页面加载完毕后再执行.

并获取到需要的元素

window.addEventListener('load', function () {
var carousel = document.querySelector('.carousel');
var ol = document.querySelector('.carousel ol');
var ul = document.querySelector('.carousel ul');
var leftArrow = document.querySelector('.left-arrow');
var rightArrow = document.querySelector('.right-arrow');
// 表示当前显示的轮播图是第几张
// 用来控制小圆点, 并实现无缝切换.
var currentIndex = 0;
// 记录图片宽度
var imageWidth = ul.firstElementChild.offsetWidth;
// 记录图片数目
var imageCount = ul.children.length;
// TODO 下面写后续代码.
});
实现左右箭头隐藏/显示

1) 先通过样式让箭头默认隐藏

.carousel .left-arrow,
.carousel .right-arrow {
/* 隐藏 */
display: none;
}

2) 给 carousel 加上鼠标经过事件, 控制箭头显示

// 1. 实现左右箭头的显示/隐藏
function showArrow() {
// 绑定鼠标事件
carousel.addEventListener('mouseenter', function () {
leftArrow.style.display = 'block';
rightArrow.style.display = 'block';
});
carousel.addEventListener('mouseleave', function () {
leftArrow.style.display = 'none';
rightArrow.style.display = 'none';
});
}
showArrow();
动态生成圆点数目

根据实际轮播图的数目, 动态生成圆点.

1) 先把 ol 里的 li 都清空

2) 动态生成 li

// 2. 动态生成小圆点
function generatePoints() {
for (var i = 0; i < ul.children.length; i++) {
var li = document.createElement('li');
ol.appendChild(li);
}
ol.children[0].className = 'current';
}
generatePoints();
实现小圆点切换选中状态

点击小圆点, 能够切换小圆点的选中状态

注意, 要把其他的圆点的选中状态去掉

// 3. 实现小圆点切换
function switchCurrent() {
// 基于事件冒泡的机制, 不必给每个 li 都绑定监听器
ol.addEventListener('click', function (e) {
if (e.target == ol) {
return;
}
// 清除原来圆点的选中状态 (全局变量 currentIndex 记录了被选中的圆点)
for (var i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
// 给被点击的圆点加上选中状态
e.target.className = 'current';
});
}
switchCurrent();
实现小圆点切换图片

1) 准备好动画函数.

distance 为正数, 则往右移动; 为负数则往左移动.

// 4. 水平动画函数
function animateX(elem, distance, callback) {
// 获取目标位置
var destPos = elem.offsetLeft + distance;
// 判断之前是否已经有定时器. 如果有了就不运行新的了
if (elem.timerId) {
return;
}
// 设置定时器完成动画
elem.timerId = setInterval(function () {
// 获取当前位置
var curPos = elem.offsetLeft;
if (curPos == destPos) {
// 已经到达目标, 拆除定时器, 并调用回调
clearInterval(elem.timerId);
elem.timerId = null;
if (callback) {
callback();
}
} else {
// 还没到达目标, 移动位置
var step = (destPos - curPos) / 10;
step = step > 0 ? Math.ceil(step) : Math.floor(step);
elem.style.left = curPos + step + 'px';
}
}, 15);
}

2) 修改刚才的 switchCurrent 调用动画函数.

注意:

  • 移动的是 ul, 而不是 li 本身.

  • 移动距离 = (上次被选中下标 - 当前被选中下标) * 图片宽度

  • 为了知道当前被点击的圆点的下标, 需要在创建圆点时加入自定制属性.

  • 如果不能正确移动, 一定先检查 ul 是否加了 position

先给 ol>li 加上 data-index 属性

// generatePoints 的 for 循环中, 加入一行代码
li.setAttribute('data-index', i);

再在点击时获取属性

function switchCurrent() {
// 基于事件冒泡的机制, 不必给每个 li 都绑定监听器
ol.addEventListener('click', function (e) {
if (e.target == ol) {
return;
}
// 清除原来圆点的选中状态
for (var i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
// 给被点击的圆点加上选中状态
e.target.className = 'current';
// [实现移动轮播图片] 移动距离 = (上次被选中下标 - 当前被选中下标) * 图片宽度
// 上次的被选中圆点下标
var lastIndex = currentIndex;
// 现在的被选中圆点下标
currentIndex = e.target.getAttribute('data-index');
// 移动 ul.
var ul = document.querySelector('.carousel>ul');
animateX(ul, (lastIndex - currentIndex) * ul.firstElementChild.offsetWidth);
});
}
switchCurrent();
实现点击左右箭头切换图片

给左右箭头加上鼠标点击事件即可.

// 5. 实现点击按钮切换图片
function switchImage() {
rightArrow.addEventListener('click', nextImage);
leftArrow.addEventListener('click', prevImage);
}
switchImage();

nextImage 实现

function nextImage() {
var lastIndex = currentIndex++;
if (currentIndex >= imageCount) {
currentIndex = 0;
}
// 移动图片
animateX(ul, -imageWidth);
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}

prevImage 实现

function prevImage() {
var lastIndex = currentIndex--;
if (currentIndex < 0) {
currentIndex = imageCount - 1;
}
// 移动图片
animateX(ul, imageWidth);
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}

注意1: 当前还不能循环滚动. 当滚动到最后一张图片的时候, 继续滚动就会出现空白.

71825169b4426fbe34c843eb82d8d01f.png

注意2: 当前还不能点的太快. 如果一个动画还没播放完快速又点击切换, 会出现圆点和图不匹配的 bug [后面解决]

实现无缝滚动

实现思路:

  • 把第一张图片复制一份, 放到最后.  

  • 当图片滚动到最后一张的时候(带动画效果), 让 ul 快速跳到最左侧(不带动画). (先播放动画, 后移动位置)

  • 如果是向前滚动, 发现已经滚动到第一张图, 就先把 left 设置到最后一个位置, 再播放动画. (先移动位置, 后播放动画)

这里不好想, 一定要画图

448d4f45e9fd4557be2e2c0b79c4eaa5.png

1) 使用代码把图片克隆过来, 插入到 ul 最后

function switchImage() {
// [实现无缝滚动]
// 先把第一张图复制一份, 插入到最后
// 也把最后一张图复制一份, 插入到最前
var firstImage = ul.firstElementChild.cloneNode(true);
ul.appendChild(firstImage);
rightArrow.addEventListener('click', nextImage);
leftArrow.addEventListener('click', prevImage);
}

2) 在响应左右箭头点击事件中, 加入无缝滚动的判断

修改 nextImage

function nextImage() {
var lastIndex = currentIndex++;
if (currentIndex >= imageCount) {
currentIndex = 0;
}
// 移动图片
animateX(ul, -imageWidth, function () {
// [实现无缝滚动]
// 当发现当前下标是最后一张图的时候, 直接快速移动 ul 到最右侧
// 先播放完动画, 再移动位置. 不放到回调函数内部不能保证和动画的先后顺序
if (currentIndex == 0) {
ul.style.left = 0;
}
});
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}

修改 prevImage

function prevImage() {
var lastIndex = currentIndex--;
if (currentIndex < 0) {
currentIndex = imageCount - 1;
// [实现无缝滚动]
// 当发现当前下标是第一张图的时候, 直接快速移动 ul 到最左侧
// 先移动位置, 后播放动画
ul.style.left = -(imageCount * imageWidth) + 'px';
}
// 移动图片
animateX(ul, imageWidth);
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}
实现自动播放轮播图

直接使用定时器调用 nextImage 即可.

// 6. 实现自动轮播
function autoPlay() {
// 初始情况下, 自动播放
function play() {
carousel.autoPlayTimer = setInterval(function () {
// 直接调用 nextImage 即可切换下一个图片
nextImage();
// 也可以使用 click 方法触发点击事件
// rightArrow.click();
}, 2000);
}
play();
// 鼠标移入, 则取消定时器
carousel.addEventListener('mouseenter', function () {
clearInterval(carousel.autoPlayTimer);
});
// 鼠标移出, 则注册定时器
carousel.addEventListener('mouseleave', play);
}
autoPlay();
节流阀

目的: 解决按钮点击速度过快的 bug.

此时如果快速点击两次右箭头或者左箭头, 会发现, 当一个动画播放过程中, 再次点击按钮, 就会发现图片没有切换, 但是下方的小圆点切换了.

原因不难理解, 主要是取决于 animateX 的函数实现.

观察这个代码

3306b9d8a0cc8bb572b63e516ec9fee1.png

我们的 animateX 函数当上一个动画还没运行完毕时, 下一个动画是不生效的. 所以第二次点击只是触发了圆点的切换, 没有触发图片播放.

解决思路: 点击按钮时临时禁用点击事件. 等动画播放完毕, 再重新注册点击事件.

修改 nextImage 和 prevImage

 function nextImage() {    // [解决快速点击箭头的bug]    // 先临时取消鼠标点击事件理    rightArrow.removeEventListener('click', nextImage);    var lastIndex = currentIndex++;    if (currentIndex >= imageCount) {        currentIndex = 0;    }    // 移动图片    animateX(ul, -imageWidth, function () {        // [实现无缝滚动]        // 当发现当前下标是最后一张图的时候, 直接快速移动 ul 到最右侧        // 先播放完动画, 再移动位置. 不放到回调函数内部不能保证和动画的先后顺序        if (currentIndex == 0) {            ul.style.left = 0;        }        // [解决快速点击箭头的bug]        // 动画播放完, 再重新设置鼠标点击事件        rightArrow.addEventListener('click', nextImage);    });    // 设置小圆点的选中状态    ol.children[lastIndex].className = '';    ol.children[currentIndex].className = 'current';}function prevImage() {    // [解决快速点击箭头的bug]    // 先临时取消鼠标点击事件理    leftArrow.removeEventListener('click', prevImage);    var lastIndex = currentIndex--;    if (currentIndex < 0) {        currentIndex = imageCount - 1;        // [实现无缝滚动]        // 当发现当前下标是第一张图的时候, 直接快速移动 ul 到最左侧        // 先移动位置, 后播放动画        ul.style.left = -(imageCount * imageWidth) + 'px';    }    // 移动图片    animateX(ul, imageWidth, function () {        // [解决快速点击箭头的bug]        // 先临时取消鼠标点击事件理        leftArrow.addEventListener('click', prevImage);    });    // 设置小圆点的选中状态    ol.children[lastIndex].className = '';    ol.children[currentIndex].className = 'current';}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值