canvas插件 fabric.js 使用

fabric.js 是 常用的 canvas 插件

官方地址:  http://fabricjs.com/
小编用的版本为 4.5.1

1, 在项目中使用

		#myCanvas{
          width:10000px;
          height:10000px;
        }
<canvas id="myCanvas" width="10000" height="10000"></canvas>
<!-- 此处引用jquery ,也可以不引用 -->
<script src="./jquery-2.0.0.min.js"></script>
<script src="./fabric.5.1.0.min.js"></script>
		// 初始化 canvas 为 fabric 对象
        var canvas = this.__canvas = new fabric.Canvas('myCanvas');

		// 画一个矩形
        var rect = new fabric.Rect({
            width: 1000,
            height: 1000,
            left: 0,
            top: 0,
            fill: 'rgba(255, 0, 0, 0.4)',
        });
		// canvas 添加矩形并渲染
        canvas.add( rect );
        canvas.renderAll();

2, 特殊用法

    下面是在使用时用到和遇到的场景

①, 基本设置

		//画板元素不能被选中
        canvas.skipTargetFind = false;
        //画板不显示选中
        canvas.selection = false;
        canvas.freeDrawingBrush.color = "red";
        canvas.freeDrawingBrush.width = 1;
        //画板不限制调整时的宽高比
        // mycanvas.uniScaleTransform = true;
        // //绑定画板事件
        canvas.on({
            // 'mouse:dblclick': this.dblShowRectFrom,//双击区域弹出
            'mouse:down': this.mouseDownDrawRect, //按下,
            'mouse:move': this.mouseMoving, //移动中,
            'mouse:up': this.mouseUpDrawRect, //抬起,
            'mouse:wheel': this.zoomsHandle, //缩放
            // 'object:moved': this.objectMoved, //移动完成
            // // 'object:moving': objectMov,//移动中
            // 'object:scaled': this.mouseScaleDrawRect,//缩放完成
            // 'object:scaling': this.mouseScalingDrawRect,//缩放中
        });

②, 画板数据的导入导出

	// 1, 导出  检测 画板上所有对象 并保存为 字符串
	 let inkTrace = [];
	 canvas.getObjects().forEach((v,i) => {
            let klass = v.toJSON();
            if(klass.type == 'image'){
            	// 对其中一些特殊对象进行处理
            }
            inkTrace.push(klass)
      })
      // 对数据最后加工
      if(inkTrace.length==0){inkTrace=""}else{inkTrace = JSON.stringify(inkTrace)}
	// 2, 导入 根据字符串处理为 fabric 可识别的对象
			JSON.parse(inkTrace).forEach((v,i) => {
				// 不同类型的数据要分别处理 数据中的 type 属性
                if(v.type=="image"){
                    new fabric.Image.fromObject(v, (e) => {
                        mycanvas.add(e)
                    })
                }
                if(v.type=="path"){
                    new fabric.Path.fromObject(v, (e) => {
                        canvas.add(e)
                    })
                }
            })
            canvas.renderAll();

③, 遮罩 Pattern ( 引用官网案例 )

		var text = new fabric.Text('Honey,\nI\'m subtle', {
            fontSize: 250,
            left: 0,
            top: 0,
            lineHeight: 1,
            originX: 'left',
            fontFamily: 'Helvetica',
            fontWeight: 'bold',
            statefullCache: true,
            scaleX: 0.4,
            scaleY: 0.4
        });
        var shape = new fabric.Rect({
            width: 200,
            height: 100,
            left: 10,
            top: 300,
        });
        canvas.add(text, shape);
        loadPattern('./xxx.png');
        function loadPattern(url) {
            fabric.util.loadImage(url, function(img) {
            console.log(img)
            text.set('fill', new fabric.Pattern({
                source: img,
                repeat: "no-repeat"
            }));
            rect.set('fill', new fabric.Pattern({
                source: img,
                repeat: "no-repeat"
            }));
            canvas.renderAll();
            });
        }

