<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import dayjs from "../../dayjs";
import { isBetweenPlugin, isoWeekPlugin, quarterOfYearPlugin } from "../../dayjs-plugins";
import { useNamespace, useLocale } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import { YhTooltip } from "../../tooltip";
import { YhInput } from "../../input";
import { YhRadioGroup, YhRadioButton } from "../../radio";
dayjs.extend(isBetweenPlugin);
dayjs.extend(isoWeekPlugin);
dayjs.extend(quarterOfYearPlugin);
defineOptions({ name: "YhGanttChart" });
const props = defineProps({
  data: { type: Array, required: true, default: () => [] },
  columns: { type: Array, required: false, default: () => [] },
  startDate: { type: [String, Number, Date], required: false },
  endDate: { type: [String, Number, Date], required: false },
  viewMode: { type: String, required: false, default: "day" },
  showDependencies: { type: Boolean, required: false, default: true },
  draggable: { type: Boolean, required: false, default: false },
  progressDraggable: { type: Boolean, required: false, default: false },
  autoSchedule: { type: Boolean, required: false, default: true },
  virtual: { type: Boolean, required: false, default: false },
  showResourceLoad: { type: Boolean, required: false, default: true },
  rowHeight: { type: Number, required: false, default: 40 },
  headerHeight: { type: Number, required: false, default: 60 },
  bordered: { type: Boolean, required: false, default: true },
  loading: { type: Boolean, required: false, default: false },
  teleported: { type: Boolean, required: false, default: true },
  themeOverrides: { type: Object, required: false }
});
const emit = defineEmits(["update:data", "update:viewMode", "task-click", "task-dblclick", "task-drag-end", "progress-drag-end", "dependency-click"]);
const ns = useNamespace("gantt-chart");
const { t } = useLocale();
const { themeStyle } = useComponentTheme(
  "gantt",
  computed(() => props.themeOverrides)
);
const resolvedColumns = computed(() => {
  const isDefaultNameColumn = props.columns.length === 1 && props.columns[0]?.prop === "name" && props.columns[0]?.width === 200;
  return isDefaultNameColumn ? [{ ...props.columns[0], label: t("ganttchart.taskName") }] : props.columns;
});
const ganttSearchPlaceholder = computed(() => t("ganttchart.searchPlaceholder"));
const ganttZoomText = computed(() => t("ganttchart.zoom"));
const ganttMilestoneText = computed(() => t("ganttchart.milestone"));
const ganttRef = ref(null);
const timelineBodyRef = ref(null);
const sidebarBodyRef = ref(null);
const sidebarHeaderRef = ref(null);
const PIXELS_PER_DAY = ref(40);
const internalViewModeState = ref(props.viewMode || "day");
const internalViewMode = computed({
  get: () => internalViewModeState.value,
  set: (val) => {
    internalViewModeState.value = val;
    emit("update:viewMode", val);
  }
});
watch(
  () => props.viewMode,
  (val) => {
    if (val) internalViewModeState.value = val;
  }
);
const minPixelsPerDay = computed(() => {
  if (internalViewMode.value === "day") return 20;
  if (internalViewMode.value === "week") return 6;
  if (internalViewMode.value === "month") return 2;
  return 0.5;
});
watch(
  internalViewMode,
  (val) => {
    if (val === "day") PIXELS_PER_DAY.value = Math.max(PIXELS_PER_DAY.value, 40);
    else if (val === "week") PIXELS_PER_DAY.value = Math.max(PIXELS_PER_DAY.value, 10);
    else if (val === "month") PIXELS_PER_DAY.value = Math.max(PIXELS_PER_DAY.value, 4);
    else if (val === "year") PIXELS_PER_DAY.value = Math.max(PIXELS_PER_DAY.value, 1);
  },
  { immediate: true }
);
const minMaxDates = computed(() => {
  let minDate = dayjs("2099-12-31");
  let maxDate = dayjs("1970-01-01");
  if (props.data.length === 0) {
    minDate = dayjs().subtract(7, "day");
    maxDate = dayjs().add(23, "day");
  } else {
    const checkTasks = (tasks) => {
      tasks.forEach((task) => {
        const start = dayjs(task.startDate);
        const end = dayjs(task.endDate);
        if (start.isBefore(minDate)) minDate = start;
        if (end.isAfter(maxDate)) maxDate = end;
        if (task.children) checkTasks(task.children);
      });
    };
    checkTasks(props.data);
    minDate = minDate.subtract(7, "day");
    maxDate = maxDate.add(7, "day");
  }
  return {
    start: props.startDate ? dayjs(props.startDate) : minDate,
    end: props.endDate ? dayjs(props.endDate) : maxDate
  };
});
const timelineStart = computed(
  () => minMaxDates.value.start.startOf(internalViewMode.value === "day" ? "day" : internalViewMode.value)
);
const timelineEnd = computed(
  () => minMaxDates.value.end.endOf(internalViewMode.value === "day" ? "day" : internalViewMode.value)
);
const expandedTasks = ref(/* @__PURE__ */ new Set());
const searchKeyword = ref("");
const toggleExpand = (task) => {
  if (expandedTasks.value.has(task.id)) {
    expandedTasks.value.delete(task.id);
  } else {
    expandedTasks.value.add(task.id);
  }
};
const getFlattenedTasks = (tasks, parentId = null, level = 0, ancestorHasNext = []) => {
  let result = [];
  tasks.forEach((task, index) => {
    const matchesSearch = !searchKeyword.value || task.name.toLowerCase().includes(searchKeyword.value.toLowerCase());
    let start = dayjs(task.startDate), end = dayjs(task.endDate);
    const isMilestone = start.isSame(end, "day");
    if (task.children?.length) {
      task.children.forEach((c) => {
        const cs = dayjs(c.startDate), ce = dayjs(c.endDate);
        if (cs.isBefore(start)) start = cs;
        if (ce.isAfter(end)) end = ce;
      });
    }
    const isLast = index === tasks.length - 1;
    const flatTask = {
      ...task,
      _original: task,
      // 保持对原始对象的引用
      _level: level,
      _parentId: parentId,
      _isLast: isLast,
      _ancestorHasNext: [...ancestorHasNext],
      _hasChildren: !!task.children?.length,
      _startDate: start.format("YYYY-MM-DD HH:mm:ss"),
      _endDate: end.format("YYYY-MM-DD HH:mm:ss"),
      _isMilestone: isMilestone,
      _matchesSearch: matchesSearch
    };
    result.push(flatTask);
    if (task.children?.length && (expandedTasks.value.has(task.id) || searchKeyword.value)) {
      result = result.concat(
        getFlattenedTasks(task.children, task.id, level + 1, [...ancestorHasNext, !isLast])
      );
    }
  });
  return result;
};
const visibleAllTasks = computed(() => {
  let list = getFlattenedTasks(props.data);
  if (searchKeyword.value) {
    return list.filter((t2) => t2._matchesSearch);
  }
  return list;
});
const scrollTop = ref(0);
const viewportHeight = ref(600);
const virtualState = computed(() => {
  if (!props.virtual) return { start: 0, end: visibleAllTasks.value.length, offsetTop: 0 };
  const start = Math.floor(scrollTop.value / props.rowHeight);
  const end = Math.min(
    start + Math.ceil(viewportHeight.value / props.rowHeight) + 2,
    visibleAllTasks.value.length
  );
  return { start, end, offsetTop: start * props.rowHeight };
});
const renderTasks = computed(
  () => visibleAllTasks.value.slice(virtualState.value.start, virtualState.value.end)
);
const totalHeight = computed(() => visibleAllTasks.value.length * props.rowHeight);
const resourceLoadMap = computed(() => {
  const loadMap = {};
  visibleAllTasks.value.forEach((t2) => {
    if (!t2.assignees) return;
    let current = dayjs(t2._startDate || t2.startDate);
    const end = dayjs(t2._endDate || t2.endDate);
    while (current.isBefore(end) || current.isSame(end, "day")) {
      const dateStr = current.format("YYYY-MM-DD");
      t2.assignees.forEach((resId) => {
        if (!loadMap[resId]) loadMap[resId] = {};
        loadMap[resId][dateStr] = (loadMap[resId][dateStr] || 0) + 1;
      });
      current = current.add(1, "day");
    }
  });
  return loadMap;
});
const totalDays = computed(
  () => Math.max(1, timelineEnd.value.diff(timelineStart.value, "day") + 1)
);
const timelineWidth = computed(() => totalDays.value * PIXELS_PER_DAY.value);
const topHeaders = computed(() => {
  const headers = [];
  let current = timelineStart.value.clone();
  while (current.isBefore(timelineEnd.value) || current.isSame(timelineEnd.value, "day")) {
    const nextCurrent = internalViewMode.value === "day" || internalViewMode.value === "week" ? current.add(1, "month").startOf("month") : current.add(1, "year").startOf("year");
    const actualEnd = nextCurrent.isAfter(timelineEnd.value) ? timelineEnd.value : nextCurrent;
    const days = actualEnd.diff(current, "day");
    if (days > 0) {
      headers.push({
        label: internalViewMode.value === "year" ? current.format("YYYY") : current.format("YYYY-MM"),
        width: days * PIXELS_PER_DAY.value
      });
    }
    current = nextCurrent;
  }
  return headers;
});
const bottomHeaders = computed(() => {
  const headers = [];
  let current = timelineStart.value.clone();
  while (current.isBefore(timelineEnd.value) || current.isSame(timelineEnd.value, "day")) {
    let label = "", days = 1;
    if (internalViewMode.value === "day") {
      label = current.format("DD");
      days = 1;
      current = current.add(1, "day");
    } else if (internalViewMode.value === "week") {
      label = `W${current.isoWeek()}`;
      days = 7;
      current = current.add(1, "week");
    } else if (internalViewMode.value === "month") {
      label = current.format("MMM");
      days = current.daysInMonth();
      current = current.add(1, "month");
    } else if (internalViewMode.value === "year") {
      label = `Q${current.quarter()}`;
      days = current.add(1, "quarter").diff(current, "day");
      current = current.add(1, "quarter");
    }
    headers.push({
      label,
      width: days * PIXELS_PER_DAY.value,
      isToday: current.subtract(1, "day").isSame(dayjs(), "day")
    });
  }
  return headers;
});
const getTaskPosition = (task, index) => {
  const start = dayjs(task.startDate);
  const end = dayjs(task.endDate);
  const isMilestone = task._isMilestone || start.isSame(end, "day");
  const daysFromStart = start.diff(timelineStart.value, "day", true);
  let duration = Math.max(0.1, end.diff(start, "day") + 1);
  let width = duration * PIXELS_PER_DAY.value;
  let left = daysFromStart * PIXELS_PER_DAY.value;
  if (isMilestone) width = 0;
  return { left, width, top: index * props.rowHeight, isMilestone };
};
const taskStyles = computed(() => {
  return renderTasks.value.map((task, index) => {
    const realIndex = virtualState.value.start + index;
    const pos = getTaskPosition(task, realIndex);
    let hasConflict = false;
    if (props.showResourceLoad && task.assignees) {
      let current = dayjs(task.startDate);
      const end = dayjs(task.endDate);
      while (current.isBefore(end) || current.isSame(end, "day")) {
        const ds = current.format("YYYY-MM-DD");
        if (task.assignees.some((rid) => (resourceLoadMap.value[rid]?.[ds] || 0) > 1)) {
          hasConflict = true;
          break;
        }
        current = current.add(1, "day");
      }
    }
    const isSummary = task._hasChildren;
    return {
      ...pos,
      task,
      index: realIndex,
      hasConflict,
      isSummary,
      style: {
        left: `${pos.isMilestone ? pos.left + PIXELS_PER_DAY.value / 2 - 8 : pos.left}px`,
        width: `${pos.isMilestone ? 16 : pos.width}px`,
        top: `${pos.top + (isSummary ? 12 : 4)}px`,
        height: `${pos.isMilestone ? 16 : isSummary ? 8 : props.rowHeight - 8}px`,
        backgroundColor: pos.isMilestone || isSummary ? "transparent" : hasConflict ? "var(--yh-color-danger)" : task.color,
        color: task.textColor,
        margin: pos.isMilestone ? "4px 0" : "0"
      }
    };
  });
});
const dependenciesLinks = computed(() => {
  if (!props.showDependencies) return [];
  const links = [];
  const idToPos = /* @__PURE__ */ new Map();
  taskStyles.value.forEach((t2) => idToPos.set(t2.task.id, t2));
  taskStyles.value.forEach((t2) => {
    if (!t2.task.dependencies) return;
    t2.task.dependencies.forEach((depId) => {
      const from = idToPos.get(depId);
      if (from) {
        const x1 = from.left + from.width, y1 = from.top + props.rowHeight / 2;
        const x2 = t2.left, y2 = t2.top + props.rowHeight / 2;
        links.push({
          id: `${depId}-${t2.task.id}`,
          from: from.task,
          to: t2.task,
          path: `M ${x1} ${y1} L ${x2} ${y2}`
        });
      }
    });
  });
  return links;
});
let activeScrollTarget = null;
let scrollTimer = null;
const handleBodyScroll = (e) => {
  const target = e.target;
  if (activeScrollTarget && activeScrollTarget !== target) return;
  activeScrollTarget = target;
  if (scrollTimer) clearTimeout(scrollTimer);
  scrollTimer = setTimeout(() => {
    activeScrollTarget = null;
  }, 100);
  let scrollT = target.scrollTop;
  if (sidebarBodyRef.value && timelineBodyRef.value) {
    const maxSidebar = sidebarBodyRef.value.scrollHeight - sidebarBodyRef.value.clientHeight;
    const maxTimeline = timelineBodyRef.value.scrollHeight - timelineBodyRef.value.clientHeight;
    if (maxSidebar > 0 && maxTimeline > 0) {
      const maxScroll = Math.min(maxSidebar, maxTimeline);
      if (scrollT > maxScroll) {
        scrollT = maxScroll;
        target.scrollTop = maxScroll;
      }
    }
  }
  scrollTop.value = scrollT;
  if (sidebarBodyRef.value && target === timelineBodyRef.value) {
    sidebarBodyRef.value.scrollTop = scrollT;
  } else if (timelineBodyRef.value && target === sidebarBodyRef.value) {
    timelineBodyRef.value.scrollTop = scrollT;
  }
  if (sidebarHeaderRef.value && target === sidebarBodyRef.value) {
    sidebarHeaderRef.value.scrollLeft = target.scrollLeft;
  }
};
const updateSuccessors = (taskId) => {
  const findTask = (id, list) => {
    for (const t2 of list) {
      if (t2.id === id) return t2;
      if (t2.children) {
        const found = findTask(id, t2.children);
        if (found) return found;
      }
    }
  };
  const findAffected = (predId, list) => {
    let affected2 = [];
    list.forEach((t2) => {
      if (t2.dependencies?.includes(predId)) affected2.push(t2);
      if (t2.children) affected2 = affected2.concat(findAffected(predId, t2.children));
    });
    return affected2;
  };
  const pred = findTask(taskId, props.data);
  if (!pred) return;
  const affected = findAffected(taskId, props.data);
  affected.forEach((task) => {
    const start = dayjs(task.startDate), end = dayjs(pred.endDate);
    if (start.isBefore(end)) {
      const dur = dayjs(task.endDate).diff(dayjs(task.startDate), "day");
      const ns2 = end.add(1, "day");
      task.startDate = ns2.format("YYYY-MM-DD HH:mm:ss");
      task.endDate = ns2.add(dur, "day").format("YYYY-MM-DD HH:mm:ss");
      updateSuccessors(task.id);
    }
  });
};
const activeDragTask = ref(null);
let dragType = null, dragStartX = 0, dragInitS = dayjs(), dragInitE = dayjs(), dragInitP = 0;
const onDragStart = (e, task, type) => {
  if (type === "progress" && !props.progressDraggable) return;
  if (type !== "progress" && !props.draggable) return;
  e.preventDefault();
  e.stopPropagation();
  activeDragTask.value = task;
  dragType = type;
  dragStartX = e.clientX;
  dragInitS = dayjs(task.startDate);
  dragInitE = dayjs(task.endDate);
  dragInitP = task.progress || 0;
  if (typeof document !== "undefined") {
    document.addEventListener("mousemove", onDragMove);
    document.addEventListener("mouseup", onDragEnd);
  }
};
const onDragMove = (e) => {
  if (!activeDragTask.value) return;
  const dx = e.clientX - dragStartX;
  const msPerDay = 864e5;
  const msOffset = dx / PIXELS_PER_DAY.value * msPerDay;
  const currentTask = activeDragTask.value;
  const originalTask = currentTask._original || currentTask;
  if (dragType === "move") {
    const newStart = dragInitS.add(msOffset, "ms").format("YYYY-MM-DD HH:mm:ss");
    const newEnd = dragInitE.add(msOffset, "ms").format("YYYY-MM-DD HH:mm:ss");
    currentTask.startDate = newStart;
    currentTask.endDate = newEnd;
    originalTask.startDate = newStart;
    originalTask.endDate = newEnd;
  } else if (dragType === "left") {
    const ns2 = dragInitS.add(msOffset, "ms");
    if (ns2.isBefore(dragInitE)) {
      const newStart = ns2.format("YYYY-MM-DD HH:mm:ss");
      currentTask.startDate = newStart;
      originalTask.startDate = newStart;
    }
  } else if (dragType === "right") {
    const ne = dragInitE.add(msOffset, "ms");
    if (ne.isAfter(dragInitS)) {
      const newEnd = ne.format("YYYY-MM-DD HH:mm:ss");
      currentTask.endDate = newEnd;
      originalTask.endDate = newEnd;
    }
  } else if (dragType === "progress") {
    const pos = getTaskPosition(currentTask, 0);
    const newProgress = Math.round(Math.max(0, Math.min(100, dragInitP + dx / pos.width * 100)));
    currentTask.progress = newProgress;
    originalTask.progress = newProgress;
  }
};
const onDragEnd = () => {
  const draggedTask = activeDragTask.value;
  if (draggedTask && props.autoSchedule) updateSuccessors(draggedTask.id);
  if (draggedTask) {
    if (dragType === "progress") {
      emit("progress-drag-end", draggedTask);
    } else {
      emit("task-drag-end", draggedTask);
    }
  }
  emit("update:data", [...props.data]);
  activeDragTask.value = null;
  dragType = null;
  if (typeof document !== "undefined") {
    document.removeEventListener("mousemove", onDragMove);
    document.removeEventListener("mouseup", onDragEnd);
  }
};
const handleTaskClick = (e, task) => {
  if (dragType) return;
  emit("task-click", task, e);
};
const handleTaskDblClick = (e, task) => {
  emit("task-dblclick", task, e);
};
const truncatedTasks = ref({});
const handleMouseEnter = (e, task) => {
  const label = e.currentTarget.querySelector(
    `.${ns.e("task-label")}`
  );
  if (label) truncatedTasks.value[task.id] = label.scrollWidth > label.clientWidth;
};
let resizeObserver = null;
onMounted(() => {
  if (typeof window !== "undefined" && window.ResizeObserver && timelineBodyRef.value) {
    viewportHeight.value = timelineBodyRef.value.clientHeight || 600;
    resizeObserver = new window.ResizeObserver((entries) => {
      for (const entry of entries) {
        if (entry.target === timelineBodyRef.value) {
          viewportHeight.value = timelineBodyRef.value.clientHeight || 600;
        }
      }
    });
    resizeObserver.observe(timelineBodyRef.value);
  }
});
onBeforeUnmount(() => {
  if (scrollTimer) {
    clearTimeout(scrollTimer);
    scrollTimer = null;
  }
  if (resizeObserver) {
    resizeObserver.disconnect();
    resizeObserver = null;
  }
  if (typeof document !== "undefined") {
    document.removeEventListener("mousemove", onDragMove);
    document.removeEventListener("mouseup", onDragEnd);
  }
  if (activeDragTask.value) {
    dragType = null;
    activeDragTask.value = null;
  }
});
</script>

