小小儁爺
2026-01-15 ec20251b12170d70751f705701f4e8c336808950
1.甘特图带分页实现示例
已添加3个文件
已修改1个文件
2745 ■■■■ 文件已修改
src/api/GanttTask.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/gantt/index.vue 874 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/gantt/甘特图不带分页.vue 897 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/gantt/甘特图带分页.vue 921 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/GanttTask.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import request from '@/utils/request'
// èŽ·å–åˆ†é¡µçš„ç”˜ç‰¹å›¾ä»»åŠ¡æ•°æ®
export function getGanttTasksPage(params) {
  return request({
    url: '/gantt/tasks/page',
    method: 'get',
    params
  })
}
// èŽ·å–æ‰€æœ‰ç”˜ç‰¹å›¾ä»»åŠ¡æ•°é‡
export function getGanttTasksCount(params) {
  return request({
    url: '/gantt/tasks/count',
    method: 'get',
    params
  })
}
// èŽ·å–ç”˜ç‰¹å›¾ä»»åŠ¡è¯¦æƒ…
export function getGanttTaskDetail(id) {
  return request({
    url: `/gantt/tasks/${id}`,
    method: 'get'
  })
}
// åˆ›å»ºç”˜ç‰¹å›¾ä»»åŠ¡
export function createGanttTask(data) {
  return request({
    url: '/gantt/tasks',
    method: 'post',
    data
  })
}
// æ›´æ–°ç”˜ç‰¹å›¾ä»»åŠ¡
export function updateGanttTask(id, data) {
  return request({
    url: `/gantt/tasks/${id}`,
    method: 'put',
    data
  })
}
// åˆ é™¤ç”˜ç‰¹å›¾ä»»åŠ¡
export function deleteGanttTask(id) {
  return request({
    url: `/gantt/tasks/${id}`,
    method: 'delete'
  })
}
src/views/gantt/index.vue
@@ -26,32 +26,55 @@
      </el-button>
    </div>
    <div id="gantt_here" style="width:100%; height:90vh;" />
    <div id="gantt_here" style="width:100%; height:calc(90vh - 50px);" />
    <!-- åˆ†é¡µç»„ä»¶ -->
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-sizes="[10, 20, 50, 100]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="totalTasks"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>
<script>
import { gantt } from '@/components/dhtmlxGantt'
import '@/components/dhtmlxGantt/codebase/dhtmlxgantt.css'
import { handleDateReduceOneDay } from '@/utils/global'
import { handleDateReduceOneDay, handleDatetime } from '@/utils/global'
export default {
  data() {
    return {
      value: 'default',
      ganttDateRange: ['2025-04-01', '2025-05-10'],
      selectedIds: []
      selectedIds: [],
      // åˆ†é¡µç›¸å…³æ•°æ®
      currentPage: 1,
      pageSize: 10,
      totalTasks: 0,
      allTasks: [], // å­˜å‚¨æ‰€æœ‰ä»»åŠ¡æ•°æ®
      paginatedTasks: [] // å½“前页的任务数据
    }
  },
  mounted() {
    // å…ˆæ”¹å˜æ—¥æœŸèŒƒå›´é…ç½®
    this.ganttDateRangeChange(this.ganttDateRange)
    this.init()
    // åˆå§‹åŒ–甘特图配置
    this.initGantt()
    // ç„¶åŽåŠ è½½ä»»åŠ¡æ•°æ®ï¼ˆä¼šè‡ªåŠ¨æ¸²æŸ“å½“å‰é¡µï¼‰
    this.loadTasks()
  },
  methods: {
    init() {
      // gantt.clearAll()
    initGantt() {
      gantt.plugins({
        auto_scheduling: true,
        critical_path: true,
@@ -250,31 +273,32 @@
        },
        {
          name: 'owner', align: 'center', width: 75, label: '负责人', template: function(task) {
            if (task.type == gantt.config.types.project) {
              return ''
            }
            const store = gantt.getDatastore('resource')
            const assignments = task[gantt.config.resource_property]
            if (!assignments || !assignments.length) {
              return 'Unassigned'
            }
            if (assignments.length == 1) {
              return store.getItem(assignments[0].resource_id).text
            }
            let result = ''
            assignments.forEach(function(assignment) {
              const owner = store.getItem(assignment.resource_id)
              if (!owner) {
                return
              }
              result += '<div class=\'owner-label\' title=\'' + owner.text + '\'>' + owner.text.substr(0, 1) + '</div>'
            })
            return result
            // if (task.type == gantt.config.types.project) {
            //   return ''
            // }
            //
            // const store = gantt.getDatastore('resource')
            // const assignments = task[gantt.config.resource_property]
            //
            // if (!assignments || !assignments.length) {
            //   return 'Unassigned'
            // }
            //
            // if (assignments.length === 1) {
            //   return store.getItem(assignments[0].resource_id).text
            // }
            //
            // let result = ''
            // assignments.forEach(function(assignment) {
            //   const owner = store.getItem(assignment.resource_id)
            //   if (!owner) {
            //     return
            //   }
            //   result += '<div class=\'owner-label\' title=\'' + owner.text + '\'>' + owner.text.substr(0, 1) + '</div>'
            // })
            //
            // return result
            return '张三'
          },
          resize: true
        },
@@ -414,311 +438,9 @@
      }
      gantt.init('gantt_here')
      gantt.parse({
        'data': [
          {
      // æ³¨æ„ï¼šè¿™é‡Œä¸ç«‹å³åŠ è½½æ•°æ®ï¼Œè€Œæ˜¯ç­‰å¾…loadTasks被调用
            'id': 1,
            'text': '项目1',
            'type': 'project',
            'start_date': '2025-04-02 00:00',
            'end_date': '2025-04-07 00:00',
            'duration': 5,
            'progress': 0.4,
            'owner': [{ 'resource_id': '5', 'value': 3 }],
            'parent': 0,
            'checked': false
          },
          {
            'id': 2,
            'text': '项目2',
            'type': 'project',
            'start_date': '02-04-2025 00:00',
            'duration': 8,
            'progress': 0.6,
            'owner': [{ 'resource_id': '5', 'value': 4 }],
            'parent': '1',
            checked: false
          },
          {
            'id': 3,
            'text': '项目3',
            'type': 'project',
            'start_date': '11-04-2025 00:00',
            'duration': 8,
            'parent': '1',
            'progress': 0.6,
            'owner': [{ 'resource_id': '5', 'value': 2 }],
            checked: false
          },
          {
            'id': 4,
            'text': '项目4',
            'type': 'project',
            'start_date': '13-04-2025 00:00',
            'duration': 5,
            'parent': '1',
            'progress': 0.5,
            'owner': [{ 'resource_id': '5', 'value': 4 }],
            'priority': 3,
            checked: true
          },
          {
            'id': 5,
            'text': '任务5',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 7,
            'parent': '2',
            'progress': 0.6,
            'owner': [{ 'resource_id': '6', 'value': 5 }],
            'priority': 1,
            checked: true
          },
          {
            'id': 6,
            'text': '任务6',
            'type': 'task',
            'calendar_id': 'custom1',
            'start_date': '03-04-2025 12:00',
            'duration': 7,
            'parent': '2',
            'progress': 0.6,
            'owner': [{ 'resource_id': '7', 'value': 1 }],
            'priority': 2,
            checked: false
          },
          {
            'id': 7,
            'text': '任务7',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '12-04-2025 00:00',
            'duration': 8,
            'parent': '3',
            'progress': 0.6,
            'owner': [{ 'resource_id': '10', 'value': 2 }],
            checked: false
          },
          {
            'id': 8,
            'text': '任务8',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '14-04-2025 00:00',
            'duration': 5,
            'parent': '4',
            'progress': 0.5,
            'owner': [{ 'resource_id': '10', 'value': 4 }, { 'resource_id': '9', 'value': 5 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 9,
            'text': '任务9',
            'type': 'task',
            'start_date': '21-04-2025 00:00',
            'duration': 4,
            'parent': '4',
            'progress': 0.5,
            'owner': [{ 'resource_id': '7', 'value': 3 }],
            checked: false
          },
          {
            'id': 10,
            'text': '任务10',
            'type': 'task',
            'start_date': '27-04-2025 00:00',
            'duration': 3,
            'parent': '4',
            'progress': 0.5,
            'owner': [{ 'resource_id': '8', 'value': 5 }],
            'priority': 2,
            checked: false
          },
          {
            'id': 11,
            'text': '项目11',
            'type': 'project',
            'progress': 0.6,
            'start_date': '02-04-2025 00:00',
            'duration': 13,
            'owner': [{ 'resource_id': '5', 'value': 4 }],
            'parent': 0,
            checked: false
          },
          {
            'id': 12,
            'text': '任务12',
            'calendar_id': 'custom2',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
            'parent': '11',
            'progress': 1,
            'owner': [{ 'resource_id': '7', 'value': 6 }],
            checked: false
          },
          {
            'id': 13,
            'text': '项目13',
            'type': 'project',
            'start_date': '03-04-2025 00:00',
            'duration': 11,
            'parent': '11',
            'progress': 0.5,
            'owner': [{ 'resource_id': '5', 'value': 2 }],
            checked: false
          },
          {
            'id': 14,
            'text': '任务14',
            'calendar_id': 'custom2',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 6,
            'parent': '11',
            'owner': [],
            'progress': 0.8,
            checked: false
          },
          {
            'id': 15,
            'text': '项目15',
            'type': 'project',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
            'parent': '11',
            'progress': 0.2,
            'owner': [{ 'resource_id': '5', 'value': 5 }],
            checked: false
          },
          {
            'id': 16,
            'text': '任务16',
            'calendar_id': 'custom2',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 7,
            'parent': '11',
            'progress': 0,
            'owner': [{ 'resource_id': '7', 'value': 2 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 17,
            'text': '任务17',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 2,
            'parent': '13',
            'progress': 1,
            'owner': [{ 'resource_id': '8', 'value': 1 }],
            'priority': 2,
            checked: false
          },
          {
            'id': 25,
            'text': '任务18',
            'type': 'task',
            'start_date': '06-04-2025 00:00',
            'parent': '13',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 1 }],
            'duration': 2,
            checked: false
          },
          {
            'id': 18,
            'text': '任务19',
            'type': 'task',
            'start_date': '10-04-2025 00:00',
            'duration': 2,
            'parent': '13',
            'progress': 0.8,
            'owner': [{ 'resource_id': '6', 'value': 2 }],
            'priority': 3,
            checked: false
          },
          {
            'id': 19,
            'text': '任务20',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '13-04-2025 00:00',
            'duration': 4,
            'parent': '13',
            'progress': 0.2,
            'owner': [{ 'resource_id': '6', 'value': 3 }],
            checked: false
          },
          {
            'id': 20,
            'text': '任务21',
            'type': 'task',
            'start_date': '13-04-2025 00:00',
            'duration': 4,
            'parent': '13',
            'progress': 0,
            'owner': [{ 'resource_id': '8', 'value': 4 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 21,
            'text': '任务22',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 4,
            'parent': '15',
            'progress': 0.5,
            'owner': [{ 'resource_id': '6', 'value': 5 }],
            checked: false
          },
          {
            'id': 22,
            'text': '任务23',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 4,
            'parent': '15',
            'progress': 0.1,
            'owner': [{ 'resource_id': '8', 'value': 3 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 23,
            'text': '任务24',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
            'parent': '15',
            'progress': 0,
            'owner': [{ 'resource_id': '8', 'value': 5 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 24,
            'text': '任务25',
            // 'type': 'milestone',
            'type': 'task',
            'start_date': '20-04-2025 00:00',
            'parent': '11',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 3 }],
            'duration': 2,
            checked: false
          }
        ]
      })
      // ç»‘定甘特图点击事件(官方推荐的事件委托用法)<span data-allow-html class='source-item source-aggregated' data-group-key='source-group-2' data-url='https://juejin.cn/post/7352376280387764278' data-id='turn0fetch0'><span data-allow-html class='source-item-num' data-group-key='source-group-2' data-id='turn0fetch0' data-url='https://juejin.cn/post/7352376280387764278'><span class='source-item-num-name' data-allow-html>https://juejin.cn/post/7352376280387764278</span></span></span>
      // ç»‘定甘特图点击事件(官方推荐的事件委托用法)
      gantt.attachEvent('onTaskClick', (id, e) => {
        // æ‰¾åˆ°ç‚¹å‡»çš„æ˜¯å¦æ˜¯å¤é€‰æ¡†
        const checkbox = e.target.closest('[data-action="check-row"]')
@@ -731,6 +453,11 @@
        const task = gantt.getTask(id)
        if (task) {
          task.checked = !task.checked
          // åŒæ—¶æ›´æ–°å…¨å±€æ•°æ®
          const globalTask = this.allTasks.find(t => t.id === id)
          if (globalTask) {
            globalTask.checked = task.checked
          }
          gantt.updateTask(id) // åªåˆ·æ–°è¿™ä¸€è¡Œï¼Œæ€§èƒ½æ›´å¥½
          this.syncSelected() // åŒæ­¥åˆ° Vue çš„ selectedIds
        }
@@ -743,50 +470,416 @@
      this.syncSelected()
    },
    // åŠ è½½ä»»åŠ¡æ•°æ®
    loadTasks() {
      // ä½¿ç”¨åŽŸæœ‰çš„ç¤ºä¾‹æ•°æ®ä½œä¸ºåŸºç¡€
      this.allTasks = [
        {
          'id': 1,
          'text': '项目1',
          'type': 'project',
          'start_date': '2025-04-02 00:00',
          'end_date': '2025-04-07 00:00',
          'duration': 5,
          'progress': 0.4,
          'owner': [{ 'resource_id': '5', 'value': 3 }],
          'parent': 0,
          'checked': false
        },
        {
          'id': 2,
          'text': '项目2',
          'type': 'project',
          'start_date': '02-04-2025 00:00',
          'duration': 8,
          'progress': 0.6,
          'owner': [{ 'resource_id': '5', 'value': 4 }],
          'parent': '1',
          checked: false
        },
        {
          'id': 3,
          'text': '项目3',
          'type': 'project',
          'start_date': '11-04-2025 00:00',
          'duration': 8,
          'parent': '1',
          'progress': 0.6,
          'owner': [{ 'resource_id': '5', 'value': 2 }],
          checked: false
        },
        {
          'id': 4,
          'text': '项目4',
          'type': 'project',
          'start_date': '13-04-2025 00:00',
          'duration': 5,
          'parent': '1',
          'progress': 0.5,
          'owner': [{ 'resource_id': '5', 'value': 4 }],
          'priority': 3,
          checked: true
        },
        {
          'id': 5,
          'text': '任务5',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 7,
          'parent': '2',
          'progress': 0.6,
          'owner': [{ 'resource_id': '6', 'value': 5 }],
          'priority': 1,
          checked: true
        },
        {
          'id': 6,
          'text': '任务6',
          'type': 'task',
          'calendar_id': 'custom1',
          'start_date': '03-04-2025 12:00',
          'duration': 7,
          'parent': '2',
          'progress': 0.6,
          'owner': [{ 'resource_id': '7', 'value': 1 }],
          'priority': 2,
          checked: false
        },
        {
          'id': 7,
          'text': '任务7',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '12-04-2025 00:00',
          'duration': 8,
          'parent': '3',
          'progress': 0.6,
          'owner': [{ 'resource_id': '10', 'value': 2 }],
          checked: false
        },
        {
          'id': 8,
          'text': '任务8',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '14-04-2025 00:00',
          'duration': 5,
          'parent': '4',
          'progress': 0.5,
          'owner': [{ 'resource_id': '10', 'value': 4 }, { 'resource_id': '9', 'value': 5 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 9,
          'text': '任务9',
          'type': 'task',
          'start_date': '21-04-2025 00:00',
          'duration': 4,
          'parent': '4',
          'progress': 0.5,
          'owner': [{ 'resource_id': '7', 'value': 3 }],
          checked: false
        },
        {
          'id': 10,
          'text': '任务10',
          'type': 'task',
          'start_date': '27-04-2025 00:00',
          'duration': 3,
          'parent': '4',
          'progress': 0.5,
          'owner': [{ 'resource_id': '8', 'value': 5 }],
          'priority': 2,
          checked: false
        },
        {
          'id': 11,
          'text': '项目11',
          'type': 'project',
          'progress': 0.6,
          'start_date': '02-04-2025 00:00',
          'duration': 13,
          'owner': [{ 'resource_id': '5', 'value': 4 }],
          'parent': 0,
          checked: false
        },
        {
          'id': 12,
          'text': '任务12',
          'calendar_id': 'custom2',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 5,
          'parent': '11',
          'progress': 1,
          'owner': [{ 'resource_id': '7', 'value': 6 }],
          checked: false
        },
        {
          'id': 13,
          'text': '项目13',
          'type': 'project',
          'start_date': '03-04-2025 00:00',
          'duration': 11,
          'parent': '11',
          'progress': 0.5,
          'owner': [{ 'resource_id': '5', 'value': 2 }],
          checked: false
        },
        {
          'id': 14,
          'text': '任务14',
          'calendar_id': 'custom2',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 6,
          'parent': '11',
          'owner': [],
          'progress': 0.8,
          checked: false
        },
        {
          'id': 15,
          'text': '项目15',
          'type': 'project',
          'start_date': '03-04-2025 00:00',
          'duration': 5,
          'parent': '11',
          'progress': 0.2,
          'owner': [{ 'resource_id': '5', 'value': 5 }],
          checked: false
        },
        {
          'id': 16,
          'text': '任务16',
          'calendar_id': 'custom2',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 7,
          'parent': '11',
          'progress': 0,
          'owner': [{ 'resource_id': '7', 'value': 2 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 17,
          'text': '任务17',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 2,
          'parent': '13',
          'progress': 1,
          'owner': [{ 'resource_id': '8', 'value': 1 }],
          'priority': 2,
          checked: false
        },
        {
          'id': 18,
          'text': '任务19',
          'type': 'task',
          'start_date': '10-04-2025 00:00',
          'duration': 2,
          'parent': '13',
          'progress': 0.8,
          'owner': [{ 'resource_id': '6', 'value': 2 }],
          'priority': 3,
          checked: false
        },
        {
          'id': 19,
          'text': '任务20',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '13-04-2025 00:00',
          'duration': 4,
          'parent': '13',
          'progress': 0.2,
          'owner': [{ 'resource_id': '6', 'value': 3 }],
          checked: false
        },
        {
          'id': 20,
          'text': '任务21',
          'type': 'task',
          'start_date': '13-04-2025 00:00',
          'duration': 4,
          'parent': '13',
          'progress': 0,
          'owner': [{ 'resource_id': '8', 'value': 4 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 21,
          'text': '任务22',
          'type': 'project',
          'start_date': '03-04-2025 00:00',
          'duration': 4,
          'parent': '0',
          'progress': 0.5,
          'owner': [{ 'resource_id': '6', 'value': 5 }],
          checked: false
        },
        {
          'id': 22,
          'text': '任务23',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 4,
          'parent': '21',
          'progress': 0.1,
          'owner': [{ 'resource_id': '8', 'value': 3 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 23,
          'text': '任务24',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 5,
          'parent': '21',
          'progress': 0,
          'owner': [{ 'resource_id': '8', 'value': 5 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 24,
          'text': '任务25',
          // 'type': 'milestone',
          'type': 'task',
          'start_date': '20-04-2025 00:00',
          'parent': '21',
          'progress': 0,
          'owner': [{ 'resource_id': '5', 'value': 3 }],
          'duration': 2,
          checked: false
        }
      ]
      // æ·»åŠ æ›´å¤šç¤ºä¾‹æ•°æ®ï¼Œä½¿åˆ†é¡µæ•ˆæžœæ›´æ˜Žæ˜¾
      // for (let i = 25; i <= 100; i++) {
      //   this.allTasks.push({
      //     'id': i,
      //     'text': '任务' + i,
      //     'type': 'task',
      //     // 'start_date': handleDatetime(`0${Math.floor(Math.random() * 9 + 1)}-04-2025 00:00`),
      //     'start_date': `0${Math.floor(Math.random() * 9 + 1)}-04-2025 00:00`,
      //     'duration': Math.floor(Math.random() * 5) + 1,
      //     'parent': Math.floor(Math.random() * 10) + 1,
      //     'progress': Math.random(),
      //     'owner': [{ 'resource_id': '5', 'value': 3 }],
      //     'priority': Math.floor(Math.random() * 3) + 1,
      //     'checked': false
      //   })
      // }
      console.log(JSON.parse(JSON.stringify(this.allTasks)))
      this.totalTasks = this.allTasks.length
      this.updatePaginatedTasks()
      this.renderGanttChart()
    },
    // æ›´æ–°åˆ†é¡µåŽçš„任务数据
    updatePaginatedTasks() {
      const startIndex = (this.currentPage - 1) * this.pageSize
      const endIndex = Math.min(startIndex + this.pageSize, this.allTasks.length)
      this.paginatedTasks = this.allTasks.slice(startIndex, endIndex)
    },
    // æ¸²æŸ“甘特图
    renderGanttChart() {
      gantt.clearAll()
      console.log(JSON.parse(JSON.stringify(this.paginatedTasks)))
      gantt.parse({
        'data': this.paginatedTasks
      })
      // ç¡®ä¿ç”˜ç‰¹å›¾é‡æ–°æ¸²æŸ“
      // gantt.render()
    },
    // é¡µå¤§å°æ”¹å˜
    handleSizeChange(newSize) {
      this.pageSize = newSize
      this.currentPage = 1 // é‡ç½®åˆ°ç¬¬ä¸€é¡µ
      this.updatePaginatedTasks()
      this.renderGanttChart()
      this.syncSelected()
    },
    // å½“前页改变
    handleCurrentChange(newPage) {
      // è®¡ç®—最大页数,防止超出范围
      const maxPage = Math.ceil(this.totalTasks / this.pageSize)
      if (newPage > maxPage) {
        this.currentPage = maxPage
      } else if (newPage < 1) {
        this.currentPage = 1
      } else {
        this.currentPage = newPage
      }
      this.updatePaginatedTasks()
      this.renderGanttChart()
      this.syncSelected()
    },
    // ç”˜ç‰¹å›¾æ—¥æœŸæ”¹å˜
    ganttDateRangeChange(val) {
      gantt.config.start_date = val[0]
      gantt.config.end_date = val[1]
      gantt.render()
    },
    // å›žé€€æ‹–动操作
    ganttUndo() {
      gantt.undo()
    },
    // å‰è¿›æ‹–动操作
    ganttRedo() {
      gantt.redo()
    },
    // æ”¾å¤§
    ganttZoomIn() {
      gantt.ext.zoom.zoomIn()
    },
    // ç¼©å°
    ganttZoomOut() {
      gantt.ext.zoom.zoomOut()
    },
    // ä»Žç”˜ç‰¹å›¾ä¸­åŒæ­¥é€‰ä¸­çš„ id åˆ° Vue data
    syncSelected() {
      const tasks = gantt.serialize().data || []
      this.selectedIds = tasks.filter(t => t.checked).map(t => t.id)
      // åŒæ­¥å½“前页面任务到全局数据
      gantt.eachTask((task) => {
        const globalTask = this.allTasks.find(t => t.id === task.id)
        if (globalTask) {
          globalTask.checked = task.checked
        }
      })
      // èŽ·å–æ‰€æœ‰é€‰ä¸­çš„ä»»åŠ¡ID
      this.selectedIds = this.allTasks.filter(t => t.checked).map(t => t.id)
      console.log(this.selectedIds)
    },
    // èŽ·å–é€‰ä¸­ä»»åŠ¡ï¼ˆç¤ºä¾‹ï¼‰
    handleGetSelected() {
      const tasks = gantt.serialize().data || []
      const selected = tasks.filter(t => t.checked)
      const selected = this.allTasks.filter(t => t.checked)
      this.$notify.success(`当前已选中${selected.length} æ¡ä»»åŠ¡`)
    },
    // æ¸…空所有选择
    handleClearSelection() {
      // èŽ·å–æ‰€æœ‰ä»»åŠ¡
      const tasks = gantt.serialize().data || []
      // éåŽ†æ‰€æœ‰ä»»åŠ¡ï¼Œå°† checked å±žæ€§è®¾ç½®ä¸º false
      tasks.forEach(task => {
      this.allTasks.forEach(task => {
        task.checked = false
      })
      // æ›´æ–°æ‰€æœ‰ä»»åŠ¡æ˜¾ç¤º
      // æ›´æ–°å½“前页面显示的任务
      gantt.eachTask((task) => {
        task.checked = false
        gantt.updateTask(task.id)
@@ -812,86 +905,17 @@
  margin: unset;
}
.local_storage {
  background: lavender;
  border: 2px dotted orange;
  font-weight: bold;
}
.gantt_grid_scale .gantt_grid_head_cell,
.gantt_task .gantt_task_scale .gantt_scale_cell {
  font-weight: bold;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.7);
}
.resource_marker {
  text-align: center;
}
.resource_marker div {
  width: 28px;
  height: 28px;
  line-height: 29px;
  display: inline-block;
  border-radius: 15px;
  color: #FFF;
  margin: 3px;
}
.resource_marker.workday_ok div {
  background: #51c185;
}
.resource_marker.workday_over div {
  background: #ff8686;
}
.owner-label {
  width: 20px;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
  display: inline-block;
  border: 1px solid #cccccc;
  border-radius: 25px;
  background: #e6e6e6;
  color: #6f6f6f;
  margin: 0 3px;
  font-weight: bold;
}
.weekend {
  background: LightGoldenrodYellow;
}
.constraint-marker {
  position: absolute;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  width: 56px;
  height: 56px;
  margin-top: -11px;
  opacity: 0.4;
  z-index: 1;
  background: url("https://docs.dhtmlx.com/gantt/samples/common/constraint-arrow.svg") center no-repeat;
  background-size: cover;
}
.constraint-marker.earliest-start {
  margin-left: -53px;
}
.constraint-marker.latest-end {
  margin-left: -3px;
  transform: rotate(180deg);
}
.taskCheckBox {
  cursor: pointer;
  z-index: 99999 !important;
}
/* åˆ†é¡µå®¹å™¨æ ·å¼ */
.pagination-container {
  margin-top: 10px;
  display: flex;
  justify-content: end;
  padding: 10px 0;
  background-color: #fff;
  border-top: 1px solid #ebeef5;
}
</style>
src/views/gantt/¸ÊÌØÍ¼²»´ø·ÖÒ³.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,897 @@
<template>
  <div style="padding: 0 10px">
    <div style="padding: 10px 0;display: flex;">
      <el-button type="primary" size="mini" @click="ganttUndo">回退拖动操作</el-button>
      <el-button type="primary" size="mini" @click="ganttRedo">前进拖动操作</el-button>
      <el-button type="primary" size="mini" @click="ganttZoomIn">放大</el-button>
      <el-button type="primary" size="mini" @click="ganttZoomOut">缩小</el-button>
      <el-date-picker
        v-model="ganttDateRange"
        style="margin-left: 10px;"
        size="mini"
        value-format="yyyy-MM-dd"
        type="daterange"
        :clearable="false"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        @change="ganttDateRangeChange"
      />
      <el-button type="primary" style="margin-left: 10px;" size="mini" @click="handleGetSelected">
        èŽ·å–å¤é€‰æ¡†é€‰ä¸­ä»»åŠ¡
      </el-button>
      <el-button size="mini" @click="handleClearSelection">
        æ¸…空复选框选择
      </el-button>
    </div>
    <div id="gantt_here" style="width:100%; height:90vh;" />
  </div>
</template>
<script>
import { gantt } from '@/components/dhtmlxGantt'
import '@/components/dhtmlxGantt/codebase/dhtmlxgantt.css'
import { handleDateReduceOneDay } from '@/utils/global'
export default {
  data() {
    return {
      value: 'default',
      ganttDateRange: ['2025-04-01', '2025-05-10'],
      selectedIds: []
    }
  },
  mounted() {
    this.ganttDateRangeChange(this.ganttDateRange)
    this.initGantt()
  },
  methods: {
    initGantt() {
      // gantt.clearAll()
      gantt.plugins({
        auto_scheduling: true,
        critical_path: true,
        drag_timeline: true,
        grouping: true,
        keyboard_navigation: true,
        marker: true,
        multiselect: true,
        tooltip: true,
        undo: true
      })
      gantt.i18n.setLocale('cn')
      gantt.config.multiselect = true // å¼€å¯å¤šä»»åŠ¡é€‰æ‹©
      gantt.config.show_links = false // ä¸æ˜¾ç¤ºè¿žæŽ¥çº¿
      /* â†“↓↓ Auto-scheduling configuration â†“↓↓ */
      gantt.config.auto_scheduling = true
      /* â†“↓↓ Group configuration â†“↓↓ */
      gantt.serverList('task_priority', [
        { key: 1, label: '高' },
        { key: 2, label: '中等' },
        { key: 3, label: '低' }
      ])
      gantt.serverList('task_status', [
        { key: 1, label: 'Planning' },
        { key: 2, label: 'Not started' },
        { key: 3, label: 'In Progress' },
        { key: 4, label: 'Complete' }
      ])
      function byId(list, id) {
        for (let i = 0; i < list.length; i++) {
          if (list[i].key == id) {
            return list[i].label || ''
          }
        }
        return ''
      }
      /* â†‘↑↑ Group configuration â†‘↑↑ */
      // æ”¾å¤§ç¼©å°å±žæ€§
      /* â†“↓↓ Zoom configuration â†“↓↓ */
      const zoomConfig = {
        levels: [
          {
            name: 'hour',
            scale_height: 27,
            min_column_width: 50,
            scales: [
              { unit: 'day', format: '%Yå¹´%M%d号' },
              { unit: 'hour', format: '%H时' }
            ]
          },
          {
            name: 'day',
            scale_height: 27,
            min_column_width: 80,
            scales: [
              // { unit: 'day', step: 1, format: '%d %M' }
              { unit: 'day', step: 1, format: '%M%d号' }
            ]
          },
          {
            name: 'week',
            scale_height: 50,
            min_column_width: 70,
            scales: [
              // {
              //   unit: 'week', step: 1, format: function(date) {
              //     const dateToStr = gantt.date.date_to_str('%d %M')
              //     const endDate = gantt.date.add(date, -6, 'day')
              //     const weekNum = gantt.date.date_to_str('%W')(date)
              //     return '第' + weekNum + '周, ' + dateToStr(date) + ' - ' + dateToStr(endDate)
              //   }
              // },
              // %M
              { unit: 'week', format: '%Y年第%W周' },
              { unit: 'day', step: 1, format: '%M%d号' }
              // { unit: 'day', step: 1, format: '%j %D' }
              // { unit: 'day', step: 1, format: '星期%D' }
            ]
          },
          {
            name: 'month',
            scale_height: 50,
            min_column_width: 120,
            scales: [
              // { unit: 'month', format: '%Yå¹´%F' },
              { unit: 'month', format: '%Yå¹´%M' },
              { unit: 'week', format: '第%W周' }
            ]
          },
          {
            name: 'quarter',
            height: 50,
            min_column_width: 90,
            scales: [
              // {
              //   unit: 'quarter', step: 1, format: function(date) {
              //     const dateToStr = gantt.date.date_to_str('%M')
              //     const endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day')
              //     return dateToStr(date) + ' - ' + dateToStr(endDate)
              //   }
              // },
              { unit: 'month', step: 1, format: '%Yå¹´%M' }
            ]
          },
          {
            name: 'year',
            scale_height: 50,
            min_column_width: 30,
            scales: [
              { unit: 'year', step: 1, format: '%Yå¹´' }
            ]
          }
        ],
        useKey: 'ctrlKey',
        trigger: 'wheel',
        element: function() {
          return gantt.$root.querySelector('.gantt_task')
        }
      }
      gantt.ext.zoom.init(zoomConfig)
      gantt.ext.zoom.setLevel('week')
      /* â†‘↑↑ Zoom configuration â†‘↑↑ */
      // æ˜¯å¦æ˜¯å·¥ä½œæ—¶é—´
      /* â†“↓↓ Working Time configuration â†“↓↓ */
      gantt.templates.scale_cell_class = function(date) {
        if (!gantt.isWorkTime(date)) {
          return 'weekend'
        }
      }
      gantt.templates.timeline_cell_class = function(item, date) {
        if (!gantt.isWorkTime({ date: date, task: item })) {
          return 'weekend'
        }
      }
      gantt.config.work_time = true
      gantt.addCalendar({
        id: 'custom1',
        worktime: {
          hours: ['8:00-12:30', '13:00-17:30'], // global work hours for weekdays
          days: [0, 1, 1, 1, 1, 1, 1]
        }
      })
      // gantt.addCalendar({
      //   id: 'custom2',
      //   hours: ['12:00-21:00'],
      //   days: [1, 0, 1, 0, 1, 0, 1]
      // })
      /* â†‘↑↑ Working Time configuration â†‘↑↑ */
      // ç”˜ç‰¹å›¾åˆ—参数设置
      /* â†“↓↓ Grid Columns configuration â†“↓↓ */
      gantt.config.reorder_grid_columns = true
      const textEditor = { type: 'text', map_to: 'text' }
      const startDateEditor = { type: 'date', map_to: 'start_date' }
      const durationEditor = { type: 'number', map_to: 'duration', min: 1, max: 100 }
      gantt.config.columns = [
        {
          name: 'checked',
          label: '选择',
          align: 'center',
          width: 35,
          resize: false,
          // å…³é”®ï¼šç”¨ template è¿”回一个复选框
          template: (task) => {
            const checked = task.checked ? 'checked' : ''
            // data-action ç”¨äºŽåœ¨äº‹ä»¶å§”托时识别是复选框
            return `<input type="checkbox" class="taskCheckBox" data-action="check-row" ${checked} />`
          }
        },
        { name: 'wbs', label: '节点', width: 80, template: gantt.getWBSCode },
        { name: 'text', tree: true, label: '任务名称', width: 200, resize: true, editor: textEditor },
        { name: 'start_date', align: 'center', label: '开始日期', width: 80, resize: true, editor: startDateEditor },
        {
          name: 'duration',
          width: 60,
          align: 'center',
          label: '工期(天)',
          resize: true,
          editor: durationEditor,
          template: function(task) {
            // å¦‚æžœduration是null或undefined,返回0
            // return (task.duration || 0) + 1 // åœ¨å½“前duration的基础上加1
            return task.duration
          }
        },
        {
          name: 'owner', align: 'center', width: 75, label: '负责人', template: function(task) {
            if (task.type == gantt.config.types.project) {
              return ''
            }
            const store = gantt.getDatastore('resource')
            const assignments = task[gantt.config.resource_property]
            if (!assignments || !assignments.length) {
              return 'Unassigned'
            }
            if (assignments.length == 1) {
              return store.getItem(assignments[0].resource_id).text
            }
            let result = ''
            assignments.forEach(function(assignment) {
              const owner = store.getItem(assignment.resource_id)
              if (!owner) {
                return
              }
              result += '<div class=\'owner-label\' title=\'' + owner.text + '\'>' + owner.text.substr(0, 1) + '</div>'
            })
            return result
          },
          resize: true
        },
        {
          name: 'priority', width: 60, label: '优先级', align: 'center', resize: true, template: function(task) {
            return byId(gantt.serverList('task_priority'), task.priority)
          }
        },
        { name: 'add', width: 44 }
      ]
      /* â†‘↑↑ Grid Columns configuration â†‘↑↑ */
      // æ±‰åŒ–窗口
      gantt.locale.labels = {
        dhx_cal_today_button: '今天',
        day_tab: '日',
        week_tab: '周',
        month_tab: '月',
        new_event: '新建日程',
        icon_save: '保存',
        icon_cancel: '关闭',
        icon_details: '详细',
        icon_edit: '编辑',
        icon_delete: '删除',
        confirm_closing: '请确认是否撤销修改!', // Your changes will be lost, are your sure?
        confirm_deleting: '是否删除计划?',
        section_description: '描述:',
        section_resources: '自定义选择:',
        section_calendar: '自定义选择2:',
        section_time: '时间范围:',
        section_type: '类型:',
        section_text: '计划名称:',
        section_test: '测试:',
        section_projectClass: '项目类型:',
        taskProjectType_0: '项目任务',
        taskProjectType_1: '普通任务',
        section_head: '负责人:',
        section_priority: '优先级:',
        taskProgress: '任务状态',
        taskProgress_0: '未开始',
        taskProgress_1: '进行中',
        taskProgress_2: '已完成',
        taskProgress_3: '已延期',
        taskProgress_4: '搁置中',
        section_template: 'Details',
        /* grid columns */
        column_text: '计划名称',
        column_start_date: '开始时间',
        column_duration: '持续时间',
        column_add: '',
        column_priority: '难度',
        /* link confirmation */
        link: '关联',
        confirm_link_deleting: '将被删除',
        message_ok: '确定',
        message_cancel: '取消',
        link_start: ' (开始)',
        link_end: ' (结束)',
        type_task: '任务',
        type_project: '项目',
        type_milestone: '里程碑',
        minutes: '分钟',
        hours: '小时',
        days: '天',
        weeks: '周',
        months: '月',
        years: 'å¹´'
      }
      gantt.config.lightbox.sections = [
        { name: 'description', height: 38, map_to: 'text', type: 'textarea', focus: true },
        {
          name: 'resources', type: 'resources', map_to: 'owner', options: gantt.serverList('people'), default_value: 8
        },
        {
          name: 'calendar', height: 25, map_to: 'calendar_id', type: 'select', options: [
            { key: '', label: '默认' },
            { key: 'custom1', label: '选项一' },
            { key: 'custom2', label: '选项二' }
          ]
        },
        { name: 'time', type: 'duration', map_to: 'auto' }
      ]
      gantt.config.resource_store = 'resource'
      gantt.config.resource_property = 'owner'
      gantt.config.order_branch = true
      gantt.config.open_tree_initially = true
      gantt.config.show_errors = false // å‘生异常时,不允许弹出警告到 UI ç•Œé¢
      const resourcesStore = gantt.createDatastore({
        name: gantt.config.resource_store,
        type: 'treeDatastore',
        initItem: function(item) {
          item.parent = item.parent || gantt.config.root_id
          item[gantt.config.resource_property] = item.parent
          item.open = true
          return item
        }
      })
      resourcesStore.attachEvent('onParse', function() {
        const people = []
        resourcesStore.eachItem(function(res) {
          if (!resourcesStore.hasChild(res.id)) {
            const copy = gantt.copy(res)
            copy.key = res.id
            copy.label = res.text
            people.push(copy)
          }
        })
        gantt.updateCollection('people', people)
      })
      resourcesStore.parse([
        { id: 1, text: 'QA', parent: null },
        { id: 2, text: 'Development', parent: null },
        { id: 3, text: 'Sales', parent: null },
        { id: 4, text: 'Other', parent: null },
        { id: 5, text: 'Unassigned', parent: 4 },
        { id: 6, text: 'John', parent: 1, unit: 'hours/day' },
        { id: 7, text: 'Mike', parent: 2, unit: 'hours/day' },
        { id: 8, text: 'Anna', parent: 2, unit: 'hours/day' },
        { id: 9, text: 'Bill', parent: 3, unit: 'hours/day' },
        { id: 10, text: 'Floe', parent: 3, unit: 'hours/day' }
      ])
      /* â†‘↑↑ Resource configuration â†‘↑↑ */
      gantt.config.grid_elastic_columns = true
      // è‡ªå®šä¹‰æµ®åŠ¨æ¡†çš„æ˜¾ç¤ºå†…å®¹   tooltip浮动框显示的End Date被追加1的问题修复(应该显示数据库的原始值)
      gantt.templates.tooltip_text = function(start, end, task) {
        return '<b>任务:</b> ' + task.text + '<br/><b>开始时间:</b> ' + `${gantt.date.date_to_str('%Y-%m-%d')(start)}` + '<br/><b>结束时间:</b> ' + handleDateReduceOneDay(end)
      }
      gantt.init('gantt_here')
      gantt.parse({
        'data': [
          {
            'id': 1,
            'text': '项目1',
            'type': 'project',
            'start_date': '2025-04-02 00:00',
            'end_date': '2025-04-07 00:00',
            'duration': 5,
            'progress': 0.4,
            'owner': [{ 'resource_id': '5', 'value': 3 }],
            'parent': 0,
            'checked': false
          },
          {
            'id': 2,
            'text': '项目2',
            'type': 'project',
            'start_date': '02-04-2025 00:00',
            'duration': 8,
            'progress': 0.6,
            'owner': [{ 'resource_id': '5', 'value': 4 }],
            'parent': '1',
            checked: false
          },
          {
            'id': 3,
            'text': '项目3',
            'type': 'project',
            'start_date': '11-04-2025 00:00',
            'duration': 8,
            'parent': '1',
            'progress': 0.6,
            'owner': [{ 'resource_id': '5', 'value': 2 }],
            checked: false
          },
          {
            'id': 4,
            'text': '项目4',
            'type': 'project',
            'start_date': '13-04-2025 00:00',
            'duration': 5,
            'parent': '1',
            'progress': 0.5,
            'owner': [{ 'resource_id': '5', 'value': 4 }],
            'priority': 3,
            checked: true
          },
          {
            'id': 5,
            'text': '任务5',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 7,
            'parent': '2',
            'progress': 0.6,
            'owner': [{ 'resource_id': '6', 'value': 5 }],
            'priority': 1,
            checked: true
          },
          {
            'id': 6,
            'text': '任务6',
            'type': 'task',
            'calendar_id': 'custom1',
            'start_date': '03-04-2025 12:00',
            'duration': 7,
            'parent': '2',
            'progress': 0.6,
            'owner': [{ 'resource_id': '7', 'value': 1 }],
            'priority': 2,
            checked: false
          },
          {
            'id': 7,
            'text': '任务7',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '12-04-2025 00:00',
            'duration': 8,
            'parent': '3',
            'progress': 0.6,
            'owner': [{ 'resource_id': '10', 'value': 2 }],
            checked: false
          },
          {
            'id': 8,
            'text': '任务8',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '14-04-2025 00:00',
            'duration': 5,
            'parent': '4',
            'progress': 0.5,
            'owner': [{ 'resource_id': '10', 'value': 4 }, { 'resource_id': '9', 'value': 5 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 9,
            'text': '任务9',
            'type': 'task',
            'start_date': '21-04-2025 00:00',
            'duration': 4,
            'parent': '4',
            'progress': 0.5,
            'owner': [{ 'resource_id': '7', 'value': 3 }],
            checked: false
          },
          {
            'id': 10,
            'text': '任务10',
            'type': 'task',
            'start_date': '27-04-2025 00:00',
            'duration': 3,
            'parent': '4',
            'progress': 0.5,
            'owner': [{ 'resource_id': '8', 'value': 5 }],
            'priority': 2,
            checked: false
          },
          {
            'id': 11,
            'text': '项目11',
            'type': 'project',
            'progress': 0.6,
            'start_date': '02-04-2025 00:00',
            'duration': 13,
            'owner': [{ 'resource_id': '5', 'value': 4 }],
            'parent': 0,
            checked: false
          },
          {
            'id': 12,
            'text': '任务12',
            'calendar_id': 'custom2',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
            'parent': '11',
            'progress': 1,
            'owner': [{ 'resource_id': '7', 'value': 6 }],
            checked: false
          },
          {
            'id': 13,
            'text': '项目13',
            'type': 'project',
            'start_date': '03-04-2025 00:00',
            'duration': 11,
            'parent': '11',
            'progress': 0.5,
            'owner': [{ 'resource_id': '5', 'value': 2 }],
            checked: false
          },
          {
            'id': 14,
            'text': '任务14',
            'calendar_id': 'custom2',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 6,
            'parent': '11',
            'owner': [],
            'progress': 0.8,
            checked: false
          },
          {
            'id': 15,
            'text': '项目15',
            'type': 'project',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
            'parent': '11',
            'progress': 0.2,
            'owner': [{ 'resource_id': '5', 'value': 5 }],
            checked: false
          },
          {
            'id': 16,
            'text': '任务16',
            'calendar_id': 'custom2',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 7,
            'parent': '11',
            'progress': 0,
            'owner': [{ 'resource_id': '7', 'value': 2 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 17,
            'text': '任务17',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 2,
            'parent': '13',
            'progress': 1,
            'owner': [{ 'resource_id': '8', 'value': 1 }],
            'priority': 2,
            checked: false
          },
          {
            'id': 25,
            'text': '任务18',
            'type': 'task',
            'start_date': '06-04-2025 00:00',
            'parent': '13',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 1 }],
            'duration': 2,
            checked: false
          },
          {
            'id': 18,
            'text': '任务19',
            'type': 'task',
            'start_date': '10-04-2025 00:00',
            'duration': 2,
            'parent': '13',
            'progress': 0.8,
            'owner': [{ 'resource_id': '6', 'value': 2 }],
            'priority': 3,
            checked: false
          },
          {
            'id': 19,
            'text': '任务20',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '13-04-2025 00:00',
            'duration': 4,
            'parent': '13',
            'progress': 0.2,
            'owner': [{ 'resource_id': '6', 'value': 3 }],
            checked: false
          },
          {
            'id': 20,
            'text': '任务21',
            'type': 'task',
            'start_date': '13-04-2025 00:00',
            'duration': 4,
            'parent': '13',
            'progress': 0,
            'owner': [{ 'resource_id': '8', 'value': 4 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 21,
            'text': '任务22',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 4,
            'parent': '15',
            'progress': 0.5,
            'owner': [{ 'resource_id': '6', 'value': 5 }],
            checked: false
          },
          {
            'id': 22,
            'text': '任务23',
            'calendar_id': 'custom1',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 4,
            'parent': '15',
            'progress': 0.1,
            'owner': [{ 'resource_id': '8', 'value': 3 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 23,
            'text': '任务24',
            'type': 'task',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
            'parent': '15',
            'progress': 0,
            'owner': [{ 'resource_id': '8', 'value': 5 }],
            'priority': 1,
            checked: false
          },
          {
            'id': 24,
            'text': '任务25',
            // 'type': 'milestone',
            'type': 'task',
            'start_date': '20-04-2025 00:00',
            'parent': '11',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 3 }],
            'duration': 2,
            checked: false
          }
        ]
      })
      // ç»‘定甘特图点击事件(官方推荐的事件委托用法)<span data-allow-html class='source-item source-aggregated' data-group-key='source-group-2' data-url='https://juejin.cn/post/7352376280387764278' data-id='turn0fetch0'><span data-allow-html class='source-item-num' data-group-key='source-group-2' data-id='turn0fetch0' data-url='https://juejin.cn/post/7352376280387764278'><span class='source-item-num-name' data-allow-html>https://juejin.cn/post/7352376280387764278</span></span></span>
      gantt.attachEvent('onTaskClick', (id, e) => {
        // æ‰¾åˆ°ç‚¹å‡»çš„æ˜¯å¦æ˜¯å¤é€‰æ¡†
        const checkbox = e.target.closest('[data-action="check-row"]')
        if (!checkbox) {
          // ä¸æ˜¯ç‚¹å¤é€‰æ¡†ï¼Œå°±ä¿æŒé»˜è®¤è¡Œä¸º
          return true
        }
        // æ˜¯å¤é€‰æ¡†ï¼šåˆ‡æ¢é€‰ä¸­çŠ¶æ€
        const task = gantt.getTask(id)
        if (task) {
          task.checked = !task.checked
          gantt.updateTask(id) // åªåˆ·æ–°è¿™ä¸€è¡Œï¼Œæ€§èƒ½æ›´å¥½
          this.syncSelected() // åŒæ­¥åˆ° Vue çš„ selectedIds
        }
        // é˜»æ­¢é»˜è®¤ç‚¹å‡»è¡Œè¡Œä¸ºï¼ˆé¿å…è¯¯è§¦å‘其他逻辑)
        return false
      })
      // åˆå§‹åŒ–完成后同步一次选中状态
      this.syncSelected()
    },
    ganttDateRangeChange(val) {
      gantt.config.start_date = val[0]
      gantt.config.end_date = val[1]
      gantt.render()
    },
    ganttUndo() {
      gantt.undo()
    },
    ganttRedo() {
      gantt.redo()
    },
    ganttZoomIn() {
      gantt.ext.zoom.zoomIn()
    },
    ganttZoomOut() {
      gantt.ext.zoom.zoomOut()
    },
    // ä»Žç”˜ç‰¹å›¾ä¸­åŒæ­¥é€‰ä¸­çš„ id åˆ° Vue data
    syncSelected() {
      const tasks = gantt.serialize().data || []
      this.selectedIds = tasks.filter(t => t.checked).map(t => t.id)
      console.log(this.selectedIds)
    },
    // èŽ·å–é€‰ä¸­ä»»åŠ¡ï¼ˆç¤ºä¾‹ï¼‰
    handleGetSelected() {
      const tasks = gantt.serialize().data || []
      const selected = tasks.filter(t => t.checked)
      this.$notify.success(`当前已选中${selected.length} æ¡ä»»åŠ¡`)
    },
    // æ¸…空所有选择
    handleClearSelection() {
      // èŽ·å–æ‰€æœ‰ä»»åŠ¡
      const tasks = gantt.serialize().data || []
      // éåŽ†æ‰€æœ‰ä»»åŠ¡ï¼Œå°† checked å±žæ€§è®¾ç½®ä¸º false
      tasks.forEach(task => {
        task.checked = false
      })
      // æ›´æ–°æ‰€æœ‰ä»»åŠ¡æ˜¾ç¤º
      gantt.eachTask((task) => {
        task.checked = false
        gantt.updateTask(task.id)
      })
      // åŒæ­¥åˆ° Vue ç»„件数据
      this.syncSelected()
      // æ˜¾ç¤ºæç¤ºä¿¡æ¯
      this.$notify.success('已清空所有选择')
    }
  }
}
</script>
<style>
body,
html {
  width: 100%;
  height: 100%;
  margin: unset;
}
.local_storage {
  background: lavender;
  border: 2px dotted orange;
  font-weight: bold;
}
.gantt_grid_scale .gantt_grid_head_cell,
.gantt_task .gantt_task_scale .gantt_scale_cell {
  font-weight: bold;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.7);
}
.resource_marker {
  text-align: center;
}
.resource_marker div {
  width: 28px;
  height: 28px;
  line-height: 29px;
  display: inline-block;
  border-radius: 15px;
  color: #FFF;
  margin: 3px;
}
.resource_marker.workday_ok div {
  background: #51c185;
}
.resource_marker.workday_over div {
  background: #ff8686;
}
.owner-label {
  width: 20px;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
  display: inline-block;
  border: 1px solid #cccccc;
  border-radius: 25px;
  background: #e6e6e6;
  color: #6f6f6f;
  margin: 0 3px;
  font-weight: bold;
}
.weekend {
  background: LightGoldenrodYellow;
}
.constraint-marker {
  position: absolute;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  width: 56px;
  height: 56px;
  margin-top: -11px;
  opacity: 0.4;
  z-index: 1;
  background: url("https://docs.dhtmlx.com/gantt/samples/common/constraint-arrow.svg") center no-repeat;
  background-size: cover;
}
.constraint-marker.earliest-start {
  margin-left: -53px;
}
.constraint-marker.latest-end {
  margin-left: -3px;
  transform: rotate(180deg);
}
.taskCheckBox {
  cursor: pointer;
  z-index: 99999 !important;
}
</style>
src/views/gantt/¸ÊÌØÍ¼´ø·ÖÒ³.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,921 @@
<template>
  <div style="padding: 0 10px">
    <div style="padding: 10px 0;display: flex;">
      <el-button type="primary" size="mini" @click="ganttUndo">回退拖动操作</el-button>
      <el-button type="primary" size="mini" @click="ganttRedo">前进拖动操作</el-button>
      <el-button type="primary" size="mini" @click="ganttZoomIn">放大</el-button>
      <el-button type="primary" size="mini" @click="ganttZoomOut">缩小</el-button>
      <el-date-picker
        v-model="ganttDateRange"
        style="margin-left: 10px;"
        size="mini"
        value-format="yyyy-MM-dd"
        type="daterange"
        :clearable="false"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        @change="ganttDateRangeChange"
      />
      <el-button type="primary" style="margin-left: 10px;" size="mini" @click="handleGetSelected">
        èŽ·å–å¤é€‰æ¡†é€‰ä¸­ä»»åŠ¡
      </el-button>
      <el-button size="mini" @click="handleClearSelection">
        æ¸…空复选框选择
      </el-button>
    </div>
    <div id="gantt_here" style="width:100%; height:calc(90vh - 50px);" />
    <!-- åˆ†é¡µç»„ä»¶ -->
    <div class="pagination-container">
      <el-pagination
        :current-page="currentPage"
        :page-sizes="[10, 20, 50, 100]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="totalTasks"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>
<script>
import { gantt } from '@/components/dhtmlxGantt'
import '@/components/dhtmlxGantt/codebase/dhtmlxgantt.css'
import { handleDateReduceOneDay, handleDatetime } from '@/utils/global'
export default {
  data() {
    return {
      value: 'default',
      ganttDateRange: ['2025-04-01', '2025-05-10'],
      selectedIds: [],
      // åˆ†é¡µç›¸å…³æ•°æ®
      currentPage: 1,
      pageSize: 10,
      totalTasks: 0,
      allTasks: [], // å­˜å‚¨æ‰€æœ‰ä»»åŠ¡æ•°æ®
      paginatedTasks: [] // å½“前页的任务数据
    }
  },
  mounted() {
    // å…ˆæ”¹å˜æ—¥æœŸèŒƒå›´é…ç½®
    this.ganttDateRangeChange(this.ganttDateRange)
    // åˆå§‹åŒ–甘特图配置
    this.initGantt()
    // ç„¶åŽåŠ è½½ä»»åŠ¡æ•°æ®ï¼ˆä¼šè‡ªåŠ¨æ¸²æŸ“å½“å‰é¡µï¼‰
    this.loadTasks()
  },
  methods: {
    initGantt() {
      gantt.plugins({
        auto_scheduling: true,
        critical_path: true,
        drag_timeline: true,
        grouping: true,
        keyboard_navigation: true,
        marker: true,
        multiselect: true,
        tooltip: true,
        undo: true
      })
      gantt.i18n.setLocale('cn')
      gantt.config.multiselect = true // å¼€å¯å¤šä»»åŠ¡é€‰æ‹©
      gantt.config.show_links = false // ä¸æ˜¾ç¤ºè¿žæŽ¥çº¿
      /* â†“↓↓ Auto-scheduling configuration â†“↓↓ */
      gantt.config.auto_scheduling = true
      /* â†“↓↓ Group configuration â†“↓↓ */
      gantt.serverList('task_priority', [
        { key: 1, label: '高' },
        { key: 2, label: '中等' },
        { key: 3, label: '低' }
      ])
      gantt.serverList('task_status', [
        { key: 1, label: 'Planning' },
        { key: 2, label: 'Not started' },
        { key: 3, label: 'In Progress' },
        { key: 4, label: 'Complete' }
      ])
      function byId(list, id) {
        for (let i = 0; i < list.length; i++) {
          if (list[i].key == id) {
            return list[i].label || ''
          }
        }
        return ''
      }
      /* â†‘↑↑ Group configuration â†‘↑↑ */
      // æ”¾å¤§ç¼©å°å±žæ€§
      /* â†“↓↓ Zoom configuration â†“↓↓ */
      const zoomConfig = {
        levels: [
          {
            name: 'hour',
            scale_height: 27,
            min_column_width: 50,
            scales: [
              { unit: 'day', format: '%Yå¹´%M%d号' },
              { unit: 'hour', format: '%H时' }
            ]
          },
          {
            name: 'day',
            scale_height: 27,
            min_column_width: 80,
            scales: [
              // { unit: 'day', step: 1, format: '%d %M' }
              { unit: 'day', step: 1, format: '%M%d号' }
            ]
          },
          {
            name: 'week',
            scale_height: 50,
            min_column_width: 70,
            scales: [
              // {
              //   unit: 'week', step: 1, format: function(date) {
              //     const dateToStr = gantt.date.date_to_str('%d %M')
              //     const endDate = gantt.date.add(date, -6, 'day')
              //     const weekNum = gantt.date.date_to_str('%W')(date)
              //     return '第' + weekNum + '周, ' + dateToStr(date) + ' - ' + dateToStr(endDate)
              //   }
              // },
              // %M
              { unit: 'week', format: '%Y年第%W周' },
              { unit: 'day', step: 1, format: '%M%d号' }
              // { unit: 'day', step: 1, format: '%j %D' }
              // { unit: 'day', step: 1, format: '星期%D' }
            ]
          },
          {
            name: 'month',
            scale_height: 50,
            min_column_width: 120,
            scales: [
              // { unit: 'month', format: '%Yå¹´%F' },
              { unit: 'month', format: '%Yå¹´%M' },
              { unit: 'week', format: '第%W周' }
            ]
          },
          {
            name: 'quarter',
            height: 50,
            min_column_width: 90,
            scales: [
              // {
              //   unit: 'quarter', step: 1, format: function(date) {
              //     const dateToStr = gantt.date.date_to_str('%M')
              //     const endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day')
              //     return dateToStr(date) + ' - ' + dateToStr(endDate)
              //   }
              // },
              { unit: 'month', step: 1, format: '%Yå¹´%M' }
            ]
          },
          {
            name: 'year',
            scale_height: 50,
            min_column_width: 30,
            scales: [
              { unit: 'year', step: 1, format: '%Yå¹´' }
            ]
          }
        ],
        useKey: 'ctrlKey',
        trigger: 'wheel',
        element: function() {
          return gantt.$root.querySelector('.gantt_task')
        }
      }
      gantt.ext.zoom.init(zoomConfig)
      gantt.ext.zoom.setLevel('week')
      /* â†‘↑↑ Zoom configuration â†‘↑↑ */
      // æ˜¯å¦æ˜¯å·¥ä½œæ—¶é—´
      /* â†“↓↓ Working Time configuration â†“↓↓ */
      gantt.templates.scale_cell_class = function(date) {
        if (!gantt.isWorkTime(date)) {
          return 'weekend'
        }
      }
      gantt.templates.timeline_cell_class = function(item, date) {
        if (!gantt.isWorkTime({ date: date, task: item })) {
          return 'weekend'
        }
      }
      gantt.config.work_time = true
      gantt.addCalendar({
        id: 'custom1',
        worktime: {
          hours: ['8:00-12:30', '13:00-17:30'], // global work hours for weekdays
          days: [0, 1, 1, 1, 1, 1, 1]
        }
      })
      // gantt.addCalendar({
      //   id: 'custom2',
      //   hours: ['12:00-21:00'],
      //   days: [1, 0, 1, 0, 1, 0, 1]
      // })
      /* â†‘↑↑ Working Time configuration â†‘↑↑ */
      // ç”˜ç‰¹å›¾åˆ—参数设置
      /* â†“↓↓ Grid Columns configuration â†“↓↓ */
      gantt.config.reorder_grid_columns = true
      const textEditor = { type: 'text', map_to: 'text' }
      const startDateEditor = { type: 'date', map_to: 'start_date' }
      const durationEditor = { type: 'number', map_to: 'duration', min: 1, max: 100 }
      gantt.config.columns = [
        {
          name: 'checked',
          label: '选择',
          align: 'center',
          width: 35,
          resize: false,
          // å…³é”®ï¼šç”¨ template è¿”回一个复选框
          template: (task) => {
            const checked = task.checked ? 'checked' : ''
            // data-action ç”¨äºŽåœ¨äº‹ä»¶å§”托时识别是复选框
            return `<input type="checkbox" class="taskCheckBox" data-action="check-row" ${checked} />`
          }
        },
        { name: 'wbs', label: '节点', width: 80, template: gantt.getWBSCode },
        { name: 'text', tree: true, label: '任务名称', width: 200, resize: true, editor: textEditor },
        { name: 'start_date', align: 'center', label: '开始日期', width: 80, resize: true, editor: startDateEditor },
        {
          name: 'duration',
          width: 60,
          align: 'center',
          label: '工期(天)',
          resize: true,
          editor: durationEditor,
          template: function(task) {
            // å¦‚æžœduration是null或undefined,返回0
            // return (task.duration || 0) + 1 // åœ¨å½“前duration的基础上加1
            return task.duration
          }
        },
        {
          name: 'owner', align: 'center', width: 75, label: '负责人', template: function(task) {
            // if (task.type == gantt.config.types.project) {
            //   return ''
            // }
            //
            // const store = gantt.getDatastore('resource')
            // const assignments = task[gantt.config.resource_property]
            //
            // if (!assignments || !assignments.length) {
            //   return 'Unassigned'
            // }
            //
            // if (assignments.length === 1) {
            //   return store.getItem(assignments[0].resource_id).text
            // }
            //
            // let result = ''
            // assignments.forEach(function(assignment) {
            //   const owner = store.getItem(assignment.resource_id)
            //   if (!owner) {
            //     return
            //   }
            //   result += '<div class=\'owner-label\' title=\'' + owner.text + '\'>' + owner.text.substr(0, 1) + '</div>'
            // })
            //
            // return result
            return '张三'
          },
          resize: true
        },
        {
          name: 'priority', width: 60, label: '优先级', align: 'center', resize: true, template: function(task) {
            return byId(gantt.serverList('task_priority'), task.priority)
          }
        },
        { name: 'add', width: 44 }
      ]
      /* â†‘↑↑ Grid Columns configuration â†‘↑↑ */
      // æ±‰åŒ–窗口
      gantt.locale.labels = {
        dhx_cal_today_button: '今天',
        day_tab: '日',
        week_tab: '周',
        month_tab: '月',
        new_event: '新建日程',
        icon_save: '保存',
        icon_cancel: '关闭',
        icon_details: '详细',
        icon_edit: '编辑',
        icon_delete: '删除',
        confirm_closing: '请确认是否撤销修改!', // Your changes will be lost, are your sure?
        confirm_deleting: '是否删除计划?',
        section_description: '描述:',
        section_resources: '自定义选择:',
        section_calendar: '自定义选择2:',
        section_time: '时间范围:',
        section_type: '类型:',
        section_text: '计划名称:',
        section_test: '测试:',
        section_projectClass: '项目类型:',
        taskProjectType_0: '项目任务',
        taskProjectType_1: '普通任务',
        section_head: '负责人:',
        section_priority: '优先级:',
        taskProgress: '任务状态',
        taskProgress_0: '未开始',
        taskProgress_1: '进行中',
        taskProgress_2: '已完成',
        taskProgress_3: '已延期',
        taskProgress_4: '搁置中',
        section_template: 'Details',
        /* grid columns */
        column_text: '计划名称',
        column_start_date: '开始时间',
        column_duration: '持续时间',
        column_add: '',
        column_priority: '难度',
        /* link confirmation */
        link: '关联',
        confirm_link_deleting: '将被删除',
        message_ok: '确定',
        message_cancel: '取消',
        link_start: ' (开始)',
        link_end: ' (结束)',
        type_task: '任务',
        type_project: '项目',
        type_milestone: '里程碑',
        minutes: '分钟',
        hours: '小时',
        days: '天',
        weeks: '周',
        months: '月',
        years: 'å¹´'
      }
      gantt.config.lightbox.sections = [
        { name: 'description', height: 38, map_to: 'text', type: 'textarea', focus: true },
        {
          name: 'resources', type: 'resources', map_to: 'owner', options: gantt.serverList('people'), default_value: 8
        },
        {
          name: 'calendar', height: 25, map_to: 'calendar_id', type: 'select', options: [
            { key: '', label: '默认' },
            { key: 'custom1', label: '选项一' },
            { key: 'custom2', label: '选项二' }
          ]
        },
        { name: 'time', type: 'duration', map_to: 'auto' }
      ]
      gantt.config.resource_store = 'resource'
      gantt.config.resource_property = 'owner'
      gantt.config.order_branch = true
      gantt.config.open_tree_initially = true
      gantt.config.show_errors = false // å‘生异常时,不允许弹出警告到 UI ç•Œé¢
      const resourcesStore = gantt.createDatastore({
        name: gantt.config.resource_store,
        type: 'treeDatastore',
        initItem: function(item) {
          item.parent = item.parent || gantt.config.root_id
          item[gantt.config.resource_property] = item.parent
          item.open = true
          return item
        }
      })
      resourcesStore.attachEvent('onParse', function() {
        const people = []
        resourcesStore.eachItem(function(res) {
          if (!resourcesStore.hasChild(res.id)) {
            const copy = gantt.copy(res)
            copy.key = res.id
            copy.label = res.text
            people.push(copy)
          }
        })
        gantt.updateCollection('people', people)
      })
      resourcesStore.parse([
        { id: 1, text: 'QA', parent: null },
        { id: 2, text: 'Development', parent: null },
        { id: 3, text: 'Sales', parent: null },
        { id: 4, text: 'Other', parent: null },
        { id: 5, text: 'Unassigned', parent: 4 },
        { id: 6, text: 'John', parent: 1, unit: 'hours/day' },
        { id: 7, text: 'Mike', parent: 2, unit: 'hours/day' },
        { id: 8, text: 'Anna', parent: 2, unit: 'hours/day' },
        { id: 9, text: 'Bill', parent: 3, unit: 'hours/day' },
        { id: 10, text: 'Floe', parent: 3, unit: 'hours/day' }
      ])
      /* â†‘↑↑ Resource configuration â†‘↑↑ */
      gantt.config.grid_elastic_columns = true
      // è‡ªå®šä¹‰æµ®åŠ¨æ¡†çš„æ˜¾ç¤ºå†…å®¹   tooltip浮动框显示的End Date被追加1的问题修复(应该显示数据库的原始值)
      gantt.templates.tooltip_text = function(start, end, task) {
        return '<b>任务:</b> ' + task.text + '<br/><b>开始时间:</b> ' + `${gantt.date.date_to_str('%Y-%m-%d')(start)}` + '<br/><b>结束时间:</b> ' + handleDateReduceOneDay(end)
      }
      gantt.init('gantt_here')
      // æ³¨æ„ï¼šè¿™é‡Œä¸ç«‹å³åŠ è½½æ•°æ®ï¼Œè€Œæ˜¯ç­‰å¾…loadTasks被调用
      // ç»‘定甘特图点击事件(官方推荐的事件委托用法)
      gantt.attachEvent('onTaskClick', (id, e) => {
        // æ‰¾åˆ°ç‚¹å‡»çš„æ˜¯å¦æ˜¯å¤é€‰æ¡†
        const checkbox = e.target.closest('[data-action="check-row"]')
        if (!checkbox) {
          // ä¸æ˜¯ç‚¹å¤é€‰æ¡†ï¼Œå°±ä¿æŒé»˜è®¤è¡Œä¸º
          return true
        }
        // æ˜¯å¤é€‰æ¡†ï¼šåˆ‡æ¢é€‰ä¸­çŠ¶æ€
        const task = gantt.getTask(id)
        if (task) {
          task.checked = !task.checked
          // åŒæ—¶æ›´æ–°å…¨å±€æ•°æ®
          const globalTask = this.allTasks.find(t => t.id === id)
          if (globalTask) {
            globalTask.checked = task.checked
          }
          gantt.updateTask(id) // åªåˆ·æ–°è¿™ä¸€è¡Œï¼Œæ€§èƒ½æ›´å¥½
          this.syncSelected() // åŒæ­¥åˆ° Vue çš„ selectedIds
        }
        // é˜»æ­¢é»˜è®¤ç‚¹å‡»è¡Œè¡Œä¸ºï¼ˆé¿å…è¯¯è§¦å‘其他逻辑)
        return false
      })
      // åˆå§‹åŒ–完成后同步一次选中状态
      this.syncSelected()
    },
    // åŠ è½½ä»»åŠ¡æ•°æ®
    loadTasks() {
      // ä½¿ç”¨åŽŸæœ‰çš„ç¤ºä¾‹æ•°æ®ä½œä¸ºåŸºç¡€
      this.allTasks = [
        {
          'id': 1,
          'text': '项目1',
          'type': 'project',
          'start_date': '2025-04-02 00:00',
          'end_date': '2025-04-07 00:00',
          'duration': 5,
          'progress': 0.4,
          'owner': [{ 'resource_id': '5', 'value': 3 }],
          'parent': 0,
          'checked': false
        },
        {
          'id': 2,
          'text': '项目2',
          'type': 'project',
          'start_date': '02-04-2025 00:00',
          'duration': 8,
          'progress': 0.6,
          'owner': [{ 'resource_id': '5', 'value': 4 }],
          'parent': '1',
          checked: false
        },
        {
          'id': 3,
          'text': '项目3',
          'type': 'project',
          'start_date': '11-04-2025 00:00',
          'duration': 8,
          'parent': '1',
          'progress': 0.6,
          'owner': [{ 'resource_id': '5', 'value': 2 }],
          checked: false
        },
        {
          'id': 4,
          'text': '项目4',
          'type': 'project',
          'start_date': '13-04-2025 00:00',
          'duration': 5,
          'parent': '1',
          'progress': 0.5,
          'owner': [{ 'resource_id': '5', 'value': 4 }],
          'priority': 3,
          checked: true
        },
        {
          'id': 5,
          'text': '任务5',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 7,
          'parent': '2',
          'progress': 0.6,
          'owner': [{ 'resource_id': '6', 'value': 5 }],
          'priority': 1,
          checked: true
        },
        {
          'id': 6,
          'text': '任务6',
          'type': 'task',
          'calendar_id': 'custom1',
          'start_date': '03-04-2025 12:00',
          'duration': 7,
          'parent': '2',
          'progress': 0.6,
          'owner': [{ 'resource_id': '7', 'value': 1 }],
          'priority': 2,
          checked: false
        },
        {
          'id': 7,
          'text': '任务7',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '12-04-2025 00:00',
          'duration': 8,
          'parent': '3',
          'progress': 0.6,
          'owner': [{ 'resource_id': '10', 'value': 2 }],
          checked: false
        },
        {
          'id': 8,
          'text': '任务8',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '14-04-2025 00:00',
          'duration': 5,
          'parent': '4',
          'progress': 0.5,
          'owner': [{ 'resource_id': '10', 'value': 4 }, { 'resource_id': '9', 'value': 5 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 9,
          'text': '任务9',
          'type': 'task',
          'start_date': '21-04-2025 00:00',
          'duration': 4,
          'parent': '4',
          'progress': 0.5,
          'owner': [{ 'resource_id': '7', 'value': 3 }],
          checked: false
        },
        {
          'id': 10,
          'text': '任务10',
          'type': 'task',
          'start_date': '27-04-2025 00:00',
          'duration': 3,
          'parent': '4',
          'progress': 0.5,
          'owner': [{ 'resource_id': '8', 'value': 5 }],
          'priority': 2,
          checked: false
        },
        {
          'id': 11,
          'text': '项目11',
          'type': 'project',
          'progress': 0.6,
          'start_date': '02-04-2025 00:00',
          'duration': 13,
          'owner': [{ 'resource_id': '5', 'value': 4 }],
          'parent': 0,
          checked: false
        },
        {
          'id': 12,
          'text': '任务12',
          'calendar_id': 'custom2',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 5,
          'parent': '11',
          'progress': 1,
          'owner': [{ 'resource_id': '7', 'value': 6 }],
          checked: false
        },
        {
          'id': 13,
          'text': '项目13',
          'type': 'project',
          'start_date': '03-04-2025 00:00',
          'duration': 11,
          'parent': '11',
          'progress': 0.5,
          'owner': [{ 'resource_id': '5', 'value': 2 }],
          checked: false
        },
        {
          'id': 14,
          'text': '任务14',
          'calendar_id': 'custom2',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 6,
          'parent': '11',
          'owner': [],
          'progress': 0.8,
          checked: false
        },
        {
          'id': 15,
          'text': '项目15',
          'type': 'project',
          'start_date': '03-04-2025 00:00',
          'duration': 5,
          'parent': '11',
          'progress': 0.2,
          'owner': [{ 'resource_id': '5', 'value': 5 }],
          checked: false
        },
        {
          'id': 16,
          'text': '任务16',
          'calendar_id': 'custom2',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 7,
          'parent': '11',
          'progress': 0,
          'owner': [{ 'resource_id': '7', 'value': 2 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 17,
          'text': '任务17',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 2,
          'parent': '13',
          'progress': 1,
          'owner': [{ 'resource_id': '8', 'value': 1 }],
          'priority': 2,
          checked: false
        },
        {
          'id': 18,
          'text': '任务19',
          'type': 'task',
          'start_date': '10-04-2025 00:00',
          'duration': 2,
          'parent': '13',
          'progress': 0.8,
          'owner': [{ 'resource_id': '6', 'value': 2 }],
          'priority': 3,
          checked: false
        },
        {
          'id': 19,
          'text': '任务20',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '13-04-2025 00:00',
          'duration': 4,
          'parent': '13',
          'progress': 0.2,
          'owner': [{ 'resource_id': '6', 'value': 3 }],
          checked: false
        },
        {
          'id': 20,
          'text': '任务21',
          'type': 'task',
          'start_date': '13-04-2025 00:00',
          'duration': 4,
          'parent': '13',
          'progress': 0,
          'owner': [{ 'resource_id': '8', 'value': 4 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 21,
          'text': '任务22',
          'type': 'project',
          'start_date': '03-04-2025 00:00',
          'duration': 4,
          'parent': '0',
          'progress': 0.5,
          'owner': [{ 'resource_id': '6', 'value': 5 }],
          checked: false
        },
        {
          'id': 22,
          'text': '任务23',
          'calendar_id': 'custom1',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 4,
          'parent': '21',
          'progress': 0.1,
          'owner': [{ 'resource_id': '8', 'value': 3 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 23,
          'text': '任务24',
          'type': 'task',
          'start_date': '03-04-2025 00:00',
          'duration': 5,
          'parent': '21',
          'progress': 0,
          'owner': [{ 'resource_id': '8', 'value': 5 }],
          'priority': 1,
          checked: false
        },
        {
          'id': 24,
          'text': '任务25',
          // 'type': 'milestone',
          'type': 'task',
          'start_date': '20-04-2025 00:00',
          'parent': '21',
          'progress': 0,
          'owner': [{ 'resource_id': '5', 'value': 3 }],
          'duration': 2,
          checked: false
        }
      ]
      // æ·»åŠ æ›´å¤šç¤ºä¾‹æ•°æ®ï¼Œä½¿åˆ†é¡µæ•ˆæžœæ›´æ˜Žæ˜¾
      // for (let i = 25; i <= 100; i++) {
      //   this.allTasks.push({
      //     'id': i,
      //     'text': '任务' + i,
      //     'type': 'task',
      //     // 'start_date': handleDatetime(`0${Math.floor(Math.random() * 9 + 1)}-04-2025 00:00`),
      //     'start_date': `0${Math.floor(Math.random() * 9 + 1)}-04-2025 00:00`,
      //     'duration': Math.floor(Math.random() * 5) + 1,
      //     'parent': Math.floor(Math.random() * 10) + 1,
      //     'progress': Math.random(),
      //     'owner': [{ 'resource_id': '5', 'value': 3 }],
      //     'priority': Math.floor(Math.random() * 3) + 1,
      //     'checked': false
      //   })
      // }
      console.log(JSON.parse(JSON.stringify(this.allTasks)))
      this.totalTasks = this.allTasks.length
      this.updatePaginatedTasks()
      this.renderGanttChart()
    },
    // æ›´æ–°åˆ†é¡µåŽçš„任务数据
    updatePaginatedTasks() {
      const startIndex = (this.currentPage - 1) * this.pageSize
      const endIndex = Math.min(startIndex + this.pageSize, this.allTasks.length)
      this.paginatedTasks = this.allTasks.slice(startIndex, endIndex)
    },
    // æ¸²æŸ“甘特图
    renderGanttChart() {
      gantt.clearAll()
      console.log(JSON.parse(JSON.stringify(this.paginatedTasks)))
      gantt.parse({
        'data': this.paginatedTasks
      })
      // ç¡®ä¿ç”˜ç‰¹å›¾é‡æ–°æ¸²æŸ“
      // gantt.render()
    },
    // é¡µå¤§å°æ”¹å˜
    handleSizeChange(newSize) {
      this.pageSize = newSize
      this.currentPage = 1 // é‡ç½®åˆ°ç¬¬ä¸€é¡µ
      this.updatePaginatedTasks()
      this.renderGanttChart()
      this.syncSelected()
    },
    // å½“前页改变
    handleCurrentChange(newPage) {
      // è®¡ç®—最大页数,防止超出范围
      const maxPage = Math.ceil(this.totalTasks / this.pageSize)
      if (newPage > maxPage) {
        this.currentPage = maxPage
      } else if (newPage < 1) {
        this.currentPage = 1
      } else {
        this.currentPage = newPage
      }
      this.updatePaginatedTasks()
      this.renderGanttChart()
      this.syncSelected()
    },
    // ç”˜ç‰¹å›¾æ—¥æœŸæ”¹å˜
    ganttDateRangeChange(val) {
      gantt.config.start_date = val[0]
      gantt.config.end_date = val[1]
      gantt.render()
    },
    // å›žé€€æ‹–动操作
    ganttUndo() {
      gantt.undo()
    },
    // å‰è¿›æ‹–动操作
    ganttRedo() {
      gantt.redo()
    },
    // æ”¾å¤§
    ganttZoomIn() {
      gantt.ext.zoom.zoomIn()
    },
    // ç¼©å°
    ganttZoomOut() {
      gantt.ext.zoom.zoomOut()
    },
    // ä»Žç”˜ç‰¹å›¾ä¸­åŒæ­¥é€‰ä¸­çš„ id åˆ° Vue data
    syncSelected() {
      // åŒæ­¥å½“前页面任务到全局数据
      gantt.eachTask((task) => {
        const globalTask = this.allTasks.find(t => t.id === task.id)
        if (globalTask) {
          globalTask.checked = task.checked
        }
      })
      // èŽ·å–æ‰€æœ‰é€‰ä¸­çš„ä»»åŠ¡ID
      this.selectedIds = this.allTasks.filter(t => t.checked).map(t => t.id)
      console.log(this.selectedIds)
    },
    // èŽ·å–é€‰ä¸­ä»»åŠ¡ï¼ˆç¤ºä¾‹ï¼‰
    handleGetSelected() {
      const selected = this.allTasks.filter(t => t.checked)
      this.$notify.success(`当前已选中${selected.length} æ¡ä»»åŠ¡`)
    },
    // æ¸…空所有选择
    handleClearSelection() {
      // éåŽ†æ‰€æœ‰ä»»åŠ¡ï¼Œå°† checked å±žæ€§è®¾ç½®ä¸º false
      this.allTasks.forEach(task => {
        task.checked = false
      })
      // æ›´æ–°å½“前页面显示的任务
      gantt.eachTask((task) => {
        task.checked = false
        gantt.updateTask(task.id)
      })
      // åŒæ­¥åˆ° Vue ç»„件数据
      this.syncSelected()
      // æ˜¾ç¤ºæç¤ºä¿¡æ¯
      this.$notify.success('已清空所有选择')
    }
  }
}
</script>
<style>
body,
html {
  width: 100%;
  height: 100%;
  margin: unset;
}
.taskCheckBox {
  cursor: pointer;
}
/* åˆ†é¡µå®¹å™¨æ ·å¼ */
.pagination-container {
  margin-top: 10px;
  display: flex;
  justify-content: end;
  padding: 10px 0;
  background-color: #fff;
  border-top: 1px solid #ebeef5;
}
</style>