Skip to content

After encapsulating the Gantt chart as a component in vue2, add data error to the Gantt chart #410

Open
@Murphy-Lv

Description

<template>
  <div>
    <div class="toolbox">
      <el-button type="primary" plain @click="exports" style="margin-left: 20px">导出</el-button>
      <el-button type="primary" plain @click="handleAdd" style="margin-left: 20px">增加</el-button>
      <div class="toolbox-item" style="margin: 0 20px">
        <el-select v-model="value" placeholder="请选择" id="zoom" @change="zoomChangeSelect" style="width: 100px">
          <el-option
            v-for="item in options"
            :key="item.value"
            :label="item.label"
            :value="item.value">
          </el-option>
        </el-select>
      </div>
      <div class="toolbox-item">
        <el-slider v-model="slider" id="zoom-range"  :min="13"  :max="25"   @change='zoomChangeRange'  style="width:200px" :step="3"></el-slider>
      </div>
    </div>
    <div class="gstc-wrapper" ref="gstc" id="gstc"></div>
  </div>
</template>

<script>
import GSTC from "gantt-schedule-timeline-calendar/dist/gstc.wasm.esm.min.js";
import { Plugin as TimelinePointer } from "gantt-schedule-timeline-calendar/dist/plugins/timeline-pointer.esm.min.js";
import { Plugin as Selection } from "gantt-schedule-timeline-calendar/dist/plugins/selection.esm.min.js";
import { Plugin as Bookmarks } from "gantt-schedule-timeline-calendar/dist/plugins/time-bookmarks.esm.min.js";
import { Plugin as ExportImage } from "gantt-schedule-timeline-calendar/dist/plugins/export-image.esm.min.js";
import { Plugin as ExportPDF } from "gantt-schedule-timeline-calendar/dist/plugins/export-pdf.esm.min.js";
import { Plugin as ProgressBar } from 'gantt-schedule-timeline-calendar/dist/plugins/progress-bar.esm.min.js';
import { Plugin as ItemMovement } from 'gantt-schedule-timeline-calendar/dist/plugins/item-movement.esm.min.js';
import { Plugin as ItemResizing } from 'gantt-schedule-timeline-calendar/dist/plugins/item-resizing.esm.min.js';
import "gantt-schedule-timeline-calendar/dist/style.css";
import "gantt-schedule-timeline-calendar/examples/reset.css";
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
let gstc, state;
let rows = {}
let items = {}