④, 多个对象合并, 并设置为 fabric 背景 ( 适用于变色和更多场景 )

			// 创建对象
			let image = new fabric.Image(image, {
                left: 10,
                top: 10,
                scaleX: 1,
                scaleY: 1,
                crossOrigin: 'anonymous',// 跨域
            })
            let shape = new fabric.Rect({
                width: image.width,
                height: image.height,
                opacity:0.4,
                fill: "#FF0000",
                left: 10,
                top: 10,
                scaleX: 1,
                scaleY: 1,
            });
            // 合并
            let group = new fabric.Group([image, shape], {})
            // 设置为背景
            canvas.setBackgroundImage(group, canvas.renderAll.bind(canvas), {});

⑤, 把 canvas对象 或者 fabric对象 导出为图片

		let image = new Image();
		// 先引入一张图片作为底图, 不引入也没关系
        image.src = './xxx.png';
        image.setAttribute('crossOrigin', 'anonymous');
        image.onload = () => {
            let fabricImage = new fabric.Image(image, {
                left: 0,
                top: 0,
                width: image.width,
                height: image.height
            })
            let shapeLine = new fabric.Rect({
                width: 1700,
                height: 800,
                fill: "#333",
                left: 0,
                top: 0,
            });
            let shapeVert = new fabric.Rect({
                width: 235,
                height: image.height,
                fill: "#333",
                left: 0,
                top: 0,
            });
            // 把这几个对象合并一下
            let group = new fabric.Group([fabricImage, shapeLine, shapeVert], {})
            let imgs = group.toDataURL({
                format: 'png', // jpeg或png
                left: 0,
                top: 0,
                width: image.width,
                height: image.height
            });
            // imgs 就是导出后的图片, 放在 img 的src中即可使用
        }

⑥, 位置的获取

应用场景: 要求背景为一块矩形, 所有的遮罩, 痕迹, 只准出现在背景矩形上, 不准超出

思路一: (计算法, 没算出来)

在有旋转角度的情况下,
     位移: 算出位移之后出现在矩形上的位置(如,右侧超出出现在右侧)
     缩放: 在有旋转角度的情况下, 拉伸已旋转的矩形, 如果超出背景矩形, 则缩放至最大高度(没算出来)
     旋转: 旋转矩形如果超出背景矩形, 计算矩形能在背景矩形旋转的最大角度(没算出来)

