diff --git a/common/changes/@visactor/vtable/3828-feature-theme-frozencolumnlineshadow_2025-05-08-10-17.json b/common/changes/@visactor/vtable/3828-feature-theme-frozencolumnlineshadow_2025-05-08-10-17.json new file mode 100644 index 0000000000..9a745e9e9f --- /dev/null +++ b/common/changes/@visactor/vtable/3828-feature-theme-frozencolumnlineshadow_2025-05-08-10-17.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: add frozenColumnLine visible on theme #3828\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/3830-bug-vtable-pivot-treeMode-occorError_2025-05-08-07-38.json b/common/changes/@visactor/vtable/3830-bug-vtable-pivot-treeMode-occorError_2025-05-08-07-38.json new file mode 100644 index 0000000000..f21e4ac098 --- /dev/null +++ b/common/changes/@visactor/vtable/3830-bug-vtable-pivot-treeMode-occorError_2025-05-08-07-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: when no rowTree treeMode occor error #3830\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/feat-gantt-export_2025-05-08-13-36.json b/common/changes/@visactor/vtable/feat-gantt-export_2025-05-08-13-36.json new file mode 100644 index 0000000000..b46804af35 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-gantt-export_2025-05-08-13-36.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/feature-text-fix_2025-04-25-13-09.json b/common/changes/@visactor/vtable/feature-text-fix_2025-04-25-13-09.json new file mode 100644 index 0000000000..a5f0e9598e --- /dev/null +++ b/common/changes/@visactor/vtable/feature-text-fix_2025-04-25-13-09.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add support for text not to be hidden #3802", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-click-edit-state-handling_2025-05-08-11-38.json b/common/changes/@visactor/vtable/fix-click-edit-state-handling_2025-05-08-11-38.json new file mode 100644 index 0000000000..47cd7ddf34 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-click-edit-state-handling_2025-05-08-11-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: unintended edit state activation on functional button clicks\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-pointerdown-resizeTaskBar-resizing_2025-05-09-08-39.json b/common/changes/@visactor/vtable/fix-pointerdown-resizeTaskBar-resizing_2025-05-09-08-39.json new file mode 100644 index 0000000000..96b6f7d1f1 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-pointerdown-resizeTaskBar-resizing_2025-05-09-08-39.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: resolve taskBar width problem when click linkPonitNode #3829\n\n", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "alonemall@163.com" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b383aaec20..c1a7f5156e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -787,6 +787,7 @@ importers: '@visactor/vchart': 1.13.3-alpha.2 '@visactor/vtable': workspace:* '@visactor/vtable-editors': workspace:* + '@visactor/vtable-gantt': workspace:* '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 axios: ^1.4.0 @@ -837,6 +838,7 @@ importers: '@visactor/vchart': 1.13.3-alpha.2 '@visactor/vtable': link:../vtable '@visactor/vtable-editors': link:../vtable-editors + '@visactor/vtable-gantt': link:../vtable-gantt '@vitejs/plugin-react': 3.1.0_vite@3.2.6 axios: 1.8.2 chai: 4.3.4 @@ -4355,7 +4357,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.25.9_@babel+core@7.20.12 magic-string: 0.27.0 react-refresh: 0.14.2 - vite: 3.2.6 + vite: 3.2.6_tp2wsfwniubhwwtz2rzahg2hve transitivePeerDependencies: - supports-color dev: true @@ -5277,6 +5279,7 @@ packages: /bindings/1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true dependencies: file-uri-to-path: 1.0.0 optional: true @@ -7548,6 +7551,7 @@ packages: /file-uri-to-path/1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true optional: true /fill-range/4.0.0: @@ -10758,6 +10762,7 @@ packages: /nan/2.22.2: resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} + requiresBuild: true optional: true /nanoid/3.1.25: diff --git a/docs/assets/demo/en/gantt/gantt-orient.md b/docs/assets/demo/en/gantt/gantt-orient.md new file mode 100644 index 0000000000..9440e7d7b5 --- /dev/null +++ b/docs/assets/demo/en/gantt/gantt-orient.md @@ -0,0 +1,308 @@ +--- +category: examples +group: gantt +title: Gantt Style — Text Not Hidden +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/gantt-label-text.gif +link: gantt/introduction +option: Gantt#taskBar +--- + +# Gantt Style - Text Not Hidden + +This example demonstrates the style configuration of not hiding taskbar text. + +## Key Configuration + +- `orient` Text orientation relative to the taskbar. Optional values: `left`, `top`, `right`, `bottom`, representing the four directions respectively. +- `orientHandleWithOverflow` Specifies the taskbar text orientation when the label cannot fit within the taskbar. Ignored if `orient` is explicitly set. + +## Code Demo + +```javascript livedemo template=vtable +// import * as VTableGantt from '@visactor/vtable-gantt'; +let ganttInstance; +const records = [ + { + id: 1, + title: 'Software Development', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-24', + end: '2024-07-28', + progress: 100, + priority: 'P0' + }, + { + id: 2, + title: 'Project Feature Review', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-25', + end: '2024-07-27', + progress: 90, + priority: 'P0' + }, + { + id: 3, + title: 'Project Create', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-29', + end: '2024-07-31', + progress: 40, + priority: 'P1' + }, + { + id: 4, + title: 'Develop feature 1', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-30', + end: '2024-08-10', + progress: 30, + priority: 'P1' + }, + { + id: 5, + title: 'Determine project scope', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-01', + end: '2024-08-05', + progress: 60, + priority: 'P0' + }, + { + id: 6, + title: 'Project Status Review', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-06', + end: '2024-08-08', + progress: 10, + priority: 'P0' + }, + { + id: 7, + title: 'Feature Testing', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-09', + end: '2024-08-15', + progress: 70, + priority: 'P1' + }, + { + id: 8, + title: 'Project Complete', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-01', + end: '2024-08-10', + progress: 70, + priority: 'P0' + } +]; + +const columns = [ + { + field: 'title', + title: 'title', + width: 'auto', + sort: true, + tree: true, + editor: 'input' + }, + { + field: 'start', + title: 'start', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'end', + title: 'end', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'priority', + title: 'priority', + width: 'auto', + sort: true, + editor: 'input' + }, + { + field: 'progress', + title: 'progress', + width: 'auto', + sort: true, + headerStyle: { + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8', + color: 'green' + }, + editor: 'input' + } +]; +const option = { + overscrollBehavior: 'none', + records, + taskListTable: { + columns, + tableWidth: 250, + minTableWidth: 100, + maxTableWidth: 600, + theme: { + headerStyle: { + borderColor: '#e1e4e8', + borderLineWidth: 1, + fontSize: 18, + fontWeight: 'bold', + color: 'red', + bgColor: '#EEF1F5' + }, + bodyStyle: { + borderColor: '#e1e4e8', + borderLineWidth: [1, 0, 1, 0], + fontSize: 16, + color: '#4D4D4D', + bgColor: '#FFF' + } + } + //rightFrozenColCount: 1 + }, + frame: { + outerFrameStyle: { + borderLineWidth: 2, + borderColor: '#e1e4e8', + cornerRadius: 8 + }, + verticalSplitLineMoveable: true, + verticalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + horizontalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + } + }, + grid: { + weekendBackgroundColor: '#f8f8f8', + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + headerRowHeight: 40, + rowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + // resizable: false, + moveable: true, + hoverBarStyle: { + barOverlayColor: 'rgba(99, 144, 0, 0.4)' + }, + labelText: '{title} {progress}%', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 16, + textAlign: 'left', + textOverflow: 'visible', + orientHandleWithOverflow: 'right', + outsideColor: '#333333' + }, + barStyle: { + width: 20, + /** 任务条的颜色 */ + barColor: '#ee8800', + /** 已完成部分任务条的颜色 */ + completedBarColor: '#91e8e0', + /** 任务条的圆角 */ + cornerRadius: 8, + /** 任务条的边框 */ + borderLineWidth: 1, + /** 边框颜色 */ + borderColor: 'black' + }, + milestoneStyle: { + borderColor: 'red', + borderLineWidth: 1, + fillColor: 'green', + width: 15 + } + }, + timelineHeader: { + colWidth: 50, + backgroundColor: '#EEF1F5', + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + scales: [ + { + unit: 'week', + step: 1, + startOfWeek: 'sunday', + format(date) { + return `Week ${date.dateIndex}`; + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5', + textStick: true + // padding: [0, 30, 0, 20] + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5' + } + } + ] + }, + rowSeriesNumber: { + title: '行号', + dragOrder: true, + headerStyle: { + bgColor: '#EEF1F5', + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8' + } + }, + scrollStyle: { + scrollRailColor: 'RGBA(246,246,246,0.5)', + visible: 'scrolling', + width: 6, + scrollSliderCornerRadius: 2, + scrollSliderColor: '#5cb85c' + } +}; +ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option); +window['ganttInstance'] = ganttInstance; +``` diff --git a/docs/assets/demo/menu.json b/docs/assets/demo/menu.json index f1944b1de4..690228b7c5 100644 --- a/docs/assets/demo/menu.json +++ b/docs/assets/demo/menu.json @@ -342,6 +342,13 @@ "zh": "隐藏底部时间刻度", "en": "Gantt Hide Hour Scale" } + }, + { + "path": "gantt-orient", + "title": { + "zh": "甘特图样式-文字不隐藏", + "en": "Gantt Style —— Text Not Hidden" + } } ] }, diff --git a/docs/assets/demo/zh/gantt/gantt-orient.md b/docs/assets/demo/zh/gantt/gantt-orient.md new file mode 100644 index 0000000000..70884216c0 --- /dev/null +++ b/docs/assets/demo/zh/gantt/gantt-orient.md @@ -0,0 +1,308 @@ +--- +category: examples +group: gantt +title: 甘特图样式-文字不隐藏 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/gantt-label-text.gif +link: gantt/introduction +option: Gantt#taskBar +--- + +# 甘特图样式—文字不隐藏 + +该示例展示了甘特图文字不隐藏的样式配置。 + +## 关键配置 + +- `orient` 相对于任务条文字方位位置,可选值:`left`, `top`, `right`, `bottom`,分别代表左、上、右、下四个方向 +- `orientHandleWithOverflow` 只有当文本在 taskbar 中容纳不下时,会根据该方位将文本显示在任务条旁边。当配置 `orient` 时,该配置无效 + +## 代码演示 + +```javascript livedemo template=vtable +// import * as VTableGantt from '@visactor/vtable-gantt'; +let ganttInstance; +const records = [ + { + id: 1, + title: 'Software Development', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-24', + end: '2024-07-28', + progress: 100, + priority: 'P0' + }, + { + id: 2, + title: 'Project Feature Review', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-25', + end: '2024-07-27', + progress: 90, + priority: 'P0' + }, + { + id: 3, + title: 'Project Create', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-29', + end: '2024-07-31', + progress: 40, + priority: 'P1' + }, + { + id: 4, + title: 'Develop feature 1', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-30', + end: '2024-08-10', + progress: 30, + priority: 'P1' + }, + { + id: 5, + title: 'Determine project scope', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-01', + end: '2024-08-05', + progress: 60, + priority: 'P0' + }, + { + id: 6, + title: 'Project Status Review', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-06', + end: '2024-08-08', + progress: 10, + priority: 'P0' + }, + { + id: 7, + title: 'Feature Testing', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-09', + end: '2024-08-15', + progress: 70, + priority: 'P1' + }, + { + id: 8, + title: 'Project Complete', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-01', + end: '2024-08-10', + progress: 70, + priority: 'P0' + } +]; + +const columns = [ + { + field: 'title', + title: 'title', + width: 'auto', + sort: true, + tree: true, + editor: 'input' + }, + { + field: 'start', + title: 'start', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'end', + title: 'end', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'priority', + title: 'priority', + width: 'auto', + sort: true, + editor: 'input' + }, + { + field: 'progress', + title: 'progress', + width: 'auto', + sort: true, + headerStyle: { + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8', + color: 'green' + }, + editor: 'input' + } +]; +const option = { + overscrollBehavior: 'none', + records, + taskListTable: { + columns, + tableWidth: 250, + minTableWidth: 100, + maxTableWidth: 600, + theme: { + headerStyle: { + borderColor: '#e1e4e8', + borderLineWidth: 1, + fontSize: 18, + fontWeight: 'bold', + color: 'red', + bgColor: '#EEF1F5' + }, + bodyStyle: { + borderColor: '#e1e4e8', + borderLineWidth: [1, 0, 1, 0], + fontSize: 16, + color: '#4D4D4D', + bgColor: '#FFF' + } + } + //rightFrozenColCount: 1 + }, + frame: { + outerFrameStyle: { + borderLineWidth: 2, + borderColor: '#e1e4e8', + cornerRadius: 8 + }, + verticalSplitLineMoveable: true, + verticalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + horizontalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + } + }, + grid: { + weekendBackgroundColor: '#f8f8f8', + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + headerRowHeight: 40, + rowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + // resizable: false, + moveable: true, + hoverBarStyle: { + barOverlayColor: 'rgba(99, 144, 0, 0.4)' + }, + labelText: '{title} {progress}%', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 16, + textAlign: 'left', + textOverflow: 'visible', + orientHandleWithOverflow: 'right', + outsideColor: '#333333' + }, + barStyle: { + width: 20, + /** 任务条的颜色 */ + barColor: '#ee8800', + /** 已完成部分任务条的颜色 */ + completedBarColor: '#91e8e0', + /** 任务条的圆角 */ + cornerRadius: 8, + /** 任务条的边框 */ + borderLineWidth: 1, + /** 边框颜色 */ + borderColor: 'black' + }, + milestoneStyle: { + borderColor: 'red', + borderLineWidth: 1, + fillColor: 'green', + width: 15 + } + }, + timelineHeader: { + colWidth: 50, + backgroundColor: '#EEF1F5', + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + scales: [ + { + unit: 'week', + step: 1, + startOfWeek: 'sunday', + format(date) { + return `Week ${date.dateIndex}`; + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5', + textStick: true + // padding: [0, 30, 0, 20] + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5' + } + } + ] + }, + rowSeriesNumber: { + title: '行号', + dragOrder: true, + headerStyle: { + bgColor: '#EEF1F5', + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8' + } + }, + scrollStyle: { + scrollRailColor: 'RGBA(246,246,246,0.5)', + visible: 'scrolling', + width: 6, + scrollSliderCornerRadius: 2, + scrollSliderColor: '#5cb85c' + } +}; +ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option); +window['ganttInstance'] = ganttInstance; +``` diff --git a/docs/assets/guide/en/plugin/contribute.md b/docs/assets/guide/en/plugin/contribute.md index 7ab2036796..f47514b782 100644 --- a/docs/assets/guide/en/plugin/contribute.md +++ b/docs/assets/guide/en/plugin/contribute.md @@ -34,6 +34,28 @@ export interface IVTablePlugin { The `runTime` parameter specifies when the plugin will run, configuring it with event types from `TableEvents`. +The `Gantt` plugin needs to implement the `VTableGantt.plugins.IGanttPlugin` interface. + +```ts +// Plugin unified interface +export interface IGanttPlugin { + // Plugin unique identifier + id: string; + // Plugin name + name: string; + // Plugin runtime trigger, if not passed in, will run directly during the Gantt build by default + runTime?: EVENT_TYPES[keyof EVENT_TYPES][]; + // Initialization method + run: (...args: any[]) => void; + // Update method, called when Gantt data or configuration updates + update?: () => void; + // Destruction method, called before Gantt instance is destroyed + release?: (gantt: Gantt) => void; +} +``` + +The `runTime` parameter specifies when the plugin will run, configuring it with event types from `EVENT_TYPES`. + #### Component Lifecycle Process:
diff --git a/docs/assets/guide/en/plugin/gantt-export-image.md b/docs/assets/guide/en/plugin/gantt-export-image.md new file mode 100644 index 0000000000..8d9b6ef66c --- /dev/null +++ b/docs/assets/guide/en/plugin/gantt-export-image.md @@ -0,0 +1,384 @@ +# Gantt Chart Export Plugin + +## Feature Introduction + +`ExportGanttPlugin` is a plugin written to support the full export of Gantt charts and to adapt to the size of the Gantt chart. + +This plugin will take effect when the Gantt chart is being `constructor` + +When you need to export an image, you can execute`exportGanttPlugin.exportToImage` to do so. + +## Plugin Configuration + +When you call`exportGanttPlugin.exportToImage`,it also needs to accept the following parameters to change the export image settings +``` +fileName: 'Gantt chart export test', +type: formatSelect.value as 'png' | 'jpeg', +// resolution ratio +scale: Number(scaleSelect.value), +backgroundColor: bgColorInput.value, +// The quality of the exported pictures +quality: 1 +``` + +## Plugin example +Initialize the plugin object and add it to the plugins in the Gantt configuration +``` +const exportGanttPlugin = new ExportGanttPlugin(); +const option = { + records, + columns, + padding: 30, + plugins: [exportGanttPlugin] +}; +``` + +```javascript livedemo template=vtable +// The plugin package needs to be introduced when in use@visactor/vtable-plugins +// import * as VTablePlugins from '@visactor/vtable-plugins'; +const EXPORT_PANEL_ID = 'gantt-export-panel'; +const exportGanttPlugin = new VTablePlugins.ExportGanttPlugin(); +const records = [ + { + id: 1, + title: 'Task 1', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-24', + end: '2024-07-26', + progress: 31, + priority: 'P0' + }, + { + id: 2, + title: 'Task 2', + developer: 'liufangfang.jane@bytedance.com', + start: '07/24/2024', + end: '08/04/2024', + progress: 60, + priority: 'P0' + }, + { + id: 3, + title: 'Task 3', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-04', + end: '2024-08-04', + progress: 100, + priority: 'P1' + }, + { + id: 4, + title: 'Task 4', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-26', + end: '2024-07-28', + progress: 31, + priority: 'P0' + }, + { + id: 5, + title: 'Task 5', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-26', + end: '2024-07-28', + progress: 60, + priority: 'P0' + }, + { + id: 6, + title: 'Task 6', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-29', + end: '2024-08-11', + progress: 100, + priority: 'P1' + } +]; + +const columns = [ + { + field: 'title', + title: 'title', + width: 'auto', + sort: true, + tree: true, + editor: 'input' + }, + { + field: 'start', + title: 'start', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'end', + title: 'end', + width: 'auto', + sort: true, + editor: 'date-input' + } +]; +const option = { + overscrollBehavior: 'none', + records, + taskListTable: { + columns, + tableWidth: 250, + minTableWidth: 100, + maxTableWidth: 600, + theme: { + headerStyle: { + borderColor: '#e1e4e8', + borderLineWidth: 1, + fontSize: 18, + fontWeight: 'bold', + color: 'red', + bgColor: '#EEF1F5' + }, + bodyStyle: { + borderColor: '#e1e4e8', + borderLineWidth: [1, 0, 1, 0], + fontSize: 16, + color: '#4D4D4D', + bgColor: '#FFF' + } + } + //rightFrozenColCount: 1 + }, + frame: { + outerFrameStyle: { + borderLineWidth: 2, + borderColor: '#e1e4e8', + cornerRadius: 8 + }, + verticalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + horizontalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + verticalSplitLineMoveable: true, + verticalSplitLineHighlight: { + lineColor: 'green', + lineWidth: 3 + } + }, + grid: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + headerRowHeight: 40, + rowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + resizable: true, + moveable: true, + hoverBarStyle: { + barOverlayColor: 'rgba(99, 144, 0, 0.4)' + }, + labelText: '{title} complete {progress}%', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 16, + textAlign: 'left', + textOverflow: 'ellipsis' + }, + barStyle: { + width: 20, + barColor: '#ee8800', + completedBarColor: '#91e8e0', + cornerRadius: 8, + borderLineWidth: 1, + borderColor: 'black' + } + }, + timelineHeader: { + colWidth: 100, + backgroundColor: '#EEF1F5', + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + scales: [ + { + unit: 'week', + step: 1, + startOfWeek: 'sunday', + format(date) { + return `Week ${date.dateIndex}`; + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5', + textStick: true + // padding: [0, 30, 0, 20] + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5' + } + } + ] + }, + markLine: [ + { + date: '2024/8/02', + scrollToMarkLine: true, + position: 'left', + style: { + lineColor: 'red', + lineWidth: 1 + } + } + ], + rowSeriesNumber: { + title: '行号', + dragOrder: true, + headerStyle: { + bgColor: '#EEF1F5', + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8' + } + }, + scrollStyle: { + scrollRailColor: 'RGBA(246,246,246,0.5)', + visible: 'scrolling', + width: 6, + scrollSliderCornerRadius: 2, + scrollSliderColor: '#5cb85c' + }, + plugins: [exportGanttPlugin] +}; + +const container = document.getElementById(CONTAINER_ID); + +// Create a packaging container +const wrapper = document.createElement('div'); +wrapper.style.height = '100%'; +wrapper.style.width = '100%'; +wrapper.style.position = 'relative'; +container.appendChild(wrapper); + +// Create the export panel and put it into the packaging container +const exportPanel = document.createElement('div'); +exportPanel.id = EXPORT_PANEL_ID; +exportPanel.style.cssText = 'padding: 2px; background-color: #f6f6f6; margin-bottom: 2px; position: absolute; z-index: 1; border: 1px solid black; opacity: 0.5;'; +wrapper.appendChild(exportPanel); + +// Create a Gantt chart container and place it in the packaging container +const ganttContainer = document.createElement('div'); +ganttContainer.style.height = '100%'; +ganttContainer.style.width = '100%'; +ganttContainer.style.position = 'relative'; +wrapper.appendChild(ganttContainer); + +// File format selection +const formatSelect = document.createElement('select'); +formatSelect.innerHTML = ` + +`; +formatSelect.style.marginRight = '5px'; + +// Zoom ratio selection +const scaleSelect = document.createElement('select'); +scaleSelect.innerHTML = ` + + + +`; +scaleSelect.style.marginRight = '5px'; + +// Background color selection +const bgColorInput = document.createElement('input'); +bgColorInput.type = 'color'; +bgColorInput.value = '#ffffff'; +bgColorInput.style.marginRight = '5px'; + +// Export button +const exportButton = document.createElement('button'); +exportButton.textContent = '导出甘特图'; +exportButton.style.marginLeft = '5px'; + +const infoText = document.createElement('div'); +infoText.innerHTML = '导出功能会直接捕获完整的甘特图和任务列表,即使部分内容在滚动区域外。'; +infoText.style.marginTop = '10px'; +infoText.style.fontSize = '12px'; +infoText.style.color = '#666'; + +// 添加控件到面板 +exportPanel.appendChild(document.createTextNode('格式: ')); +exportPanel.appendChild(formatSelect); +exportPanel.appendChild(document.createTextNode('缩放: ')); +exportPanel.appendChild(scaleSelect); +exportPanel.appendChild(document.createTextNode('背景色: ')); +exportPanel.appendChild(bgColorInput); +exportPanel.appendChild(exportButton); +exportPanel.appendChild(infoText); + +const gantt = new VTableGantt.Gantt(ganttContainer, option); + +// Bind the export event +exportButton.onclick = async () => { + try { + exportButton.disabled = true; + exportButton.textContent = '导出中...'; + + await exportGanttPlugin.exportToImage({ + fileName: '甘特图导出测试', + type: 'png', + scale: Number(scaleSelect.value), + backgroundColor: bgColorInput.value, + quality: 1 + }); + + exportButton.textContent = '导出成功!'; + setTimeout(() => { + exportButton.disabled = false; + exportButton.textContent = '导出甘特图'; + }, 2000); + } catch (error) { + exportButton.textContent = '导出失败'; + setTimeout(() => { + exportButton.disabled = false; + exportButton.textContent = '导出甘特图'; + }, 2000); + } +}; +``` +# This document was contributed by: + +[Abstract chips](https://github.com/Violet2314) \ No newline at end of file diff --git a/docs/assets/guide/en/plugin/rotate-table.md b/docs/assets/guide/en/plugin/rotate-table.md index c6f36b2e03..b8ac6a91c9 100644 --- a/docs/assets/guide/en/plugin/rotate-table.md +++ b/docs/assets/guide/en/plugin/rotate-table.md @@ -11,6 +11,8 @@ The plugin adds the rotate90WithTransform and cancelTransform methods to the tab - rotate90WithTransform: Rotate 90 degrees - cancelTransform: Cancel rotation +**Generally speaking, plugins do not need to bind APIs to table instances. They can have APIs of their own and be called directly by the business layer. For example: rotatePlugin.rotate90WithTransform()** + Please follow the example process below: 1. Ensure that the selected object is the upper container of the table, and the container of the table is full screen. The selected object can be a div or body that covers the entire screen. 2. Before calling the rotate90WithTransform interface, adjust the container's width and height. diff --git a/docs/assets/guide/en/plugin/usage.md b/docs/assets/guide/en/plugin/usage.md index 4a3470e6fe..6841019ec4 100644 --- a/docs/assets/guide/en/plugin/usage.md +++ b/docs/assets/guide/en/plugin/usage.md @@ -50,4 +50,11 @@ If you encounter issues with plugin usage, please provide feedback promptly. | `HighlightHeaderWhenSelectCellPlugin` | Highlight the selected cell | `ListTable`,`PivotTable` | | `ExcelEditCellKeyboardPlugin` | Excel edit cell keyboard plugin | `ListTable`,`PivotTable` | | `TableCarouselAnimationPlugin` | Table carousel animation plugin | `ListTable`,`PivotTable` | -| `RotateTablePlugin` | Table rotation plugin | `ListTable`,`PivotTable` | \ No newline at end of file +| `RotateTablePlugin` | Table rotation plugin | `ListTable`,`PivotTable` | + +
+ +Gantt chart VTable-Gantt component currently supports the following plugins: +| Plugin Name | Plugin Description | Applicable Object | +| --- | --- | --- | +| `ExportGanttPlugin` | Realize the full export of Gantt charts and be able to adapt to the size of the Gantt chart | `Gantt` | \ No newline at end of file diff --git a/docs/assets/guide/en/theme_and_style/theme.md b/docs/assets/guide/en/theme_and_style/theme.md index 87e6832d0b..93f24c29fd 100644 --- a/docs/assets/guide/en/theme_and_style/theme.md +++ b/docs/assets/guide/en/theme_and_style/theme.md @@ -204,7 +204,8 @@ const theme = { shadow: { width: 4, startColor: 'rgba(00, 24, 47, 0.05)', - endColor: 'rgba(00, 24, 47, 0)' + endColor: 'rgba(00, 24, 47, 0)', + visible: 'scrolling' } }, //菜单样式 diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index 9cf387f605..c7238df1d7 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -808,6 +808,13 @@ "zh": "表格旋转", "en": "rotate table" } + }, + { + "path": "gantt-export-image", + "title":{ + "zh": "全量导出甘特图", + "en": "gantt export image" + } } ] }, diff --git a/docs/assets/guide/zh/plugin/contribute.md b/docs/assets/guide/zh/plugin/contribute.md index 7365d2cfa8..13193818f0 100644 --- a/docs/assets/guide/zh/plugin/contribute.md +++ b/docs/assets/guide/zh/plugin/contribute.md @@ -34,6 +34,28 @@ export interface IVTablePlugin { 其中`runTime`指定了插件的运行时机,配置是`TableEvents`中的事件类型。 +注意:甘特图VTable-Gantt组件的插件需要实现 `VTableGantt.plugins.IGanttPlugin` 接口。 + +```ts +// 插件统一接口 +export interface IGanttPlugin { + // 插件唯一标识 + id: string; + // 插件名称 + name: string; + // 插件运行时机,如果没有传入的话默认会Gantt构建时直接运行 + runTime?: EVENT_TYPES[keyof EVENT_TYPES][]; + // 初始化方法 + run: (...args: any[]) => void; + // 更新方法,当Gantt数据或配置更新时调用 + update?: () => void; + // 销毁方法,在Gantt实例销毁前调用 + release?: (gantt: Gantt) => void; +} +``` + +其中`runTime`指定了插件的运行时机,配置是`EVENT_TYPES`中的事件类型。 + #### 组件的生命周期过程:
diff --git a/docs/assets/guide/zh/plugin/gantt-export-image.md b/docs/assets/guide/zh/plugin/gantt-export-image.md new file mode 100644 index 0000000000..cfff3360d6 --- /dev/null +++ b/docs/assets/guide/zh/plugin/gantt-export-image.md @@ -0,0 +1,392 @@ +# 甘特图导出插件 + +## 功能介绍 + +`ExportGanttPlugin`是为了支持让甘特图全量的导出并且可以适应甘特图的大小而写的插件 + +该插件会在Gantt的`constructor`的时候开始生效 + +当需要导出图片的时候,你可以去执行`exportGanttPlugin.exportToImage`来导出图片 + +## 插件配置 + +当你调用`exportGanttPlugin.exportToImage`是,里面还需要接受以下参数来更改导出图片的参数 + +``` +fileName: '甘特图导出测试', +type: formatSelect.value as 'png' | 'jpeg', +// 分辨率倍数 +scale: Number(scaleSelect.value), +backgroundColor: bgColorInput.value, +// 导出的图片的质量 +quality: 1 +``` + +## 插件示例 +初始化插件对象,添加到Gantt配置中的plugins中 +``` +const exportGanttPlugin = new ExportGanttPlugin(); +const option = { + records, + columns, + padding: 30, + plugins: [exportGanttPlugin] +}; +``` + +```javascript livedemo template=vtable +// 使用时需要引入插件包@visactor/vtable-plugins +// import * as VTablePlugins from '@visactor/vtable-plugins'; +const EXPORT_PANEL_ID = 'gantt-export-panel'; +const exportGanttPlugin = new VTablePlugins.ExportGanttPlugin(); +const records = [ + { + id: 1, + title: 'Task 1', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-24', + end: '2024-07-26', + progress: 31, + priority: 'P0' + }, + { + id: 2, + title: 'Task 2', + developer: 'liufangfang.jane@bytedance.com', + start: '07/24/2024', + end: '08/04/2024', + progress: 60, + priority: 'P0' + }, + { + id: 3, + title: 'Task 3', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-04', + end: '2024-08-04', + progress: 100, + priority: 'P1' + }, + { + id: 4, + title: 'Task 4', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-26', + end: '2024-07-28', + progress: 31, + priority: 'P0' + }, + { + id: 5, + title: 'Task 5', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-26', + end: '2024-07-28', + progress: 60, + priority: 'P0' + }, + { + id: 6, + title: 'Task 6', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-29', + end: '2024-08-11', + progress: 100, + priority: 'P1' + } +]; + +const columns = [ + { + field: 'title', + title: 'title', + width: 'auto', + sort: true, + tree: true, + editor: 'input' + }, + { + field: 'start', + title: 'start', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'end', + title: 'end', + width: 'auto', + sort: true, + editor: 'date-input' + } +]; +const option = { + overscrollBehavior: 'none', + records, + taskListTable: { + columns, + tableWidth: 250, + minTableWidth: 100, + maxTableWidth: 600, + theme: { + headerStyle: { + borderColor: '#e1e4e8', + borderLineWidth: 1, + fontSize: 18, + fontWeight: 'bold', + color: 'red', + bgColor: '#EEF1F5' + }, + bodyStyle: { + borderColor: '#e1e4e8', + borderLineWidth: [1, 0, 1, 0], + fontSize: 16, + color: '#4D4D4D', + bgColor: '#FFF' + } + } + //rightFrozenColCount: 1 + }, + frame: { + outerFrameStyle: { + borderLineWidth: 2, + borderColor: '#e1e4e8', + cornerRadius: 8 + }, + verticalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + horizontalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + verticalSplitLineMoveable: true, + verticalSplitLineHighlight: { + lineColor: 'green', + lineWidth: 3 + } + }, + grid: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + headerRowHeight: 40, + rowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + resizable: true, + moveable: true, + hoverBarStyle: { + barOverlayColor: 'rgba(99, 144, 0, 0.4)' + }, + labelText: '{title} complete {progress}%', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 16, + textAlign: 'left', + textOverflow: 'ellipsis' + }, + barStyle: { + width: 20, + /** 任务条的颜色 */ + barColor: '#ee8800', + /** 已完成部分任务条的颜色 */ + completedBarColor: '#91e8e0', + /** 任务条的圆角 */ + cornerRadius: 8, + /** 任务条的边框 */ + borderLineWidth: 1, + /** 边框颜色 */ + borderColor: 'black' + } + }, + timelineHeader: { + colWidth: 100, + backgroundColor: '#EEF1F5', + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + scales: [ + { + unit: 'week', + step: 1, + startOfWeek: 'sunday', + format(date) { + return `Week ${date.dateIndex}`; + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5', + textStick: true + // padding: [0, 30, 0, 20] + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5' + } + } + ] + }, + markLine: [ + { + date: '2024/8/02', + scrollToMarkLine: true, + position: 'left', + style: { + lineColor: 'red', + lineWidth: 1 + } + } + ], + rowSeriesNumber: { + title: '行号', + dragOrder: true, + headerStyle: { + bgColor: '#EEF1F5', + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8' + } + }, + scrollStyle: { + scrollRailColor: 'RGBA(246,246,246,0.5)', + visible: 'scrolling', + width: 6, + scrollSliderCornerRadius: 2, + scrollSliderColor: '#5cb85c' + }, + plugins: [exportGanttPlugin] +}; + +const container = document.getElementById(CONTAINER_ID); + +// 创建一个包装容器 +const wrapper = document.createElement('div'); +wrapper.style.height = '100%'; +wrapper.style.width = '100%'; +wrapper.style.position = 'relative'; +container.appendChild(wrapper); + +// 创建导出面板,放入包装容器 +const exportPanel = document.createElement('div'); +exportPanel.id = EXPORT_PANEL_ID; +exportPanel.style.cssText = 'padding: 2px; background-color: #f6f6f6; margin-bottom: 2px; position: absolute; z-index: 1; border: 1px solid black; opacity: 0.5;'; +wrapper.appendChild(exportPanel); + +// 创建甘特图容器,放入包装容器 +const ganttContainer = document.createElement('div'); +ganttContainer.style.height = '100%'; +ganttContainer.style.width = '100%'; +ganttContainer.style.position = 'relative'; +wrapper.appendChild(ganttContainer); + +// 文件格式选择 +const formatSelect = document.createElement('select'); +formatSelect.innerHTML = ` + +`; +formatSelect.style.marginRight = '5px'; + +// 缩放比例选择 +const scaleSelect = document.createElement('select'); +scaleSelect.innerHTML = ` + + + +`; +scaleSelect.style.marginRight = '5px'; + +// 背景色选择 +const bgColorInput = document.createElement('input'); +bgColorInput.type = 'color'; +bgColorInput.value = '#ffffff'; +bgColorInput.style.marginRight = '5px'; + +// 导出按钮 +const exportButton = document.createElement('button'); +exportButton.textContent = '导出甘特图'; +exportButton.style.marginLeft = '5px'; + +// 说明文本 +const infoText = document.createElement('div'); +infoText.innerHTML = '导出功能会直接捕获完整的甘特图和任务列表,即使部分内容在滚动区域外。'; +infoText.style.marginTop = '10px'; +infoText.style.fontSize = '12px'; +infoText.style.color = '#666'; + +// 添加控件到面板 +exportPanel.appendChild(document.createTextNode('格式: ')); +exportPanel.appendChild(formatSelect); +exportPanel.appendChild(document.createTextNode('缩放: ')); +exportPanel.appendChild(scaleSelect); +exportPanel.appendChild(document.createTextNode('背景色: ')); +exportPanel.appendChild(bgColorInput); +exportPanel.appendChild(exportButton); +exportPanel.appendChild(infoText); + +// 创建甘特图实例 +const gantt = new VTableGantt.Gantt(ganttContainer, option); + +// 绑定导出事件 +exportButton.onclick = async () => { + try { + exportButton.disabled = true; + exportButton.textContent = '导出中...'; + + await exportGanttPlugin.exportToImage({ + fileName: '甘特图导出测试', + type: 'png', + scale: Number(scaleSelect.value), + backgroundColor: bgColorInput.value, + quality: 1 + }); + + exportButton.textContent = '导出成功!'; + setTimeout(() => { + exportButton.disabled = false; + exportButton.textContent = '导出甘特图'; + }, 2000); + } catch (error) { + exportButton.textContent = '导出失败'; + setTimeout(() => { + exportButton.disabled = false; + exportButton.textContent = '导出甘特图'; + }, 2000); + } +}; +``` +# 本文档由由以下人员贡献 + +[抽象薯片](https://github.com/Violet2314) \ No newline at end of file diff --git a/docs/assets/guide/zh/plugin/rotate-table.md b/docs/assets/guide/zh/plugin/rotate-table.md index a5049e463e..1ae85d0d78 100644 --- a/docs/assets/guide/zh/plugin/rotate-table.md +++ b/docs/assets/guide/zh/plugin/rotate-table.md @@ -11,6 +11,8 @@ - rotate90WithTransform:旋转90度 - cancelTransform:取消旋转 +**一般情况下插件不需要将api绑定到table实例上,可以插件自身拥有api,然后由业务方直接调用。如:rotatePlugin.rotate90WithTransform( )** + 请按照下面示例过程使用: 1. 确保选择对象是表格的上层容器,且表格的容器是全屏的。选择对象可以是覆盖整屏的div或者body。 2. 在调用rotate90WithTransform接口前,将容器的宽高调整好。 diff --git a/docs/assets/guide/zh/plugin/usage.md b/docs/assets/guide/zh/plugin/usage.md index 51135567f4..de35f3e921 100644 --- a/docs/assets/guide/zh/plugin/usage.md +++ b/docs/assets/guide/zh/plugin/usage.md @@ -51,4 +51,12 @@ const option: VTable.ListTableConstructorOptions = { | `HighlightHeaderWhenSelectCellPlugin` | 高亮选中单元格 | `ListTable`,`PivotTable` | | `ExcelEditCellKeyboardPlugin` | Excel编辑单元格键盘插件 | `ListTable`,`PivotTable` | | `TableCarouselAnimationPlugin` | 表格轮播动画插件 | `ListTable`,`PivotTable` | -| `RotateTablePlugin` | 表格旋转插件 | `ListTable`,`PivotTable` | \ No newline at end of file +| `RotateTablePlugin` | 表格旋转插件 | `ListTable`,`PivotTable` | + +
+ +甘特图VTabe-Gantt组件目前支持的插件有: + +| 插件名称 | 插件描述 | 适用对象 | +| --- | --- | --- | +| `ExportGanttPlugin` | 实现全量导出甘特图,可以自适应甘特图的大小 | `Gantt` | \ No newline at end of file diff --git a/docs/assets/guide/zh/theme_and_style/theme.md b/docs/assets/guide/zh/theme_and_style/theme.md index 12eb6d5c8f..3fa9bf4614 100644 --- a/docs/assets/guide/zh/theme_and_style/theme.md +++ b/docs/assets/guide/zh/theme_and_style/theme.md @@ -204,7 +204,8 @@ const theme = { shadow: { width: 4, startColor: 'rgba(00, 24, 47, 0.05)', - endColor: 'rgba(00, 24, 47, 0)' + endColor: 'rgba(00, 24, 47, 0)', + visible: 'scrolling' } }, //菜单样式 diff --git a/docs/assets/option/en/common/frozen-column-line-style.md b/docs/assets/option/en/common/frozen-column-line-style.md index 73e2d2df4c..9a04876273 100644 --- a/docs/assets/option/en/common/frozen-column-line-style.md +++ b/docs/assets/option/en/common/frozen-column-line-style.md @@ -15,4 +15,12 @@ Shadow Overall Width Start Color ###${prefix} endColor(string) -End Color \ No newline at end of file +End Color + +###${prefix} visible(string) + +Shadow Visible Time, default is `scrolling`. + +- always: always show +- scrolling: show when scrolling + diff --git a/docs/assets/option/en/common/gantt/task-bar-label-text-style.md b/docs/assets/option/en/common/gantt/task-bar-label-text-style.md index 1d93d5430c..1071aa2f06 100644 --- a/docs/assets/option/en/common/gantt/task-bar-label-text-style.md +++ b/docs/assets/option/en/common/gantt/task-bar-label-text-style.md @@ -1,6 +1,7 @@ {{ target: common-gantt-task-bar-label-text-style }} The definition of ITaskBarLabelTextStyle is: + ``` export interface ITaskBarLabelTextStyle { fontFamily?: string; @@ -10,5 +11,9 @@ export interface ITaskBarLabelTextStyle { textOverflow?: string; textBaseline?: 'alphabetic' | 'bottom' | 'middle' | 'top'; // Sets the vertical alignment of the text within the cell padding?: number | number[]; + /** Text orientation relative to the taskbar. Optional values: 'left', 'top', 'right', 'bottom', representing the four directions respectively. */ + orient?: 'left' | 'top' | 'right' | 'bottom'; + /** Specifies the taskbar text orientation when the label cannot fit within the taskbar. Ignored if `orient` is explicitly set. */ + orientHandleWithOverflow?: 'left' | 'top' | 'right' | 'bottom'; } ``` diff --git a/docs/assets/option/zh/common/frozen-column-line-style.md b/docs/assets/option/zh/common/frozen-column-line-style.md index 39af7f912e..e2cd954a22 100644 --- a/docs/assets/option/zh/common/frozen-column-line-style.md +++ b/docs/assets/option/zh/common/frozen-column-line-style.md @@ -15,4 +15,15 @@ 开始颜色 ###${prefix} endColor(string) -结束颜色 \ No newline at end of file +结束颜色 + +###${prefix} visible(string) +阴影显示时机,默认为 `scrolling`。 + +- always: 总是显示 +- scrolling: 滚动时显示 + + + + + diff --git a/docs/assets/option/zh/common/gantt/task-bar-label-text-style.md b/docs/assets/option/zh/common/gantt/task-bar-label-text-style.md index fff8f7c7c4..393723a229 100644 --- a/docs/assets/option/zh/common/gantt/task-bar-label-text-style.md +++ b/docs/assets/option/zh/common/gantt/task-bar-label-text-style.md @@ -1,6 +1,7 @@ {{ target: common-gantt-task-bar-label-text-style }} -ITaskBarLabelTextStyle的定义为: +ITaskBarLabelTextStyle 的定义为: + ``` export interface ITaskBarLabelTextStyle { fontFamily?: string; @@ -10,5 +11,9 @@ export interface ITaskBarLabelTextStyle { textOverflow?: string; textBaseline?: 'alphabetic' | 'bottom' | 'middle' | 'top'; // 设置单元格内文字的垂直对齐方式 padding?: number | number[]; + /** 相对于任务条文字方位位置,可选值:'left', 'top', 'right', 'bottom',分别代表左、上、右、下四个方向 */ + orient?: 'left' | 'top' | 'right' | 'bottom'; + /** 只有当文本在 taskbar 中容纳不下时,会根据该方位将文本显示在任务条旁边。当配置 orient 时,该配置无效 */ + orientHandleWithOverflow?: 'left' | 'top' | 'right' | 'bottom'; } -``` \ No newline at end of file +``` diff --git a/packages/vtable-gantt/examples/gantt/gantt-milestone.ts b/packages/vtable-gantt/examples/gantt/gantt-milestone.ts index a6614db178..42d7c9354b 100644 --- a/packages/vtable-gantt/examples/gantt/gantt-milestone.ts +++ b/packages/vtable-gantt/examples/gantt/gantt-milestone.ts @@ -322,7 +322,6 @@ export function createTable() { }, headerRowHeight: 60, rowHeight: 40, - taskBar: { startDateField: 'start', endDateField: 'end', diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index 9e6eb26ffc..35d56f285c 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -65,6 +65,7 @@ import { import { DataSource } from './data/DataSource'; import { isValid } from '@visactor/vutils'; import type { GanttTaskBarNode } from './scenegraph/gantt-node'; +import { PluginManager } from './plugins/plugin-manager'; // import { generateGanttChartColumns } from './gantt-helper'; export function createRootElement(padding: any, className: string = 'vtable-gantt'): HTMLElement { const element = document.createElement('div'); @@ -112,6 +113,8 @@ export class Gantt extends EventTarget { headerHeight: number; gridHeight: number; + pluginManager: PluginManager; + parsedOptions: { timeLineHeaderRowHeights: number[]; rowHeight: number; @@ -240,6 +243,7 @@ export class Gantt extends EventTarget { this.scenegraph.afterCreateSceneGraph(); this._scrollToMarkLine(); + this.pluginManager = new PluginManager(this, options); } renderTaskBarsTable() { @@ -1006,6 +1010,7 @@ export class Gantt extends EventTarget { this.horizontalSplitLine && parentElement.removeChild(this.horizontalSplitLine); } this.scenegraph = null; + this.pluginManager.release(); } updateOption(options: GanttConstructorOptions) { diff --git a/packages/vtable-gantt/src/gantt-helper.ts b/packages/vtable-gantt/src/gantt-helper.ts index 8677e06101..930d0e1071 100644 --- a/packages/vtable-gantt/src/gantt-helper.ts +++ b/packages/vtable-gantt/src/gantt-helper.ts @@ -261,10 +261,13 @@ export function initOptions(gantt: Gantt) { fontFamily: options?.taskBar?.labelTextStyle?.fontFamily ?? 'Arial', fontSize: options?.taskBar?.labelTextStyle?.fontSize ?? 20, color: options?.taskBar?.labelTextStyle?.color ?? '#F01', + outsideColor: options?.taskBar?.labelTextStyle?.outsideColor ?? '#333333', textAlign: options?.taskBar?.labelTextStyle?.textAlign ?? 'left', textBaseline: options?.taskBar?.labelTextStyle?.textBaseline ?? 'middle', padding: options?.taskBar?.labelTextStyle?.padding ?? [0, 0, 0, 10], - textOverflow: options?.taskBar?.labelTextStyle?.textOverflow + textOverflow: options?.taskBar?.labelTextStyle?.textOverflow, + orient: options?.taskBar?.labelTextStyle?.orient, + orientHandleWithOverflow: options?.taskBar?.labelTextStyle?.orientHandleWithOverflow }; gantt.parsedOptions.taskBarCustomLayout = options?.taskBar?.customLayout; gantt.parsedOptions.taskBarCreatable = diff --git a/packages/vtable-gantt/src/index.ts b/packages/vtable-gantt/src/index.ts index b0119bbe39..d3fd7b445c 100644 --- a/packages/vtable-gantt/src/index.ts +++ b/packages/vtable-gantt/src/index.ts @@ -17,6 +17,7 @@ import { Gantt } from './Gantt'; import * as tools from './tools'; import * as VRender from './vrender'; import * as VTable from './vtable'; +import * as plugins from './plugins'; export const version = __VERSION__; /** * @namespace VTable @@ -38,5 +39,6 @@ export { TextBaselineType, tools, VRender, - VTable + VTable, + plugins }; diff --git a/packages/vtable-gantt/src/plugins/index.ts b/packages/vtable-gantt/src/plugins/index.ts new file mode 100644 index 0000000000..09479e136a --- /dev/null +++ b/packages/vtable-gantt/src/plugins/index.ts @@ -0,0 +1,2 @@ +export type { IGanttPlugin } from './interface'; +export { PluginManager } from './plugin-manager'; \ No newline at end of file diff --git a/packages/vtable-gantt/src/plugins/interface.ts b/packages/vtable-gantt/src/plugins/interface.ts new file mode 100644 index 0000000000..bacedabd2f --- /dev/null +++ b/packages/vtable-gantt/src/plugins/interface.ts @@ -0,0 +1,25 @@ +import type { EVENT_TYPES } from '../ts-types/EVENT_TYPE' +import type { Gantt } from '../Gantt.ts' + +// 插件生命周期接口 +export interface IGanttPlugin { + // 插件唯一标识 + id: string; + // 插件名称 + name: string; + // // 插件优先级,数字越小优先级越高 TODO:目前还没起作用,后续是否有安排插件优先级的需求 + // priority?: number; + + // // 插件类型,用于区分不同功能的插件 + // type: 'layout' | 'interaction' | 'style' | 'animation'; + // 插件运行时机 + runTime?: EVENT_TYPES[keyof EVENT_TYPES][]; + // // 插件依赖 + // dependencies?: string[]; + // 初始化方法,在Gantt实例创建后、首次渲染前调用 + run: (...args: any[]) => void; + // 更新方法,当Gantt数据或配置更新时调用 + update?: () => void; + // 销毁方法,在Gantt实例销毁前调用 + release?: (gantt: Gantt) => void; +} \ No newline at end of file diff --git a/packages/vtable-gantt/src/plugins/plugin-manager.ts b/packages/vtable-gantt/src/plugins/plugin-manager.ts new file mode 100644 index 0000000000..a55dd54c90 --- /dev/null +++ b/packages/vtable-gantt/src/plugins/plugin-manager.ts @@ -0,0 +1,95 @@ +import type { Gantt } from '../Gantt.ts'; // Adjust path as needed +import type { IGanttPlugin } from './interface'; // Adjust path as needed +import type { GanttConstructorOptions } from '../ts-types/gantt-engine'; // Adjust path as needed + +export class PluginManager { + private plugins: Map = new Map(); + private gantt: Gantt; + + constructor(gantt: Gantt, options: GanttConstructorOptions) { + this.gantt = gantt; + options.plugins?.forEach(plugin => { + this.register(plugin); + this._initializePluginRun(plugin); + }); + } + + private _initializePluginRun(plugin: IGanttPlugin): void { + // 检查 runTime 是否存在 + if (plugin.runTime === undefined) { + try { + plugin.run(this.gantt); + } catch (error) { + console.error(`Error executing run for plugin ${plugin.name}:`, error); + } + } else { + this._bindGanttEventForPlugin(plugin); + } + } + + + // 注册插件 + register(plugin: IGanttPlugin): void { + this.plugins.set(plugin.id, plugin); + } + + // 注册多个插件 + registerAll(plugins: IGanttPlugin[]): void { + plugins.forEach(plugin => this.register(plugin)); + } + + // 获取插件 + getPlugin(id: string): IGanttPlugin | undefined { + return this.plugins.get(id); + } + getPluginByName(name: string): IGanttPlugin | undefined { + return Array.from(this.plugins.values()).find(plugin => plugin.name === name); + } + + // 内部方法:只负责绑定事件,不负责立即执行逻辑 + private _bindGanttEventForPlugin(plugin: IGanttPlugin) { + if (plugin.runTime) { + plugin.runTime.forEach(runTime => { + this.gantt.on(runTime, (...args) => { + try { + plugin.run?.(...args, runTime, this.gantt); + } catch (error) { + console.error(`Error executing plugin ${plugin.name} on event ${String(runTime)}:`, error); + } + }); + }); + } + } + + // 更新所有插件 + updatePlugins(plugins?: IGanttPlugin[]): void { + // 先找到plugins中没有,但this.plugins中有,也就是已经被移除的插件 + const removedPlugins = Array.from(this.plugins.values()).filter(plugin => !plugins?.some(p => p.id === plugin.id)); + removedPlugins.forEach(plugin => { + this.release(); + this.plugins.delete(plugin.id); + }); + // 更新插件 + this.plugins.forEach(plugin => { + if (plugin.update) { + plugin.update(); + } + }); + // 添加新插件 + const addedPlugins = plugins?.filter(plugin => !this.plugins.has(plugin.id)); + addedPlugins?.forEach(plugin => { + this.register(plugin); + this._initializePluginRun(plugin); + }); + } + + release() { + this.plugins.forEach(plugin => { + try { + plugin.release?.(this.gantt); + } catch (error) { + console.error(`Error releasing plugin ${plugin.name}:`, error); + } + }); + } +} \ No newline at end of file diff --git a/packages/vtable-gantt/src/scenegraph/gantt-node.ts b/packages/vtable-gantt/src/scenegraph/gantt-node.ts index 166da1c3f6..1ae695b27f 100644 --- a/packages/vtable-gantt/src/scenegraph/gantt-node.ts +++ b/packages/vtable-gantt/src/scenegraph/gantt-node.ts @@ -1,16 +1,144 @@ import type { IRect, IText, IGroupGraphicAttribute } from '@visactor/vtable/es/vrender'; +import type { ITaskBarLabelTextStyle } from '../ts-types'; import { Group } from '@visactor/vtable/es/vrender'; +import { getTextPos } from '../gantt-helper'; +import { toBoxArray } from '../tools/util'; +import { isValid } from '@visactor/vutils'; +import { textMeasure } from '@visactor/vtable'; export class GanttTaskBarNode extends Group { clipGroupBox: Group; barRect?: IRect; progressRect?: IRect; textLabel?: IText; - name: string; + declare name: string; task_index: number; sub_task_index?: number; record?: any; + labelStyle?: ITaskBarLabelTextStyle; + + _lastWidth?: number; + _lastHeight?: number; + _lastX?: number; + _lastY?: number; constructor(attrs: IGroupGraphicAttribute) { super(attrs); + this._lastWidth = attrs.width; + this._lastHeight = attrs.height; + this._lastX = attrs.x; + this._lastY = attrs.y; + } + + /** + * 更新任务条文本标签的位置和样式 + * @description 根据任务条的大小和配置,更新文本标签的位置、对齐方式等属性 + * orient: 直接将文本显示在指定方位位置 + * orientHandleWithOverflow: 只有当文本溢出时才在指定方位显示,当配置了orient时此配置无效 + */ + updateTextPosition() { + if (!this.textLabel || !this.barRect) { + return; + } + + const labelStyle = this.labelStyle || {}; + const { + textAlign = 'left', + textBaseline = 'middle', + textOverflow, + color = '#333333', + outsideColor = '#333333', + padding: rawPadding = 8 + } = labelStyle; + + const padding = Array.isArray(rawPadding) ? rawPadding[3] : rawPadding; + const barWidth = this.barRect.attribute.width; + const barHeight = this.barRect.attribute.height; + + const fontSize = this.textLabel.attribute.fontSize || 12; + const fontFamily = this.textLabel.attribute.fontFamily || 'Arial'; + const text = String(this.textLabel.attribute.text || ''); + const textWidth = textMeasure.measureTextWidth(text, { fontSize, fontFamily }); + + const textFitsInBar = textWidth + padding * 2 <= barWidth; + const defaultPosition = getTextPos(toBoxArray(padding), textAlign, textBaseline, barWidth, barHeight); + const textPosition = + labelStyle.orient || + (!textFitsInBar && labelStyle.orientHandleWithOverflow ? labelStyle.orientHandleWithOverflow : null); + + this.textLabel.setAttribute('visible', true); + this.textLabel.setAttribute('textBaseline', textBaseline); + + if (textPosition) { + this.textLabel.parent?.removeChild(this.textLabel); + this.appendChild(this.textLabel); + this.textLabel.setAttribute('fill', outsideColor); + this.textLabel.setAttribute('ellipsis', undefined); + this.textLabel.setAttribute('maxLineWidth', undefined); + this.textLabel.setAttribute('zIndex', 10000); + this.setAttribute('zIndex', 10000); + + type Position = { + x: number; + y: number; + align: string; + baseline: string; + }; + type Positions = { + [key: string]: Position; + }; + + const positions: Positions = { + left: { + x: -padding, + y: barHeight / 2, + align: 'right', + baseline: 'middle' + }, + right: { + x: barWidth + padding, + y: barHeight / 2, + align: 'left', + baseline: 'middle' + }, + top: { + x: barWidth / 2, + y: -padding, + align: 'center', + baseline: 'bottom' + }, + bottom: { + x: barWidth / 2, + y: barHeight + padding, + align: 'center', + baseline: 'top' + } + }; + + const pos = positions[textPosition]; + if (pos) { + this.textLabel.setAttribute('x', pos.x); + this.textLabel.setAttribute('y', pos.y); + this.textLabel.setAttribute('textAlign', pos.align); + this.textLabel.setAttribute('textBaseline', pos.baseline); + } + } else { + this.textLabel.parent?.removeChild(this.textLabel); + this.clipGroupBox?.appendChild(this.textLabel); + this.textLabel.setAttribute('x', defaultPosition.x); + this.textLabel.setAttribute('y', defaultPosition.y); + this.textLabel.setAttribute('textAlign', textAlign); + this.textLabel.setAttribute('fill', color); + this.textLabel.setAttribute('maxLineWidth', barWidth - padding); + this.textLabel.setAttribute( + 'ellipsis', + textOverflow === 'clip' + ? '' + : textOverflow === 'ellipsis' + ? '...' + : isValid(textOverflow) + ? textOverflow + : undefined + ); + } } } diff --git a/packages/vtable-gantt/src/scenegraph/task-bar.ts b/packages/vtable-gantt/src/scenegraph/task-bar.ts index 92b806864e..b6cc4f80db 100644 --- a/packages/vtable-gantt/src/scenegraph/task-bar.ts +++ b/packages/vtable-gantt/src/scenegraph/task-bar.ts @@ -275,8 +275,13 @@ export class TaskBar { // dx: 12 + 4, // dy: this._scene._gantt.barLabelStyle.fontSize / 2 }); + barGroup.appendChild(label); barGroupBox.textLabel = label; + + barGroupBox.labelStyle = this._scene._gantt.parsedOptions.taskBarLabelStyle; + + barGroupBox.updateTextPosition(); } return barGroupBox; } @@ -288,6 +293,7 @@ export class TaskBar { const barGroup = this.initBar(index, sub_task_index); if (barGroup) { this.barContainer.insertInto(barGroup, index); //TODO + barGroup.updateTextPosition(); } } initHoverBarIcons() { diff --git a/packages/vtable-gantt/src/state/gantt-table-sync.ts b/packages/vtable-gantt/src/state/gantt-table-sync.ts index 068f950ca3..a4c3555aa5 100644 --- a/packages/vtable-gantt/src/state/gantt-table-sync.ts +++ b/packages/vtable-gantt/src/state/gantt-table-sync.ts @@ -14,7 +14,9 @@ export function syncScrollStateFromTable(gantt: Gantt) { if (args.scrollDirection === 'vertical') { const { scroll } = gantt.taskListTableInstance.stateManager; const { verticalBarPos } = scroll; - gantt.stateManager.setScrollTop(verticalBarPos, false); + if (gantt.stateManager.scroll.verticalBarPos !== verticalBarPos) { + gantt.stateManager.setScrollTop(verticalBarPos, false); + } } }); } diff --git a/packages/vtable-gantt/src/state/state-manager.ts b/packages/vtable-gantt/src/state/state-manager.ts index 80a777d67c..1e31c9aeaa 100644 --- a/packages/vtable-gantt/src/state/state-manager.ts +++ b/packages/vtable-gantt/src/state/state-manager.ts @@ -485,6 +485,7 @@ export class StateManager { if (this.selectedTaskBar.target !== target) { target.setAttribute('zIndex', 0); } + target.updateTextPosition(); this.moveTaskBar.target = null; this.moveTaskBar.deltaX = 0; this.moveTaskBar.deltaY = 0; @@ -604,8 +605,6 @@ export class StateManager { } gantt.scenegraph.updateNextFrame(); - - // } //#region 调整拖拽任务条的大小 startResizeTaskBar(target: Group, x: number, y: number, startOffsetY: number, onIconName: string) { @@ -704,6 +703,7 @@ export class StateManager { reCreateCustomNode(this._gantt, taskBarGroup, taskIndex, sub_task_index); taskBarGroup.setAttribute('zIndex', 0); } + taskBarGroup.updateTextPosition(); this.resizeTaskBar.resizing = false; this.resizeTaskBar.target = null; @@ -743,7 +743,6 @@ export class StateManager { ); this._gantt.scenegraph.updateNextFrame(); - // } //#endregion //#region 生成关联线的交互处理 @@ -751,6 +750,7 @@ export class StateManager { // if (target.name === 'task-bar-hover-shadow') { // target = target.parent.parent; // } + this.resizeTaskBar.resizing = false; // 关联线创建时,任务条resizing状态重置 this.creatingDenpendencyLink.creating = true; this.creatingDenpendencyLink.startClickedPoint = target; this.creatingDenpendencyLink.startX = x; @@ -1071,6 +1071,8 @@ function moveTaskBar(target: GanttTaskBarNode, dx: number, dy: number, state: St ]); } + target.updateTextPosition(); + state._gantt.scenegraph.refreshRecordLinkNodes(taskIndex, sub_task_index, target, dy); } @@ -1110,6 +1112,9 @@ function resizeTaskBar(target: GanttTaskBarNode, dx: number, newWidth: number, s textLabel.setAttribute('maxLineWidth', newWidth - TASKBAR_HOVER_ICON_WIDTH * 2); textLabel.setAttribute('x', position.x); } + + target.updateTextPosition(); + state.showTaskBarHover(); reCreateCustomNode(state._gantt, target, taskIndex, sub_task_index); diff --git a/packages/vtable-gantt/src/ts-types/gantt-engine.ts b/packages/vtable-gantt/src/ts-types/gantt-engine.ts index ef61d18a30..bb3ee15446 100644 --- a/packages/vtable-gantt/src/ts-types/gantt-engine.ts +++ b/packages/vtable-gantt/src/ts-types/gantt-engine.ts @@ -2,7 +2,7 @@ import type { ColumnsDefine, TYPES, ListTableConstructorOptions } from '@visacto import type { Group } from '@visactor/vtable/es/vrender'; import type { Gantt } from '../Gantt'; export type LayoutObjectId = number | string; - +import type { IGanttPlugin } from '../plugins/interface'; export interface ITimelineDateInfo { days: number; endDate: Date; @@ -217,6 +217,7 @@ export interface GanttConstructorOptions { eventOptions?: IEventOptions; keyboardOptions?: IKeyboardOptions; markLineCreateOptions?: IMarkLineCreateOptions; + plugins?: IGanttPlugin[]; } /** * IBarLabelText @@ -237,12 +238,16 @@ export interface ITaskBarLabelTextStyle { fontFamily?: string; fontSize?: number; color?: string; + /** 当文字显示在任务条外侧时的颜色,默认为黑色 */ + outsideColor?: string; textAlign?: 'center' | 'end' | 'left' | 'right' | 'start'; // 设置单元格内文字的水平对齐方式 textOverflow?: string; textBaseline?: 'alphabetic' | 'bottom' | 'middle' | 'top'; // 设置单元格内文字的垂直对齐方式 padding?: number | number[]; - // /** 相对于任务条文字方位位置,可选值:'left', 'top', 'right', 'bottom',分别代表左、上、右、下四个方向 */ - // orient?: 'left', 'top', 'right', 'bottom'; + /** 相对于任务条文字方位位置,可选值:'left', 'top', 'right', 'bottom',分别代表左、上、右、下四个方向 */ + orient?: 'left' | 'top' | 'right' | 'bottom'; + /** 只有当文本在 taskbar 中容纳不下时,会根据该方位将文本显示在任务条旁边。当配置 orient 时,该配置无效 */ + orientHandleWithOverflow?: 'left' | 'top' | 'right' | 'bottom'; } export interface ITaskBarStyle { /** 任务条的颜色 */ diff --git a/packages/vtable-plugins/demo/gantt-export-image/gantt-export-image.ts b/packages/vtable-plugins/demo/gantt-export-image/gantt-export-image.ts new file mode 100644 index 0000000000..905c9752d4 --- /dev/null +++ b/packages/vtable-plugins/demo/gantt-export-image/gantt-export-image.ts @@ -0,0 +1,541 @@ +import { ExportGanttPlugin } from '../../src'; +import * as VTableGantt from '@visactor/vtable-gantt'; + +const CONTAINER_ID = 'vTable'; +const EXPORT_PANEL_ID = 'gantt-export-panel'; +const exportGanttPlugin = new ExportGanttPlugin(); + +export function createTable() { + const records = [ + { + id: 1, + title: 'Task 1', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-24', + end: '2024-07-26', + progress: 31, + priority: 'P0' + }, + { + id: 2, + title: 'Task 2', + developer: 'liufangfang.jane@bytedance.com', + start: '07/24/2024', + end: '08/04/2024', + progress: 60, + priority: 'P0' + }, + { + id: 3, + title: 'Task 3', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-04', + end: '2024-08-04', + progress: 100, + priority: 'P1' + }, + { + id: 4, + title: 'Task 4', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-26', + end: '2024-07-28', + progress: 31, + priority: 'P0' + }, + { + id: 5, + title: 'Task 5', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-26', + end: '2024-07-28', + progress: 60, + priority: 'P0' + }, + { + id: 6, + title: 'Task 6', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-29', + end: '2024-08-11', + progress: 100, + priority: 'P1' + }, + { + id: 7, + title: 'Task 7', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-25', + end: '2024-07-28', + progress: 45, + priority: 'P0' + }, + { + id: 8, + title: 'Task 8', + developer: 'liufangfang.jane@bytedance.com', + start: '07/26/2024', + end: '07/30/2024', + progress: 82, + priority: 'P1' + }, + { + id: 9, + title: 'Task 9', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-27', + end: '2024-07-30', + progress: 15, + priority: 'P0' + }, + { + id: 10, + title: 'Task 10', + developer: 'liufangfang.jane@bytedance.com', + start: '07/28/2024', + end: '08/02/2024', + progress: 67, + priority: 'P0' + }, + { + id: 11, + title: 'Task 11', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-29', + end: '2024-08-03', + progress: 93, + priority: 'P0' + }, + { + id: 12, + title: 'Task 12', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-30', + end: '08/04/2024', + progress: 28, + priority: 'P1' + }, + { + id: 13, + title: 'Task 13', + developer: 'liufangfang.jane@bytedance.com', + start: '07/31/2024', + end: '2024-08-05', + progress: 76, + priority: 'P0' + }, + { + id: 14, + title: 'Task 14', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-01', + end: '2024-08-06', + progress: 50, + priority: 'P0' + }, + { + id: 15, + title: 'Task 15', + developer: 'liufangfang.jane@bytedance.com', + start: '08/02/2024', + end: '08/07/2024', + progress: 11, + priority: 'P1' + }, + { + id: 16, + title: 'Task 16', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-03', + end: '2024-08-08', + progress: 64, + priority: 'P0' + }, + { + id: 17, + title: 'Task 17', + developer: 'liufangfang.jane@bytedance.com', + start: '08/04/2024', + end: '2024-08-09', + progress: 89, + priority: 'P0' + }, + { + id: 18, + title: 'Task 18', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-05', + end: '08/10/2024', + progress: 33, + priority: 'P1' + }, + { + id: 19, + title: 'Task 19', + developer: 'liufangfang.jane@bytedance.com', + start: '08/06/2024', + end: '2024-08-11', + progress: 72, + priority: 'P0' + }, + { + id: 20, + title: 'Task 20', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-07', + end: '2024-08-12', + progress: 55, + priority: 'P0' + }, + { + id: 21, + title: 'Task 21', + developer: 'liufangfang.jane@bytedance.com', + start: '08/08/2024', + end: '08/13/2024', + progress: 98, + priority: 'P1' + }, + { + id: 22, + title: 'Task 22', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-09', + end: '2024-08-14', + progress: 20, + priority: 'P0' + }, + { + id: 23, + title: 'Task 23', + developer: 'liufangfang.jane@bytedance.com', + start: '08/10/2024', + end: '2024-08-15', + progress: 63, + priority: 'P0' + }, + { + id: 24, + title: 'Task 24', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-11', + end: '08/16/2024', + progress: 42, + priority: 'P1' + }, + { + id: 25, + title: 'Task 25', + developer: 'liufangfang.jane@bytedance.com', + start: '08/12/2024', + end: '2024-08-17', + progress: 85, + priority: 'P0' + }, + { + id: 26, + title: 'Task 26', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-08-13', + end: '2024-08-18', + progress: 37, + priority: 'P0' + } + ]; + + const columns = [ + { + field: 'title', + title: 'title', + width: 'auto', + sort: true, + tree: true, + editor: 'input' + }, + { + field: 'start', + title: 'start', + width: 'auto', + sort: true, + editor: 'date-input' + }, + { + field: 'end', + title: 'end', + width: 'auto', + sort: true, + editor: 'date-input' + } + ]; + const option = { + overscrollBehavior: 'none', + records, + taskListTable: { + columns, + tableWidth: 300, + minTableWidth: 100, + maxTableWidth: 1000, + theme: { + headerStyle: { + borderColor: '#e1e4e8', + borderLineWidth: 1, + fontSize: 18, + fontWeight: 'bold', + color: 'red', + bgColor: '#EEF1F5' + }, + bodyStyle: { + borderColor: '#e1e4e8', + borderLineWidth: [1, 0, 1, 0], + fontSize: 16, + color: '#4D4D4D', + bgColor: '#FFF' + } + } + //rightFrozenColCount: 1 + }, + frame: { + outerFrameStyle: { + borderLineWidth: 2, + borderColor: '#e1e4e8', + cornerRadius: 8 + }, + verticalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + horizontalSplitLine: { + lineColor: '#e1e4e8', + lineWidth: 3 + }, + verticalSplitLineMoveable: true, + verticalSplitLineHighlight: { + lineColor: 'green', + lineWidth: 3 + } + }, + grid: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + headerRowHeight: 40, + rowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + resizable: true, + moveable: true, + hoverBarStyle: { + barOverlayColor: 'rgba(99, 144, 0, 0.4)' + }, + labelText: '{title} complete {progress}%', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 16, + textAlign: 'left', + textOverflow: 'ellipsis' + }, + barStyle: { + width: 20, + /** 任务条的颜色 */ + barColor: '#ee8800', + /** 已完成部分任务条的颜色 */ + completedBarColor: '#91e8e0', + /** 任务条的圆角 */ + cornerRadius: 8, + /** 任务条的边框 */ + borderLineWidth: 1, + /** 边框颜色 */ + borderColor: 'black' + } + }, + timelineHeader: { + colWidth: 100, + backgroundColor: '#EEF1F5', + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + scales: [ + { + unit: 'week', + step: 1, + startOfWeek: 'sunday', + format(date) { + return `Week ${date.dateIndex}`; + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5', + textStick: true + // padding: [0, 30, 0, 20] + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + }, + style: { + fontSize: 20, + fontWeight: 'bold', + color: 'white', + strokeColor: 'black', + textAlign: 'right', + textBaseline: 'bottom', + backgroundColor: '#EEF1F5' + } + } + ] + }, + markLine: [ + { + date: '2024/8/02', + scrollToMarkLine: true, + position: 'left', + style: { + lineColor: 'red', + lineWidth: 1 + } + } + ], + rowSeriesNumber: { + title: '行号', + dragOrder: true, + headerStyle: { + bgColor: '#EEF1F5', + borderColor: '#e1e4e8' + }, + style: { + borderColor: '#e1e4e8' + } + }, + scrollStyle: { + scrollRailColor: 'RGBA(246,246,246,0.5)', + visible: 'scrolling', + width: 6, + scrollSliderCornerRadius: 2, + scrollSliderColor: '#5cb85c' + }, + plugins: [exportGanttPlugin] + }; + + // 获取或创建容器 + const container = document.getElementById(CONTAINER_ID)!; + + // 创建一个包装容器 + const wrapper = document.createElement('div'); + wrapper.style.height = '100%'; + wrapper.style.width = '100%'; + wrapper.style.position = 'relative'; + container.appendChild(wrapper); + + // 创建导出面板,放入包装容器 + const exportPanel = document.createElement('div'); + exportPanel.id = EXPORT_PANEL_ID; + exportPanel.style.cssText = 'padding: 2px; background-color: #f6f6f6; margin-bottom: 2px; position: absolute; z-index: 1; border: 1px solid black; opacity: 0.5;'; + wrapper.appendChild(exportPanel); + + // 创建甘特图容器,放入包装容器 + const ganttContainer = document.createElement('div'); + ganttContainer.style.height = '100%'; // 减去导出面板的高度 + ganttContainer.style.width = '100%'; + ganttContainer.style.position = 'relative'; + wrapper.appendChild(ganttContainer); + + // 文件格式选择 + const formatSelect = document.createElement('select'); + formatSelect.innerHTML = ` + + + `; + formatSelect.style.marginRight = '5px'; + + // 缩放比例选择 + const scaleSelect = document.createElement('select'); + scaleSelect.innerHTML = ` + + + + `; + scaleSelect.style.marginRight = '5px'; + + // 背景色选择 + const bgColorInput = document.createElement('input'); + bgColorInput.type = 'color'; + bgColorInput.value = '#ffffff'; + bgColorInput.style.marginRight = '5px'; + + // 导出按钮 + const exportButton = document.createElement('button'); + exportButton.textContent = '导出甘特图'; + exportButton.style.marginLeft = '5px'; + + // 说明文本 + const infoText = document.createElement('div'); + infoText.innerHTML = '导出功能会直接捕获完整的甘特图和任务列表,即使部分内容在滚动区域外。'; + infoText.style.marginTop = '10px'; + infoText.style.fontSize = '12px'; + infoText.style.color = '#666'; + + // 添加控件到面板 + exportPanel.appendChild(document.createTextNode('格式: ')); + exportPanel.appendChild(formatSelect); + exportPanel.appendChild(document.createTextNode('缩放: ')); + exportPanel.appendChild(scaleSelect); + exportPanel.appendChild(document.createTextNode('背景色: ')); + exportPanel.appendChild(bgColorInput); + exportPanel.appendChild(exportButton); + exportPanel.appendChild(infoText); + + // 创建甘特图实例 + const gantt = new VTableGantt.Gantt(ganttContainer, option); + + // 绑定导出事件 + exportButton.onclick = async () => { + try { + exportButton.disabled = true; + exportButton.textContent = '导出中...'; + + await exportGanttPlugin.exportToImage({ + fileName: '甘特图导出测试', + type: formatSelect.value as 'png' | 'jpeg', + scale: Number(scaleSelect.value), + backgroundColor: bgColorInput.value, + quality: 1 + }); + + exportButton.textContent = '导出成功!'; + setTimeout(() => { + exportButton.disabled = false; + exportButton.textContent = '导出甘特图'; + }, 2000); + } catch (error) { + console.error('导出失败:', error); + exportButton.textContent = '导出失败'; + setTimeout(() => { + exportButton.disabled = false; + exportButton.textContent = '导出甘特图'; + }, 2000); + } + }; + + return gantt; +} \ No newline at end of file diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 6f09d55fca..4b69a11790 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -3,6 +3,10 @@ export const menus = [ path: 'carousel-animation', name: '(deprecated)carousel-animation' }, + { + path: 'gantt-export-image', + name: 'gantt-export-image' + }, { path: 'header-highlight', name: '(deprecated)header-highlight' diff --git a/packages/vtable-plugins/package.json b/packages/vtable-plugins/package.json index 94d6babe22..77d9560c66 100644 --- a/packages/vtable-plugins/package.json +++ b/packages/vtable-plugins/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@visactor/vtable": "workspace:*", "@visactor/vtable-editors": "workspace:*", + "@visactor/vtable-gantt": "workspace:*", "@visactor/vchart": "1.13.3-alpha.2", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", diff --git a/packages/vtable-plugins/src/add-row-column.ts b/packages/vtable-plugins/src/add-row-column.ts index 226153f9fe..df7952092e 100644 --- a/packages/vtable-plugins/src/add-row-column.ts +++ b/packages/vtable-plugins/src/add-row-column.ts @@ -3,6 +3,7 @@ import * as VTable from '@visactor/vtable'; * 添加行和列的插件的配置选项 */ export interface AddRowColumnOptions { + id?: string; /** * 是否启用添加列 */ @@ -54,6 +55,7 @@ export class AddRowColumnPlugin implements VTable.plugins.IVTablePlugin { addRowEnable: true } ) { + this.id = pluginOptions.id ?? this.id; this.pluginOptions = pluginOptions; this.pluginOptions.addColumnEnable = this.pluginOptions.addColumnEnable ?? true; this.pluginOptions.addRowEnable = this.pluginOptions.addRowEnable ?? true; diff --git a/packages/vtable-plugins/src/column-series.ts b/packages/vtable-plugins/src/column-series.ts index 5ada1e23ba..d39819aaab 100644 --- a/packages/vtable-plugins/src/column-series.ts +++ b/packages/vtable-plugins/src/column-series.ts @@ -3,6 +3,7 @@ import * as VTable from '@visactor/vtable'; * 添加行和列的插件的配置选项 */ export interface ColumnSeriesOptions { + id?: string; columnCount: number; generateColumnTitle?: (index: number) => string; generateColumnField?: (index: number) => string; @@ -23,6 +24,7 @@ export class ColumnSeriesPlugin implements VTable.plugins.IVTablePlugin { table: VTable.ListTable; columns: { field?: string; title: string }[] = []; constructor(pluginOptions: ColumnSeriesOptions) { + this.id = pluginOptions.id ?? this.id; this.pluginOptions = Object.assign({ columnCount: 100 }, pluginOptions); } run(...args: any[]) { diff --git a/packages/vtable-plugins/src/excel-edit-cell-keyboard.ts b/packages/vtable-plugins/src/excel-edit-cell-keyboard.ts index c01585d175..0c6349be61 100644 --- a/packages/vtable-plugins/src/excel-edit-cell-keyboard.ts +++ b/packages/vtable-plugins/src/excel-edit-cell-keyboard.ts @@ -3,6 +3,7 @@ import type { TableEvents } from '@visactor/vtable/src/core/TABLE_EVENT_TYPE'; import type { EventArg } from './types'; //备用 插件配置项 目前感觉都走默认逻辑就行 export type IExcelEditCellKeyboardPluginOptions = { + id?: string; // 是否响应删除 // enableDeleteKey?: boolean; }; @@ -14,6 +15,7 @@ export class ExcelEditCellKeyboardPlugin implements VTable.plugins.IVTablePlugin table: VTable.ListTable; pluginOptions: IExcelEditCellKeyboardPluginOptions; constructor(pluginOptions?: IExcelEditCellKeyboardPluginOptions) { + this.id = pluginOptions?.id ?? this.id; this.pluginOptions = pluginOptions; this.bindEvent(); diff --git a/packages/vtable-plugins/src/focus-highlight.ts b/packages/vtable-plugins/src/focus-highlight.ts index 6cdf353635..140c29bb83 100644 --- a/packages/vtable-plugins/src/focus-highlight.ts +++ b/packages/vtable-plugins/src/focus-highlight.ts @@ -8,6 +8,7 @@ import { cellInRange } from '@visactor/vtable/es/tools/helper'; import { TABLE_EVENT_TYPE } from '@visactor/vtable'; import type * as VTable from '@visactor/vtable'; export interface FocusHighlightPluginOptions { + id?: string; fill?: string; opacity?: number; highlightRange?: CellAddress | CellRange; //初始化聚焦高亮范围 @@ -28,6 +29,7 @@ export class FocusHighlightPlugin implements VTable.plugins.IVTablePlugin { highlightRange: undefined } ) { + this.id = options.id ?? this.id; this.pluginOptions = Object.assign( { fill: '#000', diff --git a/packages/vtable-plugins/src/gantt-export-image.ts b/packages/vtable-plugins/src/gantt-export-image.ts new file mode 100644 index 0000000000..24179e8fa0 --- /dev/null +++ b/packages/vtable-plugins/src/gantt-export-image.ts @@ -0,0 +1,179 @@ +import * as VTableGantt from '@visactor/vtable-gantt'; + +// 甘特图导出配置项接口 +export interface ExportOptions { + fileName?: string; + type?: 'png' | 'jpeg'; + quality?: number; + backgroundColor?: string; + scale?: number; +} + + +/** + * 甘特图导出插件 + * @description 提供完整的甘特图导出功能,支持高分辨率输出和精准布局保留 + */ +export class ExportGanttPlugin implements VTableGantt.plugins.IGanttPlugin { + id = 'gantt-export-helper'; + name = 'Gantt Export Helper'; + private _gantt: VTableGantt.Gantt | null = null; + + // run 方法,在插件初始化时由 PluginManager调用 + run(...args: any[]): void { + const ganttInstance = args[0] as VTableGantt.Gantt; + if (!ganttInstance) { + console.error('ExportGanttPlugin: Gantt instance not provided to run method.'); + return; + } + this._gantt = ganttInstance; + } + + /** + * 执行甘特图导出操作 + * @async + * @param {ExportOptions} [options={}] 导出配置选项 + * @returns {Promise} 返回Base64格式的图片数据,或在未初始化时返回 undefined + * @throws {Error} 导出过程中发生错误时抛出异常 + */ + public async exportToImage(options: ExportOptions = {}): Promise { + if (!this._gantt) { + // 保留这个 error + console.error('ExportGanttPlugin: Gantt instance not available.'); + return undefined; + } + + const { + fileName = 'gantt-export', + type = 'png', + quality = 1, + backgroundColor = '#ffffff', + scale = window.devicePixelRatio || 1 + } = options; + + try { + const { tempContainer, clonedGantt } = this.createFullSizeContainer(scale); + + try { + await new Promise(resolve => requestAnimationFrame(resolve)); + + const totalWidth = (clonedGantt.taskListTableInstance.getAllColsWidth() + clonedGantt.getAllDateColsWidth()) * scale; + const totalHeight = clonedGantt.getAllRowsHeight() * scale; + + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = totalWidth; + exportCanvas.height = totalHeight; + const ctx = exportCanvas.getContext('2d')!; + + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, totalWidth, totalHeight); + + if (clonedGantt.taskListTableInstance?.canvas) { + ctx.drawImage( + clonedGantt.taskListTableInstance.canvas, + 0, 0, + clonedGantt.taskListTableInstance.getAllColsWidth() * scale, + totalHeight + ); + } + + const splitLineWidth = 3 * scale; + const splitLineX = clonedGantt.taskListTableInstance.getAllColsWidth() * scale; + ctx.fillStyle = 'rgb(225, 228, 232)'; + ctx.fillRect( + splitLineX - splitLineWidth / 2, + 0, + splitLineWidth, + totalHeight + ); + + const sourceX = 4 * scale; + const sourceWidth = clonedGantt.canvas.width - sourceX; + + if (clonedGantt.canvas) { + ctx.drawImage( + clonedGantt.canvas, + sourceX, 0, + sourceWidth, clonedGantt.canvas.height, + (clonedGantt.taskListTableInstance.getAllColsWidth() + 1.5) * scale, 0, + (clonedGantt.getAllDateColsWidth() - 1.5) * scale, + totalHeight + ); + } + + return this.finalizeExport(exportCanvas, fileName, type, quality); + } finally { + tempContainer.remove(); + // 确保克隆的甘特图实例被释放 + clonedGantt.release(); + } + } catch (error) { + console.error('[Gantt Export Plugin] Export failed:', error); + throw new Error(`甘特图导出失败: ${error instanceof Error ? error.message : '未知错误'}`); + } + } + + private createFullSizeContainer(scale: number) { + if (!this._gantt) { + // 保留这个 error + throw new Error('ExportGanttPlugin: Gantt instance not available to create container.'); + } + + const tempContainer = document.createElement('div'); + tempContainer.style.position = 'fixed'; + tempContainer.style.left = '-9999px'; + tempContainer.style.overflow = 'hidden'; + tempContainer.style.width = `${window.innerWidth + 100}px`; + tempContainer.style.height = `${window.innerHeight + 100}px`; + document.body.appendChild(tempContainer); + + const clonedContainer = document.createElement('div'); + + const totalWidth = this._gantt.taskListTableInstance.getAllColsWidth() + this._gantt.getAllDateColsWidth(); + const totalHeight = this._gantt.getAllRowsHeight(); + + clonedContainer.style.width = `${totalWidth}px`; + clonedContainer.style.height = `${totalHeight}px`; + tempContainer.appendChild(clonedContainer); + + const clonedGantt = new VTableGantt.Gantt(clonedContainer, { + ...this._gantt.options, + records: JSON.parse(JSON.stringify(this._gantt.records)), + taskListTable: { + ...this._gantt.options.taskListTable, + tableWidth: undefined as unknown as number, + minTableWidth: undefined as unknown as number, + maxTableWidth: undefined as unknown as number, + }, + }); + + clonedGantt.setPixelRatio(scale); + + // 禁用裁剪 + if ((clonedGantt as any).scenegraph?.ganttGroup) { + (clonedGantt as any).scenegraph.ganttGroup.setAttribute('clip', false); + } + if ((clonedGantt.taskListTableInstance as any)?.scenegraph?.tableGroup) { + (clonedGantt.taskListTableInstance as any).scenegraph.tableGroup.setAttribute('clip', false); + } + + clonedGantt.scenegraph.stage.render(); + + return { tempContainer, clonedGantt }; + } + + private finalizeExport(canvas: HTMLCanvasElement, fileName: string, type: string, quality: number): string { + const base64 = canvas.toDataURL(`image/${type}`, quality); + const link = document.createElement('a'); + link.download = `${fileName}.${type}`; + link.href = base64; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + return base64; + } + + release(): void { + this._gantt = null; + } +} \ No newline at end of file diff --git a/packages/vtable-plugins/src/highlight-header-when-select-cell.ts b/packages/vtable-plugins/src/highlight-header-when-select-cell.ts index a56b108fd0..ed376e52e0 100644 --- a/packages/vtable-plugins/src/highlight-header-when-select-cell.ts +++ b/packages/vtable-plugins/src/highlight-header-when-select-cell.ts @@ -2,6 +2,7 @@ import type { CellRange } from '@visactor/vtable/es/ts-types'; import { TABLE_EVENT_TYPE } from '@visactor/vtable'; import type { BaseTableAPI, plugins } from '@visactor/vtable'; interface IHighlightHeaderWhenSelectCellPluginOptions { + id?: string; rowHighlight?: boolean; colHighlight?: boolean; colHighlightBGColor?: string; @@ -24,6 +25,7 @@ export class HighlightHeaderWhenSelectCellPlugin implements plugins.IVTablePlugi colHeaderRanges: CellRange[] = []; rowHeaderRanges: CellRange[] = []; constructor(pluginOptions: IHighlightHeaderWhenSelectCellPluginOptions) { + this.id = pluginOptions.id ?? this.id; this.pluginOptions = pluginOptions; } run(...args: any[]) { diff --git a/packages/vtable-plugins/src/index.ts b/packages/vtable-plugins/src/index.ts index b21daf8a36..524bc770aa 100644 --- a/packages/vtable-plugins/src/index.ts +++ b/packages/vtable-plugins/src/index.ts @@ -10,3 +10,4 @@ export * from './types'; export * from './focus-highlight'; export * from './table-carousel-animation'; export * from './rotate-table'; +export * from './gantt-export-image'; diff --git a/packages/vtable-plugins/src/rotate-table.ts b/packages/vtable-plugins/src/rotate-table.ts index 92ba77f220..12ea36dddf 100644 --- a/packages/vtable-plugins/src/rotate-table.ts +++ b/packages/vtable-plugins/src/rotate-table.ts @@ -11,10 +11,11 @@ import * as VTable from '@visactor/vtable'; import type { TableEvents } from '@visactor/vtable/src/core/TABLE_EVENT_TYPE'; import type { EventArg } from './types'; import type { Matrix } from '@visactor/vutils'; -// export type IRotateTablePluginOptions = { -// // 旋转角度 -// rotate?: number; -// }; +export type IRotateTablePluginOptions = { + id?: string; + // // 旋转角度 + // rotate?: number; +}; /** * 旋转表格插件。 * 业务层旋转功能没有使用收系统接口的话,用的transform:'rotate(90deg)'的设置来达到旋转的目的。vtable及vrender都没有进行坐标处理,这样就会导致交互错乱。 @@ -22,20 +23,21 @@ import type { Matrix } from '@visactor/vutils'; * 这里使用transform:'rotate(90deg)'的设置来达到旋转的目的。 其他角度应该也是可以实现的,请自行扩展这个插件并兼容 */ export class RotateTablePlugin implements VTable.plugins.IVTablePlugin { - id = 'rotate-table'; + id = `rotate-table-${Date.now()}`; name = 'Rotate Table'; runTime = [VTable.TABLE_EVENT_TYPE.INITIALIZED]; table: VTable.ListTable; matrix: Matrix; vglobal_mapToCanvasPoint: any; // 保存vrender中vglobal的mapToCanvasPoint原方法 // pluginOptions: IRotateTablePluginOptions; - constructor() { + constructor(pluginOptions?: IRotateTablePluginOptions) { + this.id = pluginOptions?.id ?? this.id; // this.pluginOptions = pluginOptions; } run(...args: [EventArg, TableEvents[keyof TableEvents] | TableEvents[keyof TableEvents][], VTable.BaseTableAPI]) { const table: VTable.BaseTableAPI = args[2]; this.table = table as VTable.ListTable; - //将函数rotate90WithTransform绑定到table实例上 + //将函数rotate90WithTransform绑定到table实例上,一般情况下插件不需要将api绑定到table实例上,可以直接自身实现某个api功能 this.table.rotate90WithTransform = rotate90WithTransform.bind(this.table); this.table.cancelTransform = cancelTransform.bind(this.table); } diff --git a/packages/vtable-plugins/src/row-series.ts b/packages/vtable-plugins/src/row-series.ts index 3ebf5f6f11..49e2124699 100644 --- a/packages/vtable-plugins/src/row-series.ts +++ b/packages/vtable-plugins/src/row-series.ts @@ -4,6 +4,7 @@ import type { TYPES, BaseTableAPI, ListTable, ListTableConstructorOptions, plugi * 添加行和列的插件的配置选项 */ export interface RowSeriesOptions { + id?: string; rowCount: number; fillRowRecord?: (index: number) => any; rowSeriesNumber?: TYPES.IRowSeriesNumber; @@ -24,6 +25,7 @@ export class RowSeriesPlugin implements plugins.IVTablePlugin { table: ListTable; constructor(pluginOptions: RowSeriesOptions) { + this.id = pluginOptions.id ?? this.id; this.pluginOptions = Object.assign({ rowCount: 100 }, pluginOptions); } run(...args: any[]) { diff --git a/packages/vtable-plugins/src/table-carousel-animation.ts b/packages/vtable-plugins/src/table-carousel-animation.ts index 34b9e53d24..ad15052a60 100644 --- a/packages/vtable-plugins/src/table-carousel-animation.ts +++ b/packages/vtable-plugins/src/table-carousel-animation.ts @@ -7,6 +7,7 @@ function isInteger(value: number) { } export interface ITableCarouselAnimationPluginOptions { + id?: string; rowCount?: number; colCount?: number; animationDuration?: number; @@ -42,6 +43,7 @@ export class TableCarouselAnimationPlugin implements VTable.plugins.IVTablePlugi customDistRowFunction?: (row: number, table: BaseTableAPI) => { distRow: number; animation?: boolean } | undefined; customDistColFunction?: (col: number, table: BaseTableAPI) => { distCol: number; animation?: boolean } | undefined; constructor(options: ITableCarouselAnimationPluginOptions = {}) { + this.id = options.id ?? this.id; this.rowCount = options?.rowCount ?? undefined; this.colCount = options?.colCount ?? undefined; this.animationDuration = options?.animationDuration ?? 500; diff --git a/packages/vtable/src/edit/edit-manager.ts b/packages/vtable/src/edit/edit-manager.ts index ef4ebe7ebe..c8993f1393 100644 --- a/packages/vtable/src/edit/edit-manager.ts +++ b/packages/vtable/src/edit/edit-manager.ts @@ -6,6 +6,7 @@ import { getCellEventArgsSet } from '../event/util'; import type { SimpleHeaderLayoutMap } from '../layout'; import { isPromise } from '../tools/helper'; import { isValid } from '@visactor/vutils'; +import type { IIconGraphicAttribute } from '../scenegraph/graphic/icon'; export class EditManager { table: BaseTableAPI; @@ -43,6 +44,10 @@ export class EditManager { // 如果是双击自动列宽 则编辑不开启 return; } + if ((e.target?.attribute as IIconGraphicAttribute)?.funcType) { + // 点击功能图标不进入编辑 + return; + } this.beginTriggerEditCellMode = 'doubleclick'; this.startEditCell(col, row); }); @@ -50,6 +55,10 @@ export class EditManager { const clickEventId = table.on(TABLE_EVENT_TYPE.CLICK_CELL, e => { const { editCellTrigger = 'doubleclick' } = table.options; if (editCellTrigger === 'click' || (Array.isArray(editCellTrigger) && editCellTrigger.includes('click'))) { + if ((e.target?.attribute as IIconGraphicAttribute)?.funcType) { + // 点击功能图标不进入编辑 + return; + } this.beginTriggerEditCellMode = 'click'; const { col, row } = e; this.startEditCell(col, row); diff --git a/packages/vtable/src/index.ts b/packages/vtable/src/index.ts index 0ecd316bcc..0b284d84b4 100644 --- a/packages/vtable/src/index.ts +++ b/packages/vtable/src/index.ts @@ -42,7 +42,7 @@ import * as CustomLayout from './render/layout'; import { updateCell } from './scenegraph/group-creater/cell-helper'; import { renderChart } from './scenegraph/graphic/contributions/chart-render-helper'; -import { restoreMeasureText, setCustomAlphabetCharSet } from './scenegraph/utils/text-measure'; +import { restoreMeasureText, setCustomAlphabetCharSet, textMeasure } from './scenegraph/utils/text-measure'; import type { BaseTableAPI } from './ts-types/base-table'; // import { container, loadCanvasPicker } from '@src/vrender'; @@ -93,6 +93,7 @@ export { GroupColumnDefine, TextAlignType, TextBaselineType, + textMeasure, themes, data, MousePointerCellEvent, diff --git a/packages/vtable/src/layout/pivot-header-layout.ts b/packages/vtable/src/layout/pivot-header-layout.ts index 80558f3b67..e5329ab496 100644 --- a/packages/vtable/src/layout/pivot-header-layout.ts +++ b/packages/vtable/src/layout/pivot-header-layout.ts @@ -1639,10 +1639,10 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { if (this.rowHierarchyType === 'tree') { const extensionRowCount = this.extensionRows?.length ?? 0; if (this.rowHeaderTitle) { - this.rowHeaderLevelCount = 2 + extensionRowCount; + this.rowHeaderLevelCount = 1 + (this.rowDimensionTree.totalLevel ? 1 : 0) + extensionRowCount; return; } - this.rowHeaderLevelCount = 1 + extensionRowCount; + this.rowHeaderLevelCount = (this.rowDimensionTree.totalLevel ? 1 : 0) + extensionRowCount; return; } const rowLevelCount = this._getRowHeaderTreeExpandedMaxLevelCount(); @@ -2212,7 +2212,7 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { const row_pathIds = this._rowHeaderCellFullPathIds[recordRow]; //获取当前行的cellId 但这个cellId不是各级维度都有的 下面逻辑就是找全路径然后再去各个树找path的过程 let findTree = this.rowDimensionTree; //第一棵寻找的树是第一列的维度树 主树 let level = 0; //level和col对应,代表一层层树找的过程 - while (findTree) { + while (findTree && row_pathIds) { const pathIds: (number | string)[] = []; // pathIds记录寻找当前树需要匹配的cellId let cellId: LayoutObjectId = row_pathIds[level]; //row_pathIds中每个值对应了pathIds的一个节点cellId pathIds.push(cellId); diff --git a/packages/vtable/src/scenegraph/component/table-component.ts b/packages/vtable/src/scenegraph/component/table-component.ts index 1324346142..50991f7d9a 100644 --- a/packages/vtable/src/scenegraph/component/table-component.ts +++ b/packages/vtable/src/scenegraph/component/table-component.ts @@ -181,8 +181,9 @@ export class TableComponent { const shadowWidth = theme.frozenColumnLine?.shadow?.width; const shadowStartColor = theme.frozenColumnLine?.shadow?.startColor; const shadowEndColor = theme.frozenColumnLine?.shadow?.endColor; + const visible = theme.frozenColumnLine?.shadow?.visible; this.frozenShadowLine = createRect({ - visible: true, + visible: visible === 'always', pickable: false, x: 0, y: 0, @@ -201,7 +202,7 @@ export class TableComponent { } }); this.rightFrozenShadowLine = createRect({ - visible: true, + visible: visible === 'always', pickable: false, x: 0, y: 0, @@ -651,13 +652,14 @@ export class TableComponent { * @return {*} */ setFrozenColumnShadow(col: number, isRightFrozen?: boolean) { - if (col < 0) { + const colX = getColX(col, this.table, isRightFrozen); + if (col < 0 || this.table.theme.frozenColumnLine?.shadow?.visible !== 'always') { this.frozenShadowLine.setAttributes({ - visible: false + visible: false, + x: colX, + height: this.table.getDrawRange().height }); } else { - // const colX = this.table.getColsWidth(0, col); - const colX = getColX(col, this.table, isRightFrozen); this.frozenShadowLine.setAttributes({ visible: true, x: colX, @@ -672,13 +674,14 @@ export class TableComponent { * @return {*} */ setRightFrozenColumnShadow(col: number) { - if (col >= this.table.colCount) { + const colX = getColX(col, this.table, true); + if (col >= this.table.colCount || this.table.theme.frozenColumnLine?.shadow?.visible !== 'always') { this.rightFrozenShadowLine.setAttributes({ - visible: false + visible: false, + x: colX - this.rightFrozenShadowLine.attribute.width, + height: this.table.getDrawRange().height }); } else { - // const colX = this.table.getColsWidth(0, col); - const colX = getColX(col, this.table, true); this.rightFrozenShadowLine.setAttributes({ visible: true, x: colX - this.rightFrozenShadowLine.attribute.width, @@ -686,7 +689,26 @@ export class TableComponent { }); } } - + hideFrozenColumnShadow() { + const visible1 = this.table.theme.frozenColumnLine?.shadow?.visible; + const visible = this.table.theme.frozenColumnLine?.shadow?.visible ?? visible1; + if (visible !== 'scrolling') { + return; + } + this.frozenShadowLine.setAttribute('visible', false); + this.rightFrozenShadowLine.setAttribute('visible', false); + this.table.scenegraph.updateNextFrame(); + } + showFrozenColumnShadow() { + const visible1 = this.table.theme.frozenColumnLine?.shadow?.visible; + const visible = this.table.theme.frozenColumnLine?.shadow?.visible ?? visible1; + if (visible !== 'scrolling') { + return; + } + this.frozenShadowLine.setAttribute('visible', true); + this.rightFrozenShadowLine.setAttribute('visible', true); + this.table.scenegraph.updateNextFrame(); + } hideVerticalScrollBar() { const visible1 = this.table.theme.scrollStyle.visible; const verticalVisible = this.table.theme.scrollStyle.verticalVisible ?? visible1; diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index 228b7cae4a..55f6e07538 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -1286,10 +1286,12 @@ export class StateManager { } showHorizontalScrollBar(autoHide?: boolean) { this.table.scenegraph.component.showHorizontalScrollBar(); + this.table.scenegraph?.component.showFrozenColumnShadow(); if (autoHide) { // 滚轮触发滚动条显示后,异步隐藏 clearTimeout(this._clearHorizontalScrollBar); this._clearHorizontalScrollBar = setTimeout(() => { + this.table.scenegraph?.component.hideFrozenColumnShadow(); this.table.scenegraph?.component.hideHorizontalScrollBar(); }, 1000); } diff --git a/packages/vtable/src/themes/ARCO.ts b/packages/vtable/src/themes/ARCO.ts index b8b1dc9b8a..bd6ce7c875 100644 --- a/packages/vtable/src/themes/ARCO.ts +++ b/packages/vtable/src/themes/ARCO.ts @@ -129,7 +129,8 @@ export default { shadow: { width: 4, startColor: 'rgba(00, 24, 47, 0.05)', - endColor: 'rgba(00, 24, 47, 0)' + endColor: 'rgba(00, 24, 47, 0)', + visible: 'always' } }, // menuStyle: { diff --git a/packages/vtable/src/themes/BRIGHT.ts b/packages/vtable/src/themes/BRIGHT.ts index daf4afaa83..0a7569dfaa 100644 --- a/packages/vtable/src/themes/BRIGHT.ts +++ b/packages/vtable/src/themes/BRIGHT.ts @@ -76,7 +76,8 @@ export default { shadow: { width: 3, startColor: '#CBDCFE', - endColor: '#CBDCFE' + endColor: '#CBDCFE', + visible: 'always' } }, // menuStyle: { diff --git a/packages/vtable/src/themes/DARK.ts b/packages/vtable/src/themes/DARK.ts index b1ccabed18..fd7efc17e8 100644 --- a/packages/vtable/src/themes/DARK.ts +++ b/packages/vtable/src/themes/DARK.ts @@ -97,7 +97,8 @@ export default { shadow: { width: 4, startColor: 'rgba(00, 24, 47, 0.05)', - endColor: 'rgba(00, 24, 47, 0)' + endColor: 'rgba(00, 24, 47, 0)', + visible: 'always' } }, // menuStyle: { diff --git a/packages/vtable/src/themes/DEFAULT.ts b/packages/vtable/src/themes/DEFAULT.ts index e957976a29..4436a4e7a4 100644 --- a/packages/vtable/src/themes/DEFAULT.ts +++ b/packages/vtable/src/themes/DEFAULT.ts @@ -104,7 +104,8 @@ export default { shadow: { width: 3, startColor: 'rgba(225, 228, 232, 0.6)', - endColor: 'rgba(225, 228, 232, 0.6)' + endColor: 'rgba(225, 228, 232, 0.6)', + visible: 'always' } }, // menuStyle: { diff --git a/packages/vtable/src/themes/theme-define.ts b/packages/vtable/src/themes/theme-define.ts index 94004a960a..7c8938dfb7 100644 --- a/packages/vtable/src/themes/theme-define.ts +++ b/packages/vtable/src/themes/theme-define.ts @@ -645,7 +645,9 @@ export class TableTheme implements ITableThemeDefine { obj.frozenColumnLine ); this._frozenColumnLine = { - get shadow(): { width: number; startColor: string; endColor: string } | undefined { + get shadow(): + | { width: number; startColor: string; endColor: string; visible: 'always' | 'scrolling' } + | undefined { if (frozenColumnLine.shadow) { return { get width(): number { @@ -656,6 +658,9 @@ export class TableTheme implements ITableThemeDefine { }, get endColor(): string { return frozenColumnLine.shadow?.endColor ?? 'rgba(00, 24, 47, 0)'; + }, + get visible(): 'always' | 'scrolling' { + return frozenColumnLine.shadow?.visible ?? 'always'; } }; } diff --git a/packages/vtable/src/ts-types/theme.ts b/packages/vtable/src/ts-types/theme.ts index 9c9f22fc57..29fb1a956a 100644 --- a/packages/vtable/src/ts-types/theme.ts +++ b/packages/vtable/src/ts-types/theme.ts @@ -130,6 +130,8 @@ export interface ITableThemeDefine { width: number; //阴影整体宽度 startColor: string; //开始颜色 endColor: string; //结束颜色 + /**滚动条是否可见 'always' | 'scrolling' | 'none' | 'focus',常驻|滚动时|不显示|聚焦在画布上时 。默认'scrolling'*/ + visible?: 'always' | 'scrolling'; }; /** TODO 暂未生效 */ border?: { diff --git a/packages/vue-vtable/src/utils/customLayoutUtils.ts b/packages/vue-vtable/src/utils/customLayoutUtils.ts index b67ba674ef..c470043c25 100644 --- a/packages/vue-vtable/src/utils/customLayoutUtils.ts +++ b/packages/vue-vtable/src/utils/customLayoutUtils.ts @@ -1,7 +1,7 @@ import * as VTable from '@visactor/vtable'; import { convertPropsToCamelCase, toCamelCase } from './stringUtils'; import { isFunction, isObject } from '@visactor/vutils'; -import { isVNode } from 'vue'; +import { cloneVNode, isVNode } from 'vue'; // 检查属性是否为事件 function isEventProp(key: string, props: any) { @@ -46,9 +46,18 @@ export function createCustomLayout(children: any, isHeader?: boolean, args?: any if (isObject(props?.vue)) { // vue 自定义节点:无需继续循环子节点 const { element } = props.vue as any; - const targetVNode = element ?? subChildren.find(node => node?.type !== Symbol.for('v-cmt')); + let targetVNode = element ?? subChildren.find(node => node?.type !== Symbol.for('v-cmt')); + if (isVNode(targetVNode)) { + // node 标记 key 增加唯一项标记,避免重复渲染 + targetVNode = !targetVNode.key + ? cloneVNode(targetVNode, { key: `row_${args.row}_col_${args.col}` }) + : targetVNode; + } else { + targetVNode = null; + } + Object.assign(child.props.vue, { - element: isVNode(targetVNode) ? targetVNode : null, + element: targetVNode, // 不接入外部指定 container: isHeader ? args?.table?.headerDomContainer : args?.table?.bodyDomContainer });