请问下:是否有这样的前端组件呢?

请问下:Vue3或前端是否有这样的三方组件呢?求推荐:
image.png

阅读 1k
4 个回答

那么简单,自己写一个就行了。以下是我实现的效果:

代码如下:

<template>
  <div class="gantt-container">
    <div class="gantt-content" :style="ganttContentStyle">
      <!-- Product Development 阶段 -->
      <div class="phase-section">
        <div class="phase-title">Product Development</div>

        <!-- Design 子阶段 -->
        <div class="sub-phase">
          <div class="sub-phase-title">Design</div>
          <div class="tasks-row">
            <template
              v-for="(task, index) in productDevelopmentTasks"
              :key="index"
            >
              <div class="task-card">
                <div
                  class="task-header"
                  :class="{ 'task-header-purple': task.isApproval }"
                >
                  {{ task.name }}
                </div>
                <div class="task-details">
                  {{ task.startDate }} {{ task.duration }} {{ task.endDate }}
                </div>
              </div>
              <div
                v-if="index < productDevelopmentTasks.length - 1"
                class="arrow"
              ></div>
            </template>
          </div>
        </div>
      </div>

      <!-- Development 阶段 -->
      <div class="phase-section">
        <div class="phase-title">Development</div>

        <!-- Environment Setup 子阶段 -->
        <div class="sub-phase">
          <div class="sub-phase-title">Environment Setup</div>
          <div class="tasks-row">
            <template
              v-for="(task, index) in environmentSetupTasks"
              :key="index"
            >
              <div class="task-card">
                <div class="task-header">
                  {{ task.name }}
                </div>
                <div class="task-details">
                  {{ task.startDate }} {{ task.duration }} {{ task.endDate }}
                </div>
              </div>
              <div
                v-if="index < environmentSetupTasks.length - 1"
                class="arrow"
              ></div>
            </template>
          </div>
        </div>

        <!-- Frontend Development 子阶段 -->
        <div class="sub-phase">
          <div class="sub-phase-title">Frontend Development</div>
          <div class="tasks-row">
            <template v-for="(task, index) in frontendTasks" :key="index">
              <div class="task-card">
                <div class="task-header">
                  {{ task.name }}
                </div>
                <div class="task-details">
                  {{ task.startDate }} {{ task.duration }} {{ task.endDate }}
                </div>
              </div>
              <div v-if="index < frontendTasks.length - 1" class="arrow"></div>
            </template>
          </div>
        </div>

        <!-- Backend Development 子阶段 -->
        <div class="sub-phase">
          <div class="sub-phase-title">Backend Development</div>
          <div class="tasks-row">
            <template v-for="(task, index) in backendTasks" :key="index">
              <div class="task-card">
                <div class="task-header">
                  {{ task.name }}
                </div>
                <div class="task-details">
                  {{ task.startDate }} {{ task.duration }} {{ task.endDate }}
                </div>
              </div>
              <div v-if="index < backendTasks.length - 1" class="arrow"></div>
            </template>
          </div>
        </div>

        <!-- Integration 子阶段 -->
        <div class="sub-phase">
          <div class="sub-phase-title">Integration</div>
          <div class="tasks-row">
            <template v-for="(task, index) in integrationTasks" :key="index">
              <div class="task-card">
                <div class="task-header">
                  {{ task.name }}
                </div>
                <div class="task-details">
                  {{ task.startDate }} {{ task.duration }} {{ task.endDate }}
                </div>
              </div>
              <div
                v-if="index < integrationTasks.length - 1"
                class="arrow"
              ></div>
            </template>
          </div>
        </div>
      </div>
    </div>

    <!-- 缩放控制 -->
    <div class="zoom-control">
      <button class="zoom-btn" :disabled="zoomLevel <= 50" @click="zoomOut">
        -
      </button>
      <span class="zoom-value">{{ zoomLevel }}%</span>
      <button class="zoom-btn" :disabled="zoomLevel >= 150" @click="zoomIn">
        +
      </button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from "vue";

interface Task {
  name: string;
  startDate: string;
  duration: number;
  endDate: string;
  isApproval?: boolean;
}

