<template>
  <div class="gantt-shell">
    <!-- 主甘特图面板（单层，可拖动/可缩放） -->
    <div
      v-if="dialogVisible && !isMinimized"
      class="gantt-panel"
      :style="panelStyle"
    >
      <div class="gantt-dialog-title" @mousedown.prevent="onPanelDragStart">
        <span class="gantt-dialog-title-text">甘特图</span>
        <span class="gantt-dialog-title-actions" @mousedown.stop>
          <i class="gantt-dialog-header-icon el-icon-minus" title="最小化" @click="minimize"></i>
          <i class="gantt-dialog-header-icon el-icon-close" title="关闭" @click="onClose"></i>
        </span>
      </div>
      <div class="gantt-dialog-body" ref="ganttBody">
        <!-- 固定：标题栏、工具栏、图例 不随内框缩放 -->
        <div class="gantt-toolbar">
          <div class="toolbar-left">
            <span class="k-time-range">K 时范围：{{ kTimeRangeText }}</span>
          </div>
          <div class="toolbar-right">
            <el-button size="small" @click="refreshData">刷新</el-button>
            <el-dropdown trigger="click" @command="handleExport">
              <el-button size="small" type="primary">
                导出<i class="el-icon-arrow-down el-icon--right"></i>
              </el-button>
              <el-dropdown-menu slot="dropdown" class="gantt-export-dropdown-menu">
                <el-dropdown-item command="png">导出 PNG</el-dropdown-item>
                <el-dropdown-item command="pdf">导出 PDF</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </div>
        </div>

        <div class="gantt-scroll-container" ref="ganttScrollContainer">
        <div class="gantt-main">
          <div class="gantt-sidebar">
            <div class="sidebar-header">任务名称</div>
            <div class="sidebar-content">
              <div
                v-for="(item, index) in displayBars"
                :key="item.id"
                class="sidebar-item"
                :class="{ 'sidebar-item-last': index === displayBars.length - 1 }"
              >
                <div class="item-icon" :style="{ backgroundColor: item.color }"></div>
                <div class="item-name">{{ item.name }}</div>
              </div>
            </div>
          </div>

          <div class="gantt-timeline-wrap" :style="{ '--hour-width': hourWidth + 'px' }">
            <!-- 包裹层：横向滚动在 timeline-scroll，纵向滚动在 timeline-content，避免右侧竖条被裁切 -->
            <div class="timeline-scroll-clip">
              <div class="timeline-scroll" ref="timelineScroll">
                <div class="timeline-header" :style="{ width: timelineWidth + 'px' }">
                  <div
                    v-for="(tick, index) in timelineTicks"
                    :key="index"
                    class="timeline-tick"
                    :style="{
                      left: tick.left + 'px',
                      width: tick.width + 'px'
                    }"
                  >
                    <span class="time-label">{{ tick.label }}</span>
                  </div>
                </div>
                <div class="timeline-content">
                  <div
                    v-for="(item, index) in displayBars"
                    :key="item.id"
                    class="timeline-row"
                    :class="{ 'timeline-row-last': index === displayBars.length - 1 }"
                  >
                    <el-tooltip
                      effect="dark"
                      placement="top"
                      :append-to-body="true"
                      popper-class="gantt-bar-tooltip"
                      :open-delay="150"
                      :content="buildTooltipText(item)"
                    >
                      <!-- 外层占满色块区域，避免 tooltip 包 absolute 子元素时触发区域为 0 -->
                      <div
                        class="timeline-bar-host"
                        :style="{
                          position: 'absolute',
                          left: item.startPx + 'px',
                          top: '6px',
                          width: item.widthPx + 'px',
                          height: '32px',
                          zIndex: 1
                        }"
                        :title="ganttBarTitleOneLine(item)"
                      >
                      <div
                        class="timeline-bar"
                        :class="{ 'timeline-bar-editable': isBarEditable(item) }"
                        :style="{
                          left: '0',
                          width: '100%',
                          backgroundColor: item.color
                        }"
                      >
                        <span
                          v-if="isBarEditable(item)"
                          class="bar-resize-handle bar-resize-handle-left"
                          title="拖动调整开始时间"
                          @mousedown.prevent.stop="onBarResizeStart($event, item, 'start')"
                        ></span>
                        <span
                          v-if="isBarEditable(item)"
                          class="bar-resize-handle bar-resize-handle-right"
                          title="拖动调整结束时间"
                          @mousedown.prevent.stop="onBarResizeStart($event, item, 'end')"
                        ></span>
                        <div class="bar-inner">
                          <span class="bar-text bar-text-name">
                            {{ item.name }}
                          </span>
                          <span class="bar-text bar-text-time">
                            {{ item.startKTime }} — {{ item.endKTime }}
                          </span>
                        </div>
                      </div>
                      </div>
                    </el-tooltip>
                  </div>
                </div>
              </div>
            </div>
            <!-- 横向滚动由 .timeline-scroll 自带原生滚动条完成，不再使用自定义灰条 -->
          </div>
        </div>
        </div>

        <!-- 固定：图例不随内框缩放 -->
        <div class="gantt-legend">
          <span class="legend-title">图例：</span>
          <span class="legend-item">
            <span class="legend-color legend-color-route"></span>航线
          </span>
          <span class="legend-item">
            <span class="legend-color legend-color-missile"></span>导弹发射
          </span>
          <span class="legend-item">
            <span class="legend-color legend-color-hold"></span>盘旋
          </span>
        </div>
      </div>
      <div
        class="gantt-panel-resize-handle"
        title="拖拽调整窗口大小"
        @mousedown.prevent.stop="onPanelResizeStart"
      ></div>
    </div>

    <!-- 底部最小化状态条 -->
    <transition name="fade">
      <div
        v-if="isMinimized"
        class="gantt-minibar"
        @click="restore"
      >
        <div class="minibar-left">
          <span class="minibar-title">甘特图</span>
          <span class="minibar-desc">
            任务数：{{ displayBars.length }}，时间范围：{{ kTimeRangeText }}
          </span>
        </div>
        <div class="minibar-right">
          <span class="minibar-hint">点击还原</span>
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
import { getMissileParams } from '@/api/system/routes';

