antv/G6使用详细介绍,一篇文章说清antv G6如何使用

前言

AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。

G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。

它是一款国产可视化插件,中文官方文档方便阅读和学习。G6可以实现很多d3才能实现的可视化图表,d3作为一款国外很强大的可视化插件,它的官方文档是非汉语文档,社区虽然很活跃,但几乎是英文文档,阅读和学习起来并不是那么轻松,尤其是英语不太好的同学,阅读和学习d3更吃力。这时候G6就是不错的选择。因为G6包含丰富的图表类型,还可以实现节点,边等自定义。

使用步骤

安装&引入

npm install --save @antv/g6	//安装

import G6 from '@antv/g6';	//在需要的js文件引入

使用

创建容器

<div id="container"></div>
<style>
#container{
  width: 100%;
  height: 800px;
}
</style>

数据格式

const data = {
// 节点
  nodes: [
    {
      id: 'node1',
      x: 100,
      y: 200,
    },
    {
      id: 'node2',
      x: 300,
      y: 200,
    },
  ],
  // 边集
  edges: [
    // 表示一条从 node1 节点连接到 node2 节点的边
    {
      source: 'node1',
      target: 'node2',
    },
  ],
};

节点样式

主要包括nodes(节点)和edges(边),节点是画布上显示的小矩形,边是两个节点的连线,节点样式可以设置为

  • circle:圆;
  • rect:矩形;
  • ellipse:椭圆;
  • polygon:多边形;
  • fan:扇形;
  • image:图片;
  • marker:标记;
  • path:路径;
  • text:文本;
  • dom(svg):DOM(图渲染方式 renderer'svg' 时可用)。

常用方法

  • draw()画布的绘制方法。新增shape或group后,调用此方法将最新的内容渲染到画布上。
  • changeSize(width, height)改变画布的大小
  • getClientByPoint(x, y)将窗口坐标转换为canvas坐标。
  • getPointByClient(x, y)将canvas坐标转换为窗口坐标。
  • on(eventType, callback)绑定事件。
  • off(eventType, callback)事件解绑。
  • addShape(shape, attrs)添加单个图形到画布。
  • addGroup(attrs)添加单个组到画布。
  • attr()设置或获取实例的绘图属性,无参数获取,有参数更新
  • set(name, value)设置实例的属性,如visible, zIndex, id等。
  • get(name)获取实例的属性值
  • show()显示某实例对应的图形。
  • hide()隐藏某实例对应的图形
  • remove()删除实例本身
  • destroy()销毁实例
  • getBBox()获取实例的包围盒

graph method

  • graph.save()
  • graph.read(data) 读数据渲染
  • graph.find(id) 寻找数据模型
  • graph.add(type, model)
  • graph.remove(item)
  • graph.update(item, model) item为id或 项对象
  • graph.getItems();获取图内所有项
  • graph.getNodes()
  • graph.getEdges()
  • graph.getGroups()
  • graph.preventAnimate(callback) 阻止动画
  • getShape(x,y)返回该坐标点最上层的元素。
  • findById(id)根据元素ID返回对应的实例

案例

  • 脑图有个特点,就是传入data可以不需要提供x,y,new了graph之后,自动生成了x,y,然后方向可以在node里进行配置:
let centerX = 0;
graph.node(function (node: NodeConfig) {
    if (node.id === "root") {
        centerX = node.x as number;//把第一个节点的x作为center
    }

    return {
        label: node.id,
        labelCfg: {
            position://看有没有孩子和孩子数量配置左右
                node.children && node.children.length > 0
                    ? "right"
                    : (node.x as number) > centerX
                    ? "right"
                    : "left",
            offset: 5,
        },
    };
});

  • 节点拖拽移动。主要监听node:drag事件,然后去取鼠标的xy,转换为graph的xy,最后赋给节点。
graph.on("node:dragstart", (e: any) => {
    const item = e.item;
    const model = item.getModel();
    model.style.cursor = "grab";
    graph.update(item, model);
    graph.paint();
});