<template>
  <div
    ref="ganttRef"
    :class="[ns.b(), ns.is('bordered', bordered), ns.is('loading', loading)]"
    :style="themeStyle"
  >
    <div :class="ns.e('toolbar')">
      <div :class="ns.e('toolbar-left')">
        <YhInput
          id="gantt-search-input"
          v-model="searchKeyword"
          :placeholder="ganttSearchPlaceholder"
          prefix-icon="search"
          size="small"
          style="width: 200px"
        />
      </div>
      <div :class="ns.e('toolbar-right')">
        <span style="font-size: 12px; color: var(--yh-text-color-secondary)">{{
          ganttZoomText
        }}</span>
        <input
          type="range"
          v-model.number="PIXELS_PER_DAY"
          :min="minPixelsPerDay"
          max="100"
          step="0.5"
          style="width: 80px"
        />
        <YhRadioGroup
          v-model="internalViewMode"
          size="small"
          type="button"
          name="gantt-view-switcher"
        >
          <YhRadioButton value="day">{{ t("ganttchart.day") }}</YhRadioButton>
          <YhRadioButton value="week">{{ t("ganttchart.week") }}</YhRadioButton>
          <YhRadioButton value="month">{{ t("ganttchart.month") }}</YhRadioButton>
          <YhRadioButton value="year">{{ t("ganttchart.year") }}</YhRadioButton>
        </YhRadioGroup>
      </div>
    </div>

    <div :class="ns.e('main')">
      <div :class="ns.e('sidebar')">
        <div
          ref="sidebarHeaderRef"
          :class="ns.e('sidebar-header-wrapper')"
          :style="{
  height: `${headerHeight}px`
}"
        >
          <div :class="ns.e('sidebar-header')">
            <div
              v-for="col in resolvedColumns"
              :key="col.prop"
              :class="ns.e('sidebar-header-cell')"
              :style="{
  width: typeof col.width === 'number' ? `${col.width}px` : col.width
}"
            >
              {{ col.label }}
            </div>
          </div>
        </div>
        <div ref="sidebarBodyRef" :class="ns.e('sidebar-body')" @scroll="handleBodyScroll">
          <div v-if="virtual" :style="{
  height: `${virtualState.offsetTop}px`
}"></div>
          <div
            v-for="(task, index) in renderTasks"
            :key="task.id"
            :class="ns.e('row')"
            :style="{
  height: `${rowHeight}px`
}"
          >
            <div
              v-for="col in resolvedColumns"
              :key="col.prop"
              :class="ns.e('cell')"
              :style="{
  width: typeof col.width === 'number' ? `${col.width}px` : col.width
}"
            >
              <slot name="table-cell" :row="task" :column="col" :index="index">
                <template v-if="col.prop === resolvedColumns[0].prop">
                  <span :class="ns.e('tree-node')">
                    <span
                      v-for="(hasNext, i) in task._ancestorHasNext"
                      :key="i"
                      :class="[ns.e('tree-indent'), ns.is('last', !hasNext)]"
                    />
                    <span :class="[ns.e('tree-indent'), ns.is('last', task._isLast)]">
                      <span :class="ns.e('tree-content')">
                        <span
                          v-if="task._hasChildren"
                          :class="[ns.e('tree-arrow'), expandedTasks.has(task.id) && 'is-expanded']"
                          @click.stop="toggleExpand(task)"
                        >
                          <svg viewBox="0 0 1024 1024" width="12" height="12">
                            <path
                              d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.936a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 533.376a30.592 30.592 0 0 0 0-42.752L382.592 149.312a29.12 29.12 0 0 0-41.728 0z"
                            ></path>
                          </svg>
                        </span>
                        <span v-else style="display: inline-block; width: 14px"></span>
                        <span :class="ns.e('tree-line')" />
                      </span>
                    </span>
                  </span>
                </template>
                <YhTooltip
                  :content="String(task[col.prop])"
                  placement="top"
                  :teleported="teleported"
                >
                  <span :class="ns.e('cell-text')">{{ task[col.prop] }}</span>
                </YhTooltip>
              </slot>
            </div>
          </div>
          <div
            v-if="virtual"
            :style="{
  height: `${totalHeight - (virtualState.offsetTop + renderTasks.length * rowHeight)}px`
}"
          >
          </div>
        </div>
      </div>

      <div :class="ns.e('timeline')">
        <div :class="ns.e('timeline-header')" :style="{
  height: `${headerHeight}px`
}">
          <div
            :class="ns.e('timeline-header-top')"
            :style="{
  height: `${headerHeight / 2}px`,
  width: `${timelineWidth}px`
}"
          >
            <div
              v-for="(th, i) in topHeaders"
              :key="i"
              :class="ns.e('timeline-unit')"
              :style="{
  width: `${th.width}px`
}"
            >
              {{ th.label }}
            </div>
          </div>
          <div
            :class="ns.e('timeline-header-bottom')"
            :style="{
  height: `${headerHeight / 2}px`,
  width: `${timelineWidth}px`
}"
          >
            <div
              v-for="(th, i) in bottomHeaders"
              :key="i"
              :class="[ns.e('timeline-unit'), th.isToday ? ns.em('timeline-unit', 'today') : '']"
              :style="{
  width: `${th.width}px`
}"
            >
              <span
                v-if="th.width > 20"
                style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
                >{{ th.label }}</span
              >
            </div>
          </div>
        </div>
        <div ref="timelineBodyRef" :class="ns.e('timeline-body')" @scroll="handleBodyScroll">
          <div
            :style="{
  width: `${timelineWidth}px`,
  height: `${totalHeight}px`,
  position: 'relative'
}"
          >
            <div v-if="virtual" :style="{
  height: `${virtualState.offsetTop}px`
}"></div>
            <div
              :class="ns.e('grid')"
              :style="{
  width: `${timelineWidth}px`,
  height: `${totalHeight}px`
}"
            >
              <div
                v-for="(th, i) in bottomHeaders"
                :key="i"
                :class="ns.e('grid-column')"
                :style="{
  width: `${th.width}px`
}"
              >
              </div>
            </div>
            <div
              v-for="(_, index) in renderTasks"
              :key="index"
              :class="ns.e('timeline-row')"
              :style="{
  height: `${rowHeight}px`,
  width: `${timelineWidth}px`
}"
            />
            <svg
              v-if="showDependencies"
              :class="ns.e('dependency-svg')"
              :style="{
  width: `${timelineWidth}px`,
  height: `${totalHeight}px`
}"
            >
              <path
                v-for="link in dependenciesLinks"
                :key="link.id"
                :d="link.path"
                :class="ns.e('dependency-path')"
              />
            </svg>
            <YhTooltip
              v-for="ts in taskStyles"
              :key="ts.task.id"
              :content="ts.isMilestone ? `${ts.task.name} (${ganttMilestoneText})` : ts.task.name"
              placement="top"
              :teleported="teleported"
              :class="[ns.e('task-wrapper'), ns.is('milestone', ts.isMilestone), ns.is('summary', ts.isSummary), ts.task.status ? ns.e(`task-${ts.task.status}`) : '']"
              :style="ts.style"
              @mouseenter="handleMouseEnter($event, ts.task)"
            >
              <div
                :class="ns.e('task-inner')"
                style="width: 100%; height: 100%; display: flex; align-items: center"
                @click="handleTaskClick($event, ts.task)"
                @dblclick="handleTaskDblClick($event, ts.task)"
                @mousedown="onDragStart($event, ts.task, 'move')"
              >
                <div v-if="ts.isSummary" :class="ns.e('summary-bar')">
                  <div :class="ns.e('summary-left')" />
                  <div :class="ns.e('summary-right')" />
                </div>
                <div
                  v-else-if="ts.isMilestone"
                  :class="ns.e('milestone-diamond')"
                  :style="{
  backgroundColor: ts.task.color || 'var(--yh-color-primary)'
}"
                />
                <template v-else>
                  <div
                    v-if="ts.task.progress"
                    :class="ns.e('task-progress')"
                    :style="{
  width: `${ts.task.progress}%`
}"
                  />
                  <slot name="task-content" :task="ts.task">
                    <span :class="ns.e('task-label')">{{ ts.task.name }}</span>
                  </slot>
                </template>
                <template v-if="!ts.isMilestone && draggable">
                  <div
                    :class="ns.e('resizer-left')"
                    @mousedown.stop="onDragStart($event, ts.task, 'left')"
                  />
                  <div
                    :class="ns.e('resizer-right')"
                    @mousedown.stop="onDragStart($event, ts.task, 'right')"
                  />
                </template>
              </div>
            </YhTooltip>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-gantt-chart {
  --yh-gantt-border-color: var(--yh-border-color, #dcdfe6);
  --yh-gantt-bg-color: var(--yh-bg-color, #ffffff);
  --yh-gantt-header-bg-color: var(--yh-fill-color, #f5f7fa);
  --yh-gantt-task-color: var(--yh-color-primary, #409eff);
  --yh-gantt-text-color: var(--yh-text-color-primary, #303133);
  --yh-gantt-row-hover-bg: var(--yh-fill-color-light, #f5f7fa);
  --yh-gantt-line-color: var(--yh-border-color-light, #e4e7ed);
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  background: var(--yh-gantt-bg-color);
  color: var(--yh-gantt-text-color);
  border: 1px solid var(--yh-gantt-border-color);
  border-radius: 4px;
  overflow: hidden;
  box-sizing: border-box;
}
.yh-gantt-chart__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 16px;
  background: var(--yh-bg-color, #fff);
  border-bottom: 1px solid var(--yh-gantt-border-color);
  gap: 12px;
  z-index: 100;
}

.yh-gantt-chart__toolbar-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

.yh-gantt-chart__main {
  display: flex;
  flex: 1;
  overflow: hidden;
}

.yh-gantt-chart {
  /* Sidebar */
}
.yh-gantt-chart__sidebar {
  width: 480px;
  border-right: 1px solid var(--yh-gantt-border-color);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  z-index: 10;
}

.yh-gantt-chart__sidebar-header-wrapper {
  width: 100%;
  overflow: hidden;
  background: var(--yh-gantt-header-bg-color);
  border-bottom: 1px solid var(--yh-gantt-border-color);
  box-sizing: border-box;
}

.yh-gantt-chart__sidebar-header {
  display: flex;
  white-space: nowrap;
  height: 100%;
}

.yh-gantt-chart__sidebar-header-cell {
  height: 100%;
  padding: 0 12px;
  font-weight: 600;
  font-size: 13px;
  display: flex;
  align-items: center;
  border-right: 1px solid var(--yh-gantt-line-color);
  overflow: hidden;
  flex-shrink: 0;
  box-sizing: border-box;
  color: var(--yh-text-color-regular, #606266);
}

.yh-gantt-chart__sidebar-body {
  flex: 1;
  overflow: auto;
}

.yh-gantt-chart__row {
  display: flex;
  border-bottom: 1px solid var(--yh-gantt-line-color);
  width: fit-content;
  min-width: 100%;
}
.yh-gantt-chart__row:hover {
  background: var(--yh-gantt-row-hover-bg);
}

.yh-gantt-chart__cell {
  padding: 0 12px;
  display: flex;
  align-items: center;
  border-right: 1px solid var(--yh-gantt-line-color);
  white-space: nowrap;
  overflow: hidden;
  flex-shrink: 0;
  box-sizing: border-box;
}
.yh-gantt-chart__cell.is-editing {
  padding: 0;
}
.yh-gantt-chart__cell.is-editing .yh-input {
  height: 100%;
  --yh-input-border-color: transparent;
  --yh-input-hover-border-color: transparent;
  --yh-input-focus-border-color: var(--yh-color-primary);
}
.yh-gantt-chart__cell.is-editing .yh-input .yh-input__wrapper {
  box-shadow: none !important;
  border-radius: 0;
  padding: 0 12px;
  height: 100%;
  background: transparent;
}

.yh-gantt-chart__cell .yh-input {
  width: 100%;
}

.yh-gantt-chart__cell-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-left: 4px;
  flex: 1;
}

.yh-gantt-chart {
  /* Tree Nodes & Lines */
}
.yh-gantt-chart__tree-node {
  display: inline-flex;
  align-items: center;
  height: 100%;
  position: relative;
}

.yh-gantt-chart__tree-indent {
  display: inline-block;
  width: 16px;
  height: 100%;
  position: relative;
  flex-shrink: 0;
}
.yh-gantt-chart__tree-indent::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 0;
  bottom: 0;
  border-left: 1px dotted #ccc;
}
.yh-gantt-chart__tree-indent.is-last::before {
  bottom: 50%;
}

.yh-gantt-chart__tree-content {
  display: inline-flex;
  align-items: center;
  width: 16px;
  height: 100%;
  position: relative;
}

.yh-gantt-chart__tree-line {
  position: absolute;
  left: 0;
  top: 50%;
  width: 8px;
  border-top: 1px dotted #ccc;
}

.yh-gantt-chart__tree-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  cursor: pointer;
  transition: transform 0.2s;
  color: var(--yh-text-color-secondary, #909399);
  z-index: 5;
}
.yh-gantt-chart__tree-arrow:hover {
  color: var(--yh-color-primary);
}
.yh-gantt-chart__tree-arrow.is-expanded {
  transform: rotate(90deg);
}
.yh-gantt-chart__tree-arrow svg {
  fill: currentColor;
}

.yh-gantt-chart {
  /* Timeline */
}
.yh-gantt-chart__timeline {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}

.yh-gantt-chart__timeline-header {
  display: flex;
  flex-direction: column;
  background: var(--yh-gantt-header-bg-color);
  border-bottom: 1px solid var(--yh-gantt-border-color);
  box-sizing: border-box;
}

.yh-gantt-chart__timeline-header-top {
  display: flex;
  border-bottom: 1px solid var(--yh-gantt-line-color);
  box-sizing: border-box;
}

.yh-gantt-chart__timeline-header-bottom {
  display: flex;
}

.yh-gantt-chart__timeline-unit {
  display: flex;
  align-items: center;
  justify-content: center;
  border-right: 1px solid var(--yh-gantt-line-color);
  font-size: 11px;
  color: var(--yh-text-color-secondary, #909399);
  flex-shrink: 0;
  box-sizing: border-box;
  overflow: hidden;
  padding: 0 4px;
}
.yh-gantt-chart__timeline-unit--today {
  color: var(--yh-color-primary);
  font-weight: bold;
}

.yh-gantt-chart__timeline-body {
  flex: 1;
  overflow: auto;
  position: relative;
  background: var(--yh-bg-color, #fff);
  box-sizing: border-box;
}

.yh-gantt-chart__grid {
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
}

.yh-gantt-chart__grid-column {
  height: 100%;
  border-right: 1px solid var(--yh-gantt-line-color);
  box-sizing: border-box;
}

.yh-gantt-chart__timeline-row {
  border-bottom: 1px solid var(--yh-gantt-line-color);
}
.yh-gantt-chart__timeline-row:hover {
  background: var(--yh-gantt-row-hover-bg);
}

.yh-gantt-chart {
  /* Tasks */
}
.yh-gantt-chart__task-wrapper {
  position: absolute;
  background: var(--yh-gantt-task-color);
  border-radius: 4px;
  display: flex;
  align-items: center;
  padding: 0 8px;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  color: #fff;
  font-size: 12px;
  z-index: 20;
  min-width: 4px;
  box-sizing: border-box;
}
.yh-gantt-chart__task-wrapper.is-milestone {
  background: transparent !important;
  box-shadow: none !important;
  padding: 0;
  overflow: visible;
}

.yh-gantt-chart__task-wrapper.is-summary {
  background: transparent !important;
  box-shadow: none !important;
  padding: 0;
}

.yh-gantt-chart__summary-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 8px;
  background: #5e626d;
  border-radius: 2px;
  pointer-events: none;
}
.yh-gantt-chart__summary-bar::before, .yh-gantt-chart__summary-bar::after {
  content: "";
  position: absolute;
  top: 0;
  width: 0;
  height: 0;
  border-top: 10px solid #5e626d;
}
.yh-gantt-chart__summary-bar::before {
  left: 0;
  border-right: 8px solid transparent;
}
.yh-gantt-chart__summary-bar::after {
  right: 0;
  border-left: 8px solid transparent;
}

.yh-gantt-chart__milestone-diamond {
  width: 14px;
  height: 14px;
  transform: rotate(45deg);
  border: 2px solid #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.yh-gantt-chart__task-progress {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.2);
  border-radius: 4px 0 0 4px;
}

.yh-gantt-chart__dependency-svg {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 5;
}

.yh-gantt-chart__dependency-path {
  fill: none;
  stroke: var(--yh-color-primary);
  stroke-width: 1.5;
  opacity: 0.6;
}

.yh-gantt-chart__resizer-left {
  position: absolute;
  width: 8px;
  top: 0;
  bottom: 0;
  cursor: ew-resize;
  z-index: 30;
  left: 0;
}

.yh-gantt-chart__resizer-right {
  position: absolute;
  width: 8px;
  top: 0;
  bottom: 0;
  cursor: ew-resize;
  z-index: 30;
  right: 0;
}

.yh-gantt-chart__task-success {
  background: var(--yh-color-success);
}

.yh-gantt-chart__task-warning {
  background: var(--yh-color-warning);
}

.yh-gantt-chart__task-danger {
  background: var(--yh-color-danger);
}

.yh-gantt-chart__task-info {
  background: var(--yh-color-info);
}
</style>
