小小儁爺
2026-01-15 70e1ded3cea2c5a948eaf0dc6f44098ec6cd7a6a
1.甘特图优化
已添加1个文件
已修改3个文件
2402 ■■■■ 文件已修改
src/components/dhtmlxGantt/codebase/dhtmlxgantt.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/global.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/gantt/index.vue 751 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/gantt/index_back.vue 1637 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/dhtmlxGantt/codebase/dhtmlxgantt.js
@@ -24861,8 +24861,8 @@
                    },
                    tooltip_date_format: i.date_to_str("%Y-%m-%d"),
                    tooltip_text: function(e, n, i) {
                      return "<b>任务:</b> " + i.text + "<br/><b>开始时间:</b> " + t.templates.tooltip_date_format(e) + "<br/><b>结束时间:</b> " + t.templates.tooltip_date_format(n)
                      // return "<b>Task:</b> " + i.text + "<br/><b>Start date:</b> " + t.templates.tooltip_date_format(e) + "<br/><b>End date:</b> " + t.templates.tooltip_date_format(n)
                      // return "<b>任务:</b> " + i.text + "<br/><b>开始时间:</b> " + t.templates.tooltip_date_format(e) + "<br/><b>结束时间:</b> " + t.templates.tooltip_date_format(n)
                      return "<b>Task:</b> " + i.text + "<br/><b>Start date:</b> " + t.templates.tooltip_date_format(e) + "<br/><b>End date:</b> " + t.templates.tooltip_date_format(n)
                    }
                  })
              },
src/utils/global.js
@@ -41,6 +41,16 @@
  return data.getFullYear() + '-' + month + '-' + date
}
// æ—¶é—´å¤„理函数  å¹´æœˆæ—¥   å¾€å‰æŽ¨ä¸€å¤©
export function handleDateReduceOneDay(value) {
  const newDate = new Date(value)
  newDate.setDate(newDate.getDate() - 1)
  const data = new Date(newDate)
  const month = data.getMonth() < 9 ? '0' + (data.getMonth() + 1) : data.getMonth() + 1
  const date = data.getDate() <= 9 ? '0' + data.getDate() : data.getDate()
  return data.getFullYear() + '-' + month + '-' + date
}
// äº‹ä»¶å¤„理函数  æ—¶åˆ†ç§’
// èŽ·å–å½“å‰æ—¶é—´
export function handleDatetime2(value) {
src/views/gantt/index.vue
@@ -1,30 +1,5 @@
<template>
  <div style="padding: 0 10px">
    <!--    <input value="保存到本地存储" class="local_storage" type="button" @click="saveToLocalStorage()">-->
    <!--    <input value="从本地存储加载" class="local_storage" type="button" @click="loadFromLocalStorage()">-->
    <!--    <input value="操作回退" type="button" onclick="gantt.undo()">-->
    <!--    <input value="操作前进" type="button" onclick="gantt.redo()">-->
    <!--    <input type="button" value="放大" onclick="gantt.ext.zoom.zoomIn();">-->
    <!--    <input type="button" value="缩小" onclick="gantt.ext.zoom.zoomOut();">-->
    <!--    <input class="start_date" type="date" value="2025-04-01" @change="changeDates()">-->
    <!--    <input class="end_date" type="date" value="2025-05-10" @change="changeDates()">-->
    <!--    <input type="button" value="Group by priority" @click="group('priority')">-->
    <!--    <input type="button" value="Group by resources" @click="group('owner')">-->
    <!--    <input type="button" value="Reset grouping" @click="group()">-->
    <!--    <label>当前布局:-->
    <!--      <select class="layout_config" name="layout" @input="changeLayout(this.value)">-->
    <!--        <option value="default">Default</option>-->
    <!--        <option value="horizontalScrollbars">Horinzontal Scrollbars</option>-->
    <!--        <option value="resource">With Resource Panel</option>-->
    <!--        <option value="universal">Universal</option>-->
    <!--        <option value="complexScrollbars">Complex with scrollbars</option>-->
    <!--      </select>-->
    <!--    </label>-->
    <div style="padding: 10px 0;display: flex;">
      <el-button type="primary" size="mini" @click="ganttUndo">回退拖动操作</el-button>
@@ -35,6 +10,7 @@
        v-model="ganttDateRange"
        style="margin-left: 10px;"
        size="mini"
        value-format="yyyy-MM-dd"
        type="daterange"
        :clearable="false"
        range-separator="至"
@@ -45,9 +21,9 @@
      <el-button type="primary" style="margin-left: 10px;" size="mini" @click="handleGetSelected">
        èŽ·å–å¤é€‰æ¡†é€‰ä¸­ä»»åŠ¡
      </el-button>
      <!--      <el-button size="mini" @click="handleClearSelection">-->
      <!--        æ¸…空复选框选择-->
      <!--      </el-button>-->
      <el-button size="mini" @click="handleClearSelection">
        æ¸…空复选框选择
      </el-button>
    </div>
    <div id="gantt_here" style="width:100%; height:90vh;" />
@@ -55,11 +31,9 @@
</template>
<script>
// import { gantt } from 'dhtmlx-gantt'
// import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { gantt } from '@/components/dhtmlxGantt'
import '@/components/dhtmlxGantt/codebase/dhtmlxgantt.css'
import { handleDateReduceOneDay } from '@/utils/global'
export default {
  data() {
@@ -70,6 +44,7 @@
    }
  },
  mounted() {
    this.ganttDateRangeChange(this.ganttDateRange)
    this.init()
  },
  methods: {
@@ -91,14 +66,6 @@
      gantt.config.multiselect = true // å¼€å¯å¤šä»»åŠ¡é€‰æ‹©
      /* â†“↓↓ Auto-scheduling configuration â†“↓↓ */
      gantt.config.auto_scheduling = true
      // gantt.config.project_start = new Date(2025, 03, 05);
      gantt.config.project_start = '2025/03/05'
      gantt.addMarker({
        text: '项目开始',
        start_date: gantt.config.project_start
      })
      function renderDiv(task, date, className) {
        const el = document.createElement('div')
@@ -181,13 +148,13 @@
            min_column_width: 80,
            scales: [
              // { unit: 'day', step: 1, format: '%d %M' }
              { unit: 'day', step: 1, format: '%M月%d号' }
              { unit: 'day', step: 1, format: '%M%d号' }
            ]
          },
          {
            name: 'week',
            scale_height: 50,
            min_column_width: 50,
            min_column_width: 70,
            scales: [
              // {
              //   unit: 'week', step: 1, format: function(date) {
@@ -197,9 +164,11 @@
              //     return '第' + weekNum + '周, ' + dateToStr(date) + ' - ' + dateToStr(endDate)
              //   }
              // },
              { unit: 'week', format: '%Yå¹´%M第%W周' },
              // %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' }
              // { unit: 'day', step: 1, format: '星期%D' }
            ]
          },
          {
@@ -305,9 +274,21 @@
        { 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 },
        {
          name: 'owner', align: 'center', width: 75, label: '作者', template: function(task) {
          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 ''
            }
@@ -521,16 +502,22 @@
      /* â†‘↑↑ Layout configuration â†‘↑↑ */
      // è‡ªå®šä¹‰æµ®åŠ¨æ¡†çš„æ˜¾ç¤ºå†…å®¹   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',
            'text': '项目1',
            'type': 'project',
            'start_date': '02-04-2025 00:00',
            'duration': 17,
            '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,
@@ -538,7 +525,7 @@
          },
          {
            'id': 2,
            'text': '任务2',
            'text': '项目2',
            'type': 'project',
            'start_date': '02-04-2025 00:00',
            'duration': 8,
@@ -549,7 +536,7 @@
          },
          {
            'id': 3,
            'text': '任务3',
            'text': '项目3',
            'type': 'project',
            'start_date': '11-04-2025 00:00',
            'duration': 8,
@@ -560,7 +547,7 @@
          },
          {
            'id': 4,
            'text': '任务4',
            'text': '项目4',
            'type': 'project',
            'start_date': '13-04-2025 00:00',
            'duration': 5,
@@ -669,7 +656,7 @@
          },
          {
            'id': 13,
            'text': '任务13',
            'text': '项目13',
            'type': 'project',
            'start_date': '03-04-2025 00:00',
            'duration': 11,
@@ -692,7 +679,7 @@
          },
          {
            'id': 15,
            'text': '任务15',
            'text': '项目15',
            'type': 'project',
            'start_date': '03-04-2025 00:00',
            'duration': 5,
@@ -729,13 +716,12 @@
          {
            'id': 25,
            'text': '任务18',
            // 'type': 'milestone',
            'type': 'task',
            'start_date': '06-04-2025 00:00',
            'parent': '13',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 1 }],
            'duration': 0,
            'duration': 2,
            checked: false
          },
          {
@@ -819,21 +805,11 @@
            'parent': '11',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 3 }],
            'duration': 0,
            'duration': 2,
            checked: false
          }
        ]
        // "links": [
        //   { "id": "2", "source": "2", "target": "3", "type": "0" },
        //   { "id": "3", "source": "3", "target": "4", "type": "0" },
        //   { "id": "7", "source": "8", "target": "9", "type": "0" },
        //   { "id": "8", "source": "9", "target": "10", "type": "0" },
        //   { "id": "16", "source": "17", "target": "25", "type": "0" },
        //   { "id": "17", "source": "18", "target": "19", "type": "0" },
        //   { "id": "18", "source": "19", "target": "20", "type": "0" },
        //   { "id": "22", "source": "13", "target": "24", "type": "0" },
        //   { "id": "23", "source": "25", "target": "18", "type": "0" }
        // ]
      })
      // ç»‘定甘特图点击事件(官方推荐的事件委托用法)<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>