graph.on("node:drag", (e: any) => {
    // 鼠标所在位置 转化为现在目标节点所在位置
    const { clientX, clientY } = e;
    // 将视口坐标转换为屏幕/页面坐标。
    const point = graph.getPointByClient(clientX, clientY);
    const item = e.item;
    const model = item.getModel();
    item.updatePosition(point);
    graph.update(item, model);
    graph.paint();
});

graph.on("node:dragend", (e: any) => {
    const item = e.item;
    const model = item.getModel(); //直接取得model没style。。。
    model.style.cursor = "default";
    graph.update(item, model);
    graph.paint();
});
graph.on("canvas:drag", (e: any) => {
    //	console.log(e);
});
graph.on("dragstart", (e: any) => {
    //比node:dragstart先
});
graph.on("mousedown", (e: any) => {
    //比dragstart先
    const item = e.item;
    if (item) {
        const model = item.getModel();

        model.style.cursor = "grab";
        graph.update(item, model);
        graph.paint();
    }
});

自定义节点

  • 其实是先注册个节点,注册节点时,可以利用addshape做节点样子,同时可以绑定事件,给节点赋文本。
  • 需要注意是这里用的是图形分组概念,g6里面不知道哪个人搞了那么多名字全是group要么是groups很难区分。
  • 通过一个g6实例的group,可以找到其所属的item。
import React, { useEffect, useRef } from "react";
import G6 from "@antv/g6";
import { NodeConfig } from "@antv/g6/lib/types";
import GGroup from "@antv/g-canvas/lib/group";
import { IShape } from "@antv/g-canvas/lib/interfaces";
const data = {
    nodes: [
        {
            id: "Model",
            type: "model-node", //这个就是注册的
            x: 100,
            y: 100,
            style: {
                width: 160,
                height: 100,
                fill: "#f1b953",
                stroke: "#f1b953",
            },
            openIcon: {
                x: 180, // 控制图标在横轴上的位置
                y: 45, // 控制图标在纵轴上的位置
                fontSize: 20,
                style: {
                    fill: "#fc0",
                },
            },
            hideIcon: {
                x: 180, // 控制图标在横轴上的位置
                y: 45, // 控制图标在纵轴上的位置
                fontSize: 20,
                style: {
                    fill: "#666",
                },
            },
            labels: [
                {
                    x: 10,
                    y: 20,
                    label: "标题,最长10个字符~~",
                    labelCfg: {
                        fill: "#666",
                        fontSize: 14,
                        maxlength: 10,
                    },
                },
                {
                    x: 10,
                    y: 40,
                    label: "描述,最长12个字333符~~~",
                        labelCfg: {
                            fontSize: 12,
                            fill: "#999",
                            maxlength: 12,
                        },
                    },
            ],
        },
        {
            id: "node1", // String,该节点存在则必须,节点的唯一标识
            x: 100, // Number,可选,节点位置的 x 值
            y: 200, // Number,可选,节点位置的 y 值
        },
    ],
};

interface modelNodeType extends NodeConfig {
    openIcon: {
        x: number;
        y: number;
        fontSize: number;
        style: Object;
    };
    hideIcon: {
        x: number;
        y: number;
        fontSize: number;
        style: Object;
    };
    labels: Array<any>;
}

