├── .cz-config.js ├── .dumirc.ts ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .fatherrc.ts ├── .github └── workflows │ ├── gh-pages.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── commitlint.config.js ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── Gantt.less ├── Gantt.tsx ├── components │ ├── bar-list │ │ └── index.tsx │ ├── bar-thumb-list │ │ └── index.tsx │ ├── chart │ │ ├── index.less │ │ └── index.tsx │ ├── dependencies │ │ ├── Dependence.less │ │ ├── Dependence.tsx │ │ └── index.tsx │ ├── divider │ │ ├── index.less │ │ └── index.tsx │ ├── drag-present │ │ └── index.tsx │ ├── drag-resize │ │ ├── AutoScroller.ts │ │ └── index.tsx │ ├── group-bar │ │ ├── index.less │ │ └── index.tsx │ ├── invalid-task-bar │ │ ├── index.less │ │ └── index.tsx │ ├── scroll-bar │ │ ├── index.less │ │ └── index.tsx │ ├── scroll-top │ │ ├── Top.svg │ │ ├── Top_hover.svg │ │ ├── index.less │ │ └── index.tsx │ ├── selection-indicator │ │ ├── index.less │ │ └── index.tsx │ ├── table-body │ │ ├── RowToggler.less │ │ ├── RowToggler.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── table-header │ │ ├── index.less │ │ └── index.tsx │ ├── task-bar-thumb │ │ ├── index.less │ │ └── index.tsx │ ├── task-bar │ │ ├── index.less │ │ └── index.tsx │ ├── time-axis-scale-select │ │ ├── index.less │ │ └── index.tsx │ ├── time-axis │ │ ├── index.less │ │ └── index.tsx │ ├── time-indicator │ │ ├── index.less │ │ └── index.tsx │ └── today │ │ ├── index.less │ │ └── index.tsx ├── constants.ts ├── context.ts ├── hooks │ └── useDragResize.ts ├── index.tsx ├── locales │ ├── en-us.ts │ ├── index.ts │ ├── ru-ru.ts │ └── zh-cn.ts ├── store.ts ├── style │ ├── index.less │ ├── index.tsx │ └── themes │ │ ├── default.less │ │ └── index.less ├── types.ts └── utils.ts ├── test └── blah.test.tsx ├── tsconfig.json ├── typings.d.ts └── website ├── component.en-US.md ├── component.md ├── demo ├── add.en-US.tsx ├── add.tsx ├── basic.en-US.tsx ├── basic.tsx ├── child.tsx ├── column.tsx ├── custom.tsx ├── demo.tsx ├── dependence.tsx ├── filterUnit.tsx └── render.tsx ├── en ├── _meta.json ├── component.mdx └── index.mdx ├── index.en-US.md ├── index.md ├── tsconfig.json └── zh ├── _meta.json ├── component.mdx └── index.mdx /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { 4 | value: 'feat', 5 | name: '✨ feat: 新功能', 6 | }, 7 | { 8 | value: 'fix', 9 | name: '🐛 fix: 修复bug', 10 | }, 11 | { 12 | value: 'refactor', 13 | name: '♻️ refactor: 代码重构(既不是新功能也不是改bug)', 14 | }, 15 | { 16 | value: 'chore', 17 | name: '🎫 chore: 修改流程配置', 18 | }, 19 | { 20 | value: 'docs', 21 | name: '📝 docs: 修改了文档', 22 | }, 23 | { 24 | value: 'test', 25 | name: '✅ test: 更新了测试用例', 26 | }, 27 | { 28 | value: 'style', 29 | name: '💄 style: 修改了样式文件', 30 | }, 31 | { 32 | value: 'perf', 33 | name: '⚡️ perf: 新能优化', 34 | }, 35 | { 36 | value: 'revert', 37 | name: '⏪ revert: 回退提交', 38 | }, 39 | { 40 | value: 'ci', 41 | name: '⏪ ci: 持续集成', 42 | }, 43 | { 44 | value: 'build', 45 | name: '⏪ build: 打包', 46 | }, 47 | ], 48 | scopes: [], 49 | allowCustomScopes: true, 50 | allowBreakingChanges: ['feat', 'fix'], 51 | subjectLimit: 50, 52 | messages: { 53 | type: '请选择你本次改动的修改类型', 54 | customScope: '\n请明确本次改动的范围(可填):', 55 | subject: '简短描述本次改动:\n', 56 | body: '详细描述本次改动 (可填). 使用 "|" 换行:\n', 57 | breaking: '请列出任何 BREAKING CHANGES (可填):\n', 58 | footer: '请列出本次改动关闭的ISSUE (可填). 比如: #31, #34:\n', 59 | confirmCommit: '你确定提交本次改动吗?', 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | themeConfig: { 5 | name: 'rc-gantt', 6 | socialLinks: { 7 | github: 'https://github.com/ahwgs/react-gantt', 8 | }, 9 | }, 10 | resolve: { 11 | docDirs: ['./website'], 12 | }, 13 | locales: [ 14 | { id: 'zh-CN', name: '中文' }, 15 | { id: 'en-US', name: 'EN' }, 16 | ], 17 | outputPath: './dist-website', 18 | base: '/react-gantt/', 19 | publicPath: '/react-gantt/', 20 | }); 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | example 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@umijs/lint/dist/config/eslint'), 3 | parser: '@typescript-eslint/parser', 4 | rules: { 5 | 'import/prefer-default-export': 0, 6 | 'import/extensions': 0, 7 | 'jsx-quotes': 0, 8 | 'unicorn/consistent-destructuring': 0, 9 | 'import/no-extraneous-dependencies': 0, 10 | '@typescript-eslint/no-var-requires': 0, 11 | 'comma-dangle': 0, 12 | 'unicorn/filename-case': 0, 13 | '@typescript-eslint/no-unused-vars': 0, 14 | 'unicorn/no-null': 0, 15 | '@typescript-eslint/no-use-before-define': 0, 16 | 'unicorn/prevent-abbreviations': 0, 17 | 'class-methods-use-this': 0, 18 | 'brace-style': 0, 19 | 'unicorn/no-array-reduce': 0, 20 | 'operator-linebreak': 0, 21 | 'arrow-parens': 0, 22 | '@typescript-eslint/no-redeclare': 0, 23 | 'no-underscore-dangle': 0, 24 | '@typescript-eslint/no-namespace': 0, 25 | 'space-before-function-paren': 0, 26 | 'no-unused-expressions': 'off', 27 | '@typescript-eslint/no-unused-expressions': 'off', 28 | 'unicorn/no-new-array': 'off', 29 | 'react/no-deprecated': 'off', 30 | 'unicorn/consistent-function-scoping': 'off', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: {}, 6 | cjs: {}, 7 | platform: 'browser', 8 | }); 9 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # default branch 7 | workflow_dispatch: # 手动触发 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-22.04 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '16' 21 | 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v2 24 | with: 25 | version: 7.0.0 26 | 27 | - name: Install deps 28 | run: pnpm install 29 | 30 | - name: Build 31 | run: pnpm run docs:build 32 | 33 | - name: Deploy 34 | uses: peaceiris/actions-gh-pages@v3 35 | with: 36 | github_token: ${{ secrets.CI_TOKEN }} 37 | publish_dir: ./dist-website 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | # master push 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [14.19.2] #指定node版本 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} #使用action安装node环境 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Set Npm Info 22 | run: | 23 | npm config set registry https://registry.npmjs.org 24 | npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }} 25 | git config --global user.email "ah_wgs@126.com" 26 | git config --global user.name "ahwgs" 27 | 28 | - name: Install pnpm 29 | uses: pnpm/action-setup@v2.2.2 30 | with: 31 | version: 7.0.0 32 | 33 | - name: Support TLS1.2 34 | run: npm install -g https://tls-test.npmjs.com/tls-test-1.0.0.tgz 35 | 36 | - name: Install deps 37 | run: pnpm install 38 | 39 | - name: Build 40 | run: pnpm run build 41 | 42 | - name: Publish 43 | run: npm publish 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dumi/tmp 2 | .dumi/tmp-test 3 | .dumi/tmp-production 4 | 5 | *.log 6 | .DS_Store 7 | node_modules 8 | .cache 9 | dist 10 | yarn.lock 11 | .umi 12 | .idea 13 | dist-website 14 | dist-cjs 15 | esm 16 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | *.yaml 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pluginSearchDirs: false, 3 | plugins: [ 4 | require.resolve('prettier-plugin-organize-imports'), 5 | require.resolve('prettier-plugin-packagejson'), 6 | ], 7 | printWidth: 80, 8 | proseWrap: 'never', 9 | singleQuote: true, 10 | trailingComma: 'all', 11 | overrides: [ 12 | { 13 | files: '*.md', 14 | options: { 15 | proseWrap: 'preserve', 16 | }, 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@umijs/lint/dist/config/stylelint" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["dumirc", "gantt"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahwgs/react-gantt/73ed7d18b9ea93a7e5eea881e6030a8a2b766c7e/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) ahwgs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

rc-gantt

3 |

React Gantt Component

