首页 > 其他分享 >Vue3 流程图组件库 :Vue Flow

Vue3 流程图组件库 :Vue Flow

时间:2024-09-24 16:51:28浏览次数:3  
标签:Vue const Flow value id vue Vue3 import 节点

Vue Flow 是一个轻量级的 Vue 3 组件库,它允许开发者以简洁直观的方式创建动态流程图。本篇文章记录一下Vue Flow的基本用法


安装

npm add @vue-flow/core

流程图的构成

Nodes、Edges、Handles

主题

默认样式

通过导入样式文件应用

/* these are necessary styles for vue flow */
@import '@vue-flow/core/dist/style.css';

/* this contains the default theme, these are optional styles */
@import '@vue-flow/core/dist/theme-default.css';

对默认主题进行调整

1.可以使用css类名去覆盖

.vue-flow__node-custom {
        background: purple;
        color: white;
        border: 1px solid purple;
        border-radius: 4px;
        box-shadow: 0 0 0 1px purple;
        padding: 8px;
    }

2.可以在组件上使用style或者class属性进行替换

<VueFlow
    :nodes="nodes"
    :edges="edges"
    class="my-diagram-class"  
    :style="{ background: 'red' }"
    />

3.通过在全局的css文件中对组件的样式变量进行覆盖

:root {
        --vf-node-bg: #fff;
        --vf-node-text: #222;
        --vf-connection-path: #b1b1b7;
        --vf-handle: #555;
    }

具体的css类名和变量名可以通过查阅官方文档确认 Theming | Vue Flow

Nodes

Nodes是流程图中的一个基本组件,可以在图表中可视化任何类型的数据,独立存在并通过edges互连从而创建数据映射

1.展示节点

节点的渲染是通过给VueFlow组件的nodes参数传入一个数组实现的

<script setup>
import { ref, onMounted } from 'vue'
import { VueFlow, Panel } from '@vue-flow/core'

const nodes = ref([
  {
    id: '1',
    position: { x: 50, y: 50 },
    data: { label: 'Node 1', },
  }
]);

function addNode() {
  const id = Date.now().toString()
  
  nodes.value.push({
    id,
    position: { x: 150, y: 50 },
    data: { label: `Node ${id}`, },
  })
}
</script>

<template>
  <VueFlow :nodes="nodes">
    <Panel>
      <button type="button" @click="addNode">Add a node</button>
    </Panel>
  </VueFlow>
</template>

2.节点的增删

对于节点的增加和删除,我们可以通过直接改变nodes参数来实现,也可以使用 useVueFlow 提供的方法addNodesremoveNodes直接改变组件内部的状态实现

3.节点的更新

节点的更新同样可以使用改变nodes参数来实现,也可以使用useVueFlow得到的实例instance上的updateNodeData,传入对应组件的id和数据对象来更新;

instance.updateNode(nodeId, { selectable: false, draggable: false })

通过对实例的findNode方法拿到的节点实例直接修改组件state同样能够起到更新节点的效果

const node = instance.findNode(nodeId) 
node.data = { ...node.data, hello: 'world', }

4.节点的类型

节点的类型通过nodes数组中对应节点项的type属性确定

默认节点(type:'default')

Vue3 流程图组件库 :Vue Flow_css

input节点(type:'input')

Vue3 流程图组件库 :Vue Flow_连线_02

output节点(type:'output')

Vue3 流程图组件库 :Vue Flow_连线_03

自定义节点(type:'custom', type:'special',...)

除了默认的节点类型,用户也可以创建自定义的节点类型

模板插槽模式
<script setup>
import { ref } from 'vue'
import { VueFlow } from '@vue-flow/core'

import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'

export const nodes = ref([
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `custom`
    type: 'custom',
    position: { x: 50, y: 50 },
  },
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `special`
    type: 'special',
    position: { x: 150, y: 50 },
  }
])
</script>

<template>
  <VueFlow :nodes="nodes">
    <template #node-custom="customNodeProps">
      <CustomNode v-bind="customNodeProps" />
    </template>
    
    <template #node-special="specialNodeProps">
      <SpecialNode v-bind="specialNodeProps" />
    </template>
  </VueFlow>