@@ -861,285 +837,6 @@
      this.syncSelected()
    },
    // ä¿å­˜åˆ°æœ¬åœ°å­˜å‚¨
    loadFromLocalStorage() {
      this.loadGeneralConfig()
      this.loadStacks()
      this.loadGridColumnsConfig()
      this.loadLayoutConfig()
      this.loadVariousConfig()
    },
    // ä»Žæœ¬åœ°å­˜å‚¨åŠ è½½
    saveToLocalStorage() {
      this.saveGeneralConfig()
      this.saveVariousConfig()
      this.saveGridColumnsConfig()
      this.saveLayoutConfig()
      this.saveStacks()
    },
    saveGeneralConfig() {
      const generalConfig = {}
      // add properties that you want to save in the local storage
      const properties = [
        'grid_width',
        'start_date',
        'end_date'
        // examples of the properties you may want to add
        // "skip_off_time",
        // "show_tasks_outside_timescale",
        // "rtl",
        // "resize_rows",
        // "keyboard_navigation",
        // "keyboard_navigation_cells",
      ]
      properties.forEach(function(prop) {
        switch (typeof gantt.config[prop]) {
          case 'number':
          case 'string':
          case 'boolean':
            generalConfig[prop] = gantt.config[prop]
            break
          case 'object':
            if (gantt.config[prop] && typeof gantt.config[prop].getMonth === 'function') {
              generalConfig[prop] = gantt.date.date_to_str(gantt.config.date_format)(gantt.config[prop])
            }
            break
          // objects and methods should be set from the application
        }
      })
      const storageName = `DHTMLX Gantt: General Configuration`
      const serializedConfig = JSON.stringify(generalConfig) + ''
      localStorage.setItem(storageName, serializedConfig)
    },
    loadGeneralConfig() {
      const storageName = `DHTMLX Gantt: General Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const generalConfig = JSON.parse(loadedConfig)
      const dateProperties = [
        'start_date',
        'end_date',
        'project_start',
        'project_end'
      ]
      dateProperties.forEach(function(prop) {
        if (generalConfig[prop]) {
          generalConfig[prop] = gantt.date.str_to_date(gantt.config.date_format)(generalConfig[prop])
        }
      })
      gantt.mixin(gantt.config, generalConfig, true)
    },
    saveVariousConfig() {
      const variousConfig = {
        scrollState: gantt.getScrollState(),
        groupMode: gantt.getState().group_mode,
        zoomLevel: gantt.ext.zoom.getCurrentLevel()
      }
      const storageName = `DHTMLX Gantt: Various Configuration`
      const serializedConfig = JSON.stringify(variousConfig) + ''
      localStorage.setItem(storageName, serializedConfig)
    },
    loadVariousConfig() {
      const storageName = `DHTMLX Gantt: Various Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const variousConfig = JSON.parse(loadedConfig)
      gantt.scrollTo(variousConfig.scrollState.x, variousConfig.scrollState.y)
      if (variousConfig.groupMode) {
        // this is a custom function. in your case, it may be a different name
        this.group(variousConfig.groupMode)
      }
      gantt.ext.zoom.setLevel(variousConfig.zoomLevel)
    },
    saveGridColumnsConfig() {
      const storageName = `DHTMLX Gantt: Grid Columns Configuration`
      const serializedConfig = JSON.stringify(gantt.config.columns) + ''
      // objects and functions cannot be saved
      localStorage.setItem(storageName, serializedConfig)
    },
    loadGridColumnsConfig() {
      const storageName = `DHTMLX Gantt: Grid Columns Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const gridColumnsConfig = JSON.parse(loadedConfig)
      // as objects and functions cannot be saved, we add them from the existing columns
      // also, this approach helps saving the column order
      gridColumnsConfig.forEach(function(column) {
        const existingColumn = gantt.getGridColumn(column.name)
        gantt.mixin(column, existingColumn, false)
      })
      gantt.config.columns = gridColumnsConfig
    },
    saveLayoutConfig() {
      const layoutConfig = {
        currentLayout,
        gridWidth: gantt.getLayoutView('grid').$state.width
      }
      switch (layoutConfig.currentLayout) {
        case 'resource':
          layoutConfig.ganttPanelHeight = gantt.getLayoutView('ganttPanelCell').$lastSize.y
        case 'default':
          layoutConfig.gridWidth = gantt.config.grid_width
          break
        case 'universal':
        case 'complexScrollbars':
          layoutConfig.ganttPanelHeight = gantt.getLayoutView('mainGrid').$lastSize.y
          break
      }
      const storageName = `DHTMLX Gantt: Layout Configuration`
      const serializedConfig = JSON.stringify(layoutConfig) + ''
      localStorage.setItem(storageName, serializedConfig)
    },
    loadLayoutConfig() {
      const storageName = `DHTMLX Gantt: Layout Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const layoutConfig = JSON.parse(loadedConfig)
      this.changeLayout(layoutConfig.currentLayout)
      document.querySelector('.layout_config').value = layoutConfig.currentLayout
      switch (layoutConfig.currentLayout) {
        case 'horizontalScrollbars':
          gantt.config.layout.cols[0].width = layoutConfig.gridWidth
          break
        case 'resource':
          gantt.config.layout.rows[0].height = layoutConfig.ganttPanelHeight
          break
        case 'universal':
        case 'complexScrollbars':
          gantt.config.layout.cols[0].width = layoutConfig.gridWidth
          gantt.config.layout.cols[0].rows[0].height = layoutConfig.ganttPanelHeight
          gantt.config.layout.cols[2].rows[0].height = layoutConfig.ganttPanelHeight
          break
      }
      gantt.init('gantt_here')
      // with the rows[cols[]] layout configuration, we need to rely
      // on the grid_width config, and it is correctly applied only
      // after we use the `render` method
      if (layoutConfig.currentLayout == 'default' || layoutConfig.currentLayout == 'resource') {
        gantt.config.grid_width = layoutConfig.gridWidth
        gantt.render()
      }
    },
    saveStacks() {
      this.saveStack('Undo')
      this.saveStack('Redo')
    },
    loadStacks() {
      this.loadStack('Undo')
      this.loadStack('Redo')
    },
    saveStack(stackType) {
      const stack = gantt.copy(gantt[`get${stackType}Stack`]())
      stack.forEach(function(action) {
        action.commands.forEach(function(command) {
          command.oldValue = gantt.json.serializeTask(command.oldValue)
          command.value = gantt.json.serializeTask(command.value)
          const assignments = gantt.config.resource_property
          // if (command.oldValue[assignments]) {
          //   for (assignment in command.oldValue[assignments]) {
          //     command.oldValue[assignments][assignment] = gantt.json.serializeTask(command.oldValue[assignments][assignment])
          //   }
          // }
          // if (command.value[assignments]) {
          //   for (assignment in command.value[assignments]) {
          //     command.value[assignments][assignment] = gantt.json.serializeTask(command.value[assignments][assignment])
          //   }
          // }
        })
      })
      const serializedStack = JSON.stringify(stack) + ''
      const storageName = `DHTMLX Gantt: ${stackType} Stack`
      localStorage.setItem(storageName, serializedStack)
    },
    loadStack(stackType) {
      const storageName = `DHTMLX Gantt: ${stackType} Stack`
      const serializedStack = localStorage.getItem(storageName)
      const loadedStack = JSON.parse(serializedStack)
      loadedStack.forEach(function(action) {
        action.commands.forEach(function(command) {
          convertDateProperties(command.oldValue)
          convertDateProperties(command.value)
          const assignments = gantt.config.resource_property
          // if (command.oldValue[assignments]) {
          //   for (assignment in command.oldValue[assignments]) {
          //     convertDateProperties(command.oldValue[assignments][assignment])
          //   }
          // }
          // if (command.value[assignments]) {
          //   for (assignment in command.value[assignments]) {
          //     convertDateProperties(command.value[assignments][assignment])
          //   }
          // }
        })
      })
      gantt[`clear${stackType}Stack`]()
      const stack = gantt[`get${stackType}Stack`]()
      loadedStack.forEach(function(action) {
        stack.push(action)
      })
      function convertDateProperties(obj) {
        const dateProperties = [
          'start_date',
          'end_date',
          'constraint_date'
        ]
        dateProperties.forEach(function(prop) {
          if (obj[prop]) {
            obj[prop] = gantt.date.parseDate(obj[prop], gantt.config.date_format)
          }
        })
      }
    },
    // changeDates() {
    //   const startDateEl = document.querySelector('.start_date')
    //   const endDateEl = document.querySelector('.end_date')
    //   const startDate = new Date(startDateEl.value)
    //   const endDate = new Date(endDateEl.value)
    //   console.log(startDate)
    //   console.log(endDate)
    //   if (!+startDate || !+endDate) {
    //     return
    //   }
    //
    //   gantt.config.start_date = startDate
    //   gantt.config.end_date = endDate
    //   gantt.render()
    // },
    ganttDateRangeChange(val) {
      gantt.config.start_date = val[0]
      gantt.config.end_date = val[1]
@@ -1159,339 +856,9 @@
      gantt.ext.zoom.zoomOut()
    },
    // åˆ†ç»„
    group(type) {
      switch (type) {
        case 'priority':
          gantt.groupBy({
            groups: gantt.serverList('task_priority'),
            relation_property: type,
            group_id: 'key',
            group_text: 'label'
          })
          break
        case 'owner':
          const groups = gantt.getDatastore('resource').getItems().map(function(item) {
            const group = gantt.copy(item)
            group.group_id = group.id
            group.id = gantt.uid()
            return group
          })
          gantt.groupBy({
            groups: groups,
            relation_property: gantt.config.resource_property,
            group_id: 'group_id',
            group_text: 'text',
            delimiter: ', ',
            default_group_label: 'Not Assigned'
          })
          break
        default:
          gantt.groupBy(false)
          break
      }
    },
    changeLayout(value) {
      console.log(value)
      // currentLayout = value
      const resourceConfig = {
        columns: [
          {
            name: 'name', label: 'Name', tree: true, template: function(resource) {
              return resource.text
            }
          },
          {
            name: 'workload', label: 'Workload', template: function(resource) {
              let totalDuration = 0
              if (resource.$level == 2) {
                const assignment = gantt.getResourceAssignments(resource.$resource_id, resource.$task_id)[0]
                totalDuration = resource.duration * assignment.value
              } else {
                const assignments = getResourceAssignments(resource.id)
                assignments.forEach(function(assignment) {
                  const task = gantt.getTask(assignment.task_id)
                  totalDuration += Number(assignment.value) * task.duration
                })
              }
              return (totalDuration || 0) + 'h'
            }
          }
        ]
      }
      const defaultLayout = {
        css: 'gantt_container',
        rows: [
          {
            cols: [
              {
                // the default grid view
                view: 'grid',
                scrollX: 'scrollHor',
                scrollY: 'scrollVer'
              },
              { resizer: true, width: 1 },
              {
                // the default timeline view
                view: 'timeline',
                scrollX: 'scrollHor',
                scrollY: 'scrollVer'
              },
              {
                view: 'scrollbar',
                id: 'scrollVer'
              }
            ]
          },
          {
            view: 'scrollbar',
            id: 'scrollHor'
          }
        ]
      }
      const gridWidthScrollbarLayout = {
        css: 'gantt_container',
        cols: [
          {
            rows: [
              {
                view: 'grid',
                scrollable: true,
                scrollX: 'scrollHor1',
                scrollY: 'scrollVer'
              },
              {
                view: 'scrollbar',
                id: 'scrollHor1',
                scroll: 'x',
                group: 'hor'
              }
            ]
          },
          { resizer: true, width: 1 },
          {
            rows: [
              {
                view: 'timeline',
                scrollX: 'scrollHor',
                scrollY: 'scrollVer'
              },
              {
                view: 'scrollbar',
                id: 'scrollHor',
                scroll: 'x',
                group: 'hor'
              }
            ]
          },
          {
            view: 'scrollbar',
            id: 'scrollVer'
          }
        ]
      }
      const resourceLayoutGeneral = {
        css: 'gantt_container',
        rows: [
          {
            id: 'ganttPanelCell',
            cols: [
              { view: 'grid', group: 'grids', scrollY: 'scrollVer' },
              { resizer: true, width: 1 },
              { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
              { view: 'scrollbar', id: 'scrollVer', group: 'vertical' }
            ],
            gravity: 2
          },
          { resizer: true, width: 1 },
          {
            config: resourceConfig,
            cols: [
              { view: 'resourceGrid', group: 'grids', width: 435, scrollY: 'resourceVScroll' },
              { resizer: true, width: 1 },
              { view: 'resourceTimeline', scrollX: 'scrollHor', scrollY: 'resourceVScroll' },
              { view: 'scrollbar', id: 'resourceVScroll', group: 'vertical' }
            ],
            gravity: 1
          },
          { view: 'scrollbar', id: 'scrollHor' }
        ]
      }
      const universalLayout = {
        css: 'gantt_container',
        cols: [
          {
            width: 400,
            rows: [
              {
                id: 'mainGrid',
                linkedView: 'mainTimeline',
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'grid', scrollX: 'gridScrollX', scrollable: true, scrollY: 'scrollVer' }
                    ]
                  }
                ]
              },
              { resizer: true, width: 1 },
              {
                id: 'resourceGrid',
                linkedView: 'resourceTimeline',
                group: 'resourceLoad',
                config: resourceConfig,
                cols: [
                  {
                    rows: [
                      { view: 'resourceGrid', scrollY: 'scrollVer2', scrollX: 'gridScrollX', scrollable: true },
                      { view: 'scrollbar', id: 'gridScrollX' }
                    ]
                  }
                ]
              }
            ]
          },
          { resizer: true, width: 1 },
          {
            rows: [
              {
                id: 'mainTimeline',
                linkedView: 'mainGrid',
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer' }
                ]
              },
              { resizer: true, width: 1 },
              {
                id: 'resourceTimeline',
                linkedView: 'resourceGrid',
                group: 'resourceLoad',
                cols: [
                  {
                    rows: [
                      { view: 'resourceTimeline', scrollX: 'scrollHor', scrollY: 'scrollVer2' },
                      { view: 'scrollbar', id: 'scrollHor' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer2' }
                ]
              }
            ]
          }
        ]
      }
      const complexLayoutWithScrollbars = {
        css: 'gantt_container',
        cols: [
          {
            width: 400,
            // min_width: 100,
            rows: [
              {
                id: 'mainGrid',
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'grid', scrollX: 'gridScrollX', scrollable: true, scrollY: 'gridScrollY' },
                      { view: 'scrollbar', id: 'gridScrollX', group: 'mainGantt' }
                    ]
                  },
                  { view: 'scrollbar', id: 'gridScrollY' }
                ]
              },
              { resizer: true, width: 1 },
              {
                group: 'resources',
                config: resourceConfig,
                cols: [
                  {
                    rows: [
                      { view: 'resourceGrid', scrollY: 'gridScrollY2', scrollX: 'gridScrollX2', scrollable: true },
                      { view: 'scrollbar', id: 'gridScrollX2', group: 'resourcePanel' }
                    ]
                  },
                  { view: 'scrollbar', id: 'gridScrollY2' }
                ]
              }
            ]
          },
          // {view: "scrollbar", id: "grid",scrollX: "grid"},
          { resizer: true, width: 1 },
          {
            rows: [
              {
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
                      { view: 'scrollbar', id: 'scrollHor', group: 'mainGantt' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer' }
                ]
              },
              { resizer: true, width: 1 },
              {
                group: 'resources',
                cols: [
                  {
                    rows: [
                      { view: 'resourceTimeline', scrollX: 'scrollHor2', scrollY: 'scrollVer2' },
                      { view: 'scrollbar', id: 'scrollHor2', group: 'resourcePanel' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer2' }
                ]
              }
            ]
          }
        ]
      }
      switch (value) {
        case 'default':
          gantt.config.layout = defaultLayout
          break
        case 'horizontalScrollbars':
          gantt.config.layout = gridWidthScrollbarLayout
          break
        case 'resource':
          gantt.config.layout = resourceLayoutGeneral
          break
        case 'universal':
          gantt.config.layout = universalLayout
          break
        case 'complexScrollbars':
          gantt.config.layout = complexLayoutWithScrollbars
          break
      }
      gantt.init('gantt_here')
    },
    // ä»Žç”˜ç‰¹å›¾ä¸­åŒæ­¥é€‰ä¸­çš„ id åˆ° Vue data
    syncSelected() {
      const tasks = gantt.serialize().data || []
      console.log(tasks)
      this.selectedIds = tasks.filter(t => t.checked).map(t => t.id)
      console.log(this.selectedIds)
    },