const HOUR_WIDTH = 100; // 每小时宽度（像素）

function minutesToKTime(minutes) {
  const m = Number(minutes);
  if (!Number.isFinite(m)) return 'K+00:00';
  const sign = m >= 0 ? '+' : '-';
  const abs = Math.abs(m);
  // 使用“先把总分钟四舍五入，再做进位”避免出现 min=60 但小时不进位的问题（例如 K+03:60）
  const totalMin = Math.round(abs);
  const h = Math.floor(totalMin / 60);
  const min = totalMin % 60;
  return `K${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
}

function formatDurationText(startMinutes, endMinutes) {
  const s = Number(startMinutes);
  const e = Number(endMinutes);
  if (!Number.isFinite(s) || !Number.isFinite(e)) return '--';
  const deltaMin = Math.max(0, e - s);
  const totalSec = Math.round(deltaMin * 60);
  const h = Math.floor(totalSec / 3600);
  const m = Math.floor((totalSec % 3600) / 60);
  const sec = totalSec % 60;
  if (h > 0) return `${h}小时${m}分${sec}秒`;
  if (m > 0) return `${m}分${sec}秒`;
  return `${sec}秒`;
}

export default {
  name: 'GanttDrawer',
  props: {
    value: { type: Boolean, default: false },
    timeRange: { type: Object, default: () => ({ minMinutes: 0, maxMinutes: 120 }) },
    routeBars: { type: Array, default: () => [] },
    holdBars: { type: Array, default: () => [] },
    currentRoomId: { type: [String, Number], default: null },
    routes: { type: Array, default: () => [] },
    activeRouteIds: { type: Array, default: () => [] }
  },
  data() {
    return {
      missileBars: [],
      exporting: false,
      isMinimized: false,
      displayBars: [],
      hourWidth: HOUR_WIDTH,
      panelLeft: null,
      panelTop: 60,
      panelWidth: 1200,
      panelHeight: 800,
      panelMinWidth: 860,
      panelMinHeight: 460,
      isPanelDragging: false,
      panelDragOffsetX: 0,
      panelDragOffsetY: 0,
      isPanelResizing: false,
      panelResizeStartX: 0,
      panelResizeStartY: 0,
      panelResizeStartW: 0,
      panelResizeStartH: 0,
      barResizeState: null
    };
  },
  computed: {
    dialogVisible: {
      get() { return this.value; },
      set(v) { this.$emit('input', v); }
    },
    panelStyle() {
      const defaultLeft = Math.max(0, (window.innerWidth - this.panelWidth) / 2 - 20);
      const left = this.panelLeft == null ? defaultLeft : this.panelLeft;
      const top = this.panelTop == null ? 60 : this.panelTop;
      return {
        left: `${Math.max(0, left)}px`,
        top: `${Math.max(0, top)}px`,
        width: `${this.panelWidth}px`,
        height: `${this.panelHeight}px`
      };
    },
    minMinutes() {
      return this.timeRange && Number.isFinite(this.timeRange.minMinutes) ? this.timeRange.minMinutes : 0;
    },
    maxMinutes() {
      return this.timeRange && Number.isFinite(this.timeRange.maxMinutes) ? this.timeRange.maxMinutes : 120;
    },
    spanMinutes() {
      const s = this.maxMinutes - this.minMinutes;
      return s > 0 ? s : 120;
    },
    kTimeRangeText() {
      return `${minutesToKTime(this.minMinutes)} — ${minutesToKTime(this.maxMinutes)}`;
    },
    // 时间轴总宽度（像素）：按照分钟 / 60 * hourWidth 计算
    timelineWidth() {
      return (this.spanMinutes / 60) * this.hourWidth;
    },
    // 表头刻度：按 2 小时一个主刻度 K+HH:00，时间轴向上取整到整 2 小时（例如 13:32 -> 14:00）
    timelineTicks() {
      const ticks = [];
      const startHour = Math.floor(this.minMinutes / 60);
      const rawEndHour = Math.ceil(this.maxMinutes / 60);
      const endHour = rawEndHour % 2 === 0 ? rawEndHour : rawEndHour + 1;
      const stepHour = 2;
      const hw = this.hourWidth;

      for (let h = startHour; h <= endHour; h += stepHour) {
        const minutes = h * 60;
        ticks.push({
          label: minutesToKTime(minutes),
          left: ((minutes - this.minMinutes) / 60) * hw,
          width: hw * 2
        });
      }
      return ticks;
    }
  },
  watch: {
    value(v) {
      if (v) {
        this.isMinimized = false;
        this.ensurePanelInViewport();
        this.loadMissiles();
        this.rebuildGanttData();
      }
    },
    timeRange: {
      handler() {
        this.rebuildGanttData();
      },
      deep: true
    },
    routeBars: {
      handler() {
        this.rebuildGanttData();
      },
      deep: true
    },
    holdBars: {
      handler() {
        this.rebuildGanttData();
      },
      deep: true
    },
    missileBars: {
      handler() {
        this.rebuildGanttData();
      },
      deep: true
    },
    routes: {
      handler() {
        if (this.dialogVisible) {
          this.loadMissiles();
        }
      },
      deep: true
    },
    activeRouteIds() {
      if (this.dialogVisible) {
        this.loadMissiles();
      }
    }
  },
  methods: {
    minimize() {
      this.isMinimized = true;
      this.dialogVisible = false;
    },
    restore() {
      this.isMinimized = false;
      this.dialogVisible = true;
    },
    onClose() {
      this.$emit('input', false);
    },
    ensurePanelInViewport() {
      if (this.panelLeft == null) return;
      const maxLeft = Math.max(0, window.innerWidth - this.panelWidth);
      const maxTop = Math.max(0, window.innerHeight - this.panelHeight);
      this.panelLeft = Math.max(0, Math.min(maxLeft, this.panelLeft));
      this.panelTop = Math.max(0, Math.min(maxTop, this.panelTop));
    },
    onPanelDragStart(e) {
      if (this.isPanelResizing) return;
      this.isPanelDragging = true;
      const currentLeft = this.panelLeft == null ? Math.max(0, (window.innerWidth - this.panelWidth) / 2 - 20) : this.panelLeft;
      const currentTop = this.panelTop == null ? 60 : this.panelTop;
      this.panelDragOffsetX = e.clientX - currentLeft;
      this.panelDragOffsetY = e.clientY - currentTop;
      document.addEventListener('mousemove', this.onPanelDragMove);
      document.addEventListener('mouseup', this.onPanelDragEnd);
    },
    onPanelDragMove(e) {
      if (!this.isPanelDragging) return;
      e.preventDefault();
      const maxLeft = Math.max(0, window.innerWidth - this.panelWidth);
      const maxTop = Math.max(0, window.innerHeight - this.panelHeight);
      const left = e.clientX - this.panelDragOffsetX;
      const top = e.clientY - this.panelDragOffsetY;
      this.panelLeft = Math.max(0, Math.min(maxLeft, left));
      this.panelTop = Math.max(0, Math.min(maxTop, top));
    },
    onPanelDragEnd() {
      this.isPanelDragging = false;
      document.removeEventListener('mousemove', this.onPanelDragMove);
      document.removeEventListener('mouseup', this.onPanelDragEnd);
    },
    onPanelResizeStart(e) {
      if (this.isPanelDragging) return;
      this.isPanelResizing = true;
      this.panelResizeStartX = e.clientX;
      this.panelResizeStartY = e.clientY;
      this.panelResizeStartW = this.panelWidth;
      this.panelResizeStartH = this.panelHeight;
      document.addEventListener('mousemove', this.onPanelResizeMove);
      document.addEventListener('mouseup', this.onPanelResizeEnd);
    },
    onPanelResizeMove(e) {
      if (!this.isPanelResizing) return;
      e.preventDefault();
      const dx = e.clientX - this.panelResizeStartX;
      const dy = e.clientY - this.panelResizeStartY;
      this.panelWidth = Math.max(this.panelMinWidth, Math.min(window.innerWidth - 20, this.panelResizeStartW + dx));
      this.panelHeight = Math.max(this.panelMinHeight, Math.min(window.innerHeight - 20, this.panelResizeStartH + dy));
      this.ensurePanelInViewport();
    },
    onPanelResizeEnd() {
      this.isPanelResizing = false;
      document.removeEventListener('mousemove', this.onPanelResizeMove);
      document.removeEventListener('mouseup', this.onPanelResizeEnd);
    },
    rebuildGanttData() {
      const list = [];
      const span = this.spanMinutes || 1;

      const hw = this.hourWidth;
      (this.routeBars || []).forEach(r => {
        const durationMin = Math.max(0, r.endMinutes - r.startMinutes);
        const startPx = ((r.startMinutes - this.minMinutes) / 60) * hw;
        const widthPx = Math.max(40, (durationMin / 60) * hw);
        const endLabelMinutes = Math.min(r.endMinutes, this.maxMinutes);
        list.push({
          id: r.id,
          name: r.name,
          type: 'route',
          color: r.color || '#409EFF',
          startMinutes: r.startMinutes,
          endMinutes: r.endMinutes,
          actualStartKTime: minutesToKTime(r.startMinutes),
          actualEndKTime: minutesToKTime(r.endMinutes),
          startKTime: minutesToKTime(r.startMinutes),
          endKTime: minutesToKTime(endLabelMinutes),
          startPx,
          widthPx
        });
      });

      (this.holdBars || []).forEach(h => {
        const durationMin = Math.max(0, h.endMinutes - h.startMinutes);
        const startPx = ((h.startMinutes - this.minMinutes) / 60) * hw;
        const widthPx = Math.max(40, (durationMin / 60) * hw);
        const endLabelMinutes = Math.min(h.endMinutes, this.maxMinutes);
        list.push({
          id: h.id,
          name: h.name,
          type: 'hold',
          color: h.color || '#E6A23C',
          startMinutes: h.startMinutes,
          endMinutes: h.endMinutes,
          actualStartKTime: minutesToKTime(h.startMinutes),
          actualEndKTime: minutesToKTime(h.endMinutes),
          startKTime: minutesToKTime(h.startMinutes),
          endKTime: minutesToKTime(endLabelMinutes),
          startPx,
          widthPx
        });
      });

      (this.missileBars || []).forEach(m => {
        const durationMin = Math.max(1, m.endMinutes - m.startMinutes);
        const startPx = ((m.startMinutes - this.minMinutes) / 60) * hw;
        const widthPx = Math.max(40, (durationMin / 60) * hw);
        const endLabelMinutes = Math.min(m.endMinutes, this.maxMinutes);
        list.push({
          id: m.id,
          name: m.name,
          type: 'missile',
          color: m.color || '#F56C6C',
          startMinutes: m.startMinutes,
          endMinutes: m.endMinutes,
          actualStartKTime: minutesToKTime(m.startMinutes),
          actualEndKTime: minutesToKTime(m.endMinutes),
          startKTime: minutesToKTime(m.startMinutes),
          endKTime: minutesToKTime(endLabelMinutes),
          startPx,
          widthPx
        });
      });

      this.displayBars = list.sort((a, b) => a.startMinutes - b.startMinutes);
    },
    isBarEditable(item) {
      return item && item.type === 'route';
    },
    onBarResizeStart(e, item, edge) {
      if (!this.isBarEditable(item)) return;
      this.barResizeState = {
        id: item.id,
        edge,
        startX: e.clientX,
        startMinutes: item.startMinutes,
        endMinutes: item.endMinutes
      };
      document.addEventListener('mousemove', this.onBarResizeMove);
      document.addEventListener('mouseup', this.onBarResizeEnd);
    },
    onBarResizeMove(e) {
      if (!this.barResizeState) return;
      e.preventDefault();
      const state = this.barResizeState;
      const deltaMinutes = ((e.clientX - state.startX) / this.hourWidth) * 60;
      const roundedDelta = Math.round(deltaMinutes);
      const minDuration = 1;
      const idx = this.displayBars.findIndex(b => b.id === state.id);
      if (idx < 0) return;
      const target = this.displayBars[idx];
      let nextStart = state.startMinutes;
      let nextEnd = state.endMinutes;
      if (state.edge === 'start') {
        nextStart = Math.min(state.endMinutes - minDuration, state.startMinutes + roundedDelta);
      } else {
        nextEnd = Math.max(state.startMinutes + minDuration, state.endMinutes + roundedDelta);
      }
      this.applyBarMinutes(target, nextStart, nextEnd);
    },
    onBarResizeEnd() {
      if (!this.barResizeState) return;
      const state = this.barResizeState;
      const target = this.displayBars.find(b => b.id === state.id);
      if (target) {
        this.$emit('bar-time-change', {
          id: target.id,
          type: target.type,
          name: target.name,
          startMinutes: target.startMinutes,
          endMinutes: target.endMinutes
        });
      }
      this.barResizeState = null;
      document.removeEventListener('mousemove', this.onBarResizeMove);
      document.removeEventListener('mouseup', this.onBarResizeEnd);
    },
    applyBarMinutes(bar, startMinutes, endMinutes) {
      const durationMin = Math.max(0, endMinutes - startMinutes);
      bar.startMinutes = startMinutes;
      bar.endMinutes = endMinutes;
      bar.startKTime = minutesToKTime(startMinutes);
      bar.endKTime = minutesToKTime(Math.min(endMinutes, this.maxMinutes));
      bar.actualStartKTime = minutesToKTime(startMinutes);
      bar.actualEndKTime = minutesToKTime(endMinutes);
      bar.startPx = ((startMinutes - this.minMinutes) / 60) * this.hourWidth;
      bar.widthPx = Math.max(40, (durationMin / 60) * this.hourWidth);
    },
    buildTooltipText(item) {
      const start = item.actualStartKTime || item.startKTime;
      const end = item.actualEndKTime || item.endKTime;
      const duration = formatDurationText(item.startMinutes, item.endMinutes);
      return `${item.name}\n时间段：${start} — ${end}\n时长：${duration}`;
    },
    /** 浏览器原生 title（单行），作后备；悬浮块区域小时也能看到 */
    ganttBarTitleOneLine(item) {
      const start = item.actualStartKTime || item.startKTime;
      const end = item.actualEndKTime || item.endKTime;
      const duration = formatDurationText(item.startMinutes, item.endMinutes);
      return `${item.name} | ${start} — ${end} | ${duration}`;
    },
    refreshData() {
      this.loadMissiles();
      this.$message.success('已刷新');
    },
    loadMissiles() {
      if (!this.currentRoomId || !this.activeRouteIds || this.activeRouteIds.length === 0) {
        this.missileBars = [];
        return;
      }
      const promises = this.activeRouteIds.map(routeId => {
        const route = this.routes.find(r => r.id === routeId);
        if (!route) return Promise.resolve([]);
        const rId = this.currentRoomId;
        return getMissileParams({
          roomId: rId,
          routeId: route.id,
          platformId: route.platformId != null ? route.platformId : 0
        }).then(res => {
          let data = res.data;
          if (!data) return [];
          if (!Array.isArray(data)) data = [data];
          const routeLabel = route.name || String(routeId);
          return data
            .filter(m => m.launchTimeMinutesFromK != null)
            .map((m, idx) => {
              const start = Number(m.launchTimeMinutesFromK);
              const distanceKm = m.distance != null ? Number(m.distance) : 1000;
              const speedKmh = 1400;
              const durationMinutes = (distanceKm / speedKmh) * 60;
              const duration = Math.max(1, durationMinutes);
              return {
                id: `missile-${route.id}-${idx}`,
                name: `${routeLabel}发射导弹`,
                startMinutes: start,
                endMinutes: start + duration,
                color: '#F56C6C'
              };
            });
        }).catch(() => []);
      });
      Promise.all(promises).then(arrays => {
        this.missileBars = arrays.flat();
      });
    },
    minutesToKTime,
    handleExport(cmd) {
      if (cmd === 'png') this.exportPng();
      else if (cmd === 'pdf') this.exportPdf();
    },
    /** 导出用：捕获内框完整内容（含超出可视区域部分），非仅视口 */
    getExportTarget() {
      const container = this.$refs.ganttScrollContainer;
      const content = container;
      if (container) return { container, content };
      return null;
    },
    exportPng() {
      if (this.exporting) return;
      this.exporting = true;
      import('html2canvas').then(({ default: html2canvas }) => {
        const target = this.getExportTarget();
        if (!target) {
          this.exporting = false;
          this.$message.warning('无法获取甘特图区域');
          return;
        }
        const { container, content } = target;
        const scrollLeft = container.scrollLeft;
        const scrollTop = container.scrollTop;
        const origOverflow = container.style.overflow;
        const origWidth = container.style.width;
        const origHeight = container.style.height;
        const fullW = container.scrollWidth;
        const fullH = container.scrollHeight;
        container.style.overflow = 'visible';
        container.style.width = fullW + 'px';
        container.style.height = fullH + 'px';
        const captureEl = content && content.offsetHeight > 0 ? content : container;
        html2canvas(captureEl, { useCORS: true, scale: 2, backgroundColor: '#fff', width: fullW, height: fullH })
          .then(canvas => {
            container.style.overflow = origOverflow;
            container.style.width = origWidth;
            container.style.height = origHeight;
            container.scrollLeft = scrollLeft;
            container.scrollTop = scrollTop;
            const link = document.createElement('a');
            link.download = `甘特图_${Date.now()}.png`;
            link.href = canvas.toDataURL('image/png');
            link.click();
            this.$message.success('PNG 已导出');
          })
          .catch(err => {
            container.style.overflow = origOverflow;
            container.style.width = origWidth;
            container.style.height = origHeight;
            container.scrollLeft = scrollLeft;
            container.scrollTop = scrollTop;
            console.error(err);
            this.$message.error('导出 PNG 失败');
          })
          .finally(() => { this.exporting = false; });
      }).catch(() => {
        this.$message.error('请先安装 html2canvas');
        this.exporting = false;
      });
    },
    exportPdf() {
      if (this.exporting) return;
      this.exporting = true;
      Promise.all([
        import('html2canvas'),
        import('jspdf')
      ]).then(([{ default: html2canvas }, { default: jsPDF }]) => {
        const target = this.getExportTarget();
        if (!target) {
          this.exporting = false;
          this.$message.warning('无法获取甘特图区域');
          return;
        }
        const { container, content } = target;
        const scrollLeft = container.scrollLeft;
        const scrollTop = container.scrollTop;
        const origOverflow = container.style.overflow;
        const origWidth = container.style.width;
        const origHeight = container.style.height;
        const fullW = container.scrollWidth;
        const fullH = container.scrollHeight;
        container.style.overflow = 'visible';
        container.style.width = fullW + 'px';
        container.style.height = fullH + 'px';
        const captureEl = content && content.offsetHeight > 0 ? content : container;
        html2canvas(captureEl, { useCORS: true, scale: 2, backgroundColor: '#fff', width: fullW, height: fullH })
          .then(canvas => {
            container.style.overflow = origOverflow;
            container.style.width = origWidth;
            container.style.height = origHeight;
            container.scrollLeft = scrollLeft;
            container.scrollTop = scrollTop;
            const imgData = canvas.toDataURL('image/png');
            const w = canvas.width;
            const h = canvas.height;
            const pdf = new jsPDF({
              orientation: w > h ? 'landscape' : 'portrait',
              unit: 'px',
              format: [w, h]
            });
            pdf.addImage(imgData, 'PNG', 0, 0, w, h);
            pdf.save(`甘特图_${Date.now()}.pdf`);
            this.$message.success('PDF 已导出');
          })
          .catch(err => {
            container.style.overflow = origOverflow;
            container.style.width = origWidth;
            container.style.height = origHeight;
            container.scrollLeft = scrollLeft;
            container.scrollTop = scrollTop;
            console.error(err);
            this.$message.error('导出 PDF 失败');
          })
          .finally(() => { this.exporting = false; });
      }).catch(() => {
        this.$message.error('请先安装 html2canvas 和 jspdf');
        this.exporting = false;
      });
    }
  },
  beforeDestroy() {
    this.onPanelDragEnd();
    this.onPanelResizeEnd();
    this.onBarResizeEnd();
  }
};
</script>

<style scoped>
.gantt-shell {
  position: relative;
}
.gantt-panel {
  position: fixed;
  display: flex;
  flex-direction: column;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.12);
  z-index: 2500;
  overflow: hidden;
}
.gantt-dialog-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  padding: 0 16px 16px;
  background: #f5f7fb;
  overflow: hidden;
}
.gantt-toolbar {
  flex-shrink: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #e4e7ed;
  margin-bottom: 10px;
}
.toolbar-left { font-size: 13px; color: #606266; }
.toolbar-right { display: flex; gap: 8px; align-items: center; }
.k-time-range { font-weight: 500; }
/* 内框：可缩放，overflow:auto 产生局部滚动条 */
.gantt-scroll-container {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: auto;
  border-radius: 6px;
  box-shadow: 0 0 0 1px #ebeef5;
  background: #fff;
}
.gantt-main {
  display: flex;
  background: #fff;
  overflow: hidden;
  box-shadow: none;
}
.gantt-sidebar {
  width: 200px;
  flex-shrink: 0;
  border-right: 1px solid #f0f0f0;
  display: flex;
  flex-direction: column;
}
.sidebar-header {
  height: 40px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  background: #f5f7fa;
  font-size: 13px;
  font-weight: 500;
  color: #606266;
}
.sidebar-content { flex: 1; overflow-y: auto; }
.sidebar-item {
  height: 44px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  border-bottom: 1px solid #f0f0f0;
}
.item-icon {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  margin-right: 8px;
  flex-shrink: 0;
}
.item-name {
  font-size: 13px;
  color: #303133;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gantt-timeline-wrap {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-width: 0;
  background: #fff; /* 与主区一致，避免任何缝隙显灰 */
}
/* 包裹层：固定高度，避免子元素把右侧竖条裁切 */
.timeline-scroll-clip {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* 横向仍可滚动；同时显示在同一层，避免“内外两层感” */
.timeline-scroll {
  flex: 1;
  min-height: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow-x: auto;
  overflow-y: hidden !important;
  position: relative;
  border: none;
  background: #fff;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.timeline-header {
  flex-shrink: 0;
  height: 40px;
  position: relative;
  background: #f9fafb;
  border-bottom: 1px solid #f0f0f0;
}
.timeline-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  border-right: 1px solid #e4e7ed;
  box-sizing: border-box;
  padding-left: 2px;
  display: flex;
  align-items: center;
}
.time-label {
  font-size: 12px;
  color: #606266;
  font-weight: 500;
  text-align: left;
}
.timeline-content {
  flex: 1;
  min-height: 0;
  position: relative;
  overflow-x: hidden;
  overflow-y: hidden;
  background: #fff;
  border-bottom: none;
}
.timeline-row {
  height: 44px;
  position: relative;
  border-bottom: 1px solid #f5f5f5;
  background-image:
    repeating-linear-gradient(
      to right,
      #e5e7eb 0,
      #e5e7eb 1px,
      transparent 1px,
      transparent var(--hour-width)
    ),
    repeating-linear-gradient(
      to right,
      rgba(0,0,0,0.08) 0,
      rgba(0,0,0,0.08) 1px,
      transparent 1px,
      transparent calc(var(--hour-width) / 2)
    );
  background-size: var(--hour-width) 100%, calc(var(--hour-width) / 2) 100%;
}
.timeline-row:last-child,
.timeline-row.timeline-row-last {
  border-bottom: none !important; /* 最后一行无底边，避免与底部滚动条之间出现“可拖细线” */
}
.sidebar-item-last {
  border-bottom: none !important;
}
/* 时间轴与底部滚动条之间不增加任何边框/线，避免误以为有一条可拖的线 */
.timeline-scroll-clip {
  border-bottom: none !important;
}
/* 色块外包一层，保证 tooltip 触发区域与色块一致 */
.timeline-bar-host {
  pointer-events: auto;
}
.timeline-bar-host .timeline-bar {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  min-width: 0;
}
.timeline-bar {
  position: absolute;
  top: 6px;
  height: 32px;
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: default;
  min-width: 80px;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.05);
}
.timeline-bar.timeline-bar-editable {
  cursor: ew-resize;
}
.bar-resize-handle {
  position: absolute;
  top: 0;
  width: 10px;
  height: 100%;
  z-index: 2;
}
.bar-resize-handle-left {
  left: 0;
  cursor: w-resize;
}
.bar-resize-handle-right {
  right: 0;
  cursor: e-resize;
}
.bar-resize-handle::after {
  content: '';
  position: absolute;
  top: 7px;
  bottom: 7px;
  width: 2px;
  background: rgba(255, 255, 255, 0.85);
}
.bar-resize-handle-left::after {
  left: 2px;
}
.bar-resize-handle-right::after {
  right: 2px;
}
.bar-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  width: 100%;
}
.bar-text {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
  color: #fff;
}
.bar-text-name {
  font-size: 12px;
  font-weight: 500;
}
.bar-text-time {
  font-size: 10px;
  opacity: 0.9;
  margin-top: 2px;
}
.gantt-legend {
  flex-shrink: 0;
  margin-top: 10px;
  padding: 6px 0 2px;
  border-top: none;
  font-size: 12px;
  line-height: 1.7;
  color: #606266;
  white-space: normal;
  word-break: keep-all;
}
.legend-title { margin-right: 10px; color: #909399; }
.legend-item { margin-right: 14px; display: inline-flex; align-items: center; gap: 6px; }
.legend-color {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  display: inline-block;
}
.legend-color-route { background: #409EFF; }
.legend-color-missile { background: #F56C6C; }
.legend-color-hold { background: #E6A23C; }

.gantt-minibar {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 8px;
  height: 40px;
  background: rgba(255, 255, 255, 0.96);
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  padding: 0 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 13px;
  color: #606266;
  cursor: pointer;
  z-index: 3000;
  border: 1px solid #e4e7ed;
}
.gantt-panel-resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 20px;
  height: 20px;
  cursor: nwse-resize;
  user-select: none;
  z-index: 20;
  background: linear-gradient(to top left, transparent 50%, rgba(64, 158, 255, 0.28) 50%);
}
.gantt-panel-resize-handle:hover {
  background: linear-gradient(to top left, transparent 50%, rgba(64, 158, 255, 0.42) 50%);
}
.minibar-left {
  display: flex;
  align-items: center;
  gap: 12px;
}
.minibar-title {
  font-weight: 600;
  color: #303133;
}
.minibar-desc {
  color: #909399;
}
.minibar-right {
  color: #409EFF;
  font-size: 12px;
}
</style>

<style>
.gantt-dialog-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 8px 12px 8px 16px;
  border-bottom: 1px solid #e9edf3;
  background: linear-gradient(180deg, #fafbfd 0%, #f5f8fc 100%);
  cursor: move;
}
.gantt-dialog-title-text {
  font-size: 14px;
  line-height: 20px;
  font-weight: 500;
  color: #5c6778;
  letter-spacing: 0.2px;
}
.gantt-dialog-title-actions {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.gantt-dialog-header-icon {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  color: #909399;
  cursor: pointer;
  transition: color 0.2s;
  border-radius: 4px;
  border: none;
  background: none;
  outline: none;
}
.gantt-dialog-header-icon:hover {
  color: #303133;
  background: #eef2f8;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
/* 挂到 body 的 tooltip，需高于甘特面板 z-index:2500 */
.gantt-bar-tooltip {
  z-index: 10050 !important;
  max-width: 360px;
  white-space: pre-line;
  line-height: 1.45;
}
/* 下拉挂到 body 时默认 z-index≈2001，低于 .gantt-panel(2500)，菜单会被面板挡住 */
.gantt-export-dropdown-menu.el-dropdown-menu {
  z-index: 10060 !important;
}
</style>