</template>
<script setup>
import { ref } from 'vue'
import { VueFlow } from '@vue-flow/core'

import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'

export const nodes = ref([
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `custom`
    type: 'custom',
    position: { x: 50, y: 50 },
  },
  {
    id: '1',
    data: { label: 'Node 1' },
    // this will create the node-type `special`
    type: 'special',
    position: { x: 150, y: 50 },
  }
])
</script>

<template>
  <VueFlow :nodes="nodes">
    <template #node-custom="customNodeProps">
      <CustomNode v-bind="customNodeProps" />
    </template>
    
    <template #node-special="specialNodeProps">
      <SpecialNode v-bind="specialNodeProps" />
    </template>
  </VueFlow>
</template>

在配置了自定义组件后,VueFlow会将节点类型字段和插槽名字进行动态匹配,从而正确渲染。

Node-types对象模式

直接将引入的组件对象通过VueFlow的nodeTypes参数传入,需要注意的是要去除组件对象的响应式

<script setup>
import { markRaw } from 'vue'
import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'

const nodeTypes = {
  custom: markRaw(CustomNode),
  special: markRaw(SpecialNode),
}

const nodes = ref([
  {
    id: '1',
    data: { label: 'Node 1' },
    type: 'custom',
  },
  {
    id: '1',
    data: { label: 'Node 1' },
    type: 'special',
  }
])
</script>

<template>
  <VueFlow :nodes="nodes" :nodeTypes="nodeTypes" />
</template>

节点事件

参考:Nodes | Vue Flow

Edges

Edges就是节点之间的连线部分,每一条连线都是从一个handle到另一个handle,其拥有独立的id;

展示Edges

Edges的渲染是通过给VueFlow组件的edges参数传入一个数组实现的,需要配合nodes一起确定节点之间的连线关系;

<script setup>
import { ref, onMounted } from 'vue'
import { VueFlow } from '@vue-flow/core'

const nodes = ref([
  {
    id: '1',
    position: { x: 50, y: 50 },
    data: { label: 'Node 1', },
  },
  {
    id: '2',
    position: { x: 50, y: 250 },
    data: { label: 'Node 2', },
  }
]);

const edges = ref([
  {
    id: 'e1->2',
    source: '1',
    target: '2',
  }
]);
</script>

<template>
  <VueFlow :nodes="nodes" :edges="edges" />
</template>

增删和更新Edges

和节点的类似,可以通过直接改变edges传参实现,同时useVueFlow也提供了对Edges的操作方法[addEdges],(vueflow.dev/typedocs/in…) removeEdges

Edges的更新

同样和节点类型类似,可以通过useVueFlow拿到实例,使用实例的updateEdgeData方法进行更新,也可以使用findEdge拿到的edge直接修改对应的state进行更新

instance.updateEdgeData(edgeId, { hello: 'mona' }) 
edge.data = { ...edge.data, hello: 'world', }

Edges类型

默认连线(type:'default')

Vue3 流程图组件库 :Vue Flow_Vue_04

阶梯连线(type:'step')

Vue3 流程图组件库 :Vue Flow_Vue_05

直线连接(type:'straight')

Vue3 流程图组件库 :Vue Flow_css_06

自定义连接

用法和自定义节点类似,只是插槽名变为edge-开头,参数名由nodeTypes变为edgeTypes

edge事件

参考:Edges | Vue Flow

Handles

节点边缘上的小圆圈,使用拖拽的方式进行节点之间的连接

使用Handle

Handle是以组件的方式在节点中引入的

<script setup>
import { Handle } from '@vue-flow/core'
  
defineProps(['id', 'sourcePosition', 'targetPosition', 'data'])
</script>

<template>
  <Handle type="source" :position="sourcePosition" />
  
  <span>{{ data.label }}</span>
  
  <Handle type="target" :position="targetPosition" />
</template>

Handle 位置

可以通过Handle组件的position参数来调整其位置

<Handle type="source" :position="Position.Right" /> 
<Handle type="target" :position="Position.Left" />

多个Handle使用时需要注意组件需要有唯一id

<Handle id="source-a" type="source" :position="Position.Right" /> <Handle id="source-b" type="source" :position="Position.Right" />