export default {
  name: "GSTC",
  props: {
    rowsFromDB:{
      type: Array,
      required: true,
      default: () => []
    },
    itemsFromDB:{
      type: Array,
      required: true,
      default: () => []
    },
    columnsFromDB:{
      type: Array,
      required: true,
      default: () => []
    },
    itemResizingOptions:{
      type: Object,
      required: true
    },
  },
  data() {
    return {
      // rowsFromDB:[
      //   {
      //     id: "1", //行标识符
      //     label: "Row 1",
      //   },
      //   {
      //     id: "2",
      //     label: "Row 2",
      //   },
      //   {
      //     id: "3",
      //     label: "Row 3",
      //   },
      // ],
      // itemsFromDB:[
      //   {
      //     id: "1", // 商品 ID 由 GSTCID 函数包装
      //     label: "Item 1", // 项目标签
      //     rowId: "1", // 此项应出现在哪一行
      //     time: {
      //       // zoom: 21,
      //       start: GSTC.api.date("2020-01-01 09:10:00").startOf("minute").valueOf(),
      //       end: GSTC.api.date("2020-01-01 10:15:00").endOf("minute").valueOf(),
      //       // leftGlobal //我们当前距离左侧的时间(以毫秒为单位) - 当您滚动时,此值将会更新
      //       // leftGlobalDate // 与"leftGlobal"相同,但为dayjs日期而不是毫秒
      //       // rightGlobal // 我们当前距离右侧的时间(以毫秒为单位) - 当您滚动时,此值将会更新
      //       // rightGlobalDate // 与"rightGlobal"相同,但为 dayjs 日期而不是毫秒
      //       // onLevelDates // 您可以使用函数数组来控制日历中显示的日期
      //       // onCurrentViewLevelDates // 与“onLevelDates”相同,但仅限于当前显示的日期 - 水平滚动移动时触发(见下面的通知)
      //       // onDate // 创建新日历日期时将触发的函数数组
      //     },
      //     // classNames:['additional-custom-class'], // 您想要添加到当前项目的 css 类数组,函数也应该返回一个 classNames 数组({item, vido})=>['my-item-class']
      //     style: { background: this.getRandomColor() }, // 你可以定义项目的样式,例如{background:'red'}
      //     // linkedWith: "1" // 链接的其他项目数组,当您调整大小/移动其中一个时,另一个将跟随
      //     // height // 项目高度
      //     // top //  行内顶部偏移量
      //     // gap //  顶部和底部项目间隙
      //     // minWidth // 最小显示宽度
      //     // dependant // 其他依赖项的数组
      //     // overlap // 如果您希望项目与其他项目重叠 - 对于所有项目,请将此处留空,并将默认项目重叠选项设置为 true
      //     description: 'Lorem ipsum dolor sit amet',
      //     progress:20,
      //   },
      //   {
      //     id: "2",
      //     label: "Item 2",
      //     rowId: "1",
      //     time: {
      //       start: GSTC.api.date("2020-01-01 09:10:00").startOf("minute").valueOf(),
      //       end: GSTC.api.date("2020-01-01 09:35:00").endOf("minute").valueOf(),
      //     },
      //     description: 'Lorem ipsum dolor sit amet',
      //     style: { background: this.getRandomColor() },
      //     progress:30,
      //   },
      //   {
      //     id: "3",
      //     label: "Item 3",
      //     rowId: "2",
      //     time: {
      //       start: GSTC.api.date("2020-01-02 08:08:00").startOf("minute").valueOf(),
      //       end: GSTC.api.date("2020-01-02 09:15:00").endOf("minute").valueOf(),
      //     },
      //     description: 'Lorem ipsum dolor sit amet',
      //     style: { background: this.getRandomColor() },
      //     progress:40,
      //   },
      // ],
      // columnsFromDB:[
      //   {
      //     id: "id", //列的 ID
      //     data: ({ row }) => GSTC.api.sourceID(row.id), //string| function 因为string它是一个应该存在于行配置中的属性名称,并且将显示相应的值,如果数据是,function它将以{row, vido}对象作为参数执行 - 该函数应该返回一个字符串或lit-html模板
      //     sortable: ({ row }) => Number(GSTC.api.sourceID(row.id)), // 如果这是一个string则应该包含对行进行排序所需的行字段的名称
      //     width: 80, // 列宽
      //     // 列标题配置如下所述
      //     header: {
      //       content: "ID", //string|lit-html 此标题的标签可以是简单字符串或lit-html模板
      //     },
      //     hidden:false, // 隐藏具有此属性的列
      //   },
      //   {
      //     id: "label",
      //     data: "label",
      //     sortable: "label",
      //     isHTML: false, // 如果设置为 truedata选项将会呈现为 HTML,所以要小心,不要让用户注入任何不安全的东西!
      //     width: 230,
      //     header: {
      //       content: "Label",
      //     },
      //   },
      //   {
      //     id: "action",
      //     sortable: "action",
      //     isHTML: false, // 如果设置为 truedata选项将会呈现为 HTML,所以要小心,不要让用户注入任何不安全的东西!
      //     width: 230,
      //     header: {
      //       content: "操作",
      //     },
      //     data({ row,vido }) {
      //       return vido.html`<el-button type="text" class="split-button" data-id="${row.id}" style="margin-right: 10px;cursor: pointer">拆分</el-button>
      //               <el-button type="text" class="remove-button" data-id="${row.id}" style="cursor: pointer">删除</el-button>`;
      //     }
      //   },
      // ],
      // 项目是否可以上下移动
      itemMovementOptions:{
        // enabled: false,
        snapToTime: {
          start({ startTime }) {
            return startTime;
          },
          end({ endTime }) {
            return endTime;
          },
        },
        autoScroll: {
          speed: {
            horizontal: 1, // turn off horizontal auto scroll
            vertical: 1, // turn off vertical auto scroll
          },
        },
        events:{
          onMove({ items }) {
            return items.before.map((beforeMovementItem, index) => {
              const afterMovementItem = items.after[index];
              const myItem = GSTC.api.merge({}, afterMovementItem);
              if(myItem.resizable==false){
                myItem.rowId = !beforeMovementItem.rowId;
              }else{
                myItem.rowId = beforeMovementItem.rowId;
              }
              return myItem;
            });
          },
          onEnd:({ items })=>{
            this.moveEnd(items)
            return items.after;
          },
        },
      },
      // 项目在同一行是否可以发生碰撞
      // itemResizingOptions:{
      //   enabled: true,
      //   // 项目前后拖动不按单元格拖动
      //   snapToTime: {
      //     start({ startTime }) {
      //       return startTime;
      //     },
      //     end({ endTime }) {
      //       return endTime;
      //     },
      //   },
      //   events:{
      //     onEnd({ items }) {
      //       this.resize(items)
      //       console.log(items,'items')
      //       console.log('Resizing done', items.after);
      //       return items.after;
      //     },
      //   },
      // },
      calendarLevel0:[
        {
          zoomTo: 13,
          period: 'minute',
          main: true,
          periodIncrement: 15,
          format({ timeStart }) {
            return timeStart.format('HH:mm');
          },
        },
        {
          zoomTo: 16,
          period: 'hour',
          main: true,
          periodIncrement: 1,
          format({ timeStart }) {
            return timeStart.format('HH:mm');
          },
        },
        {
          zoomTo: 19,
          period: 'day',
          main: true,
          periodIncrement: 1,
          classNames: ['gstc-date-medium'],
          format({ timeStart, className, vido }) {
            return vido.html`<span class="${className}-content gstc-date-bold">${timeStart.format(
              'DD'
            )}</span> <span class="${className}-content gstc-date-thin">${timeStart.format('dddd')}19</span>`;
          },
        },
        {
          zoomTo: 22,
          period: 'week',
          main: true,
          periodIncrement: 1,
          format({ timeStart, timeEnd, className, vido }) {
            return vido.html`<div class="${className}-content gstc-date-top">${timeStart.format('DD')} - ${timeEnd.format(
              'DD'
            )}</div><div class="${className}-content gstc-date-small gstc-date-thin">${timeStart.format(
              'ddd'
            )} - ${timeEnd.format('dd')}</div>`;
          },
        },
        {
          zoomTo: 25,
          period: 'month',
          main: true,
          periodIncrement: 1,
          classNames: ['gstc-date-month-level-1'],
          format({ timeStart, vido, className }) {
            return vido.html`<div class="${className}-content gstc-date-top">${timeStart.format(
              'MMM'
            )}</div><div class="${className}-content gstc-date-small gstc-date-bottom">${timeStart.format('MM')}</div>`;
          },
        },
        {
          zoomTo: 27,
          period: 'month',
          main: true,
          periodIncrement: 1,
          classNames: ['gstc-date-vertical'],
          format({ timeStart, className, vido }) {
            return vido.html`<div class="${className}-content gstc-date-top">${timeStart.format(
              'MM'
            )}</div><div class="${className}-content gstc-date-extra-small">${timeStart.format('MMM')}</div>`;
          },
        },
        {
          zoomTo: 28,
          period: 'year',
          main: true,
          periodIncrement: 1,
          classNames: ['gstc-date-big'],
          format({ timeStart }) {
            return timeStart.format('YYYY');
          },
        },
        {
          zoomTo: 29,
          period: 'year',
          main: true,
          periodIncrement: 1,
          classNames: ['gstc-date-medium'],
          format({ timeStart }) {
            return timeStart.format('YYYY');
          },
        },
        {
          zoomTo: 30,
          period: 'year',
          main: true,
          periodIncrement: 1,
          classNames: ['gstc-date-medium'],
          format({ timeStart }) {
            return timeStart.format('YY');
          },
        },
        {
          zoomTo: 100,
          period: 'year',
          main: true,
          periodIncrement: 1,
          format() {
            return null;
          },
        },
      ],
      top:[
        {
          zoomTo: 17,
          period: "day",
          periodIncrement: 1,
          classNames: ["gstc-date-medium gstc-date-left"],
          format({ timeStart }) {
            return timeStart.format("YYYY-MM-DD (dddd)");
          },
        },
        {
          zoomTo: 23,
          period: "month",
          periodIncrement: 1,
          format({ timeStart }) {
            return timeStart.format("YYYY-MM");
          },
        },
        {
          zoomTo: 24,
          period: "month",
          periodIncrement: 1,
          format({ timeStart }) {
            return timeStart.format("YYYY-MM");
          },
        },
        {
          zoomTo: 25,
          period: "month",
          periodIncrement: 1,
          format({ timeStart }) {
            return timeStart.format("YYYY-MM");
          },
        },
        {
          zoomTo: 27,
          period: "year",
          periodIncrement: 1,
          format({ timeStart }) {
            return timeStart.format("YYYY");
          },
        },
        {
          zoomTo: 100,
          period: "year",
          periodIncrement: 1,
          format() {
            return null;
          },
        },
      ],
      options: [
        {
          value: '13',
          label: '刻视图'
        }, {
          value: '16',
          label: '时视图'
        }, {
          value: '19',
          label: '日视图'
        }, {
          value: '22',
          label: '周视图'
        }, {
          value: '25',
          label: '月视图'
        }
      ],
      value: '13',
      slider:13,
      from:null,
      to:null,
    }
  },
  mounted() {
    // this.$EventBus.$on("gstcData", (columnsFromDB, rowsFromDB, itemsFromDB) => {
    //   this.gstcInit(columnsFromDB, rowsFromDB, itemsFromDB)
    // });
  },
  beforeDestroy(){
    //移除监听事件
    this.$EventBus.$off("gstcData")
  },

  methods: {
    gstcInit(columnsFromDB, rowsFromDB, itemsFromDB){
      console.log(rowsFromDB,'rowsFromDB')
      console.log(itemsFromDB,'itemsFromDB')
      this.updateItemColors();
      const config = {
        licenseKey:'23',
        plugins: [TimelinePointer(), Selection(), Bookmarks(),ExportImage(), ExportPDF(),ProgressBar(), ItemResizing(this.itemResizingOptions), ItemMovement(this.itemMovementOptions),],
        list: {
          columns: {
            data: GSTC.api.fromArray(columnsFromDB),
          },
          // rows: GSTC.api.fromArray(rowsFromDB),
          rows: rows,
        },
        chart: {
          // items: GSTC.api.fromArray(itemsFromDB),
          items: items,
          calendarLevels:[this.top,this.calendarLevel0],
          item: {
            height: 50,
          },
        },
        locale: {
          name: "zh",
          Now: "Now",
          weekdays:["周日","周一","周二","周三","周四","周五","周六"],
          months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
        },
        actions: {
          'chart-timeline-items-row-item': [this.itemTippy],
        },
        scroll: {
          // 甘特图水平滚动
          horizontal: {
            width: 20,
            minInnerSize: 40,
            multiplier: 3,
            precise: false,
            byPixels: false,
          },
          // 甘特图垂直滚动
          vertical: {
            width: 20,
            minInnerSize: 40,
            multiplier: 3,
            precise: false,
            byPixels: false,
          },
        },
        slots: {
          'chart-timeline-items-row-item': { content: [this.itemSlot] },
        },
      };

      // 从配置对象生成GSTC状态
      state = GSTC.api.stateFromConfig(config);
      state.update('config.chart.time.zoom', this.slider);
      // 创建实例并挂载组件
      gstc = GSTC({
        element: this.$refs.gstc,
        state,
      });

      globalThis.gstc = gstc;
      globalThis.state = state;
      this.setupEventListeners();

    },
    setupEventListeners() {
      // 给动态渲染的按钮绑定事件
      document.querySelectorAll('.split-button').forEach(button => {
        button.addEventListener('click', (event) => {
          const rowId = event.target.getAttribute('data-id');
          this.handleSplit(rowId); // 手动调用 handleSplit 方法
        });
      });
      document.querySelectorAll('.remove-button').forEach(button => {
        button.addEventListener('click', (event) => {
          const rowId = event.target.getAttribute('data-id');
          this.handleRemove(rowId); // 手动调用 handleRemove 方法
        });
      });
    },
    handleSplit(rowId) {
      this.$emit('split',rowId)
      // 执行拆分操作
    },
    handleRemove(rowId){
      this.$emit('remove',rowId)
    },
    // 检查时间是否重叠
    checkTimeOverlap(itemA, itemB) {
      return itemA.time.start < itemB.time.end && itemB.time.start < itemA.time.end;
    },

    // 设置任务条颜色为红色(如果重叠)
    updateItemColors() {
      // 遍历所有的项目
      for (let i = 0; i < this.itemsFromDB.length; i++) {
        const itemA = this.itemsFromDB[i];
        // 对于每个项目,检查和其他同一行的项目是否有时间重叠
        for (let j = i + 1; j < this.itemsFromDB.length; j++) {
          const itemB = this.itemsFromDB[j];
          if(itemA.employeeName&&itemB.employeeName){
            if (itemA.employeeId === itemB.employeeId && this.checkTimeOverlap(itemA, itemB)) {
              // 如果时间重叠,设置为红色
              itemA.style = { background: '#E74C3C' };
              itemB.style = { background: '#E74C3C' };
            }
          }
        }
      }
    },
    /**
     * @description:zoom滑块回调事件
     */
    zoomChangeRange(ev) {
      state.update('config.chart.time.zoom', ev);
    },
    /**
     * @description:zoom选择回调事件
     */
    zoomChangeSelect(ev) {
      state.update('config.chart.time.zoom', ev);
    },
    /**
     * @description:任务条显示toolTip信息方法
     */
    itemTippy(element, data) {
      let _this = this
      _this.setTippyContent(element, data)
      return {
        update(element, data) {
          if (element._tippy) element._tippy.destroy();
          _this.setTippyContent(element, data);
        },
        destroy(element, data) {
          if (element._tippy) element._tippy.destroy();
        },
      };
    },
    setTippyContent(element, data) {
      if (!gstc) return
      if ((!data || !data.item) && element._tippy) return element._tippy.destroy()
      // const itemData = gstc.api.getItemData(data.item.id)
      const itemData = data.itemData
      if (!itemData) return element._tippy.destroy()
      if (itemData.detached && element._tippy) return element._tippy.destroy()
      if (!itemData.detached && !element._tippy) tippy(element, { trigger: 'mouseenter click' })
      if (!element._tippy) return
      const startDate = itemData.time.startDate
      const endDate = itemData.time.endDate
      const remark = data.item.remark?data.item.remark:''
      const tooltipContent = `
                            工序:${data.item.technique}
                            型号:${data.item.itemCode}
                            操作工:${data.item.employee}
                            任务量:${data.item.taskLoadQty}
                            时间:${startDate.format('YYYY-MM-DD HH:mm')}${endDate.format('YYYY-MM-DD HH:mm')}
                            备注:${remark}
                            `
      element._tippy.setContent(tooltipContent)
    },
    /**
     * @description:导出为PDF
     */
    exports(){
      gstc.api.plugins.ExportPDF.download('人员排班');
    },
    /**
     * @description:任务条插槽
     */
    itemSlot(vido, props) {
      const { html, onChange, update } = vido;
      let imageSrc = '';
      let duration = '';
      let employee = '';
      onChange((newProps) => {
        props = newProps;
        if (!props || !props.item) return;
        imageSrc = props.item.img;
        duration = props.item.duration;
        employee = props.item.employee;
        update();
      });

      return (content) => {
        if (!props || !props.item) return content;
        return html`
      <div class="item-text">
        <div class="item-label">${employee}</div>
        <div class="item-description" style="font-size:11px;margin-top:2px;color:#fffffff0;line-height:1em;">
          ${duration}
        </div>
      </div>`;
      };
    },
    generateNewRows(row) {
      console.log(row,'generateNewRows')
      // let rows = {};
      for (let i = 0; i < row.length; i++) {
        const id = GSTC.api.GSTCID(String(this.rowsFromDB[i].id));
        // const id = String(i);
        rows[id] = {
          id,
          employeeName: row[i].employeeName,
        };
      }
      return rows;
    },
    generateNewItems(item) {
      console.log(item,'generateNewItems')
      let rowsIds = Object.keys(rows);
      console.log(rowsIds,'rowsIds')
      let dateIncrement = 0;
      for (let i = 0;  i < item.length; i++) {
        let rowId = GSTC.api.GSTCID(String(item[i].rowId));
        let id = GSTC.api.GSTCID(String(item[i].id));
        if (dateIncrement >= 30) dateIncrement = 0;
        const startTime = item[i].time.start;
        const endTime = item[i].time.end;
        items[id] = {
          id,
          rowId,
          label: `Item ${i + 1}`,
          time: {
            start: startTime,
            end: endTime,
          },
        };
        dateIncrement++;
      }
      return items;
    },
    handleAdd(){
      console.log(this.generateNewRows(),'this.generateNewRows()')
      console.log(this.generateNewItems(),'this.generateNewItems()')
      state.update('config', (config) => {
        config.list.rows = this.generateNewRows();
        config.chart.items = this.generateNewItems();
        return config;
      });
    },
    add(row,item){
      console.log(this.generateNewRows(row),'rows')
      console.log(this.generateNewItems(item),'items')
      console.log(state,'state')
      state.update('config', (config) => {
        config.list.rows = this.generateNewRows(row);
        config.chart.items = this.generateNewItems(item);
        return config;
      });
      setTimeout(()=> {
        this.gstcInit(this.columnsFromDB, this.generateNewRows(row), this.generateNewItems(item))
      },0)
    },
    // 增加进度条
    addItems(item){
      state.update(`config.list.rows.${item.id}`, item);
      state.update(`config.chart.items.${item.id}`, item);
    },
    // 增加行
    addRows(item){
      state.update(`config.list.rows.${item.id}`, item);
    },
    // 删除进度条
    delItems(item){
      const selectedItems = [item]
      state.update('config.chart.items', (items) => {
        for (const item of selectedItems) {
          delete items[item.id];
        }
        return items;
      });
    },
    // 点击行跳转至当前任务
    scrollToItem(row) {
      const api = gstc.api;
      const firstItem = gstc.state.get(`config.chart.items.${row.id}`);
      api.scrollToTime(firstItem.time.start, false);
    },
    // 进度条整个拖拽回调
    moveEnd(items){
      this.$emit('moveEnd',items.after)
    },
  },
};
</script>
<style scoped>

.gstc-wrapper {
  margin: 20px;
}
.toolbox {
  display: flex;
  align-items: center;
}
::v-deep .gstc__chart-timeline-items-row-item {
  border-radius: 4px !important;
}
</style>

The add method in the code
Error message:Item with id "gstcid-1974152527788326943" does not exists in "$data.chart.items". TIP: For performance reasons, state is mutable, so try not to modify items from the "state.get" method before you copy them (with "gstc.api.clone(items)" for example).

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions