Vue 整合 Vue Flow:从零构建交互式流程图

引言

        Vue Flow 是一个基于 Vue 的流程图库,结合 Vue.js 的组件化优势,可用于创建交互式、可视化的流程图。本文将逐步引导你整合 Vue Flow,涵盖从环境配置到自定义节点、拖拽、连线、删除、JSON 管理和优化,适合初学者和高级开发者。

目的

 本文旨在提供一个全面的指南,帮助开发者:

  • 理解 Vue Flow 的核心功能和集成流程。
  • 实现交互式流程图,包括拖拽、连线和节点管理。
  • 掌握数据持久化(保存和导入 JSON),提升项目实用性。
  • 优化性能并解决常见问题,确保生产环境稳定。

适用场景

  • 工作流管理:设计审批或任务流程。
  • 数据可视化:展示组织结构或网络拓扑。
  • 工业自动化:模拟设备连接和生产流程。
  • 教育工具:可视化算法或逻辑步骤。
  • 说明插图:绘制一个工业流程图,包含 “设备1”, “工序1”, “设备2” 节点和连接线。

环境准备

  • 项目初始化:使用 Vue 3 + Vite 创建项目。

  • 依赖安装:

npm install @vue-flow/core @vue-flow/background @vue-flow/controls @vue-flow/minimap ant-design-vue

@vue-flow/core:核心库。
@vue-flow/background:背景网格。
@vue-flow/controls:交互控制。
@vue-flow/minimap:画布概览。
ant-design-vue:用于按钮样式。
在这里插入图片描述

基础组件 (index.vue)

创建 src/components/FlowChart.vue 作为流程图容器。

<template>
  <div class="flow-container">
    <div class="layout">
      <!-- 左侧可选项区域 -->
      <div class="sidebar">
        <h3>可拖拽节点</h3>
        <div
          v-for="option in nodeOptions"
          :key="option.type"
          class="draggable-node"
          :data-type="option.type"
          draggable="true"
          @dragstart="onDragStart"
        >
          <div class="node-label">{{ option.label }}</div>
          <div class="node-preview-wrapper">
            <div :class="['inner-shape', option.shapeClass]">
              <div class="label">{{ option.label }}</div>
            </div>
          </div>
        </div>
      </div>

      <!-- 右侧 Vue Flow 画布 -->
      <div class="flow-area">
        <div class="toolbar">
          <a-button type="primary" @click="addRandomNode">添加随机节点</a-button>
<!--          <a-button type="danger" @click="deleteSelected">删除选中节点</a-button>-->
          <a-button type="default" @click="exportJson">导出 JSON</a-button>
          <a-button type="default" @click="saveExportJson">保存流程图</a-button>
        </div>

        <VueFlow
          v-model:nodes="nodes"
          v-model:edges="edges"
          :node-types="nodeTypes"
          :default-edge-options="{
            type: 'smoothstep',
            animated: true,
            markerEnd: { type: 'arrowclosed', color: '#ff0000' },
            style: { stroke: '#ff0000', strokeWidth: 2, strokeDasharray: 'none' },
          }"
          :connection-line-style="{ stroke: '#ff0000', strokeWidth: 2, strokeDasharray: 'none' }"
          :fit-view-on-init="true"
          :connectable="true"
          @drop="onDrop"
          @dragover.prevent
          @connect="onConnect"
          @node-click="onNodeClick"
          @node-drag-start="onNodeDragStart"
          @node-drag-stop="onNodeDragStop"
          @pane-click="onPaneClick"
        >
          <Background variant="dots" :gap="20" />
          <Controls />
          <MiniMap />
        </VueFlow>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {ref, markRaw, onMounted} from 'vue'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import type { NodeTypesObject } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import { message } from 'ant-design-vue'
import DiamondNode from './DiamondNode.vue'
import CircleNode from './CircleNode.vue'
import ImageNode from './ImageNode.vue'
import DeviceNode from './DeviceNode.vue'
import UserNode from './UserNode.vue'
import RoundedRectangleNode from './RoundedRectangleNode.vue'
import { editingNodeId } from './store'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import '../style/node-styles.css'

const nodes = ref([])
const edges = ref<any>([])

const nodeTypes: NodeTypesObject = {
  diamond: markRaw(DiamondNode),
  circle: markRaw(CircleNode),
  image: markRaw(ImageNode),
  user: markRaw(UserNode),
  device: markRaw(DeviceNode),
  roundedRectangle: markRaw(RoundedRectangleNode),
}

const { addNodes, getNodes, deleteElements,removeNodes } = useVueFlow()

const nodeOptions = ref([
  { type: 'circle', label: '开始节点', shapeClass: 'node-circle' },
  { type: 'circle', label: '结束节点', shapeClass: 'node-circle' },
  // { type: 'image', label: '图片节点', shapeClass: 'node-image' }, // 新增图片节点
  // { type: 'device', label: '设备节点', shapeClass: 'node-device' }, // 新增设备节点
  // { type: 'user', label: '人员节点', shapeClass: 'node-user' }, // 新增人员节点
  // { type: 'roundedRectangle', label: '圆角长方形节点', shapeClass: 'node-rounded-rectangle' },

])

function initData(){
  return [
    { type: 'device', label: '设备节点', shapeClass: 'node-device',id:1,code:'123' },
    { type: 'device', label: '工序1', shapeClass: 'node-device',id:1,code:'123' },
    { type: 'device', label: '工序2', shapeClass: 'node-device',id:1,code:'123' },
    { type: 'device', label: '工序3', shapeClass: 'node-device',id:1,code:'123' },
  ]
}
function getData(){
    nodes.value = [{"":""}];
     edges.value = [{"":""}];
     
onMounted(() => {
  let data  =initData();
  nodeOptions.value = [...nodeOptions.value, ...data];
  getData();
})
// Drag-and-drop handlers
function onDragStart(event: DragEvent) {
  if (event.dataTransfer && event.target instanceof HTMLElement) {
    const type = event.target.dataset.type
    if (type) {
      event.dataTransfer.setData('application/vueflow-node-type', type)
    }
  }
}

function onDrop(event: DragEvent) {
  const type = event.dataTransfer?.getData('application/vueflow-node-type')
  if (!type) return

  const bounds = (event.currentTarget as HTMLElement).getBoundingClientRect()
  const position = {
    x: event.clientX,
    y: event.clientY,
  }

  const label = type === 'diamond' ? '判断节点' : type === 'circle' ? '开始节点' : '圆角长方形节点'
  addNodes([
    {
      id: `${type}-${Date.now()}`,
      type,
      position,
      data: { label },
    },
  ])
}

// Connection handler
function onConnect(params: any) {
  edges.value = [
    ...edges.value,
    {
      ...params,
      id: `edge-${params.source}-${params.target}`,
      type: 'smoothstep',
      animated: true,
      markerEnd: { type: 'arrowclosed', color: '#ff0000' },
      style: { stroke: '#ff0000', strokeWidth: 2, strokeDasharray: 'none' },
    },
  ]
}

// Node interaction handlers
function onNodeClick(event: any, node: any) {
  nodes.value = nodes.value.map(n => ({
    ...n,
    selected: n.id === node.id ? true : false,
  }))
  // if (node.type === 'circle') {
  //   editingNodeId.value = node.id // 仅对圆形节点启用编辑模式
  // }
}

function onNodeDragStart(event: any, node: any) {
  console.log('Node drag started:', node)
}

function onNodeDragStop(event: any, node: any) {
  console.log('Node drag stopped:', node)
}

function onPaneClick() {
  nodes.value = nodes.value.map(n => ({ ...n, selected: false }))
  editingNodeId.value = null // 退出编辑模式
}

// Add a random node
function addRandomNode() {
  const types = ['diamond', 'circle', 'roundedRectangle','image','user','device']
  const type = types[Math.floor(Math.random() * types.length)]
  const label = type === 'diamond' ? '判断节点' : type === 'circle' ? '圆形节点' : '圆角长方形节点'
  addNodes([
    {
      id: `${type}-${Date.now()}`,
      type,
      position: { x: Math.random() * 500, y: Math.random() * 500 },
      data: { label },
    },
  ])
}

// Delete selected nodes
function deleteSelected() {
  const selectedIds = getNodes.value.filter(n => n.selected).map(n => n.id)
  if (selectedIds.length === 0) {
    message.warning('请先选中一个节点')
    return
  }
  removeNodes({ selectedIds })
  message.success('节点删除成功')
  editingNodeId.value = null // 退出编辑模式
}


function saveExportJson(){
  //todo 保存到数据库
}
// Export JSON
function exportJson() {
  const json = JSON.stringify({ nodes: nodes.value ,edges: edges.value }, null, 2)
  const blob = new Blob([json], { type: 'application/json' })
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'flowchart.json'
  a.click()
  window.URL.revokeObjectURL(url)
  message.success('JSON 文件已导出')
}

// 更新节点标签
function updateNodeLabel(nodeId: string, newLabel: string) {
  nodes.value = nodes.value.map(n =>
    n.id === nodeId ? { ...n, data: { ...n.data, label: newLabel } } : n
  )
  editingNodeId.value = null // 编辑完成后退出编辑模式
}
</script>

       说明:基础组件包含侧边栏(拖拽源)、工具栏(操作按钮)和 VueFlow 画布,绑定了节点和边数据。

自定义组件 (矩形、菱形等)

定义不同形状的节点,添加多连接点。

RectangleNode.vue (矩形节点):

<template>
  <div class="node rectangle">
    <Handle type="target" position="top" id="top-target" />
    <Handle type="source" position="top" id="top-source" />
    <Handle type="target" position="bottom" id="bottom-target" />
    <Handle type="source" position="bottom" id="bottom-source" />
    <Handle type="target" position="left" id="left-target" />
    <Handle type="source" position="left" id="left-source" />
    <Handle type="target" position="right" id="right-target" />
    <Handle type="source" position="right" id="right-source" />
    <div class="label">{{ data.label }}</div>
  </div>
</template>

<script setup>
import { Handle } from '@vue-flow/core'
defineProps({ data: Object })
</script>

<style scoped>
.node {
  width: 100px;
  height: 60px;
  border: 1px solid #333;
  display: flex;
  align-items: center;
  justify-content: center;
}
.label { text-align: center; }
</style>

DiamondNode.vue (菱形节点):

<template>
  <div class="diamond-node">
    <!-- 顶部连接点:source 和 target -->
    <Handle type="target" position="top" class="handle" id="top-target" />
    <Handle type="source" position="top" class="handle" id="top-source" />
    <!-- 左侧连接点:source 和 target -->
    <Handle type="target" position="left" class="handle" id="left-target" />
    <Handle type="source" position="left" class="handle" id="left-source" />
    <!-- 右侧连接点:source 和 target -->
    <Handle type="target" position="right" class="handle" id="right-target" />
    <Handle type="source" position="right" class="handle" id="right-source" />
    <!-- 底部连接点:source 和 target -->
    <Handle type="target" position="bottom" class="handle" id="bottom-target" />
    <Handle type="source" position="bottom" class="handle" id="bottom-source" />
    <!-- 节点本体 -->
    <div class="diamond">
      <div class="label">{{ data.label }}</div>
    </div>
  </div>
</template>

<script setup>
import { Handle } from '@vue-flow/core'

defineProps({
  data: Object,
})
</script>

<style scoped>
.diamond-node {
  position: relative;
  width: 80px;
  height: 80px;
  overflow: visible;
}

.diamond {
  width: 100%;
  height: 100%;
  background: #2ec4b6;
  transform: rotate(45deg);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid #333;
  color: white;
  font-weight: bold;
  box-sizing: border-box;
  z-index: 1;
}

.label {
  transform: rotate(-45deg);
  text-align: center;
  pointer-events: none;
  font-size: 12px;
  padding: 4px;
  max-width: 90%;
  word-break: break-all;
}

.handle {
  width: 10px;
  height: 10px;
  background: #ff0000;
  border-radius: 50%;
  position: absolute;
  z-index: 2;
}

:deep(.vue-flow__handle-top) {
  top: -10px; /* 增加偏移量,使点移到顶部外部 */
  left: 50%;
  transform: translateX(-50%);
}

:deep(.vue-flow__handle-bottom) {
  bottom: -10px; /* 增加偏移量,使点移到底部外部 */
  left: 50%;
  transform: translateX(-50%);
}

:deep(.vue-flow__handle-left) {
  left: -10px; /* 增加偏移量,使点移到左侧外部 */
  top: 50%;
  transform: translateY(-50%);
}

:deep(.vue-flow__handle-right) {
  right: -10px; /* 增加偏移量,使点移到右侧外部 */
  top: 50%;
  transform: translateY(-50%);
}
</style>

说明:每个节点包含上下左右的 target 和 source 连接点,允许多向连接。

ImageNode(自定义图片节点):

<template>
  <div class="image-node">
    <!-- 顶部连接点:source 和 target -->
    <Handle type="target" position="top" class="handle" id="top-target" />
    <Handle type="source" position="top" class="handle" id="top-source" />
    <!-- 左侧连接点:source 和 target -->
    <Handle type="target" position="left" class="handle" id="left-target" />
    <Handle type="source" position="left" class="handle" id="left-source" />
    <!-- 右侧连接点:source 和 target -->
    <Handle type="target" position="right" class="handle" id="right-target" />
    <Handle type="source" position="right" class="handle" id="right-source" />
    <!-- 底部连接点:source 和 target -->
    <Handle type="target" position="bottom" class="handle" id="bottom-target" />
    <Handle type="source" position="bottom" class="handle" id="bottom-source" />
    <!-- 节点本体 -->
    <div class="image-container">
      <img :src="imageSrc" alt="Image Node" class="node-image" />
    </div>
  </div>
</template>

<script setup>
import { Handle } from '@vue-flow/core'
import { computed } from 'vue'

defineProps({
  data: Object,
})

const imageSrc = computed(() => {
  return new URL('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDQ5LjEgKDUxMTQ3KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5Hcm91cCAyPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+CiAgICAgICAgPGNpcmNsZSBpZD0icGF0aC0xIiBjeD0iMzYiIGN5PSIzNiIgcj0iMzYiPjwvY2lyY2xlPgogICAgICAgIDxmaWx0ZXIgeD0iLTkuNyUiIHk9Ii02LjklIiB3aWR0aD0iMTE5LjQlIiBoZWlnaHQ9IjExOS40JSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94IiBpZD0iZmlsdGVyLTIiPgogICAgICAgICAgICA8ZmVPZmZzZXQgZHg9IjAiIGR5PSIyIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIj48L2ZlT2Zmc2V0PgogICAgICAgICAgICA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIyIiBpbj0ic2hhZG93T2Zmc2V0T3V0ZXIxIiByZXN1bHQ9InNoYWRvd0JsdXJPdXRlcjEiPjwvZmVHYXVzc2lhbkJsdXI+CiAgICAgICAgICAgIDxmZUNvbXBvc2l0ZSBpbj0ic2hhZG93Qmx1ck91dGVyMSIgaW4yPSJTb3VyY2VBbHBoYSIgb3BlcmF0b3I9Im91dCIgcmVzdWx0PSJzaGFkb3dCbHVyT3V0ZXIxIj48L2ZlQ29tcG9zaXRlPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAgIDAgMCAwIDAgMCAgIDAgMCAgIDAgMCAwIDAgMCAgMCAwIDAgMC4wNCAwIiB0eXBlPSJtYXRyaXgiIGluPSJzaGFkb3dCbHVyT3V0ZXIxIj48L2ZlQ29sb3JNYXRyaXg+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0i5Z+656GA5rWB56iL5Zu+LTAxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTA2LjAwMDAwMCwgLTkzLjAwMDAwMCkiPgogICAgICAgICAgICA8ZyBpZD0iR3JvdXAtMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTEwLjAwMDAwMCwgOTUuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iT3ZhbCI+CiAgICAgICAgICAgICAgICAgICAgPHVzZSBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIxIiBmaWx0ZXI9InVybCgjZmlsdGVyLTIpIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgICAgICAgICA8dXNlIGZpbGwtb3BhY2l0eT0iMC49MiIgZmlsbD0iI2NjY2M5OThjIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHhsaW5rOmhyZWY9IiNwYXRoLTEiPjwvdXNlPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgc3Ryb2tlPSIjY2NjYzMzYzkiIHN0cm9rZS13aWR0aD0iMSIgY3g9IjM2IiBjeT0iMzYiIHI9IjM1LjUiPjwvY2lyY2xlPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgPHRleHQgaWQ9Iue7k+adn+iKgueCuSIgZm9udC1mYW1pbHk9IlBpbmdGYW5nU0MtUmVndWxhciwgUGluZ0ZhbmcgU0MiIGZvbnQtc2l6ZT0iMTIiIGZvbnQtd2VpZ2h0PSJub3JtYWwiIGxpbmUtc3BhY2luZz0iMTIiIGZpbGw9IiMwMDAwMDAiIGZpbGwtb3BhY2l0eT0iMC42NSI+CiAgICAgICAgICAgICAgICAgICAgPHRzcGFuIHg9IjEyIiB5PSI0MSI+57uT5p2f6IqC54K5PC90c3Bhbj4KICAgICAgICAgICAgICAgIDwvdGV4dD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+', import.meta.url).href
})
</script>

<style scoped>
.image-node {
  position: relative;
  width: 50px; /* 调整为适合图片的尺寸 */
  height: 50px; /* 调整为适合图片的尺寸 */
  overflow: visible;
}

.image-container {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 2px solid #333;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  box-sizing: border-box;
}

.node-image {
  max-width: 50px;
  max-height: 50px;
  overflow: visible;
  position: relative;

}

.handle {
  width: 10px;
  height: 10px;
  background: #ff0000;
  border-radius: 50%;
  position: absolute;
  z-index: 2;
}

:deep(.vue-flow__handle-top) {
  top: -5px; /* 移到外部 */
  left: 50%;
  transform: translateX(-50%);
}

:deep(.vue-flow__handle-bottom) {
  bottom: -5px; /* 移到外部 */
  left: 50%;
  transform: translateX(-50%);
}

:deep(.vue-flow__handle-left) {
  left: -5px; /* 移到外部 */
  top: 50%;
  transform: translateY(-50%);
}

:deep(.vue-flow__handle-right) {
  right: -5px; /* 移到外部 */
  top: 50%;
  transform: translateY(-50%);
}
</style>

在这里插入图片描述

操作实现 (#操作实现)

     拖拽节点 (#拖拽节点)

原理: 使用 draggable 和 onDrop,从侧边栏拖动节点到画布。
代码: 见 index.vue 中的 onDragStart 和 onDrop。
效果:拖动 “开始” 或 “决策” 到画布,生成对应节点,位置基于鼠标坐标。

使用 draggable 和 onDrop,从侧边栏拖动节点到画布。
代码:见 index.vue 中的 onDragStart 和 onDrop。
效果:拖动 “开始” 或 “决策” 到画布,生成对应节点,位置基于鼠标坐标。

     连线 (多连接点)

原理:@connect 捕获连接,Handle 定义多连接点,smoothstep 提供平滑线。
代码:见 index.vue 中的 onConnect。
效果:点击节点任意连接点拖动,生成红线连接,箭头指向目标。
在这里插入图片描述

     删除节点

原理:@node-click 选中,deleteSelected 调用 removeElements。
代码:见 index.vue 中的 onNodeClick 和 deleteSelected。
效果:点击节点高亮,点击删除按钮移除节点及相关边。

在这里插入图片描述

     保存为 JSON

原理:exportJson 序列化 nodes 和 edges,生成下载文件。
代码:见 index.vue 中的 exportJson。

unction exportJson() {
  const json = JSON.stringify({ nodes: nodes.value ,edges: edges.value }, null, 2)
  const blob = new Blob([json], { type: 'application/json' })
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'flowchart.json'
  a.click()
  window.URL.revokeObjectURL(url)
  message.success('JSON 文件已导出')
}

效果:点击按钮下载 flowchart.json,包含当前结构。
在这里插入图片描述

     导入 JSON

原理:importJson 解析上传文件,更新 nodes 和 edges。
代码:见 index.vue 中的 importJson。
效果:上传 JSON 文件,画布还原节点和边。

性能优化建议

  • 限制节点数量:使用虚拟列表优化大规模节点渲染。
  • 防抖处理:对 onDrop 和 onConnect 添加防抖,减少频繁更新。
  • 懒加载:大图节点时,延迟加载图片资源。

常见问题与解决

  • 拖拽位置偏移:检查 bounds 计算,调整 event.clientX - bounds.left。
  • 连接点无效:确保 Handle ID 唯一,检查 connectable 属性。
  • JSON 解析错误:验证文件格式,添加错误处理:
reader.onload = (e: any) => {
  try {
    const data = JSON.parse(e.target.result)
    nodes.value = data.nodes || []
    edges.value = data.edges || []
    message.success('导入成功')
  } catch (error) {
    message.error('JSON 格式错误')
  }
}

总结

      通过 Vue 整合 Vue Flow,开发者可构建功能丰富的交互式流程图。本文从环境准备到自定义节点、拖拽、连线、删除和 JSON 管理,提供了完整指南。Vue Flow 的灵活性使其适用于工作流、数据可视化等场景,建议根据需求优化性能并处理异常。

demo见:

        https://gitee.com/codingtodie/vue-integration-with-vue-flow

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wáng bēn

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

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

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

打赏作者

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

抵扣说明:

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

余额充值