多个Handle在同一侧时需要手动调整位置防止重叠

<Handle id="source-a" type="source" :position="Position.Right" style="top: 10px" /> 
<Handle id="source-b" type="source" :position="Position.Right" style="bottom: 10px; top: auto;" />

Handle的隐藏

需要使用样式opacity,不能使用v-if和v-show

<Handle type="source" :position="Position.Right" style="opacity: 0" />

是否限制连接可以使用Handle组件的connectable参数,传入一个布尔值

<Handle type="source" :position="Position.Right" :connectable="handleConnectable" />

连接模式

<VueFlow :connection-mode="ConnectionMode.Strict" />

配置了ConnectionMode.Strict后只允许在相同类型的Handle之间进行连接

动态位置

在需要动态处理Handle的位置时,需要调用updateNodeInternals方法传入需要更新的节点id数组去应用,防止边缘未对其的情况出现。

import { useVueFlow } from '@vue-flow/core'

const { updateNodeInternals } = useVueFlow()

const onSomeEvent = () => {
  updateNodeInternals(['1'])
}

Composables

Vue Flow提供了一些用于获取流程图及其内部组件相关数据的API,可以参考文档 Composables | Vue Flow

Controlled Flow

Vue Flow同样提供了一些API用于对流程图的更新过程进行手动控制并且监听对应事件 受控流量 |Vue 流程 (vueflow.dev)

来看一下官方文档Demo

Layouting | Vue Flow 这个demo较全的使用到了Vue Flow中的一些基本用法:

Vue3 流程图组件库 :Vue Flow_Vue_07

1.主流程:App.vue:

import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import { nextTick, ref } from "vue";
import { Panel, VueFlow, useVueFlow } from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import Icon from "./icon.vue";
import ProcessNode from "./processNode.vue";
import AnimationEdge from "./animationEdge.vue";
import { initialEdges, initialNodes } from "./initialElements";
import { useRunProcess } from "./useRunProcess";
import { useShuffle } from "./useShuffle";
import { useLayout } from "./useLayout";

// 节点的初始化数据
const nodes = ref(initialNodes);

// 节点的连接关系
const edges = ref(initialEdges);

// 打乱节点之间的连接关系
const shuffle = useShuffle();

// useLayout 处理节点布局对齐等
const { graph, layout, previousDirection } = useLayout();

const { fitView } = useVueFlow();

// 将节点和连线随机化
async function shuffleGraph() {
  await stop();

  reset(nodes.value);

  edges.value = shuffle(nodes.value);

  nextTick(() => {
    layoutGraph(previousDirection.value);
  });
}

// 进行重新排版
async function layoutGraph(direction) {
  await stop();

  reset(nodes.value);

  nodes.value = layout(nodes.value, edges.value, direction);

  nextTick(() => {
    fitView();
    run(nodes.value);
  });
}
<template>
  <div class="layout-flow">
    <VueFlow :nodes="nodes" :edges="edges" @nodes-initialized="layoutGraph('LR')">
    <!--    以插槽方式传入节点和连线    -->
      <template #node-process="props">
        <ProcessNode 
        :data="props.data" 
        :source-position="props.sourcePosition" 
        :target-position="props.targetPosition" />
      </template>

      <template #edge-animation="edgeProps">
        <AnimationEdge
          :id="edgeProps.id"
          :source="edgeProps.source"
          :target="edgeProps.target"
          :source-x="edgeProps.sourceX"
          :source-y="edgeProps.sourceY"
          :targetX="edgeProps.targetX"
          :targetY="edgeProps.targetY"
          :source-position="edgeProps.sourcePosition"
          :target-position="edgeProps.targetPosition"
        />
      </template>

      <Background />

      <Panel class="process-panel" position="top-right">
        <div class="layout-panel">
          <button v-if="isRunning" class="stop-btn" title="stop" @click="stop">
            <Icon name="stop" />
            <span class="spinner" />
          </button>
          <button v-else title="start" @click="run(nodes)">
            <Icon name="play" />
          </button>

          <button title="set horizontal layout" @click="layoutGraph('LR')">
            <Icon name="horizontal" />
          </button>

          <button title="set vertical layout" @click="layoutGraph('TB')">
            <Icon name="vertical" />
          </button>

          <button title="shuffle graph" @click="shuffleGraph">
            <Icon name="shuffle" />
          </button>
        </div>

        <div class="checkbox-panel">
          <label>Cancel on error</label>
          <input v-model="cancelOnError" type="checkbox" />
        </div>
      </Panel>
    </VueFlow>
  </div>