// 注册自定义节点
G6.registerNode(
    "model-node",
    {
        drawShape(cfg: modelNodeType, group) {
            const opts = cfg;
            const openIcon = opts.openIcon;
            const hideIcon = opts.hideIcon;
            // 添加节点
            const shape = group!.addShape("rect", {
                    name: "model-node",
                    draggable: true, // 让自定义节点支持拖拽
                    attrs: cfg.style,
            });

            const openSwitch = group!.addShape("circle", {
                draggable: true,
                attrs: {
                    r: 10,
                    ...openIcon,
                    ...openIcon.style,
                },
                className: "state-open",
            });

            const hideSwitch = group!.addShape("circle", {
                draggable: true,
                attrs: {
                    r: 10,
                    ...hideIcon,
                    ...hideIcon.style,
                },
                className: "state-hide",
            });

            // 添加多行文本
            for (let i = 0; i < cfg.labels.length; i++) {
                const item = cfg.labels[i];
                const {
                    label,
                    labelCfg: { maxlength },
                } = item;

                let text = maxlength ? label.substr(0, maxlength) : label || "";

                if (label.length > maxlength) {
                    text = `${text}...`;
                }

                group!.addShape("text", {
                    attrs: {
                        text,
                        ...item,
                        ...item.labelCfg,
                    },
                });
            }
            this.bindEvent(group, openSwitch);
            this.bindEvent(group, hideSwitch);

            return shape;
        },
        bindEvent(group: GGroup, btn: IShape) {
            //ggroup就是graphics group缩写
            btn.on("click", () => {
                const open = group
                    .get("children")
                    .find((child: any) => child.cfg.className === "state-open");
                    const close = group
                        .get("children")
                        .find((child: any) => child.cfg.className === "state-hide");
                    if (btn.cfg.className === "state-open") {
                        const item = group.get("item"); //在这个图形分组下的item
                        const model = item.getModel();
                        open.toBack();
                        close.toFront(); //这个是让2个圆z轴位置变化 item上的方法
                        model.style.height = 100;
                        item.update(model);
                    } else if (btn.cfg.className === "state-hide") {
                        const item = group.get("item");
                        const model = item.getModel();
                        close.toBack(); //Item 上的方法
                        open.toFront(); //item上的方法
                        model.style.height = 50;
                        item.update(model); //item上的方法
                    }
                });
            },
	},
    "single-node"
); // 继承自内置节点
function App() {
    const ref = useRef<HTMLDivElement>(null);
    useEffect(() => {
        const graph = new G6.Graph({
            container: ref.current!,
            width: 800,
            height: 800,
            // renderer: 'svg',
            fitCenter: true,
            modes: {
                default: ["drag-canvas", "zoom-canvas", "drag-node"],
            },
        });
        // 传入数据
        graph.data(data);
        // 执行渲染
        graph.render();
        // graph.fitView();
    }, []);

    return <div ref={ref} id="container"></div>;
}

export default App;

  • g6的自定义边跟自定义节点是类似操作。
G6.registerEdge("hvh", {
    draw(cfg, group) {
        const startPoint = cfg!.startPoint!;
        const endPoint = cfg!.endPoint!;
        const startArrow = (cfg!.style && cfg!.style.startArrow) || undefined;
        const endArrow = (cfg!.style && cfg!.style.endArrow) || undefined;
        const shape = group!.addShape("path", {
            attrs: {
                stroke: "#333",
                path: [
                    ["M", startPoint.x, startPoint.y],
                    [
                        "L",
                        endPoint.x / 3 + (1 / 3) * startPoint.x,
                        endPoint.y / 2 + (1 / 3) * startPoint.y,
                    ],
                    [
                        "L",
                        endPoint.x * 1.1 + (2 / 3) * startPoint.x,
                        endPoint.y / 2 + (2 / 3) * startPoint.y,
                    ],

                    ["L", endPoint.x, endPoint.y],
                ],
                startArrow, //初始化配统一的箭头
                endArrow,
            },
            // must be assigned in G6 3.3 and later versions. it can be any value you want
            name: "path-shape",
        });
        return shape;
    },
    setState(name, value, item) { //这个方法在3.3后可以不用,改为直接设置全局node/edge StateStyles
        const group = item!.getContainer();
        const shape = group.get("children")[0]; // 顺序根据 draw 时确定
        if (name === "active") {
            if (value) {
                //
                shape.attr("stroke", "red");
                shape.attr("lineWidth", 3);
            } else {
                shape.attr("stroke", "#333");
                shape.attr("lineWidth", 1);
            }
        }
        if (name === "selected") {
            if (value) {
                shape.attr("lineWidth", 3);
            } else {
                shape.attr("lineWidth", 1);
            }
        }
    },
});

  • 初始化可以统一配个箭头样式:
defaultEdge: {
    type: "line-arrow",
    style: {
        stroke: "#F6BD16",
        startArrow: {
            path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
            fill: "#F6BD16",
        },
        endArrow: {
            path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
            fill: "#F6BD16",
        },
    },
},