4 |
5 | 6 | English | [简体中文](./README.zh-CN.md) 7 | 8 | ## 🐯 Infos 9 | 10 | [![NPM version][npm-badge]][npm-url] 11 | [![NPM downloads][npm-downloads]][npm-url] 12 | 13 | 14 | [npm-badge]: https://img.shields.io/npm/v/rc-gantt.svg?style=flat 15 | [npm-url]: https://www.npmjs.com/package/rc-gantt 16 | [npm-downloads]: http://img.shields.io/npm/dm/rc-gantt.svg?style=flat 17 | 18 | 19 | ## WebSite 20 | 21 | [https://ahwgs.github.io/react-gantt/en-US](https://ahwgs.github.io/react-gantt/en-US) 22 | 23 | ## Quick Start 24 | 25 | ```bash 26 | # Install Dependencies 27 | $ yarn add rc-gantt 28 | 29 | # Use 30 | 31 | import RcGantt, { GanttProps, enUS } from 'rc-gantt' 32 | 33 | const data = new Array(100).fill({ 34 | name: 'Title', 35 | startDate: '2021-07-10', 36 | endDate: '2021-07-12', 37 | collapsed: false, 38 | children: [ 39 | { 40 | startDate: '2021-07-10', 41 | endDate: '2021-07-12', 42 | name: 'TitleTitle', 43 | collapsed: false, 44 | content: '123123123', 45 | }, 46 | ], 47 | }) 48 | 49 | const App = () => { 50 | return ( 51 |
52 | { 65 | return true 66 | }} 67 | /> 68 |
69 | ) 70 | } 71 | 72 | ReactDOM.render(, document.getElementById('root')) 73 | ``` 74 | 75 | ## Feedback 76 | 77 | Please visit [Github](https://github.com/ahwgs/react-gantt/issues) Or add WeChat, note `rc-gantt` 78 | 79 | WeChat ID: JavaScript_97 80 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 |

rc-gantt

3 |

React Gantt Component

4 |
5 | 6 | [English](./README.md) | 简体中文 7 | 8 | ## 🐯 使用情况 9 | 10 | [![NPM version][npm-badge]][npm-url] 11 | [![NPM downloads][npm-downloads]][npm-url] 12 | 13 | 14 | [npm-badge]: https://img.shields.io/npm/v/rc-gantt.svg?style=flat 15 | [npm-url]: https://www.npmjs.com/package/rc-gantt 16 | [npm-downloads]: http://img.shields.io/npm/dm/rc-gantt.svg?style=flat 17 | 18 | 19 | ## 官网 20 | 21 | [https://ahwgs.github.io/react-gantt/](https://ahwgs.github.io/react-gantt/) 22 | 23 | ## 快速使用 24 | 25 | ```bash 26 | # 安装依赖 27 | $ yarn add rc-gantt 28 | 29 | # 使用组件 30 | 31 | import RcGantt, { GanttProps } from 'rc-gantt' 32 | 33 | const data = new Array(100).fill({ 34 | name: '一个名称一个名称一个名称一个名称', 35 | startDate: '2021-07-10', 36 | endDate: '2021-07-12', 37 | collapsed: false, 38 | children: [ 39 | { 40 | startDate: '2021-07-10', 41 | endDate: '2021-07-12', 42 | name: '一个名称', 43 | collapsed: false, 44 | content: '123123123', 45 | }, 46 | ], 47 | }) 48 | 49 | const App = () => { 50 | return ( 51 |
52 | { 64 | return true 65 | }} 66 | /> 67 |
68 | ) 69 | } 70 | 71 | ReactDOM.render(, document.getElementById('root')) 72 | ``` 73 | 74 | ## 问题反馈 75 | 76 | 请访问 [Github](https://github.com/ahwgs/react-gantt/issues) 或加微信,备注 `rc-gantt` 77 | 78 | 微信号:JavaScript_97 79 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { '\\.(css|scss|less)$': 'identity-obj-proxy' }, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-gantt", 3 | "version": "0.3.3", 4 | "description": "react gantt component", 5 | "homepage": "https://ahwgs.github.io/react-gantt/#/", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ahwgs/react-gantt.git" 9 | }, 10 | "license": "MIT", 11 | "main": "dist/cjs/index.js", 12 | "module": "dist/esm/index.js", 13 | "types": "dist/esm/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "father build", 19 | "build:watch": "father dev", 20 | "dev": "dumi dev", 21 | "docs:build": "dumi build", 22 | "docs:preview": "dumi preview", 23 | "doctor": "father doctor", 24 | "lint": "npm run lint:es", 25 | "lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"", 26 | "prepare": "husky install && dumi setup", 27 | "prepublishOnly": "father doctor && npm run build", 28 | "start": "npm run dev" 29 | }, 30 | "commitlint": { 31 | "extends": [ 32 | "@commitlint/config-conventional" 33 | ] 34 | }, 35 | "lint-staged": { 36 | "*.{md,json}": [ 37 | "prettier --write --no-error-on-unmatched-pattern" 38 | ], 39 | "*.{css,less}": [ 40 | "stylelint --fix", 41 | "prettier --write" 42 | ], 43 | "*.{js,jsx}": [ 44 | "eslint --fix", 45 | "prettier --write" 46 | ], 47 | "*.{ts,tsx}": [ 48 | "eslint --fix", 49 | "prettier --parser=typescript --write" 50 | ] 51 | }, 52 | "dependencies": { 53 | "@babel/runtime": "^7.14.8", 54 | "ahooks": "^2.9.2", 55 | "classnames": "^2.2.6", 56 | "dayjs": "^1.9.7", 57 | "lodash": "^4.17.20", 58 | "lodash-es": "^4.17.21", 59 | "mobx": "4.7.0", 60 | "mobx-react-lite": "1.5.2" 61 | }, 62 | "devDependencies": { 63 | "@commitlint/cli": "^17.1.2", 64 | "@commitlint/config-conventional": "^17.1.0", 65 | "@types/jest": "^29.5.14", 66 | "@types/lodash": "^4.17.16", 67 | "@types/react": "^18.0.0", 68 | "@types/react-dom": "^18.0.0", 69 | "@umijs/lint": "^4.0.0", 70 | "dumi": "^2.4.13", 71 | "eslint": "^8.23.0", 72 | "father": "^4.5.2", 73 | "husky": "^8.0.1", 74 | "lint-staged": "^13.0.3", 75 | "prettier": "^2.7.1", 76 | "prettier-plugin-organize-imports": "^3.0.0", 77 | "react": "^17.0.1", 78 | "react-dom": "^17.0.1", 79 | "stylelint": "^14.9.1", 80 | "tslib": "^2.8.1", 81 | "typescript": "^4.8.2" 82 | }, 83 | "peerDependencies": { 84 | "react": ">=17.0.0", 85 | "react-dom": ">=17.0.0" 86 | }, 87 | "publishConfig": { 88 | "access": "public" 89 | }, 90 | "authors": [ 91 | "ahwgs " 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /src/Gantt.less: -------------------------------------------------------------------------------- 1 | @import './style/themes/index'; 2 | 3 | .@{gantt-prefix}-body { 4 | 5 | height: 100%; 6 | width: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | position: relative; 10 | border: 1px solid #f0f0f0; 11 | border-radius: 4px; 12 | background: #fff; 13 | 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | header { 21 | position: relative; 22 | overflow: hidden; 23 | width: 100%; 24 | height: 56px; 25 | } 26 | 27 | main { 28 | position: relative; 29 | overflow-x: hidden; 30 | overflow-y: auto; 31 | width: 100%; 32 | flex: 1; 33 | border-top: 1px solid #f0f0f0; 34 | will-change: transform; 35 | will-change: overflow; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Gantt.tsx: -------------------------------------------------------------------------------- 1 | import { useSize } from 'ahooks'; 2 | import type { Dayjs } from 'dayjs'; 3 | import React, { 4 | useContext, 5 | useEffect, 6 | useImperativeHandle, 7 | useMemo, 8 | useRef, 9 | } from 'react'; 10 | import './Gantt.less'; 11 | import Chart from './components/chart'; 12 | import Divider from './components/divider'; 13 | import ScrollBar from './components/scroll-bar'; 14 | import ScrollTop from './components/scroll-top'; 15 | import SelectionIndicator from './components/selection-indicator'; 16 | import TableBody from './components/table-body'; 17 | import TableHeader from './components/table-header'; 18 | import TimeAxis from './components/time-axis'; 19 | import TimeAxisScaleSelect from './components/time-axis-scale-select'; 20 | import TimeIndicator from './components/time-indicator'; 21 | import { BAR_HEIGHT, ROW_HEIGHT, TABLE_INDENT } from './constants'; 22 | import type { GanttContext } from './context'; 23 | import Context from './context'; 24 | import { zhCN } from './locales'; 25 | import GanttStore from './store'; 26 | import type { DefaultRecordType, Gantt } from './types'; 27 | 28 | const prefixCls = 'gantt'; 29 | 30 | const Body = ({ children }) => { 31 | const { store } = useContext(Context); 32 | const reference = useRef(null); 33 | const size = useSize(reference); 34 | useEffect(() => { 35 | store.syncSize(size); 36 | }, [size, store]); 37 | return ( 38 |
39 | {children} 40 |
41 | ); 42 | }; 43 | export interface GanttProps { 44 | data: Gantt.Record[]; 45 | columns: Gantt.Column[]; 46 | dependencies?: Gantt.Dependence[]; 47 | onUpdate: ( 48 | record: Gantt.Record, 49 | startDate: string, 50 | endDate: string, 51 | ) => Promise; 52 | startDateKey?: string; 53 | endDateKey?: string; 54 | isRestDay?: (date: string) => boolean; 55 | unit?: Gantt.Sight; 56 | rowHeight?: number; 57 | columnsWidth?: number; 58 | innerRef?: React.MutableRefObject; 59 | getBarColor?: GanttContext['getBarColor']; 60 | showBackToday?: GanttContext['showBackToday']; 61 | showUnitSwitch?: GanttContext['showUnitSwitch']; 62 | onRow?: GanttContext['onRow']; 63 | tableIndent?: GanttContext['tableIndent']; 64 | expandIcon?: GanttContext['expandIcon']; 65 | renderBar?: GanttContext['renderBar']; 66 | renderGroupBar?: GanttContext['renderGroupBar']; 67 | renderInvalidBar?: GanttContext['renderInvalidBar']; 68 | renderBarThumb?: GanttContext['renderBarThumb']; 69 | onBarClick?: GanttContext['onBarClick']; 70 | tableCollapseAble?: GanttContext['tableCollapseAble']; 71 | scrollTop?: GanttContext['scrollTop']; 72 | disabled?: boolean; 73 | alwaysShowTaskBar?: boolean; 74 | renderLeftText?: GanttContext['renderLeftText']; 75 | renderRightText?: GanttContext['renderLeftText']; 76 | onExpand?: GanttContext['onExpand']; 77 | /** 78 | * 自定义日期筛选维度 79 | */ 80 | customSights?: Gantt.SightConfig[]; 81 | locale?: GanttLocale; 82 | 83 | /** 84 | * 隐藏左侧表格 85 | */ 86 | hideTable?: boolean; 87 | } 88 | export interface GanttRef { 89 | backToday: () => void; 90 | getWidthByDate: (startDate: Dayjs, endDate: Dayjs) => number; 91 | } 92 | 93 | export interface GanttLocale { 94 | today: string; 95 | day: string; 96 | days: string; 97 | week: string; 98 | month: string; 99 | quarter: string; 100 | halfYear: string; 101 | firstHalf: string; 102 | secondHalf: string; 103 | majorFormat: { 104 | day: string; 105 | week: string; 106 | month: string; 107 | quarter: string; 108 | halfYear: string; 109 | }; 110 | minorFormat: { 111 | day: string; 112 | week: string; 113 | month: string; 114 | quarter: string; 115 | halfYear: string; 116 | }; 117 | } 118 | 119 | export const defaultLocale: GanttLocale = { ...zhCN }; 120 | 121 | const GanttComponent = ( 122 | props: GanttProps, 123 | ) => { 124 | const { 125 | data, 126 | columns, 127 | dependencies = [], 128 | onUpdate, 129 | startDateKey = 'startDate', 130 | endDateKey = 'endDate', 131 | isRestDay, 132 | getBarColor, 133 | showBackToday = true, 134 | showUnitSwitch = true, 135 | unit, 136 | onRow, 137 | tableIndent = TABLE_INDENT, 138 | expandIcon, 139 | renderBar, 140 | renderInvalidBar, 141 | renderGroupBar, 142 | onBarClick, 143 | tableCollapseAble = true, 144 | renderBarThumb, 145 | scrollTop = true, 146 | rowHeight = ROW_HEIGHT, 147 | columnsWidth, 148 | innerRef, 149 | disabled = false, 150 | alwaysShowTaskBar = true, 151 | renderLeftText, 152 | renderRightText, 153 | onExpand, 154 | customSights = [], 155 | locale = { ...defaultLocale }, 156 | hideTable = false, 157 | } = props; 158 | 159 | const store = useMemo( 160 | () => 161 | new GanttStore({ 162 | rowHeight, 163 | disabled, 164 | customSights, 165 | locale, 166 | columnsWidth, 167 | }), 168 | [rowHeight], 169 | ); 170 | useEffect(() => { 171 | store.setData(data, startDateKey, endDateKey); 172 | }, [data, endDateKey, startDateKey, store]); 173 | 174 | useEffect(() => { 175 | store.setColumns(columns); 176 | }, [columns, store]); 177 | 178 | useEffect(() => { 179 | store.setOnUpdate(onUpdate); 180 | }, [onUpdate, store]); 181 | 182 | useEffect(() => { 183 | store.setDependencies(dependencies); 184 | }, [dependencies, store]); 185 | 186 | useEffect(() => { 187 | store.setHideTable(hideTable); 188 | }, [hideTable]); 189 | 190 | useEffect(() => { 191 | if (isRestDay) store.setIsRestDay(isRestDay); 192 | }, [isRestDay, store]); 193 | 194 | useEffect(() => { 195 | if (unit) store.switchSight(unit); 196 | }, [unit, store]); 197 | 198 | useImperativeHandle(innerRef, () => ({ 199 | backToday: () => store.scrollToToday(), 200 | getWidthByDate: store.getWidthByDate, 201 | })); 202 | 203 | const ContextValue = React.useMemo( 204 | () => ({ 205 | prefixCls, 206 | store, 207 | getBarColor, 208 | showBackToday, 209 | showUnitSwitch, 210 | onRow, 211 | tableIndent, 212 | expandIcon, 213 | renderBar, 214 | renderInvalidBar, 215 | renderGroupBar, 216 | onBarClick, 217 | tableCollapseAble, 218 | renderBarThumb, 219 | scrollTop, 220 | barHeight: BAR_HEIGHT, 221 | alwaysShowTaskBar, 222 | renderLeftText, 223 | renderRightText, 224 | onExpand, 225 | hideTable, 226 | }), 227 | [ 228 | store, 229 | getBarColor, 230 | showBackToday, 231 | showUnitSwitch, 232 | onRow, 233 | tableIndent, 234 | expandIcon, 235 | renderBar, 236 | renderInvalidBar, 237 | renderGroupBar, 238 | onBarClick, 239 | tableCollapseAble, 240 | renderBarThumb, 241 | scrollTop, 242 | alwaysShowTaskBar, 243 | renderLeftText, 244 | renderRightText, 245 | onExpand, 246 | hideTable, 247 | ], 248 | ); 249 | 250 | return ( 251 | 252 | 253 |
254 | {!hideTable && } 255 | 256 |
257 |
258 | 259 | {!hideTable && } 260 | 261 |
262 | {!hideTable && } 263 | {showBackToday && } 264 | {showUnitSwitch && } 265 | 266 | {scrollTop && } 267 | 268 |
269 | ); 270 | }; 271 | export default GanttComponent; 272 | -------------------------------------------------------------------------------- /src/components/bar-list/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { observer } from 'mobx-react-lite' 3 | import React, { useContext } from 'react' 4 | import Context from '../../context' 5 | import GroupBar from '../group-bar' 6 | import InvalidTaskBar from '../invalid-task-bar' 7 | import TaskBar from '../task-bar' 8 | 9 | const BarList: React.FC = () => { 10 | const { store } = useContext(Context) 11 | const barList = store.getBarList 12 | const { count, start } = store.getVisibleRows 13 | return ( 14 | <> 15 | {barList.slice(start, start + count).map(bar => { 16 | if (bar._group) return 17 | 18 | return bar.invalidDateRange ? : 19 | })} 20 | 21 | ) 22 | } 23 | export default observer(BarList) 24 | -------------------------------------------------------------------------------- /src/components/bar-thumb-list/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { observer } from 'mobx-react-lite' 3 | import React, { useContext } from 'react' 4 | import Context from '../../context' 5 | import TaskBarThumb from '../task-bar-thumb' 6 | 7 | const BarThumbList: React.FC = () => { 8 | const { store } = useContext(Context) 9 | const barList = store.getBarList 10 | const { count, start } = store.getVisibleRows 11 | return ( 12 | <> 13 | {barList.slice(start, start + count).map(bar => { 14 | if (store.getTaskBarThumbVisible(bar)) return 15 | return null 16 | })} 17 | 18 | ) 19 | } 20 | export default observer(BarThumbList) 21 | -------------------------------------------------------------------------------- /src/components/chart/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | 3 | .@{gantt-prefix}-chart { 4 | position: absolute; 5 | top: 0; 6 | overflow-x: hidden; 7 | overflow-y: hidden; 8 | } 9 | 10 | .@{gantt-prefix}-chart-svg-renderer { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | } 15 | 16 | .@{gantt-prefix}-render-chunk { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | will-change: transform; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/chart/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite' 2 | import React, { memo, useCallback, useContext, useEffect } from 'react' 3 | import Context from '../../context' 4 | import BarList from '../bar-list' 5 | import BarThumbList from '../bar-thumb-list' 6 | import Dependencies from '../dependencies' 7 | import DragPresent from '../drag-present' 8 | import Today from '../today' 9 | import './index.less' 10 | 11 | const Chart: React.FC = () => { 12 | const { store, prefixCls } = useContext(Context) 13 | const { tableWidth, viewWidth, bodyScrollHeight, translateX, chartElementRef } = store 14 | const minorList = store.getMinorList() 15 | const handleMouseMove = useCallback( 16 | (event: React.MouseEvent) => { 17 | event.persist() 18 | store.handleMouseMove(event) 19 | }, 20 | [store] 21 | ) 22 | 23 | const handleMouseLeave = useCallback(() => { 24 | store.handleMouseLeave() 25 | }, [store]) 26 | useEffect(() => { 27 | const element = chartElementRef.current 28 | if (element) element.addEventListener('wheel', store.handleWheel) 29 | 30 | return () => { 31 | if (element) element.removeEventListener('wheel', store.handleWheel) 32 | } 33 | }, [chartElementRef, store]) 34 | return ( 35 |
46 | 54 | 55 | 62 | 63 | 64 | 65 | {minorList.map(item => 66 | item.isWeek ? ( 67 | 68 | 69 | 78 | 79 | ) : ( 80 | 81 | 82 | 83 | ) 84 | )} 85 | 86 | 87 | 88 |
95 | 96 | 97 | 98 |
99 |
100 | ) 101 | } 102 | export default memo(observer(Chart)) 103 | -------------------------------------------------------------------------------- /src/components/dependencies/Dependence.less: -------------------------------------------------------------------------------- 1 | .task-dependency-line { 2 | z-index: -1; 3 | 4 | .line { 5 | stroke: #f87872; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/dependencies/Dependence.tsx: -------------------------------------------------------------------------------- 1 | import find from 'lodash/find'; 2 | import { observer } from 'mobx-react-lite'; 3 | import React, { useContext } from 'react'; 4 | import Context from '../../context'; 5 | import type { Gantt } from '../../types'; 6 | import './Dependence.less'; 7 | 8 | const spaceX = 10; 9 | const spaceY = 10; 10 | interface DependenceProps { 11 | data: Gantt.Dependence; 12 | } 13 | interface Point { 14 | x: number; 15 | y: number; 16 | } 17 | /** 18 | * 获取关键点 19 | * 20 | * @param from 21 | * @param to 22 | */ 23 | function getPoints(from: Point, to: Point, type: Gantt.DependenceType) { 24 | const { x: fromX, y: fromY } = from; 25 | const { x: toX, y: toY } = to; 26 | const sameSide = type === 'finish_finish' || type === 'start_start'; 27 | // 同向,只需要两个关键点 28 | if (sameSide) { 29 | if (type === 'start_start') { 30 | return [ 31 | { x: Math.min(fromX - spaceX, toX - spaceX), y: fromY }, 32 | { x: Math.min(fromX - spaceX, toX - spaceX), y: toY }, 33 | ]; 34 | } 35 | return [ 36 | { x: Math.max(fromX + spaceX, toX + spaceX), y: fromY }, 37 | { x: Math.max(fromX + spaceX, toX + spaceX), y: toY }, 38 | ]; 39 | } 40 | // 不同向,需要四个关键点 41 | 42 | return [ 43 | { x: type === 'finish_start' ? fromX + spaceX : fromX - spaceX, y: fromY }, 44 | { 45 | x: type === 'finish_start' ? fromX + spaceX : fromX - spaceX, 46 | y: toY - spaceY, 47 | }, 48 | { 49 | x: type === 'finish_start' ? toX - spaceX : toX + spaceX, 50 | y: toY - spaceY, 51 | }, 52 | { x: type === 'finish_start' ? toX - spaceX : toX + spaceX, y: toY }, 53 | ]; 54 | } 55 | const Dependence: React.FC = ({ data }) => { 56 | const { store, barHeight } = useContext(Context); 57 | const { from, to, type, color = '#f87872' } = data; 58 | const barList = store.getBarList; 59 | const fromBar = find(barList, (bar) => bar.record.id === from); 60 | const toBar = find(barList, (bar) => bar.record.id === to); 61 | if (!fromBar || !toBar) return null; 62 | 63 | const posY = barHeight / 2; 64 | const [start, end] = (() => [ 65 | { 66 | x: 67 | type === 'finish_finish' || type === 'finish_start' 68 | ? fromBar.translateX + fromBar.width 69 | : fromBar.translateX, 70 | y: fromBar.translateY + posY, 71 | }, 72 | { 73 | x: 74 | type === 'finish_finish' || type === 'start_finish' 75 | ? toBar.translateX + toBar.width 76 | : toBar.translateX, 77 | y: toBar.translateY + posY, 78 | }, 79 | ])(); 80 | const points = [...getPoints(start, end, type), end]; 81 | const endPosition = 82 | type === 'start_finish' || type === 'finish_finish' ? -1 : 1; 83 | return ( 84 | 85 | `L${point.x},${point.y}`).join('\n')} 90 | L${end.x},${end.y} 91 | `} 92 | strokeWidth="1" 93 | fill="none" 94 | /> 95 | 105 | 106 | ); 107 | }; 108 | export default observer(Dependence); 109 | -------------------------------------------------------------------------------- /src/components/dependencies/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import Context from '../../context'; 4 | import Dependence from './Dependence'; 5 | 6 | const Dependencies: React.FC = () => { 7 | const { store } = useContext(Context); 8 | const { dependencies } = store; 9 | return ( 10 | <> 11 | {dependencies.map((dependence) => ( 12 | 13 | ))} 14 | 15 | ); 16 | }; 17 | export default observer(Dependencies); 18 | -------------------------------------------------------------------------------- /src/components/divider/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @divider-prefix-cls: ~'@{gantt-prefix}-divider'; 3 | 4 | .@{divider-prefix-cls} { 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | cursor: col-resize; 9 | 10 | &:hover { 11 | hr { 12 | border-color: #3b88f4; 13 | 14 | &:before { 15 | background: #3b88f4; 16 | } 17 | } 18 | 19 | .@{divider-prefix-cls}-icon-wrapper { 20 | background-color: #3b88f4; 21 | border-color: #3b88f4; 22 | border-top: 0; 23 | border-bottom: 0; 24 | cursor: pointer; 25 | 26 | &:after { 27 | content: ''; 28 | right: -3px; 29 | position: absolute; 30 | width: 2px; 31 | height: 30px; 32 | background-color: transparent; 33 | } 34 | 35 | .@{divider-prefix-cls}-arrow:after, 36 | .@{divider-prefix-cls}-arrow:before { 37 | background-color: #fff; 38 | } 39 | } 40 | } 41 | 42 | & > hr { 43 | margin: 0; 44 | height: 100%; 45 | width: 0; 46 | border: none; 47 | border-right: 1px solid transparent; 48 | } 49 | 50 | & > &-icon-wrapper { 51 | position: absolute; 52 | left: 1px; 53 | top: 50%; 54 | transform: translateY(-50%); 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | width: 14px; 59 | height: 30px; 60 | border-radius: 0 4px 4px 0; 61 | border: 1px solid #f0f0f0; 62 | border-left: 0; 63 | background-color: #fff; 64 | } 65 | 66 | &-arrow:before { 67 | bottom: -1px; 68 | transform: rotate(30deg); 69 | } 70 | 71 | &-arrow:after { 72 | top: -1px; 73 | transform: rotate(-30deg); 74 | } 75 | 76 | &-arrow:after, 77 | &-arrow:before { 78 | content: ''; 79 | display: block; 80 | position: relative; 81 | width: 2px; 82 | height: 8px; 83 | background-color: #bfbfbf; 84 | border-radius: 1px; 85 | } 86 | 87 | &-arrow&-reverse:before { 88 | transform: rotate(-30deg); 89 | } 90 | 91 | &-arrow&-reverse:after { 92 | transform: rotate(30deg); 93 | } 94 | } 95 | 96 | .@{divider-prefix-cls}_only > hr:before { 97 | content: ''; 98 | position: absolute; 99 | border-top: 7px solid white; 100 | border-bottom: 7px solid white; 101 | background: #a7add0; 102 | z-index: 2; 103 | height: 26px; 104 | top: 50%; 105 | transform: translateY(-50%); 106 | width: 2px; 107 | } 108 | 109 | .@{divider-prefix-cls}_only > hr { 110 | border-color: #a7add0; 111 | } 112 | -------------------------------------------------------------------------------- /src/components/divider/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { observer } from 'mobx-react-lite'; 3 | import React, { useCallback, useContext } from 'react'; 4 | import Context from '../../context'; 5 | import useDragResize from '../../hooks/useDragResize'; 6 | import './index.less'; 7 | 8 | const Divider: React.FC = () => { 9 | const { store, tableCollapseAble, prefixCls } = useContext(Context); 10 | const prefixClsDivider = `${prefixCls}-divider`; 11 | const { tableWidth } = store; 12 | 13 | const handleClick = useCallback( 14 | (event: React.MouseEvent) => { 15 | event.stopPropagation(); 16 | store.toggleCollapse(); 17 | }, 18 | [store], 19 | ); 20 | const left = tableWidth; 21 | 22 | const handleResize = useCallback( 23 | ({ width }: { width: number }) => { 24 | store.handleResizeTableWidth(width); 25 | }, 26 | [store], 27 | ); 28 | const [handleMouseDown, resizing] = useDragResize(handleResize, { 29 | initSize: { 30 | width: tableWidth, 31 | }, 32 | minWidth: 200, 33 | maxWidth: store.width * 0.6, 34 | }); 35 | return ( 36 |
44 | {resizing && ( 45 |
56 | )} 57 |
58 | {tableCollapseAble && ( 59 |
e.stopPropagation()} 63 | onClick={handleClick} 64 | > 65 | 70 |
71 | )} 72 |
73 | ); 74 | }; 75 | export default observer(Divider); 76 | -------------------------------------------------------------------------------- /src/components/drag-present/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import Context from '../../context'; 4 | 5 | /** 6 | * 拖动时的提示条 7 | */ 8 | const DragPresent: React.FC = () => { 9 | const { store } = useContext(Context); 10 | const { dragging, draggingType, bodyScrollHeight } = store; 11 | 12 | if (!dragging) { 13 | return null; 14 | } 15 | // 和当前拖动的块一样长 16 | const { width, translateX } = dragging; 17 | const left = translateX; 18 | const right = translateX + width; 19 | const leftLine = draggingType === 'left' || draggingType === 'move'; 20 | const rightLine = draggingType === 'right' || draggingType === 'move'; 21 | return ( 22 | 23 | {leftLine && } 24 | 31 | {rightLine && } 32 | 33 | ); 34 | }; 35 | export default observer(DragPresent); 36 | -------------------------------------------------------------------------------- /src/components/drag-resize/AutoScroller.ts: -------------------------------------------------------------------------------- 1 | class AutoScroller { 2 | constructor({ 3 | scroller, 4 | rate = 5, 5 | space = 50, 6 | onAutoScroll, 7 | reachEdge, 8 | }: { 9 | scroller?: HTMLElement; 10 | rate?: number; 11 | space?: number; 12 | onAutoScroll: (delta: number) => void; 13 | reachEdge: (position: 'left' | 'right') => boolean; 14 | }) { 15 | this.scroller = scroller || null; 16 | this.rate = rate; 17 | this.space = space; 18 | this.onAutoScroll = onAutoScroll; 19 | this.reachEdge = reachEdge; 20 | } 21 | 22 | rate: number; 23 | 24 | space: number; 25 | 26 | scroller: HTMLElement | null = null; 27 | 28 | autoScrollPos: number = 0; 29 | 30 | clientX: number | null = null; 31 | 32 | scrollTimer: number | null = null; 33 | 34 | onAutoScroll: (delta: number) => void; 35 | 36 | reachEdge: (position: 'left' | 'right') => boolean; 37 | 38 | handleDraggingMouseMove = (event: MouseEvent) => { 39 | this.clientX = event.clientX; 40 | }; 41 | handleScroll = (position: 'left' | 'right') => { 42 | if (this.reachEdge(position)) { 43 | return; 44 | } 45 | if (position === 'left') { 46 | this.autoScrollPos -= this.rate; 47 | this.onAutoScroll(-this.rate); 48 | } else if (position === 'right') { 49 | this.autoScrollPos += this.rate; 50 | this.onAutoScroll(this.rate); 51 | } 52 | }; 53 | 54 | start = () => { 55 | this.autoScrollPos = 0; 56 | document.addEventListener('mousemove', this.handleDraggingMouseMove); 57 | const scrollFunc = () => { 58 | if (this.scroller && this.clientX !== null) { 59 | if ( 60 | this.clientX + this.space > 61 | this.scroller?.getBoundingClientRect().right 62 | ) { 63 | this.handleScroll('right'); 64 | } else if ( 65 | this.clientX - this.space < 66 | this.scroller?.getBoundingClientRect().left 67 | ) { 68 | this.handleScroll('left'); 69 | } 70 | } 71 | 72 | this.scrollTimer = requestAnimationFrame(scrollFunc); 73 | }; 74 | this.scrollTimer = requestAnimationFrame(scrollFunc); 75 | }; 76 | 77 | // 停止自动滚动 78 | stop = () => { 79 | document.removeEventListener('mousemove', this.handleDraggingMouseMove); 80 | if (this.scrollTimer) { 81 | cancelAnimationFrame(this.scrollTimer); 82 | } 83 | }; 84 | } 85 | export default AutoScroller; 86 | -------------------------------------------------------------------------------- /src/components/drag-resize/index.tsx: -------------------------------------------------------------------------------- 1 | import { usePersistFn } from 'ahooks'; 2 | import { observer } from 'mobx-react-lite'; 3 | import React, { useMemo, useRef, useState } from 'react'; 4 | import { createPortal } from 'react-dom'; 5 | import AutoScroller from './AutoScroller'; 6 | 7 | interface DragResizeProps 8 | extends Omit, 'onResize'> { 9 | onResize: ({ width, x }: { width: number; x: number }) => void; 10 | /* 拖拽前的size */ 11 | onResizeEnd?: ({ width, x }: { width: number; x: number }) => void; 12 | onBeforeResize?: () => void; 13 | minWidth?: number; 14 | type: 'left' | 'right' | 'move'; 15 | grid?: number; 16 | scroller?: HTMLElement; 17 | defaultSize: { 18 | width: number; 19 | x: number; 20 | }; 21 | autoScroll?: boolean; 22 | onAutoScroll?: (delta: number) => void; 23 | reachEdge?: (position: 'left' | 'right') => boolean; 24 | /* 点击就算开始 */ 25 | clickStart?: boolean; 26 | disabled?: boolean; 27 | } 28 | const snap = (n: number, size: number): number => Math.round(n / size) * size; 29 | const DragResize: React.FC = ({ 30 | type, 31 | onBeforeResize, 32 | onResize, 33 | onResizeEnd, 34 | minWidth = 0, 35 | grid, 36 | defaultSize: { x: defaultX, width: defaultWidth }, 37 | scroller, 38 | autoScroll: enableAutoScroll = true, 39 | onAutoScroll, 40 | reachEdge = () => false, 41 | clickStart = false, 42 | children, 43 | disabled = false, 44 | ...otherProps 45 | }) => { 46 | const [resizing, setResizing] = useState(false); 47 | const handleAutoScroll = usePersistFn((delta: number) => { 48 | updateSize(); 49 | onAutoScroll && onAutoScroll(delta); 50 | }); 51 | // TODO persist reachEdge 52 | const autoScroll = useMemo( 53 | () => 54 | new AutoScroller({ scroller, onAutoScroll: handleAutoScroll, reachEdge }), 55 | [handleAutoScroll, scroller, reachEdge], 56 | ); 57 | const positionRef = useRef({ 58 | clientX: 0, 59 | width: defaultWidth, 60 | x: defaultX, 61 | }); 62 | const moveRef = useRef({ 63 | clientX: 0, 64 | }); 65 | const updateSize = usePersistFn(() => { 66 | if (disabled) return; 67 | const distance = 68 | moveRef.current.clientX - 69 | positionRef.current.clientX + 70 | autoScroll.autoScrollPos; 71 | switch (type) { 72 | case 'left': { 73 | let width = positionRef.current.width - distance; 74 | if (minWidth !== undefined) width = Math.max(width, minWidth); 75 | 76 | if (grid) width = snap(width, grid); 77 | 78 | const pos = width - positionRef.current.width; 79 | const x = positionRef.current.x - pos; 80 | onResize({ width, x }); 81 | break; 82 | } 83 | // 向右,x不变,只变宽度 84 | case 'right': { 85 | let width = positionRef.current.width + distance; 86 | if (minWidth !== undefined) width = Math.max(width, minWidth); 87 | 88 | if (grid) width = snap(width, grid); 89 | 90 | const { x } = positionRef.current; 91 | onResize({ width, x }); 92 | break; 93 | } 94 | case 'move': { 95 | const { width } = positionRef.current; 96 | let rightDistance = distance; 97 | if (grid) rightDistance = snap(distance, grid); 98 | 99 | const x = positionRef.current.x + rightDistance; 100 | onResize({ width, x }); 101 | break; 102 | } 103 | } 104 | }); 105 | const handleMouseMove = usePersistFn((event: MouseEvent) => { 106 | if (disabled) return; 107 | if (!resizing) { 108 | setResizing(true); 109 | if (!clickStart) onBeforeResize && onBeforeResize(); 110 | } 111 | moveRef.current.clientX = event.clientX; 112 | updateSize(); 113 | }); 114 | 115 | const handleMouseUp = usePersistFn(() => { 116 | if (disabled) return; 117 | autoScroll.stop(); 118 | window.removeEventListener('mousemove', handleMouseMove); 119 | window.removeEventListener('mouseup', handleMouseUp); 120 | if (resizing) { 121 | setResizing(false); 122 | onResizeEnd && 123 | onResizeEnd({ 124 | x: positionRef.current.x, 125 | width: positionRef.current.width, 126 | }); 127 | } 128 | }); 129 | const handleMouseDown = usePersistFn( 130 | (event: React.MouseEvent) => { 131 | if (disabled) return; 132 | event.stopPropagation(); 133 | if (enableAutoScroll && scroller) autoScroll.start(); 134 | 135 | if (clickStart) { 136 | onBeforeResize && onBeforeResize(); 137 | setResizing(true); 138 | } 139 | positionRef.current.clientX = event.clientX; 140 | positionRef.current.x = defaultX; 141 | positionRef.current.width = defaultWidth; 142 | window.addEventListener('mousemove', handleMouseMove); 143 | window.addEventListener('mouseup', handleMouseUp); 144 | }, 145 | ); 146 | 147 | return ( 148 |
149 | {resizing && 150 | createPortal( 151 |
, 162 | document.body, 163 | )} 164 | {children} 165 |
166 | ); 167 | }; 168 | export default observer(DragResize); 169 | -------------------------------------------------------------------------------- /src/components/group-bar/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | 3 | .@{gantt-prefix}-group-bar { 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | display: flex; 8 | 9 | & .@{gantt-prefix}-bar { 10 | position: relative; 11 | top: -3px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/group-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import classNames from 'classnames'; 4 | import { Gantt } from '../../types'; 5 | import { getMaxRange } from '../../utils'; 6 | import Context from '../../context'; 7 | import './index.less'; 8 | interface GroupBarProps { 9 | data: Gantt.Bar; 10 | } 11 | const height = 8; 12 | const GroupBar: React.FC = ({ data }) => { 13 | const { prefixCls, renderGroupBar } = useContext(Context); 14 | const { translateY } = data; 15 | const { translateX, width } = getMaxRange(data); 16 | return ( 17 |
24 |
25 |
26 | {renderGroupBar ? ( 27 | renderGroupBar(data, { 28 | width, 29 | height, 30 | }) 31 | ) : ( 32 | 39 | 55 | 56 | )} 57 |
58 |
59 |
60 | ); 61 | }; 62 | export default observer(GroupBar); 63 | -------------------------------------------------------------------------------- /src/components/invalid-task-bar/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @invalid-task-bar-prefix-cls: ~'@{gantt-prefix}-invalid-task-bar'; 3 | 4 | .@{invalid-task-bar-prefix-cls} { 5 | position: absolute; 6 | left: 0; 7 | width: 100vw; 8 | 9 | &-block { 10 | position: absolute; 11 | width: 16px; 12 | min-width: 8px; 13 | height: 9px; 14 | left: 0; 15 | border: 1px solid; 16 | border-radius: 2px; 17 | cursor: pointer; 18 | z-index: 1 19 | } 20 | 21 | &-date { 22 | position: absolute; 23 | top: -6px; 24 | white-space: nowrap; 25 | color: #262626; 26 | font-size: 12px 27 | } 28 | } -------------------------------------------------------------------------------- /src/components/invalid-task-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useCallback, useState, useRef } from 'react' 2 | import { observer } from 'mobx-react-lite' 3 | import { usePersistFn } from 'ahooks' 4 | import Context from '../../context' 5 | import { Gantt } from '../../types' 6 | import DragResize from '../drag-resize' 7 | import './index.less' 8 | 9 | interface TaskBarProps { 10 | data: Gantt.Bar 11 | } 12 | const barH = 8 13 | let startX = 0 14 | const renderInvalidBarDefault = element => element 15 | const InvalidTaskBar: React.FC = ({ data }) => { 16 | const { store, prefixCls, renderInvalidBar = renderInvalidBarDefault } = useContext(Context) 17 | const triggerRef = useRef(null) 18 | const { translateY, translateX, width, dateTextFormat, record } = data 19 | const [visible, setVisible] = useState(false) 20 | 21 | const { disabled = false } = record || {} 22 | 23 | const { translateX: viewTranslateX, rowHeight } = store 24 | const top = translateY 25 | const prefixClsInvalidTaskBar = `${prefixCls}-invalid-task-bar` 26 | 27 | const handleMouseEnter = useCallback(() => { 28 | if (data.stepGesture === 'moving') return 29 | startX = triggerRef.current?.getBoundingClientRect()?.left || 0 30 | setVisible(true) 31 | }, [data.stepGesture]) 32 | const handleMouseLeave = useCallback(() => { 33 | if (data.stepGesture === 'moving') return 34 | 35 | setVisible(false) 36 | store.handleInvalidBarLeave() 37 | }, [data.stepGesture, store]) 38 | const handleMouseMove = useCallback( 39 | (event: React.MouseEvent) => { 40 | if (data.stepGesture === 'moving') return 41 | 42 | const pointerX = viewTranslateX + (event.clientX - startX) 43 | // eslint-disable-next-line no-shadow 44 | const { left, width } = store.startXRectBar(pointerX) 45 | store.handleInvalidBarHover(data, left, Math.ceil(width)) 46 | }, 47 | [data, store, viewTranslateX] 48 | ) 49 | 50 | const handleBeforeResize = () => { 51 | store.handleInvalidBarDragStart(data) 52 | } 53 | const handleResize = useCallback( 54 | ({ width: newWidth, x }) => { 55 | store.updateBarSize(data, { width: newWidth, x }) 56 | }, 57 | [data, store] 58 | ) 59 | const handleLeftResizeEnd = useCallback( 60 | (oldSize: { width: number; x: number }) => { 61 | store.handleInvalidBarDragEnd(data, oldSize) 62 | }, 63 | [data, store] 64 | ) 65 | const handleAutoScroll = useCallback( 66 | (delta: number) => { 67 | store.setTranslateX(store.translateX + delta) 68 | }, 69 | [store] 70 | ) 71 | const reachEdge = usePersistFn((position: 'left' | 'right') => position === 'left' && store.translateX <= 0) 72 | 73 | if (disabled) return null 74 | 75 | return ( 76 | 95 |
104 | {visible && 105 | renderInvalidBar( 106 | , 135 | data 136 | )} 137 | 138 | ) 139 | } 140 | export default observer(InvalidTaskBar) 141 | -------------------------------------------------------------------------------- /src/components/scroll-bar/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @scroll-bar-prefix-cls: ~'@{gantt-prefix}-scroll_bar'; 3 | 4 | .@{scroll-bar-prefix-cls} { 5 | position: absolute; 6 | bottom: 0; 7 | left: 16px; 8 | height: 12px; 9 | 10 | &-thumb { 11 | position: absolute; 12 | height: 100%; 13 | border-radius: 4px; 14 | background-color: #262626; 15 | opacity: 0.2; 16 | cursor: pointer; 17 | will-change: transform; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/scroll-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import { usePersistFn } from 'ahooks' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { memo, useCallback, useContext, useRef, useState } from 'react' 4 | import Context from '../../context' 5 | import './index.less' 6 | 7 | const ScrollBar: React.FC = () => { 8 | const { store, prefixCls } = useContext(Context) 9 | const { tableWidth, viewWidth } = store 10 | const width = store.scrollBarWidth 11 | const prefixClsScrollBar = `${prefixCls}-scroll_bar` 12 | const [resizing, setResizing] = useState(false) 13 | const positionRef = useRef({ 14 | scrollLeft: 0, 15 | left: 0, 16 | translateX: 0, 17 | }) 18 | const handleMouseMove = usePersistFn((event: MouseEvent) => { 19 | const distance = event.clientX - positionRef.current.left 20 | // TODO 调整倍率 21 | store.setTranslateX(distance * (store.viewWidth / store.scrollBarWidth) + positionRef.current.translateX) 22 | }) 23 | 24 | const handleMouseUp = useCallback(() => { 25 | window.removeEventListener('mousemove', handleMouseMove) 26 | window.removeEventListener('mouseup', handleMouseUp) 27 | setResizing(false) 28 | }, [handleMouseMove]) 29 | 30 | const handleMouseDown = useCallback( 31 | (event: React.MouseEvent) => { 32 | positionRef.current.left = event.clientX 33 | positionRef.current.translateX = store.translateX 34 | positionRef.current.scrollLeft = store.scrollLeft 35 | window.addEventListener('mousemove', handleMouseMove) 36 | window.addEventListener('mouseup', handleMouseUp) 37 | setResizing(true) 38 | }, 39 | [handleMouseMove, handleMouseUp, store.scrollLeft, store.translateX] 40 | ) 41 | 42 | return ( 43 |
49 | {resizing && ( 50 |
61 | )} 62 |
69 |
70 | ) 71 | } 72 | export default memo(observer(ScrollBar)) 73 | -------------------------------------------------------------------------------- /src/components/scroll-top/Top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Top 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/scroll-top/Top_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TOP-hover 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/scroll-top/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @scroll-top-prefix-cls: ~'@{gantt-prefix}-scroll_top'; 3 | 4 | .@{scroll-top-prefix-cls} { 5 | position: absolute; 6 | right: 24px; 7 | bottom: 8px; 8 | width: 40px; 9 | height: 40px; 10 | cursor: pointer; 11 | background-image: url('./Top.svg'); 12 | background-size: contain; 13 | 14 | &:hover { 15 | background-image: url('./Top_hover.svg'); 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/scroll-top/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import Context from '../../context'; 4 | import './index.less'; 5 | 6 | const ScrollTop: React.FC = () => { 7 | const { store, scrollTop: scrollTopConfig, prefixCls } = useContext(Context); 8 | const { scrollTop } = store; 9 | const handleClick = useCallback(() => { 10 | if (store.mainElementRef.current) { 11 | store.mainElementRef.current.scrollTop = 0; 12 | } 13 | }, [store.mainElementRef]); 14 | if (scrollTop <= 100 || !store.mainElementRef.current) { 15 | return null; 16 | } 17 | const prefixClsScrollTop = `${prefixCls}-scroll_top`; 18 | return ( 19 |
24 | ); 25 | }; 26 | export default observer(ScrollTop); 27 | -------------------------------------------------------------------------------- /src/components/selection-indicator/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @selection-indicator-prefix-cls: ~'@{gantt-prefix}-selection-indicator'; 3 | 4 | .@{selection-indicator-prefix-cls} { 5 | position: absolute; 6 | width: 100%; 7 | background: rgba(0, 0, 0, 0.04); 8 | pointer-events: none; 9 | z-index: 10; 10 | } -------------------------------------------------------------------------------- /src/components/selection-indicator/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import Context from '../../context'; 4 | import './index.less'; 5 | 6 | /** 7 | * 鼠标hover效果模拟 8 | */ 9 | const SelectionIndicator: React.FC = () => { 10 | const { store, prefixCls } = useContext(Context); 11 | const { showSelectionIndicator, selectionIndicatorTop, rowHeight } = store; 12 | const prefixClsSelectionIndicator = `${prefixCls}-selection-indicator`; 13 | return showSelectionIndicator ? ( 14 |
21 | ) : null; 22 | }; 23 | export default observer(SelectionIndicator); 24 | -------------------------------------------------------------------------------- /src/components/table-body/RowToggler.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @row-toggler-prefix-cls: ~'@{gantt-prefix}-row-toggler'; 3 | 4 | .@{row-toggler-prefix-cls} { 5 | width: 24px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | color: #d9d9d9; 10 | cursor: pointer; 11 | position: relative; 12 | z-index: 5; 13 | 14 | &:hover { 15 | color: #8c8c8c; 16 | } 17 | 18 | & > i { 19 | width: 20px; 20 | height: 20px; 21 | background: white; 22 | } 23 | 24 | & > i > svg { 25 | transition: transform 218ms; 26 | fill: currentColor; 27 | } 28 | 29 | &-collapsed > i > svg { 30 | transform: rotate(-90deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/table-body/RowToggler.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import './RowToggler.less' 4 | 5 | interface RowTogglerProps { 6 | onClick: React.DOMAttributes['onClick'] 7 | collapsed: boolean 8 | level: number 9 | prefixCls?: string 10 | } 11 | const RowToggler: React.FC = ({ onClick, collapsed, level, prefixCls = '' }) => { 12 | const prefixClsRowToggler = `${prefixCls}-row-toggler` 13 | return ( 14 |
15 |
20 | 21 | {level <= 0 ? ( 22 | 23 | 24 | 25 | ) : ( 26 | 27 | 28 | 29 | )} 30 | 31 |
32 |
33 | ) 34 | } 35 | export default RowToggler 36 | -------------------------------------------------------------------------------- /src/components/table-body/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @table-body-prefix-cls: ~'@{gantt-prefix}-table-body'; 3 | 4 | .@{table-body-prefix-cls} { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | overflow: hidden; 9 | 10 | &-row, 11 | &-border_row { 12 | display: flex; 13 | align-items: center; 14 | position: absolute; 15 | width: 100%; 16 | } 17 | 18 | &-border_row { 19 | height: 100%; 20 | pointer-events: none; 21 | } 22 | 23 | &-cell { 24 | position: relative; 25 | display: flex; 26 | align-items: center; 27 | border-right: 1px solid #f0f0f0; 28 | height: 100%; 29 | color: #2e405e; 30 | user-select: none; 31 | padding: 0 8px; 32 | font-size: 14px; 33 | } 34 | 35 | &-ellipsis { 36 | flex: 1; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | white-space: nowrap; 40 | } 41 | 42 | &-row-indentation { 43 | height: 100%; 44 | position: absolute; 45 | left: 0; 46 | pointer-events: none; 47 | 48 | &:before { 49 | content: ''; 50 | position: absolute; 51 | height: 100%; 52 | left: 0; 53 | width: 1px; 54 | bottom: 0; 55 | background-color: #d9e6f2; 56 | } 57 | } 58 | 59 | &-row-indentation-both { 60 | &:after { 61 | content: ''; 62 | position: absolute; 63 | width: 100%; 64 | bottom: 0; 65 | left: 0; 66 | height: 1px; 67 | background-color: #d9e6f2; 68 | } 69 | } 70 | 71 | &-row-indentation-hidden { 72 | visibility: hidden; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/table-body/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { observer } from 'mobx-react-lite'; 3 | import React, { useCallback, useContext } from 'react'; 4 | import { TOP_PADDING } from '../../constants'; 5 | import Context from '../../context'; 6 | import RowToggler from './RowToggler'; 7 | import './index.less'; 8 | 9 | const TableRows = () => { 10 | const { store, onRow, tableIndent, expandIcon, prefixCls, onExpand } = 11 | useContext(Context); 12 | const { columns, rowHeight } = store; 13 | const columnsWidth = store.getColumnsWidth; 14 | const barList = store.getBarList; 15 | 16 | const { count, start } = store.getVisibleRows; 17 | const prefixClsTableBody = `${prefixCls}-table-body`; 18 | if (barList.length === 0) { 19 | return ( 20 |
27 | 暂无数据 28 |
29 | ); 30 | } 31 | return ( 32 | <> 33 | {barList.slice(start, start + count).map((bar, rowIndex) => { 34 | // 父元素如果是其最后一个祖先的子,要隐藏上一层的线 35 | const parent = bar._parent; 36 | const parentItem = parent?._parent; 37 | let isLastChild = false; 38 | if ( 39 | parentItem?.children && 40 | parentItem.children[parentItem.children.length - 1] === bar._parent 41 | ) 42 | isLastChild = true; 43 | 44 | return ( 45 |
{ 54 | onRow?.onClick(bar.record); 55 | }} 56 | > 57 | {columns.map((column, index) => ( 58 |
71 | {index === 0 && 72 | new Array(bar._depth).fill(0).map((_, i) => ( 73 |
90 | ))} 91 | {index === 0 && bar._childrenCount > 0 && ( 92 |
102 | {expandIcon ? ( 103 | expandIcon({ 104 | level: bar._depth, 105 | collapsed: bar._collapsed, 106 | onClick: (event) => { 107 | event.stopPropagation(); 108 | if (onExpand) 109 | onExpand(bar.task.record, !bar._collapsed); 110 | store.setRowCollapse(bar.task, !bar._collapsed); 111 | }, 112 | }) 113 | ) : ( 114 | { 119 | event.stopPropagation(); 120 | if (onExpand) 121 | onExpand(bar.task.record, !bar._collapsed); 122 | store.setRowCollapse(bar.task, !bar._collapsed); 123 | }} 124 | /> 125 | )} 126 |
127 | )} 128 | 129 | {column.render 130 | ? column.render(bar.record) 131 | : bar.record[column.name]} 132 | 133 |
134 | ))} 135 |
136 | ); 137 | })} 138 | 139 | ); 140 | }; 141 | const ObserverTableRows = observer(TableRows); 142 | const TableBorders = () => { 143 | const { store, prefixCls } = useContext(Context); 144 | const { columns } = store; 145 | const columnsWidth = store.getColumnsWidth; 146 | const barList = store.getBarList; 147 | if (barList.length === 0) return null; 148 | 149 | const prefixClsTableBody = `${prefixCls}-table-body`; 150 | return ( 151 |
152 | {columns.map((column, index) => ( 153 |
164 | ))} 165 |
166 | ); 167 | }; 168 | const ObserverTableBorders = observer(TableBorders); 169 | 170 | const TableBody: React.FC = () => { 171 | const { store, prefixCls } = useContext(Context); 172 | const handleMouseMove = useCallback( 173 | (event: React.MouseEvent) => { 174 | event.persist(); 175 | store.handleMouseMove(event); 176 | }, 177 | [store], 178 | ); 179 | const handleMouseLeave = useCallback(() => { 180 | store.handleMouseLeave(); 181 | }, [store]); 182 | const prefixClsTableBody = `${prefixCls}-table-body`; 183 | return ( 184 |
193 | 194 | 195 |
196 | ); 197 | }; 198 | export default observer(TableBody); 199 | -------------------------------------------------------------------------------- /src/components/table-header/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @table-header-prefix-cls: ~'@{gantt-prefix}-table-header'; 3 | 4 | .@{table-header-prefix-cls} { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | overflow: hidden; 9 | 10 | &-head { 11 | position: relative; 12 | } 13 | 14 | &-row { 15 | position: absolute; 16 | left: 0; 17 | display: flex; 18 | transition: height 0.3s; 19 | width: 100%; 20 | } 21 | 22 | &-cell { 23 | position: relative; 24 | display: flex; 25 | border-right: 1px solid #f0f0f0; 26 | } 27 | 28 | &-head-cell { 29 | display: flex; 30 | flex: 1; 31 | align-items: center; 32 | overflow: hidden; 33 | padding: 0 12px; 34 | user-select: none; 35 | font-size: 14px; 36 | color: #2e405e; 37 | } 38 | 39 | &-ellipsis { 40 | flex: 1; 41 | overflow: hidden; 42 | text-overflow: ellipsis; 43 | white-space: nowrap; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/table-header/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import React, { useContext } from 'react'; 3 | import Context from '../../context'; 4 | import './index.less'; 5 | 6 | const TableHeader: React.FC = () => { 7 | const { store, prefixCls } = useContext(Context); 8 | const { columns, tableWidth } = store; 9 | const width = tableWidth; 10 | const columnsWidth = store.getColumnsWidth; 11 | const prefixClsTableHeader = `${prefixCls}-table-header`; 12 | return ( 13 |
14 |
18 |
19 | {columns.map((column, index) => ( 20 |
31 |
32 | 33 | {column.label} 34 | 35 |
36 |
37 | ))} 38 |
39 |
40 |
41 | ); 42 | }; 43 | export default observer(TableHeader); 44 | -------------------------------------------------------------------------------- /src/components/task-bar-thumb/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @task-bar-thumb-prefix-cls: ~'@{gantt-prefix}-task-bar-thumb'; 3 | 4 | .@{task-bar-thumb-prefix-cls} { 5 | position: absolute; 6 | cursor: pointer; 7 | white-space: nowrap; 8 | z-index: 2; 9 | overflow: hidden; 10 | max-width: 200px; 11 | color: #595959; 12 | text-overflow: ellipsis; 13 | word-break: keep-all; 14 | line-height: 16px; 15 | user-select: none; 16 | font-size: 12px; 17 | padding-right: 16px; 18 | 19 | display:flex; 20 | align-items: center; 21 | 22 | &-left { 23 | transform: translate(0); 24 | } 25 | 26 | &-right { 27 | transform: translate(-100%); 28 | } 29 | 30 | &-circle-left{ 31 | height: 10px; 32 | width: 10px; 33 | border-radius:50%; 34 | margin-right: 10px; 35 | } 36 | 37 | &-circle-right{ 38 | height: 10px; 39 | width: 10px; 40 | border-radius:50%; 41 | margin-left: 10px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/task-bar-thumb/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useMemo } from 'react' 2 | import { observer } from 'mobx-react-lite' 3 | import classNames from 'classnames' 4 | import Context from '../../context' 5 | import { Gantt } from '../../types' 6 | import './index.less' 7 | 8 | interface TaskBarProps { 9 | data: Gantt.Bar 10 | } 11 | 12 | const TaskBarThumb: React.FC = ({ data }) => { 13 | const { store, renderBarThumb, prefixCls, getBarColor } = useContext(Context) 14 | const prefixClsTaskBarThumb = `${prefixCls}-task-bar-thumb` 15 | const { translateX: viewTranslateX, viewWidth } = store 16 | const { translateX, translateY, label, record } = data 17 | 18 | const type = useMemo(() => { 19 | const rightSide = viewTranslateX + viewWidth 20 | return translateX - rightSide > 0 ? 'right' : 'left' 21 | }, [translateX, viewTranslateX, viewWidth]) 22 | const left = useMemo( 23 | () => (type === 'right' ? viewTranslateX + viewWidth - 5 : viewTranslateX + 2), 24 | [type, viewTranslateX, viewWidth] 25 | ) 26 | const handleClick = useCallback( 27 | (e: React.MouseEvent) => { 28 | e.stopPropagation() 29 | store.scrollToBar(data, type) 30 | }, 31 | [data, store, type] 32 | ) 33 | 34 | const getBackgroundColor = useMemo( 35 | () => record.backgroundColor || (getBarColor && getBarColor(record).backgroundColor), 36 | [record] 37 | ) 38 | 39 | return ( 40 |
52 | {type === 'left' && ( 53 |
54 | )} 55 | {renderBarThumb ? renderBarThumb(data.record, type) : label} 56 | {type === 'right' && ( 57 |
58 | )} 59 |
60 | ) 61 | } 62 | export default observer(TaskBarThumb) 63 | -------------------------------------------------------------------------------- /src/components/task-bar/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @task-bar-prefix-cls: ~'@{gantt-prefix}-task-bar'; 3 | 4 | .@{task-bar-prefix-cls} { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | display: flex; 9 | 10 | &-loading { 11 | position: absolute; 12 | top: 0; 13 | bottom: 0; 14 | left: 0; 15 | right: 0; 16 | cursor: not-allowed; 17 | z-index: 9; 18 | } 19 | 20 | &-bar { 21 | position: relative; 22 | height: 8px; 23 | line-height: 8px; 24 | border-radius: 4px; 25 | top: -1px; 26 | cursor: pointer; 27 | } 28 | 29 | &-invalid-date-range { 30 | display: none; 31 | } 32 | 33 | &-resize-bg { 34 | position: absolute; 35 | left: 0; 36 | top: -5px; 37 | border-radius: 4px; 38 | box-shadow: 0 2px 4px 0 #f7f7f7; 39 | border: 1px solid #f0f0f0; 40 | background-color: #fff; 41 | 42 | &-compact { 43 | height: 17px; 44 | } 45 | } 46 | 47 | &-resize-handle { 48 | position: absolute; 49 | left: 0; 50 | top: -4px; 51 | width: 14px; 52 | height: 16px; 53 | z-index: 3; 54 | background: white; 55 | 56 | &:after, 57 | &:before { 58 | position: absolute; 59 | top: 4px; 60 | bottom: 16px; 61 | width: 2px; 62 | height: 8px; 63 | border-radius: 2px; 64 | background-color: #d9d9d9; 65 | content: ''; 66 | } 67 | 68 | &-disabled { 69 | cursor: not-allowed !important; 70 | } 71 | 72 | &-left { 73 | cursor: col-resize; 74 | 75 | &:before { 76 | left: 4px; 77 | } 78 | 79 | &:after { 80 | right: 4px; 81 | } 82 | } 83 | 84 | &-right { 85 | cursor: col-resize; 86 | 87 | &:before { 88 | left: 4px; 89 | } 90 | 91 | &:after { 92 | right: 4px; 93 | } 94 | } 95 | } 96 | 97 | &-date-text { 98 | color: #262626; 99 | } 100 | 101 | &-date-text, 102 | &-label { 103 | position: absolute; 104 | white-space: nowrap; 105 | font-size: 12px; 106 | top: -4px; 107 | } 108 | 109 | &-label { 110 | overflow: hidden; 111 | max-width: 200px; 112 | color: #595959; 113 | text-overflow: ellipsis; 114 | word-break: keep-all; 115 | line-height: 16px; 116 | -webkit-user-select: none; 117 | -moz-user-select: none; 118 | -ms-user-select: none; 119 | user-select: none; 120 | height: 16px; 121 | cursor: pointer; 122 | top: -14px; 123 | } 124 | 125 | // &-dependency-handle { 126 | // position: absolute; 127 | // top: -5px; 128 | // left: 0; 129 | // cursor: pointer; 130 | // height: 18px; 131 | // z-index: 1; 132 | 133 | // &:hover { 134 | // .inner { 135 | // fill: #1b9aee 136 | // } 137 | // } 138 | 139 | // &.right { 140 | // text-align: right 141 | // } 142 | 143 | // &.loose { 144 | // top: 0 145 | // } 146 | // } 147 | 148 | // .done .dependency-handle .outer { 149 | // stroke: #d9d9d9 150 | // } 151 | 152 | // .done .dependency-handle .inner { 153 | // fill: #d9d9d9 154 | // } 155 | 156 | // .done .dependency-handle:hover .inner { 157 | // fill: #8c8c8c 158 | // } 159 | } 160 | 161 | // .overdue { 162 | // .dependency-handle .outer { 163 | // stroke: #fcc 164 | // } 165 | 166 | // .dependency-handle .inner { 167 | // fill: #fd998f 168 | // } 169 | 170 | // .dependency-handle:hover .inner { 171 | // fill: #f87872 172 | // } 173 | // } 174 | -------------------------------------------------------------------------------- /src/components/task-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import { usePersistFn } from 'ahooks' 2 | import classNames from 'classnames' 3 | import dayjs from 'dayjs' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { useCallback, useContext, useMemo } from 'react' 6 | import { TOP_PADDING } from '../../constants' 7 | import Context from '../../context' 8 | import { ONE_DAY_MS } from '../../store' 9 | import { Gantt } from '../../types' 10 | import DragResize from '../drag-resize' 11 | import './index.less' 12 | 13 | interface TaskBarProps { 14 | data: Gantt.Bar 15 | } 16 | 17 | const TaskBar: React.FC = ({ data }) => { 18 | const { 19 | store, 20 | getBarColor, 21 | renderBar, 22 | onBarClick, 23 | prefixCls, 24 | barHeight, 25 | alwaysShowTaskBar, 26 | renderLeftText, 27 | renderRightText, 28 | } = useContext(Context) 29 | const { 30 | width, 31 | translateX, 32 | translateY, 33 | invalidDateRange, 34 | stepGesture, 35 | dateTextFormat, 36 | record, 37 | loading, 38 | getDateWidth, 39 | } = data 40 | 41 | const { disabled = false } = record || {} 42 | 43 | const prefixClsTaskBar = `${prefixCls}-task-bar` 44 | 45 | const { selectionIndicatorTop, showSelectionIndicator, rowHeight, locale } = store 46 | 47 | const showDragBar = useMemo(() => { 48 | if (!showSelectionIndicator) return false 49 | // 差值 50 | const baseTop = TOP_PADDING + rowHeight / 2 - barHeight / 2 51 | return selectionIndicatorTop === translateY - baseTop 52 | }, [showSelectionIndicator, selectionIndicatorTop, translateY, rowHeight, barHeight]) 53 | 54 | const themeColor = useMemo(() => { 55 | if (translateX + width >= dayjs().valueOf() / store.pxUnitAmp) return ['#95DDFF', '#64C7FE'] 56 | return ['#FD998F', '#F96B5D'] 57 | }, [store.pxUnitAmp, translateX, width]) 58 | 59 | const handleBeforeResize = (type: Gantt.MoveType) => () => { 60 | if (disabled) return 61 | store.handleDragStart(data, type) 62 | } 63 | const handleResize = useCallback( 64 | ({ width: newWidth, x }) => { 65 | if (disabled) return 66 | store.updateBarSize(data, { width: newWidth, x }) 67 | }, 68 | [data, store, disabled] 69 | ) 70 | const handleLeftResizeEnd = useCallback( 71 | (oldSize: { width: number; x: number }) => { 72 | store.handleDragEnd() 73 | store.updateTaskDate(data, oldSize, 'left') 74 | }, 75 | [data, store] 76 | ) 77 | const handleRightResizeEnd = useCallback( 78 | (oldSize: { width: number; x: number }) => { 79 | store.handleDragEnd() 80 | store.updateTaskDate(data, oldSize, 'right') 81 | }, 82 | [data, store] 83 | ) 84 | 85 | const handleMoveEnd = useCallback( 86 | (oldSize: { width: number; x: number }) => { 87 | store.handleDragEnd() 88 | store.updateTaskDate(data, oldSize, 'move') 89 | }, 90 | [data, store] 91 | ) 92 | const handleAutoScroll = useCallback( 93 | (delta: number) => { 94 | store.setTranslateX(store.translateX + delta) 95 | }, 96 | [store] 97 | ) 98 | const allowDrag = showDragBar && !loading 99 | 100 | const handleClick = useCallback( 101 | (e: React.MouseEvent) => { 102 | e.stopPropagation() 103 | if (onBarClick) onBarClick(data.record) 104 | }, 105 | [data.record, onBarClick] 106 | ) 107 | const reachEdge = usePersistFn((position: 'left' | 'right') => position === 'left' && store.translateX <= 0) 108 | // 根据不同的视图确定拖动时的单位,在任何视图下都以一天为单位 109 | const grid = useMemo(() => ONE_DAY_MS / store.pxUnitAmp, [store.pxUnitAmp]) 110 | 111 | const moveCalc = -(width / store.pxUnitAmp); 112 | 113 | const days = useMemo(() => { 114 | const daysWidth = Number(getDateWidth(translateX + width + moveCalc, translateX)); 115 | 116 | return `${daysWidth} ${daysWidth > 1 ? locale.days : locale.day}` 117 | }, [translateX, width, moveCalc, translateX]) 118 | 119 | return ( 120 |
131 | {loading &&
} 132 |
133 | {allowDrag && ( 134 | <> 135 | {/* {stepGesture !== 'moving' && ( 136 |
137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 | )} 145 | {stepGesture !== 'moving' && ( 146 |
147 | 148 | 149 | 150 | 151 | 152 | 153 |
154 | )} */} 155 | 175 | 195 |
199 | 200 | )} 201 | 217 | {renderBar ? ( 218 | renderBar(data, { 219 | width: width + 1, 220 | height: barHeight + 1, 221 | }) 222 | ) : ( 223 | 230 | 255 | 256 | )} 257 | 258 |
259 | {(allowDrag || disabled || alwaysShowTaskBar) && ( 260 |
261 | {days} 262 |
263 | )} 264 | {(stepGesture === 'moving' || allowDrag || alwaysShowTaskBar) && ( 265 | <> 266 |
267 | {renderRightText ? renderRightText(data) : dateTextFormat(translateX + width + moveCalc)} 268 |
269 |
270 | {renderLeftText ? renderLeftText(data) : dateTextFormat(translateX)} 271 |
272 | 273 | )} 274 |
275 | ) 276 | } 277 | export default observer(TaskBar) 278 | -------------------------------------------------------------------------------- /src/components/time-axis-scale-select/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | 3 | .@{gantt-prefix}-time-axis-scale-select { 4 | .next-menu { 5 | position: relative; 6 | min-width: 150px; 7 | padding: 4px 0; 8 | margin: 0; 9 | list-style: none; 10 | border-radius: 4px; 11 | background: #fff; 12 | line-height: 36px; 13 | font-size: 14px 14 | } 15 | 16 | .next-menu, 17 | .next-menu *, 18 | .next-menu :after, 19 | .next-menu :before { 20 | box-sizing: border-box 21 | } 22 | 23 | .next-menu, 24 | .next-select-trigger, 25 | .next-select .next-select-inner { 26 | min-width: unset 27 | } 28 | 29 | .next-menu-item-text { 30 | line-height: 36px 31 | } 32 | } 33 | 34 | .time-axis-scale-select__3fTI .next-menu-item-text { 35 | line-height: 36px 36 | } 37 | 38 | .@{gantt-prefix}-shadow { 39 | position: absolute; 40 | top: 4px; 41 | right: 0; 42 | width: 90px; 43 | height: 48px; 44 | z-index: 0; 45 | transition: box-shadow 0.5s 46 | } 47 | 48 | .@{gantt-prefix}-shadow.@{gantt-prefix}-scrolling { 49 | box-shadow: -3px 0 7px 0 #e5e5e5 50 | } 51 | 52 | .@{gantt-prefix}-trigger { 53 | position: absolute; 54 | top: 0; 55 | right: 0; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | height: 56px; 60 | border-top-right-radius: 4px; 61 | background-color: #fff; 62 | border-left: 1px solid #f0f0f0; 63 | color: #bfbfbf; 64 | padding: 0 8px 0 12px; 65 | cursor: pointer; 66 | width: 90px; 67 | z-index: 1; 68 | transition: color 0.2s 69 | } 70 | 71 | .@{gantt-prefix}-trigger:hover { 72 | color: #8c8c8c 73 | } 74 | 75 | .@{gantt-prefix}-trigger:hover .@{gantt-prefix}-text { 76 | color: #262626 77 | } 78 | 79 | .@{gantt-prefix}-trigger .@{gantt-prefix}-text { 80 | white-space: nowrap; 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | margin-right: 4px; 84 | font-size: 14px; 85 | color:#202d40; 86 | } 87 | 88 | .dropdown-icon { 89 | width: 20px; 90 | height: 20px; 91 | line-height: 20px; 92 | 93 | svg { 94 | fill: currentColor; 95 | } 96 | } 97 | 98 | .next-overlay-wrapper { 99 | position: absolute; 100 | top: 0; 101 | left: 0; 102 | width: 100% 103 | } 104 | 105 | .next-overlay-wrapper .next-overlay-inner { 106 | z-index: 1001; 107 | border-radius: 4px; 108 | box-shadow: 0 12px 32px 0 rgba(38, 38, 38, 0.16); 109 | -webkit-transform: translateZ(0); 110 | transform: translateZ(0) 111 | } 112 | 113 | .next-overlay-wrapper .next-overlay-backdrop { 114 | position: fixed; 115 | z-index: 1001; 116 | top: 0; 117 | left: 0; 118 | width: 100%; 119 | height: 100%; 120 | background: #000; 121 | transition: opacity .3s; 122 | opacity: 0 123 | } 124 | 125 | .next-overlay-wrapper.opened .next-overlay-backdrop { 126 | opacity: .3 127 | } 128 | 129 | .next-menu-item { 130 | position: relative; 131 | padding: 0 12px 0 40px; 132 | transition: background .2s ease; 133 | color: #262626; 134 | cursor: pointer; 135 | display: flex; 136 | align-items: center; 137 | 138 | .@{gantt-prefix}-selected_icon { 139 | position: absolute; 140 | left: 12px; 141 | width: 20px; 142 | height: 20px; 143 | line-height: 20px; 144 | 145 | svg { 146 | fill: rgb(27, 154, 238); 147 | } 148 | } 149 | 150 | &:hover { 151 | font-weight: 400; 152 | background-color: #f7f7f7; 153 | } 154 | } 155 | 156 | .next-menu-item.next-selected { 157 | color: #262626; 158 | background-color: #fff 159 | } 160 | 161 | .next-menu-item.next-selected .next-menu-icon-arrow { 162 | color: #bfbfbf 163 | } 164 | -------------------------------------------------------------------------------- /src/components/time-axis-scale-select/index.tsx: -------------------------------------------------------------------------------- 1 | import { useClickAway } from 'ahooks' 2 | import classNames from 'classnames' 3 | import { observer } from 'mobx-react-lite' 4 | import React, { useCallback, useContext, useRef, useState } from 'react' 5 | import Context from '../../context' 6 | import { Gantt } from '../../types' 7 | import './index.less' 8 | 9 | const TimeAxisScaleSelect: React.FC = () => { 10 | const { store, prefixCls } = useContext(Context) 11 | const { sightConfig, scrolling, viewTypeList } = store 12 | const [visible, setVisible] = useState(false) 13 | const ref = useRef(null) 14 | useClickAway(() => { 15 | setVisible(false) 16 | }, ref) 17 | const handleClick = useCallback(() => { 18 | setVisible(true) 19 | }, []) 20 | const handleSelect = useCallback( 21 | (item: Gantt.SightConfig) => { 22 | store.switchSight(item.type) 23 | setVisible(false) 24 | }, 25 | [store] 26 | ) 27 | const selected = sightConfig.type 28 | const isSelected = useCallback((key: string) => key === selected, [selected]) 29 | return ( 30 |
31 |
32 |
{sightConfig.label}
33 | 34 | 35 | 36 | 37 | 38 |
39 |
44 | {visible && ( 45 |
46 |
51 |
52 |
    53 | {viewTypeList.map(item => ( 54 |
  • { 58 | handleSelect(item) 59 | }} 60 | className={classNames('next-menu-item', { 61 | 'next-selected': isSelected(item.type), 62 | })} 63 | > 64 | {isSelected(item.type) && ( 65 | 66 | 67 | 68 | 69 | 70 | )} 71 | 72 | {item.label} 73 | 74 |
  • 75 | ))} 76 |
77 |
78 |
79 |
80 | )} 81 |
82 | ) 83 | } 84 | export default observer(TimeAxisScaleSelect) 85 | -------------------------------------------------------------------------------- /src/components/time-axis/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @time-axis-prefix-cls: ~'@{gantt-prefix}-time-axis'; 3 | 4 | .@{time-axis-prefix-cls} { 5 | height: 56px; 6 | position: absolute; 7 | top: 0; 8 | user-select: none; 9 | overflow: hidden; 10 | cursor: ew-resize; 11 | 12 | &-render-chunk { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | height: 56px; 17 | pointer-events: none; 18 | user-select: none; 19 | will-change: transform; 20 | } 21 | &-today { 22 | background-color: #2c7ef8; 23 | border-radius: 50%; 24 | color: #fff; 25 | } 26 | 27 | &-major { 28 | position: absolute; 29 | overflow: hidden; 30 | box-sizing: content-box; 31 | height: 28px; 32 | border-right: 1px solid #f0f0f0; 33 | font-weight: 400; 34 | text-align: left; 35 | font-size: 13px; 36 | line-height: 28px; 37 | 38 | &-label { 39 | overflow: hidden; 40 | padding-left: 8px; 41 | white-space: nowrap; 42 | } 43 | } 44 | 45 | &-minor { 46 | position: absolute; 47 | top: 27px; 48 | box-sizing: content-box; 49 | height: 28px; 50 | border-top: 1px solid #f0f0f0; 51 | border-right: 1px solid #f0f0f0; 52 | text-align: center; 53 | font-size: 12px; 54 | line-height: 28px; 55 | color: #202d40; 56 | &.weekends { 57 | background-color: hsla(0, 0%, 96.9%, 0.5); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/time-axis/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react' 2 | import { observer } from 'mobx-react-lite' 3 | import classNames from 'classnames' 4 | import DragResize from '../drag-resize' 5 | import Context from '../../context' 6 | 7 | import './index.less' 8 | 9 | const TimeAxis: React.FC = () => { 10 | const { store, prefixCls } = useContext(Context) 11 | const prefixClsTimeAxis = `${prefixCls}-time-axis` 12 | const { sightConfig, isToday } = store 13 | const majorList = store.getMajorList() 14 | const minorList = store.getMinorList() 15 | const handleResize = useCallback( 16 | ({ x }) => { 17 | store.handlePanMove(-x) 18 | }, 19 | [store] 20 | ) 21 | const handleLeftResizeEnd = useCallback(() => { 22 | store.handlePanEnd() 23 | }, [store]) 24 | 25 | const getIsToday = useCallback( 26 | item => { 27 | const { key } = item 28 | const { type } = sightConfig 29 | return type === 'day' && isToday(key) 30 | }, 31 | [sightConfig, isToday] 32 | ) 33 | 34 | return ( 35 | 44 |
51 |
57 | {majorList.map(item => ( 58 |
59 |
{item.label}
60 |
61 | ))} 62 | {minorList.map(item => ( 63 |
68 |
73 | {item.label} 74 |
75 |
76 | ))} 77 |
78 |
79 |
80 | ) 81 | } 82 | export default observer(TimeAxis) 83 | -------------------------------------------------------------------------------- /src/components/time-indicator/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | @time-indicator-prefix-cls: ~'@{gantt-prefix}-time-indicator'; 3 | 4 | .@{time-indicator-prefix-cls} { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | background-color: #096dd9; 9 | box-shadow: 0 2px 4px rgba(1, 113, 194, 0.1); 10 | transform: translate(12px, 14px); 11 | transition: opacity 0.3s; 12 | // button style 13 | padding: 0 7px; 14 | color: #fff; 15 | border-radius: 4px; 16 | outline: 0; 17 | display: inline-flex; 18 | align-items: center; 19 | justify-content: center; 20 | box-sizing: border-box; 21 | user-select: none; 22 | vertical-align: middle; 23 | cursor: pointer; 24 | border:none; 25 | font-size:12px; 26 | &-scrolling { 27 | opacity: 0 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/time-indicator/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useCallback, useMemo } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import classNames from 'classnames'; 4 | import Context from '../../context'; 5 | import './index.less'; 6 | 7 | const TimeIndicator: React.FC = () => { 8 | const { store, prefixCls } = useContext(Context); 9 | const { 10 | scrolling, 11 | translateX, 12 | tableWidth, 13 | viewWidth, 14 | todayTranslateX, 15 | locale, 16 | } = store; 17 | const prefixClsTimeIndicator = `${prefixCls}-time-indicator`; 18 | const type = todayTranslateX < translateX ? 'left' : 'right'; 19 | const left = type === 'left' ? tableWidth : 'unset'; 20 | const right = type === 'right' ? 111 : 'unset'; 21 | const display = useMemo(() => { 22 | const isOverLeft = todayTranslateX < translateX; 23 | const isOverRight = todayTranslateX > translateX + viewWidth; 24 | return isOverLeft || isOverRight ? 'block' : 'none'; 25 | }, [todayTranslateX, translateX, viewWidth]); 26 | const handleClick = useCallback(() => { 27 | store.scrollToToday(); 28 | }, [store]); 29 | return ( 30 | 41 | ); 42 | }; 43 | export default observer(TimeIndicator); 44 | -------------------------------------------------------------------------------- /src/components/today/index.less: -------------------------------------------------------------------------------- 1 | @import '../../style/themes/index'; 2 | 3 | .@{gantt-prefix}-today { 4 | position: absolute; 5 | top: 0; 6 | background: #096dd9; 7 | width: 1px; 8 | height: 1px; 9 | text-align: center; 10 | line-height: 1px; 11 | border-radius: 50%; 12 | font-size: 12px; 13 | color: #fff; 14 | pointer-events: none; 15 | } 16 | 17 | .@{gantt-prefix}-today_line { 18 | width: 1px; 19 | background: #096dd9; 20 | margin-left: 15px; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/today/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { observer } from 'mobx-react-lite' 3 | import Context from '../../context' 4 | import './index.less' 5 | 6 | const Today: React.FC = () => { 7 | const { store, prefixCls } = useContext(Context) 8 | return ( 9 |
15 |
21 |
22 | ) 23 | } 24 | export default observer(Today) 25 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ROW_HEIGHT = 40 2 | export const HEADER_HEIGHT = 56 3 | export const BAR_HEIGHT = 8 4 | export const TOP_PADDING = 0 5 | export const TABLE_INDENT = 30 6 | // 图表最小比例 7 | export const MIN_VIEW_RATE = 0.5 8 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import GanttStore from './store' 3 | import { DefaultRecordType, Gantt } from './types' 4 | 5 | export interface GanttContext { 6 | prefixCls: string 7 | store: GanttStore 8 | getBarColor?: (record: Gantt.Record) => { 9 | backgroundColor: string 10 | borderColor: string 11 | } 12 | showBackToday: boolean 13 | showUnitSwitch: boolean 14 | onRow?: { 15 | onClick: (record: Gantt.Record) => void 16 | } 17 | tableIndent: number 18 | barHeight: number 19 | expandIcon?: ({ 20 | level, 21 | collapsed, 22 | onClick, 23 | }: { 24 | level: number 25 | collapsed: boolean 26 | onClick: (event: React.MouseEvent) => void 27 | }) => React.ReactNode 28 | renderBar?: (barInfo: Gantt.Bar, { width, height }: { width: number; height: number }) => React.ReactNode 29 | renderInvalidBar?: (element: React.ReactNode, barInfo: Gantt.Bar) => React.ReactNode 30 | renderGroupBar?: ( 31 | barInfo: Gantt.Bar, 32 | { width, height }: { width: number; height: number } 33 | ) => React.ReactNode 34 | renderBarThumb?: (item: Gantt.Record, type: 'left' | 'right') => React.ReactNode 35 | onBarClick?: (record: Gantt.Record) => void 36 | tableCollapseAble: boolean 37 | scrollTop: boolean | React.CSSProperties 38 | alwaysShowTaskBar?: boolean 39 | renderLeftText?: (barInfo: Gantt.Bar) => React.ReactNode 40 | renderRightText?: (barInfo: Gantt.Bar) => React.ReactNode 41 | onExpand?: (record: Gantt.Record, collapsed: boolean) => void 42 | 43 | hideTable?: boolean 44 | } 45 | const context = createContext({} as GanttContext) 46 | export default context 47 | -------------------------------------------------------------------------------- /src/hooks/useDragResize.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState } from 'react'; 2 | import { usePersistFn } from 'ahooks'; 3 | 4 | export default function useDragResize( 5 | handleResize: ({ width }: { width: number }) => void, 6 | { 7 | initSize, 8 | minWidth: minWidthConfig, 9 | maxWidth: maxWidthConfig, 10 | }: { 11 | initSize: { 12 | width: number; 13 | }; 14 | minWidth?: number; 15 | maxWidth?: number; 16 | } 17 | ): [(event: React.MouseEvent) => void, boolean] { 18 | const [resizing, setResizing] = useState(false); 19 | const positionRef = useRef({ 20 | left: 0, 21 | }); 22 | const initSizeRef = useRef(initSize); 23 | const handleMouseMove = usePersistFn(async (event: MouseEvent) => { 24 | const distance = event.clientX - positionRef.current.left; 25 | let width = initSizeRef.current.width + distance; 26 | if (minWidthConfig !== undefined) { 27 | width = Math.max(width, minWidthConfig); 28 | } 29 | if (maxWidthConfig !== undefined) { 30 | width = Math.min(width, maxWidthConfig); 31 | } 32 | handleResize({ width }); 33 | }); 34 | const handleMouseUp = useCallback(() => { 35 | window.removeEventListener('mousemove', handleMouseMove); 36 | window.removeEventListener('mouseup', handleMouseUp); 37 | setResizing(false); 38 | }, [handleMouseMove]); 39 | const handleMouseDown = useCallback( 40 | (event: React.MouseEvent) => { 41 | positionRef.current.left = event.clientX; 42 | initSizeRef.current = initSize; 43 | window.addEventListener('mousemove', handleMouseMove); 44 | window.addEventListener('mouseup', handleMouseUp); 45 | setResizing(true); 46 | }, 47 | [handleMouseMove, handleMouseUp, initSize] 48 | ); 49 | return [handleMouseDown, resizing]; 50 | } 51 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GanttProps, GanttRef } from './Gantt'; 2 | import GanttComponent from './Gantt'; 3 | import type { Gantt } from './types'; 4 | import { EGanttSightValues } from './types'; 5 | 6 | export * from './locales'; 7 | export { EGanttSightValues, Gantt, GanttProps, GanttRef }; 8 | export default GanttComponent; 9 | -------------------------------------------------------------------------------- /src/locales/en-us.ts: -------------------------------------------------------------------------------- 1 | import { GanttLocale } from "../Gantt"; 2 | 3 | export const enUS: GanttLocale = Object.freeze({ 4 | today: "Today", 5 | day: "Day", 6 | days: "Days", 7 | week: "Week", 8 | month: "Month", 9 | quarter: "Quarter", 10 | halfYear: "Half year", 11 | firstHalf: "First half", 12 | secondHalf: "Second half", 13 | majorFormat: { 14 | day: "YYYY, MMMM", 15 | week: "YYYY, MMMM", 16 | month: "YYYY", 17 | quarter: "YYYY", 18 | halfYear: "YYYY", 19 | }, 20 | minorFormat: { 21 | day: "D", 22 | week: "wo [week]", 23 | month: "MMMM", 24 | quarter: "[Q]Q", 25 | halfYear: "YYYY-", 26 | } 27 | }); -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./en-us"; 2 | export * from "./zh-cn"; 3 | export * from "./ru-ru"; 4 | -------------------------------------------------------------------------------- /src/locales/ru-ru.ts: -------------------------------------------------------------------------------- 1 | import { GanttLocale } from "../Gantt"; 2 | 3 | export const ruRU: GanttLocale = Object.freeze({ 4 | today: 'Сегодня', 5 | day: 'День', 6 | days: 'Дни', 7 | week: 'Неделя', 8 | month: 'Месяц', 9 | quarter: 'Квартал', 10 | halfYear: 'Полугодие', 11 | firstHalf: 'Первое полугодие', 12 | secondHalf: 'Второе полугодие', 13 | majorFormat: { 14 | day: 'MMMM YYYY', 15 | week: 'MMMM YYYY', 16 | month: 'YYYY', 17 | quarter: 'YYYY', 18 | halfYear: 'YYYY', 19 | }, 20 | minorFormat: { 21 | day: 'D', 22 | week: 'wo [неделя]', 23 | month: 'MMMM', 24 | quarter: '[К]Q', 25 | halfYear: 'YYYY-', 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/locales/zh-cn.ts: -------------------------------------------------------------------------------- 1 | import { GanttLocale } from "../Gantt"; 2 | 3 | export const zhCN: GanttLocale = Object.freeze({ 4 | today: "今天", 5 | day: "日视图", 6 | days: "天数", 7 | week: "周视图", 8 | month: "月视图", 9 | quarter: "季视图", 10 | halfYear: "年视图", 11 | firstHalf: "上半年", 12 | secondHalf: "下半年", 13 | majorFormat: { 14 | day: "YYYY年MM月", 15 | week: "YYYY年MM月", 16 | month: "YYYY年", 17 | quarter: "YYYY年", 18 | halfYear: "YYYY年", 19 | }, 20 | minorFormat: { 21 | day: "YYYY-MM-D", 22 | week: "YYYY-w周", 23 | month: "YYYY-MM月", 24 | quarter: "YYYY-第Q季", 25 | halfYear: "YYYY-", 26 | } 27 | }); -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import type { Dayjs } from 'dayjs'; 2 | import dayjs from 'dayjs'; 3 | import advancedFormat from 'dayjs/plugin/advancedFormat'; 4 | import isBetween from 'dayjs/plugin/isBetween'; 5 | import isLeapYear from 'dayjs/plugin/isLeapYear'; 6 | import quarterOfYear from 'dayjs/plugin/quarterOfYear'; 7 | import weekOfYear from 'dayjs/plugin/weekOfYear'; 8 | import weekday from 'dayjs/plugin/weekday'; 9 | import debounce from 'lodash/debounce'; 10 | import find from 'lodash/find'; 11 | import throttle from 'lodash/throttle'; 12 | import { action, computed, observable, runInAction, toJS } from 'mobx'; 13 | import type React from 'react'; 14 | import { createRef } from 'react'; 15 | import type { GanttLocale, GanttProps as GanttProperties } from './Gantt'; 16 | import { defaultLocale } from './Gantt'; 17 | import { HEADER_HEIGHT, TOP_PADDING } from './constants'; 18 | import type { Gantt } from './types'; 19 | import { EGanttSightValues } from './types'; 20 | import { flattenDeep, transverseData } from './utils'; 21 | 22 | dayjs.extend(weekday); 23 | dayjs.extend(weekOfYear); 24 | dayjs.extend(quarterOfYear); 25 | dayjs.extend(advancedFormat); 26 | dayjs.extend(isBetween); 27 | dayjs.extend(isLeapYear); 28 | export const ONE_DAY_MS = 86400000; 29 | // 视图日视图、周视图、月视图、季视图、年视图 30 | export const getViewTypeList = (locale) => { 31 | return [ 32 | { 33 | type: 'day', 34 | label: locale.day, 35 | value: EGanttSightValues.day, 36 | }, 37 | { 38 | type: 'week', 39 | label: locale.week, 40 | value: EGanttSightValues.week, 41 | }, 42 | { 43 | type: 'month', 44 | label: locale.month, 45 | value: EGanttSightValues.month, 46 | }, 47 | { 48 | type: 'quarter', 49 | label: locale.quarter, 50 | value: EGanttSightValues.quarter, 51 | }, 52 | { 53 | type: 'halfYear', 54 | label: locale.halfYear, 55 | value: EGanttSightValues.halfYear, 56 | }, 57 | ] as Gantt.SightConfig[]; 58 | }; 59 | function isRestDay(date: string) { 60 | const calc = [0, 6]; 61 | return calc.includes(dayjs(date).weekday()); 62 | } 63 | 64 | class GanttStore { 65 | constructor({ 66 | rowHeight, 67 | disabled = false, 68 | customSights, 69 | locale, 70 | columnsWidth, 71 | }: { 72 | rowHeight: number; 73 | disabled: boolean; 74 | customSights: Gantt.SightConfig[]; 75 | locale: GanttLocale; 76 | columnsWidth?: number; 77 | }) { 78 | this.width = 1320; 79 | this.height = 418; 80 | this.viewTypeList = customSights.length 81 | ? customSights 82 | : getViewTypeList(locale); 83 | const sightConfig = customSights.length 84 | ? customSights[0] 85 | : getViewTypeList(locale)[0]; 86 | const translateX = 87 | dayjs(this.getStartDate()).valueOf() / (sightConfig.value * 1000); 88 | const bodyWidth = this.width; 89 | const viewWidth = 704; 90 | const tableWidth = columnsWidth ?? 500; 91 | this.viewWidth = viewWidth; 92 | this.tableWidth = tableWidth; 93 | this.translateX = translateX; 94 | this.sightConfig = sightConfig; 95 | this.bodyWidth = bodyWidth; 96 | this.rowHeight = rowHeight; 97 | this.disabled = disabled; 98 | this.locale = locale; 99 | } 100 | 101 | locale = { ...defaultLocale }; 102 | 103 | _wheelTimer: number | undefined; 104 | 105 | scrollTimer: number | undefined; 106 | 107 | @observable data: Gantt.Item[] = []; 108 | 109 | @observable originData: Gantt.Record[] = []; 110 | 111 | @observable columns: Gantt.Column[] = []; 112 | 113 | @observable dependencies: Gantt.Dependence[] = []; 114 | 115 | @observable scrolling = false; 116 | 117 | @observable scrollTop = 0; 118 | 119 | @observable collapse = false; 120 | 121 | @observable tableWidth: number; 122 | 123 | @observable viewWidth: number; 124 | 125 | @observable width: number; 126 | 127 | @observable height: number; 128 | 129 | @observable bodyWidth: number; 130 | 131 | @observable translateX: number; 132 | 133 | @observable sightConfig: Gantt.SightConfig; 134 | 135 | @observable showSelectionIndicator = false; 136 | 137 | @observable selectionIndicatorTop = 0; 138 | 139 | @observable dragging: Gantt.Bar | null = null; 140 | 141 | @observable draggingType: Gantt.MoveType | null = null; 142 | 143 | @observable disabled = false; 144 | 145 | viewTypeList = getViewTypeList(this.locale); 146 | 147 | gestureKeyPress = false; 148 | 149 | mainElementRef = createRef(); 150 | 151 | chartElementRef = createRef(); 152 | 153 | isPointerPress = false; 154 | 155 | startDateKey = 'startDate'; 156 | 157 | endDateKey = 'endDate'; 158 | 159 | autoScrollPos = 0; 160 | 161 | clientX = 0; 162 | 163 | rowHeight: number; 164 | 165 | onUpdate: GanttProperties['onUpdate'] = () => Promise.resolve(true); 166 | 167 | isRestDay = isRestDay; 168 | 169 | getStartDate() { 170 | return dayjs().subtract(10, 'day').toString(); 171 | } 172 | 173 | setIsRestDay(function_: (date: string) => boolean) { 174 | this.isRestDay = function_ || isRestDay; 175 | } 176 | 177 | @action 178 | setData(data: Gantt.Record[], startDateKey: string, endDateKey: string) { 179 | this.startDateKey = startDateKey; 180 | this.endDateKey = endDateKey; 181 | this.originData = data; 182 | this.data = transverseData(data, startDateKey, endDateKey); 183 | } 184 | 185 | @action 186 | toggleCollapse() { 187 | if (this.tableWidth > 0) { 188 | this.tableWidth = 0; 189 | this.viewWidth = this.width - this.tableWidth; 190 | } else { 191 | this.initWidth(); 192 | } 193 | } 194 | 195 | @action 196 | setRowCollapse(item: Gantt.Item, collapsed: boolean) { 197 | item.collapsed = collapsed; 198 | // this.barList = this.getBarList(); 199 | } 200 | 201 | @action 202 | setOnUpdate(onUpdate: GanttProperties['onUpdate']) { 203 | this.onUpdate = onUpdate; 204 | } 205 | 206 | @action 207 | setColumns(columns: Gantt.Column[]) { 208 | this.columns = columns; 209 | } 210 | 211 | @action 212 | setDependencies(dependencies: Gantt.Dependence[]) { 213 | this.dependencies = dependencies; 214 | } 215 | 216 | @action 217 | setHideTable(isHidden = false) { 218 | if (isHidden) { 219 | this.tableWidth = 0; 220 | this.viewWidth = this.width - this.tableWidth; 221 | } else { 222 | this.initWidth(); 223 | } 224 | } 225 | 226 | @action 227 | handlePanMove(translateX: number) { 228 | this.scrolling = true; 229 | this.setTranslateX(translateX); 230 | } 231 | 232 | @action 233 | handlePanEnd() { 234 | this.scrolling = false; 235 | } 236 | 237 | @action syncSize(size: { width?: number; height?: number }) { 238 | if (!size.height || !size.width) return; 239 | 240 | const { width, height } = size; 241 | if (this.height !== height) this.height = height; 242 | 243 | if (this.width !== width) { 244 | this.width = width; 245 | this.initWidth(); 246 | } 247 | } 248 | 249 | @action handleResizeTableWidth(width: number) { 250 | const columnsWidthArr = this.columns.filter((column) => column.width > 0); 251 | if (this.columns.length === columnsWidthArr.length) return; 252 | this.tableWidth = width; 253 | this.viewWidth = this.width - this.tableWidth; 254 | } 255 | 256 | @action initWidth() { 257 | this.tableWidth = this.totalColumnWidth || 250; 258 | this.viewWidth = this.width - this.tableWidth; 259 | // 图表宽度不能小于 200 260 | if (this.viewWidth < 200) { 261 | this.viewWidth = 200; 262 | this.tableWidth = this.width - this.viewWidth; 263 | } 264 | } 265 | 266 | @action 267 | setTranslateX(translateX: number) { 268 | this.translateX = Math.max(translateX, 0); 269 | } 270 | 271 | @action switchSight(type: Gantt.Sight) { 272 | const target = find(this.viewTypeList, { type }); 273 | if (target) { 274 | this.sightConfig = target; 275 | this.setTranslateX( 276 | dayjs(this.getStartDate()).valueOf() / (target.value * 1000), 277 | ); 278 | } 279 | } 280 | 281 | @action scrollToToday() { 282 | const translateX = this.todayTranslateX - this.viewWidth / 2; 283 | this.setTranslateX(translateX); 284 | } 285 | 286 | getTranslateXByDate(date: string) { 287 | return dayjs(date).startOf('day').valueOf() / this.pxUnitAmp; 288 | } 289 | 290 | @computed get todayTranslateX() { 291 | return dayjs().startOf('day').valueOf() / this.pxUnitAmp; 292 | } 293 | 294 | @computed get scrollBarWidth() { 295 | const MIN_WIDTH = 30; 296 | return Math.max((this.viewWidth / this.scrollWidth) * 160, MIN_WIDTH); 297 | } 298 | 299 | @computed get scrollLeft() { 300 | const rate = this.viewWidth / this.scrollWidth; 301 | const currentDate = dayjs(this.translateAmp).toString(); 302 | // 默认滚动条在中间 303 | const half = (this.viewWidth - this.scrollBarWidth) / 2; 304 | const viewScrollLeft = 305 | half + 306 | rate * 307 | (this.getTranslateXByDate(currentDate) - 308 | this.getTranslateXByDate(this.getStartDate())); 309 | return Math.min( 310 | Math.max(viewScrollLeft, 0), 311 | this.viewWidth - this.scrollBarWidth, 312 | ); 313 | } 314 | 315 | @computed get scrollWidth() { 316 | // TODO 待研究 317 | // 最小宽度 318 | const init = this.viewWidth + 200; 319 | return Math.max( 320 | Math.abs( 321 | this.viewWidth + 322 | this.translateX - 323 | this.getTranslateXByDate(this.getStartDate()), 324 | ), 325 | init, 326 | ); 327 | } 328 | 329 | // 内容区滚动高度 330 | @computed get bodyClientHeight() { 331 | // 1是边框 332 | return this.height - HEADER_HEIGHT - 1; 333 | } 334 | 335 | @computed get getColumnsWidth(): number[] { 336 | if (this.columns.length === 1 && this.columns[0]?.width < 200) return [200]; 337 | const totalColumnWidth = this.columns.reduce( 338 | (width, item) => width + (item.width || 0), 339 | 0, 340 | ); 341 | const totalFlex = this.columns.reduce( 342 | (total, item) => total + (item.width ? 0 : item.flex || 1), 343 | 0, 344 | ); 345 | const restWidth = this.tableWidth - totalColumnWidth; 346 | return this.columns.map((column) => { 347 | if (column.width) return column.width; 348 | 349 | if (column.flex) return restWidth * (column.flex / totalFlex); 350 | 351 | return restWidth * (1 / totalFlex); 352 | }); 353 | } 354 | 355 | @computed get totalColumnWidth(): number { 356 | return this.getColumnsWidth.reduce((width, item) => width + (item || 0), 0); 357 | } 358 | 359 | // 内容区滚动区域域高度 360 | @computed get bodyScrollHeight() { 361 | let height = this.getBarList.length * this.rowHeight + TOP_PADDING; 362 | if (height < this.bodyClientHeight) height = this.bodyClientHeight; 363 | 364 | return height; 365 | } 366 | 367 | // 1px对应的毫秒数 368 | @computed get pxUnitAmp() { 369 | return this.sightConfig.value * 1000; 370 | } 371 | 372 | /** 当前开始时间毫秒数 */ 373 | @computed get translateAmp() { 374 | const { translateX } = this; 375 | return this.pxUnitAmp * translateX; 376 | } 377 | 378 | getDurationAmp() { 379 | const clientWidth = this.viewWidth; 380 | return this.pxUnitAmp * clientWidth; 381 | } 382 | 383 | getWidthByDate = (startDate: Dayjs, endDate: Dayjs) => 384 | (endDate.valueOf() - startDate.valueOf()) / this.pxUnitAmp; 385 | 386 | getMajorList(): Gantt.Major[] { 387 | const majorFormatMap: { [key in Gantt.Sight]: string } = { 388 | day: this.locale.majorFormat.day, 389 | week: this.locale.majorFormat.week, 390 | month: this.locale.majorFormat.month, 391 | quarter: this.locale.majorFormat.quarter, 392 | halfYear: this.locale.majorFormat.halfYear, 393 | }; 394 | const { translateAmp } = this; 395 | const endAmp = translateAmp + this.getDurationAmp(); 396 | const { type } = this.sightConfig; 397 | const format = majorFormatMap[type]; 398 | 399 | const getNextDate = (start: Dayjs) => { 400 | if (type === 'day' || type === 'week') return start.add(1, 'month'); 401 | 402 | return start.add(1, 'year'); 403 | }; 404 | 405 | const getStart = (date: Dayjs) => { 406 | if (type === 'day' || type === 'week') return date.startOf('month'); 407 | 408 | return date.startOf('year'); 409 | }; 410 | 411 | const getEnd = (date: Dayjs) => { 412 | if (type === 'day' || type === 'week') return date.endOf('month'); 413 | 414 | return date.endOf('year'); 415 | }; 416 | 417 | // 初始化当前时间 418 | let currentDate = dayjs(translateAmp); 419 | const dates: Gantt.MajorAmp[] = []; 420 | 421 | // 对可视区域内的时间进行迭代 422 | while (currentDate.isBetween(translateAmp - 1, endAmp + 1)) { 423 | const majorKey = currentDate.format(format); 424 | 425 | let start = currentDate; 426 | const end = getEnd(start); 427 | if (dates.length > 0) start = getStart(currentDate); 428 | 429 | dates.push({ 430 | label: majorKey, 431 | startDate: start, 432 | endDate: end, 433 | }); 434 | 435 | // 获取下次迭代的时间 436 | start = getStart(currentDate); 437 | currentDate = getNextDate(start); 438 | } 439 | 440 | return this.majorAmp2Px(dates); 441 | } 442 | 443 | majorAmp2Px(ampList: Gantt.MajorAmp[]) { 444 | const { pxUnitAmp } = this; 445 | return ampList.map((item) => { 446 | const { startDate } = item; 447 | const { endDate } = item; 448 | const { label } = item; 449 | const left = startDate.valueOf() / pxUnitAmp; 450 | const width = (endDate.valueOf() - startDate.valueOf()) / pxUnitAmp; 451 | 452 | return { 453 | label, 454 | left, 455 | width, 456 | key: startDate.format('YYYY-MM-DD HH:mm:ss'), 457 | }; 458 | }); 459 | } 460 | 461 | getMinorList(): Gantt.Minor[] { 462 | const minorFormatMap = { 463 | day: this.locale.minorFormat.day, 464 | week: this.locale.minorFormat.week, 465 | month: this.locale.minorFormat.month, 466 | quarter: this.locale.minorFormat.quarter, 467 | halfYear: this.locale.minorFormat.halfYear, 468 | }; 469 | const fstHalfYear = new Set([0, 1, 2, 3, 4, 5]); 470 | 471 | const startAmp = this.translateAmp; 472 | const endAmp = startAmp + this.getDurationAmp(); 473 | const format = minorFormatMap[this.sightConfig.type]; 474 | 475 | const getNextDate = (start: Dayjs) => { 476 | const map = { 477 | day() { 478 | return start.add(1, 'day'); 479 | }, 480 | week() { 481 | return start.add(1, 'week'); 482 | }, 483 | month() { 484 | return start.add(1, 'month'); 485 | }, 486 | quarter() { 487 | return start.add(1, 'quarter'); 488 | }, 489 | halfYear() { 490 | return start.add(6, 'month'); 491 | }, 492 | }; 493 | 494 | return map[this.sightConfig.type](); 495 | }; 496 | const setStart = (date: Dayjs) => { 497 | const map = { 498 | day() { 499 | return date.startOf('day'); 500 | }, 501 | week() { 502 | return date.weekday(1).hour(0).minute(0).second(0); 503 | }, 504 | month() { 505 | return date.startOf('month'); 506 | }, 507 | quarter() { 508 | return date.startOf('quarter'); 509 | }, 510 | halfYear() { 511 | if (fstHalfYear.has(date.month())) 512 | return date.month(0).startOf('month'); 513 | 514 | return date.month(6).startOf('month'); 515 | }, 516 | }; 517 | 518 | return map[this.sightConfig.type](); 519 | }; 520 | const setEnd = (start: Dayjs) => { 521 | const map = { 522 | day() { 523 | return start.endOf('day'); 524 | }, 525 | week() { 526 | return start.weekday(7).hour(23).minute(59).second(59); 527 | }, 528 | month() { 529 | return start.endOf('month'); 530 | }, 531 | quarter() { 532 | return start.endOf('quarter'); 533 | }, 534 | halfYear() { 535 | if (fstHalfYear.has(start.month())) 536 | return start.month(5).endOf('month'); 537 | 538 | return start.month(11).endOf('month'); 539 | }, 540 | }; 541 | 542 | return map[this.sightConfig.type](); 543 | }; 544 | const getMinorKey = (date: Dayjs) => { 545 | if (this.sightConfig.type === 'halfYear') 546 | return ( 547 | date.format(format) + 548 | (fstHalfYear.has(date.month()) 549 | ? this.locale.firstHalf 550 | : this.locale.secondHalf) 551 | ); 552 | 553 | return date.format(format); 554 | }; 555 | 556 | // 初始化当前时间 557 | let currentDate = dayjs(startAmp); 558 | const dates: Gantt.MinorAmp[] = []; 559 | while (currentDate.isBetween(startAmp - 1, endAmp + 1)) { 560 | const minorKey = getMinorKey(currentDate); 561 | const start = setStart(currentDate); 562 | const end = setEnd(start); 563 | dates.push({ 564 | label: minorKey.split('-').pop() as string, 565 | startDate: start, 566 | endDate: end, 567 | }); 568 | currentDate = getNextDate(start); 569 | } 570 | 571 | return this.minorAmp2Px(dates); 572 | } 573 | 574 | startXRectBar = (startX: number) => { 575 | let date = dayjs(startX * this.pxUnitAmp); 576 | const dayRect = () => { 577 | const stAmp = date.startOf('day'); 578 | const endAmp = date.endOf('day'); 579 | // @ts-expect-error 580 | const left = stAmp / this.pxUnitAmp; 581 | // @ts-expect-error 582 | const width = (endAmp - stAmp) / this.pxUnitAmp; 583 | 584 | return { 585 | left, 586 | width, 587 | }; 588 | }; 589 | const weekRect = () => { 590 | if (date.weekday() === 0) date = date.add(-1, 'week'); 591 | 592 | const left = date.weekday(1).startOf('day').valueOf() / this.pxUnitAmp; 593 | const width = (7 * 24 * 60 * 60 * 1000 - 1000) / this.pxUnitAmp; 594 | 595 | return { 596 | left, 597 | width, 598 | }; 599 | }; 600 | const monthRect = () => { 601 | const stAmp = date.startOf('month').valueOf(); 602 | const endAmp = date.endOf('month').valueOf(); 603 | const left = stAmp / this.pxUnitAmp; 604 | const width = (endAmp - stAmp) / this.pxUnitAmp; 605 | 606 | return { 607 | left, 608 | width, 609 | }; 610 | }; 611 | 612 | const map = { 613 | day: dayRect, 614 | week: weekRect, 615 | month: weekRect, 616 | quarter: monthRect, 617 | halfYear: monthRect, 618 | }; 619 | 620 | return map[this.sightConfig.type](); 621 | }; 622 | 623 | minorAmp2Px(ampList: Gantt.MinorAmp[]): Gantt.Minor[] { 624 | const { pxUnitAmp } = this; 625 | return ampList.map((item) => { 626 | const { startDate } = item; 627 | const { endDate } = item; 628 | 629 | const { label } = item; 630 | const left = startDate.valueOf() / pxUnitAmp; 631 | const width = (endDate.valueOf() - startDate.valueOf()) / pxUnitAmp; 632 | 633 | let isWeek = false; 634 | if (this.sightConfig.type === 'day') 635 | isWeek = this.isRestDay(startDate.toString()); 636 | 637 | return { 638 | label, 639 | left, 640 | width, 641 | isWeek, 642 | key: startDate.format('YYYY-MM-DD HH:mm:ss'), 643 | }; 644 | }); 645 | } 646 | 647 | getTaskBarThumbVisible(barInfo: Gantt.Bar) { 648 | const { width, translateX: barTranslateX, invalidDateRange } = barInfo; 649 | if (invalidDateRange) return false; 650 | 651 | const rightSide = this.translateX + this.viewWidth; 652 | return ( 653 | barTranslateX + width < this.translateX || barTranslateX - rightSide > 0 654 | ); 655 | } 656 | 657 | scrollToBar(barInfo: Gantt.Bar, type: 'left' | 'right') { 658 | const { translateX: barTranslateX, width } = barInfo; 659 | const translateX1 = this.translateX + this.viewWidth / 2; 660 | const translateX2 = barTranslateX + width; 661 | 662 | const diffX = Math.abs(translateX2 - translateX1); 663 | let translateX = this.translateX + diffX; 664 | 665 | if (type === 'left') translateX = this.translateX - diffX; 666 | 667 | this.setTranslateX(translateX); 668 | } 669 | 670 | @computed get getBarList(): Gantt.Bar[] { 671 | const { pxUnitAmp, data } = this; 672 | // 最小宽度 673 | const minStamp = 11 * pxUnitAmp; 674 | // TODO 去除高度读取 675 | const height = 8; 676 | const baseTop = TOP_PADDING + this.rowHeight / 2 - height / 2; 677 | const topStep = this.rowHeight; 678 | 679 | const dateTextFormat = (startX: number) => 680 | dayjs(startX * pxUnitAmp).format('YYYY-MM-DD'); 681 | 682 | const getDateWidth = (start: number, endX: number) => { 683 | const startDate = dayjs(start * pxUnitAmp); 684 | const endDate = dayjs(endX * pxUnitAmp); 685 | return `${startDate.diff(endDate, 'day') + 1}`; 686 | }; 687 | 688 | const flattenData = flattenDeep(data); 689 | const barList = flattenData.map((item, index) => { 690 | const valid = item.startDate && item.endDate; 691 | let startAmp = dayjs(item.startDate || 0) 692 | .startOf('day') 693 | .valueOf(); 694 | let endAmp = dayjs(item.endDate || 0) 695 | .endOf('day') 696 | .valueOf(); 697 | 698 | // 开始结束日期相同默认一天 699 | if (Math.abs(endAmp - startAmp) < minStamp) { 700 | startAmp = dayjs(item.startDate || 0) 701 | .startOf('day') 702 | .valueOf(); 703 | endAmp = dayjs(item.endDate || 0) 704 | .endOf('day') 705 | .add(minStamp, 'millisecond') 706 | .valueOf(); 707 | } 708 | 709 | const width = valid ? (endAmp - startAmp) / pxUnitAmp : 0; 710 | const translateX = valid ? startAmp / pxUnitAmp : 0; 711 | const translateY = baseTop + index * topStep; 712 | const { _parent } = item; 713 | const record = { ...item.record, disabled: this.disabled }; 714 | const bar: Gantt.Bar = { 715 | key: item.key, 716 | task: item, 717 | record, 718 | translateX, 719 | translateY, 720 | width, 721 | label: item.content, 722 | stepGesture: 'end', // start(开始)、moving(移动)、end(结束) 723 | invalidDateRange: !item.endDate || !item.startDate, // 是否为有效时间区间 724 | dateTextFormat, 725 | getDateWidth, 726 | loading: false, 727 | _group: item.group, 728 | _collapsed: item.collapsed, // 是否折叠 729 | _depth: item._depth as number, // 表示子节点深度 730 | _index: item._index, // 任务下标位置 731 | _parent, // 原任务数据 732 | _childrenCount: !item.children ? 0 : item.children.length, // 子任务 733 | }; 734 | item._bar = bar; 735 | return bar; 736 | }); 737 | // 进行展开扁平 738 | return observable(barList); 739 | } 740 | 741 | @action 742 | handleWheel = (event: WheelEvent) => { 743 | if (event.deltaX !== 0) { 744 | event.preventDefault(); 745 | event.stopPropagation(); 746 | } 747 | if (this._wheelTimer) clearTimeout(this._wheelTimer); 748 | // 水平滚动 749 | if (Math.abs(event.deltaX) > 0) { 750 | this.scrolling = true; 751 | this.setTranslateX(this.translateX + event.deltaX); 752 | } 753 | this._wheelTimer = window.setTimeout(() => { 754 | this.scrolling = false; 755 | }, 100); 756 | }; 757 | 758 | handleScroll = (event: React.UIEvent) => { 759 | const { scrollTop } = event.currentTarget; 760 | this.scrollY(scrollTop); 761 | }; 762 | 763 | scrollY = throttle((scrollTop: number) => { 764 | this.scrollTop = scrollTop; 765 | }, 100); 766 | 767 | // 虚拟滚动 768 | @computed get getVisibleRows() { 769 | const visibleHeight = this.bodyClientHeight; 770 | // 多渲染几个,减少空白 771 | const visibleRowCount = Math.ceil(visibleHeight / this.rowHeight) + 10; 772 | 773 | const start = Math.max(Math.ceil(this.scrollTop / this.rowHeight) - 5, 0); 774 | return { 775 | start, 776 | count: visibleRowCount, 777 | }; 778 | } 779 | 780 | handleMouseMove = debounce((event) => { 781 | if (!this.isPointerPress) this.showSelectionBar(event); 782 | }, 5); 783 | 784 | handleMouseLeave() { 785 | this.showSelectionIndicator = false; 786 | } 787 | 788 | @action 789 | showSelectionBar(event: MouseEvent) { 790 | const scrollTop = this.mainElementRef.current?.scrollTop || 0; 791 | const { top } = this.mainElementRef.current?.getBoundingClientRect() || { 792 | top: 0, 793 | }; 794 | // 内容区高度 795 | const contentHeight = this.getBarList.length * this.rowHeight; 796 | const offsetY = event.clientY - top + scrollTop; 797 | if (offsetY - contentHeight > TOP_PADDING) { 798 | this.showSelectionIndicator = false; 799 | } else { 800 | const topValue = 801 | Math.floor((offsetY - TOP_PADDING) / this.rowHeight) * this.rowHeight + 802 | TOP_PADDING; 803 | this.showSelectionIndicator = true; 804 | this.selectionIndicatorTop = topValue; 805 | } 806 | } 807 | 808 | getHovered = (top: number) => { 809 | const baseTop = top - (top % this.rowHeight); 810 | return ( 811 | this.selectionIndicatorTop >= baseTop && 812 | this.selectionIndicatorTop <= baseTop + this.rowHeight 813 | ); 814 | }; 815 | 816 | @action 817 | handleDragStart(barInfo: Gantt.Bar, type: Gantt.MoveType) { 818 | this.dragging = barInfo; 819 | this.draggingType = type; 820 | barInfo.stepGesture = 'start'; 821 | this.isPointerPress = true; 822 | } 823 | 824 | @action 825 | handleDragEnd() { 826 | if (this.dragging) { 827 | this.dragging.stepGesture = 'end'; 828 | this.dragging = null; 829 | } 830 | this.draggingType = null; 831 | this.isPointerPress = false; 832 | } 833 | 834 | @action 835 | handleInvalidBarLeave() { 836 | this.handleDragEnd(); 837 | } 838 | 839 | @action 840 | handleInvalidBarHover(barInfo: Gantt.Bar, left: number, width: number) { 841 | barInfo.translateX = left; 842 | barInfo.width = width; 843 | this.handleDragStart(barInfo, 'create'); 844 | } 845 | 846 | @action 847 | handleInvalidBarDragStart(barInfo: Gantt.Bar) { 848 | barInfo.stepGesture = 'moving'; 849 | } 850 | 851 | @action 852 | handleInvalidBarDragEnd( 853 | barInfo: Gantt.Bar, 854 | oldSize: { width: number; x: number }, 855 | ) { 856 | barInfo.invalidDateRange = false; 857 | this.handleDragEnd(); 858 | this.updateTaskDate(barInfo, oldSize, 'create'); 859 | } 860 | 861 | @action 862 | updateBarSize( 863 | barInfo: Gantt.Bar, 864 | { width, x }: { width: number; x: number }, 865 | ) { 866 | barInfo.width = width; 867 | barInfo.translateX = Math.max(x, 0); 868 | barInfo.stepGesture = 'moving'; 869 | } 870 | 871 | getMovedDay(ms: number): number { 872 | return Math.round(ms / ONE_DAY_MS); 873 | } 874 | 875 | /** 更新时间 */ 876 | @action 877 | async updateTaskDate( 878 | barInfo: Gantt.Bar, 879 | oldSize: { width: number; x: number }, 880 | type: 'move' | 'left' | 'right' | 'create', 881 | ) { 882 | const { translateX, width, task, record } = barInfo; 883 | const oldStartDate = barInfo.task.startDate; 884 | const oldEndDate = barInfo.task.endDate; 885 | let startDate = oldStartDate; 886 | let endDate = oldEndDate; 887 | 888 | if (type === 'move') { 889 | const moveTime = this.getMovedDay( 890 | (translateX - oldSize.x) * this.pxUnitAmp, 891 | ); 892 | // 移动,只根据移动距离偏移 893 | startDate = dayjs(oldStartDate) 894 | .add(moveTime, 'day') 895 | .format('YYYY-MM-DD HH:mm:ss'); 896 | endDate = dayjs(oldEndDate) 897 | .add(moveTime, 'day') 898 | .hour(23) 899 | .minute(59) 900 | .second(59) 901 | .format('YYYY-MM-DD HH:mm:ss'); 902 | } else if (type === 'left') { 903 | const moveTime = this.getMovedDay( 904 | (translateX - oldSize.x) * this.pxUnitAmp, 905 | ); 906 | // 左侧移动,只改变开始时间 907 | startDate = dayjs(oldStartDate) 908 | .add(moveTime, 'day') 909 | .format('YYYY-MM-DD HH:mm:ss'); 910 | } else if (type === 'right') { 911 | const moveTime = this.getMovedDay( 912 | (width - oldSize.width) * this.pxUnitAmp, 913 | ); 914 | // 右侧移动,只改变结束时间 915 | endDate = dayjs(oldEndDate) 916 | .add(moveTime, 'day') 917 | .hour(23) 918 | .minute(59) 919 | .second(59) 920 | .format('YYYY-MM-DD HH:mm:ss'); 921 | } else if (type === 'create') { 922 | // 创建 923 | startDate = dayjs(translateX * this.pxUnitAmp).format( 924 | 'YYYY-MM-DD HH:mm:ss', 925 | ); 926 | endDate = dayjs((translateX + width) * this.pxUnitAmp) 927 | .subtract(1) 928 | .hour(23) 929 | .minute(59) 930 | .second(59) 931 | .format('YYYY-MM-DD HH:mm:ss'); 932 | } 933 | if (startDate === oldStartDate && endDate === oldEndDate) return; 934 | 935 | runInAction(() => { 936 | barInfo.loading = true; 937 | }); 938 | const success = await this.onUpdate(toJS(record), startDate, endDate); 939 | if (success) { 940 | runInAction(() => { 941 | task.startDate = startDate; 942 | task.endDate = endDate; 943 | }); 944 | } else { 945 | barInfo.width = oldSize.width; 946 | barInfo.translateX = oldSize.x; 947 | } 948 | } 949 | 950 | isToday(key: string) { 951 | const now = dayjs().format('YYYY-MM-DD'); 952 | const target = dayjs(key).format('YYYY-MM-DD'); 953 | return target === now; 954 | } 955 | } 956 | 957 | export default GanttStore; 958 | -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | @import './themes/index'; 2 | -------------------------------------------------------------------------------- /src/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | -------------------------------------------------------------------------------- /src/style/themes/default.less: -------------------------------------------------------------------------------- 1 | @gantt-prefix: gantt; 2 | -------------------------------------------------------------------------------- /src/style/themes/index.less: -------------------------------------------------------------------------------- 1 | @import './default.less'; 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Dayjs } from 'dayjs'; 2 | import type React from 'react'; 3 | 4 | export type DefaultRecordType = Record; 5 | 6 | export enum EGanttSightValues { 7 | day = 2880, 8 | week = 3600, 9 | month = 14400, 10 | quarter = 86400, 11 | halfYear = 115200, 12 | } 13 | export namespace Gantt { 14 | export interface Major { 15 | width: number; 16 | left: number; 17 | label: string; 18 | key: string; 19 | } 20 | export interface MajorAmp { 21 | label: string; 22 | startDate: Dayjs; 23 | endDate: Dayjs; 24 | } 25 | export interface Minor { 26 | width: number; 27 | left: number; 28 | label: string; 29 | isWeek: boolean; 30 | key: string; 31 | } 32 | export interface MinorAmp { 33 | label: string; 34 | startDate: Dayjs; 35 | endDate: Dayjs; 36 | } 37 | export type Sight = 'day' | 'week' | 'month' | 'quarter' | 'halfYear'; 38 | export type MoveType = 'left' | 'right' | 'move' | 'create'; 39 | 40 | export interface SightConfig { 41 | type: Sight; 42 | label: string; 43 | value: EGanttSightValues; 44 | } 45 | export interface Bar { 46 | key: React.Key; 47 | label: string; 48 | width: number; 49 | translateX: number; 50 | translateY: number; 51 | stepGesture: string; 52 | invalidDateRange: boolean; 53 | dateTextFormat: (startX: number) => string; 54 | getDateWidth: (startX: number, endX: number) => string; 55 | task: Item; 56 | record: Record; 57 | loading: boolean; 58 | _group?: boolean; 59 | _collapsed: boolean; 60 | _depth: number; 61 | _index?: number; 62 | _childrenCount: number; 63 | _parent?: Item; 64 | } 65 | export interface Item { 66 | record: Record; 67 | key: React.Key; 68 | startDate: string | null; 69 | endDate: string | null; 70 | content: string; 71 | collapsed: boolean; 72 | group?: boolean; 73 | children?: Item[]; 74 | _parent?: Item; 75 | _bar?: Bar; 76 | _depth?: number; 77 | _index?: number; 78 | } 79 | 80 | export type Record = RecordType & { 81 | group?: boolean; 82 | borderColor?: string; 83 | backgroundColor?: string; 84 | collapsed?: boolean; 85 | children?: Record[]; 86 | disabled?: boolean; 87 | }; 88 | 89 | export type ColumnAlign = 'center' | 'right' | 'left'; 90 | export interface Column { 91 | width?: number; 92 | minWidth?: number; 93 | maxWidth?: number; 94 | flex?: number; 95 | name: string; 96 | label: string; 97 | style?: object; 98 | render?: (item: Record) => React.ReactNode; 99 | align?: ColumnAlign; 100 | } 101 | export type DependenceType = 102 | | 'start_finish' 103 | | 'finish_start' 104 | | 'start_start' 105 | | 'finish_finish'; 106 | export interface Dependence { 107 | from: string; 108 | to: string; 109 | type: DependenceType; 110 | color?: string; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Gantt } from './types' 2 | 3 | /** 4 | * 将树形数据向下递归为一维数组 5 | * 6 | * @param {any} arr 数据源 7 | */ 8 | export function flattenDeep(array: Gantt.Item[] = [], depth = 0, parent?: Gantt.Item | undefined): Gantt.Item[] { 9 | let index = 0 10 | return array.reduce((flat: Gantt.Item[], item) => { 11 | item._depth = depth 12 | item._parent = parent 13 | item._index = index 14 | index += 1 15 | return [...flat, item, ...(item.children && !item.collapsed ? flattenDeep(item.children, depth + 1, item) : [])] 16 | }, []) 17 | } 18 | 19 | export function getMaxRange(bar: Gantt.Bar) { 20 | let minTranslateX = 0 21 | let maxTranslateX = 0 22 | const temporary: Gantt.Bar[] = [bar] 23 | 24 | while (temporary.length > 0) { 25 | const current = temporary.shift() 26 | if (current) { 27 | const { translateX = 0, width = 0 } = current 28 | if (minTranslateX === 0) minTranslateX = translateX || 0 29 | 30 | if (translateX) { 31 | minTranslateX = Math.min(translateX, minTranslateX) 32 | maxTranslateX = Math.max(translateX + width, maxTranslateX) 33 | } 34 | if (current.task.children && current.task.children.length > 0) 35 | for (const t of current.task.children) if (t._bar) temporary.push(t._bar) 36 | } 37 | } 38 | 39 | return { 40 | translateX: minTranslateX, 41 | width: maxTranslateX - minTranslateX, 42 | } 43 | } 44 | const genKey = (() => { 45 | let key = 0 46 | return function () { 47 | return key++ 48 | } 49 | })() 50 | export function transverseData(data: Gantt.Record[] = [], startDateKey: string, endDateKey: string) { 51 | const result: Gantt.Item[] = [] 52 | 53 | for (const record of data) { 54 | const item: Gantt.Item = { 55 | key: genKey(), 56 | record, 57 | content: '', 58 | group: record.group, 59 | startDate: record[startDateKey] || '', 60 | endDate: record[endDateKey] || '', 61 | collapsed: record.collapsed || false, 62 | children: transverseData(record.children || [], startDateKey, endDateKey), 63 | } 64 | result.push(item) 65 | } 66 | return result 67 | } 68 | -------------------------------------------------------------------------------- /test/blah.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import Gantt from '../src'; 4 | 5 | describe('Basic', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render( 9 | true} 34 | />, 35 | div, 36 | ); 37 | ReactDOM.unmountComponentAtNode(div); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "declaration": true, 5 | "module": "esnext", 6 | "target": "es2019", 7 | "lib": ["dom", "es2016"], 8 | "importHelpers": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "jsx": "react", 13 | "moduleResolution": "node", 14 | "baseUrl": "./", 15 | "paths": { 16 | "@@/*": [".dumi/tmp/*"], 17 | "rc-gantt": ["src"], 18 | "rc-gantt/*": ["src/*", "*"] 19 | } 20 | }, 21 | "include": [".dumirc.ts", "src/**/*", "./typings.d.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' 2 | declare module '*.less' { 3 | const resource: { [key: string]: string } 4 | export = resource 5 | } 6 | -------------------------------------------------------------------------------- /website/component.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Component 3 | nav: 4 | path: /component 5 | title: Component 6 | order: 1 7 | --- 8 | 9 | ### Basic Component 10 | 11 | use `data` `columns` `onUpdate` config, Show a basic Gantt chart 12 | 13 | 14 | 15 | ### Add Task Bar 16 | 17 | click to create task`bar` 18 | 19 | 20 | 21 | ### Multi-level structure 22 | 23 | Ensure that each node contains the `children` attribute to achieve a multi-level structure 24 | 25 | You can get the current expanded status with `onExpand` 26 | 27 | 28 | 29 | ### Customizing table columns 30 | 31 | `columns` type definition see Type Definition 32 | 33 | where if each column is configured with the `width` property. The total width is calculated internally by the component. The default initialized table width is the total width 34 | 35 | 36 | 37 | ### Dependency Structure 38 | 39 | 40 | 41 | ### Custom Rendering 42 | 43 | 44 | 45 | ### Customized filtering 46 | 47 | Default current date filtering support Day, Week, Month, Quarter, Year. Support incoming configuration to customize the filtering dimension 48 | 49 | 50 | 51 | ### Advanced Usage 52 | 53 | Introducing the use of the built-in methods on `alwaysShowTaskBar`, `unit` and `innerRef`. 54 | 55 | 56 | 57 | ## Type Definition 58 | 59 | ### `Column` Definition 60 | 61 | ```typescript 62 | export type ColumnAlign = 'center' | 'right' | 'left' 63 | export interface Column { 64 | width?: number 65 | minWidth?: number 66 | maxWidth?: number 67 | flex?: number 68 | name: string 69 | label: string 70 | style?: Object 71 | render?: (item: Record) => React.ReactNode 72 | align?: ColumnAlign 73 | } 74 | ``` 75 | 76 | ### `data` Definition 77 | 78 | The following fields are built in, and special treatment will be done if the data contains the following attributes 79 | 80 | ```typescript 81 | export type Record = RecordType & { 82 | group?: boolean 83 | borderColor?: string 84 | backgroundColor?: string 85 | collapsed?: boolean 86 | children?: Record[] 87 | disabled?: boolean 88 | } 89 | ``` 90 | 91 | ### `Dependence` Definition 92 | 93 | ```typescript 94 | export type DependenceType = 'start_finish' | 'finish_start' | 'start_start' | 'finish_finish' 95 | export interface Dependence { 96 | from: string 97 | to: string 98 | type: DependenceType 99 | } 100 | ``` 101 | 102 | ### `Bar` Definition 103 | 104 | When we need to use some custom functions, we will be returned the following type of data, where `record` is the source data 105 | 106 | ```typescript 107 | export interface Bar { 108 | key: React.Key 109 | label: string 110 | width: number 111 | translateX: number 112 | translateY: number 113 | stepGesture: string 114 | invalidDateRange: boolean 115 | dateTextFormat: (startX: number) => string 116 | getDateWidth: (startX: number, endX: number) => string 117 | task: Item 118 | record: Record 119 | loading: boolean 120 | _group?: boolean 121 | _collapsed: boolean 122 | _depth: number 123 | _index?: number 124 | _childrenCount: number 125 | _parent?: Item 126 | } 127 | ``` 128 | 129 | ### `Sight` Definition 130 | 131 | ```typescript 132 | export type Sight = 'day' | 'week' | 'month' | 'quarter' | 'halfYear' 133 | ``` 134 | 135 | ## API 136 | 137 | | 参数 | 说明 | 类型 | 默认值 | 138 | | --- | --- | --- | --- | 139 | | data | Data | `Gantt.Record[]` | | 140 | | columns | Data columns | `Gantt.Column[]` | 141 | | dependencies | Dependencies | `Gantt.Dependence[]` | `[]` | 142 | | onUpdate | Update callback | `(record: Gantt.Record, startDate: string, endDate: string) => Promise` | 143 | | startDateKey | Start date key | `string` | `startDate` | 144 | | endDateKey | End date key | `string` | `startDate` | 145 | | isRestDay | Returns whether it is a holiday | `(date: string) => boolean` | | 146 | | unit | Current view | `Gantt.Sight` | | 147 | | rowHeight | Line height | `number` | 148 | | columnWidth | Default column width | `number` | 149 | | getBarColor | Returns to default bar style | `(record: Gantt.Record) => {backgroundColor: string;borderColor: string}` | 150 | | showBackToday | Show `Back to Today` button | `boolean` | 151 | | showUnitSwitch | Shows the unit switcher | `boolean` | 152 | | onRow | Row events | `{onClick: (record: Gantt.Record) => void}` | 153 | | tableIndent | Table indentation | `number` | `30` | 154 | | expandIcon | Expand child node icon | `` | 155 | | renderBar | Custom bar rendering | `renderBar?: (barInfo: Gantt.Bar, { width, height }: { width: number; height: number }) => React.ReactNode` | 156 | | renderGroupBar | Custom group bar render | | 157 | | renderInvalidBar | Custom invalid bar render | | 158 | | renderBarThumb | Custom bar thumb render | | 159 | | onBarClick | On bar click callback | `(record: Gantt.Record) => void` | 160 | | alwaysShowTaskBar | Whether to display the left and right side contents | `boolean` | `true` | 161 | | disabled | Whether to disable the chart | `boolean` | `false` | 162 | | renderLeftText | Custom rendering of the left content area | `(barInfo: Gantt.Bar) => React.ReactNode` | 163 | | renderRightText | Custom rendering of the right content area | `(barInfo: Gantt.Bar) => React.ReactNode` | 164 | | onExpand | On expand callback | `(record: Gantt.Record,collapsed:boolean) => void` | 165 | 166 | ## Methods 167 | 168 | has `innerRef` 169 | 170 | | 参数 | 说明 | 类型 | 默认值 | 171 | | -------------- | -------------- | ---------- | ------ | 172 | | backToday | backToday | `Function` | 173 | | getWidthByDate | getWidthByDate | `Function` | 174 | -------------------------------------------------------------------------------- /website/component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: rc-gantt 组件示例 3 | nav: 4 | path: /component 5 | title: 组件示例 6 | order: 1 7 | --- 8 | 9 | ### 基础使用 10 | 11 | 只需要配置 `data` `columns` `onUpdate` 即可展示一个最简甘特图 12 | 13 | 14 | 15 | ### 新增任务 16 | 17 | 点击即可新建 `bar` 18 | 19 | 20 | 21 | ### 多级结构 22 | 23 | 确保每个节点中包含 `children` 属性,即可实现多级结构 24 | 25 | 可以通过 `onExpand` 获取当前展开的状态 26 | 27 | 28 | 29 | ### 自定义表格列 30 | 31 | `columns` 类型定义见类型定义 32 | 33 | 其中,如果每列都配置 `width` 属性。组件内部会计算总宽度。默认初始化表格宽度为总宽度 34 | 35 | 36 | 37 | ### 依赖结构 38 | 39 | 40 | 41 | ### 自定义渲染 42 | 43 | 44 | 45 | ### 自定义筛选 46 | 47 | 默认当前日期筛选支持 日、周、月、季、年。支持传入配置,自定义筛选维度 48 | 49 | 50 | 51 | ### 高级用法 52 | 53 | 主要介绍 `alwaysShowTaskBar` `unit` 以及 `innerRef` 上内置方法的使用 54 | 55 | 56 | 57 | ## 类型定义 58 | 59 | ### `Column` 定义 60 | 61 | ```typescript 62 | export type ColumnAlign = 'center' | 'right' | 'left' 63 | export interface Column { 64 | width?: number 65 | minWidth?: number 66 | maxWidth?: number 67 | flex?: number 68 | name: string 69 | label: string 70 | style?: Object 71 | render?: (item: Record) => React.ReactNode 72 | align?: ColumnAlign 73 | } 74 | ``` 75 | 76 | ### `data` 定义 77 | 78 | 其中内置了如下几个字段,如果数据中包含如下属性会做特殊处理 79 | 80 | ```typescript 81 | export type Record = RecordType & { 82 | group?: boolean 83 | borderColor?: string 84 | backgroundColor?: string 85 | collapsed?: boolean 86 | children?: Record[] 87 | disabled?: boolean 88 | } 89 | ``` 90 | 91 | ### `Dependence` 定义 92 | 93 | ```typescript 94 | export type DependenceType = 'start_finish' | 'finish_start' | 'start_start' | 'finish_finish' 95 | export interface Dependence { 96 | from: string 97 | to: string 98 | type: DependenceType 99 | } 100 | ``` 101 | 102 | ### `Bar` 定义 103 | 104 | 在我们需要使用一些自定义函数时,会给我们返回如下类型数据,其中 `record` 为源数据 105 | 106 | ```typescript 107 | export interface Bar { 108 | key: React.Key 109 | label: string 110 | width: number 111 | translateX: number 112 | translateY: number 113 | stepGesture: string 114 | invalidDateRange: boolean 115 | dateTextFormat: (startX: number) => string 116 | getDateWidth: (startX: number, endX: number) => string 117 | task: Item 118 | record: Record 119 | loading: boolean 120 | _group?: boolean 121 | _collapsed: boolean 122 | _depth: number 123 | _index?: number 124 | _childrenCount: number 125 | _parent?: Item 126 | } 127 | ``` 128 | 129 | ### `Sight` 定义 130 | 131 | ```typescript 132 | export type Sight = 'day' | 'week' | 'month' | 'quarter' | 'halfYear' 133 | ``` 134 | 135 | ## API 136 | 137 | | 参数 | 说明 | 类型 | 默认值 | 138 | | --- | --- | --- | --- | 139 | | data | 数据源 | `Gantt.Record[]` | | 140 | | columns | 数据列 | `Gantt.Column[]` | 141 | | dependencies | 依赖数组 | `Gantt.Dependence[]` | `[]` | 142 | | onUpdate | 更新回调 | `(record: Gantt.Record, startDate: string, endDate: string) => Promise` | 143 | | startDateKey | 开始时间属性 key | `string` | `startDate` | 144 | | endDateKey | 结束时间属性 key | `string` | `startDate` | 145 | | isRestDay | 返回是否是节假日 | `(date: string) => boolean` | | 146 | | unit | 当前视图 | `Gantt.Sight` | | 147 | | rowHeight | 行高 | `number` | 148 | | columnWidth | 列默认宽度 | `number` | 149 | | getBarColor | 返回默认条样式 | `(record: Gantt.Record) => {backgroundColor: string;borderColor: string}` | 150 | | showBackToday | 展示返回今日 | `boolean` | 151 | | showUnitSwitch | 展示视图切换 | `boolean` | 152 | | onRow | 行事件 | `{onClick: (record: Gantt.Record) => void}` | 153 | | tableIndent | 表格缩进 | `number` | `30` | 154 | | expandIcon | 展开子节点图表 | `` | 155 | | renderBar | 自定义渲染 bar | `renderBar?: (barInfo: Gantt.Bar, { width, height }: { width: number; height: number }) => React.ReactNode` | 156 | | renderGroupBar | 自定义渲染组 | | 157 | | renderInvalidBar | 自定义渲染拖拽 | | 158 | | renderBarThumb | 自定义缩略渲染 | | 159 | | onBarClick | 行点击事件 | `(record: Gantt.Record) => void` | 160 | | alwaysShowTaskBar | 是否展示左右侧内容 | `boolean` | `true` | 161 | | disabled | 是否禁用图表 | `boolean` | `false` | 162 | | renderLeftText | 自定义渲染左侧内容区 | `(barInfo: Gantt.Bar) => React.ReactNode` | 163 | | renderRightText | 自定义渲染右侧内容区 | `(barInfo: Gantt.Bar) => React.ReactNode` | 164 | | onExpand | 点击展开图标时触发 | `(record: Gantt.Record,collapsed:boolean) => void` | 165 | 166 | ## 方法 167 | 168 | 对外抛出 `innerRef` 169 | 170 | | 参数 | 说明 | 类型 | 默认值 | 171 | | -------------- | -------- | ---------- | ------ | 172 | | backToday | 返回今日 | `Function` | 173 | | getWidthByDate | 返回事件 | `Function` | 174 | -------------------------------------------------------------------------------- /website/demo/add.en-US.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import RcGantt, { enUS } from 'rc-gantt'; 6 | import React from 'react'; 7 | 8 | interface Data { 9 | name: string; 10 | startDate: string; 11 | endDate: string; 12 | } 13 | 14 | const data = Array.from({ length: 100 }).fill({ 15 | name: 'Title', 16 | }) as Data[]; 17 | 18 | const App = () => ( 19 |
20 | 21 | locale={enUS} 22 | data={data} 23 | columns={[ 24 | { 25 | name: 'name', 26 | label: 'Title', 27 | width: 100, 28 | }, 29 | ]} 30 | onUpdate={async (row, startDate, endDate) => { 31 | console.log('update', row, startDate, endDate); 32 | return true; 33 | }} 34 | /> 35 |
36 | ); 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /website/demo/add.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import RcGantt from 'rc-gantt'; 5 | import React from 'react'; 6 | 7 | interface Data { 8 | name: string; 9 | startDate: string; 10 | endDate: string; 11 | } 12 | 13 | const data = Array.from({ length: 100 }).fill({ 14 | name: '一个名称一个名称一个名称一个名称', 15 | }) as Data[]; 16 | 17 | const App = () => ( 18 |
19 | 20 | data={data} 21 | columns={[ 22 | { 23 | name: 'name', 24 | label: '名称', 25 | width: 100, 26 | }, 27 | ]} 28 | onUpdate={async (row, startDate, endDate) => { 29 | console.log('update', row, startDate, endDate); 30 | return true; 31 | }} 32 | /> 33 |
34 | ); 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /website/demo/basic.en-US.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import RcGantt, { enUS } from 'rc-gantt'; 7 | import React, { useState } from 'react'; 8 | 9 | interface Data { 10 | id: number; 11 | name: string; 12 | startDate: string; 13 | endDate: string; 14 | } 15 | 16 | function createData(len: number) { 17 | const result: Data[] = []; 18 | for (let i = 0; i < len; i++) { 19 | result.push({ 20 | id: i, 21 | name: 'Title' + i, 22 | startDate: dayjs().subtract(-i, 'day').format('YYYY-MM-DD'), 23 | endDate: dayjs().add(i, 'day').format('YYYY-MM-DD'), 24 | }); 25 | } 26 | return result; 27 | } 28 | 29 | const App = () => { 30 | const [data, setData] = useState(createData(20)); 31 | console.log('data', data); 32 | return ( 33 |
34 | 35 | data={data} 36 | columns={[ 37 | { 38 | name: 'name', 39 | label: 'Custom Title', 40 | width: 100, 41 | }, 42 | ]} 43 | locale={enUS} 44 | onUpdate={async (row, startDate, endDate) => { 45 | console.log('update', row, startDate, endDate); 46 | setData((prev) => { 47 | const newList = [...prev]; 48 | const index = newList.findIndex((val) => val.id === row.id); 49 | newList[index] = { 50 | ...row, 51 | startDate: dayjs(startDate).format('YYYY-MM-DD'), 52 | endDate: dayjs(endDate).format('YYYY-MM-DD'), 53 | }; 54 | return newList; 55 | }); 56 | return true; 57 | }} 58 | /> 59 |
60 | ); 61 | }; 62 | 63 | export default App; 64 | -------------------------------------------------------------------------------- /website/demo/basic.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import RcGantt from 'rc-gantt'; 7 | import React, { useState } from 'react'; 8 | 9 | interface Data { 10 | id: number; 11 | name: string; 12 | startDate: string; 13 | endDate: string; 14 | } 15 | 16 | function createData(len: number) { 17 | const result: Data[] = []; 18 | for (let i = 0; i < len; i++) { 19 | result.push({ 20 | id: i, 21 | name: '一个名称一个名称一个名称一个名称', 22 | startDate: dayjs().subtract(-i, 'day').format('YYYY-MM-DD'), 23 | endDate: dayjs().add(i, 'day').format('YYYY-MM-DD'), 24 | }); 25 | } 26 | return result; 27 | } 28 | 29 | const App = () => { 30 | const [data, setData] = useState(createData(20)); 31 | console.log('data', data); 32 | return ( 33 |
34 | 35 | data={data} 36 | columns={[ 37 | { 38 | name: 'name', 39 | label: '名称', 40 | width: 100, 41 | }, 42 | ]} 43 | onUpdate={async (row, startDate, endDate) => { 44 | console.log('update', row, startDate, endDate); 45 | setData((prev) => { 46 | const newList = [...prev]; 47 | const index = newList.findIndex((val) => val.id === row.id); 48 | newList[index] = { 49 | ...row, 50 | startDate: dayjs(startDate).format('YYYY-MM-DD'), 51 | endDate: dayjs(endDate).format('YYYY-MM-DD'), 52 | }; 53 | return newList; 54 | }); 55 | return true; 56 | }} 57 | /> 58 |
59 | ); 60 | }; 61 | 62 | export default App; 63 | -------------------------------------------------------------------------------- /website/demo/child.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import RcGantt from 'rc-gantt'; 7 | import React from 'react'; 8 | 9 | interface Data { 10 | name: string; 11 | startDate: string; 12 | endDate: string; 13 | } 14 | 15 | const node = { 16 | name: '一个名称一个名称一个名称一个名称', 17 | startDate: dayjs().format('YYYY-MM-DD'), 18 | endDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 19 | collapsed: true, 20 | }; 21 | 22 | const childList = [ 23 | { 24 | ...node, 25 | children: [{ ...node, children: [{ ...node }] }], 26 | }, 27 | { 28 | ...node, 29 | }, 30 | ]; 31 | 32 | const data = Array.from({ length: 100 }).fill({ 33 | ...node, 34 | children: childList, 35 | }) as Data[]; 36 | 37 | const onExpand = (record, collapsed) => { 38 | console.log('onExpand', record, collapsed); 39 | }; 40 | 41 | const App = () => ( 42 |
43 | 44 | data={data} 45 | onExpand={onExpand} 46 | columns={[ 47 | { 48 | name: 'name', 49 | label: '名称', 50 | }, 51 | ]} 52 | onUpdate={async () => true} 53 | /> 54 |
55 | ); 56 | 57 | export default App; 58 | -------------------------------------------------------------------------------- /website/demo/column.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import dayjs from 'dayjs'; 5 | import RcGantt from 'rc-gantt'; 6 | import React from 'react'; 7 | 8 | interface Data { 9 | name: string; 10 | startDate: string; 11 | endDate: string; 12 | } 13 | 14 | const data = Array.from({ length: 100 }).fill({ 15 | name: '一个名称一个名称一个名称一个名称', 16 | startDate: dayjs().format('YYYY-MM-DD'), 17 | endDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 18 | }) as Data[]; 19 | 20 | const App = () => ( 21 |
22 | 23 | data={data} 24 | columns={[ 25 | { 26 | name: 'name', 27 | label: '名称', 28 | width: 200, 29 | }, 30 | { 31 | name: 'startDate', 32 | label: '开始时间', 33 | }, 34 | { 35 | name: 'endDate', 36 | label: '结束时间', 37 | render: (record) => {record.endDate}, 38 | }, 39 | ]} 40 | tableIndent={0} 41 | onUpdate={async () => true} 42 | /> 43 |
44 | ); 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /website/demo/custom.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import RcGantt, { Gantt, GanttRef } from 'rc-gantt'; 7 | import React, { useRef, useState } from 'react'; 8 | 9 | const data = Array.from({ length: 100 }).fill({ 10 | name: '一个名称一个名称一个名称一个名称', 11 | startDate: dayjs().format('YYYY-MM-DD'), 12 | endDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 13 | }); 14 | 15 | const Button = ({ 16 | active, 17 | children, 18 | onClick, 19 | ...resetProps 20 | }: { 21 | active: boolean; 22 | children: React.ReactNode; 23 | onClick: React.MouseEventHandler; 24 | }) => ( 25 | 32 | ); 33 | 34 | const App = () => { 35 | const [val1, setVal1] = useState(true); 36 | 37 | const [val3, setVal3] = useState(false); 38 | 39 | const [val2, setVal2] = useState('day'); 40 | 41 | const ref = useRef(); 42 | 43 | const sightList: Gantt.Sight[] = [ 44 | 'day', 45 | 'halfYear', 46 | 'month', 47 | 'quarter', 48 | 'week', 49 | ]; 50 | 51 | const onBackToday = () => { 52 | if (ref && ref.current) ref.current.backToday(); 53 | }; 54 | 55 | return ( 56 |
57 |
58 | true} 70 | getBarColor={() => ({ 71 | backgroundColor: 'red', 72 | borderColor: 'yellow', 73 | })} 74 | hideTable={val3} 75 | alwaysShowTaskBar={val1} 76 | unit={val2} 77 | /> 78 | 79 |
80 |
81 | setVal1(e.target.checked)} 84 | type="checkbox" 85 | /> 86 | alwaysShowTaskBar 是否永远展示左右侧文案 87 |
88 |
89 | unit 当前视图 90 | {sightList.map((s: Gantt.Sight) => ( 91 | 94 | ))} 95 |
96 |
97 | unit 当前视图 98 | 101 |
102 |
103 | 106 |
107 |
108 |
109 |
110 | ); 111 | }; 112 | export default App; 113 | -------------------------------------------------------------------------------- /website/demo/demo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import RcGantt, { enUS } from 'rc-gantt'; 6 | 7 | const data = new Array(100).fill({ 8 | name: 'Title', 9 | startDate: '2021-07-10', 10 | endDate: '2021-07-12', 11 | collapsed: false, 12 | children: [ 13 | { 14 | startDate: '2021-07-10', 15 | endDate: '2021-07-12', 16 | name: 'TitleTitle', 17 | collapsed: false, 18 | content: '123123123', 19 | }, 20 | ], 21 | }); 22 | 23 | const App = () => { 24 | return ( 25 |
26 | { 39 | return true; 40 | }} 41 | /> 42 |
43 | ); 44 | }; 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /website/demo/dependence.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import RcGantt, { Gantt } from 'rc-gantt'; 7 | import React from 'react'; 8 | 9 | interface Data { 10 | name: string; 11 | startDate: string; 12 | endDate: string; 13 | } 14 | 15 | const data = [ 16 | { 17 | name: '一个名称一个名称一个名称一个名称', 18 | startDate: dayjs().format('YYYY-MM-DD'), 19 | endDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 20 | id: '1', 21 | }, 22 | { 23 | name: '一个名称一个名称一个名称一个名称', 24 | startDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 25 | endDate: dayjs().add(2, 'week').format('YYYY-MM-DD'), 26 | id: '2', 27 | }, 28 | { 29 | name: '一个名称一个名称一个名称一个名称', 30 | startDate: dayjs().add(2, 'week').format('YYYY-MM-DD'), 31 | endDate: dayjs().add(3, 'week').format('YYYY-MM-DD'), 32 | id: '3', 33 | }, 34 | ]; 35 | 36 | const dependencies: Gantt.Dependence[] = [ 37 | { 38 | from: '1', 39 | to: '2', 40 | type: 'finish_start', 41 | color: 'blue', 42 | }, 43 | { 44 | from: '2', 45 | to: '3', 46 | type: 'finish_start', 47 | }, 48 | ]; 49 | 50 | const App = () => ( 51 |
52 | 53 | dependencies={dependencies} 54 | data={data} 55 | columns={[ 56 | { 57 | name: 'name', 58 | label: '名称', 59 | }, 60 | ]} 61 | onUpdate={async () => true} 62 | /> 63 |
64 | ); 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /website/demo/filterUnit.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import type { Gantt } from 'rc-gantt'; 7 | import RcGantt, { EGanttSightValues } from 'rc-gantt'; 8 | import React from 'react'; 9 | 10 | interface Data { 11 | name: string; 12 | startDate: string; 13 | endDate: string; 14 | } 15 | 16 | const data = Array.from({ length: 100 }).fill({ 17 | name: '一个名称一个名称一个名称一个名称', 18 | startDate: dayjs().format('YYYY-MM-DD'), 19 | endDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 20 | }) as Data[]; 21 | 22 | const customSights: Gantt.SightConfig[] = [ 23 | { 24 | label: '自定义日', 25 | value: EGanttSightValues.day, 26 | type: 'day', 27 | }, 28 | { 29 | label: '自定义周', 30 | value: EGanttSightValues.week, 31 | type: 'week', 32 | }, 33 | ]; 34 | 35 | const App = () => ( 36 |
37 | 38 | data={data} 39 | columns={[ 40 | { 41 | name: 'name', 42 | label: '名称', 43 | width: 100, 44 | }, 45 | ]} 46 | customSights={customSights} 47 | onUpdate={async (row, startDate, endDate) => { 48 | return true; 49 | }} 50 | /> 51 |
52 | ); 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /website/demo/render.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | 5 | import dayjs from 'dayjs'; 6 | import RcGantt from 'rc-gantt'; 7 | import React from 'react'; 8 | 9 | const data = Array.from({ length: 100 }).fill({ 10 | name: '一个名称一个名称一个名称一个名称', 11 | startDate: dayjs().format('YYYY-MM-DD'), 12 | endDate: dayjs().add(1, 'week').format('YYYY-MM-DD'), 13 | }); 14 | 15 | const App = () => ( 16 |
17 | 左侧自定义渲染} 28 | renderRightText={() => 左侧自定义渲染} 29 | onUpdate={async () => true} 30 | getBarColor={() => ({ 31 | backgroundColor: 'red', 32 | borderColor: 'yellow', 33 | })} 34 | renderBar={(barInfo, { width, height }) => ( 35 |
36 | renderBar{barInfo.label} 37 |
38 | )} 39 | /> 40 |
41 | ); 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /website/en/_meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "Component Examples", 4 | "link": "/component", 5 | "activeMatch": "/component/" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /website/en/component.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Component Examples 3 | --- 4 | 5 | ## Component Examples 6 | 7 | ### Basic Component 8 | 9 | use `data` `columns` `onUpdate` config, Show a basic Gantt chart 10 | 11 | 12 | 13 | ### Add Task Bar 14 | 15 | click to create task`bar` 16 | 17 | 18 | 19 | ### Multi-level structure 20 | 21 | Ensure that each node contains the `children` attribute to achieve a multi-level structure 22 | 23 | You can get the current expanded status with `onExpand` 24 | 25 | 26 | 27 | ### Customizing table columns 28 | 29 | `columns` type definition see Type Definition 30 | 31 | where if each column is configured with the `width` property. The total width is calculated internally by the component. The default initialized table width is the total width 32 | 33 | 34 | 35 | ### Dependency Structure 36 | 37 | 38 | 39 | ### Custom Rendering 40 | 41 | 42 | 43 | ### Customized filtering 44 | 45 | Default current date filtering support Day, Week, Month, Quarter, Year. Support incoming configuration to customize the filtering dimension 46 | 47 | 48 | 49 | ### Advanced Usage 50 | 51 | Introducing the use of the built-in methods on `alwaysShowTaskBar`, `unit` and `innerRef`. 52 | 53 | 54 | 55 | ## Type Definition 56 | 57 | ### `Column` Definition 58 | 59 | ```typescript 60 | export type ColumnAlign = 'center' | 'right' | 'left' 61 | export interface Column { 62 | width?: number 63 | minWidth?: number 64 | maxWidth?: number 65 | flex?: number 66 | name: string 67 | label: string 68 | style?: Object 69 | render?: (item: Record) => React.ReactNode 70 | align?: ColumnAlign 71 | } 72 | ``` 73 | 74 | ### `data` Definition 75 | 76 | The following fields are built in, and special treatment will be done if the data contains the following attributes 77 | 78 | ```typescript 79 | export type Record = RecordType & { 80 | group?: boolean 81 | borderColor?: string 82 | backgroundColor?: string 83 | collapsed?: boolean 84 | children?: Record[] 85 | disabled?: boolean 86 | } 87 | ``` 88 | 89 | ### `Dependence` Definition 90 | 91 | ```typescript 92 | export type DependenceType = 'start_finish' | 'finish_start' | 'start_start' | 'finish_finish' 93 | export interface Dependence { 94 | from: string 95 | to: string 96 | type: DependenceType 97 | } 98 | ``` 99 | 100 | ### `Bar` Definition 101 | 102 | When we need to use some custom functions, we will be returned the following type of data, where `record` is the source data 103 | 104 | ```typescript 105 | export interface Bar { 106 | key: React.Key 107 | label: string 108 | width: number 109 | translateX: number 110 | translateY: number 111 | stepGesture: string 112 | invalidDateRange: boolean 113 | dateTextFormat: (startX: number) => string 114 | getDateWidth: (startX: number, endX: number) => string 115 | task: Item 116 | record: Record 117 | loading: boolean 118 | _group?: boolean 119 | _collapsed: boolean 120 | _depth: number 121 | _index?: number 122 | _childrenCount: number 123 | _parent?: Item 124 | } 125 | ``` 126 | 127 | ### `Sight` Definition 128 | 129 | ```typescript 130 | export type Sight = 'day' | 'week' | 'month' | 'quarter' | 'halfYear' 131 | ``` 132 | 133 | ## API 134 | 135 | | 参数 | 说明 | 类型 | 默认值 | 136 | | --- | --- | --- | --- | 137 | | data | Data | `Gantt.Record[]` | | 138 | | columns | Data columns | `Gantt.Column[]` | 139 | | dependencies | Dependencies | `Gantt.Dependence[]` | `[]` | 140 | | onUpdate | Update callback | `(record: Gantt.Record, startDate: string, endDate: string) => Promise` | 141 | | startDateKey | Start date key | `string` | `startDate` | 142 | | endDateKey | End date key | `string` | `startDate` | 143 | | isRestDay | Returns whether it is a holiday | `(date: string) => boolean` | | 144 | | unit | Current view | `Gantt.Sight` | | 145 | | rowHeight | Line height | `number` | 146 | | columnWidth | Default column width | `number` | 147 | | getBarColor | Returns to default bar style | `(record: Gantt.Record) => {backgroundColor: string;borderColor: string}` | 148 | | showBackToday | Show `Back to Today` button | `boolean` | 149 | | showUnitSwitch | Shows the unit switcher | `boolean` | 150 | | onRow | Row events | `{onClick: (record: Gantt.Record) => void}` | 151 | | tableIndent | Table indentation | `number` | `30` | 152 | | expandIcon | Expand child node icon | `` | 153 | | renderBar | Custom bar rendering | `renderBar?: (barInfo: Gantt.Bar, { width, height }: { width: number; height: number }) => React.ReactNode` | 154 | | renderGroupBar | Custom group bar render | | 155 | | renderInvalidBar | Custom invalid bar render | | 156 | | renderBarThumb | Custom bar thumb render | | 157 | | onBarClick | On bar click callback | `(record: Gantt.Record) => void` | 158 | | alwaysShowTaskBar | Whether to display the left and right side contents | `boolean` | `true` | 159 | | disabled | Whether to disable the chart | `boolean` | `false` | 160 | | renderLeftText | Custom rendering of the left content area | `(barInfo: Gantt.Bar) => React.ReactNode` | 161 | | renderRightText | Custom rendering of the right content area | `(barInfo: Gantt.Bar) => React.ReactNode` | 162 | | onExpand | On expand callback | `(record: Gantt.Record,collapsed:boolean) => void` | 163 | 164 | ## Methods 165 | 166 | has `innerRef` 167 | 168 | | 参数 | 说明 | 类型 | 默认值 | 169 | | -------------- | -------------- | ---------- | ------ | 170 | | backToday | backToday | `Function` | 171 | | getWidthByDate | getWidthByDate | `Function` | 172 | -------------------------------------------------------------------------------- /website/en/index.mdx: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## 📦 Install dependencies 4 | 5 | import { PackageManagerTabs } from '@theme' 6 | 7 | 8 | 9 | ## 🔨 Demo 10 | 11 | ```tsx pure 12 | import RcGantt, { enUS } from 'rc-gantt' 13 | 14 | const data = new Array(100).fill({ 15 | name: 'Title', 16 | startDate: '2021-07-10', 17 | endDate: '2021-07-12', 18 | collapsed: false, 19 | children: [ 20 | { 21 | startDate: '2021-07-10', 22 | endDate: '2021-07-12', 23 | name: 'TitleTitle', 24 | collapsed: false, 25 | content: '123123123', 26 | }, 27 | ], 28 | }) 29 | 30 | const App = () => { 31 | return ( 32 |
33 | { 46 | return true 47 | }} 48 | /> 49 |
50 | ) 51 | } 52 | 53 | ReactDOM.render(, document.getElementById('root')) 54 | ``` 55 | 56 | 57 | 58 | ## Feedback 59 | 60 | Please visit [Github](https://github.com/ahwgs/react-gantt/issues) 61 | -------------------------------------------------------------------------------- /website/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'React Gantt Component' 3 | hero: 4 | title: 'rc-gantt' 5 | description: React Gantt Component 6 | actions: 7 | - text: Quick Start → 8 | link: /en-US/component 9 | 10 | - text: GitHub 11 | link: https://github.com/ahwgs/react-gantt 12 | --- 13 | 14 | ### 🐯 Infos 15 | 16 | [![NPM version][npm-badge]][npm-url] 17 | [![NPM downloads][npm-downloads]][npm-url] 18 | 19 | 20 | [npm-badge]: https://img.shields.io/npm/v/rc-gantt.svg?style=flat 21 | [npm-url]: https://www.npmjs.com/package/rc-gantt 22 | [npm-downloads]: http://img.shields.io/npm/dm/rc-gantt.svg?style=flat 23 | 24 | 25 | ### 📦 Installation 26 | 27 | ```bash [pnpm] 28 | pnpm add rc-gantt # yarn add rc-gantt 29 | ``` 30 | 31 | ### 🔨 Getting Started 32 | 33 | 34 | ### 👬 Feedback 35 | 36 | Please visit [Github](https://github.com/ahwgs/react-gantt/issues) 37 | -------------------------------------------------------------------------------- /website/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'React Gantt Component' 3 | hero: 4 | title: 'rc-gantt' 5 | description: React 甘特图组件 6 | actions: 7 | - text: 快速上手 → 8 | link: /component 9 | - text: GitHub 10 | link: https://github.com/ahwgs/react-gantt 11 | --- 12 | 13 | ### 🐯 使用情况 14 | 15 | [![NPM version][npm-badge]][npm-url] 16 | [![NPM downloads][npm-downloads]][npm-url] 17 | 18 | 19 | [npm-badge]: https://img.shields.io/npm/v/rc-gantt.svg?style=flat 20 | [npm-url]: https://www.npmjs.com/package/rc-gantt 21 | [npm-downloads]: http://img.shields.io/npm/dm/rc-gantt.svg?style=flat 22 | 23 | 24 | ### 📦 安装 25 | 26 | ```bash [pnpm] 27 | pnpm add rc-gantt # yarn add rc-gantt 28 | ``` 29 | 30 | ### 🔨 使用 31 | 32 | 33 | 查看更多使用方式:请看 [基础使用](/component#基础使用) 34 | 35 | ### 👬 问题反馈 36 | 37 | 请访问 [Github](https://github.com/ahwgs/react-gantt/issues) 或加微信,备注 `rc-gantt` 38 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "esModuleInterop": true, 6 | "paths": { 7 | "rc-gantt": ["../src/index.tsx"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /website/zh/_meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "组件示例", 4 | "link": "/component", 5 | "activeMatch": "/component/" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /website/zh/component.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 组件示例 3 | --- 4 | 5 | ## 组件 6 | 7 | ### 基础使用 8 | 9 | 只需要配置 `data` `columns` `onUpdate` 即可展示一个最简甘特图 10 | 11 | 12 | 13 | ### 新增任务 14 | 15 | 点击即可新建 `bar` 16 | 17 | 18 | 19 | ### 多级结构 20 | 21 | 确保每个节点中包含 `children` 属性,即可实现多级结构 22 | 23 | 可以通过 `onExpand` 获取当前展开的状态 24 | 25 | 26 | 27 | ### 自定义表格列 28 | 29 | `columns` 类型定义见类型定义 30 | 31 | 其中,如果每列都配置 `width` 属性。组件内部会计算总宽度。默认初始化表格宽度为总宽度 32 | 33 | 34 | 35 | ### 依赖结构 36 | 37 | 38 | 39 | ### 自定义渲染 40 | 41 | 42 | 43 | ### 自定义筛选 44 | 45 | 默认当前日期筛选支持 日、周、月、季、年。支持传入配置,自定义筛选维度 46 | 47 | 48 | 49 | ### 高级用法 50 | 51 | 主要介绍 `alwaysShowTaskBar` `unit` 以及 `innerRef` 上内置方法的使用 52 | 53 | 54 | 55 | ## 类型定义 56 | 57 | ### `Column` 定义 58 | 59 | ```typescript 60 | export type ColumnAlign = 'center' | 'right' | 'left' 61 | export interface Column { 62 | width?: number 63 | minWidth?: number 64 | maxWidth?: number 65 | flex?: number 66 | name: string 67 | label: string 68 | style?: Object 69 | render?: (item: Record) => React.ReactNode 70 | align?: ColumnAlign 71 | } 72 | ``` 73 | 74 | ### `data` 定义 75 | 76 | 其中内置了如下几个字段,如果数据中包含如下属性会做特殊处理 77 | 78 | ```typescript 79 | export type Record = RecordType & { 80 | group?: boolean 81 | borderColor?: string 82 | backgroundColor?: string 83 | collapsed?: boolean 84 | children?: Record[] 85 | disabled?: boolean 86 | } 87 | ``` 88 | 89 | ### `Dependence` 定义 90 | 91 | ```typescript 92 | export type DependenceType = 'start_finish' | 'finish_start' | 'start_start' | 'finish_finish' 93 | export interface Dependence { 94 | from: string 95 | to: string 96 | type: DependenceType 97 | } 98 | ``` 99 | 100 | ### `Bar` 定义 101 | 102 | 在我们需要使用一些自定义函数时,会给我们返回如下类型数据,其中 `record` 为源数据 103 | 104 | ```typescript 105 | export interface Bar { 106 | key: React.Key 107 | label: string 108 | width: number 109 | translateX: number 110 | translateY: number 111 | stepGesture: string 112 | invalidDateRange: boolean 113 | dateTextFormat: (startX: number) => string 114 | getDateWidth: (startX: number, endX: number) => string 115 | task: Item 116 | record: Record 117 | loading: boolean 118 | _group?: boolean 119 | _collapsed: boolean 120 | _depth: number 121 | _index?: number 122 | _childrenCount: number 123 | _parent?: Item 124 | } 125 | ``` 126 | 127 | ### `Sight` 定义 128 | 129 | ```typescript 130 | export type Sight = 'day' | 'week' | 'month' | 'quarter' | 'halfYear' 131 | ``` 132 | 133 | ## API 134 | 135 | | 参数 | 说明 | 类型 | 默认值 | 136 | | --- | --- | --- | --- | 137 | | data | 数据源 | `Gantt.Record[]` | | 138 | | columns | 数据列 | `Gantt.Column[]` | 139 | | dependencies | 依赖数组 | `Gantt.Dependence[]` | `[]` | 140 | | onUpdate | 更新回调 | `(record: Gantt.Record, startDate: string, endDate: string) => Promise` | 141 | | startDateKey | 开始时间属性 key | `string` | `startDate` | 142 | | endDateKey | 结束时间属性 key | `string` | `startDate` | 143 | | isRestDay | 返回是否是节假日 | `(date: string) => boolean` | | 144 | | unit | 当前视图 | `Gantt.Sight` | | 145 | | rowHeight | 行高 | `number` | 146 | | columnWidth | 列默认宽度 | `number` | 147 | | getBarColor | 返回默认条样式 | `(record: Gantt.Record) => {backgroundColor: string;borderColor: string}` | 148 | | showBackToday | 展示返回今日 | `boolean` | 149 | | showUnitSwitch | 展示视图切换 | `boolean` | 150 | | onRow | 行事件 | `{onClick: (record: Gantt.Record) => void}` | 151 | | tableIndent | 表格缩进 | `number` | `30` | 152 | | expandIcon | 展开子节点图表 | `` | 153 | | renderBar | 自定义渲染 bar | `renderBar?: (barInfo: Gantt.Bar, { width, height }: { width: number; height: number }) => React.ReactNode` | 154 | | renderGroupBar | 自定义渲染组 | | 155 | | renderInvalidBar | 自定义渲染拖拽 | | 156 | | renderBarThumb | 自定义缩略渲染 | | 157 | | onBarClick | 行点击事件 | `(record: Gantt.Record) => void` | 158 | | alwaysShowTaskBar | 是否展示左右侧内容 | `boolean` | `true` | 159 | | disabled | 是否禁用图表 | `boolean` | `false` | 160 | | renderLeftText | 自定义渲染左侧内容区 | `(barInfo: Gantt.Bar) => React.ReactNode` | 161 | | renderRightText | 自定义渲染右侧内容区 | `(barInfo: Gantt.Bar) => React.ReactNode` | 162 | | onExpand | 点击展开图标时触发 | `(record: Gantt.Record,collapsed:boolean) => void` | 163 | 164 | ## 方法 165 | 166 | 对外抛出 `innerRef` 167 | 168 | | 参数 | 说明 | 类型 | 默认值 | 169 | | -------------- | -------- | ---------- | ------ | 170 | | backToday | 返回今日 | `Function` | 171 | | getWidthByDate | 返回事件 | `Function` | 172 | -------------------------------------------------------------------------------- /website/zh/index.mdx: -------------------------------------------------------------------------------- 1 | # rc-gantt 甘特图组件 2 | 3 | ## 📦 安装依赖 4 | 5 | import { PackageManagerTabs } from '@theme' 6 | 7 | 8 | 9 | ## 🔨 快速开始 10 | 11 | ```tsx pure 12 | import RcGantt, { enUS } from 'rc-gantt' 13 | 14 | const data = new Array(100).fill({ 15 | name: 'Title', 16 | startDate: '2021-07-10', 17 | endDate: '2021-07-12', 18 | collapsed: false, 19 | children: [ 20 | { 21 | startDate: '2021-07-10', 22 | endDate: '2021-07-12', 23 | name: 'TitleTitle', 24 | collapsed: false, 25 | content: '123123123', 26 | }, 27 | ], 28 | }) 29 | 30 | const App = () => { 31 | return ( 32 |
33 | { 46 | return true 47 | }} 48 | /> 49 |
50 | ) 51 | } 52 | 53 | ReactDOM.render(, document.getElementById('root')) 54 | ``` 55 | 56 | 57 | 58 | 查看更多:[基础使用](/component#基础使用) 59 | 60 | ## 问题反馈 61 | 62 | 请访问 [Github](https://github.com/ahwgs/react-gantt/issues) 或加微信,备注 `rc-gantt` 63 | --------------------------------------------------------------------------------