export default defineComponent({
  name: "Test",
  setup() {
    const zoomLevel = ref(60);

    const zoomIn = () => {
      if (zoomLevel.value < 150) {
        zoomLevel.value += 10;
      }
    };

    const zoomOut = () => {
      if (zoomLevel.value > 50) {
        zoomLevel.value -= 10;
      }
    };

    const ganttContentStyle = computed(() => ({
      transform: `scale(${zoomLevel.value / 100})`,
      transformOrigin: "top left"
    }));

    // Product Development 阶段的任务
    const productDevelopmentTasks: Task[] = [
      {
        name: "Market Research",
        startDate: "01-01-2026",
        duration: 2,
        endDate: "03-01-2026"
      },
      {
        name: "Competitive Analysis",
        startDate: "03-01-2026",
        duration: 3,
        endDate: "06-01-2026"
      },
      {
        name: "Define User Personas",
        startDate: "06-01-2026",
        duration: 2,
        endDate: "08-01-2026"
      },
      {
        name: "User Interviews",
        startDate: "08-01-2026",
        duration: 3,
        endDate: "11-01-2026"
      },
      {
        name: "Create UX Wireframes",
        startDate: "11-01-2026",
        duration: 3,
        endDate: "14-01-2026"
      },
      {
        name: "UI Visual Design",
        startDate: "14-01-2026",
        duration: 4,
        endDate: "18-01-2026"
      },
      {
        name: "Stakeholder Review",
        startDate: "18-01-2026",
        duration: 2,
        endDate: "20-01-2026"
      },
      {
        name: "Design Approval",
        startDate: "20-01-2026",
        duration: 1,
        endDate: "21-01-2026",
        isApproval: true
      }
    ];

    // Environment Setup 子阶段的任务
    const environmentSetupTasks: Task[] = [
      {
        name: "Setup Dev Environment",
        startDate: "21-01-2026",
        duration: 1,
        endDate: "22-01-2026"
      },
      {
        name: "Install Dependencies",
        startDate: "22-01-2026",
        duration: 2,
        endDate: "24-01-2026"
      }
    ];

    // Frontend Development 子阶段的任务
    const frontendTasks: Task[] = [
      {
        name: "Basic Layout & Structure",
        startDate: "24-01-2026",
        duration: 2,
        endDate: "26-01-2026"
      },
      {
        name: "Implement Core Features",
        startDate: "26-01-2026",
        duration: 2,
        endDate: "28-01-2026"
      },
      {
        name: "Implement Core Features",
        startDate: "28-01-2026",
        duration: 2,
        endDate: "30-01-2026"
      },
      {
        name: "UI Integration",
        startDate: "30-01-2026",
        duration: 2,
        endDate: "01-02-2026"
      }
    ];

    // Backend Development 子阶段的任务
    const backendTasks: Task[] = [
      {
        name: "API Design",
        startDate: "01-02-2026",
        duration: 2,
        endDate: "03-02-2026"
      },
      {
        name: "Database Setup",
        startDate: "03-02-2026",
        duration: 2,
        endDate: "05-02-2026"
      },
      {
        name: "Backend Implementation",
        startDate: "05-02-2026",
        duration: 3,
        endDate: "08-02-2026"
      }
    ];

    // Integration 子阶段的任务
    const integrationTasks: Task[] = [
      {
        name: "Frontend-Backend Integration",
        startDate: "08-02-2026",
        duration: 2,
        endDate: "10-02-2026"
      }
    ];

    return {
      zoomLevel,
      zoomIn,
      zoomOut,
      ganttContentStyle,
      productDevelopmentTasks,
      environmentSetupTasks,
      frontendTasks,
      backendTasks,
      integrationTasks
    };
  }
});
</script>

<style lang="less" scoped>
.gantt-container {
  width: 100%;
  height: 100vh;
  background-color: #ffffff;
  position: relative;
  overflow: auto;
  padding: 40px;
}

.gantt-content {
  background-color: #e8f5e9;
  min-width: 2000px;
  padding: 60px 40px;
  position: relative;
}

.phase-section {
  margin-bottom: 60px;

  &:last-child {
    margin-bottom: 0;
  }
}

.phase-title {
  color: #4caf50;
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 30px;
  padding-left: 10px;
  position: relative;

  &::before {
    content: "-";
    position: absolute;
    left: 0;
  }
}

.sub-phase {
  margin-bottom: 40px;
  margin-left: 30px;

  &:last-child {
    margin-bottom: 0;
  }
}

.sub-phase-title {
  color: #4caf50;
  font-size: 16px;
  font-weight: 500;
  margin-bottom: 20px;
  padding-left: 10px;
  position: relative;

  &::before {
    content: "-";
    position: absolute;
    left: 0;
  }
}

.tasks-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.task-card {
  display: flex;
  flex-direction: column;
  min-width: 180px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.task-header {
  background-color: #2196f3;
  color: #ffffff;
  padding: 12px 16px;
  font-size: 14px;
  font-weight: 500;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;

  &.task-header-purple {
    background-color: #9c27b0;
  }
}

.task-details {
  background-color: #f5f5f5;
  color: #000000;
  padding: 10px 16px;
  font-size: 12px;
  text-align: center;
  white-space: nowrap;
}

.arrow {
  width: 30px;
  height: 2px;
  background-color: #9e9e9e;
  position: relative;
  margin: 0 5px;

  &::after {
    content: "";
    position: absolute;
    right: -6px;
    top: -4px;
    width: 0;
    height: 0;
    border-left: 8px solid #9e9e9e;
    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
  }
}

.zoom-control {
  position: fixed;
  right: 30px;
  bottom: 30px;
  background-color: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  display: flex;
  align-items: center;
  gap: 0;
  padding: 4px;
  z-index: 1000;
}

.zoom-btn {
  width: 32px;
  height: 32px;
  border: none;
  background-color: transparent;
  color: #333;
  font-size: 18px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s;

  &:hover:not(:disabled) {
    background-color: #f0f0f0;
  }

  &:disabled {
    opacity: 0.4;
    cursor: not-allowed;
  }
}

.zoom-value {
  min-width: 50px;
  text-align: center;
  font-size: 14px;
  color: #333;
  padding: 0 8px;
}
</style>

流程型甘特图 + 节点卡片 + 连线 + 分组泳道(Swimlane)

我理解应该有两种方案,一种就是完全的图编辑,类似与 antv https://antv.antgroup.com/

image.png
image.png

另一种就是固定结构的简易版。完全基于 DOM 组件搞出来(类似阿里云的云效流水线编排吧)

image.png

一般来说为了编辑方便,我会选择牺牲自由度,选择方案二。

新手上路,请多包涵

vue-okr-tree 页面整体还是得自己改

Vue3 里推荐这俩:Vue Flow(能做这种节点流程图,自定义性强)、Element Plus 的 Steps 组件(基础但够用)。我之前用 Vue Flow 搭过类似的,改改样式就差不多~

推荐问题