mycanvas.on({
            //'mouse:down': this.mouseDownDrawRect, //按下,
            //'mouse:move': this.mouseMoving, //移动中,
            //'mouse:up': this.mouseUpDrawRect, //抬起,
            //'mouse:wheel': this.zoomsHandle, //缩放
            'object:moved': this.objectMov, //移动完成
            //'object:moving': this.objectMov,//移动中
            'object:scaled': this.mouseScaleDrawRect,//缩放完成
            //'object:scaling': this.mouseScalingDrawRect,//缩放中
            'object:rotated': this.objectRotated,// 旋转完成
            //'object:rotating': this.objectRotated,// 旋转中
        });
        // 移动完成计算
        objectMov = (t) => {
        const { mainCanvas } = this.state;
        let mycanvas = mainCanvas,oImgList = mycanvas.getObjects(),oImg = t?oImgList[oImgList.length-1]:mycanvas.getActiveObject();
        if(!oImg) { return false;}
        let ep = oImg.aCoords,// 目标对象的四个坐标点
            angle= Math.floor(( oImg.angle ) / 90),// 目标对象的旋转角度
            ptl = mycanvas.backgroundImage.aCoords.tl,
            pbr = mycanvas.backgroundImage.aCoords.br,et = 0,el = 0,eb = 0,er = 0,w = 0,h = 0;
        switch (angle) {
            case 0:
                et = ep.tl.y;el = ep.bl.x;eb = ep.br.y;er = ep.tr.x;
                el < ptl.x && oImg.set({ left:ptl.x + ep.tl.x - ep.bl.x }); // 左侧超出
                if(er > pbr.x) { // 右侧超出
                    if((ep.tr.x - ep.bl.x) <= (pbr.x - ptl.x)) { oImg.set({ left:pbr.x - (ep.tr.x - ep.tl.x) })}
                    if((ep.tr.x - ep.bl.x) > (pbr.x - ptl.x)) { oImg.set({ left:ptl.x }) }
                }
                et < ptl.y && oImg.set({ top:ptl.y })// 上侧超出
                if(eb > pbr.y) { // 下侧超出
                    if((ep.br.y - ep.tl.y) <= (pbr.y - ptl.y)) { oImg.set({ top:pbr.y - (ep.br.y - ep.tl.y) })}
                    if((ep.br.y - ep.tl.y) > (pbr.y - ptl.y)) { oImg.set({ top:ptl.y }) }
                }
                break;
            case 1:
                et = ep.bl.y;el = ep.br.x;eb = ep.tr.y;er = ep.tl.x;
                er > pbr.x && oImg.set({ left:pbr.x  })// 右侧超出
                el < ptl.x && oImg.set({ left:ptl.x + ep.tl.x - ep.br.x });// 左侧超出
                et < ptl.y && oImg.set({ top:ptl.y + (ep.tl.y - ep.bl.y) })// 上侧超出
                eb > pbr.y && oImg.set({ top:pbr.y - (ep.tr.y - ep.tl.y) })// 下侧超出
                break;
            case 2:
                et = ep.br.y;el = ep.tr.x;eb = ep.tl.y;er = ep.bl.x;
                el < ptl.x && oImg.set({ left:ptl.x + ep.tl.x - ep.tr.x });// 左侧超出
                er > pbr.x && oImg.set({ left:pbr.x - (ep.bl.x - ep.tl.x)})// 右侧超出
                et < ptl.y && oImg.set({ top:ptl.y + (ep.tl.y - ep.br.y) })// 上侧超出
                eb > pbr.y && oImg.set({ top:pbr.y  })// 下侧超出
                break;
            case 3:
                et = ep.tr.y;el = ep.tl.x;eb = ep.bl.y;er = ep.br.x;
                el < ptl.x && oImg.set({ left:ptl.x }); // 左侧超出
                er > pbr.x && oImg.set({ left:pbr.x - (ep.br.x - ep.tl.x)})// 右侧超出
                et < ptl.y && oImg.set({ top:ptl.y + (ep.tl.y - ep.tr.y) })// 上侧超出
                eb > pbr.y && oImg.set({ top:pbr.y - (ep.bl.y - ep.tl.y) })// 下侧超出
        }
        mycanvas.renderAll()
    }
获取所有的对象: mycanvas.getObjects()
获取最后一个对象: var objList = mycanvas.getObjects(),obj = objList[objList.length-1]
获取被选中的对象: mycanvas.getActiveObject()

缩放和旋转没算出来, 代码删了留着日后解决,

思路二 (记录位置) 补充: 鼠标位移留痕不规则线条在如下代码中

方法非常简单, mousedown记录选中矩形状态信息, mouseup检测是否超出, 是则回退至原来的位置

