fabric.js学习

一,前言

1.fabric [ˈfæbrɪk'] 是一个功能强大的运行在HTML5 canvasJavaScriptfabriccanvas提供了一个交互式对象模型,通过简洁的api就可以在画布上进行丰富的操作。

2.可以使用包管理工具直接安装:npm install fabric

3.fabric兼容pc端和移动端,不支持运行在小程序环境下

二,初始化画布

1.首先创建一个canvas元素,作为容器

<canvas id="mapCanvas" width="350" height="200"></canvas>

2.实例化一个canvas上下文对象,接管原生容器,根据使用api的不同,初始化后的画布可以分为 可交互不可交互 两种

import { fabric } from "fabric";
//可交互画布,用于需要编辑图片等场景
const canvasCtx = new fabric.Canvas("mapCanvas", {
    enableRetinaScaling: true,
    perPixelTargetFind: true, // 对象基于像素检测
    skipTargetFind: true,
    
    //框选
    selection: true, //是否支持鼠标框选
    selectable: true, //是否支持鼠标框选
    selectionColor: "rgba(255,255,255,0.3)", // 鼠标框选背景色
    selectionLineWidth: 1, // 画布中鼠标框选边框
    
    preserveObjectStacking: true, //取消被选中的元素会处于顶层的默认行为
    backgroundColor: '#fff', //画布背景颜色
  });

//不可交互画布, 用于画海报等场景,鼠标事件都无效
const canvasCtx = new fabric.StaticCanvas("mapCanvas",{})

3.创建canvas元素时可直接设置widthheight ,当一开始不确定宽高时可不设置,之后使用setWidthsetHeight动态设置

canvasCtx.setWidth(400)
canvasCtx.setHeight(400)

4.相应的可以使用getWidthgetHeight获取canvas的宽高

canvasCtx.getWidth()
canvasCtx.getHeight()

三,基础图像绘制

1.fabric已经内置了许多基础图像,调用相应api,可直接应用,免去了原生繁琐的过程。

(1)矩形 [ 圆角矩形 ]
(2)圆
(3)椭圆
(4)直线
(5)曲线
(6)折线
(7)虚线
(8)多边形
(9)三角形

2.矩形绘制 fabric.Rect

(1)使用fabric.Rect绘制矩形,主要配置项为左上角的(left,top)widthheight

let forbiddenArea = new fabric.Rect({
	//主要配置属性
    left: 0, //距离画布左侧的距离,单位是像素
    top: 0, //距离画布上边的距离
    width: 100,
    height: 100,
	
	//样式配置属性
    fill: "#fef0f08a", //填充的颜色
    stroke: "#fab6b6", //边界的颜色
	//通过配置rx,ry,可以让矩形具有圆角
    rx:20,
    ry:20,
 });

2.绘制圆 fabric.Circle

(1)使用fabric.Circle绘制圆,主要配置项为中心点(left,top)和半径radius

let circle = new fabric.Circle({
  //主要配置属性
  left: 10, //距离左边的距离
  top: 10, //距离上边的距离
  radius: 2, //圆的半径
  
  //样式配置属性
  fill: "#eee", //填充的颜色
  stroke: "#fab6b6", //边界的颜色
});

3.绘制椭圆 fabric.Ellipse

(1)使用fabric.Ellipse绘制圆,主要配置项为中心点(left,top)和长短半轴rxry

let circle = new fabric.Ellipse({
  //主要配置属性
  left: 10, //距离左边的距离
  top: 10, //距离上边的距离
  rx: 10, 
  ry: 10, 
  
  //样式配置属性
  fill: "#eee", //填充的颜色
});

4.绘制直线 fabric.Line

(1)使用fabric.Line绘制直线,主要配置项为两个点x1,y1,x2,y2

let line = new fabric.Line([x1,y1,x2,y2],{
    stroke: "#c45656",
    strokeWidth: 3,
});

5.绘制线段 fabric.Polyline

(1)使用fabric.Polyline绘制线段,主要配置项为多个点x1,y1,x2,y2

  const polyline = new fabric.Polyline([
    {x: 30, y: 30},
    {x: 150, y: 140},
    {x: 240, y: 150},
    {x: 100, y: 30}
  ], {
    fill: 'transparent', // 如果画折线,需要填充透明
    stroke: '#6639a6', // 线段颜色
    strokeWidth: 5 // 线段粗细 
  })

6.绘制不规则图形 fabric.Path

(1)使用fabric.Path绘制不规则图形,主要配置项为一个字符串,其中M代表移动到某个点,L是中途的点,z表示闭合

  const path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
  path.set({ left: 120, top: 120,fill:'red' });

7.绘制曲线

(1)fabric提供了自由画笔的功能让我们可以随意绘制线条

(2)使用isDrawingMode来开启自由绘制模式

  //1.在初始化canvas实例时,直接开启自由画笔
  const canvasCtx = new fabric.Canvas("mapCanvas", {
  	isDrawingMode:true,
  });
  
  //2.设置`isDrawingMode`动态开启
  canvasCtx.isDrawingMode =true;

  //设置画笔颜色		
  canvasCtx.freeDrawingBrush.color = "#f56c6c";
  //设置画笔粗细
  canvasCtx.freeDrawingBrush.width = 3;

(3)关闭自由绘制

  //设置isDrawingMode为false
  canvasCtx.isDrawingMode =false;
 
  //关闭的时机可以根据需求设置,这里介绍鼠标弹起时关闭
  canvasCtx.on("mouse:up", function (opt) {
    this.isDrawingMode = false;
  });

(4)自由绘制的曲线,也会生成实例对象

8.绘制虚线

(1)绘制虚线要使用strokeDashArray属性,strokeDashArray[a,b] 含义为 每隔a个像素空b个像素,直线和线段都可以使用这个属性来绘制成虚线。


const line = new fabric.Line([x1,y1,x2,y2],{
    stroke: "#c45656",
    strokeWidth: 3,
    strokeDashArray: [20, 10],
});

 const polyline = new fabric.Polyline(
    [
      { x: 30, y: 30 },
      { x: 150, y: 140 },
      { x: 240, y: 150 },
      { x: 100, y: 30 },
    ],
    {
      fill: "transparent", // 如果画折线,需要填充透明
      hasBorder: true,
      strokeDashArray: [20, 10],
    }
  );

四,图片绘制

1.将图片绘制进canvas有两种方式

(1)方式一:根据图片元素绘制图片,使用fabric.Image

  let image = new Image();
  image.src = 'xxxx';
  image.onload = ()=>{
  	//1.创建图片实例对象
    let imgInstance = new fabric.Image(image, {
      left: 0, //位置
      top: 0,
      scaleX:1,
      scaleY:1,
      erasable:false, //是否可擦除
      hasControls: false, // 是否开启图层的控件
      evented:true,//是否可以支持事件
      ...
    });
    //实例对象的width和height可以获取图片的大小
    imgInstance.width
    imgInstance.height
    //2.添加到画布
    canvasCtx.add(imgInstance);
  };

(2)方式二:根据图片地址绘制图片fabric.Image.fromURL

  new fabric.Image.fromURL("/resources/images/robot.png", function (oImg) {
  	//oImg是图片对象实例
    oImg.set({
      top:10,
      left:10,
      scaleX:1,
      scaleY:1,
      evented: false,
      ...
    });
    //实例对象的width和height可以获取图片的大小
    oImg.width
    oImg.height
    
    canvasCtx.add(oImg);
  });

2.当我们已经声明了一个fabric图片对象,可以在维持原对象的情况下使用setSrc动态修改图片。

  //注意,第一个参数只能是图片地址远程地址本地地址,base64等,不能是图像元素
  oimg.setSrc("./img.png", () => {
    canvasCtx.renderAll();
  });

五,绘制文本

1.绘制普通文本

  let text = new fabric.Text(`hello \n world!`, {
      left: 0,
      top: 0,
      fill: 'red', // 字体颜色
      fontSize: 12, //字体大小
      fontWeight: 100, //字体粗细
      charSpacing: 10 //字体间距,
      angle: 30, // 旋转
      backgroundColor: '#ffd460', // 背景色
      stroke: '#3f72af', // 文字描边颜色(蓝色)
      strokeWidth: 2, // 文字描边粗细
      textAlign: 'lfet', // 对齐方式:left 左对齐; right 右对齐; center 居中
      opacity: 0.8, // 不透明度
      selectable: true, // 能否被选中,默认true
      shadow: 'rgba(0, 0, 0, 0.5) 5px 5px 5px', // 投影
      ...
    });

2.绘制可编辑文本

  let text = new fabric.IText(`hello \n world!`, {
      left: 0,
      top: 0,
      cornerColor: 'pink', // 角的颜色(被选中时)
      borderColor: 'yellowGreen', // 边框颜色(被选中时)
      ...
    });

3.绘制文本框

  let text = new fabric.Textbox(`hello \n world!`, {
      left: 0,
      top: 0,
      cornerColor: 'pink', // 角的颜色(被选中时)
      borderColor: 'yellowGreen', // 边框颜色(被选中时)
      ...
    });

4.可以通过set实例方法动态修改文本,和其他属性

text.set({
	text:'change content'
})

六,将绘制对象添加进画布

1.在声明了一个图形或者图片等对象时,还需要使用使用add 添加进画布

canvas.add(klassObject)

2.我们可以使用getObjects获取所有已绘制进画布的对象,该方法返回一个元素为kclass对象的数组,需要注意的是没有add进画布的对象,无法通过getObjects获取到,因为还没有绘制进画布中

canvas.getObjects()

七,kclass对象

1.在fabric中,本质上绘制的每一个图形都是一个kclass对象,如下是一个含有许多对象信息的kclass
在这里插入图片描述
2.简单介绍下kclass可以获取对象的一些有用的信息

(1)lineCoords : 线性对象边界的四个点坐标,是在画布缩放移动时,动态变化的当前时刻快照的对象坐标,

(2)aCoords : 原始对象边界的四个点坐标原始值,该属性反映了画布没缩放没移动时的对象位置,每一个lineCoords都对应着一个aCoordslineCoords是当前肉眼看到的根据画布缩放移动计算出来的位置,aCoords是隐藏在背后的原始位置

(3)top,left :对象定位时的topleft,注意lefttop不会因为画布的缩放和移动而计算,一直都是元素在画布原始状态(scale1,没有move)时的值,非常的准确,

(4)angle:对象旋转的角度

(5)自定义的一些属性,也会在kclass对象中展示

(6)配置的属性如conerSize等等

七,对象的定位

1.在fabric中,我们使用对象的topleft来定位对象在画布中的位置,topleft默认是对象的左上角位置

2.我们可以通过配置对象的originXoriginY来修改默认原点的位置

3.originXoriginY分别代表对象平移的水平起始点和垂直起始点,可以设置的值如下

originX : "left"|"right"|"center"
originY : "top"|"bottom"|"center"

4.如下图所示,相同的lefttop值,原点不一样时,对象的位置是不一样的,红色的为默认的原点,紫色的originXoriginYcenter

在这里插入图片描述

八,设置对象属性

1.对于所有对象,除了在创建时可以声明属性,我们还可以使用对象的set实例方法动态设置属性

 kclassObject.set({
      left: 10,
      top: 22,
      angle:100,
      ....
 });

九,对象的控件

1.一个可交互的画布上的元素支持缩放,平移和旋转的操作,实现的方式就是自带的控件,如下图所示
在这里插入图片描述

2.当我们点击对象时,就会激活这个控件操作框,同时,我们还可以通过setActiveObject(klassObject)这个api在代码层面激活这个控件操作框

3.我们可以通过一些配置属性,修改这些控件的大小,颜色,边距等等

4.除了已有的操作控件,我们还可以自定义和修改已有控件

(1)自定义一个删除控件

//删除图标
var deleteImg = document.createElement("img");
deleteImg.src = deleteIcon;

//在fabric.Object.prototype.controls.xxx 上通过 fabric.Control 自定义控件
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
  x: 0.5,
  y: -0.5,
  offsetY: -16,
  offsetX: 16,
  cursorStyle: "pointer",
  //自定义点击事件
  mouseUpHandler: function (eventData, transform) {
    var target = transform.target;
	//target 为当前对象,这里可执行业务判断
    canvasCtx.remove(target);
    canvasCtx.requestRenderAll();
  },
  //渲染图标包含图标大下位置,
  render: function (ctx, left, top, styleOverride, fabricObject) {
  	//fabricObject为当前对象,可根据当前对象自定义是否渲染删除按钮
    var size = this.cornerSize + 2;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(deleteImg, -size / 2, -size / 2, size, size);
    ctx.restore();
  },
  cornerSize: 18,
});

