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/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/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/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/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/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/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/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/ts-types/gantt-engine.ts b/packages/vtable-gantt/src/ts-types/gantt-engine.ts index ef61d18a30..0257500b6a 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 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/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/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';