// objectMov 位移
mycanvas.on({
            'mouse:down': this.mouseDownDrawRect, //按下,
            'mouse:move': this.mouseMoving, //移动中,
            'mouse:up': this.mouseUpDrawRect, //抬起,
            //'mouse:wheel': this.zoomsHandle, //缩放
            //'object:moved': this.objectMov, //移动完成
            //'object:moving': this.objectMov,//移动中
            //'object:scaled': this.mouseScaleDrawRect,//缩放完成
            //'object:scaling': this.mouseScalingDrawRect,//缩放中
            //'object:rotated': this.objectRotated,// 旋转完成
            //'object:rotating': this.objectRotated,// 旋转中
        });
        mouseDownDrawRect = () => {
            _this_obj : JSON.parse(JSON.stringify(mycanvas.getActiveObject()))
    	};
    	mouseMoving = (options) => {
    	// canvasPointSave 在 mouseUpDrawRect 中定义
        // 判断是否可以留痕
        if( chousedType == 0 && mycanvas.backgroundImage && canvasPointSave.tl){
            let ptl = canvasPointSave.tl,
            pbr = canvasPointSave.br,
            eposition = options.pointer;
            if ( eposition.x < ptl.x || eposition.x > pbr.x || eposition.y < ptl.y || eposition.y > pbr.y) {
                mycanvas.isDrawingMode = false;
            }else{
                mycanvas.isDrawingMode = true;
            }
        }
    };
    	mouseUpDrawRect = (options) => {
    	// 如果是鼠标画不规则线条, 则重新判断位置
    	mycanvas.isDrawingMode = false;
        let canvasObjArr = mycanvas.getObjects();
        if( chousedType == 0 && canvasObjArr.length > 0){
            this.referDrawRect();
        }
        // 如果是位移,缩放,旋转则计算位置
        if( options.transform ){
            if( options.transform.action== "drag"){
                this.objectMov();
            }
            if( options.transform.action== "scaleX" ||  options.transform.action== "scaleY" ||  options.transform.action== "scale"){
                this.mouseScalingDrawRect();this.objectMov();
            }
            if( options.transform.action== "rotate"){
                this.objectRotated();this.objectMov();
            }
        }else if(options.target){
            this.objectMov();
        }
        // 记录位置
        if(mycanvas.backgroundImage) { canvasPointSave = mycanvas.backgroundImage.lineCoords }
    };
    objectMov = (t) => {
        const { mainCanvas } = this.state;
        let mycanvas = mainCanvas,oImgList = mycanvas.getObjects(),oImg = t?oImgList[oImgList.length-1]:mycanvas.getActiveObject();
        if(!oImg) { return false;}
        let ep = oImg.aCoords,// 目标对象的四个坐标点
            angle= Math.floor(( oImg.angle ) / 90),// 目标对象的旋转角度
            ptl = mycanvas.backgroundImage.aCoords.tl,
            pbr = mycanvas.backgroundImage.aCoords.br,et = 0,el = 0,eb = 0,er = 0,w = 0,h = 0;
        switch (angle) {
            case 0:
                et = ep.tl.y;el = ep.bl.x;eb = ep.br.y;er = ep.tr.x;
                el < ptl.x && oImg.set({ left:ptl.x + ep.tl.x - ep.bl.x }); // 左侧超出
                if(er > pbr.x) { // 右侧超出
                    if((ep.tr.x - ep.bl.x) <= (pbr.x - ptl.x)) { oImg.set({ left:pbr.x - (ep.tr.x - ep.tl.x) })}
                    if((ep.tr.x - ep.bl.x) > (pbr.x - ptl.x)) { oImg.set({ left:ptl.x }) }
                }
                et < ptl.y && oImg.set({ top:ptl.y })// 上侧超出
                if(eb > pbr.y) { // 下侧超出
                    if((ep.br.y - ep.tl.y) <= (pbr.y - ptl.y)) { oImg.set({ top:pbr.y - (ep.br.y - ep.tl.y) })}
                    if((ep.br.y - ep.tl.y) > (pbr.y - ptl.y)) { oImg.set({ top:ptl.y }) }
                }
                break;
            case 1:
                et = ep.bl.y;el = ep.br.x;eb = ep.tr.y;er = ep.tl.x;
                er > pbr.x && oImg.set({ left:pbr.x  })// 右侧超出
                el < ptl.x && oImg.set({ left:ptl.x + ep.tl.x - ep.br.x });// 左侧超出
                et < ptl.y && oImg.set({ top:ptl.y + (ep.tl.y - ep.bl.y) })// 上侧超出
                eb > pbr.y && oImg.set({ top:pbr.y - (ep.tr.y - ep.tl.y) })// 下侧超出
                break;
            case 2:
                et = ep.br.y;el = ep.tr.x;eb = ep.tl.y;er = ep.bl.x;
                el < ptl.x && oImg.set({ left:ptl.x + ep.tl.x - ep.tr.x });// 左侧超出
                er > pbr.x && oImg.set({ left:pbr.x - (ep.bl.x - ep.tl.x)})// 右侧超出
                et < ptl.y && oImg.set({ top:ptl.y + (ep.tl.y - ep.br.y) })// 上侧超出
                eb > pbr.y && oImg.set({ top:pbr.y  })// 下侧超出
                break;
            case 3:
                et = ep.tr.y;el = ep.tl.x;eb = ep.bl.y;er = ep.br.x;
                el < ptl.x && oImg.set({ left:ptl.x }); // 左侧超出
                er > pbr.x && oImg.set({ left:pbr.x - (ep.br.x - ep.tl.x)})// 右侧超出
                et < ptl.y && oImg.set({ top:ptl.y + (ep.tl.y - ep.tr.y) })// 上侧超出
                eb > pbr.y && oImg.set({ top:pbr.y - (ep.bl.y - ep.tl.y) })// 下侧超出
        }
        mycanvas.renderAll()
    }
    mouseScalingDrawRect = () => {
        const { mainCanvas,_this_obj } = this.state;
        let mycanvas = mainCanvas,obj = mycanvas.getActiveObject();
        if(!obj) { return false;}
        let ep = obj.aCoords,// 目标对象的四个坐标点
            angle= Math.floor(( obj.angle ) / 90),et = 0,el = 0,eb = 0,er = 0,w = 0,h = 0;// 目标对象的旋转角度
        switch (angle) {
            case 0:
                et = ep.tl.y;el = ep.bl.x;eb = ep.br.y;er = ep.tr.x;
                w = ep.tr.x - ep.bl.x ;h = ep.br.y - ep.tl.y ;
                break;
            case 1:
                et = ep.bl.y;el = ep.br.x;eb = ep.tr.y;er = ep.tl.x;
                w = ep.tl.x - ep.br.x ;h = ep.tr.y - ep.bl.y ;
                break;
            case 2:
                et = ep.br.y;el = ep.tr.x;eb = ep.tl.y;er = ep.bl.x;
                w = ep.bl.x - ep.tr.x ;h = ep.tl.y - ep.br.y ;
                break;
            case 3:
                et = ep.tr.y;el = ep.tl.x;eb = ep.bl.y;er = ep.br.x;
                w = ep.br.x - ep.tl.x ;h = ep.bl.y - ep.tr.y ;
        }
        if(w > mycanvas.backgroundImage.width || h > mycanvas.backgroundImage.height){
            obj.set({ 
                scaleY: _this_obj.scaleY,
                scaleX: _this_obj.scaleX,
                left: _this_obj.left,
                top: _this_obj.top,
            }) 
        }
        mycanvas.renderAll()
    }
    referDrawRect = () => {
        const { mainCanvas } = this.state;
        let mycanvas = mainCanvas,objList = mycanvas.getObjects(),obj = objList[objList.length-1];
        if(!obj) { return false;}
        let scx = mycanvas.backgroundImage.width / obj.width;// -0.05
        let scy = mycanvas.backgroundImage.height / obj.height;// -0.05
        if (obj.scaleX > scx) {
            obj.set({scaleX:scx})
        };
        if (obj.scaleY > scy) {
            obj.set({scaleY:scy})
        };
        mycanvas.renderAll()
        this.objectMov(true)
    }
    objectRotated = () => {
        const { mainCanvas, _this_obj } = this.state;
        let mycanvas = mainCanvas,oImg = mycanvas.getActiveObject();// 目标对象
        if(!oImg) { return false;}
        let ep = oImg.aCoords,// 目标对象的四个坐标点
            angle= Math.floor(( oImg.angle ) / 90),// 目标对象的旋转角度
            ptl = mycanvas.backgroundImage.aCoords.tl,
            pbr = mycanvas.backgroundImage.aCoords.br,et = 0,el = 0,eb = 0,er = 0,w = 0,h = 0,lock = false;
        switch (angle) {
            case 0:
                et = ep.tl.y;el = ep.bl.x;eb = ep.br.y;er = ep.tr.x;
                w = ep.tr.x - ep.bl.x ;h = ep.br.y - ep.tl.y ;
                break;
            case 1:
                et = ep.bl.y;el = ep.br.x;eb = ep.tr.y;er = ep.tl.x;
                w = ep.tl.x - ep.br.x ;h = ep.tr.y - ep.bl.y ;
                break;
            case 2:
                et = ep.br.y;el = ep.tr.x;eb = ep.tl.y;er = ep.bl.x;
                w = ep.bl.x - ep.tr.x ;h = ep.tl.y - ep.br.y ;
                break;
            case 3:
                et = ep.tr.y;el = ep.tl.x;eb = ep.bl.y;er = ep.br.x;
                w = ep.br.x - ep.tl.x ;h = ep.bl.y - ep.tr.y ;
        }
        if(w > mycanvas.backgroundImage.width || h > mycanvas.backgroundImage.height ) {
            oImg.set({ 
                angle: _this_obj.angle,
                left: _this_obj.left,
                top: _this_obj.top,
            }) 
        }
        mycanvas.renderAll()
    }