(2)修改已有控件,修改旋转按钮的样式

//旋转按钮图标
var circleImg = document.createElement("img");
circleImg.src = circleIcon;

//在fabric.Object.prototype.controls.mtr (mtr代表旋转控件) 上通过 fabric.Control 自定义控件
fabric.Object.prototype.controls.mtr = new fabric.Control({
  x: 0,
  y: -0.5,
  offsetY: -20,
  cursorStyle: "pointer",
  //控件的业务,默认的旋转的原点是定位的原点,我们可以根据originX originY修改旋转原点
  actionHandler: fabric.controlsUtils.rotationWithSnapping,
  cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
  //渲染图标
  render: function (ctx, left, top, styleOverride, fabricObject) {
    let size = 18;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle!));
    ctx.drawImage(circleElement, -size / 2, -size / 2, size, size);
    ctx.restore();
  },
});

效果如下
在这里插入图片描述

十,限制控件行为

1.有时候我们想要我们绘制的对象不具备部分控件行为,例如旋转等,或者甚至不具备任一项行为,fabric也提供了各种配置和api来实现这一点

2.我们可以通过配置对象的 hasControls 属性为false禁用所以功能,此时不会渲染操作框

3.我们也可以配置部分属性配置,限制部分行为。

(1)限制元素的旋转:lockRotation = true
(2)限制元素x轴的缩放:lockScalingX= true
(3)限制元素y轴的缩放:lockScalingY= true
(4)限制元素上下移动:lockMovementY =true
(5)限制元素左右移动:lockMovementX =true

4.此外还可以使用setControlsVisibility,该方法用于隐藏操作控件,从而达到禁用功能

 workNode.setControlsVisibility({
      bl: false, //左下角
      br: false, //右下角
      mb: false, //下中角
      
      ml: false, //左中角
      mr: false, //右中角
      
      tl: false, //左上角
      tr: false, //右上角
      mt: false, //上中角
      
      mtr:false, //旋转角
  });

十一,事件

1.画布对象元素对象都有on实例方法用于绑定事件,当设置对象的属性 eventedfalse时,绑定的事件不生效

canvasCtx.on(eventname,handler)

canvasCtx.on("mouse:down",(e)=>{})
text.on("mouse:down",(e)=>{})

2.支持的常用事件

(1)鼠标上常用的操作事件

mouse:down 或 mousedown 鼠标按下时触发
mouse:move
mouse:up 或 mouseup 鼠标抬起时触发
mouse:down:before
mouse:move:before
mouse:up:before
mouse:over 或 mouseover 鼠标悬浮时触发
mouse:out 或 mouseout 鼠标进入后离开时触发
mouse:dbclick 或 mousedblclick 鼠标双击时触发

//for example
canvasCtx.on("mouse:down",(e)=>{})
text.on("mouse:down",(e)=>{})

(2)对象操作的一些监控