</template>
.layout-flow {
  background-color: #1a192b;
  height: 100%;
  width: 100%;
}

.process-panel,
.layout-panel {
  display: flex;
  gap: 10px;
}

.process-panel {
  background-color: #2d3748;
  padding: 10px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
}

.process-panel button {
  border: none;
  cursor: pointer;
  background-color: #4a5568;
  border-radius: 8px;
  color: white;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

.process-panel button {
  font-size: 16px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.checkbox-panel {
  display: flex;
  align-items: center;
  gap: 10px;
}

.process-panel button:hover,
.layout-panel button:hover {
  background-color: #2563eb;
  transition: background-color 0.2s;
}

.process-panel label {
  color: white;
  font-size: 12px;
}

.stop-btn svg {
  display: none;
}

.stop-btn:hover svg {
  display: block;
}

.stop-btn:hover .spinner {
  display: none;
}

.spinner {
  border: 3px solid #f3f3f3;
  border-top: 3px solid #2563eb;
  border-radius: 50%;
  width: 10px;
  height: 10px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

2.useShuffle.js

该文件提供的方法主要是用来随机打乱节点以及连线的关系

// 打乱数组的顺序
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// 根据节点数组生成一个可能的节点之间的映射关系
function generatePossibleEdges(nodes) {
  const possibleEdges = [];

  for (const sourceNode of nodes) {
    for (const targetNode of nodes) {
      if (sourceNode.id !== targetNode.id) {
        const edgeId = `e${sourceNode.id}-${targetNode.id}`;
        possibleEdges.push({
          id: edgeId,
          source: sourceNode.id,
          target: targetNode.id,
          type: "animation",
          animated: true
        });
      }
    }
  }

  return possibleEdges;
}

// 返回新的节点连接关系;
export function useShuffle() {
  return nodes => {
    const possibleEdges = generatePossibleEdges(nodes);
    shuffleArray(possibleEdges);

    const usedNodes = new Set();
    const newEdges = [];

    for (const edge of possibleEdges) {
      if (
        !usedNodes.has(edge.target) &&
        (usedNodes.size === 0 || usedNodes.has(edge.source))
      ) {
        newEdges.push(edge);
        usedNodes.add(edge.source);
        usedNodes.add(edge.target);
      }
    }

    return newEdges;
  };
}

3.useLayout.js

使用dagre对节点进行排版,返回排版后的图数据;

import dagre from "dagre";
import { ref } from "vue";
import { Position, useVueFlow } from "@vue-flow/core";

export function useLayout() {
  const { findNode } = useVueFlow();

  const graph = ref(new dagre.graphlib.Graph());

  const previousDirection = ref("LR");

  function layout(nodes, edges, direction) {
    const dagreGraph = new dagre.graphlib.Graph();

    graph.value = dagreGraph;

    // 设置默认的边标签
    dagreGraph.setDefaultEdgeLabel(() => ({}));

    const isHorizontal = direction === "LR";

    // 设置图布局
    dagreGraph.setGraph({ rankdir: direction });

    previousDirection.value = direction;

    for (const node of nodes) {
      // 查找到节点的信息
      const graphNode = findNode(node.id);
      // 设置节点
      dagreGraph.setNode(node.id, {
        width: graphNode.dimensions.width || 150,
        height: graphNode.dimensions.height || 50
      });
    }

    // 设置边
    for (const edge of edges) {
      dagreGraph.setEdge(edge.source, edge.target);
    }

    // 排版
    dagre.layout(dagreGraph);

    // 排版结束后返回新的节点状态
    return nodes.map(node => {
      const nodeWithPosition = dagreGraph.node(node.id);

      return {
        ...node,
        targetPosition: isHorizontal ? Position.Left : Position.Top,
        sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
        position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
      };
    });
  }

  return { graph, layout, previousDirection };
}

4.useRunProcess.js

用于模拟流程运行过程中的各种状态

import { ref, toRef, toValue } from "vue";
import { useVueFlow } from "@vue-flow/core";

export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
  const { updateNodeData, getConnectedEdges } = useVueFlow();

  const graph = toRef(() => toValue(dagreGraph));

  // 是否正在运行
  const isRunning = ref(false);

  //已执行的节点
  const executedNodes = new Set();

  // 当前正在执行的节点
  const runningTasks = new Map();

  // 即将执行的节点
  const upcomingTasks = new Set();

  async function runNode(node, isStart = false) {
    if (executedNodes.has(node.id)) {
      return;
    }

    // 加入到即将执行的节点
    upcomingTasks.add(node.id);

    // 过滤出指向当前节点的连线
    const incomers = getConnectedEdges(node.id).filter(
      connection => connection.target === node.id
    );

    // 等待进入动画全部执行完成
    await Promise.all(
      incomers.map(incomer => until(() => !incomer.data.isAnimating))
    );

    // 清空
    upcomingTasks.clear();

    if (!isRunning.value) {
      return;
    }

    // 节点加入到已经执行的节点
    executedNodes.add(node.id);

    // 更新节点的状态
    updateNodeData(node.id, {
      isRunning: true,
      isFinished: false,
      hasError: false,
      isCancelled: false
    });

    const delay = Math.floor(Math.random() * 2000) + 1000;

    return new Promise(resolve => {
      const timeout = setTimeout(
        async () => {
          // 获取当前节点的所有后续子节点
          const children = graph.value.successors(node.id);

          // 随机抛出错误
          const willThrowError = Math.random() < 0.15;

          // 模拟错误的情况
          if (!isStart && willThrowError) {
            updateNodeData(node.id, { isRunning: false, hasError: true });

            if (toValue(cancelOnError)) {
              // 跳过错误节点后续子节点的处理
              await skipDescendants(node.id);
              // 删除节点对应正在执行的任务
              runningTasks.delete(node.id);

              // @ts-expect-error
              resolve();
              return;
            }
          }

          // 更新节点的状态未结束
          updateNodeData(node.id, { isRunning: false, isFinished: true });

          runningTasks.delete(node.id);

          // 递归执行后续节点
          if (children.length > 0) {
            await Promise.all(children.map(id => runNode({ id })));
          }
          resolve();
        },
        isStart ? 0 : delay
      );
      // 将当前任务加入到运行任务
      runningTasks.set(node.id, timeout);
    });
  }

  // 从起始节点开始执行的情况
  async function run(nodes) {
    if (isRunning.value) {
      return;
    }

    reset(nodes);

    isRunning.value = true;

    // 过滤出起始节点
    const startingNodes = nodes.filter(
      node => graph.value.predecessors(node.id)?.length === 0
    );

    // 调用runNode从起始节点执行
    await Promise.all(startingNodes.map(node => runNode(node, true)));

    clear();
  }

  //重置
  function reset(nodes) {
    clear();

    for (const node of nodes) {
      updateNodeData(node.id, {
        isRunning: false,
        isFinished: false,
        hasError: false,
        isSkipped: false,
        isCancelled: false
      });
    }
  }

  async function skipDescendants(nodeId) {
    const children = graph.value.successors(nodeId);

    for (const child of children) {
      updateNodeData(child, { isRunning: false, isSkipped: true });
      await skipDescendants(child);
    }
  }

  // 暂停运行
  async function stop() {
    isRunning.value = false;

    for (const nodeId of upcomingTasks) {
      clearTimeout(runningTasks.get(nodeId));
      runningTasks.delete(nodeId);
      updateNodeData(nodeId, {
        isRunning: false,
        isFinished: false,
        hasError: false,
        isSkipped: false,
        isCancelled: true
      });
      await skipDescendants(nodeId);
    }

    for (const [nodeId, task] of runningTasks) {
      clearTimeout(task);
      runningTasks.delete(nodeId);
      updateNodeData(nodeId, {
        isRunning: false,
        isFinished: false,
        hasError: false,
        isSkipped: false,
        isCancelled: true
      });
      await skipDescendants(nodeId);
    }

    executedNodes.clear();
    upcomingTasks.clear();
  }

  function clear() {
    isRunning.value = false;
    executedNodes.clear();
    runningTasks.clear();
  }

  return { run, stop, reset, isRunning };
}

// 等待直到condition为true
async function until(condition) {
  return new Promise(resolve => {
    const interval = setInterval(() => {
      if (condition()) {
        clearInterval(interval);
        resolve();
      }
    }, 100);
  });
}

5.processNode.js

流程图节点组件,根据节点状态显示不同的样式

import { computed, toRef } from 'vue'
import { Handle, useHandleConnections } from '@vue-flow/core'

const props = defineProps({
  data: {
    type: Object,
    required: true,
  },
  sourcePosition: {
    type: String,
  },
  targetPosition: {
    type: String,
  },
})

const sourceConnections = useHandleConnections({
  type: 'target',
})

const targetConnections = useHandleConnections({
  type: 'source',
})

// 判断是发送节点还是接收节点
const isSender = toRef(() => sourceConnections.value.length <= 0)

const isReceiver = toRef(() => targetConnections.value.length <= 0)

// 根据节点的数据参数来确定节点的背景颜色
const bgColor = computed(() => {
  if (isSender.value) {
    return '#2563eb'
  }

  if (props.data.hasError) {
    return '#f87171'
  }

  if (props.data.isFinished) {
    return '#42B983'
  }

  if (props.data.isCancelled) {
    return '#fbbf24'
  }

  return '#4b5563'
})

const processLabel = computed(() => {
  if (props.data.hasError) {
    return '❌'
  }

  if (props.data.isSkipped) {
    return '

标签:Vue,const,Flow,value,id,vue,Vue3,import,节点
From: https://blog.51cto.com/u_16802720/12101013

相关文章

  • 【Vue】修改登录页的用户密码输入框
    一、需求场景当前输入框样式采用element-ui默认表单项样式 需要改造成UI设计的效果 二、实现方式我找了公司之前的项目,发现他们用HTML原生的input标签实现的,需要给原生标签写样式那现在的这个项目是直接用el-input做的,我觉得再用之前项目的实现方式不太理想所以看看能......
  • 基于Python+Vue开发的蛋糕商城管理系统源码+开发文档
    项目简介该项目是基于Python+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力......
  • 基于Java+Springboot+Vue开发的体育用品商城管理系统源码+开发文档
    项目简介该项目是基于Java+Springboot+Vue开发的体育用品商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的体育用品商城管理系统项目,大学生可以在实践中学习和......
  • 基于Java+Springboot+Vue开发的体育场馆预约管理系统源码+开发文档
    项目简介该项目是基于Java+Springboot+Vue开发的体育场馆预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的体育场馆管理系统项目,大学生可以在实践中学习和提升......
  • 基于Java+Springboot+Vue开发的旅游景区管理系统源码+参考文章1.3万字
    项目简介该项目是基于Java+Springboot+Vue开发的旅游景区管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的旅游景区管理系统项目,大学生可以在实践中学习和提升自己......
  • 基于Python+Vue开发的旅游景区管理系统源码+开发文档1.3万字
    项目简介该项目是基于Python+Vue开发的旅游景区管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的旅游景区管理系统项目,大学生可以在实践中学习和提升自己的能力......
  • 基于Java+Springboot+Vue开发的鲜花商城管理系统源码+参考文章1.3万字
    项目简介该项目是基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己......
  • 基于Python+Vue开发的医院门诊预约挂号系统源码+开发文档
    项目简介该项目是基于Python+Vue开发的医院门诊预约挂号系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的医院门诊预约挂号管理系统项目,大学生可以在实践中学习和......
  • 基于Python+Vue开发的音乐推荐管理系统源码+开发文档
    项目简介该项目是基于Python+Vue开发的音乐推荐管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的音乐推荐管理系统项目,大学生可以在实践中学习和提升自己的能力......
  • 基于Python+Vue开发的电影订票管理系统源码+开发文档
    项目简介该项目是基于Python+Vue开发的电影订票管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的电影订票管理系统项目,大学生可以在实践中学习和提升自己的能力......