@@ -1500,30 +867,30 @@
    handleGetSelected() {
      const tasks = gantt.serialize().data || []
      const selected = tasks.filter(t => t.checked)
      this.$message.info(`当前已选中${selected.length} æ¡ä»»åŠ¡`)
      this.$notify.success(`当前已选中${selected.length} æ¡ä»»åŠ¡`)
    },
    // æ¸…空所有选择
    handleClearSelection() {
      // gantt.unselectAll();
      // this.selectedIds = [];
      // æ›´æ–°æ‰€æœ‰å¤é€‰æ¡†çŠ¶æ€ä¸ºæœªé€‰ä¸­
      // èŽ·å–æ‰€æœ‰ä»»åŠ¡
      const tasks = gantt.serialize().data || []
      tasks.forEach(t => {
        if (t.checked) {
          console.log(t, '执行')
          t.checked = false
          gantt.updateTask(t.id)
        }
      })
      const checkboxes = gantt.$container.querySelectorAll('.taskCheckBox')
      checkboxes.forEach(checkbox => {
        checkbox.checked = false
      // éåŽ†æ‰€æœ‰ä»»åŠ¡ï¼Œå°† checked å±žæ€§è®¾ç½®ä¸º false
      tasks.forEach(task => {
        task.checked = false
      })
      // æ›´æ–°æ‰€æœ‰ä»»åŠ¡æ˜¾ç¤º
      gantt.eachTask((task) => {
        task.checked = false
        gantt.updateTask(task.id)
      })
      // åŒæ­¥åˆ° Vue ç»„件数据
      this.syncSelected()
      // gantt.render()
      // æ˜¾ç¤ºæç¤ºä¿¡æ¯
      this.$notify.success('已清空所有选择')
    }
  }
src/views/gantt/index_back.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1637 @@
<template>
  <div style="padding: 0 10px">
    <!--    <input value="保存到本地存储" class="local_storage" type="button" @click="saveToLocalStorage()">-->
    <!--    <input value="从本地存储加载" class="local_storage" type="button" @click="loadFromLocalStorage()">-->
    <!--    <input value="操作回退" type="button" onclick="gantt.undo()">-->
    <!--    <input value="操作前进" type="button" onclick="gantt.redo()">-->
    <!--    <input type="button" value="放大" onclick="gantt.ext.zoom.zoomIn();">-->
    <!--    <input type="button" value="缩小" onclick="gantt.ext.zoom.zoomOut();">-->
    <!--    <input class="start_date" type="date" value="2025-04-01" @change="changeDates()">-->
    <!--    <input class="end_date" type="date" value="2025-05-10" @change="changeDates()">-->
    <!--    <input type="button" value="Group by priority" @click="group('priority')">-->
    <!--    <input type="button" value="Group by resources" @click="group('owner')">-->
    <!--    <input type="button" value="Reset grouping" @click="group()">-->
    <!--    <label>当前布局:-->
    <!--      <select class="layout_config" name="layout" @input="changeLayout(this.value)">-->
    <!--        <option value="default">Default</option>-->
    <!--        <option value="horizontalScrollbars">Horinzontal Scrollbars</option>-->
    <!--        <option value="resource">With Resource Panel</option>-->
    <!--        <option value="universal">Universal</option>-->
    <!--        <option value="complexScrollbars">Complex with scrollbars</option>-->
    <!--      </select>-->
    <!--    </label>-->
    <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"
        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 'dhtmlx-gantt'
// import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { gantt } from '@/components/dhtmlxGantt'
import '@/components/dhtmlxGantt/codebase/dhtmlxgantt.css'
export default {
  data() {
    return {
      value: 'default',
      ganttDateRange: ['2025-04-01', '2025-05-10'],
      selectedIds: []
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    init() {
      // 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.config.multiselect = true // å¼€å¯å¤šä»»åŠ¡é€‰æ‹©
      /* â†“↓↓ Auto-scheduling configuration â†“↓↓ */
      gantt.config.auto_scheduling = true
      // gantt.config.project_start = new Date(2025, 03, 05);
      // gantt.config.project_start = '2025/03/05'
      // gantt.addMarker({
      //   text: '项目开始',
      //   start_date: gantt.config.project_start
      // })
      function renderDiv(task, date, className) {
        const el = document.createElement('div')
        el.className = className
        const sizes = gantt.getTaskPosition(task, date)
        el.style.left = sizes.left + 'px'
        el.style.top = sizes.top + 'px'
        return el
      }
      gantt.attachEvent('onGanttReady', function() {
        // gantt.addTaskLayer(function draw_deadline(task) {
        //   const constraintType = gantt.getConstraintType(task);
        //   const types = gantt.config.constraint_types;
        //   if (constraintType != types.ASAP && constraintType != types.ALAP && task.constraint_date) {
        //     const dates = gantt.getConstraintLimitations(task);
        //
        //     const els = document.createElement("div");
        //
        //     if (dates.earliestStart) {
        //       els.appendChild(renderDiv(task, dates.earliestStart, 'constraint-marker earliest-start'));
        //     }
        //
        //     if (dates.latestEnd) {
        //       els.appendChild(renderDiv(task, dates.latestEnd, 'constraint-marker latest-end'));
        //     }
        //
        //     els.title = gantt.locale.labels[constraintType] + " " + gantt.templates.task_date(task.constraint_date);
        //
        //     if (els.children.length)
        //       return els;
        //   }
        //   return false;
        // });
      })
      /* â†‘↑↑ Auto-scheduling configuration â†‘↑↑ */
      /* â†“↓↓ 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' }
      ])
      gantt.i18n.setLocale('cn')
      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: 0, 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
        } },
        {
          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 â†‘↑↑ */
      /* â†“↓↓ Resource configuration â†“↓↓ */
      function getResourceAssignments(resourceId) {
        let assignments
        const store = gantt.getDatastore(gantt.config.resource_store)
        const resource = store.getItem(resourceId)
        if (resource.$level === 0) {
          assignments = []
          store.getChildren(resourceId).forEach(function(childId) {
            assignments = assignments.concat(gantt.getResourceAssignments(childId))
          })
        } else if (resource.$level === 1) {
          assignments = gantt.getResourceAssignments(resourceId)
        } else {
          assignments = gantt.getResourceAssignments(resource.$resource_id, resource.$task_id)
        }
        return assignments
      }
      gantt.templates.resource_cell_class = function(start_date, end_date, resource, tasks) {
        const css = []
        css.push('resource_marker')
        if (tasks.length <= 1) {
          css.push('workday_ok')
        } else {
          css.push('workday_over')
        }
        return css.join(' ')
      }
      gantt.templates.resource_cell_value = function(start_date, end_date, resource, tasks) {
        let result = 0
        tasks.forEach(function(item) {
          const assignments = gantt.getResourceAssignments(resource.id, item.id)
          assignments.forEach(function(assignment) {
            const task = gantt.getTask(assignment.task_id)
            result += assignment.value * 1
          })
        })
        if (result % 1) {
          result = Math.round(result * 10) / 10
        }
        return '<div>' + result + '</div>'
      }
      gantt.locale.labels.section_resources = 'Owners'
      gantt.locale.labels.section_calendar = 'Calendar'
      // æ±‰åŒ–窗口
      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 â†‘↑↑ */
      /* â†“↓↓ Layout configuration â†“↓↓ */
      gantt.config.grid_elastic_columns = true
      // let currentLayout = 'default'
      /* â†‘↑↑ Layout configuration â†‘↑↑ */
      // 20250211 tooltip浮动框显示的End Date被追加1的问题修复(应该显示数据库的原始值)
      // è‡ªå®šä¹‰æµ®åŠ¨æ¡†çš„æ˜¾ç¤ºå†…å®¹
      gantt.templates.tooltip_text = function(start, end, task) {
        // console.log(start, end, task)
        // ä½¿ç”¨åŽŸå§‹ç»“æŸæ—¥æœŸæ˜¾ç¤º
        var originalEndDate = task.original_end_date ? gantt.date.date_to_str('%Y-%m-%d')(task.original_end_date) : ''
        // return `<b>工作描述:</b> ${task.text}<br/>
        //     <b>开始日期:</b> ${gantt.date.date_to_str('%Y-%m-%d')(task.start_date)}<br/>
        //     <b>结束日期:</b> ${originalEndDate}<br/>
        //     <b>工期:</b> ${task.duration + 1}天<br/>
        //     <b>进度:</b> ${task.progress * 100}%<br/>
        //  `
        // <b>责任人:</b> ${task.username}<br/>
        // return "<b>任务:</b> " + task.text + "<br/><b>开始时间:</b> " + task.templates.tooltip_date_format(start) + "<br/><b>结束时间:</b> " + task.templates.tooltip_date_format(end)
        return '<b>任务:</b> ' + task.text + '<br/><b>开始时间:</b> ' + `${gantt.date.date_to_str('%Y-%m-%d')(start)}` + '<br/><b>结束时间:</b> ' + `${gantt.date.date_to_str('%Y-%m-%d')(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': 'milestone',
            'type': 'task',
            'start_date': '06-04-2025 00:00',
            'parent': '13',
            'progress': 0,
            'owner': [{ 'resource_id': '5', 'value': 1 }],
            'duration': 0,
            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': 0,
            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()
    },
    // ä¿å­˜åˆ°æœ¬åœ°å­˜å‚¨
    loadFromLocalStorage() {
      this.loadGeneralConfig()
      this.loadStacks()
      this.loadGridColumnsConfig()
      this.loadLayoutConfig()
      this.loadVariousConfig()
    },
    // ä»Žæœ¬åœ°å­˜å‚¨åŠ è½½
    saveToLocalStorage() {
      this.saveGeneralConfig()
      this.saveVariousConfig()
      this.saveGridColumnsConfig()
      this.saveLayoutConfig()
      this.saveStacks()
    },
    saveGeneralConfig() {
      const generalConfig = {}
      // add properties that you want to save in the local storage
      const properties = [
        'grid_width',
        'start_date',
        'end_date'
        // examples of the properties you may want to add
        // "skip_off_time",
        // "show_tasks_outside_timescale",
        // "rtl",
        // "resize_rows",
        // "keyboard_navigation",
        // "keyboard_navigation_cells",
      ]
      properties.forEach(function(prop) {
        switch (typeof gantt.config[prop]) {
          case 'number':
          case 'string':
          case 'boolean':
            generalConfig[prop] = gantt.config[prop]
            break
          case 'object':
            if (gantt.config[prop] && typeof gantt.config[prop].getMonth === 'function') {
              generalConfig[prop] = gantt.date.date_to_str(gantt.config.date_format)(gantt.config[prop])
            }
            break
          // objects and methods should be set from the application
        }
      })
      const storageName = `DHTMLX Gantt: General Configuration`
      const serializedConfig = JSON.stringify(generalConfig) + ''
      localStorage.setItem(storageName, serializedConfig)
    },
    loadGeneralConfig() {
      const storageName = `DHTMLX Gantt: General Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const generalConfig = JSON.parse(loadedConfig)
      const dateProperties = [
        'start_date',
        'end_date',
        'project_start',
        'project_end'
      ]
      dateProperties.forEach(function(prop) {
        if (generalConfig[prop]) {
          generalConfig[prop] = gantt.date.str_to_date(gantt.config.date_format)(generalConfig[prop])
        }
      })
      gantt.mixin(gantt.config, generalConfig, true)
    },
    saveVariousConfig() {
      const variousConfig = {
        scrollState: gantt.getScrollState(),
        groupMode: gantt.getState().group_mode,
        zoomLevel: gantt.ext.zoom.getCurrentLevel()
      }
      const storageName = `DHTMLX Gantt: Various Configuration`
      const serializedConfig = JSON.stringify(variousConfig) + ''
      localStorage.setItem(storageName, serializedConfig)
    },
    loadVariousConfig() {
      const storageName = `DHTMLX Gantt: Various Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const variousConfig = JSON.parse(loadedConfig)
      gantt.scrollTo(variousConfig.scrollState.x, variousConfig.scrollState.y)
      if (variousConfig.groupMode) {
        // this is a custom function. in your case, it may be a different name
        this.group(variousConfig.groupMode)
      }
      gantt.ext.zoom.setLevel(variousConfig.zoomLevel)
    },
    saveGridColumnsConfig() {
      const storageName = `DHTMLX Gantt: Grid Columns Configuration`
      const serializedConfig = JSON.stringify(gantt.config.columns) + ''
      // objects and functions cannot be saved
      localStorage.setItem(storageName, serializedConfig)
    },
    loadGridColumnsConfig() {
      const storageName = `DHTMLX Gantt: Grid Columns Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const gridColumnsConfig = JSON.parse(loadedConfig)
      // as objects and functions cannot be saved, we add them from the existing columns
      // also, this approach helps saving the column order
      gridColumnsConfig.forEach(function(column) {
        const existingColumn = gantt.getGridColumn(column.name)
        gantt.mixin(column, existingColumn, false)
      })
      gantt.config.columns = gridColumnsConfig
    },
    saveLayoutConfig() {
      const layoutConfig = {
        currentLayout,
        gridWidth: gantt.getLayoutView('grid').$state.width
      }
      switch (layoutConfig.currentLayout) {
        case 'resource':
          layoutConfig.ganttPanelHeight = gantt.getLayoutView('ganttPanelCell').$lastSize.y
        case 'default':
          layoutConfig.gridWidth = gantt.config.grid_width
          break
        case 'universal':
        case 'complexScrollbars':
          layoutConfig.ganttPanelHeight = gantt.getLayoutView('mainGrid').$lastSize.y
          break
      }
      const storageName = `DHTMLX Gantt: Layout Configuration`
      const serializedConfig = JSON.stringify(layoutConfig) + ''
      localStorage.setItem(storageName, serializedConfig)
    },
    loadLayoutConfig() {
      const storageName = `DHTMLX Gantt: Layout Configuration`
      const loadedConfig = localStorage.getItem(storageName)
      const layoutConfig = JSON.parse(loadedConfig)
      this.changeLayout(layoutConfig.currentLayout)
      document.querySelector('.layout_config').value = layoutConfig.currentLayout
      switch (layoutConfig.currentLayout) {
        case 'horizontalScrollbars':
          gantt.config.layout.cols[0].width = layoutConfig.gridWidth
          break
        case 'resource':
          gantt.config.layout.rows[0].height = layoutConfig.ganttPanelHeight
          break
        case 'universal':
        case 'complexScrollbars':
          gantt.config.layout.cols[0].width = layoutConfig.gridWidth
          gantt.config.layout.cols[0].rows[0].height = layoutConfig.ganttPanelHeight
          gantt.config.layout.cols[2].rows[0].height = layoutConfig.ganttPanelHeight
          break
      }
      gantt.init('gantt_here')
      // with the rows[cols[]] layout configuration, we need to rely
      // on the grid_width config, and it is correctly applied only
      // after we use the `render` method
      if (layoutConfig.currentLayout == 'default' || layoutConfig.currentLayout == 'resource') {
        gantt.config.grid_width = layoutConfig.gridWidth
        gantt.render()
      }
    },
    saveStacks() {
      this.saveStack('Undo')
      this.saveStack('Redo')
    },
    loadStacks() {
      this.loadStack('Undo')
      this.loadStack('Redo')
    },
    saveStack(stackType) {
      const stack = gantt.copy(gantt[`get${stackType}Stack`]())
      stack.forEach(function(action) {
        action.commands.forEach(function(command) {
          command.oldValue = gantt.json.serializeTask(command.oldValue)
          command.value = gantt.json.serializeTask(command.value)
          const assignments = gantt.config.resource_property
          // if (command.oldValue[assignments]) {
          //   for (assignment in command.oldValue[assignments]) {
          //     command.oldValue[assignments][assignment] = gantt.json.serializeTask(command.oldValue[assignments][assignment])
          //   }
          // }
          // if (command.value[assignments]) {
          //   for (assignment in command.value[assignments]) {
          //     command.value[assignments][assignment] = gantt.json.serializeTask(command.value[assignments][assignment])
          //   }
          // }
        })
      })
      const serializedStack = JSON.stringify(stack) + ''
      const storageName = `DHTMLX Gantt: ${stackType} Stack`
      localStorage.setItem(storageName, serializedStack)
    },
    loadStack(stackType) {
      const storageName = `DHTMLX Gantt: ${stackType} Stack`
      const serializedStack = localStorage.getItem(storageName)
      const loadedStack = JSON.parse(serializedStack)
      loadedStack.forEach(function(action) {
        action.commands.forEach(function(command) {
          convertDateProperties(command.oldValue)
          convertDateProperties(command.value)
          const assignments = gantt.config.resource_property
          // if (command.oldValue[assignments]) {
          //   for (assignment in command.oldValue[assignments]) {
          //     convertDateProperties(command.oldValue[assignments][assignment])
          //   }
          // }
          // if (command.value[assignments]) {
          //   for (assignment in command.value[assignments]) {
          //     convertDateProperties(command.value[assignments][assignment])
          //   }
          // }
        })
      })
      gantt[`clear${stackType}Stack`]()
      const stack = gantt[`get${stackType}Stack`]()
      loadedStack.forEach(function(action) {
        stack.push(action)
      })
      function convertDateProperties(obj) {
        const dateProperties = [
          'start_date',
          'end_date',
          'constraint_date'
        ]
        dateProperties.forEach(function(prop) {
          if (obj[prop]) {
            obj[prop] = gantt.date.parseDate(obj[prop], gantt.config.date_format)
          }
        })
      }
    },
    // changeDates() {
    //   const startDateEl = document.querySelector('.start_date')
    //   const endDateEl = document.querySelector('.end_date')
    //   const startDate = new Date(startDateEl.value)
    //   const endDate = new Date(endDateEl.value)
    //   if (!+startDate || !+endDate) {
    //     return
    //   }
    //
    //   gantt.config.start_date = startDate
    //   gantt.config.end_date = endDate
    //   gantt.render()
    // },
    ganttDateRangeChange(val) {
      console.log(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()
    },
    // åˆ†ç»„
    group(type) {
      switch (type) {
        case 'priority':
          gantt.groupBy({
            groups: gantt.serverList('task_priority'),
            relation_property: type,
            group_id: 'key',
            group_text: 'label'
          })
          break
        case 'owner':
          const groups = gantt.getDatastore('resource').getItems().map(function(item) {
            const group = gantt.copy(item)
            group.group_id = group.id
            group.id = gantt.uid()
            return group
          })
          gantt.groupBy({
            groups: groups,
            relation_property: gantt.config.resource_property,
            group_id: 'group_id',
            group_text: 'text',
            delimiter: ', ',
            default_group_label: 'Not Assigned'
          })
          break
        default:
          gantt.groupBy(false)
          break
      }
    },
    changeLayout(value) {
      console.log(value)
      // currentLayout = value
      const resourceConfig = {
        columns: [
          {
            name: 'name', label: 'Name', tree: true, template: function(resource) {
              return resource.text
            }
          },
          {
            name: 'workload', label: 'Workload', template: function(resource) {
              let totalDuration = 0
              if (resource.$level == 2) {
                const assignment = gantt.getResourceAssignments(resource.$resource_id, resource.$task_id)[0]
                totalDuration = resource.duration * assignment.value
              } else {
                const assignments = getResourceAssignments(resource.id)
                assignments.forEach(function(assignment) {
                  const task = gantt.getTask(assignment.task_id)
                  totalDuration += Number(assignment.value) * task.duration
                })
              }
              return (totalDuration || 0) + 'h'
            }
          }
        ]
      }
      const defaultLayout = {
        css: 'gantt_container',
        rows: [
          {
            cols: [
              {
                // the default grid view
                view: 'grid',
                scrollX: 'scrollHor',
                scrollY: 'scrollVer'
              },
              { resizer: true, width: 1 },
              {
                // the default timeline view
                view: 'timeline',
                scrollX: 'scrollHor',
                scrollY: 'scrollVer'
              },
              {
                view: 'scrollbar',
                id: 'scrollVer'
              }
            ]
          },
          {
            view: 'scrollbar',
            id: 'scrollHor'
          }
        ]
      }
      const gridWidthScrollbarLayout = {
        css: 'gantt_container',
        cols: [
          {
            rows: [
              {
                view: 'grid',
                scrollable: true,
                scrollX: 'scrollHor1',
                scrollY: 'scrollVer'
              },
              {
                view: 'scrollbar',
                id: 'scrollHor1',
                scroll: 'x',
                group: 'hor'
              }
            ]
          },
          { resizer: true, width: 1 },
          {
            rows: [
              {
                view: 'timeline',
                scrollX: 'scrollHor',
                scrollY: 'scrollVer'
              },
              {
                view: 'scrollbar',
                id: 'scrollHor',
                scroll: 'x',
                group: 'hor'
              }
            ]
          },
          {
            view: 'scrollbar',
            id: 'scrollVer'
          }
        ]
      }
      const resourceLayoutGeneral = {
        css: 'gantt_container',
        rows: [
          {
            id: 'ganttPanelCell',
            cols: [
              { view: 'grid', group: 'grids', scrollY: 'scrollVer' },
              { resizer: true, width: 1 },
              { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
              { view: 'scrollbar', id: 'scrollVer', group: 'vertical' }
            ],
            gravity: 2
          },
          { resizer: true, width: 1 },
          {
            config: resourceConfig,
            cols: [
              { view: 'resourceGrid', group: 'grids', width: 435, scrollY: 'resourceVScroll' },
              { resizer: true, width: 1 },
              { view: 'resourceTimeline', scrollX: 'scrollHor', scrollY: 'resourceVScroll' },
              { view: 'scrollbar', id: 'resourceVScroll', group: 'vertical' }
            ],
            gravity: 1
          },
          { view: 'scrollbar', id: 'scrollHor' }
        ]
      }
      const universalLayout = {
        css: 'gantt_container',
        cols: [
          {
            width: 400,
            rows: [
              {
                id: 'mainGrid',
                linkedView: 'mainTimeline',
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'grid', scrollX: 'gridScrollX', scrollable: true, scrollY: 'scrollVer' }
                    ]
                  }
                ]
              },
              { resizer: true, width: 1 },
              {
                id: 'resourceGrid',
                linkedView: 'resourceTimeline',
                group: 'resourceLoad',
                config: resourceConfig,
                cols: [
                  {
                    rows: [
                      { view: 'resourceGrid', scrollY: 'scrollVer2', scrollX: 'gridScrollX', scrollable: true },
                      { view: 'scrollbar', id: 'gridScrollX' }
                    ]
                  }
                ]
              }
            ]
          },
          { resizer: true, width: 1 },
          {
            rows: [
              {
                id: 'mainTimeline',
                linkedView: 'mainGrid',
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer' }
                ]
              },
              { resizer: true, width: 1 },
              {
                id: 'resourceTimeline',
                linkedView: 'resourceGrid',
                group: 'resourceLoad',
                cols: [
                  {
                    rows: [
                      { view: 'resourceTimeline', scrollX: 'scrollHor', scrollY: 'scrollVer2' },
                      { view: 'scrollbar', id: 'scrollHor' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer2' }
                ]
              }
            ]
          }
        ]
      }
      const complexLayoutWithScrollbars = {
        css: 'gantt_container',
        cols: [
          {
            width: 400,
            // min_width: 100,
            rows: [
              {
                id: 'mainGrid',
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'grid', scrollX: 'gridScrollX', scrollable: true, scrollY: 'gridScrollY' },
                      { view: 'scrollbar', id: 'gridScrollX', group: 'mainGantt' }
                    ]
                  },
                  { view: 'scrollbar', id: 'gridScrollY' }
                ]
              },
              { resizer: true, width: 1 },
              {
                group: 'resources',
                config: resourceConfig,
                cols: [
                  {
                    rows: [
                      { view: 'resourceGrid', scrollY: 'gridScrollY2', scrollX: 'gridScrollX2', scrollable: true },
                      { view: 'scrollbar', id: 'gridScrollX2', group: 'resourcePanel' }
                    ]
                  },
                  { view: 'scrollbar', id: 'gridScrollY2' }
                ]
              }
            ]
          },
          // {view: "scrollbar", id: "grid",scrollX: "grid"},
          { resizer: true, width: 1 },
          {
            rows: [
              {
                group: 'gantt',
                cols: [
                  {
                    rows: [
                      { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
                      { view: 'scrollbar', id: 'scrollHor', group: 'mainGantt' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer' }
                ]
              },
              { resizer: true, width: 1 },
              {
                group: 'resources',
                cols: [
                  {
                    rows: [
                      { view: 'resourceTimeline', scrollX: 'scrollHor2', scrollY: 'scrollVer2' },
                      { view: 'scrollbar', id: 'scrollHor2', group: 'resourcePanel' }
                    ]
                  },
                  { view: 'scrollbar', id: 'scrollVer2' }
                ]
              }
            ]
          }
        ]
      }
      switch (value) {
        case 'default':
          gantt.config.layout = defaultLayout
          break
        case 'horizontalScrollbars':
          gantt.config.layout = gridWidthScrollbarLayout
          break
        case 'resource':
          gantt.config.layout = resourceLayoutGeneral
          break
        case 'universal':
          gantt.config.layout = universalLayout
          break
        case 'complexScrollbars':
          gantt.config.layout = complexLayoutWithScrollbars
          break
      }
      gantt.init('gantt_here')
    },
    // ä»Žç”˜ç‰¹å›¾ä¸­åŒæ­¥é€‰ä¸­çš„ id åˆ° Vue data
    syncSelected() {
      const tasks = gantt.serialize().data || []
      console.log(tasks)
      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>