modified
rotating
scaling
moving
skewing

//for example
text.on("rotating",(e)=>{})

注意: 对于绘制一个矩形,当元素缩放拉伸时,在事件对象上获取的widthheight是不会变化的,即使通过getObjects获取这个对象,也会一直是第一次绘制的值,但是在kclass里有scareXscareY体现了长和宽伸缩的值,因此我们可以使用scareX * widthscareY * height 获取此时此刻矩形元素的长和宽

3.fabric的鼠标事件能够兼容在移动端的手势操作

mouse:down  ==> 在移动端手指点击一下也会触发
mouse:move  ==> 在移动端手指移动也会触发
mouse:up ==> 在移动端手指抬起也会触发

mouse:down:before 
mouse:move:before
mouse:up:before

mouse:over  ==> 不支持移动端
mouse:out ==> 移动端手指离开元素触发
mouse:dbclick ==> 在移动端双击也会触发

4.注意,虽然部分鼠标事件兼容移动端,但是同一个事件在PC端和移动端返回的事件对象是不一样的,pc端返回的mouseEvent,移动端返回的touchEvent,两者返回的字段含义不一样,要自己做兼容。

5.注意,当对画布进行拖动和缩放后,事件对象的offsetXoffsetY会产生偏移,不能准确的表示位置,需要使用如下的方法转化坐标

//根据缩放等级转化鼠标的x,y
function getTransformedPosX(canvasCtx, x) {
  let zoom = Number(canvasCtx.getZoom());
  return (x - canvasCtx.viewportTransform[4]) / zoom;
}
function getTransformedPosY(canvasCtx, y) {
  let zoom = Number(canvasCtx.getZoom());
  return (y - canvasCtx.viewportTransform[5]) / zoom;
}

十二,橡皮擦

1.fabric.js 的基础包并没有包含橡皮擦模块,要使用橡皮擦功能可以使用npm安装fabric-with-erasing

2.fabric-with-erasing是在fabric.js的基础上加上橡皮擦,因此在使用fabric-with-erasing后,可以不需要fabric包。

3.开启橡皮擦

  canvasCtx.isDrawingMode = true; // 进入绘画模式
  canvasCtx.freeDrawingBrush = new fabric.EraserBrush(canvasCtx); // 使用橡皮擦画笔
  canvasCtx.freeDrawingBrush.width = 8; // 设置画笔粗细为 10

4.恢复橡皮擦擦除部分

  canvasCtx.isDrawingMode = true;
  canvasCtx.freeDrawingBrush = new fabric.EraserBrush(canvasCtx);
  canvasCtx.freeDrawingBrush.width = 10;
  canvasCtx.freeDrawingBrush.inverted = true;

5.关闭橡皮擦功能

  canvasCtx.isDrawingMode = false;

6.橡皮擦经过的部分会变成透明的

十二,设置画布zoom和获取zoom

1.canvas默认层级是1

2.设置层级方法: canvasCtx.setZoom(zoom);

3.获取层级方法: canvasCtx.getZoom();

4.根据鼠标滚轮来设置层级:

  canvasCtx.on("mouse:wheel", function (opt) {
    var delta = opt.e.deltaY;
    var zoom = canvasCtx.getZoom();
    zoom *= 0.999 ** delta;
    if (zoom > 20) zoom = 20;
    if (zoom < 0.01) zoom = 0.01;
    canvasCtx.setZoom(zoom);
    opt.e.preventDefault();
    opt.e.stopPropagation();
  });

十三,修改鼠标样式

1.fabric支持修改不同状态下默认的鼠标样式

(1)默认的鼠标样式

canvasCtx.defaultCursor = 'default' 

(2)鼠标移动到对象上的样式

canvasCtx.hoverCursor = 'move' 

(3)鼠标拖动时的样式

canvasCtx.moveCursor = 'move' 

(4)自由绘制时的样式

canvasCtx.freeDrawingCursor = 'crosshair' 

(5)旋转时的样式

canvasCtx.rotationCursor = 'crosshair' 

2.上面的样式都是默认样式,我们可以赋值为其它css支持的cursor

十四,将画布保存为图片

1.使用toDataURL可以将canvas转为图片,该函数返回的是图片的base64格式字符串。

let imgUrl = canvasCtx.toDataURL({
    format: "png",
  })

2.可以通过topleftwidthheight参数将指定部分canvas绘制成图片。

let imgUrl = canvasCtx.toDataURL({
    format: "png",
    width: mapImgInstance.lineCoords.br.x - mapImgInstance.lineCoords.bl.x,
    height: mapImgInstance.lineCoords.bl.y - mapImgInstance.lineCoords.tr.y,
    left: mapImgInstance.lineCoords.bl.x,
    top: mapImgInstance.lineCoords.tl.y,
  });

十五,释放canvas

1.使用dispose可以释放已经创建的canvas

canvasCtx.dispose()

十六,移动端手势支持

1.使用fabric_with_gestures版本 ,没有具体使用过,需要的另外查阅

        touch:gesture
        touch:drag
        touch:shake
        touch:longpress

