├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs └── README_zh.md ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── data.js │ └── logo.png ├── components │ ├── Gantt.vue │ └── excel.js ├── main.js └── util │ └── index.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | dd@ddamy.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Vue3 Gantt Chart Component 2 | 3 | ![gantt](https://blog.ddamy.com/assets/img/gantt.jpeg) 4 | 5 | [简体中文](https://github.com/ddmy/vue3-gantt/blob/master/docs/README_zh.md) 6 | 7 | ### Introduction 8 | A simplified Gantt chart component developed based on vue3.x: 9 | - [x] Supports exporting the Gantt chart to an Excel file. 10 | - [x] Supports exporting the Gantt chart as an image. 11 | - [x] Supports displaying overlapping schedules. 12 | - [x] Supports dynamic configuration for responsive updates. 13 | - [x] Responsive layout, compatible with small screens. 14 | 15 | > If you encounter any issues during use, feel free to raise issues 😊. 16 | 17 | [Demo Online Preview](https://blog.ddamy.com/assets/demo/gantt/) 18 | 19 | ### Usage 20 | ```html 21 | 27 | ``` 28 | ```js 29 | import { ref } from 'vue' 30 | import Gantt from 'vue3-gantt' 31 | import 'vue3-gantt/dist/style.css' 32 | const dateRangeList = ref(['2022-01-01', '2022-03-05']) 33 | const data = ref([ 34 | { 35 | type: 'normal', 36 | color: '', 37 | name: 'Project 1', 38 | schedule: [ 39 | { 40 | id: 333330, 41 | name: '900 Warriors Simultaneous Online Celebration Event', 42 | desc: 'This event is very important, generating millions of revenue. It is a cross-departmental collaboration and a major project with the CEO personally present to command. Everyone must work together!', 43 | backgroundColor: 'rgb(253, 211, 172)', 44 | textColor: 'rgb(245, 36, 9)', 45 | days: ["2022-01-15","2022-02-05"] 46 | }, 47 | { 48 | id: 555550, 49 | name: 'XXXXXX', 50 | desc: 'This event is very important, generating millions of revenue. It is a cross-departmental collaboration and a major project with the CEO personally present to command. Everyone must work together!', 51 | backgroundColor: '#28f', 52 | textColor: '#fff', 53 | days: ["2022-02-15","2022-02-25"] 54 | }, 55 | ], 56 | }, 57 | { 58 | type: 'normal', 59 | color: '', 60 | name: 'Meteor Butterfly Sword', 61 | schedule: [ 62 | { 63 | id: 222221, 64 | name: 'Chinese New Year Event', 65 | desc: 'This event is very important, generating millions of revenue. It is a cross-departmental collaboration and a major project with the CEO personally present to command. Everyone must work together!', 66 | backgroundColor: '#482', 67 | textColor: '#fff', 68 | days: ["2022-02-25","2022-03-10"] 69 | } 70 | ], 71 | }, 72 | ]) 73 | ``` 74 | 75 | ### Component Props 76 | 77 | | Key | Type | Default | Description | 78 | | ------ | ------ | -------- | ------------ | 79 | | data | Array[Object] | [] | antt chart data | 80 | | dateRangeList | Array | [] | The date range within the current chart. This array should have a length of 2, with elements as the start and end date in the format 'YYYY-MM-DD'. | 81 | | itemText | String | null | The header description for the items in the Gantt chart. | 82 | | dateText | String | null | The header description for the dates in the Gantt chart. | 83 | | activeDate | String | Today | The day to be highlighted on the timeline (does not override schedule styles). Format: 'YYYY-MM-DD'. | 84 | | repeatMode | Object | See below | Configuration for handling overlapping schedules. | 85 | | itemWidth | Number | 40 | - | The width of date cells, minimum 40. | 86 | | itemHeight | Number | 40 | - | The height of date cells, minimum 40. | 87 | | scheduleTitle | Function | null | - | A function to display custom text above the schedule. It receives the schedule information as a parameter and should return the text to be rendered. | 88 | | borderColor | String | '#eee' | - | The color of the table borders. | 89 | 90 | > The content width of the component needs to be controlled manually to ensure the minimum width. 91 | 92 | ### Component Events 93 | 94 | | Event | Type | Description | 95 | | ------ | ------ | ------------ | 96 | | scheduleClick | Function | Callback event when clicking on a schedule. Receives the schedule details as a parameter. | 97 | | scrollXEnd | Function | Event triggered when the horizontal scrollbar reaches the end. | 98 | | scrollYEnd | Function | Event triggered when the vertical scrollbar reaches the end. | 99 | 100 | ### Data Configuration: data Array[Object] 101 | 102 | | Key | Value | Description | 103 | | ------ | ------ | ------ | 104 | | type | 'alike'\|\|'normal' | The project type (display style). | 105 | | color | CSS color format | Background color for the current project. Applicable when the type is 'alike'. | 106 | | name | String | The name of the current project. | 107 | | schedule | Array[Object] | The project schedules. | 108 | 109 | ### Schedule Configuration: schedule Object 110 | 111 | > For easier development, you can extend additional fields based on the following. 112 | 113 | | Key | Description | 114 | | ------ | -------------------- | 115 | | id | A globally unique ID for the schedule. | 116 | | name | The name of the schedule. | 117 | | desc | Description of the schedule. | 118 | | backgroundColor | Background color for the schedule. | 119 | | textColor | Text color for the schedule name. | 120 | | days | Array of Schedule Dates | The array contains valid and consecutive dates in the format YYYY-MM-DD. Alternatively, it can be represented as an array of two elements denoting the start and end dates. | 121 | 122 | ### repeatMode Configuration: Object 123 | 124 | | Key | Options | Default | 说Description明 | 125 | | ------ | ------ | -------- | ---------- | 126 | | mode | 'cover'\|\|'extract' | 'cover' | Handling mode for overlapping schedules. 'cover' will simply overlap schedules, while 'extract' will extract and group overlapping schedules separately. | 127 | | backgroundColor | CSS color format | '#FFFFCC' | Background color for the extracted schedules in 'extract' mode. | 128 | | textColor | CSS color format | '#336666' | Text color for the extracted schedules in 'extract' mode. | 129 | | name | `String`\|\|`Function` | 'Overlapping Schedules' | Text to display for overlapping schedules. If it's a function, it receives a list of overlapping schedules as a parameter. | 130 | | desc | `String`\|\|`Function` | 'These are multiple schedules.' | Description to display for overlapping schedules. If it's a function, it receives a list of overlapping schedules as a parameter. | 131 | 132 | 133 | ### Component Instance Methods 134 | 135 | #### Export Full Snapshot Image of the Current Gantt Chart 136 | 137 | ```html 138 | 142 | 143 | ``` 144 | 145 | ```js 146 | const gantt = ref(null) 147 | 148 | const exportImg = () => { 149 | gantt.value.exportImg({ download: true, waterValue: 'Made by YiJio' }) 150 | } 151 | ``` 152 | > The exportImg method accepts an Object to configure the behavior of exporting the image. It returns a Promise, and upon successful completion, it receives the base64 value of the image. 153 | > | Parameter | Optional Values | Default Value | Description | 154 | > | ------ | ------ | -------- | ---------- | 155 | > | download | `Boolean` | `true` | Whether to automatically download the image. | 156 | > | waterValue | `String` | `''` | Watermark text to be added to the image. If empty, no watermark will be added. Customizing the text style is not supported at the moment. | 157 | 158 | 159 | #### Exporting Current Gantt Chart to Excel 160 | 161 | ```html 162 | 166 | 167 | ``` 168 | ```js 169 | const gantt = ref(null) 170 | 171 | const exportGanttExcel = () => { 172 | gantt.value.exportGanttExcel({ fileName: 'TestList' }) 173 | } 174 | ``` 175 | > The exportGanttExcel method receives an Object named file to configure the export file information. 176 | > | Parameter | Optional Values | Default Value | Description | 177 | > | ------ | ------ | -------- | ---------- | 178 | > | fileName | `String` | '数据' | The name of the exported file. | 179 | -------------------------------------------------------------------------------- /docs/README_zh.md: -------------------------------------------------------------------------------- 1 | ### Vue3甘特图插件 2 | 3 | ![gantt](https://blog.ddamy.com/assets/img/gantt.jpeg) 4 | 5 | ### 简介 6 | 基于`vue3.x`开发的精简版甘特图 7 | - [x] 支持导出甘特图Excel文件 8 | - [x] 支持导出甘特图图片 9 | - [x] 支持日程重叠展示 10 | - [x] 支持动态配置响应更新 11 | - [x] 响应式布局,兼容小屏幕展示 12 | 13 | > 如果您在使用过程中遇到任何相关问题,欢迎您提 issues😊, 14 | 15 | [Demo在线预览](https://blog.ddamy.com/assets/demo/gantt/) 16 | 17 | ### 使用方式 18 | ```html 19 | 25 | ``` 26 | ```js 27 | import { ref } from 'vue' 28 | import Gantt from 'vue3-gantt' 29 | import 'vue3-gantt/dist/style.css' 30 | const dateRangeList = ref(['2022-01-01', '2022-03-05']) 31 | const data = ref([ 32 | { 33 | type: 'normal', 34 | color: '', 35 | name: '项目1', 36 | schedule: [ 37 | { 38 | id: 333330, 39 | name: '900勇士同时在线庆祝活动', 40 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 41 | backgroundColor: 'rgb(253, 211, 172)', 42 | textColor: 'rgb(245, 36, 9)', 43 | days: ["2022-01-15","2022-02-05"] 44 | }, 45 | { 46 | id: 555550, 47 | name: 'XXXXXX', 48 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 49 | backgroundColor: '#28f', 50 | textColor: '#fff', 51 | days: ["2022-02-15","2022-02-25"] 52 | }, 53 | ], 54 | }, 55 | { 56 | type: 'normal', 57 | color: '', 58 | name: '流星蝴蝶剑', 59 | schedule: [ 60 | { 61 | id: 222221, 62 | name: '小年活动', 63 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 64 | backgroundColor: '#482', 65 | textColor: '#fff', 66 | days: ["2022-02-25","2022-03-10"] 67 | } 68 | ], 69 | }, 70 | ]) 71 | ``` 72 | 73 | ### 组件接收参数 74 | 75 | | 参数名 | 类型 | 默认值 | 说明 | 76 | | ------ | ------ | -------- | ------------ | 77 | | data | Array[Object] | [] | 甘特图数据 | 78 | | dateRangeList | Array | [] | 当前图表内的日期区间,此数组长度为2,内容为起始时间, 格式为'YYYY-MM-DD' | 79 | | itemText | String | null | 表头描述文字 | 80 | | dateText | String | null | 表头描述文字 | 81 | | activeDate | String | 今天 | 当前时间轴高亮显示的一天,(不会覆盖日程样式),'YYYY-MM-DD'格式时间字符串 | 82 | | repeatMode | Object | 见下方 | 重叠日程展示模式配置 | 83 | | itemWidth | Number | 40 | 日期格子的宽度,最小40 | 84 | | itemHeight | Number | 40 | 日期格子的高度度,最小40 | 85 | | scheduleTitle | Function | null | 日程上面展示的文本,function接收日程信息为参数,最终使用该方法返回值渲染 | 86 | | borderColor | String | '#eee' | 表格边框颜色 | 87 | 88 | > 组件内容宽度需要自行控制把握最小宽度 89 | 90 | ### 组件事件 91 | 92 | | 参数名 | 类型 | 说明 | 93 | | ------ | ------ | ------------ | 94 | | scheduleClick | Function | 点击日程的回调事件,接收一个日程详情参数 | 95 | | scrollXEnd | Function | 横向滚动条滚动到底部的事件 | 96 | | scrollYEnd | Function | 竖向滚动条滚动到底部的事件 | 97 | 98 | ### data配置 Array[Object] 99 | 100 | | 参数名 | 取值 |说明 | 101 | | ------ | ------ | ------ | 102 | | type | 'alike'\|\|'normal' | 项目类型(展示风格) | 103 | | color | css颜色格式 | 当前项目背景色, type为'alike'时生效 | 104 | | name | String | 当前项目名称 | 105 | | schedule | Array[Object] | 项目日程 | 106 | 107 | ### schedule 项目日程配置 108 | 109 | > 为了便于业务开发,可以在以下基础上任意拓展字段 110 | 111 | | 参数名 |说明 | 112 | | ------ | -------------------- | 113 | | id | 日程全局唯一id | 114 | | name | 日程名称 | 115 | | desc | 日程描述 | 116 | | backgroundColor | 日程背景色 | 117 | | textColor | 日程名称展示文字颜色 | 118 | | days | 日程日期列表`Array`, 数组内容为合法的连续的日期,日期格式为 `YYYY-MM-DD`,也可以简写为长度为2的数组,数组内容分别为起始日期 | 119 | 120 | ### repeatMode配置 Object 121 | 122 | | 参数名 | 可选值 | 默认值 | 说明 | 123 | | ------ | ------ | -------- | ---------- | 124 | | mode | 'cover'\|\|'extract' | 'cover' | 重叠日程的处理方式,正常覆盖或者单独提取重复日程再组合, cover会忽略repeatMode其余选项 | 125 | | backgroundColor | css颜色格式 | '#FFFFCC' | extract模式下的背景色 | 126 | | textColor | css颜色格式 | '#336666' | extract模式下的文字颜色 | 127 | | name | `String`\|\|`Function` | '重叠日程' | 重叠日程的展示文字,Function接收一个list参数,参数为重叠日程Array | 128 | | desc | `String`\|\|`Function` | '这是多个日程' | 重叠日程的描述文字,Function接收一个list参数,参数为重叠日程Array | 129 | 130 | 131 | ### 组件实例对外暴露的方法 132 | 133 | #### 导出当前甘特图的完整快照图片 134 | 135 | ```html 136 | 140 | 141 | ``` 142 | 143 | ```js 144 | const gantt = ref(null) 145 | 146 | const exportImg = () => { 147 | gantt.value.exportImg({ download: true, waterValue: 'YiJio制作' }) 148 | } 149 | ``` 150 | > `exportImg` 方法接收一个`Object`, 配置图片的导出行为, `exportImg` 返回一个`Promise`, 成功状态会接收到图片的base64值 151 | > | 参数名 | 可选值 | 默认值 | 说明 | 152 | > | ------ | ------ | -------- | ---------- | 153 | > | download | `Boolean` | `true` | 是否自动下载图片 | 154 | > | waterValue | `String` | `''` | 图片水印文字, 为空就不添加水印,暂不支持自定义文字样式 | 155 | 156 | 157 | #### 导出当前甘特图Excel 158 | 159 | ```html 160 | 164 | 165 | ``` 166 | ```js 167 | const gantt = ref(null) 168 | 169 | const exportGanttExcel = () => { 170 | gantt.value.exportGanttExcel({ fileName: '测试列表' }) 171 | } 172 | ``` 173 | > `exportGanttExcel`接收一个对象`file`,配置导出文件的信息 174 | > | 参数名 | 可选值 | 默认值 | 说明 | 175 | > | ------ | ------ | -------- | ---------- | 176 | > | fileName | `String` | '数据' | 导出文件名称 | 177 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/components/Gantt.vue' 2 | export * from './src/util/index.js' 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-gantt", 3 | "private": false, 4 | "version": "1.1.8-7", 5 | "description": "Vue3甘特图组件", 6 | "author": "Yijio", 7 | "license": "ISC", 8 | "files": [ 9 | "dist" 10 | ], 11 | "main": "./dist/gantt.umd.js", 12 | "module": "./dist/gantt.es.js", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/gantt.es.js", 16 | "require": "./dist/gantt.umd.js" 17 | }, 18 | "./dist/*": "./dist/*" 19 | }, 20 | "scripts": { 21 | "dev": "vite", 22 | "build": "vite build", 23 | "preview": "vite preview" 24 | }, 25 | "dependencies": { 26 | "@ctrl/tinycolor": "^3.6.0", 27 | "core-js": "^3.21.1", 28 | "html2canvas": "^1.4.1", 29 | "lay-excel": "^1.7.6", 30 | "lodash": "^4.17.21" 31 | }, 32 | "devDependencies": { 33 | "@vitejs/plugin-vue": "^4.0.0", 34 | "less": "^4.1.2", 35 | "terser": "^5.16.1", 36 | "vite": "^4.0.4" 37 | }, 38 | "keywords": [ 39 | "vue3", 40 | "gantt", 41 | "甘特图", 42 | "vue", 43 | "Excel" 44 | ], 45 | "homepage": "https://github.com/ddmy/vue-gantt#readme", 46 | "bugs": { 47 | "url": "https://github.com/ddmy/vue-gantt/issues" 48 | }, 49 | "peerDependencies": { 50 | "vue": "^3.2.45" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 123 | 124 | 137 | 138 | -------------------------------------------------------------------------------- /src/assets/data.js: -------------------------------------------------------------------------------- 1 | import { fethDaysRange } from '../util/index' 2 | /** 3 | * alike: 全部一样 4 | */ 5 | export default { 6 | data1: [ 7 | { 8 | type: 'alike', // 代表 9 | color: 'rgb(255,222,215)', 10 | name: '年度考核' 11 | }, 12 | { 13 | type: 'normal', // 代表 14 | color: '', 15 | name: '完美世界', 16 | schedule: [ 17 | { 18 | id: 333330, 19 | name: '900勇士同时在线庆祝活动', 20 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 21 | backgroundColor: 'rgb(253, 211, 172)', 22 | textColor: 'rgb(245, 36, 9)', 23 | days: fethDaysRange('2022-01-15', '2022-02-05') 24 | }, 25 | { 26 | id: 555550, 27 | name: '正月不剃头-重叠一个', 28 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 29 | backgroundColor: '#28f', 30 | textColor: '#fff', 31 | days: fethDaysRange('2022-02-27', '2022-03-15') 32 | }, 33 | { 34 | id: 222220, 35 | name: '春节活动', 36 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 37 | backgroundColor: '#768', 38 | textColor: '#fff', 39 | days: fethDaysRange('2022-02-28', '2022-03-02') 40 | }, 41 | { 42 | id: 111110, 43 | name: '中元节活动', 44 | desc: '这个活动很重要,营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 45 | backgroundColor: '#369', 46 | textColor: '#fff', 47 | days: fethDaysRange('2022-03-2', '2022-03-8') 48 | }, 49 | { 50 | id: 44440, 51 | name: '三八妇女活动', 52 | desc: '这个活动很重要,营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 53 | backgroundColor: '#a59', 54 | textColor: '#fff', 55 | days: fethDaysRange('2022-03-26', '2022-04-20') 56 | }, 57 | ], 58 | }, 59 | { 60 | type: 'normal', // 代表 61 | color: '', 62 | name: '流星蝴蝶剑', 63 | schedule: [ 64 | { 65 | id: 222221, 66 | name: '小年活动', 67 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 68 | backgroundColor: '#482', 69 | textColor: '#fff', 70 | days: fethDaysRange('2022-02-25', '2022-03-10') 71 | }, 72 | { 73 | id: 111111, 74 | name: '中元节活动', 75 | desc: '这个活动很重要,营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 76 | backgroundColor: '#389', 77 | textColor: '#fff', 78 | days: fethDaysRange('2022-02-10', '2022-03-13') 79 | }, 80 | { 81 | id: 3333331, 82 | name: '情人节活动', 83 | desc: '浪漫情人节,让玩家冲大把大把的钱,营收破亿!!!冲冲冲!', 84 | backgroundColor: 'pink', 85 | textColor: '#333', 86 | days: fethDaysRange('2022-04-23', '2022-05-10') 87 | }, 88 | { 89 | id: 444441, 90 | name: '双12冲冲冲!', 91 | desc: '双12是一年一度的割韭菜的季节,猎人们摩拳擦掌,按耐已久~', 92 | backgroundColor: '#000', 93 | textColor: '#fff', 94 | days: fethDaysRange('2022-02-1', '2022-03-01') 95 | }, 96 | ], 97 | }, 98 | { 99 | type: 'normal', // 代表 100 | color: '', 101 | name: '天涯明月刀', 102 | schedule: [ 103 | { 104 | id: 333332, 105 | name: '庆元旦活动', 106 | desc: '这个活动很重要,营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 107 | backgroundColor: '#fa6', 108 | textColor: '#fff', 109 | days: fethDaysRange('2022-03-28', '2022-04-10') 110 | }, 111 | { 112 | id: 111112, 113 | name: '中元节活动', 114 | desc: '这个活动很重要,营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 115 | backgroundColor: '#521', 116 | textColor: '#fff', 117 | days: fethDaysRange('2022-02-05', '2022-02-17') 118 | }, 119 | { 120 | id: 444442, 121 | name: '好友回归活动', 122 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 123 | backgroundColor: '#2dd', 124 | textColor: '#fff', 125 | days: fethDaysRange('2022-03-15', '2022-03-22') 126 | }, 127 | { 128 | id: 222222, 129 | name: '元宵西游记联动', 130 | desc: '这个活动很重要,6666666营收数百万,跨部门合作的一个大项目,BOSS亲自下场坐镇指挥,大家一定要团结一心!', 131 | backgroundColor: '#636', 132 | textColor: '#fff', 133 | days: fethDaysRange('2022-02-12', '2022-02-20') 134 | }, 135 | ], 136 | }, 137 | ], 138 | data2: [ 139 | { 140 | name: '长线版本节点', 141 | color: '#f6f6f6', 142 | type: 'alike' 143 | }, 144 | { 145 | name: '和平精英', 146 | type: 'normal', 147 | schedule: [ 148 | { 149 | id: 1111, 150 | name: '元旦玩法+S级商业化', 151 | desc: '活跃提升20%,收入提升50%', 152 | backgroundColor: '#ffdebc', 153 | textColor: '#ff5d35', 154 | // days: ['2022-01-30', '2022-01-31', '2022-02-01', '2022-02-02'] 155 | days: ['2022-01-30', '2022-02-07'] 156 | }, 157 | { 158 | id: 1112, 159 | name: 'S26新赛季,战令更新', 160 | desc: '活跃提升20%,收入提升30%', 161 | backgroundColor: '#ffdebc', 162 | textColor: '#ff5d35', 163 | days: ['2022-02-06', '2022-02-07', '2022-02-08', '2022-02-09'] 164 | }, 165 | { 166 | id: 1113, 167 | name: '新皮肤活动', 168 | desc: '活跃提升20%,收入提升30%', 169 | backgroundColor: '#ffdebc', 170 | textColor: '#ff5d35', 171 | days: ['2022-02-18', '2022-02-19', '2022-02-20', '2022-02-21', '2022-02-22', '2022-02-23', '2022-02-24'] 172 | }, 173 | ] 174 | }, 175 | { 176 | name: '金铲铲之战', 177 | type: 'normal', 178 | schedule: [ 179 | { 180 | id: 2221, 181 | name: '元旦玩法+S级商业化', 182 | desc: '活跃提升20%,收入提升50%', 183 | backgroundColor: '#003333', 184 | textColor: '#999999', 185 | days: ['2022-01-28','2022-01-29','2022-01-30', '2022-01-31', '2022-02-01'] 186 | }, 187 | { 188 | id: 2222, 189 | name: 'S26新赛季,战令更新', 190 | desc: '活跃提升20%,收入提升30%', 191 | backgroundColor: '#99AACC', 192 | textColor: '#336699', 193 | days: ['2022-01-30', '2022-01-31', '2022-02-01', '2022-02-02', '2022-02-03', '2022-02-04', '2022-02-05'] 194 | }, 195 | { 196 | id: 2223, 197 | name: '五星级活动', 198 | desc: '活跃提升20%,收入提升30%', 199 | backgroundColor: '#006633', 200 | textColor: '#CCCC66', 201 | days: ['2022-02-10', '2022-02-16'] 202 | }, 203 | ] 204 | }, 205 | { 206 | name: '西游记', 207 | type: 'normal', 208 | schedule: [ 209 | { 210 | id: 2221, 211 | name: '元旦玩法+S级商业化', 212 | desc: '活跃提升20%,收入提升50%', 213 | backgroundColor: '#003333', 214 | textColor: '#999999', 215 | days: ['2022-04-28','2022-05-01'] 216 | }, 217 | { 218 | id: 2222, 219 | name: 'S26新赛季,战令更新', 220 | desc: '活跃提升20%,收入提升30%', 221 | backgroundColor: '#99CCCC', 222 | textColor: '#336699', 223 | days: ['2022-03-20', '2022-04-05'] 224 | }, 225 | { 226 | id: 2223, 227 | name: 'FGDG级活动', 228 | desc: '活跃提升20%,收入提升30%', 229 | backgroundColor: '#CC6633', 230 | textColor: '#CCCC66', 231 | days: ['2022-02-26', '2022-03-11'] 232 | }, 233 | ] 234 | }, 235 | { 236 | name: '水浒传', 237 | type: 'normal', 238 | schedule: [ 239 | { 240 | id: 2221, 241 | name: '元旦玩法+S级商业化', 242 | desc: '活跃提升20%,收入提升50%', 243 | backgroundColor: '#003333', 244 | textColor: '#999999', 245 | days: ['2022-02-12','2022-02-21'] 246 | }, 247 | { 248 | id: 2222, 249 | name: 'S26新赛季,战令更新', 250 | desc: '活跃提升20%,收入提升30%', 251 | backgroundColor: '#99CCCC', 252 | textColor: '#336699', 253 | days: ['2022-02-23', '2022-03-05'] 254 | }, 255 | { 256 | id: 2223, 257 | name: 'RRRRR星级活动', 258 | desc: '活跃提升20%,收入提升30%', 259 | backgroundColor: '#006633', 260 | textColor: '#CCCC66', 261 | days: ['2022-02-16', '2022-02-19'] 262 | }, 263 | ] 264 | }, 265 | { 266 | name: '三国演义', 267 | type: 'normal', 268 | schedule: [] 269 | }, 270 | { 271 | name: '平凡的世界', 272 | type: 'normal', 273 | schedule: [ 274 | { 275 | id: 2223233, 276 | name: '无敌大马路', 277 | desc: 'scsscscsc,收入提升30%', 278 | backgroundColor: '#369', 279 | textColor: '#fff', 280 | days: ['2022-01-15', '2022-03-02'] 281 | }, 282 | ] 283 | }, 284 | { 285 | name: '白鹿原', 286 | type: 'normal', 287 | schedule: [ 288 | { 289 | id: 2223233, 290 | name: 'SSSS', 291 | desc: 'scsscscsc,收入提升30%', 292 | backgroundColor: '#369', 293 | textColor: '#fff', 294 | days: ['2022-02-15', '2022-03-02'] 295 | }, 296 | ] 297 | }, 298 | { 299 | name: '小兵张嘎', 300 | type: 'normal', 301 | schedule: [ 302 | { 303 | id: 2223233, 304 | name: 'SSSS', 305 | desc: 'scsscscsc,收入提升30%', 306 | backgroundColor: '#268', 307 | textColor: '#fff', 308 | days: ['2022-02-05', '2022-02-13'] 309 | }, 310 | ] 311 | }, 312 | ///////////////// 313 | { 314 | name: '长线版本节点', 315 | color: '#f6f6f6', 316 | type: 'alike' 317 | }, 318 | { 319 | name: '和平精英', 320 | type: 'normal', 321 | schedule: [ 322 | { 323 | id: 1111, 324 | name: '元旦玩法+S级商业化', 325 | desc: '活跃提升20%,收入提升50%', 326 | backgroundColor: '#ffdebc', 327 | textColor: '#ff5d35', 328 | // days: ['2022-01-30', '2022-01-31', '2022-02-01', '2022-02-02'] 329 | days: ['2022-01-30', '2022-02-07'] 330 | }, 331 | { 332 | id: 1112, 333 | name: 'S26新赛季,战令更新', 334 | desc: '活跃提升20%,收入提升30%', 335 | backgroundColor: '#ffdebc', 336 | textColor: '#ff5d35', 337 | days: ['2022-02-06', '2022-02-07', '2022-02-08', '2022-02-09'] 338 | }, 339 | { 340 | id: 1113, 341 | name: '新皮肤活动', 342 | desc: '活跃提升20%,收入提升30%', 343 | backgroundColor: '#ffdebc', 344 | textColor: '#ff5d35', 345 | days: ['2022-02-18', '2022-02-19', '2022-02-20', '2022-02-21', '2022-02-22', '2022-02-23', '2022-02-24'] 346 | }, 347 | ] 348 | }, 349 | { 350 | name: '金铲铲之战', 351 | type: 'normal', 352 | schedule: [ 353 | { 354 | id: 2221, 355 | name: '元旦玩法+S级商业化', 356 | desc: '活跃提升20%,收入提升50%', 357 | backgroundColor: '#003333', 358 | textColor: '#999999', 359 | days: ['2022-01-28','2022-01-29','2022-01-30', '2022-01-31', '2022-02-01'] 360 | }, 361 | { 362 | id: 2222, 363 | name: 'S26新赛季,战令更新', 364 | desc: '活跃提升20%,收入提升30%', 365 | backgroundColor: '#99AACC', 366 | textColor: '#336699', 367 | days: ['2022-01-30', '2022-01-31', '2022-02-01', '2022-02-02', '2022-02-03', '2022-02-04', '2022-02-05'] 368 | }, 369 | { 370 | id: 2223, 371 | name: '五星级活动', 372 | desc: '活跃提升20%,收入提升30%', 373 | backgroundColor: '#006633', 374 | textColor: '#CCCC66', 375 | days: ['2022-02-10', '2022-02-16'] 376 | }, 377 | ] 378 | }, 379 | { 380 | name: '西游记', 381 | type: 'normal', 382 | schedule: [ 383 | { 384 | id: 2221, 385 | name: '元旦玩法+S级商业化', 386 | desc: '活跃提升20%,收入提升50%', 387 | backgroundColor: '#003333', 388 | textColor: '#999999', 389 | days: ['2022-04-28','2022-05-01'] 390 | }, 391 | { 392 | id: 2222, 393 | name: 'S26新赛季,战令更新', 394 | desc: '活跃提升20%,收入提升30%', 395 | backgroundColor: '#99CCCC', 396 | textColor: '#336699', 397 | days: ['2022-03-20', '2022-04-05'] 398 | }, 399 | { 400 | id: 2223, 401 | name: 'FGDG级活动', 402 | desc: '活跃提升20%,收入提升30%', 403 | backgroundColor: '#CC6633', 404 | textColor: '#CCCC66', 405 | days: ['2022-02-26', '2022-03-11'] 406 | }, 407 | ] 408 | }, 409 | { 410 | name: '水浒传', 411 | type: 'normal', 412 | schedule: [ 413 | { 414 | id: 2221, 415 | name: '元旦玩法+S级商业化', 416 | desc: '活跃提升20%,收入提升50%', 417 | backgroundColor: '#003333', 418 | textColor: '#999999', 419 | days: ['2022-02-12','2022-02-21'] 420 | }, 421 | { 422 | id: 2222, 423 | name: 'S26新赛季,战令更新', 424 | desc: '活跃提升20%,收入提升30%', 425 | backgroundColor: '#99CCCC', 426 | textColor: '#336699', 427 | days: ['2022-02-23', '2022-03-05'] 428 | }, 429 | { 430 | id: 2223, 431 | name: 'RRRRR星级活动', 432 | desc: '活跃提升20%,收入提升30%', 433 | backgroundColor: '#006633', 434 | textColor: '#CCCC66', 435 | days: ['2022-02-16', '2022-02-19'] 436 | }, 437 | ] 438 | }, 439 | { 440 | name: '三国演义', 441 | type: 'normal', 442 | schedule: [] 443 | }, 444 | { 445 | name: '平凡的世界', 446 | type: 'normal', 447 | schedule: [ 448 | { 449 | id: 2223233, 450 | name: '无敌大马路', 451 | desc: 'scsscscsc,收入提升30%', 452 | backgroundColor: '#369', 453 | textColor: '#fff', 454 | days: ['2022-01-15', '2022-03-02'] 455 | }, 456 | ] 457 | }, 458 | { 459 | name: '白鹿原', 460 | type: 'normal', 461 | schedule: [ 462 | { 463 | id: 2223233, 464 | name: 'SSSS', 465 | desc: 'scsscscsc,收入提升30%', 466 | backgroundColor: '#369', 467 | textColor: '#fff', 468 | days: ['2022-02-15', '2022-03-02'] 469 | }, 470 | ] 471 | }, 472 | { 473 | name: '小兵张嘎', 474 | type: 'normal', 475 | schedule: [ 476 | { 477 | id: 2223233, 478 | name: 'SSSS', 479 | desc: 'scsscscsc,收入提升30%', 480 | backgroundColor: '#268', 481 | textColor: '#fff', 482 | days: ['2022-02-05', '2022-02-13'] 483 | }, 484 | ] 485 | }, 486 | ], 487 | } -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddmy/vue3-gantt/ba6afe47e3c63cd04680b350562bcf2844d680a0/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Gantt.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 591 | 592 | 593 | 823 | -------------------------------------------------------------------------------- /src/components/excel.js: -------------------------------------------------------------------------------- 1 | import LAY_EXCEL from 'lay-excel' 2 | import { TinyColor } from '@ctrl/tinycolor'; 3 | 4 | //将26进制转10进制 5 | var ConvertNum = function (str) { 6 | var n = 0; 7 | var s = str.match(/./g);//求出字符数组 8 | var j = 0; 9 | for (var i = str.length - 1, j = 1; i >= 0; i--, j *= 26) { 10 | var c = s[i].toUpperCase(); 11 | if (c < 'A' || c > 'Z') { 12 | return 0; 13 | } 14 | n += (c.charCodeAt(0) - 64) * j; 15 | } 16 | return n; 17 | } 18 | 19 | //将10进制转26进制 20 | var Convert26 = function(num){ 21 | var str=""; 22 | while (num > 0){ 23 | var m = num % 26; 24 | if (m == 0){ 25 | m = 26; 26 | } 27 | str = String.fromCharCode(m + 64) + str; 28 | num = (num - m) / 26; 29 | } 30 | return str; 31 | } 32 | 33 | // 处理16进制简写色值,返回6位 34 | const transColor = val => { 35 | const color = new TinyColor(val).toHexString() 36 | return color.substring(1) 37 | } 38 | 39 | export function exportExcel (file, rangeDate, list, dateText = '', itemText = '') { 40 | const { fileName = '数据' } = file 41 | console.log('导出excel', rangeDate, list) 42 | // 前三行日期范围 43 | let data = [] 44 | const headArr = [{ name: `${dateText}-${itemText}` }, {}, {}] 45 | const headKeys = ['name'] 46 | let monthMerageConfig = [] 47 | const templateLine = { name: '' } 48 | rangeDate.forEach(item => { 49 | item.forEach(day => { 50 | headArr[0][`${day.year}-${day.month}-${day.day}`] = `${day.year}-${day.month}` 51 | headArr[1][`${day.year}-${day.month}-${day.day}`] = day.day 52 | headArr[2][`${day.year}-${day.month}-${day.day}`] = day.week 53 | headKeys.push(`${day.year}-${day.month}-${day.day}`) 54 | 55 | templateLine[`${day.year}-${day.month}-${day.day}`] = '' 56 | }) 57 | // 计算顶部月份合并范围 58 | const prev = monthMerageConfig.at(-1) ? Convert26(ConvertNum(monthMerageConfig.at(-1).at(-1))) : 'A' 59 | const end = Convert26(ConvertNum(prev) + item.length) 60 | monthMerageConfig.push([Convert26(ConvertNum(prev) + 1), end]) 61 | }) 62 | // 处理顶部月份合并范围索引 63 | monthMerageConfig = monthMerageConfig.map(item => { 64 | item = item.map(index => index + 1) 65 | return item 66 | }) 67 | data = data.concat(headArr) 68 | 69 | 70 | const totalDays = headKeys.length - 1 71 | 72 | // 循环计算数据列 73 | list.forEach(item => { 74 | const tmp = JSON.parse(JSON.stringify(templateLine)) 75 | tmp.name = item.name 76 | tmp.type = item.type 77 | tmp.renderWorks = item.renderWorks 78 | data.push(tmp) 79 | }) 80 | 81 | const dataMergeConfig = [] 82 | /** 83 | * 记录需要设置的单元格样式 84 | * { 85 | * type: 1, // 1 设置背景色, 文字颜色 86 | * backgroundColor, textColor: 设置的值 87 | * range: 设置的范围 88 | * } 89 | */ 90 | const setStyleList = [] 91 | data.forEach((item, index) => { 92 | // excel中的行数 93 | const currentLine = index + 1 94 | // 默认占据整行的数据 95 | if (item.type === 'alike') { 96 | dataMergeConfig.push([`B${currentLine}`, `${Convert26(totalDays + 1)}${currentLine}`]) 97 | // 设置单元格背景色 98 | setStyleList.push({ 99 | type: 1, 100 | backgroundColor: 'F6F6F6', 101 | range: `A${currentLine}:${Convert26(totalDays + 1)}${currentLine}` 102 | }) 103 | } else if (item.renderWorks) { 104 | item.renderWorks.forEach(renderItem => { 105 | if (renderItem.type === 'works') { 106 | // 当前日程的起始位置 107 | const startFindIndex = headKeys.findIndex(headItem => headItem === renderItem.days[0]) 108 | const startIndex = Convert26((startFindIndex < 1 ? 1 : startFindIndex) + 1) 109 | // 当前日程结束位置 110 | const endFIndIndex = headKeys.findIndex(headItem => headItem === renderItem.days.at(-1)) 111 | const endIndex = Convert26((endFIndIndex < 1 ? headKeys.length - 1 : endFIndIndex) + 1) 112 | // 处理单元格合并 113 | dataMergeConfig.push([`${startIndex}${currentLine}`, `${endIndex}${currentLine}`]) 114 | // 设置单元格背景色 115 | setStyleList.push({ 116 | type: 1, 117 | backgroundColor: renderItem.backgroundColor.replace('#', ''), 118 | textColor: renderItem.textColor.replace('#', ''), 119 | range: `${startIndex}${currentLine}:${endIndex}${currentLine}` 120 | }) 121 | // 设置单元格内容 122 | renderItem.days.forEach(day => { 123 | data[currentLine - 1][day] = renderItem.name 124 | }) 125 | } 126 | }) 127 | } 128 | }) 129 | 130 | 131 | const merageConfig = [ ['A1', 'A3'], ...monthMerageConfig, ...dataMergeConfig ] 132 | // console.log('merageConfig--->', merageConfig, dataMergeConfig) 133 | // console.log('data--->', data) 134 | const resultData = LAY_EXCEL.filterExportData(data, headKeys) 135 | const mergeConf = LAY_EXCEL.makeMergeConfig(merageConfig) 136 | const colConf = LAY_EXCEL.makeColConfig({ A: 120, ZZZZ: 60 }, 60) 137 | const rowConf = LAY_EXCEL.makeRowConfig({ 1: 40, 99999: 40 }, 40) 138 | 139 | LAY_EXCEL.setExportCellStyle(resultData, null, { 140 | s: { 141 | alignment: { horizontal: 'center', vertical: 'center', wrapText: true }, 142 | border: { 143 | top: { style: 'thin', color: { rgb: 'E2E4E8' } }, 144 | left: { style: 'thin', color: { rgb: 'E2E4E8' } }, 145 | right: { style: 'thin', color: { rgb: 'E2E4E8' } }, 146 | bottom: { style: 'thin', color: { rgb: 'E2E4E8' } }, 147 | } 148 | } 149 | }) 150 | 151 | // 批量应用单元格样式 152 | setStyleList.forEach(item => { 153 | if (item.type === 1) { 154 | LAY_EXCEL.setExportCellStyle(resultData, item.range, { 155 | s: { 156 | fill: { bgColor: { indexed: 64 }, fgColor: { rgb: transColor(item.backgroundColor || 'FFFFFF')} }, 157 | font: { color: { rgb: transColor(item.textColor || '000000') } }, 158 | border: { 159 | top: { style: 'thin', color: { rgb: 'E2E4E8' } }, 160 | left: { style: 'thin', color: { rgb: 'E2E4E8' } }, 161 | right: { style: 'thin', color: { rgb: 'E2E4E8' } }, 162 | bottom: { style: 'thin', color: { rgb: 'E2E4E8' } }, 163 | }, 164 | alignment: { horizontal: 'center', vertical: 'center', wrapText: true } 165 | } 166 | }) 167 | } 168 | }) 169 | 170 | // console.log(resultData) 171 | // return 172 | LAY_EXCEL.exportExcel({ sheet1: resultData },`${fileName}.xlsx`, 'xlsx', { 173 | extend: { 174 | '!cols': colConf, 175 | '!rows': rowConf, 176 | '!merges': mergeConf 177 | } 178 | }) 179 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import 'core-js/actual/array/at' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | // 加工日期范围 2 | export const computedDaysRange = (daysArr, endDay = null) => { 3 | let result = daysArr 4 | if (typeof endDay === 'string') { 5 | result = fethDaysRange(daysArr, endDay) 6 | } 7 | return result.map(item => fetchDayDetail(item)) 8 | } 9 | 10 | // 获取指定日期详细信息 11 | export const fetchDayDetail = (current) => { 12 | let currentDate = null 13 | if (typeof current === 'string') { 14 | currentDate = new Date(current) 15 | } else if (current instanceof Date) { 16 | currentDate = current 17 | } 18 | const weekName = ["日", "一", "二", "三", "四", "五", "六"] 19 | 20 | return { 21 | year: String(currentDate.getFullYear()).padStart(4, '0'), 22 | month: String(currentDate.getMonth() + 1).padStart(2, '0'), 23 | day: String(currentDate.getDate()).padStart(2, '0'), 24 | week: weekName[currentDate.getDay()] 25 | } 26 | } 27 | 28 | // 获取指定月份每一天日期 2022-02 29 | export const fethDays = (str) => { 30 | const month = str.replace(/\//g, '-') 31 | const monthArr = month.split('-').map(item => Number(item)) 32 | if (monthArr.length !== 2) throw new Error('获取月份日期参数错误:', str) 33 | const count = new Date(...monthArr, 0).getDate() 34 | return new Array(count).fill().map((item, index) => index + 1) 35 | } 36 | 37 | // 获取指定范围每一天的日期 38 | export const fethDaysRange = (start, stop) => { 39 | const current = new Date(start.replace(/\//g, '-')) 40 | const end = new Date(stop) 41 | current.setHours(0, 0, 0, 0) 42 | const result = [] 43 | while (end.getTime() >= current.getTime()) { 44 | const res = fetchDayDetail(current) 45 | result.push(`${res.year}-${res.month}-${res.day}`) 46 | current.setDate(current.getDate() + 1) 47 | } 48 | return result 49 | } 50 | 51 | // 范围日期按 月份归类 52 | export const splitDaysForMonth = (daysArr) => { 53 | const res = {} 54 | daysArr.forEach(item => { 55 | const name = item.year + '-' + item.month 56 | if (res[name]) { 57 | res[name].push(item) 58 | } else { 59 | res[name] = [item] 60 | } 61 | }) 62 | return Object.values(res) 63 | } 64 | 65 | // 查询指定日期是否在指定范围内 66 | export const todayInRange = (today, range) => { 67 | return fethDaysRange(...range).includes(today) 68 | } 69 | 70 | // 获取指定月份日期列表 71 | export const fetchMonthRangeDay = (str) => { 72 | const dateList = String(str).replace('/', '-').split('-') 73 | return fethDays(str).map(item => { 74 | return String(dateList[0]).padStart(4, '0') + '-' + String(dateList[1]).padStart(2, '0') + '-' + String(item).padStart(2, '0') 75 | }) 76 | } 77 | 78 | export const fetchToday = () => { 79 | const now = new Date() 80 | const currentYear = String(now.getFullYear()).padStart(4, '0') 81 | const currentMonth = String(now.getMonth() + 1).padStart(2, '0') 82 | const currentDay = String(now.getDate()).padStart(2, '0') 83 | return `${currentYear}-${currentMonth}-${currentDay}` 84 | } 85 | 86 | export const fetchThreeDays = (now = new Date()) => { 87 | const currentYear = now.getFullYear() 88 | const currentMonth = now.getMonth() + 1 89 | const currentDate = `${currentYear}-${ String(currentMonth).padStart(2, '0')}` 90 | let prevDate = '' 91 | if (currentMonth === 1) { 92 | prevDate = `${String(currentYear - 1).padStart(4, '0')}-12` 93 | } else { 94 | prevDate = `${String(currentYear).padStart(4, '0')}-${String(currentMonth - 1).padStart(2, '0')}` 95 | } 96 | let nextDate = '' 97 | if (currentMonth === 12) { 98 | nextDate = `${String(currentYear + 1).padStart(4, '0')}-01` 99 | } else { 100 | nextDate = `${String(currentYear).padStart(4, '0')}-${String(currentMonth + 1).padStart(2, '0')}` 101 | } 102 | 103 | const prev = fetchMonthRangeDay(prevDate) 104 | const current = fetchMonthRangeDay(currentDate) 105 | const next = fetchMonthRangeDay(nextDate) 106 | let result = [...prev, ...current, ...next] 107 | result.prev = prev 108 | result.current = current 109 | result.next = next 110 | return result 111 | } 112 | 113 | const dateSplitForValue = (data) => { 114 | if (!data.length) return {} 115 | const start = data[0].days[0] 116 | const endTime = data.map(item => new Date(item.days.at(-1)).getTime()).sort((a, b) => a - b).at(-1) 117 | const endDate = fetchDayDetail(new Date(endTime)) 118 | const end = `${endDate.year}-${endDate.month}-${endDate.day}` 119 | const res = {} 120 | fethDaysRange(start, end).forEach(key => { 121 | const current = data.map((item, index) => { 122 | if (item.days.includes(key)) return index 123 | return false 124 | }).filter(item => item !== false) 125 | if (current.length) { 126 | res[key] = current 127 | } 128 | }) 129 | return res 130 | } 131 | 132 | export const workListSplitForRepeat = (arr, repeatMode) => { 133 | const resArr = arr.map(schedule => { 134 | if (schedule.type !== 'normal') return schedule 135 | const obj = dateSplitForValue(schedule.schedule) 136 | const values = Object.values(obj) 137 | const keys = Object.keys(obj) 138 | if (!keys.length) return schedule 139 | // const resMap = [...new Set(values.map(item => JSON.stringify(item)))].map(item => JSON.parse(item)) 140 | const resMap = [] 141 | for (let i = 0; i < values.length; i++) { 142 | if (JSON.stringify(resMap.at(-1)) !== JSON.stringify(values[i])) { 143 | resMap.push(values[i]) 144 | resMap.at(-1).days = [ keys[i] ] 145 | } else { 146 | if (resMap.at(-1)) { 147 | if (resMap.at(-1).days) { 148 | resMap.at(-1).days.push(keys[i]) 149 | } else { 150 | resMap.at(-1).days = [ keys[i] ] 151 | } 152 | } 153 | } 154 | } 155 | let result = new Array(resMap.length).fill({}) 156 | result = result.map((item, index) => { 157 | let r = {} 158 | if (resMap[index].length === 1) { 159 | r = { 160 | ...schedule.schedule[resMap[index][0]], 161 | days: resMap[index].days, 162 | daysSource: schedule.schedule[resMap[index][0]].days 163 | } 164 | if (r.list) { 165 | // 这是第n次,循环,重叠数据已经处理过 166 | let desc = '' 167 | let name = '' 168 | if (typeof repeatMode.desc === 'function') { 169 | desc = repeatMode.desc(r.list) 170 | } else { 171 | desc = repeatMode.desc 172 | } 173 | if (typeof repeatMode.name === 'function') { 174 | name = repeatMode.name(r.list) 175 | } else { 176 | name = repeatMode.name 177 | } 178 | r = { 179 | ...r, 180 | desc, name, backgroundColor: repeatMode.backgroundColor, textColor: repeatMode.textColor 181 | } 182 | } 183 | } else { 184 | const list = resMap[index].map(v => schedule.schedule[v]) 185 | let desc = '' 186 | let name = '' 187 | if (typeof repeatMode.desc === 'function') { 188 | desc = repeatMode.desc(list) 189 | } else { 190 | desc = repeatMode.desc 191 | } 192 | if (typeof repeatMode.name === 'function') { 193 | name = repeatMode.name(list) 194 | } else { 195 | name = repeatMode.name 196 | } 197 | r = { 198 | desc, 199 | backgroundColor: repeatMode.backgroundColor, 200 | textColor: repeatMode.textColor, 201 | name, 202 | list, 203 | days: resMap[index].days, 204 | } 205 | } 206 | // r.days = values.map((v, i) => { 207 | // if (JSON.stringify(v) === JSON.stringify(resMap[index])) { 208 | // return keys[i] 209 | // } 210 | // }).filter(v => v) 211 | // console.log('*********', r) 212 | return r 213 | }) 214 | schedule.schedule = result 215 | return schedule 216 | }) 217 | // console.log('resArr--->', resArr) 218 | return resArr 219 | } 220 | 221 | export const fetchNextMonthRange = (date = new Date()) => { 222 | return fetchThreeDays(date).next 223 | } 224 | 225 | export const fetchPrevMonthRange = (date = new Date()) => { 226 | return fetchThreeDays(date).prev 227 | } 228 | 229 | export const fetchTodayMonthRange = (date = new Date()) => { 230 | return fetchThreeDays(date).current 231 | } 232 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | const path = require('path') 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | base: './', 9 | plugins: [vue()], 10 | build: { 11 | target: 'esnext', 12 | lib: { 13 | entry: path.resolve(__dirname, './index.js'), 14 | name: 'Gantt', 15 | fileName: (format) => `gantt.${format}.js` 16 | }, 17 | rollupOptions: { 18 | // 确保外部化处理那些你不想打包进库的依赖 19 | external: ['vue', 'html2canvas', 'lodash', 'lay-excel', '@ctrl/tinycolor'], 20 | output: { 21 | // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 22 | globals: { 23 | vue: 'Vue', 24 | html2canvas: 'html2canvas', 25 | lodash: 'lodash', 26 | 'lay-excel': 'lay-excel', 27 | '@ctrl/tinycolor': '@ctrl/tinycolor' 28 | } 29 | } 30 | }, 31 | minify: 'terser', 32 | sourcemap: true 33 | } 34 | }) 35 | --------------------------------------------------------------------------------