说明

  • g6里为了传递信息,设置了state,这个状态有全局设置或者单个设置。一般来说,统一用全局设置。这个主要用来判断点击了没有hover了没有之类。

  • 有点诡异的是好像相同行为触发的不同状态之间会冲突,所以最好是1个状态对应多值而不是多个状态对应二值。

  • 然后文档全篇在说怎么设置值,没说怎么获取值。。。。。。。后来发现item里写了个方法getState可以拿到state所有值,我也是服了。。state的概念里不写怎么获取,只写hasState是用来判断二值的。然后这个方法获取的值也很诡异,是个数组字符串,比如设置的值是active , ‘1’ ,那么数组里面值是:active:1的字符串。(这设计谁想出来的。。。不能做成key value?不能按key取值?实在不行按active:1字符串取值也行啊)

  • 比如上面那个例子就有冲突,监听那里改下:

graph.on("edge:click", (ev: IG6GraphEvent) => {
    const edge = ev.item;
    const value = edge!.getStates()[0];
    if (value !== "active:0") {
        if (value === "active:2") {
            graph.setItemState(edge!, "active", "0");
        } else {
            graph.setItemState(edge!, "active", "2"); // 切换选中
        }
    }
});

graph.on("edge:mouseenter", (ev: IG6GraphEvent) => {
    const edge = ev.item;
    const value = edge!.getStates()[0];
    if (value !== "active:2") {
        graph.setItemState(edge!, "active", "1");
    }
});

graph.on("edge:mouseleave", (ev: IG6GraphEvent) => {
    const edge = ev.item;
    const value = edge!.getStates()[0];
    if (value !== "active:2") {
        graph.setItemState(edge!, "active", "0");
    }
});

编辑器

对于编辑,我摸索了一番,实际就是要自己做个dom面板来进行编辑,我以右键弹出选框选择编辑label为例(实际应该点击后把input设置成显示,修改完毕搞个按钮然后点击保存。再关闭input)。我直接就input不操作显示与否了:

import React, { useEffect, useRef, useState } from "react";
import G6 from "@antv/g6";
import { NodeConfig, Item, IG6GraphEvent } from "@antv/g6/lib/types";
import GGroup from "@antv/g-canvas/lib/group";
import { IShape } from "@antv/g-canvas/lib/interfaces";
const data = {
    nodes: [
        {
            id: "Model",
            type: "model-node", //这个就是注册的
            x: 200,
            y: 100,
            style: {
                width: 160,
                height: 100,
                fill: "#f1b953",
                stroke: "#f1b953",
            },
            openIcon: {
                x: 180, // 控制图标在横轴上的位置
                y: 45, // 控制图标在纵轴上的位置
                fontSize: 20,
                style: {
                        fill: "#fc0",
                },
            },
            hideIcon: {
                x: 180, // 控制图标在横轴上的位置
                y: 45, // 控制图标在纵轴上的位置
                fontSize: 20,
                style: {
                    fill: "#666",
                },
            },
            labels: [
                {
                    x: 10,
                    y: 20,
                    label: "标题,最长10个字符~~",
                    labelCfg: {
                        fill: "#666",
                        fontSize: 14,
                        maxlength: 10,
                    },
                },
                {
                    x: 10,
                    y: 40,
                    label: "描述,最长12个字333符~~~",
                    labelCfg: {
                        fontSize: 12,
                        fill: "#999",
                        maxlength: 12,
                    },
                },
            ],
            anchorPoints: [
                //这属性用来设定边的连接中心
                [0, 0.5],
                [0, 1],
            ],
        },
        {
            id: "node1", // String,该节点存在则必须,节点的唯一标识
            label: "node1",
            x: 10, // Number,可选,节点位置的 x 值
            y: 200, // Number,可选,节点位置的 y 值
            size: 50,
            anchorPoints: [
                //这属性用来设定边的连接中心
                [1, 0.5],
                [1, 0.8],
            ],
        },
        {
            id: "node2", // String,该节点存在则必须,节点的唯一标识
            label: "node2",
            size: 50,
            x: 70, // Number,可选,节点位置的 x 值
            y: 20, // Number,可选,节点位置的 y 值
            anchorPoints: [
                //这属性用来设定边的连接中心
                [1, 0.5],
                [0, 0.5],
                [0.5, 1],
            ],
        },
    ],
    edges: [
        {
            id: "edge1",
            target: "Model",
            source: "node1",
            type: "hvh",
            // 该边连入 source 点的第 0 个 anchorPoint,
            sourceAnchor: 1,
            // 该边连入 target 点的第1个 anchorPoint,
            targetAnchor: 1,
        },
        {
            id: "edge2",
            target: "node2",
            source: "node1",
            type: "hvh",
        },
        {
            id: "edge3",
            target: "node2",
            source: "Model",
            type: "hvh",
            targetAnchor: 2,
        },
    ],
};