注意事项

1, canvas 使用和 img 相关操作时 如果 跨域 需要加上 crossOrigin: 'anonymous' 属性;
2, fabric.js 是根据 html 中 canvas标签的id 来初始化的, 如需 初始化多个fabric , 则需 注意 canvas 标签的 id, 以及 window.全局配置的参数(使用vue和react时)
3, fabric 初始化过后的画布, 如果发生 位移缩放 , 再进行放置对象是要 进行计算


计算规则如下
// 发生位移计算
			// 右上角
            let tr = canvas.backgroundImage.lineCoords.tr;
            // 右下角
            let br = canvas.backgroundImage.lineCoords.br;
            // 左上角
            let tl = canvas.backgroundImage.lineCoords.tl;
            // 左下角
            let bl = canvas.backgroundImage.lineCoords.bl;
            //判断试卷最右侧 是否离开了可视区域
            if (tr.x <= 50) {
                var delta = new fabric.Point(50, 0);
                relativeMouseX += 50;
                canvas.relativePan(delta);
                return;
            }
            //判断试卷最上侧 是否离开了可视区域
            if (tr.y > screenHeight - 50) {
                var delta = new fabric.Point(0, -50);
                relativeMouseY -= 50;
                canvas.relativePan(delta);
                return;
            }
            //判断试卷最下侧 是否离开了可视区域
            if (br.y <= 50) {
                var delta = new fabric.Point(0, 50);
                relativeMouseY += 50;
                canvas.relativePan(delta);
                return;
            }
            //判断试卷最左侧 是否离开了可视区域
            if (tl.x > screenWidth - 50) {
                var delta = new fabric.Point(-50, 0);
                relativeMouseX -= 50;
                canvas.relativePan(delta);
                return;
            }

            var delta = new fabric.Point(options.e.movementX, options.e.movementY);
            canvas.relativePan(delta);
            relativeMouseX += options.e.movementX; //累计每一次移动时候的偏差
            relativeMouseY += options.e.movementY;