2.双指缩放

  handleDoubleFinger(options) {
                const {
                    clientX: finger1X,
                    clientY: finger1Y
                } = options.e.targetTouches[0];
                const {
                    clientX: finger2X,
                    clientY: finger2Y
                } = options.e.targetTouches[1];
                const powX = (finger2X - finger1X) * (finger2X - finger1X);
                const powY = (finger2Y - finger1Y) * (finger2Y - finger1Y);
                // 计算两个手指之间的距离
                const distance = Math.sqrt(powX + powY);
                // 每次缩放的比例
                let ratio = -0.05;
                if (distance > preDistance) {
                    ratio = 0.05;
                }
                preDistance = distance;
                const x = scaleCenter.x || (Math.abs(finger1X + finger2X)) / 2;
                const y = scaleCenter.y || (Math.abs(finger1Y + finger2Y)) / 2;
                scaleCenter = {
                    x, y
                };
                // 计算当前缩放的大小
                let zoom = ratio + canvasCtx.getZoom(); // 获取当前缩放比
                zoom = Math.max(0.5, zoom);
                zoom = Math.min(3, zoom);
                const zoomPoint = new fabric.Point(x, y);
                canvasCtx.zoomToPoint(zoomPoint, zoom);
            }

十七,操作像素

1.fabric没有直接操作像素的方法,想要操作像素可以使用原生的canvas

2.关键的两个apigetImageDataputImageData

  //初始化canvas上下文
  let canvas = document.getElementById("canvas");
  let ctx = canvas.getContext("2d");
  
  //初始化图像元素
  let img = new Image();
  img.crossOrigin = "anonymous";
  img.src = url; // url是要改变图像的地址
		
  //图像加载完成后
  img.onload = () => {
	canvas.width = img.width;
    canvas.height = img.height;
	
	//往canvas绘制图像元素
    ctx.drawImage(img, 0, 0);
    //获取图像像素对象
    let imgData = ctx.getImageData(0, 0, img.width, img.height);
	//imgData.data 类似于 [r1,g1,b1,a1,r2,g2,b2,a2,r3,g3,b3,a3,...] 每四个元素组成一个rgba

	//...根据索引可以直接修改像素值
	
	//使用putImageData可以把改变后的图片,重新绘制到canvas上
	ctx.putImageData(imgData, 0, 0);
	//之后可以将图片重新下载下来
    let url = canvas.toDataURL("image/png", { quality: 1 });
  }
polyline.getCenterPoint() //获取中心位置

十八,参考资料

1.中文api文档

### 如何在 Fabric.js 中导出不规则形状的图像 为了实现在 Fabric.js 中导出不规则形状的图像,可以采用如下方式: 通过 `fabric.Canvas` 对象的方法 `.toDataURL()` 或者 `.toDataURLWithMultiplier()` 可以将整个画布的内容转换成数据 URL 形式的图片。如果希望只导出特定的对象或一组对象,则可以通过创建一个新的临时画布来渲染这些对象并调用上述方法。 对于复杂图形如由多个基本形状组合而成的情况,先构建好目标图形再执行导出操作是一个有效策略。下面给出一段具体的代码实例用于展示如何完成这一过程[^4]: ```javascript // 创建一个隐藏的新画布用于临时渲染要导出的对象们 var tempCanvas = this.lowerCanvasEl.cloneNode(); tempCanvas.width = canvas.getWidth(); tempCanvas.height = canvas.getHeight(); // 将原画布上的指定对象复制到新画布上 var objectsToExport = []; // 这里应该填入想要导出的具体对象列表 objectsToExport.forEach(function(object){ object.set({ canvas: tempCanvas, left: object.left * scale, top: object.top * scale }); }); // 渲染至新的上下文中 var ctx = tempCanvas.getContext('2d'); ctx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); for (let obj of objectsToExport) { obj.render(ctx); } // 获取导出后的图像URL字符串 var imgData = tempCanvas.toDataURL("image/png"); // 移除不再需要的临时资源 document.body.removeChild(tempCanvas); console.log(imgData); // 输出得到的数据链接形式的结果 ``` 此段脚本展示了怎样选取某些特殊对象并将它们单独保存下来作为一张独立的 PNG 图片文件。注意这里的 `scale` 参数是用来调整最终输出尺寸大小的比例因子;而 `objectsToExport` 数组则需根据实际情况填充那些待导出的目标对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值