interface modelNodeType extends NodeConfig {
    openIcon: {
        x: number;
        y: number;
        fontSize: number;
        style: Object;
    };
    hideIcon: {
        x: number;
        y: number;
        fontSize: number;
        style: Object;
    };
    labels: Array<any>;
}

function shapesAddAttr(shape: Array<any>, key: string, value: any) {
    shape.forEach((v) => v.attr(key, value));
}

G6.registerEdge("hvh", {
    draw(cfg, group) {
        const startPoint = cfg!.startPoint!;
        const endPoint = cfg!.endPoint!;
        const startArrow = (cfg!.style && cfg!.style.startArrow) || undefined;
        const endArrow = (cfg!.style && cfg!.style.endArrow) || undefined;
        const shape = group!.addShape("path", {
            attrs: {
                stroke: "#333",
                path: [
                        ["M", startPoint.x, startPoint.y],
                        [
                                "L",
                                endPoint.x / 3 + (1 / 3) * startPoint.x,
                                endPoint.y / 2 + (1 / 3) * startPoint.y,
                        ],
                        [
                                "L",
                                endPoint.x * 1.1 + (2 / 3) * startPoint.x,
                                endPoint.y / 2 + (2 / 3) * startPoint.y,
                        ],

                        ["L", endPoint.x, endPoint.y],
                ],
                startArrow, //初始化配统一的箭头
                endArrow,
            },
            // must be assigned in G6 3.3 and later versions. it can be any value you want
            name: "path-shape",
        });
        return shape;
    },
    setState(name, value, item) {
        //这个方法在3.3后可以不用,改为直接设置全局node/edge StateStyles
        const group = item!.getContainer();
        const shape = group.get("children"); // 顺序根据 draw 时确定
        if (name === "active") {
            switch (value) {
                case "0":
                    shapesAddAttr(shape, "stroke", "#333");
                    shapesAddAttr(shape, "lineWidth", 1);
                    break;
                case "1":
                    shapesAddAttr(shape, "stroke", "red");
                    shapesAddAttr(shape, "lineWidth", 3);
                    break;
                case "2":
                    shapesAddAttr(shape, "stroke", "blue");
                    shapesAddAttr(shape, "lineWidth", 3);
                    break;
                default:
                    return;
            }
        }
    },
});

// 注册自定义节点
G6.registerNode(
    "model-node",
    {
        drawShape(cfg: modelNodeType, group) {
            const opts = cfg;
            const openIcon = opts.openIcon;
            const hideIcon = opts.hideIcon;
            // 添加节点
            const shape = group!.addShape("rect", {
                    name: "model-node",
                    draggable: true, // 让自定义节点支持拖拽
                    attrs: cfg.style,
            });

            const openSwitch = group!.addShape("circle", {
                    draggable: true,
                    attrs: {
                            r: 10,
                            ...openIcon,
                            ...openIcon.style,
                    },
                    className: "state-open",
            });

            const hideSwitch = group!.addShape("circle", {
                    draggable: true,
                    attrs: {
                            r: 10,
                            ...hideIcon,
                            ...hideIcon.style,
                    },
                    className: "state-hide",
            });

            // 添加多行文本
            for (let i = 0; i < cfg.labels.length; i++) {
                const item = cfg.labels[i];
                const {
                    label,
                    labelCfg: { maxlength },
                } = item;

                let text = maxlength ? label.substr(0, maxlength) : label || "";

                if (label.length > maxlength) {
                    text = `${text}...`;
                }

                group!.addShape("text", {
                    attrs: {
                        text,
                        ...item,
                        ...item.labelCfg,
                    },
                });
            }
            this.bindEvent(group, openSwitch);
            this.bindEvent(group, hideSwitch);

            return shape;
        },
        bindEvent(group: GGroup, btn: IShape) {
            //ggroup就是graphics group缩写
            btn.on("click", () => {
                const open = group
                    .get("children")
                    .find((child: any) => child.cfg.className === "state-open");
                const close = group
                    .get("children")
                    .find((child: any) => child.cfg.className === "state-hide");
                if (btn.cfg.className === "state-open") {
                    const item = group.get("item"); //在这个图形分组下的item
                    const model = item.getModel();
                    open.toBack();
                    close.toFront(); //这个是让2个圆z轴位置变化 item上的方法
                    model.style.height = 100;
                    item.update(model);
                } else if (btn.cfg.className === "state-hide") {
                    const item = group.get("item");
                    const model = item.getModel();
                    close.toBack(); //Item 上的方法
                    open.toFront(); //item上的方法
                    model.style.height = 50;
                    item.update(model); //item上的方法
                }
            });
        },
    },
    "single-node"
); // 继承自内置节点