// 发生缩放计算
	//  缩放代码
		let wheelPost = e?e.e.deltaY:wheel,
            pointerX = e.pointer.x,
            pointerY = e.pointer.y,
            zoomSpeed = 0.03,
            zoom, zoomPoint, lastzoom, lastzoomPoint={x:0,y:0}, lastmousePoint={x:0,y:0}, relativeMouseX, relativeMouseY;// 通过zoomSpeed 控制缩放速度
        zoom = (wheelPost > 0 ? -zoomSpeed : zoomSpeed) + canvas.getZoom();
        zoom = Math.max(0.1, zoom); //最小为原来的1/10
        zoom = Math.min(3, zoom); //最大是原来的3倍
        zoomPoint = new fabric.Point(pointerX, pointerY);
        canvas.zoomToPoint(zoomPoint, zoom);

        lastzoomPoint.x =
            lastzoomPoint.x + (zoomPoint.x - lastmousePoint.x - relativeMouseX) / lastzoom;
        lastzoomPoint.y =
            lastzoomPoint.y + (zoomPoint.y - lastmousePoint.y - relativeMouseY) / lastzoom;

        lastmousePoint.x = zoomPoint.x;
        lastmousePoint.y = zoomPoint.y;
        lastzoom = zoom;

        relativeMouseX = 0;
        relativeMouseY = 0;

// 需要获取值时
	transformMouse = (mouseX, mouseY) => {
        let { lastzoomPoint,zoomPoint,relativeMouseX,relativeMouseY } = this.state;
        let {
            mainCanvas
        } = this.state;

        let mycanvas = mainCanvas;

        let x = lastzoomPoint.x + (mouseX - zoomPoint.x - relativeMouseX) / mycanvas.getZoom();
        let y = lastzoomPoint.y + (mouseY - zoomPoint.y - relativeMouseY) / mycanvas.getZoom();
        return { x, y };
    };
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值