function App() {
    const ref = useRef<HTMLDivElement>(null);
    const [state, setState] = useState("");
    const [changeItem, setChangeItem] = useState<Item>();
    useEffect(() => {
        const contextMenu = new G6.Menu({
            getContent(graph) {
                console.log("graph", graph);
                return `<div>编辑lable</div>`;
            },
            handleMenuClick: (target, item) => {
                //target是dom item 是Item
                //只有click了才知道是哪个节点触发的
                const model = item.getModel();
                const value = model.label;
                if (typeof value === "string") {
                    //将节点的值赋给input并绑上onchange给它
                    setState(value);
                    setChangeItem(item); //要调用更新,最小是item item才有update
                }
            },
        });
        const graph = new G6.Graph({
            container: ref.current!,
            width: 800,
            height: 800,
            // renderer: 'svg',
            fitCenter: true,
            modes: {
                default: ["drag-canvas", "zoom-canvas", "drag-node"],
                //edit: ["click-select", "click-add-node"],
            },
            defaultEdge: {
                type: "line-arrow",
                style: {
                    stroke: "#F6BD16",
                    startArrow: {
                        path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
                        fill: "#F6BD16",
                    },
                    endArrow: {
                        path: "M 0,0 L 12,6 L 9,0 L 12,-6 Z",
                        fill: "#F6BD16",
                    },
                },
            },
            plugins: [contextMenu],
        });

        // graph.on("node:click", (ev: any) => {
        // 	console.log(ev);
        // 	graph.setMode("edit");
        // });

        graph.on("edge:click", (ev: IG6GraphEvent) => {
            const edge = ev.item;
            const value = edge!.getStates()[0];
            if (value !== "active:0") {
                if (value === "active:2") {
                    graph.setItemState(edge!, "active", "0");
                } else {
                    graph.setItemState(edge!, "active", "2"); // 切换选中
                }
            }
        });

        graph.on("edge:mouseenter", (ev: IG6GraphEvent) => {
            const edge = ev.item;
            const value = edge!.getStates()[0];
            if (value !== "active:2") {
                graph.setItemState(edge!, "active", "1");
            }
        });

        graph.on("edge:mouseleave", (ev: IG6GraphEvent) => {
            const edge = ev.item;
            const value = edge!.getStates()[0];
            if (value !== "active:2") {
                graph.setItemState(edge!, "active", "0");
            }
        });

		
        // 传入数据
        graph.data(data);
        // 执行渲染
        graph.render();
        // graph.fitView();
    }, []);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setState(e.target.value);
        //修改state后改变节点Label
        if (changeItem) {//其他样式控制同理这么操作
            const model = changeItem.getModel();
            model.label = e.target.value;
            changeItem.update(model);
        }
    };

    return (
        <div>
            <div id="editor">
                <span>修改label :</span>
                <input  value={state} onChange={handleChange}></input>
            </div>
            <div ref={ref} id="container"></div>
        </div>
    );
}

export default App;

最后需要注意下自定义节点的格式,如果像我这么写自定义节点,那么label很可能就不对或者没有,所以事先需要规划好到底哪些属性应该去配置,哪些属性可以配置。

  • 5
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值