├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── example ├── imgs │ ├── 2FB3B186-4C3D-4c51-B76C-36111C3BF908.png │ ├── 5132BB7D-C48F-4bab-848B-2F33767F1135.png │ ├── 68F5807B-07A0-4a8c-B096-48DA0AEC71BF.png │ ├── 9001E442-B21F-4dbf-ABAD-D5C1988FA13A.png │ └── 在线excel.drawio └── src │ ├── App.tsx │ ├── css │ └── styles.css │ ├── favicon.ico │ ├── imgs │ └── 2FB3B186-4C3D-4c51-B76C-36111C3BF908.png │ ├── index.html │ └── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Grid.tsx ├── components │ ├── cell │ │ ├── Cell.tsx │ │ ├── CellOverlay.tsx │ │ ├── DraggableRect.tsx │ │ ├── HeaderCell.tsx │ │ ├── LeftCell.tsx │ │ ├── NormalCell.tsx │ │ ├── SingleCell.tsx │ │ └── components │ │ │ ├── NImage.tsx │ │ │ └── NText.tsx │ ├── layer │ │ ├── contextMenuArea │ │ │ ├── ContextMenuLayer.tsx │ │ │ ├── imgs │ │ │ │ ├── insert1.png │ │ │ │ ├── insert2.png │ │ │ │ ├── insert3.png │ │ │ │ └── insert4.png │ │ │ └── styles.module.css │ │ ├── cornerArea │ │ │ ├── CornerArea.tsx │ │ │ └── styles.module.css │ │ ├── editArea │ │ │ ├── EditAreaLayer.tsx │ │ │ └── styles.module.css │ │ ├── scrollArea │ │ │ ├── ScrollArea.tsx │ │ │ └── styles.module.css │ │ ├── selectArea │ │ │ ├── SelectAreaLayer.tsx │ │ │ ├── components │ │ │ │ ├── copyArea │ │ │ │ │ ├── CopyArea.tsx │ │ │ │ │ └── styles.module.css │ │ │ │ └── selectFill │ │ │ │ │ ├── SelectFill.tsx │ │ │ │ │ └── styles.module.css │ │ │ ├── imgs │ │ │ │ ├── cur.png │ │ │ │ └── img-full.svg │ │ │ └── styles.module.css │ │ └── singleArea │ │ │ ├── SingleArea.tsx │ │ │ └── styles.module.css │ └── toolbar │ │ ├── ToolBar.tsx │ │ ├── components │ │ ├── ColorPanel.tsx │ │ ├── FloatImage.tsx │ │ └── styles.module.css │ │ ├── imgs │ │ ├── align-center.png │ │ ├── align-left.png │ │ ├── align-right.png │ │ ├── back.png │ │ ├── border-all.png │ │ ├── border-color.png │ │ ├── border-none.png │ │ ├── border-style.png │ │ ├── clear.png │ │ ├── cloud-upload.png │ │ ├── computer.png │ │ ├── export-image.png │ │ ├── file-export.png │ │ ├── float-image.png │ │ ├── front.png │ │ ├── image.png │ │ ├── insert-img.png │ │ ├── merge-cells.png │ │ ├── paint-fill.png │ │ ├── split-cells.png │ │ ├── text-color.png │ │ ├── text-fill.png │ │ ├── text-italic.png │ │ ├── triangle-down.png │ │ ├── underline.png │ │ ├── vertical-align-botto.png │ │ ├── vertical-align-middl.png │ │ ├── vertical-align-top.png │ │ ├── xlsx.png │ │ ├── 前进-实.png │ │ ├── 导出文件.png │ │ └── 撤销.png │ │ └── styles.module.css ├── hooks │ ├── useImage.ts │ ├── useMouseEvent.ts │ └── useSize.ts ├── index.ts ├── stores │ ├── CellStore.ts │ ├── CopyStore.ts │ ├── FloatImageStore.ts │ ├── MouseEventStore.ts │ └── ToolBarStore.ts └── utils │ ├── constants.ts │ └── index.ts ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | public 6 | # don't lint nyc coverage output 7 | coverage -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": ["airbnb-typescript", "plugin:prettier/recommended"], 6 | "env": { 7 | "jest": true 8 | }, 9 | "rules": { 10 | // Make prettier code formatting suggestions more verbose. 11 | "prettier/prettier": ["warn"], 12 | // Disable => <> replacement. Feel free to change 13 | "react/jsx-fragments": "off", 14 | // Disable prefer default export 15 | "import/prefer-default-export": "off", 16 | "@typescript-eslint/object-curly-spacing": "off", 17 | "react/jsx-filename-extension": [0], 18 | "import/extensions": "off", 19 | "import/no-extraneous-dependencies": "off", 20 | "@typescript-eslint/no-unused-vars":"off" 21 | }, 22 | "overrides": [ 23 | { 24 | "files": ["*.ts", "*.tsx"], 25 | "parserOptions": { 26 | "ecmaVersion": 12, 27 | "project": ["./tsconfig.json"] 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 4 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 'true' -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexander Tarasov 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 |
3 | 4 |
5 | 6 | # 【Simple Sheet】一款高性能的在线表格EXCEL文档系统 7 | [![React](https://img.shields.io/badge/React-18.0.0-brightgreen)](https://reactjs.org/) 8 | [![Mobx](https://img.shields.io/badge/Mobx-5.9.0-brightgreen)](https://mobx.js.org/react-integration.html) 9 | [![React Konva](https://img.shields.io/badge/React%20Konva-18.3.2-brightgreen)](https://konvajs.org/docs/react/index.html) 10 | 11 | ## 功能和特性 12 | 13 | * 高性能(使用canvas进行渲染) 14 | * 可定制化 15 | * 支持行、列宽度高度、自动筛选视图、单元格样式和格式设置等 16 | * 计算公式 17 | 18 | [体验地址](https://www.nihaoshijie.com.cn/mypro/simple-sheet/index.html)。 19 | 20 | 21 | 22 | ## 技术栈 23 | 24 | 25 | React.js+Mobx+TypeScript 26 | 27 | 相关博文:[前端在线文档技术解析](https://juejin.cn/post/7125360490347397127) 28 | 29 | 30 | ## 展示效果 31 | 32 | ![](https://github.com/lvming6816077/simple-sheet/blob/main/example/imgs/5132BB7D-C48F-4bab-848B-2F33767F1135.png) 33 | 34 | 35 | ## 交流和建议 36 | 37 | 目前来说该项目只是作为一个开源项目并且只有我个人在业余时间开发,所以很多功能(实时协作,后端服务,复杂公式等等)都没有完善,其实目的是为了给一些对 web sheet 感兴趣的同学入门,当然有兴趣的同学也可以深入进来一起参与,感谢支持! 38 | 39 | 相关建议+共同开发联系:*441403517@qq.com* 40 | 41 | ## License 42 | 43 | MIT 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/imgs/2FB3B186-4C3D-4c51-B76C-36111C3BF908.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/example/imgs/2FB3B186-4C3D-4c51-B76C-36111C3BF908.png -------------------------------------------------------------------------------- /example/imgs/5132BB7D-C48F-4bab-848B-2F33767F1135.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/example/imgs/5132BB7D-C48F-4bab-848B-2F33767F1135.png -------------------------------------------------------------------------------- /example/imgs/68F5807B-07A0-4a8c-B096-48DA0AEC71BF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/example/imgs/68F5807B-07A0-4a8c-B096-48DA0AEC71BF.png -------------------------------------------------------------------------------- /example/imgs/9001E442-B21F-4dbf-ABAD-D5C1988FA13A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/example/imgs/9001E442-B21F-4dbf-ABAD-D5C1988FA13A.png -------------------------------------------------------------------------------- /example/imgs/在线excel.drawio: -------------------------------------------------------------------------------- 1 | 5VnLctowFP0aLZuxbPzQ0ganXaQz6WTRZqlixXYqLEcIMP36SrKM8YOBvMg0MMwgXUlX8jn3Hl8NwJkuqq8cl9l3lhAKbCupgDMDtg1tC8kfZdnWlsD2akPK88RMag13+V9ijJaxrvKELDsTBWNU5GXXOGdFQeaiY8Ocs0132gOj3V1LnJKB4W6O6dD6M09E1jyF39q/kTzNmp2hZx54gZvJ5kmWGU7YZs/kxMCZcsZE3VpUU0IVeA0u9brrA6O7g3FSiFMWhDfr0qru1+sb+rT6AW37hj9+sWsva0xX5oHNYcW2QUCeu1RNuROmlFCWcrwATlQSni+IILw/dtsORJssF+SuxHPlYSNDRNoysaCyB2XzIa9IQ7rqm9MQLkh18DHhDjwZdYTJrfhWTjELbMfgbQLOtkx/09IHJ8aW7VPnGyM2IZPufLeoyoYB9hkgBwNMSSKDzHQZFxlLWYFp3FojzlZFQpRXS/baOTeMlQasRyLE1oCHV4J1oU3wMtPrW1zVrs9FlROKRb7urhtDyCy9Zbn02LKB/A4b0O6BLDBPiTCr9qP3iKOdQDSOlmzF52TgSBO2e56Xc+h8vkSZWOOIflyiTD4fyG4PZPvDQXaPg0yKJFTvTtmbU7xc5vMuUF1xIlUufqn2FZy4pn+v+xY03Vm1N3e23evs0aNtL5IteXid/cdedEOa9mhwR1hobK9UQeh1o2DH+BHxGqpgL5yC09T0rUTQG4TOFBdrubntUYlY9JvLVqpaIHZBMAPI1Q35dUDsgdAC0VRZIgiCYdjJNBPdQFsKzv6QKaNMxUfBCqLTlNKeCdM8LVS0yrjQma6SNpdVXGgGFnmS6PfqmAR0w/kdapDJMOm9kWhz3ivl0YC3GVuMkYaAjKgAKa6CCIRIszcFUaga6Fp9L4A9bzzJ9tgLzsle816+xPrRcVBXOd0X1o99R3a/EH3n+hHCU3PQVxqJPBAHIIoBgjr1EAiDC0g9x+q9KEdq0vPm3ie8IPfkbQfex5Wk0LlghUO9kPdeqnDoAK/nUrjh7e2AwnkgigDSNUUQamHzAJqBEF6CwrlOl+6R7Duvwo3dB2vOHpiOr5YG72nFmoEvS51XoZwA/bLS6DTjDdVX8tM4k4er/Zko+N9V1O9l20iNf24VHV7PhrL6rJt9RyEVew2G/qsU8zyX7kmvlnBO1MOhI/eIo3e+dUN/yGt9q45klTjRYqrLxcAHoVbVMNbyKoeQvs19elUdUNTkwturquy2/9nUFLf/fDnxPw== -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ElementType, ReactNode, useMemo, useRef } from 'react' 2 | 3 | import SimpleSheet from '../../src/index'; 4 | // import SimpleSheet from '../../dist/index.esm.js'; 5 | 6 | import './css/styles.css' 7 | const App: React.FC = () => { 8 | const simpleSheetRef = useRef(null) 9 | 10 | var isChrome = navigator.userAgent.indexOf('Chrome') > -1 // 是否是谷歌 11 | 12 | 13 | 14 | return ( 15 | <> 16 | {/* */} 20 |
21 | Simple Sheet 22 | GitHub 23 |
24 |
Simple Sheet
25 |
26 | 27 | 28 | {!isChrome ?
29 | 请使用Chrome浏览器体验 30 |
: null} 31 | 32 |
33 |
34 |
35 |

特性和功能:

36 |
    37 |
  • 高性能(使用canvas进行渲染)
  • 38 |
  • 可定制化
  • 39 |
  • 支持行、列宽度高度、自动筛选视图、单元格样式和格式设置等
  • 40 |
  • 计算公式
  • 41 |
42 |
开始
43 |
44 |
45 | 46 |
Powered by lvming6816077
47 | 48 | ) 49 | } 50 | 51 | export default App 52 | -------------------------------------------------------------------------------- /example/src/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | .nav-wrapper { 11 | background-color: #00acc1 !important; 12 | height: 64px; 13 | line-height: 64px; 14 | box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 12%), 0 1px 5px 0 rgb(0 0 0 / 20%); 15 | } 16 | .title { 17 | position: absolute; 18 | color: #fff; 19 | display: inline-block; 20 | font-size: 2.1rem; 21 | padding: 0; 22 | margin-left: 20px; 23 | } 24 | .sub-title { 25 | width: 864px; 26 | margin: 0 auto; 27 | margin-top:100px; 28 | margin-bottom:20px; 29 | font-size: 4.2rem; 30 | line-height: 110%; 31 | color:#3482f6; 32 | font-family: system-ui; 33 | } 34 | .desc { 35 | 36 | margin: 0 auto; 37 | margin-top:100px; 38 | 39 | width: 864px; 40 | } 41 | .desc-inner { 42 | color:#fff; 43 | width: 400px; 44 | padding: 30px; 45 | padding-bottom: 50px; 46 | background-color: #546e7a !important; 47 | box-shadow:0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 12%), 0 1px 5px 0 rgb(0 0 0 / 20%); 48 | } 49 | .desc-inner h4 { 50 | margin-top: -8px; 51 | } 52 | .desc-inner li { 53 | margin-top:8px; 54 | } 55 | .btn { 56 | height: 36px; 57 | width: 110px; 58 | background-color: rgb(33, 150, 243); 59 | text-align: center; 60 | border-radius: 3px; 61 | cursor: pointer; 62 | line-height: 34px; 63 | float: right; 64 | } 65 | .git { 66 | float: right; 67 | margin-right: 14px; 68 | color: #fff; 69 | text-decoration: none; 70 | } 71 | .title-logo { 72 | width: 73px; 73 | height: 73px; 74 | display: inline-block; 75 | background-size: cover; 76 | margin-right: 10px; 77 | vertical-align: -5px; 78 | /* background-image: url(../imgs/sheet_5.png); */ 79 | display: none; 80 | } 81 | .logo { 82 | width: 73px; 83 | height: 73px; 84 | display: inline-block; 85 | background-size: cover; 86 | margin-right: 10px; 87 | vertical-align: -5px; 88 | background-image: url(../imgs/2FB3B186-4C3D-4c51-B76C-36111C3BF908.png); 89 | } 90 | .power { 91 | position: absolute; 92 | bottom: 15px; 93 | left: 50%; 94 | font-size: 12px; 95 | color: #a2a2a2; 96 | } 97 | .power a{ 98 | text-decoration: none; 99 | } 100 | .wrapper { 101 | position: absolute; 102 | left: 0; 103 | right: 0; 104 | bottom: -71px; 105 | top: 0; 106 | text-align: center; 107 | line-height: 280px; 108 | z-index: 999; 109 | background-color: #dbdbdb5c; 110 | } -------------------------------------------------------------------------------- /example/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/example/src/favicon.ico -------------------------------------------------------------------------------- /example/src/imgs/2FB3B186-4C3D-4c51-B76C-36111C3BF908.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/example/src/imgs/2FB3B186-4C3D-4c51-B76C-36111C3BF908.png -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Simple Sheet 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from './App' 5 | 6 | 7 | const root = ReactDOM.createRoot(document.getElementById("root")); 8 | 9 | 10 | root.render(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-sheet", 3 | "version": "2.2.6", 4 | "description": "simple-sheet", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/types", 8 | "scripts": { 9 | "start": "cross-env NODE_ENV=development webpack serve --config webpack.config.js", 10 | "builddemo": "webpack --config webpack.config.js", 11 | "build": "rollup -c", 12 | "prepare": "npm run build", 13 | "lint": "eslint src --ext .js,.jsx,.ts,.tsx", 14 | "fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix", 15 | "prettier": "prettier src/**/*.{ts,tsx} --write" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/lvming6816077/simple-sheet" 20 | }, 21 | "author": "lvming6816077", 22 | "license": "MIT", 23 | "keywords": [ 24 | "react", 25 | "reactjs", 26 | "react-component", 27 | "konva", 28 | "mobx" 29 | ], 30 | "devDependencies": { 31 | "@rollup/plugin-typescript": "^8.3.2", 32 | "@types/css-modules": "^1.0.2", 33 | "@types/lodash": "^4.14.182", 34 | "@types/react": "^18.0.12", 35 | "@types/react-dom": "^18.0.5", 36 | "clean-webpack-plugin": "^4.0.0", 37 | "cross-env": "^7.0.3", 38 | "css-loader": "^6.7.1", 39 | "eslint": "^8.19.0", 40 | "eslint-config-airbnb-typescript": "^16.1.0", 41 | "eslint-config-prettier": "^8.0.0", 42 | "eslint-config-react-app": "^7.0.1", 43 | "eslint-plugin-import": "^2.22.0", 44 | "eslint-plugin-jsx-a11y": "^6.4.1", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "eslint-plugin-react": "^7.21.5", 47 | "eslint-plugin-react-hooks": "^4.0.8", 48 | "eslint-webpack-plugin": "^3.1.1", 49 | "postcss": "^8.4.14", 50 | "postcss-url": "^10.1.3", 51 | "react": "^18.1.0", 52 | "react-dom": "^18.1.0", 53 | "rollup": "^2.75.5", 54 | "rollup-plugin-postcss": "^4.0.2", 55 | "style-loader": "^3.3.1", 56 | "tslib": "^2.4.0", 57 | "typescript": "^4.7.3", 58 | "webpack": "^5.73.0", 59 | "webpack-cli": "^4.10.0" 60 | }, 61 | "peerDependencies": { 62 | "react": "^18.1.0", 63 | "react-dom": "^18.1.0" 64 | }, 65 | "dependencies": { 66 | "@szhsin/react-menu": "^3.1.1", 67 | "html-webpack-plugin": "^5.5.0", 68 | "konva": "^8.3.10", 69 | "lodash": "^4.17.21", 70 | "mobx": "^5.9.0", 71 | "mobx-react-lite": "^1.0.1", 72 | "prettier": "^2.0.0", 73 | "react-konva": "^18.2.1", 74 | "react-tooltip": "^4.2.21", 75 | "react-viewer": "^3.2.2", 76 | "ts-loader": "^9.3.1", 77 | "webpack-dev-server": "^4.9.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import postcss from "rollup-plugin-postcss"; 3 | const url = require('postcss-url'); 4 | 5 | const outputDefaults = { 6 | sourcemap: true, 7 | globals: { react: "React" }, 8 | }; 9 | 10 | export default { 11 | input: "./src/index.ts", 12 | 13 | output: [ 14 | { 15 | file: "./dist/index.cjs.js", 16 | format: "cjs", 17 | exports: "default", 18 | ...outputDefaults, 19 | }, 20 | { 21 | file: "./dist/index.esm.js", 22 | format: "esm", 23 | ...outputDefaults, 24 | }, 25 | ], 26 | 27 | plugins: [ 28 | typescript({ tsconfig: "./tsconfig.json" }), 29 | postcss({ 30 | autoModules: true , 31 | plugins:[ 32 | url({ 33 | url: "inline", // enable inline assets using base64 encoding 34 | maxSize: 30, // maximum file size to inline (in kilobytes) 35 | fallback: "copy", // fallback method to use if max size is exceeded 36 | }) 37 | ] 38 | }), 39 | ], 40 | 41 | external: ["react", "react-dom",/react\/jsx-runtime/,"mobx-react-lite","react-konva","lodash","mobx",/@szhsin\/*/,"react-tooltip"], 42 | }; 43 | -------------------------------------------------------------------------------- /src/Grid.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | KeyboardEventHandler, 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useImperativeHandle, 8 | useMemo, 9 | useRef, 10 | useState, 11 | } from 'react' 12 | // import styles from "./styles.module.css"; 13 | 14 | import { Stage, Layer, Group, Text, Rect } from 'react-konva' 15 | import Cell from '@/components/cell/Cell' 16 | import { KonvaEventObject } from 'konva/lib/Node' 17 | import SelectAreaLayer from '@/components/layer/selectArea/SelectAreaLayer' 18 | 19 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 20 | import { observer } from 'mobx-react-lite' 21 | import { toJS } from 'mobx' 22 | import EditAreaLayer from './components/layer/editArea/EditAreaLayer' 23 | import { 24 | CellAttrs, 25 | CellMap, 26 | CellStoreContext, 27 | MouseClick, 28 | } from './stores/CellStore' 29 | import _ from 'lodash' 30 | import ScrollArea from './components/layer/scrollArea/ScrollArea' 31 | import { generaCell, getScrollWidthAndHeight } from './utils' 32 | import ToolBar from './components/toolbar/ToolBar' 33 | 34 | import { 35 | headerCell, 36 | leftCell, 37 | normalCell, 38 | singleCell, 39 | rowStopIndex, 40 | columnStopIndex, 41 | containerWidth, 42 | containerHeight, 43 | initConstants, 44 | } from '@/utils/constants' 45 | import { CellOverlay } from './components/cell/CellOverlay' 46 | import CornerArea from './components/layer/cornerArea/CornerArea' 47 | import Konva from 'konva' 48 | import { ToolBarStoreContext } from './stores/ToolBarStore' 49 | import FloatImage from './components/toolbar/components/FloatImage' 50 | import { FloatImageStoreContext } from './stores/FloatImageStore' 51 | import ContextMenuLayer from './components/layer/contextMenuArea/ContextMenuLayer' 52 | import Viewer from 'react-viewer' 53 | import { useSize } from './hooks/useSize' 54 | import { CopyStoreContext } from './stores/CopyStore' 55 | import SingleArea from './components/layer/singleArea/SingleArea' 56 | 57 | export interface GridProps { 58 | width?: number 59 | height?: number 60 | onRef?: any 61 | initData?: CellMap 62 | } 63 | 64 | const Grid = observer( 65 | (props: GridProps, ref: any) => { 66 | initConstants(props) 67 | 68 | useImperativeHandle(ref, () => ({ 69 | getCellData, 70 | setCellData, 71 | stage: stageRef.current, 72 | })) 73 | const getCellData = () => { 74 | return toJS(cellsMap) 75 | } 76 | const setCellData = (map: CellMap) => { 77 | cellStore.cellsMap = map 78 | } 79 | const width = containerWidth 80 | const height = containerHeight 81 | 82 | const mouseEventStore = useContext(MouseEventStoreContext) 83 | const setDV = mouseEventStore.mouseDown 84 | const setUV = mouseEventStore.mouseUp 85 | const setMV = mouseEventStore.mouseMove 86 | const setDBC = mouseEventStore.mouseDBC 87 | const setRC = mouseEventStore.mouseRC 88 | 89 | const cellStore = useContext(CellStoreContext) 90 | const toolbarStore = useContext(ToolBarStoreContext) 91 | const floatImageStore = useContext(FloatImageStoreContext) 92 | const copyStore = useContext(CopyStoreContext) 93 | 94 | var cellsMap = cellStore.cellsMap 95 | 96 | if (props.initData) { 97 | cellsMap = generaCell( 98 | props.initData, 99 | cellStore.rowStopIndex, 100 | cellStore.columnStopIndex 101 | ) 102 | } 103 | 104 | const cells = _.values(cellsMap) 105 | 106 | const header = cells.filter((i) => i?.type == 'header') 107 | const left = cells.filter((i) => i?.type == 'left') 108 | const single = cells.filter((i) => i?.type == 'single') 109 | const normal = cells.filter((i) => i?.type == 'normal') 110 | const border = cells.filter((i) => i?.borderStyle) 111 | 112 | let { swidth, sheight } = useSize() 113 | const scrolRef = useRef(null) 114 | 115 | const onScroll = (e: any) => { 116 | mouseEventStore.scrollLeft = e.target.scrollLeft 117 | mouseEventStore.scrollTop = e.target.scrollTop 118 | } 119 | 120 | const wheelRef = useRef(null) 121 | 122 | const handleWheel = (event: any) => { 123 | event.preventDefault() 124 | const isHorizontally = event.shiftKey 125 | 126 | const { deltaX, deltaY, deltaMode } = event 127 | 128 | // console.log(sheight - containerHeight) 129 | 130 | mouseEventStore.scrollTop = Math.min( 131 | sheight - containerHeight - 3, 132 | Math.max(0, mouseEventStore.scrollTop + deltaY) 133 | ) 134 | } 135 | useEffect(() => { 136 | wheelRef.current?.addEventListener('wheel', handleWheel, { 137 | passive: false, 138 | }) 139 | 140 | return () => { 141 | wheelRef.current?.removeEventListener('wheel', handleWheel) 142 | } 143 | }) 144 | 145 | useEffect(() => { 146 | scrolRef.current!.scrollTop = mouseEventStore.scrollTop 147 | }, [mouseEventStore.scrollTop]) 148 | useEffect(() => { 149 | scrolRef.current!.scrollLeft = mouseEventStore.scrollLeft 150 | }, [mouseEventStore.scrollLeft]) 151 | 152 | // useEffect(()=>{ 153 | // console.log(toolbarStore.floatImage) 154 | // },[toolbarStore.floatImage]) 155 | 156 | const stageRef = useRef(null) 157 | 158 | const mouseEventProp = { 159 | onMouseUp: (e: KonvaEventObject) => 160 | setUV({ 161 | ...e.target.attrs, 162 | value: e.target.attrs.text, 163 | } as CellAttrs), 164 | onMouseMove: (e: KonvaEventObject) => { 165 | setMV({ 166 | ...e.target.attrs, 167 | value: e.target.attrs.text, 168 | } as CellAttrs) 169 | }, 170 | onMouseDown: (e: KonvaEventObject) => { 171 | // console.log(e.evt.button) 172 | // if (e.evt.button == 2) return // 鼠标左键 173 | setDV({ 174 | ...e.target.attrs, 175 | value: e.target.attrs.text, 176 | rightClick: e.evt.button == 2, 177 | } as MouseClick) 178 | }, 179 | } 180 | 181 | const onKeyDown = (event: React.KeyboardEvent) => { 182 | if (cellStore.editCell) return 183 | if (event.ctrlKey && event.keyCode === 67) { 184 | console.log('你按下了CTRL+C') 185 | copyStore.copyCurrentCells(cellStore) 186 | } 187 | if (event.ctrlKey && event.keyCode === 86) { 188 | console.log('你按下了CTRL+V') 189 | copyStore.pasteCurrentCells(cellStore) 190 | } 191 | 192 | if (event.ctrlKey && event.keyCode === 88) { 193 | console.log('你按下了CTRL+X') 194 | copyStore.cutCurrentCells(cellStore) 195 | } 196 | 197 | if (event.keyCode == 46 || event.keyCode == 8) { 198 | console.log('delete') 199 | 200 | copyStore.delCurrentCells(cellStore, floatImageStore) 201 | } 202 | } 203 | 204 | return ( 205 |
209 | 210 |
220 |
230 | 239 | ) => { 240 | e.evt.preventDefault() 241 | setRC({ 242 | clientX: e.evt.clientX, 243 | clientY: e.evt.clientY, 244 | ...e.target.attrs, 245 | value: e.target.attrs.text, 246 | }) 247 | }} 248 | > 249 | 250 | 256 | ) => { 257 | if (e.evt.button == 2) return // 鼠标左键 258 | setDBC({ 259 | ...e.target.attrs, 260 | value: e.target.attrs.text, 261 | } as CellAttrs) 262 | }} 263 | > 264 | {/* // 白色背景 */} 265 | 270 | {normal.map((o) => ( 271 | 272 | ))} 273 | {border.map((o) => ( 274 | 279 | ))} 280 | 281 | 285 | {floatImageStore.floatImage.map((o) => ( 286 | 290 | ))} 291 | 292 | 293 | 297 | {header.map((o) => ( 298 | 299 | ))} 300 | 301 | 305 | {left.map((o) => ( 306 | 307 | ))} 308 | 309 | {single.map((o) => ( 310 | 311 | ))} 312 | 313 | 314 |
325 | 326 | 327 |
328 |
329 |
342 | 346 |
347 |
360 | 361 | 362 | 363 | 364 |
365 |
366 | 0} 370 | onClose={() => { 371 | toolbarStore.currentBigImg = [] 372 | }} 373 | images={toolbarStore.currentBigImg} 374 | /> 375 |
376 | ) 377 | }, 378 | { forwardRef: true } 379 | ) 380 | 381 | export default Grid 382 | -------------------------------------------------------------------------------- /src/components/cell/Cell.tsx: -------------------------------------------------------------------------------- 1 | import { CellStoreContext } from '@/stores/CellStore' 2 | import { observer } from 'mobx-react-lite' 3 | import React, { 4 | CSSProperties, 5 | useCallback, 6 | useContext, 7 | useEffect, 8 | useRef, 9 | useState, 10 | } from 'react' 11 | 12 | import { Stage, Text, Group, Rect } from 'react-konva' 13 | import HeaderCell from './HeaderCell' 14 | import LeftCell from './LeftCell' 15 | import NormalCell from './NormalCell' 16 | import SingleCell from './SingleCell' 17 | 18 | interface IProps {} 19 | 20 | const Cell = React.memo((props: any) => { 21 | const { type = 'normal' } = props 22 | 23 | if (type == 'normal') return 24 | if (type == 'header') return 25 | if (type == 'left') return 26 | if (type == 'single') return 27 | return null 28 | }) 29 | 30 | export default Cell 31 | -------------------------------------------------------------------------------- /src/components/cell/CellOverlay.tsx: -------------------------------------------------------------------------------- 1 | import { CellStoreContext } from '@/stores/CellStore' 2 | import React, { memo, useContext } from 'react' 3 | // import { CellProps } from "./Cell"; 4 | import { Shape } from 'react-konva' 5 | // import { RendererProps } from "./Grid"; 6 | 7 | const EMPTY_ARRAY: number[] = [] 8 | 9 | export const getOffsetFromWidth = (width: number) => { 10 | return width / 2 - 0.5 11 | } 12 | 13 | const CellOverlay = React.memo((props: any) => { 14 | var { 15 | x, 16 | y, 17 | width, 18 | height, 19 | // stroke, 20 | color, 21 | strokeTopColor = color, 22 | strokeRightColor = color, 23 | strokeBottomColor = color, 24 | strokeLeftColor = color, 25 | strokeDash = EMPTY_ARRAY, 26 | strokeTopDash = strokeDash, 27 | strokeRightDash = strokeDash, 28 | strokeBottomDash = strokeDash, 29 | strokeLeftDash = strokeDash, 30 | strokeWidth = 0.5, 31 | strokeTopWidth = strokeWidth, 32 | strokeRightWidth = strokeWidth, 33 | strokeBottomWidth = strokeWidth, 34 | strokeLeftWidth = strokeWidth, 35 | lineCap = 'butt', 36 | ownKey, 37 | isMerge, 38 | } = props 39 | // console.log(ownKey) 40 | 41 | const cellStore = useContext(CellStoreContext) 42 | const cellsMap = cellStore.cellsMap 43 | var mergeRect: { 44 | [key: string]: number 45 | } = {} 46 | 47 | if (isMerge) { 48 | const [firstkey, endkey] = isMerge 49 | if (ownKey == endkey) { 50 | mergeRect = { 51 | x: cellsMap[firstkey]!.x, 52 | y: cellsMap[firstkey]!.y, 53 | width: 54 | cellsMap[endkey]!.x - 55 | cellsMap[firstkey]!.x + 56 | cellsMap[endkey]!.width, 57 | height: 58 | cellsMap[endkey]!.y - 59 | cellsMap[firstkey]!.y + 60 | cellsMap[endkey]!.height, 61 | } 62 | x = mergeRect.x 63 | y = mergeRect.y 64 | width = mergeRect.width 65 | height = mergeRect.height 66 | } else { 67 | return null 68 | } 69 | } 70 | 71 | const userStroke = 72 | strokeTopColor || 73 | strokeRightColor || 74 | strokeBottomColor || 75 | strokeLeftColor 76 | if (!userStroke) return null 77 | 78 | return ( 79 | { 86 | /* Top border */ 87 | if (strokeTopColor) { 88 | context.beginPath() 89 | context.moveTo( 90 | strokeLeftColor 91 | ? -getOffsetFromWidth(strokeLeftWidth) 92 | : 0, 93 | 0.5 94 | ) 95 | context.lineTo( 96 | shape.width() + 97 | (strokeRightColor 98 | ? getOffsetFromWidth(strokeRightWidth) + 1 99 | : 1), 100 | 0.5 101 | ) 102 | context.setAttr('strokeStyle', strokeTopColor) 103 | context.setAttr('lineWidth', 1) 104 | context.setAttr('lineCap', lineCap) 105 | context.setLineDash(strokeTopDash) 106 | context.stroke() 107 | } 108 | /* Bottom border */ 109 | if (strokeBottomColor) { 110 | context.beginPath() 111 | context.moveTo( 112 | strokeLeftColor 113 | ? -getOffsetFromWidth(strokeLeftWidth) 114 | : 0, 115 | shape.height() + 0.5 116 | ) 117 | context.lineTo( 118 | shape.width() + 119 | (strokeRightColor 120 | ? getOffsetFromWidth(strokeRightWidth) + 1 121 | : 1), 122 | shape.height() + 0.5 123 | ) 124 | context.setAttr('lineWidth', strokeBottomWidth) 125 | context.setAttr('strokeStyle', strokeBottomColor) 126 | context.setAttr('lineCap', lineCap) 127 | context.setLineDash(strokeBottomDash) 128 | context.stroke() 129 | } 130 | /* Left border */ 131 | if (strokeLeftColor) { 132 | context.beginPath() 133 | context.moveTo( 134 | 0.5, 135 | strokeTopColor ? -getOffsetFromWidth(strokeTopWidth) : 0 136 | ) 137 | context.lineTo( 138 | 0.5, 139 | shape.height() + 140 | (strokeBottomColor 141 | ? getOffsetFromWidth(strokeBottomWidth) + 1 142 | : 1) 143 | ) 144 | context.setAttr('strokeStyle', strokeLeftColor) 145 | context.setAttr('lineWidth', strokeLeftWidth) 146 | context.setAttr('lineCap', lineCap) 147 | context.setLineDash(strokeLeftDash) 148 | context.stroke() 149 | } 150 | /* Right border */ 151 | if (strokeRightColor) { 152 | context.beginPath() 153 | context.moveTo( 154 | shape.width() + 0.5, 155 | strokeTopColor ? -getOffsetFromWidth(strokeTopWidth) : 0 156 | ) 157 | context.lineTo( 158 | shape.width() + 0.5, 159 | shape.height() + 160 | (strokeBottomColor 161 | ? getOffsetFromWidth(strokeBottomWidth) + 1 162 | : 1) 163 | ) 164 | context.setAttr('strokeStyle', strokeRightColor) 165 | context.setAttr('lineWidth', strokeRightWidth) 166 | context.setAttr('lineCap', lineCap) 167 | context.setLineDash(strokeRightDash) 168 | context.stroke() 169 | } 170 | }} 171 | /> 172 | ) 173 | }) 174 | 175 | export { CellOverlay } 176 | -------------------------------------------------------------------------------- /src/components/cell/DraggableRect.tsx: -------------------------------------------------------------------------------- 1 | import { CellStoreContext } from '@/stores/CellStore' 2 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 3 | import { KonvaEventObject } from 'konva/lib/Node' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { 6 | CSSProperties, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useRef, 11 | useState, 12 | } from 'react' 13 | import _, { zip } from 'lodash' 14 | import { Stage, Text, Group, Rect, Line } from 'react-konva' 15 | import { 16 | containerHeight, 17 | containerWidth, 18 | dragMinWidth, 19 | dragMinHeight, 20 | leftCell, 21 | normalCell, 22 | } from '@/utils/constants' 23 | const DraggableRect = React.memo((props: any) => { 24 | const cellStore = useContext(CellStoreContext) 25 | const mouseEventStore = useContext(MouseEventStoreContext) 26 | 27 | const { type, x, y, height, width } = props 28 | 29 | return ( 30 | <> 31 | {type == 'header' ? ( 32 | { 42 | const node = e.target 43 | if (node.height() !== containerHeight) { 44 | node.zIndex(70) 45 | node.height(containerHeight) 46 | node.width(1) 47 | node.opacity(0.5) 48 | } 49 | }} 50 | onDragEnd={(e) => { 51 | const node = e.target 52 | node.width(width) 53 | node.height(height) 54 | 55 | const newWidth = node.x() - props.ownx + props.width 56 | const k = props.ownKey 57 | 58 | cellStore.changeWidth( 59 | k, 60 | Math.max(newWidth, dragMinWidth) 61 | ) 62 | 63 | node.opacity(0) 64 | }} 65 | onMouseEnter={(e) => { 66 | e.target.opacity(1) 67 | document.body.style.cursor = 'ew-resize' 68 | }} 69 | onMouseLeave={(e) => { 70 | e.target.opacity(0) 71 | document.body.style.cursor = 'default' 72 | }} 73 | dragBoundFunc={(pos) => { 74 | var rx = props.ownx - mouseEventStore.scrollLeft 75 | if (pos.x - rx < dragMinWidth) { 76 | return { 77 | x: dragMinWidth + rx, 78 | y: 0, 79 | } 80 | } 81 | 82 | return { 83 | ...pos, 84 | y: 0, 85 | } 86 | }} 87 | /> 88 | ) : ( 89 | { 99 | const node = e.target 100 | if (node.width() !== containerWidth) { 101 | node.setZIndex(40) 102 | 103 | node.width(containerWidth) 104 | node.height(1) 105 | node.opacity(0.5) 106 | } 107 | }} 108 | onDragEnd={(e) => { 109 | const node = e.target 110 | node.width(width) 111 | node.height(height) 112 | node.opacity(0) 113 | 114 | const newHeight = node.y() - props.owny + props.height 115 | const k = props.ownKey 116 | 117 | cellStore.changeHeight( 118 | k, 119 | Math.max(newHeight, dragMinHeight) 120 | ) 121 | }} 122 | onMouseEnter={(e) => { 123 | document.body.style.cursor = 'ns-resize' 124 | e.target.opacity(1) 125 | }} 126 | onMouseLeave={(e) => { 127 | document.body.style.cursor = 'default' 128 | e.target.opacity(0) 129 | }} 130 | dragBoundFunc={(pos) => { 131 | var ry = props.owny - mouseEventStore.scrollTop 132 | if (pos.y - ry < dragMinHeight) { 133 | return { 134 | y: ry + dragMinHeight, 135 | x: 0, 136 | } 137 | } 138 | return { 139 | ...pos, 140 | x: 0, 141 | } 142 | }} 143 | /> 144 | )} 145 | 146 | ) 147 | }) 148 | 149 | export default DraggableRect 150 | -------------------------------------------------------------------------------- /src/components/cell/HeaderCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 2 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 3 | import { dragHandleWidth, headerCell } from '@/utils/constants' 4 | import { KonvaEventObject } from 'konva/lib/Node' 5 | import { observer } from 'mobx-react-lite' 6 | import React, { 7 | CSSProperties, 8 | useCallback, 9 | useContext, 10 | useEffect, 11 | useRef, 12 | useState, 13 | } from 'react' 14 | 15 | import { Stage, Text, Group, Rect, Line } from 'react-konva' 16 | import DraggableRect from './DraggableRect' 17 | 18 | const HeaderCell = React.memo((props: any) => { 19 | let { 20 | width, 21 | height, 22 | ownKey, 23 | fill = headerCell.fill, 24 | strokeWidth = 1.5, 25 | stroke = '#c6c6c6', 26 | align = 'center', 27 | verticalAlign = 'middle', 28 | textColor = '#333', 29 | padding = 5, 30 | fontFamily = 'Arial', 31 | fontSize = 12, 32 | children, 33 | wrap = 'none', 34 | fontWeight = 'normal', 35 | fontStyle = 'normal', 36 | textDecoration, 37 | alpha = 1, 38 | strokeEnabled = true, 39 | globalCompositeOperation = 'multiply', 40 | type, 41 | } = props 42 | 43 | let x = props.x + 0, 44 | y = props.y + 0 45 | 46 | let num = Number(ownKey.split(':')[1]) 47 | 48 | let text = String.fromCharCode(num + 64) 49 | let c = Math.ceil(num / 26) 50 | if (c > 1) { 51 | var str = '' 52 | for (var i = 1; i <= c; i++) { 53 | str = str + String.fromCharCode((num > 26 ? 1 : num) + 64) 54 | num = num - 26 55 | } 56 | text = str 57 | } 58 | 59 | const cellStore = useContext(CellStoreContext) 60 | 61 | const textStyle = `${fontWeight} ${fontStyle}` 62 | 63 | const clickHeader = () => { 64 | cellStore.areaHeaderCell(ownKey) 65 | } 66 | 67 | return ( 68 | <> 69 | 79 | { 88 | document.body.style.cursor = 'pointer' 89 | 90 | }} 91 | onMouseLeave={(e) => { 92 | document.body.style.cursor = 'default' 93 | }} 94 | verticalAlign={verticalAlign} 95 | align={align} 96 | fontFamily={fontFamily} 97 | fontStyle={textStyle} 98 | textDecoration={textDecoration} 99 | padding={padding} 100 | wrap={wrap} 101 | fontSize={fontSize} 102 | hitStrokeWidth={0} 103 | type={type} 104 | ownKey={ownKey} 105 | /> 106 | 107 | 115 | 116 | ) 117 | }) 118 | 119 | export default HeaderCell 120 | -------------------------------------------------------------------------------- /src/components/cell/LeftCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellStoreContext } from '@/stores/CellStore' 2 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 3 | import { dragHandleHeight, leftCell } from '@/utils/constants' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { 6 | CSSProperties, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useRef, 11 | useState, 12 | } from 'react' 13 | 14 | import { Stage, Text, Group, Rect } from 'react-konva' 15 | import DraggableRect from './DraggableRect' 16 | 17 | const LeftCell = React.memo((props: any) => { 18 | let { 19 | width, 20 | height, 21 | ownKey, 22 | fill = leftCell.fill, 23 | strokeWidth = 1.5, 24 | stroke = '#c6c6c6', 25 | align = 'center', 26 | verticalAlign = 'middle', 27 | textColor = '#333', 28 | padding = 5, 29 | fontFamily = 'Arial', 30 | fontSize = 12, 31 | children, 32 | wrap = 'none', 33 | fontWeight = 'normal', 34 | fontStyle = 'normal', 35 | textDecoration, 36 | alpha = 1, 37 | strokeEnabled = true, 38 | globalCompositeOperation = 'multiply', 39 | type, 40 | } = props 41 | 42 | let x = props.x + 0, 43 | y = props.y + 0 44 | 45 | const cellStore = useContext(CellStoreContext) 46 | 47 | let text = ownKey.split(':')[0] 48 | 49 | const textStyle = `${fontWeight} ${fontStyle}` 50 | 51 | const clickHeader = () => { 52 | cellStore.areaLeftCell(ownKey) 53 | } 54 | 55 | return ( 56 | <> 57 | 68 | { 79 | document.body.style.cursor = 'pointer' 80 | }} 81 | onMouseLeave={(e) => { 82 | document.body.style.cursor = 'default' 83 | }} 84 | fontFamily={fontFamily} 85 | fontStyle={textStyle} 86 | textDecoration={textDecoration} 87 | padding={padding} 88 | wrap={wrap} 89 | type={type} 90 | fontSize={fontSize} 91 | hitStrokeWidth={0} 92 | ownKey={ownKey} 93 | /> 94 | 102 | 103 | ) 104 | }) 105 | 106 | export default LeftCell 107 | -------------------------------------------------------------------------------- /src/components/cell/NormalCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellStoreContext } from '@/stores/CellStore' 2 | import { ToolBarStoreContext } from '@/stores/ToolBarStore' 3 | import { normalCell } from '@/utils/constants' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { 6 | CSSProperties, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useRef, 11 | useState, 12 | } from 'react' 13 | 14 | import { Stage, Text, Group, Rect } from 'react-konva' 15 | import NImage from './components/NImage' 16 | import NText from './components/NText' 17 | import HeaderCell from './HeaderCell' 18 | import LeftCell from './LeftCell' 19 | import SingleCell from './SingleCell' 20 | 21 | interface IProps {} 22 | 23 | const Cell = React.memo((props: any) => { 24 | const { 25 | x = 0, 26 | y = 0, 27 | width, 28 | height, 29 | fill, 30 | align = 'left', 31 | verticalAlign = 'middle', 32 | textColor = '#333', 33 | padding = 5, 34 | fontFamily = normalCell.fontFamily, 35 | fontSize = normalCell.fontSize, 36 | wrap = 'none', 37 | alpha = 1, 38 | strokeEnabled = true, 39 | type = 'normal', 40 | ownKey, 41 | isMerge, 42 | value, 43 | imgUrl, 44 | imgLoaded, 45 | } = props 46 | 47 | const cellStore = useContext(CellStoreContext) 48 | const cellsMap = cellStore.cellsMap 49 | 50 | const tabBarStore = useContext(ToolBarStoreContext) 51 | 52 | const fontWeight = tabBarStore.currentTextFillBold ? 'bold' : '' 53 | const fontItalic = tabBarStore.currentTextFillItalic ? 'italic' : '' 54 | 55 | const fontStyle = (fontItalic + ' ' + fontWeight).trim() || 'normal' 56 | 57 | const textDecoration = tabBarStore.currentTextFillUnderline 58 | 59 | // console.log(props) 60 | 61 | var mergeRect: any = {} 62 | 63 | if (isMerge) { 64 | const [firstkey, endkey] = isMerge 65 | if (ownKey == endkey) { 66 | mergeRect = { 67 | x: cellsMap[firstkey]!.x, 68 | y: cellsMap[firstkey]!.y, 69 | width: 70 | cellsMap[endkey]!.x - 71 | cellsMap[firstkey]!.x + 72 | cellsMap[endkey]!.width, 73 | height: 74 | cellsMap[endkey]!.y - 75 | cellsMap[firstkey]!.y + 76 | cellsMap[endkey]!.height, 77 | } 78 | } 79 | } 80 | 81 | const renderCell = () => { 82 | var p = { 83 | ownKey: ownKey, 84 | type: type, 85 | x: x, 86 | y: y, 87 | height: height, 88 | width: width, 89 | value: value, 90 | fill: textColor, 91 | textColor:textColor, 92 | verticalAlign: verticalAlign, 93 | align: align, 94 | fontFamily: fontFamily, 95 | fontStyle: fontStyle, 96 | textDecoration: textDecoration, 97 | padding: padding, 98 | wrap: wrap, 99 | fontSize: fontSize, 100 | imgUrl: imgUrl, 101 | imgLoaded: imgLoaded, 102 | } 103 | 104 | if (mergeRect.width) { 105 | p.width = mergeRect.width 106 | p.height = mergeRect.height 107 | p.x = mergeRect.x 108 | p.y = mergeRect.y 109 | if (imgUrl) { 110 | return 111 | } else { 112 | return 113 | } 114 | } else { 115 | if (imgUrl) { 116 | return 117 | } else { 118 | return 119 | } 120 | } 121 | } 122 | 123 | return ( 124 | 125 | 138 | {renderCell()} 139 | 140 | ) 141 | }) 142 | 143 | export default Cell 144 | -------------------------------------------------------------------------------- /src/components/cell/SingleCell.tsx: -------------------------------------------------------------------------------- 1 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 2 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 3 | import { KonvaEventObject } from 'konva/lib/Node' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { 6 | CSSProperties, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useRef, 11 | useState, 12 | } from 'react' 13 | 14 | import { Stage, Text, Group, Rect, Line } from 'react-konva' 15 | 16 | const SingleCell = React.memo( 17 | observer((props: any) => { 18 | let { width, height, stroke = '#c6c6c6' } = props 19 | 20 | let x = props.x + 0, 21 | y = props.y + 0 22 | 23 | const cellStore = useContext(CellStoreContext) 24 | 25 | const activeCell = cellStore.activeCell 26 | 27 | let text1 = activeCell?.ownKey.split(':')[0] 28 | let text2 = String.fromCharCode( 29 | Number(activeCell?.ownKey.split(':')[1]) + 64 30 | ) 31 | 32 | let textS = text1 + ':' + text2 33 | 34 | return ( 35 | <> 36 | 45 | 55 | 56 | ) 57 | }) 58 | ) 59 | 60 | export default SingleCell 61 | -------------------------------------------------------------------------------- /src/components/cell/components/NImage.tsx: -------------------------------------------------------------------------------- 1 | import useImage from '@/hooks/useImage' 2 | import { CellStoreContext } from '@/stores/CellStore' 3 | import { ToolBarStoreContext } from '@/stores/ToolBarStore' 4 | import { normalCell } from '@/utils/constants' 5 | import { observer } from 'mobx-react-lite' 6 | 7 | import React, { 8 | CSSProperties, 9 | useCallback, 10 | useContext, 11 | useEffect, 12 | useMemo, 13 | useRef, 14 | useState, 15 | } from 'react' 16 | 17 | import { Stage, Text, Group, Rect, Image } from 'react-konva' 18 | 19 | interface IProps {} 20 | 21 | const NImage = React.memo((props: any) => { 22 | const toolbarStore = useContext(ToolBarStoreContext) 23 | const cellStore = useContext(CellStoreContext) 24 | let { imgUrl, width, height, x, y, ownKey, imgLoaded } = props 25 | 26 | const spacing = 3 27 | 28 | const { image, width: imageWidth, height: imageHeight, status } = useImage({ 29 | imgUrl, 30 | }) 31 | 32 | const aspectRatio = useMemo(() => { 33 | return Math.min( 34 | (width - spacing) / imageWidth, 35 | (height - spacing) / imageHeight 36 | ) 37 | }, [imageWidth, imageHeight, width, height]) 38 | 39 | // 动态调整单元格大小 40 | if (status == 'loaded' && !imgLoaded) { 41 | let maxw = 150, 42 | _w = 0, 43 | _h = 0 44 | if (imageWidth > maxw) { 45 | _w = maxw 46 | _h = (_w * imageHeight) / imageWidth 47 | } else { 48 | _w = imageWidth 49 | _h = (_w * imageHeight) / imageWidth 50 | } 51 | cellStore.changeWidth(ownKey, _w) 52 | cellStore.changeHeight(ownKey, _h) 53 | 54 | cellStore.imgLoadedCell(ownKey) 55 | } 56 | 57 | if (!imgLoaded) { 58 | return null 59 | } 60 | 61 | let _width = Math.min(imageWidth, aspectRatio * imageWidth) 62 | let _height = Math.min(imageHeight, aspectRatio * imageHeight) 63 | 64 | let _x = x, 65 | _y = y 66 | 67 | if (width > _width) { 68 | _x = _x + (width - _width) / 2 69 | _height = _height - spacing 70 | // y = y + spacing 71 | } 72 | if (height > _height) { 73 | _y = _y + (height - _height) / 2 74 | _width = _width - spacing 75 | _x = _x + spacing / 2 76 | } 77 | 78 | const dbClick = () => { 79 | toolbarStore.currentBigImg = [{ src: imgUrl, alt: '' }] as any 80 | } 81 | 82 | return ( 83 | <> 84 | 91 | 101 | 102 | ) 103 | }) 104 | export default NImage 105 | -------------------------------------------------------------------------------- /src/components/cell/components/NText.tsx: -------------------------------------------------------------------------------- 1 | // import { CellStoreContext } from '@/stores/CellStore' 2 | // import { ToolBarStoreContext } from '@/stores/ToolBarStore' 3 | import { normalCell } from '@/utils/constants' 4 | import { observer } from 'mobx-react-lite' 5 | import React, { 6 | CSSProperties, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useRef, 11 | useState, 12 | } from 'react' 13 | 14 | import { Stage, Text, Group, Rect, Shape } from 'react-konva' 15 | 16 | interface IProps {} 17 | 18 | export const isNull = (value: any) => 19 | value === void 0 || value === null || value === '' 20 | 21 | const NText = React.memo((props: any) => { 22 | const { 23 | x = 0, 24 | y = 0, 25 | width, 26 | height, 27 | align = 'left', 28 | verticalAlign = 'middle', 29 | textColor = '#333', 30 | padding = 5, 31 | fontFamily = normalCell.fontFamily, 32 | fontSize = normalCell.fontSize, 33 | wrap = 'none', 34 | type = 'normal', 35 | ownKey, 36 | value, 37 | fontStyle, 38 | textDecoration, 39 | isMerge, 40 | } = props 41 | 42 | 43 | return ( 44 | <> 45 | {/* */} 46 | {isMerge ? ( 47 | { 53 | // // context.clearRect(0,0,width,height) 54 | 55 | // context.beginPath(); 56 | // // debugger 57 | // context.moveTo(0, 0) 58 | // context.lineTo(width, 0); 59 | // context.setAttr('strokeStyle', '#d9d9d9') 60 | // context.setAttr('lineWidth', 2.5) 61 | // context.stroke() 62 | // context.beginPath(); 63 | // // debugger 64 | // context.moveTo(width, 0) 65 | // // context.moveTo(width, 0) 66 | // context.lineTo(width, height); 67 | // // context.lineTo(0, height); 68 | // // context.lineTo(0,0); 69 | // // context.lineTo(x, y); 70 | // context.setAttr('strokeStyle', '#d9d9d9') 71 | // context.setAttr('lineWidth', 2.5) 72 | // context.stroke() 73 | // }} 74 | height={height} 75 | ownKey={ownKey} 76 | type={type} 77 | width={width} 78 | > 79 | ) : null} 80 | 81 | {isNull(value) ? null : ( 82 | 101 | )} 102 | 103 | ) 104 | }) 105 | 106 | export default NText 107 | -------------------------------------------------------------------------------- /src/components/layer/contextMenuArea/ContextMenuLayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | useCallback, 4 | useContext, 5 | useEffect, 6 | useRef, 7 | useState, 8 | } from 'react' 9 | 10 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 11 | import styles from './styles.module.css' 12 | import { observer } from 'mobx-react-lite' 13 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 14 | import { getCurrentCellByOwnKey, getCurrentCellByXY } from '@/utils' 15 | import _ from 'lodash' 16 | import { headerCell, leftCell } from '@/utils/constants' 17 | import { 18 | ControlledMenu, 19 | Menu, 20 | MenuDivider, 21 | MenuItem, 22 | useMenuState, 23 | } from '@szhsin/react-menu' 24 | import { CopyStoreContext } from '@/stores/CopyStore' 25 | 26 | interface IProps {} 27 | 28 | const ContextMenuLayer = (props: IProps) => { 29 | const cellStore = useContext(CellStoreContext) 30 | const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 }) 31 | const [menuProps, toggleMenu] = useMenuState() 32 | const mouseEventStore = useContext(MouseEventStoreContext) 33 | const rc = mouseEventStore.rcCellAttr 34 | const copyStore = useContext(CopyStoreContext) 35 | useEffect(() => { 36 | if (!rc) return 37 | 38 | setAnchorPoint({ x: rc.clientX, y: rc.clientY }) 39 | toggleMenu(true) 40 | }, [rc]) 41 | 42 | const addRow = (type: string) => { 43 | let cur = getCurrentCellByOwnKey(rc!.ownKey, cellStore.cellsMap, true) 44 | 45 | if (type == 'up') { 46 | } else { 47 | cellStore.addCellRowBelow(cur!.ownKey) 48 | } 49 | } 50 | const addCol = (type: string) => { 51 | let cur = getCurrentCellByOwnKey(rc!.ownKey, cellStore.cellsMap, true) 52 | 53 | if (type == 'right') { 54 | cellStore.addCellRowRight(cur!.ownKey) 55 | } else { 56 | } 57 | } 58 | 59 | return ( 60 |
70 | toggleMenu(false)} 75 | > 76 | copyStore.cutCurrentCells(cellStore)}> 77 | 剪切(Ctrl+X) 78 | 79 | copyStore.copyCurrentCells(cellStore)}> 80 | 复制(Ctrl+C) 81 | 82 | copyStore.pasteCurrentCells(cellStore)} 84 | > 85 | 粘贴(Ctrl+V) 86 | 87 | 88 | {/* 89 |
addRow('up')}> 90 |
93 |
插入一行(上)
94 |
95 | 96 |
*/} 97 | 98 |
addRow('below')} 101 | > 102 |
105 |
106 | 插入一行(下) 107 |
108 |
109 |
110 | {/* 111 |
112 |
115 |
插入一列(左)
116 |
117 | 118 |
*/} 119 | 120 |
addCol('right')} 123 | > 124 |
127 |
128 | 插入一列(右) 129 |
130 |
131 |
132 |
133 |
134 | ) 135 | } 136 | 137 | export default observer(ContextMenuLayer) 138 | -------------------------------------------------------------------------------- /src/components/layer/contextMenuArea/imgs/insert1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/layer/contextMenuArea/imgs/insert1.png -------------------------------------------------------------------------------- /src/components/layer/contextMenuArea/imgs/insert2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/layer/contextMenuArea/imgs/insert2.png -------------------------------------------------------------------------------- /src/components/layer/contextMenuArea/imgs/insert3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/layer/contextMenuArea/imgs/insert3.png -------------------------------------------------------------------------------- /src/components/layer/contextMenuArea/imgs/insert4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/layer/contextMenuArea/imgs/insert4.png -------------------------------------------------------------------------------- /src/components/layer/contextMenuArea/styles.module.css: -------------------------------------------------------------------------------- 1 | .border-item { 2 | display: flex; 3 | } 4 | .icon-item { 5 | width: 19px; 6 | height:19px; 7 | background-size: cover; 8 | margin-right: 4px; 9 | } 10 | .border-item .item-text { 11 | font-size: 13px; 12 | margin-top: -1px; 13 | } 14 | .item-icon-insert-1 { 15 | background-image: url('./imgs/insert1.png'); 16 | } 17 | .item-icon-insert-2 { 18 | background-image: url('./imgs/insert4.png'); 19 | } 20 | .item-icon-insert-3 { 21 | background-image: url('./imgs/insert2.png'); 22 | } 23 | .item-icon-insert-4 { 24 | background-image: url('./imgs/insert3.png'); 25 | } -------------------------------------------------------------------------------- /src/components/layer/cornerArea/CornerArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | useCallback, 4 | useContext, 5 | useEffect, 6 | useMemo, 7 | useRef, 8 | useState, 9 | } from 'react' 10 | 11 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 12 | import styles from './styles.module.css' 13 | import { observer } from 'mobx-react-lite' 14 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 15 | import { getCurrentCellByXY } from '@/utils' 16 | import _ from 'lodash' 17 | import { headerCell, leftCell } from '@/utils/constants' 18 | 19 | const CornerArea = () => { 20 | const cellStore = useContext(CellStoreContext) 21 | const selectArea = cellStore.selectArea 22 | const activeCell = cellStore.activeCell 23 | // const setSelectArea = cellStore.setSelectArea 24 | const getHeaderAreaCell: CSSProperties = useMemo(() => { 25 | var style: CSSProperties = {} 26 | if (selectArea) { 27 | const o = selectArea 28 | 29 | style = { 30 | left: o.left, 31 | top: 0, 32 | width: o.right - o.left, 33 | height: headerCell.height, 34 | } 35 | 36 | return style 37 | } else if (activeCell) { 38 | const o = activeCell 39 | 40 | style = { 41 | left: o.x, 42 | top: 0, 43 | width: o.width, 44 | height: headerCell.height, 45 | } 46 | 47 | return style 48 | } 49 | 50 | return style 51 | }, [selectArea, activeCell]) 52 | 53 | const getLeftAreaCell: CSSProperties = useMemo(() => { 54 | var style: CSSProperties = {} 55 | 56 | if (selectArea) { 57 | const o = selectArea 58 | 59 | style = { 60 | left: 0, 61 | top: o.top, 62 | width: leftCell.width, 63 | height: o.bottom - o.top, 64 | } 65 | 66 | return style 67 | } else if (activeCell) { 68 | const o = activeCell 69 | 70 | style = { 71 | left: 0, 72 | top: o.y, 73 | width: leftCell.width, 74 | height: o.height, 75 | } 76 | 77 | return style 78 | } 79 | return style 80 | }, [selectArea, activeCell]) 81 | 82 | const mouseEventStore = useContext(MouseEventStoreContext) 83 | 84 | return ( 85 |
96 |
105 |
114 |
115 | ) 116 | } 117 | 118 | export default observer(CornerArea) 119 | -------------------------------------------------------------------------------- /src/components/layer/cornerArea/styles.module.css: -------------------------------------------------------------------------------- 1 | .header-area,.left-area { 2 | position: absolute; 3 | background-color:rgb(126 126 126 / 10%); 4 | will-change: left,top,width,bottom,right,height; 5 | transform: translateZ(0); 6 | } -------------------------------------------------------------------------------- /src/components/layer/editArea/EditAreaLayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | useCallback, 4 | useContext, 5 | useEffect, 6 | useRef, 7 | useState, 8 | } from 'react' 9 | 10 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 11 | import styles from './styles.module.css' 12 | import { observer } from 'mobx-react-lite' 13 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 14 | 15 | import _ from 'lodash' 16 | import { 17 | headerCell, 18 | leftCell, 19 | normalCell, 20 | singleCell, 21 | rowStopIndex, 22 | columnStopIndex, 23 | } from '@/utils/constants' 24 | import { getCurrentCellByOwnKey, getCurrentCellByXY } from '@/utils/index' 25 | import { CopyStoreContext } from '@/stores/CopyStore' 26 | 27 | interface IProps {} 28 | 29 | const EditAreaLayer = (props: any) => { 30 | const mouseEventStore = useContext(MouseEventStoreContext) 31 | const dbc = mouseEventStore.dbcCellAttr 32 | 33 | const cellStore = useContext(CellStoreContext) 34 | 35 | const setEditCell = cellStore.setEditCell 36 | 37 | const editCell = cellStore.editCell 38 | 39 | useEffect(() => { 40 | if (!dbc || !dbc.ownKey || dbc.noEdit) return 41 | var cur = getCurrentCellByOwnKey(dbc!.ownKey, cellStore.cellsMap, true) 42 | 43 | if (cur?.isMerge) { 44 | setEditCell({ 45 | ...cur, 46 | ownKey: cellStore.cellsMap[cur.isMerge[1]]!.ownKey, 47 | value: cellStore.cellsMap[cur.isMerge[1]]?.value, 48 | }) 49 | return 50 | } 51 | 52 | setEditCell(dbc) 53 | }, [dbc]) 54 | 55 | // useEffect(() => { 56 | // setEditCell(null) 57 | // }, [mouseEventStore.downCellAttr]) 58 | 59 | const editCellRenderer = (o: any) => { 60 | const style: CSSProperties = { 61 | position: 'absolute', 62 | left: o.x, 63 | top: o.y, 64 | borderWidth: o.strokeWidth, 65 | borderColor: o.stroke, 66 | width: o.width + 1, 67 | height: o.height + 1, 68 | borderStyle: 'solid', 69 | boxSizing: 'border-box', 70 | boxShadow: 'rgb(60 64 67 / 15%) 0px 2px 6px 2px', 71 | backgroundColor: '#fff', 72 | } 73 | 74 | return ( 75 |
76 | 99 |
100 | ) 101 | } 102 | 103 | const getEditCellSelection = useCallback(() => { 104 | if (!editCell) return null 105 | 106 | // console.log(editCell) 107 | 108 | const cell = editCellRenderer({ 109 | stroke: '#1a73e8', 110 | strokeWidth: 2, 111 | fill: 'transparent', 112 | x: editCell.x, 113 | y: editCell.y, 114 | width: editCell.width, 115 | height: editCell.height, 116 | value: editCell.value, 117 | ownKey: editCell.ownKey, 118 | }) 119 | 120 | return cell 121 | }, [editCell]) 122 | 123 | return ( 124 |
136 |
143 | {getEditCellSelection()} 144 |
145 |
146 | ) 147 | } 148 | 149 | export default observer(EditAreaLayer) 150 | -------------------------------------------------------------------------------- /src/components/layer/editArea/styles.module.css: -------------------------------------------------------------------------------- 1 | .edit-textarea { 2 | font-size: 14px; 3 | width: 100%; 4 | height: 100%; 5 | padding: 0px 1px; 6 | margin: 0px; 7 | box-sizing: border-box; 8 | border-width: 0px; 9 | outline: none; 10 | resize: none; 11 | overflow: hidden; 12 | vertical-align: top; 13 | background: #fff; 14 | pointer-events: all; 15 | line-height: 17px; 16 | text-indent: 1px; 17 | white-space: normal; 18 | word-wrap: break-word; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/layer/scrollArea/ScrollArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | useCallback, 4 | useContext, 5 | useEffect, 6 | useRef, 7 | useState, 8 | } from 'react' 9 | 10 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 11 | import styles from './styles.module.css' 12 | import { observer } from 'mobx-react-lite' 13 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 14 | import { getCurrentCellByXY } from '@/utils' 15 | import _ from 'lodash' 16 | 17 | interface IProps { 18 | swidth: number 19 | sheight: number 20 | } 21 | 22 | const ScrollArea = (props: IProps) => { 23 | return ( 24 |
34 | ) 35 | } 36 | 37 | export default ScrollArea 38 | -------------------------------------------------------------------------------- /src/components/layer/scrollArea/styles.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/layer/scrollArea/styles.module.css -------------------------------------------------------------------------------- /src/components/layer/selectArea/SelectAreaLayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | startTransition, 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useMemo, 8 | useRef, 9 | useState, 10 | } from 'react' 11 | 12 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 13 | import styles from './styles.module.css' 14 | import { observer } from 'mobx-react-lite' 15 | import { CellAttrs, CellStoreContext, SelectArea } from '@/stores/CellStore' 16 | 17 | import { 18 | getCurrentCellByOwnKey, 19 | getCurrentCellByXY, 20 | getCurrentCellsByArea, 21 | getScrollWidthAndHeight, 22 | } from '@/utils' 23 | import _ from 'lodash' 24 | import { 25 | containerHeight, 26 | containerWidth, 27 | headerCell, 28 | leftCell, 29 | normalCell, 30 | } from '@/utils/constants' 31 | import SelectFill from './components/selectFill/SelectFill' 32 | import { FloatImageStoreContext } from '@/stores/FloatImageStore' 33 | import { ToolBarStoreContext } from '@/stores/ToolBarStore' 34 | import { useSize } from '@/hooks/useSize' 35 | import { CopyStoreContext } from '@/stores/CopyStore' 36 | import CopyArea from './components/copyArea/CopyArea' 37 | 38 | interface IProps {} 39 | 40 | const SelectAreaLayer = (props: any) => { 41 | const isSelecting = useRef(false) 42 | 43 | const cellStore = useContext(CellStoreContext) 44 | const toolbarStore = useContext(ToolBarStoreContext) 45 | const floatImageStore = useContext(FloatImageStoreContext) 46 | // const copyStore = useContext(CopyStoreContext) 47 | 48 | const selectArea = cellStore.selectArea 49 | const setSelectArea = cellStore.setSelectArea 50 | 51 | // const copyArea = copyStore.currentCopyArea 52 | 53 | const getSelectAreaCell: CSSProperties = useMemo(() => { 54 | var style: CSSProperties = { 55 | position: 'absolute', 56 | } 57 | if (!selectArea) return style 58 | 59 | const o = selectArea 60 | 61 | style = { 62 | position: 'absolute', 63 | left: o.left, 64 | top: o.top, 65 | width: o.right - o.left, 66 | height: o.bottom - o.top, 67 | } 68 | 69 | return style 70 | }, [selectArea]) 71 | 72 | const activeCell = cellStore.activeCell 73 | const setActiveCell = cellStore.setActiveCell 74 | 75 | const getActiveCellSelection = useMemo(() => { 76 | if (!activeCell) return undefined 77 | 78 | const style: CSSProperties = { 79 | left: activeCell.x, 80 | top: activeCell.y, 81 | width: activeCell?.width + 1, 82 | height: activeCell?.height + 1, 83 | // backgroundColor:selectArea?'#fff':'transparent' 84 | } 85 | 86 | return style 87 | }, [activeCell]) 88 | 89 | const mouseEventStore = useContext(MouseEventStoreContext) 90 | const dv = mouseEventStore.downCellAttr 91 | const uv = mouseEventStore.upCellAttr 92 | const mv = mouseEventStore.moveCellAttr 93 | 94 | // const cellsMap = cellStore.cellsMap 95 | 96 | let { swidth, sheight } = useSize() 97 | 98 | useEffect(() => { 99 | if (activeCell) { 100 | let cur = getCurrentCellByOwnKey( 101 | activeCell?.ownKey || '', 102 | cellStore.cellsMap, 103 | true 104 | ) 105 | setActiveCell(cur) 106 | } 107 | if (selectArea) { 108 | var first = getCurrentCellByOwnKey( 109 | cellStore.selectStart!.ownKey, 110 | cellStore.cellsMap, 111 | true 112 | ) 113 | var last = getCurrentCellByOwnKey( 114 | cellStore.selectEnd!.ownKey, 115 | cellStore.cellsMap, 116 | true 117 | ) 118 | // console.log(first,last) 119 | const o = { 120 | top: first!.y, 121 | bottom: last!.y + last!.height, 122 | left: first!.x, 123 | right: last!.x + last!.width, 124 | } 125 | 126 | setSelectArea(o) 127 | } 128 | 129 | // let arr = _.values(cellStore.cellsMap) 130 | // arr.filter(i=>{ 131 | // if (i?.isMerge) { 132 | // if(i.isMerge[0] == i.ownKey){ 133 | // console.log(i.ownKey) 134 | // } 135 | // } 136 | // }) 137 | }, [cellStore.cellsMap]) 138 | 139 | const expandScrollArea = (cur: CellAttrs) => { 140 | if (!cur) return 141 | var isRightBound = 142 | cur.x + cur.width - mouseEventStore.scrollLeft >= 143 | containerWidth - 20 144 | var isLeftBound = cur.x - mouseEventStore.scrollLeft <= leftCell.width 145 | var isBottomBound = 146 | cur.y + cur.height - mouseEventStore.scrollTop >= 147 | containerHeight - 20 148 | 149 | var isTopBound = cur.y - mouseEventStore.scrollTop <= headerCell.height 150 | 151 | if (isRightBound) { 152 | mouseEventStore.scrollLeft = Math.min( 153 | swidth - containerWidth, 154 | mouseEventStore.scrollLeft + cur.width 155 | ) 156 | } else if (isLeftBound) { 157 | mouseEventStore.scrollLeft = Math.max( 158 | 0, 159 | mouseEventStore.scrollLeft - cur.width 160 | ) 161 | } else if (isBottomBound) { 162 | mouseEventStore.scrollTop = Math.min( 163 | sheight - containerHeight, 164 | mouseEventStore.scrollTop + cur.height 165 | ) 166 | } else if (isTopBound) { 167 | mouseEventStore.scrollTop = Math.max( 168 | 0, 169 | mouseEventStore.scrollTop - cur.height 170 | ) 171 | } 172 | } 173 | 174 | const timer = useRef(0) 175 | const expandScrollAreaCheck = (flag: string, cur: CellAttrs) => { 176 | if (flag == 'start') { 177 | if (timer.current) { 178 | clearInterval(timer.current) 179 | } 180 | timer.current = window.setInterval(() => { 181 | expandScrollArea(cur) 182 | }, 300) 183 | } else { 184 | if (timer.current) { 185 | clearInterval(timer.current) 186 | } 187 | } 188 | } 189 | 190 | useEffect(() => { 191 | floatImageStore.currentTransformerId = '' 192 | // console.log(mouseEventStore.rcCellAttr) 193 | 194 | if (dv?.type != 'normal') return 195 | 196 | let cur = getCurrentCellByOwnKey( 197 | dv?.ownKey || '', 198 | cellStore.cellsMap, 199 | true 200 | ) 201 | 202 | if (dv?.rightClick) { 203 | if (activeCell) { 204 | } else { 205 | setActiveCell(cur) 206 | } 207 | } else { 208 | setActiveCell(cur) 209 | setSelectArea(null) 210 | } 211 | 212 | isSelecting.current = true 213 | cellStore.selectEnd = null 214 | cellStore.selectStart = cur 215 | ? { 216 | ...cur, 217 | x: cur.x, 218 | y: cur.y, 219 | } 220 | : null 221 | }, [dv]) 222 | 223 | useEffect(() => { 224 | if (isSelecting.current && !mouseEventStore.selectFilling) { 225 | // let cur = getCurrentCellByOwnKey( 226 | // mv?.ownKey || '', 227 | // cellStore.cellsMap, 228 | // true 229 | // ) 230 | let cur = mv 231 | if (!cur) return 232 | 233 | const start = cellStore.selectStart 234 | 235 | if (start == null) return 236 | 237 | // 回到起点,置为空 238 | if (cur.x == start.x && cur.y == start.y) { 239 | setSelectArea(null) 240 | return 241 | } 242 | 243 | let top = Math.min(start.y, cur.y) 244 | let bottom = Math.max(start.y + start.height, cur.y + cur.height) 245 | let left = Math.min(start.x, cur.x) 246 | let right = Math.max(start.x + start.width, cur.x + cur.width) 247 | 248 | const o = { top, bottom, left, right } 249 | 250 | // 判断是否覆盖了mergecell 251 | let arr = getCurrentCellsByArea(o, cellStore.cellsMap).filter( 252 | (i) => i?.isMerge 253 | ) 254 | 255 | arr.forEach((item) => { 256 | var cur = getCurrentCellByOwnKey( 257 | item!.ownKey, 258 | cellStore.cellsMap, 259 | true 260 | ) 261 | o.top = Math.min(o.top, cur!.y) 262 | o.bottom = Math.max(o.bottom, cur!.y + cur!.height) 263 | o.left = Math.min(o.left, cur!.x) 264 | o.right = Math.max(o.right, cur!.x + cur!.width) 265 | }) 266 | 267 | expandScrollAreaCheck('start', cur) 268 | // expandScrollArea(cur) 269 | 270 | setSelectArea(o) 271 | } 272 | }, [mv]) 273 | 274 | useEffect(() => { 275 | if (isSelecting.current && uv) { 276 | isSelecting.current = false 277 | 278 | cellStore.selectEnd = { 279 | ...uv, 280 | } 281 | } 282 | expandScrollAreaCheck('stop', null) 283 | }, [uv]) 284 | 285 | return ( 286 |
297 |
305 | {selectArea ? ( 306 |
310 | ) : null} 311 | 312 | 313 | 314 |
319 | {activeCell?.imgUrl && activeCell?.imgLoaded ? ( 320 |
323 | (toolbarStore.currentBigImg = [ 324 | { src: activeCell?.imgUrl, alt: '' }, 325 | ] as any) 326 | } 327 | >
328 | ) : null} 329 |
332 | (mouseEventStore.selectFilling = true) 333 | } 334 | onMouseUp={() => 335 | (mouseEventStore.selectFilling = false) 336 | } 337 | >
338 |
339 | 340 | 341 |
342 |
343 | ) 344 | } 345 | 346 | export default observer(SelectAreaLayer) 347 | -------------------------------------------------------------------------------- /src/components/layer/selectArea/components/copyArea/CopyArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | startTransition, 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useMemo, 8 | useRef, 9 | useState, 10 | } from 'react' 11 | 12 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 13 | import styles from './styles.module.css' 14 | import { observer } from 'mobx-react-lite' 15 | import { CellAttrs, CellStoreContext, SelectArea } from '@/stores/CellStore' 16 | 17 | import { 18 | getCurrentCellByOwnKey, 19 | getCurrentCellByXY, 20 | getCurrentCellsByArea, 21 | getScrollWidthAndHeight, 22 | } from '@/utils' 23 | import _ from 'lodash' 24 | import { 25 | containerHeight, 26 | containerWidth, 27 | headerCell, 28 | leftCell, 29 | normalCell, 30 | } from '@/utils/constants' 31 | import { CopyCurrentArea, CopyStoreContext } from '@/stores/CopyStore' 32 | 33 | interface IProps {} 34 | 35 | export type CopyAreaStrye = { 36 | viewBox?: string 37 | } & CSSProperties 38 | 39 | const CopyArea = (props: any) => { 40 | const cellStore = useContext(CellStoreContext) 41 | const copyStore = useContext(CopyStoreContext) 42 | 43 | const getCopyArea: JSX.Element = useMemo(() => { 44 | if (!copyStore.currentCopyArea) return <> 45 | 46 | const o = copyStore.currentCopyArea 47 | 48 | const width = o.right - o.left 49 | const height = o.bottom - o.top 50 | 51 | var style = { 52 | left: o.left, 53 | top: o.top, 54 | width: width, 55 | height: height, 56 | viewBox: '0 0 ' + width / 2 + ' ' + height / 2, 57 | } 58 | 59 | const d = `m0,0 v${height / 2} h${width / 2} v-${height / 2} h-${ 60 | width / 2 61 | } z` 62 | return ( 63 | <> 64 | 71 | 77 | 86 | 87 | 88 | ) 89 | }, [copyStore.currentCopyArea]) 90 | 91 | useEffect(() => { 92 | if (copyStore.currentCopyArea) { 93 | var list = getCurrentCellsByArea(copyStore.currentCopyArea,cellStore.cellsMap) 94 | var first = list[0] 95 | var last = list[list.length-1] 96 | // console.log(first,last) 97 | const o = { 98 | top: first!.y, 99 | bottom: last!.y + last!.height, 100 | left: first!.x, 101 | right: last!.x + last!.width, 102 | } 103 | 104 | copyStore.currentCopyArea = o 105 | 106 | } 107 | }, [cellStore.cellsMap]) 108 | 109 | const mouseEventStore = useContext(MouseEventStoreContext) 110 | const dbc = mouseEventStore.dbcCellAttr 111 | useEffect(() => { 112 | if (!dbc || !dbc.ownKey || dbc.noEdit) return 113 | 114 | copyStore.currentCopyArea = null 115 | }, [dbc]) 116 | 117 | return <>{getCopyArea} 118 | } 119 | 120 | export default observer(CopyArea) 121 | -------------------------------------------------------------------------------- /src/components/layer/selectArea/components/copyArea/styles.module.css: -------------------------------------------------------------------------------- 1 | .copy-area { 2 | 3 | position: absolute 4 | } 5 | 6 | :global .dash-animation :local { 7 | animation-name:ring; 8 | animation-duration:160s; 9 | animation-timing-function: linear; 10 | animation-iteration-count:infinite; 11 | } 12 | @keyframes ring { 13 | from { 14 | stroke-dashoffset:0; 15 | } 16 | to { 17 | stroke-dashoffset:6000; 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/layer/selectArea/components/selectFill/SelectFill.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | startTransition, 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useMemo, 8 | useRef, 9 | useState, 10 | } from 'react' 11 | 12 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 13 | import styles from './styles.module.css' 14 | import { observer } from 'mobx-react-lite' 15 | import { CellAttrs, CellStoreContext, SelectArea } from '@/stores/CellStore' 16 | 17 | import { 18 | getCurrentCellByOwnKey, 19 | getCurrentCellByXY, 20 | getCurrentCellsByArea, 21 | getScrollWidthAndHeight, 22 | } from '@/utils' 23 | import _ from 'lodash' 24 | import { 25 | containerHeight, 26 | containerWidth, 27 | headerCell, 28 | leftCell, 29 | normalCell, 30 | } from '@/utils/constants' 31 | 32 | interface IProps {} 33 | 34 | const SelectFill = (props: any) => { 35 | const cellStore = useContext(CellStoreContext) 36 | 37 | const selectFillArea = cellStore.selectFillArea 38 | const setSelectFillArea = cellStore.setSelectFillArea 39 | 40 | const getSelectFillCell: CSSProperties = useMemo(() => { 41 | var style: CSSProperties = { 42 | position: 'absolute', 43 | } 44 | if (!selectFillArea) return style 45 | 46 | const o = selectFillArea 47 | 48 | style = { 49 | position: 'absolute', 50 | left: o.left, 51 | top: o.top, 52 | width: o.right - o.left, 53 | height: o.bottom - o.top, 54 | } 55 | 56 | return style 57 | }, [selectFillArea]) 58 | 59 | const mouseEventStore = useContext(MouseEventStoreContext) 60 | const dv = mouseEventStore.downCellAttr 61 | const uv = mouseEventStore.upCellAttr 62 | const mv = mouseEventStore.moveCellAttr 63 | 64 | useEffect(() => { 65 | if (mouseEventStore.selectFilling) { 66 | let cur = mv 67 | if (!cur) return 68 | 69 | const start = cellStore.activeCell 70 | 71 | if (start == null) return 72 | 73 | // 回到起点,置为空 74 | if (cur.x == start.x && cur.y == start.y) { 75 | setSelectFillArea(null) 76 | return 77 | } 78 | 79 | if (cur.y != start.y) { 80 | let top = Math.min(start.y, cur.y) 81 | let bottom = Math.max( 82 | start.y + start.height, 83 | cur.y + cur.height 84 | ) 85 | let left = start.x 86 | let right = start.x + start.width 87 | const o = { top, bottom, left, right } 88 | setSelectFillArea(o) 89 | } 90 | 91 | if (cur.x != start.x) { 92 | let top = start.y 93 | let bottom = start.y + start.height 94 | let left = Math.min(start.x, cur.x) 95 | let right = Math.max(start.x + start.width, cur.x + cur.width) 96 | const o = { top, bottom, left, right } 97 | setSelectFillArea(o) 98 | } 99 | } 100 | }, [mv]) 101 | 102 | useEffect(() => { 103 | if (selectFillArea) { 104 | cellStore.setSelectFillAreaCell( 105 | selectFillArea, 106 | cellStore.activeCell 107 | ) 108 | setSelectFillArea(null) 109 | } 110 | mouseEventStore.selectFilling = false 111 | }, [uv]) 112 | 113 | return ( 114 |
118 | ) 119 | } 120 | 121 | export default observer(SelectFill) 122 | -------------------------------------------------------------------------------- /src/components/layer/selectArea/components/selectFill/styles.module.css: -------------------------------------------------------------------------------- 1 | .select-fill-area { 2 | 3 | opacity: 1; 4 | user-select: none; 5 | pointer-events: none; 6 | box-sizing: border-box; 7 | will-change: left,top,width,bottom,right,height; 8 | transform: translateZ(0); 9 | border: 1px dashed #000; 10 | /* box-shadow: rgb(255 255 255) 0px 0px 0px 0.666667px, rgb(31 187 125) 0px 0px 0px 2px; */ 11 | position: absolute; 12 | /* transition: all 60ms; */ 13 | } 14 | -------------------------------------------------------------------------------- /src/components/layer/selectArea/imgs/cur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/layer/selectArea/imgs/cur.png -------------------------------------------------------------------------------- /src/components/layer/selectArea/imgs/img-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/layer/selectArea/styles.module.css: -------------------------------------------------------------------------------- 1 | .select-area { 2 | background-color: rgba(14, 101, 235, 0.1); 3 | opacity: 1; 4 | user-select: none; 5 | pointer-events: none; 6 | box-sizing: border-box; 7 | will-change: left,top,width,bottom,right,height; 8 | transform: translateZ(0); 9 | border: 1px solid rgb(26, 115, 232); 10 | /* box-shadow: rgb(255 255 255) 0px 0px 0px 0.666667px, rgb(26, 115, 232) 0px 0px 0px 2px; */ 11 | position: absolute; 12 | /* transition: all 60ms; */ 13 | } 14 | .active-cell { 15 | position: absolute; 16 | transition: all 70ms; 17 | will-change: left,top,width,bottom,right,height; 18 | 19 | border-width: 2px; 20 | border-color: #1a73e8; 21 | 22 | border-style: solid; 23 | box-sizing: border-box; 24 | } 25 | .active-cell-corner { 26 | pointer-events: auto; 27 | position: absolute; 28 | cursor: crosshair; 29 | font-size: 0; 30 | height: 4px; 31 | width: 4px; 32 | right: -4px; 33 | bottom: -4px; 34 | border: 1px solid #ffffff; 35 | background: #4b89ff; 36 | } 37 | .img-icon { 38 | width: 16px; 39 | height: 16px; 40 | background-image: url(./imgs/img-full.svg); 41 | background-size: cover; 42 | position: absolute; 43 | right: 0; 44 | top:0; 45 | cursor: pointer; 46 | pointer-events: all; 47 | } 48 | 49 | /* :global #canvasWrap { */ 50 | /* cursor: url('./imgs/cur.png'); */ 51 | /* cursor:url("./imgs/cur.png") 7 7, default; */ 52 | /* } */ 53 | -------------------------------------------------------------------------------- /src/components/layer/singleArea/SingleArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | useCallback, 4 | useContext, 5 | useEffect, 6 | useMemo, 7 | useRef, 8 | useState, 9 | } from 'react' 10 | 11 | import { MouseEventStoreContext } from '@/stores/MouseEventStore' 12 | import styles from './styles.module.css' 13 | import { observer } from 'mobx-react-lite' 14 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 15 | import { getCurrentCellByXY } from '@/utils' 16 | import _ from 'lodash' 17 | import { headerCell, leftCell, singleCell } from '@/utils/constants' 18 | 19 | const SingleArea = () => { 20 | const cellStore = useContext(CellStoreContext) 21 | const selectArea = cellStore.selectArea 22 | const activeCell = cellStore.activeCell 23 | 24 | 25 | return ( 26 |
cellStore.areaAllCell()} 29 | style={{ 30 | position: 'absolute', 31 | left: 1, 32 | top: 1, 33 | width: singleCell.width-1, 34 | height: singleCell.height-1, 35 | cursor:'pointer', 36 | background:'#fff', 37 | pointerEvents:'auto', 38 | }} 39 | > 40 |
41 | 42 |
43 | ) 44 | } 45 | 46 | export default observer(SingleArea) 47 | -------------------------------------------------------------------------------- /src/components/layer/singleArea/styles.module.css: -------------------------------------------------------------------------------- 1 | .right-bottom-triangle { 2 | position: absolute; 3 | right: 3px; 4 | bottom: 2px; 5 | width: 0; 6 | height: 0; 7 | border-top: 15px solid #fff; 8 | border-right: 15px solid #ecedee; 9 | border-left: 15px solid #fff; 10 | cursor: pointer; 11 | } -------------------------------------------------------------------------------- /src/components/toolbar/ToolBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | useCallback, 4 | useContext, 5 | useEffect, 6 | useMemo, 7 | useRef, 8 | useState, 9 | } from 'react' 10 | 11 | import { Menu, MenuItem, MenuButton } from '@szhsin/react-menu' 12 | import '@szhsin/react-menu/dist/index.css' 13 | 14 | // import { MouseEventStoreContext } from '@/stores/MouseEventStore' 15 | import ReactTooltip from 'react-tooltip' 16 | import styles from './styles.module.css' 17 | import { observer } from 'mobx-react-lite' 18 | import { CellAttrs, CellStoreContext } from '@/stores/CellStore' 19 | // import { ToolBarStoreContext } from '@/stores/ToolBarStore' 20 | 21 | import _ from 'lodash' 22 | import ColorPanel from './components/ColorPanel' 23 | import { cellDash, floatImageStyle, normalCell } from '@/utils/constants' 24 | import Konva from 'konva' 25 | import { getScrollWidthAndHeight } from '@/utils' 26 | import { ToolBarStoreContext } from '@/stores/ToolBarStore' 27 | import { FloatImageStoreContext } from '@/stores/FloatImageStore' 28 | import useImage from '@/hooks/useImage' 29 | import { useSize } from '@/hooks/useSize' 30 | 31 | interface IProps { 32 | stageRef: React.MutableRefObject 33 | } 34 | 35 | const ToolBar = (props: IProps) => { 36 | const cellStore = useContext(CellStoreContext) 37 | const toolbarStore = useContext(ToolBarStoreContext) 38 | const floatImageStore = useContext(FloatImageStoreContext) 39 | 40 | const mergeCell = () => { 41 | toolbarStore.mergeCell(cellStore) 42 | } 43 | const splitCell = () => { 44 | toolbarStore.splitCell(cellStore) 45 | } 46 | 47 | const colorCell = (color: string) => { 48 | toolbarStore.colorBorderCell(color, cellStore) 49 | } 50 | 51 | const borderStyleCell = (style: string) => { 52 | toolbarStore.dashBorderCell(cellDash[style], cellStore) 53 | } 54 | 55 | const toggleBorderCell = (flag: boolean) => { 56 | toolbarStore.toggleBorderCell(flag, cellStore) 57 | } 58 | const fillCell = (color: string) => { 59 | toolbarStore.fillCell(color, cellStore) 60 | } 61 | 62 | const textBoldCell = () => { 63 | toolbarStore.textBoldCell(cellStore) 64 | } 65 | const textItalicCell = () => { 66 | toolbarStore.textItalicCell(cellStore) 67 | } 68 | const clearCell = ()=>{ 69 | toolbarStore.clearCell(cellStore) 70 | } 71 | 72 | const textUnderLineCell = () => { 73 | toolbarStore.textUnderlineCell(cellStore) 74 | } 75 | 76 | const textColorCell = (color: string) => { 77 | toolbarStore.textColorCell(color, cellStore) 78 | } 79 | 80 | const [curVA, setCurVA] = useState('middle') 81 | const verticalAlignCell = (align: string) => { 82 | setCurVA(align) 83 | toolbarStore.verticalAlignCell(align, cellStore) 84 | } 85 | const getVAlignBtn = () => { 86 | if (curVA == 'middle') { 87 | return
88 | } else if (curVA == 'top') { 89 | return
90 | } else { 91 | return
92 | } 93 | } 94 | 95 | const [curA, setCurA] = useState('left') 96 | const alignCell = (align: string) => { 97 | setCurA(align) 98 | toolbarStore.alignCell(align, cellStore) 99 | } 100 | const getAlignBtn = () => { 101 | if (curA == 'center') { 102 | return
103 | } else if (curVA == 'left') { 104 | return
105 | } else { 106 | return
107 | } 108 | } 109 | 110 | const [curF, setCurF] = useState(normalCell.fontFamily) 111 | const fontFamilyCell = (str: string) => { 112 | setCurF(str) 113 | toolbarStore.fontFamaiyCell(str, cellStore) 114 | } 115 | useEffect(() => { 116 | setCurFS( 117 | cellStore.activeCell?.fontSize 118 | ? cellStore.activeCell?.fontSize 119 | : normalCell.fontSize 120 | ) 121 | setCurF( 122 | cellStore.activeCell?.fontFamily 123 | ? cellStore.activeCell?.fontFamily 124 | : normalCell.fontFamily 125 | ) 126 | }, [cellStore.activeCell]) 127 | 128 | const [curFS, setCurFS] = useState(normalCell.fontSize) 129 | const fontSizeCell = (size: number) => { 130 | setCurFS(size) 131 | toolbarStore.fontSizeCell(size, cellStore) 132 | } 133 | 134 | let { swidth, sheight } = useSize() 135 | 136 | const exportImage = () => { 137 | function downloadURI(uri: string, name: string) { 138 | if (!uri) return 139 | var link = document.createElement('a') 140 | link.download = name 141 | link.href = uri 142 | document.body.appendChild(link) 143 | link.click() 144 | document.body.removeChild(link) 145 | // delete link; 146 | } 147 | 148 | var dataURL = props.stageRef.current?.toDataURL({ 149 | x: 0, 150 | y: 0, 151 | width: swidth, 152 | height: sheight, 153 | pixelRatio: 3, 154 | }) 155 | downloadURI(dataURL || '', `sheet-${Date.now()}.png`) 156 | } 157 | const exportXlsx = () => { 158 | fetch('https://www.nihaoshijie.com.cn/simple-sheet/sheet/downloadxlsx',{ 159 | method: 'POST', 160 | headers: { 161 | 'Content-Type': 'application/json;charset=utf-8' 162 | }, 163 | body: JSON.stringify({desc:JSON.stringify(cellStore.cellsMap)}) 164 | }) 165 | .then(response => { 166 | return response.blob() 167 | }) 168 | .then((blob) => { 169 | 170 | var url = window.URL.createObjectURL(blob); 171 | var a = document.createElement('a'); 172 | a.href = url; 173 | a.download = "simple-sheet-"+Date.now()+".xlsx"; 174 | document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox 175 | a.click(); 176 | a.remove(); //afterwards we remove the element again 177 | }) 178 | .catch(err => console.log('Request Failed', err)); 179 | } 180 | 181 | 182 | const inputRef = useRef(null) 183 | let uploadImgType: string | null = null 184 | 185 | const selecteFileHandler = (event: any) => { 186 | let file = event.target.files[0] 187 | 188 | var pettern = /^image/ 189 | 190 | if (!file) return 191 | 192 | if (!pettern.test(file.type)) { 193 | alert('图片格式不正确') 194 | return 195 | } 196 | var reader = new FileReader() 197 | reader.readAsDataURL(file) 198 | reader.onload = function (e) { 199 | var base64 = reader.result || '' 200 | 201 | if (uploadImgType == 'local') { 202 | const img = base64?.toString() 203 | toolbarStore.uploadImgCell(img, cellStore) 204 | } 205 | if (uploadImgType == 'float') { 206 | let x = floatImageStyle.initX, 207 | y = floatImageStyle.initY 208 | if (cellStore.activeCell) { 209 | x = cellStore.activeCell.x 210 | y = cellStore.activeCell.y 211 | } 212 | floatImageStore.addFloatImage({ 213 | id: Date.now().toString(), 214 | x: x, 215 | y: y, 216 | width: floatImageStyle.initWidth, 217 | height: floatImageStyle.initHeight, 218 | imgUrl: base64?.toString(), 219 | transformObj: null, 220 | }) 221 | cellStore.setActiveCell(null) 222 | } 223 | 224 | inputRef.current!.value = '' 225 | } 226 | } 227 | 228 | const uploadImg = (type: string) => { 229 | uploadImgType = type 230 | if (type == 'local') { 231 | inputRef.current?.click() 232 | } else if (type == 'net') { 233 | let str = prompt('请输入图片URL地址', '') 234 | if (str) { 235 | toolbarStore.uploadImgCell(str, cellStore) 236 | } 237 | } else { 238 | inputRef.current?.click() 239 | } 240 | } 241 | 242 | return ( 243 |
244 | 249 | 250 |
251 |
252 |
253 |
254 |
255 |
256 | 257 |
258 | 259 | 263 |
{curF}
264 |
265 |
266 | } 267 | > 268 | fontFamilyCell('Arial')}> 269 | Arial 270 | 271 | fontFamilyCell('Helvetica')}> 272 | Helvetica 273 | 274 | fontFamilyCell('Calibri')}> 275 | Calibri 276 | 277 | fontFamilyCell('Tahoma')}> 278 | Tahoma 279 | 280 | fontFamilyCell('Times')}> 281 | Times 282 | 283 | 284 | 285 | 289 |
{curFS}
290 |
291 | 292 | } 293 | > 294 | {[10, 12, 14, 16, 18, 20, 22, 24].map((i) => ( 295 | fontSizeCell(i)} key={i}> 296 | {i} 297 | 298 | ))} 299 |
300 | 301 |
302 | 303 | 304 | 308 |
309 | 310 | } 311 | > 312 | 313 | 314 | 315 |
316 |
323 |
328 |
329 |
336 |
341 |
342 |
349 |
354 |
355 |
356 |
357 |
358 |
359 |
364 |
365 |
366 |
371 |
372 | 376 |
377 | 378 | } 379 | > 380 | toggleBorderCell(true)}> 381 |
382 |
385 |
所有框线
386 |
387 |
388 | toggleBorderCell(false)}> 389 |
390 |
393 |
无框线
394 |
395 |
396 |
397 | 398 | 402 |
403 | 404 | } 405 | > 406 | borderStyleCell('solid')}> 407 |
411 |
412 | borderStyleCell('dashed')}> 413 |
417 |
418 | borderStyleCell('dotted')}> 419 |
423 |
424 |
425 | 429 |
430 | 431 | } 432 | > 433 | 434 | 435 | 436 |
437 | 438 | 442 |
443 | 444 | } 445 | > 446 | 447 | 448 | 449 |
450 | 451 | 455 | {getVAlignBtn()} 456 | 457 |
458 | 459 | } 460 | > 461 | verticalAlignCell('top')}> 462 |
463 |
464 | verticalAlignCell('middle')}> 465 |
466 |
467 | verticalAlignCell('bottom')}> 468 |
469 |
470 |
471 | 472 | 476 | {getAlignBtn()} 477 |
478 | 479 | } 480 | > 481 | alignCell('left')}> 482 |
483 |
484 | alignCell('center')}> 485 |
486 |
487 | alignCell('right')}> 488 |
489 |
490 |
491 | 492 | 496 |
497 | 498 | } 499 | > 500 | uploadImg('local')}> 501 |
502 |
505 |
本地图片
506 |
507 |
508 | uploadImg('net')}> 509 |
510 |
513 |
网络图片
514 |
515 |
516 | uploadImg('float')}> 517 |
518 |
521 |
浮动图片
522 |
523 |
524 |
525 | 531 | 532 |
533 | 534 | 538 |
539 | 540 | } 541 | > 542 | 543 | exportImage()}> 544 |
545 |
548 |
导出图片
549 |
550 |
551 | exportXlsx()}> 552 |
553 |
556 |
导出.xlsx
557 |
558 |
559 |
560 | 561 | ) 562 | } 563 | 564 | export default observer(ToolBar) 565 | -------------------------------------------------------------------------------- /src/components/toolbar/components/FloatImage.tsx: -------------------------------------------------------------------------------- 1 | import useImage from '@/hooks/useImage' 2 | import { CellStoreContext } from '@/stores/CellStore' 3 | import { FloatImageStoreContext, TransformObj } from '@/stores/FloatImageStore' 4 | import { ToolBarStoreContext } from '@/stores/ToolBarStore' 5 | import { FloatImage as _FloatImage } from '@/stores/FloatImageStore' 6 | import { normalCell } from '@/utils/constants' 7 | import Konva from 'konva' 8 | import { observer } from 'mobx-react-lite' 9 | import React, { 10 | CSSProperties, 11 | useCallback, 12 | useContext, 13 | useEffect, 14 | useMemo, 15 | useRef, 16 | useState, 17 | } from 'react' 18 | 19 | import { Stage, Text, Group, Rect, Image, Transformer } from 'react-konva' 20 | import _ from 'lodash' 21 | 22 | // interface InnerImage extends Pick<_FloatImage, 'x' | 'y'> {width?:number,height?:number} 23 | 24 | const FloatImage = React.memo( 25 | observer((props: _FloatImage) => { 26 | let { imgUrl, width, height, x, y, id } = props 27 | const floatImageStore = useContext(FloatImageStoreContext) 28 | const cellStore = useContext(CellStoreContext) 29 | 30 | const spacing = 0 31 | 32 | const { 33 | image, 34 | width: imageWidth, 35 | height: imageHeight, 36 | status, 37 | } = useImage({ imgUrl }) 38 | 39 | const trRef = React.useRef(null) 40 | const imgRef = React.useRef(null) 41 | 42 | const aspectRatio = useMemo(() => { 43 | return Math.min( 44 | (width - spacing) / imageWidth, 45 | (height - spacing) / imageHeight 46 | ) 47 | }, [imageWidth, imageHeight, width, height]) 48 | 49 | let _width = Math.min(imageWidth, aspectRatio * imageWidth) 50 | let _height = Math.min(imageHeight, aspectRatio * imageHeight) 51 | 52 | let _x = x, 53 | _y = y 54 | 55 | const isSelected = useMemo(() => { 56 | return id == floatImageStore.currentTransformerId 57 | }, [floatImageStore.currentTransformerId]) 58 | 59 | useEffect(() => { 60 | if (isSelected && trRef.current && imgRef.current) { 61 | trRef.current.nodes([imgRef.current]) 62 | trRef.current.getLayer()!.batchDraw() 63 | } 64 | }, [isSelected, status]) 65 | 66 | if (status !== 'loaded') { 67 | return null 68 | } 69 | 70 | const onSelect = () => { 71 | floatImageStore.currentTransformerId = id 72 | hideArea() 73 | } 74 | 75 | const hideArea = () => { 76 | cellStore.setActiveCell(null) 77 | cellStore.setSelectArea(null) 78 | } 79 | 80 | const onChange = (o: TransformObj) => { 81 | floatImageStore.floatImage.forEach((i) => { 82 | if (i.id == id) { 83 | i.transformObj = { 84 | x: o.x, 85 | y: o.y, 86 | width: o.width, 87 | height: o.height, 88 | } 89 | } 90 | }) 91 | } 92 | 93 | return ( 94 | <> 95 | { 104 | document.body.style.cursor = 'move' 105 | }} 106 | onMouseLeave={(e) => { 107 | document.body.style.cursor = 'default' 108 | }} 109 | onClick={onSelect} 110 | onTap={onSelect} 111 | onDragEnd={(e) => { 112 | onChange({ 113 | x: e.target.x(), 114 | y: e.target.y(), 115 | width: width, 116 | height: height, 117 | }) 118 | }} 119 | onMouseDown={() => hideArea()} 120 | onTransformEnd={(e) => { 121 | const node = imgRef.current 122 | if (!node) return 123 | const scaleX = node.scaleX() 124 | const scaleY = node.scaleY() 125 | 126 | // we will reset it back 127 | // node.scaleX(1); 128 | // node.scaleY(1); 129 | onChange({ 130 | x: node.x(), 131 | y: node.y(), 132 | // set minimal value 133 | width: Math.max(5, node.width() * scaleX), 134 | height: Math.max(node.height() * scaleY), 135 | }) 136 | }} 137 | /> 138 | {isSelected && ( 139 | { 149 | // limit resize 150 | if (newBox.width < 5 || newBox.height < 5) { 151 | return oldBox 152 | } 153 | return newBox 154 | }} 155 | /> 156 | )} 157 | 158 | ) 159 | }) 160 | ) 161 | export default FloatImage 162 | -------------------------------------------------------------------------------- /src/components/toolbar/components/styles.module.css: -------------------------------------------------------------------------------- 1 | .color-table { 2 | width: 200px; 3 | height: 140px; 4 | } 5 | .color-table-item { 6 | width: 20px; 7 | height: 20px; 8 | cursor: pointer; 9 | } 10 | .color-table-item:hover { 11 | border: 1px solid #ddd; 12 | box-sizing: border-box; 13 | } -------------------------------------------------------------------------------- /src/components/toolbar/imgs/align-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/align-center.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/align-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/align-left.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/align-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/align-right.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/back.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/border-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/border-all.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/border-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/border-color.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/border-none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/border-none.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/border-style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/border-style.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/clear.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/cloud-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/cloud-upload.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/computer.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/export-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/export-image.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/file-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/file-export.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/float-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/float-image.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/front.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/image.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/insert-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/insert-img.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/merge-cells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/merge-cells.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/paint-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/paint-fill.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/split-cells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/split-cells.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/text-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/text-color.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/text-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/text-fill.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/text-italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/text-italic.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/triangle-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/triangle-down.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/underline.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/vertical-align-botto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/vertical-align-botto.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/vertical-align-middl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/vertical-align-middl.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/vertical-align-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/vertical-align-top.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/xlsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/xlsx.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/前进-实.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/前进-实.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/导出文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/导出文件.png -------------------------------------------------------------------------------- /src/components/toolbar/imgs/撤销.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvming6816077/simple-sheet/bbf1c7f87b890a51b1ea72826a7abdda63a6f5e3/src/components/toolbar/imgs/撤销.png -------------------------------------------------------------------------------- /src/components/toolbar/styles.module.css: -------------------------------------------------------------------------------- 1 | .tool-bar-wrap { 2 | height: 40px; 3 | display: flex; 4 | background-color: #f8f9fa; 5 | align-items: center; 6 | } 7 | .btn-wrap { 8 | min-width: 30px; 9 | height: 30px; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | cursor: pointer; 14 | 15 | } 16 | .triangle { 17 | width: 10px; 18 | height: 10px; 19 | background-image: url('./imgs/triangle-down.png'); 20 | background-size: cover; 21 | margin-left: 2px; 22 | margin-right: 5px; 23 | } 24 | .btn-wrap:hover { 25 | background-color: #e2e3e4; 26 | } 27 | 28 | .acitve-btn-wrap.btn-wrap { 29 | background-color: #e2e3e4; 30 | } 31 | .merge-cell { 32 | width: 20px; 33 | height: 20px; 34 | background-image: url('./imgs/merge-cells.png'); 35 | background-size: cover; 36 | } 37 | .split-cell { 38 | width: 20px; 39 | height: 20px; 40 | background-image: url('./imgs/split-cells.png'); 41 | background-size: cover; 42 | } 43 | 44 | .border { 45 | width: 20px; 46 | height: 20px; 47 | background-image: url('./imgs/border-all.png'); 48 | background-size: cover; 49 | } 50 | 51 | .border-item { 52 | display: flex; 53 | } 54 | .item-icon-all { 55 | background-image: url('./imgs/border-all.png'); 56 | } 57 | .item-icon-none { 58 | background-image: url('./imgs/border-none.png'); 59 | } 60 | .icon-item { 61 | width: 15px; 62 | height: 15px; 63 | background-size: cover; 64 | margin-right: 4px; 65 | } 66 | 67 | .border-item .item-text { 68 | font-size: 13px; 69 | margin-top: -1px; 70 | } 71 | :global .border-menu .szh-menu__item { 72 | padding-left: 5px; 73 | padding-right: 5px; 74 | padding-top: 5px; 75 | padding-bottom: 5px; 76 | font-size: 14px; 77 | } 78 | :global .react-viewer-transition { 79 | transition: opacity 3ms !important; 80 | } 81 | :global .border-menu { 82 | padding:0; 83 | pointer-events: auto; 84 | } 85 | :global .no .szh-menu__item--hover { 86 | background-color: #fff; 87 | } 88 | 89 | :global .szh-menu { 90 | min-width: 0px; 91 | } 92 | :global .font-size .szh-menu__item { 93 | min-width: 30px; 94 | justify-content: center; 95 | } 96 | 97 | .border-style { 98 | width: 15px; 99 | height: 15px; 100 | background-image: url('./imgs/border-style.png'); 101 | background-size: cover; 102 | } 103 | .border-style-item { 104 | height:11px; 105 | width:62px; 106 | border-width:2px; 107 | border-color: #000; 108 | border-width: 0; 109 | transform: translateY(-4px); 110 | } 111 | 112 | .border-color { 113 | width: 15px; 114 | height: 15px; 115 | background-image: url('./imgs/border-color.png'); 116 | background-size: cover; 117 | } 118 | .paint-fill { 119 | width: 17px; 120 | height: 17px; 121 | background-image: url('./imgs/paint-fill.png'); 122 | background-size: cover; 123 | } 124 | .text-bold { 125 | width: 17px; 126 | height: 17px; 127 | background-image: url('./imgs/text-fill.png'); 128 | background-size: cover; 129 | } 130 | .text-italic { 131 | width: 15px; 132 | height: 15px; 133 | background-image: url('./imgs/text-italic.png'); 134 | background-size: cover; 135 | } 136 | .text-underline { 137 | width: 17px; 138 | height: 17px; 139 | background-image: url('./imgs/underline.png'); 140 | background-size: cover; 141 | } 142 | .text-color { 143 | width: 22px; 144 | height: 22px; 145 | background-image: url('./imgs/text-color.png'); 146 | background-size: cover; 147 | } 148 | .cell-v-align { 149 | width: 20px; 150 | height: 20px; 151 | background-image: url('./imgs/vertical-align-middl.png'); 152 | background-size: cover; 153 | } 154 | .cell-v-align-top { 155 | width: 20px; 156 | height: 20px; 157 | background-image: url('./imgs/vertical-align-top.png'); 158 | background-size: cover; 159 | } 160 | .cell-v-align-middle { 161 | width: 20px; 162 | height: 20px; 163 | background-image: url('./imgs/vertical-align-middl.png'); 164 | background-size: cover; 165 | } 166 | .cell-v-align-bottom { 167 | width: 20px; 168 | height: 20px; 169 | background-image: url('./imgs/vertical-align-botto.png'); 170 | background-size: cover; 171 | } 172 | 173 | .cell-align-left { 174 | width: 20px; 175 | height: 20px; 176 | background-image: url('./imgs/align-left.png'); 177 | background-size: cover; 178 | } 179 | .cell-align-center { 180 | width: 20px; 181 | height: 20px; 182 | background-image: url('./imgs/align-center.png'); 183 | background-size: cover; 184 | } 185 | .cell-align-right { 186 | width: 20px; 187 | height: 20px; 188 | background-image: url('./imgs/align-right.png'); 189 | background-size: cover; 190 | } 191 | .font-family { 192 | font-size: 14px; 193 | margin-left: 5px; 194 | } 195 | :global .ReactTooltip { 196 | padding: 4px !important; 197 | font-size: 12px !important; 198 | } 199 | 200 | .divider { 201 | display: inline-block; 202 | border-right: 1px solid #e0e2e4; 203 | width: 0; 204 | vertical-align: middle; 205 | height: 19px; 206 | margin: 0px 3px 0; 207 | } 208 | .back-cell { 209 | width: 17px; 210 | height: 17px; 211 | background-image: url('./imgs/back.png'); 212 | background-size: cover; 213 | } 214 | .front-cell { 215 | width: 17px; 216 | height: 17px; 217 | background-image: url('./imgs/front.png'); 218 | background-size: cover; 219 | } 220 | .export { 221 | width: 17px; 222 | height: 17px; 223 | background-image: url('./imgs/file-export.png'); 224 | background-size: cover; 225 | } 226 | .export-image { 227 | width: 16px; 228 | height: 16px; 229 | background-image: url('./imgs/image.png'); 230 | background-size: cover; 231 | } 232 | .export-xlsx { 233 | width: 16px; 234 | height: 16px; 235 | background-image: url('./imgs/xlsx.png'); 236 | background-size: cover; 237 | } 238 | .insert-img { 239 | width: 18px; 240 | height: 18px; 241 | background-image: url('./imgs/insert-img.png'); 242 | background-size: cover; 243 | } 244 | .clear-cell { 245 | width: 16px; 246 | height: 16px; 247 | background-image: url('./imgs/clear.png'); 248 | background-size: cover; 249 | } 250 | .item-icon-insert-1 { 251 | background-image: url('./imgs/computer.png'); 252 | } 253 | .item-icon-insert-2 { 254 | background-image: url('./imgs/cloud-upload.png'); 255 | } 256 | .item-icon-insert-3 { 257 | background-image: url('./imgs/float-image.png'); 258 | } 259 | -------------------------------------------------------------------------------- /src/hooks/useImage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export interface UseImageProps { 4 | imgUrl: string 5 | crossOrigin?: string 6 | ownKey?: string 7 | } 8 | 9 | export interface UseImageResults { 10 | image?: HTMLImageElement 11 | width: number 12 | height: number 13 | status: string 14 | } 15 | 16 | const useImage = ({ imgUrl, crossOrigin, ownKey }: UseImageProps) => { 17 | const defaultState = { 18 | image: undefined, 19 | status: 'loading', 20 | width: 0, 21 | height: 0, 22 | } 23 | const [state, setState] = useState(() => defaultState) 24 | 25 | useEffect(() => { 26 | if (!imgUrl) return 27 | var img = new Image() 28 | setState(defaultState) 29 | 30 | function onload() { 31 | setState({ 32 | image: img, 33 | height: img.height, 34 | width: img.width, 35 | status: 'loaded', 36 | }) 37 | } 38 | function onerror() { 39 | setState((prev) => ({ 40 | ...prev, 41 | image: undefined, 42 | status: 'failed', 43 | })) 44 | } 45 | img.addEventListener('load', onload) 46 | img.addEventListener('error', onerror) 47 | 48 | crossOrigin && (img.crossOrigin = crossOrigin) 49 | img.src = imgUrl 50 | 51 | return () => { 52 | img.removeEventListener('load', onload) 53 | img.removeEventListener('error', onerror) 54 | } 55 | }, [imgUrl, crossOrigin]) 56 | 57 | return state 58 | } 59 | 60 | export default useImage 61 | -------------------------------------------------------------------------------- /src/hooks/useMouseEvent.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | 3 | export type MouseHookObj = { 4 | v: T 5 | setValue: (v: T) => void 6 | } 7 | export function useMouseDown(value: T): MouseHookObj { 8 | const [dv, setValue] = useState(value) 9 | 10 | const setDValue = (v: T) => { 11 | setValue(v) 12 | } 13 | 14 | return { v: dv, setValue: setDValue } 15 | } 16 | export function useMouseUp(value: T): MouseHookObj { 17 | const [uv, setValue] = useState(value) 18 | 19 | const setUValue = (v: T) => { 20 | setValue(v) 21 | } 22 | 23 | return { v: uv, setValue: setUValue } 24 | } 25 | export function useMouseMove(callback?: (v: T) => void) { 26 | console.log(callback) 27 | const setMValue = (_v: T) => { 28 | console.log('22') 29 | debugger 30 | callback && callback(_v) 31 | } 32 | 33 | return setMValue 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/useSize.ts: -------------------------------------------------------------------------------- 1 | import { CellStore, CellStoreContext } from '@/stores/CellStore' 2 | import { getScrollWidthAndHeight } from '@/utils' 3 | import { useEffect, useState, useRef, useMemo, useContext } from 'react' 4 | 5 | // canvas实时大小 6 | export const useSize = () => { 7 | const cellStore = useContext(CellStoreContext) 8 | let { swidth, sheight } = useMemo( 9 | () => 10 | getScrollWidthAndHeight( 11 | cellStore.cellsMap, 12 | cellStore.rowStopIndex, 13 | cellStore.columnStopIndex 14 | ), 15 | [cellStore.cellsMap, cellStore.rowStopIndex, cellStore.columnStopIndex] 16 | ) 17 | 18 | return { swidth, sheight } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Grid from './Grid' 2 | export default Grid 3 | -------------------------------------------------------------------------------- /src/stores/CellStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx' 2 | import { createContext, startTransition } from 'react' 3 | import _ from 'lodash' 4 | 5 | import { 6 | clearCellFromat, 7 | generaCell, 8 | getCurrentCellByOwnKey, 9 | getCurrentCellByXY, 10 | getCurrentCellsByArea, 11 | } from '@/utils' 12 | import { 13 | columnStopIndex, 14 | headerCell, 15 | leftCell, 16 | rowStopIndex, 17 | } from '@/utils/constants' 18 | 19 | export type BorderStyle = { 20 | color?: string 21 | strokeDash?: number[] 22 | } 23 | export type CellAttrs = { 24 | x: number 25 | y: number 26 | width: number 27 | height: number 28 | value?: string 29 | key?: string 30 | type?: string 31 | ownKey: string 32 | fill?: string 33 | isMerge?: string[] 34 | borderStyle?: BorderStyle 35 | fontWeight?: string | boolean 36 | textColor?: string 37 | verticalAlign?: string 38 | align?: string 39 | fontFamily?: string 40 | fontSize?: number 41 | fontItalic?: string | boolean 42 | textDecoration?: string | boolean 43 | imgUrl?: string 44 | imgLoaded?: boolean 45 | noEdit?: boolean 46 | } | null 47 | 48 | export type MouseClick = 49 | | ({ 50 | rightClick?: boolean 51 | } & CellAttrs) 52 | | null 53 | 54 | export type RCCellAttrs = 55 | | ({ 56 | clientX: number 57 | clientY: number 58 | } & CellAttrs) 59 | | null 60 | 61 | export type CellMap = { 62 | [key: string]: CellAttrs 63 | } 64 | 65 | export type SelectArea = { 66 | left: number 67 | top: number 68 | bottom: number 69 | right: number 70 | // width:number, 71 | // height:number, 72 | border?: boolean 73 | } | null 74 | 75 | export class CellStore { 76 | @action.bound 77 | mergeCell(list: CellAttrs[]) { 78 | if (list.length < 2) return 79 | 80 | var mergekey: string[] = [ 81 | list[0]!.ownKey, 82 | list[list.length - 1]!.ownKey, 83 | ] 84 | if (list.some(i=>i?.value)) { 85 | alert('合并单元格操作仅会保留右下角数据') 86 | } 87 | list.forEach((i, index) => { 88 | 89 | if (index != list.length - 1) { 90 | // i!.value = '' 91 | clearCellFromat(i) 92 | } 93 | }) 94 | for (var key in this.cellsMap) { 95 | if (_.find(list, { ownKey: key })) { 96 | this.cellsMap[key]!.isMerge = mergekey 97 | } else { 98 | this.cellsMap[key]!.isMerge = 99 | this.cellsMap[key]!.isMerge == undefined 100 | ? undefined 101 | : this.cellsMap[key]!.isMerge 102 | } 103 | } 104 | } 105 | 106 | @action.bound 107 | splitCell(list: CellAttrs[]) { 108 | // var mergekey:string[] = [list[0]!.ownKey,list[list.length-1]!.ownKey] 109 | list.forEach((i, index) => { 110 | i!.isMerge = undefined 111 | }) 112 | // for (var key in this.cellsMap) { 113 | // if (_.find(list,{ownKey:key})) { 114 | // this.cellsMap[key]!.isMerge = mergekey 115 | // } else { 116 | // this.cellsMap[key]!.isMerge = this.cellsMap[key]!.isMerge == undefined ? undefined : this.cellsMap[key]!.isMerge 117 | // } 118 | // } 119 | } 120 | 121 | @action.bound 122 | fillCell(color: string, list: CellAttrs[]) { 123 | list.forEach((i, index) => { 124 | i!.fill = color 125 | }) 126 | } 127 | 128 | @action.bound 129 | areaHeaderCell(ownKey: string) { 130 | // console.log(mergekey)\ 131 | var list = [] 132 | for (var key in this.cellsMap) { 133 | var item = this.cellsMap[key] 134 | if (item?.ownKey.split(':')[1] == ownKey.split(':')[1]) { 135 | if (item.type == 'normal') { 136 | list.push(item) 137 | } 138 | } 139 | } 140 | 141 | var o = { 142 | left: list[0].x, 143 | top: list[0].y, 144 | bottom: list[list.length - 1].y + list[list.length - 1].height, 145 | right: list[0].x + list[0].width, 146 | border: true, 147 | } 148 | this.selectStart = getCurrentCellByXY(o.left, o.top, this.cellsMap) 149 | this.selectEnd = getCurrentCellByXY( 150 | o.left, 151 | list[list.length - 1].y, 152 | this.cellsMap 153 | ) 154 | 155 | this.setSelectArea(o) 156 | this.setActiveCell(null) 157 | } 158 | 159 | @action.bound 160 | areaLeftCell(ownKey: string) { 161 | // console.log(mergekey)\ 162 | var list = [] 163 | for (var key in this.cellsMap) { 164 | var item = this.cellsMap[key] 165 | if (item?.ownKey.split(':')[0] == ownKey.split(':')[0]) { 166 | if (item.type == 'normal') { 167 | list.push(item) 168 | } 169 | } 170 | } 171 | 172 | var o = { 173 | left: list[0].x, 174 | top: list[0].y, 175 | bottom: list[list.length - 1].y + list[list.length - 1].height, 176 | right: list[list.length - 1].x + list[list.length - 1].width, 177 | border: true, 178 | } 179 | 180 | this.selectStart = getCurrentCellByXY(o.left, o.top, this.cellsMap) 181 | this.selectEnd = getCurrentCellByXY( 182 | list[list.length - 1].x, 183 | list[list.length - 1].y, 184 | this.cellsMap 185 | ) 186 | 187 | this.setSelectArea(o) 188 | this.setActiveCell(null) 189 | } 190 | 191 | @action.bound 192 | areaAllCell() { 193 | 194 | var first = this.cellsMap['1:1'] 195 | var last = this.cellsMap[(this.rowStopIndex)+':'+(this.columnStopIndex)] 196 | var list = [first,last] 197 | 198 | 199 | var o = { 200 | left: list[0]!.x, 201 | top: list[0]!.y, 202 | bottom: list[list.length - 1]!.y + list[list.length - 1]!.height, 203 | right: list[list.length - 1]!.x + list[list.length - 1]!.width, 204 | border: true, 205 | } 206 | 207 | this.selectStart = getCurrentCellByXY(o.left, o.top, this.cellsMap) 208 | this.selectEnd = getCurrentCellByXY( 209 | list[list.length - 1]!.x, 210 | list[list.length - 1]!.y, 211 | this.cellsMap 212 | ) 213 | 214 | this.setSelectArea(o) 215 | this.setActiveCell(null) 216 | 217 | } 218 | 219 | // tempx:number = 0 220 | 221 | @action.bound 222 | changeWidth(ownKey: string, newwidth: number) { 223 | var copy: CellMap = this.cellsMap 224 | 225 | for (var key in copy) { 226 | var item = copy[key] 227 | let lk = item?.ownKey?.split(':')[1] 228 | 229 | let rk = ownKey.split(':')[1] 230 | // console.log(ownKey) 231 | if (lk == rk) { 232 | item!.width = newwidth 233 | // console.log(item?.width) 234 | } 235 | } 236 | 237 | this.cellsMap = generaCell( 238 | copy, 239 | this.rowStopIndex, 240 | this.columnStopIndex 241 | ) 242 | 243 | } 244 | 245 | @action.bound 246 | changeHeight(ownKey: string, newheight: number) { 247 | // return 248 | var copy: CellMap = this.cellsMap 249 | 250 | for (var key in copy) { 251 | var item = copy[key] 252 | let lk = item?.ownKey?.split(':')[0] 253 | 254 | let rk = ownKey.split(':')[0] 255 | // console.log(ownKey) 256 | if (lk == rk) { 257 | item!.height = newheight 258 | } 259 | } 260 | 261 | this.cellsMap = generaCell( 262 | copy, 263 | this.rowStopIndex, 264 | this.columnStopIndex 265 | ) 266 | } 267 | 268 | @action.bound 269 | setSelectFillAreaCell(area: SelectArea, current: CellAttrs) { 270 | let cells = getCurrentCellsByArea(area, this.cellsMap) 271 | 272 | let len = cells.length 273 | 274 | if (cells.some((i) => i?.isMerge)) { 275 | alert('若要执行此操作,所有单元格大小需相同') 276 | return 277 | } 278 | 279 | cells.forEach((item, index) => { 280 | item!.fill = current?.fill 281 | if (current?.value && !isNaN(Number(current?.value))) { 282 | item!.value = (Number(current?.value) + index).toString() 283 | } else { 284 | item!.value = current?.value 285 | } 286 | 287 | item!.borderStyle = current?.borderStyle 288 | item!.fontFamily = current?.fontFamily 289 | item!.fontWeight = current?.fontWeight 290 | item!.align = current?.align 291 | item!.fontItalic = current?.fontItalic 292 | item!.textDecoration = current?.textDecoration 293 | item!.fontSize = current?.fontSize 294 | item!.verticalAlign = current?.verticalAlign 295 | item!.textColor = current?.textColor 296 | item!.imgUrl = current?.imgUrl 297 | }) 298 | } 299 | 300 | @action.bound 301 | imgLoadedCell(ownKey: string) { 302 | this.cellsMap[ownKey]!.imgLoaded = true 303 | } 304 | 305 | @action.bound 306 | addCellRowBelow(ownKey: string) { 307 | var row = Number(ownKey.split(':')[0]) 308 | 309 | this.rowStopIndex++ 310 | 311 | var _copy: CellMap = this.cellsMap 312 | 313 | const getPrevMergeK = ( 314 | rowIndex: number, 315 | columnIndex: number, 316 | type: string 317 | ) => { 318 | if (type == 'first') { 319 | if (rowIndex > row) { 320 | return rowIndex + 1 + ':' + columnIndex 321 | } 322 | } 323 | if (type == 'last') { 324 | if (rowIndex > row) { 325 | return rowIndex + 1 + ':' + columnIndex 326 | } 327 | } 328 | return rowIndex + ':' + columnIndex 329 | } 330 | 331 | const getMerge = (ov: string[], rowIndex: number) => { 332 | let res = ov || [] 333 | if (res && res.length) { 334 | var first = res[0] 335 | var firstRow = Number(first.split(':')[0]) 336 | var fristCol = Number(first.split(':')[1]) 337 | res[0] = getPrevMergeK(firstRow, fristCol, 'first') 338 | 339 | var last = res[1] 340 | var lastRow = Number(last.split(':')[0]) 341 | var lastCol = Number(last.split(':')[1]) 342 | 343 | res[1] = getPrevMergeK(lastRow, lastCol, 'last') 344 | 345 | // 发现上一个是merge,就清除 346 | if (rowIndex - 1 == row && lastRow == row) { 347 | return undefined 348 | } 349 | } 350 | 351 | return res.length == 2 ? res : undefined 352 | } 353 | 354 | const getPrevV = (ov: any, rowIndex: number) => { 355 | if (rowIndex - 1 == row) { 356 | return undefined 357 | } 358 | 359 | return ov 360 | } 361 | 362 | const getPrevK = (rowIndex: number, columnIndex: number) => { 363 | if (rowIndex >= row + 1) { 364 | return rowIndex - 1 + ':' + columnIndex 365 | } 366 | 367 | return rowIndex + ':' + columnIndex 368 | } 369 | 370 | this.cellsMap = generaCell( 371 | _copy, 372 | this.rowStopIndex, 373 | this.columnStopIndex, 374 | { getPrevK, getMerge, getPrevV } 375 | ) 376 | } 377 | 378 | @action.bound 379 | addCellRowRight(ownKey: string) { 380 | var col = Number(ownKey.split(':')[1]) 381 | 382 | this.columnStopIndex++ 383 | // this.cellsMap = generaCell(copy,this.rowStopIndex,this.columnStopIndex) 384 | 385 | var _copy: CellMap = this.cellsMap 386 | 387 | const getPrevMergeK = ( 388 | rowIndex: number, 389 | columnIndex: number, 390 | type: string 391 | ) => { 392 | if (type == 'first') { 393 | if (columnIndex > col) { 394 | return rowIndex + ':' + (columnIndex + 1) 395 | } 396 | } 397 | if (type == 'last') { 398 | if (columnIndex > col) { 399 | return rowIndex + ':' + (columnIndex + 1) 400 | } 401 | } 402 | return rowIndex + ':' + columnIndex 403 | } 404 | 405 | const getMerge = ( 406 | ov: string[], 407 | rowIndex: number, 408 | columnIndex: number 409 | ) => { 410 | let res = ov || [] 411 | if (res && res.length) { 412 | var first = res[0] 413 | var firstRow = Number(first.split(':')[0]) 414 | var fristCol = Number(first.split(':')[1]) 415 | res[0] = getPrevMergeK(firstRow, fristCol, 'first') 416 | 417 | var last = res[1] 418 | var lastRow = Number(last.split(':')[0]) 419 | var lastCol = Number(last.split(':')[1]) 420 | 421 | res[1] = getPrevMergeK(lastRow, lastCol, 'last') 422 | 423 | // 发现上一个是merge,就清除 424 | if (columnIndex - 1 == col && lastCol == col) { 425 | return undefined 426 | } 427 | } 428 | 429 | return res.length == 2 ? res : undefined 430 | } 431 | 432 | const getPrevV = (ov: any, rowIndex: number, columnIndex: number) => { 433 | if (columnIndex - 1 == col) { 434 | return undefined 435 | } 436 | 437 | return ov 438 | } 439 | 440 | const getPrevK = (rowIndex: number, columnIndex: number) => { 441 | if (columnIndex >= col + 1) { 442 | return rowIndex + ':' + (columnIndex - 1) 443 | } 444 | 445 | return rowIndex + ':' + columnIndex 446 | } 447 | 448 | this.cellsMap = generaCell( 449 | _copy, 450 | this.rowStopIndex, 451 | this.columnStopIndex, 452 | { getPrevK, getMerge, getPrevV } 453 | ) 454 | } 455 | 456 | // @action.bound 457 | // activeHeader(left: number, right: number) { 458 | 459 | // for (var key in this.cellsMap) { 460 | // var item:any = this.cellsMap[key] 461 | 462 | // if (item?.type == 'header') { 463 | // if (item.x >= left && item.x < right) { 464 | // item!.fill = '#e9eaed' 465 | // } else { 466 | // item!.fill = headerCell.fill 467 | // } 468 | // } 469 | // } 470 | // } 471 | 472 | // @action.bound 473 | // activeLeft(top: number, bottom: number) { 474 | 475 | // for (var key in this.cellsMap) { 476 | // var item:any = this.cellsMap[key] 477 | 478 | // if (item?.type == 'left') { 479 | // if (item.y >= top && item.y < bottom) { 480 | // item!.fill = '#e9eaed' 481 | // } else { 482 | // item!.fill = leftCell.fill 483 | // } 484 | // } 485 | // } 486 | // } 487 | 488 | @action.bound 489 | setSelectArea(o: SelectArea) { 490 | this.selectArea = o 491 | } 492 | 493 | @action.bound 494 | setActiveCell(o: CellAttrs) { 495 | this.activeCell = o 496 | } 497 | 498 | @action.bound 499 | setSelectFillArea(o: SelectArea) { 500 | this.selectFillArea = o 501 | } 502 | 503 | @action.bound 504 | setEditCell(o: CellAttrs) { 505 | this.editCell = o 506 | } 507 | 508 | @observable 509 | rowStopIndex: number = rowStopIndex 510 | 511 | @observable 512 | columnStopIndex: number = columnStopIndex 513 | 514 | @observable 515 | cellsMap: CellMap = generaCell({}, this.rowStopIndex, this.columnStopIndex) 516 | 517 | @observable 518 | selectArea: SelectArea = null 519 | 520 | @observable 521 | selectFillArea: SelectArea = null 522 | 523 | @observable 524 | activeCell: CellAttrs = null 525 | 526 | @observable 527 | selectStart: CellAttrs = null 528 | 529 | @observable 530 | selectEnd: CellAttrs = null 531 | 532 | @observable 533 | editCell: CellAttrs = null 534 | 535 | // @computed 536 | // get getcells() { 537 | 538 | // } 539 | } 540 | 541 | export const CellStoreContext = createContext(new CellStore()) 542 | -------------------------------------------------------------------------------- /src/stores/CopyStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx' 2 | import { createContext } from 'react' 3 | import _ from 'lodash' 4 | 5 | import { 6 | getCurrentCellsByArea, 7 | getCurrentCellByOwnKey, 8 | getCurrentCellByXY, 9 | getCellsByMergeKey, 10 | getCurrentCellsRectByArea, 11 | clearCellFromat, 12 | getLastCell, 13 | } from '@/utils' 14 | 15 | import { 16 | BorderStyle, 17 | CellAttrs, 18 | CellMap, 19 | CellStore, 20 | CellStoreContext, 21 | // SelectArea, 22 | } from './CellStore' 23 | import { defaultBorderStyle } from '@/utils/constants' 24 | import { FloatImageStore } from './FloatImageStore' 25 | 26 | export type CopyCurrentArea = { 27 | left: number 28 | top: number 29 | bottom: number 30 | right: number 31 | } | null 32 | 33 | class CopyStore { 34 | @observable 35 | currentCopyArea: CopyCurrentArea = null 36 | 37 | cutFlag: boolean = false 38 | 39 | @action.bound 40 | async copyCurrentCells(cellStore: CellStore) { 41 | this.cutFlag = false 42 | let arr: any = [[]] 43 | if (cellStore.selectArea) { 44 | this.currentCopyArea = cellStore.selectArea 45 | arr = getCurrentCellsRectByArea( 46 | cellStore.selectArea, 47 | cellStore.cellsMap 48 | ) 49 | 50 | await navigator.clipboard.writeText(JSON.stringify(arr)) 51 | } else if (cellStore.activeCell) { 52 | let cur = getCurrentCellByOwnKey( 53 | cellStore.activeCell?.ownKey || '', 54 | cellStore.cellsMap, 55 | true 56 | ) 57 | this.currentCopyArea = { 58 | left: cur!.x, 59 | top: cur!.y, 60 | bottom: cur!.y + cur!.height, 61 | right: cur!.x + cur!.width, 62 | } 63 | 64 | arr = getCurrentCellsRectByArea( 65 | this.currentCopyArea, 66 | cellStore.cellsMap 67 | ) 68 | } 69 | 70 | try { 71 | await navigator.clipboard.writeText(JSON.stringify(arr)) 72 | } catch (e) { 73 | console.log('用户取消权限') 74 | } 75 | } 76 | 77 | @action.bound 78 | async pasteCurrentCells(cellStore: CellStore) { 79 | let first = null 80 | let text = null 81 | let o = null 82 | 83 | try { 84 | text = await navigator.clipboard.readText() 85 | 86 | o = JSON.parse(text) 87 | } catch (e) { 88 | o = [[{ value: text }]] 89 | } 90 | 91 | if (cellStore.activeCell) { 92 | let cur: any = getCurrentCellByOwnKey( 93 | cellStore.activeCell?.ownKey || '', 94 | cellStore.cellsMap, 95 | ) 96 | 97 | first = cur 98 | 99 | if (this.cutFlag) { 100 | var list = getCurrentCellsByArea( 101 | this.currentCopyArea, 102 | cellStore.cellsMap 103 | ) 104 | list.forEach((i) => { 105 | i!.value = undefined 106 | }) 107 | this.currentCopyArea = null 108 | await navigator.clipboard.writeText('[]') 109 | } 110 | } 111 | 112 | if (o && o.length && first) { 113 | var m = o.length, 114 | n = o[0].length 115 | 116 | var oldFirst = o[0][0] 117 | 118 | var oldFirstRow = Number(oldFirst.ownKey.split(':')[0]) 119 | var oldFirstCol = Number(oldFirst.ownKey.split(':')[1]) 120 | 121 | 122 | var firstRow = Number(first.ownKey.split(':')[0]) 123 | var firstCol = Number(first.ownKey.split(':')[1]) 124 | 125 | var originCellMap = JSON.parse(JSON.stringify(cellStore.cellsMap)) 126 | 127 | var lastCell = getLastCell(cellStore.cellsMap) 128 | 129 | 130 | for (var i = 0; i < m; i++) { 131 | for (var j = 0; j < n; j++) { 132 | var c: CellAttrs | any = 133 | cellStore.cellsMap[i + firstRow + ':' + (j + firstCol)] 134 | if (!c) break 135 | if (c.isMerge) { 136 | alert('不能对合并单元格做部分修改') 137 | // 回复到之前的状态 138 | cellStore.cellsMap = originCellMap 139 | return 140 | break 141 | } 142 | var _o = o[i][j] 143 | delete _o.ownKey // 把原来的ownkey清除 144 | for (var key in c) { 145 | if (_o[key]) { 146 | if (key == 'isMerge') { 147 | 148 | 149 | var oldFirstKey = _o.isMerge[0] 150 | var oldLastKey = _o.isMerge[1] 151 | var l1 = Number(oldFirstKey.split(':')[0])+(firstRow-oldFirstRow) 152 | var l2 = (Number(oldFirstKey.split(':')[1])+(firstCol-oldFirstCol)) 153 | 154 | // 处理边界 155 | l1 = Math.min(Number(lastCell?.ownKey.split(':')[0]),l1) 156 | l2 = Math.min(Number(lastCell?.ownKey.split(':')[1]),l2) 157 | 158 | var c1 = Number(oldLastKey.split(':')[0])+(firstRow-oldFirstRow) 159 | var c2 = (Number(oldLastKey.split(':')[1])+(firstCol-oldFirstCol)) 160 | 161 | // 处理边界 162 | c1 = Math.min(Number(lastCell?.ownKey.split(':')[0]),c1) 163 | c2 = Math.min(Number(lastCell?.ownKey.split(':')[1]),c2) 164 | 165 | 166 | c[key] = [l1 + ':' + l2,c1 + ':' + c2] 167 | 168 | } else { 169 | c[key] = _o[key] 170 | } 171 | 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | @action.bound 180 | async cutCurrentCells(cellStore: CellStore) { 181 | this.copyCurrentCells(cellStore) 182 | this.cutFlag = true 183 | } 184 | 185 | @action.bound 186 | async delCurrentCells( 187 | cellStore: CellStore, 188 | floatImageStore: FloatImageStore 189 | ) { 190 | // 删除图片 191 | if (floatImageStore.currentTransformerId) { 192 | floatImageStore.removeFloatImage( 193 | floatImageStore.currentTransformerId 194 | ) 195 | return 196 | } 197 | 198 | if (cellStore.selectArea) { 199 | let list = getCurrentCellsByArea( 200 | cellStore.selectArea, 201 | cellStore.cellsMap 202 | ) 203 | list.forEach((i) => { 204 | i!.value = undefined 205 | i!.imgUrl = undefined 206 | i!.imgLoaded = undefined 207 | }) 208 | } else if (cellStore.activeCell) { 209 | let cur = getCurrentCellByOwnKey( 210 | cellStore.activeCell?.ownKey || '', 211 | cellStore.cellsMap, 212 | true 213 | ) 214 | 215 | cur!.value = undefined 216 | cur!.imgUrl = undefined 217 | cur!.imgLoaded = undefined 218 | } 219 | } 220 | 221 | @action.bound 222 | changeFloatImage(o: any) {} 223 | } 224 | 225 | export const CopyStoreContext = createContext(new CopyStore()) 226 | -------------------------------------------------------------------------------- /src/stores/FloatImageStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx' 2 | import { createContext } from 'react' 3 | import _ from 'lodash' 4 | 5 | import { 6 | getCurrentCellsByArea, 7 | getCurrentCellByOwnKey, 8 | getCurrentCellByXY, 9 | getCellsByMergeKey, 10 | } from '@/utils' 11 | 12 | import { 13 | BorderStyle, 14 | CellAttrs, 15 | CellMap, 16 | CellStore, 17 | CellStoreContext, 18 | SelectArea, 19 | } from './CellStore' 20 | import { defaultBorderStyle } from '@/utils/constants' 21 | 22 | export type FloatImage = { 23 | id: string 24 | width: number 25 | height: number 26 | x: number 27 | y: number 28 | transformObj: TransformObj | null 29 | imgUrl: string 30 | } 31 | 32 | export type Pick = { 33 | [P in K]: T[P] 34 | } 35 | 36 | export type TransformObj = {} & Pick 37 | 38 | export class FloatImageStore { 39 | @observable 40 | currentTransformerId: string = '' 41 | 42 | @observable 43 | floatImage: FloatImage[] = [] 44 | 45 | @action.bound 46 | addFloatImage(o: FloatImage) { 47 | this.currentTransformerId = o.id 48 | this.floatImage = [...this.floatImage, o] 49 | } 50 | 51 | @action.bound 52 | removeFloatImage(id: string) { 53 | _.remove(this.floatImage, { id: id }) 54 | } 55 | 56 | @action.bound 57 | changeFloatImage(o: FloatImage) { 58 | const index = _.findIndex(this.floatImage, { id: o.id }) 59 | this.floatImage[index] = o 60 | } 61 | } 62 | 63 | export const FloatImageStoreContext = createContext(new FloatImageStore()) 64 | -------------------------------------------------------------------------------- /src/stores/MouseEventStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx' 2 | import { createContext } from 'react' 3 | import { CellAttrs, MouseClick, RCCellAttrs } from './CellStore' 4 | 5 | class MouseEventStore { 6 | lastMoveCellAttr: CellAttrs = null 7 | 8 | @observable 9 | upCellAttr: CellAttrs = null 10 | 11 | @observable 12 | downCellAttr: MouseClick = null 13 | 14 | @observable 15 | moveCellAttr: CellAttrs = null 16 | 17 | @observable 18 | dbcCellAttr: CellAttrs = null 19 | 20 | @observable 21 | rcCellAttr: RCCellAttrs = null 22 | 23 | @action.bound 24 | mouseUp(obj: CellAttrs) { 25 | this.upCellAttr = obj 26 | } 27 | 28 | @action.bound 29 | mouseDown(obj: MouseClick) { 30 | this.downCellAttr = obj 31 | } 32 | 33 | @action.bound 34 | mouseMove(obj: CellAttrs) { 35 | 36 | // 优化,缓存上一次的move结果,使其不会触发多次 37 | if (this.lastMoveCellAttr == null) { 38 | this.moveCellAttr = obj 39 | this.lastMoveCellAttr = obj 40 | } 41 | if ( 42 | this.lastMoveCellAttr && 43 | this.lastMoveCellAttr.ownKey != obj?.ownKey 44 | ) { 45 | this.moveCellAttr = obj 46 | this.lastMoveCellAttr = obj 47 | } 48 | } 49 | 50 | @action.bound 51 | mouseDBC(obj: CellAttrs) { 52 | this.dbcCellAttr = obj 53 | } 54 | 55 | @action.bound 56 | mouseRC(obj: RCCellAttrs) { 57 | this.rcCellAttr = obj 58 | } 59 | 60 | @observable 61 | scrollLeft: number = 0 62 | 63 | @observable 64 | scrollTop: number = 0 65 | 66 | @observable 67 | selectFilling: boolean = false 68 | } 69 | 70 | export const MouseEventStoreContext = createContext(new MouseEventStore()) 71 | -------------------------------------------------------------------------------- /src/stores/ToolBarStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from 'mobx' 2 | import { createContext } from 'react' 3 | import _ from 'lodash' 4 | 5 | import { 6 | getCurrentCellsByArea, 7 | getCurrentCellByOwnKey, 8 | getCurrentCellByXY, 9 | getCellsByMergeKey, 10 | clearCellFromat, 11 | } from '@/utils' 12 | 13 | import { 14 | BorderStyle, 15 | CellAttrs, 16 | CellMap, 17 | CellStore, 18 | CellStoreContext, 19 | SelectArea, 20 | } from './CellStore' 21 | import { defaultBorderStyle } from '@/utils/constants' 22 | import useImage from '@/hooks/useImage' 23 | 24 | class ToolBarStore { 25 | @action.bound 26 | mergeCell(cellStore: CellStore) { 27 | if (!cellStore.selectArea) return 28 | let cells = getCurrentCellsByArea( 29 | cellStore.selectArea, 30 | cellStore.cellsMap 31 | ) 32 | 33 | cellStore.mergeCell(cells) 34 | 35 | let first = cells[0] 36 | let last = cells[cells.length - 1] 37 | 38 | // first!.width = last!.x - first!.x + last!.width 39 | // first!.height = last!.y - first!.y + last!.height 40 | // cells.forEach(i=>{ 41 | // i!.fill = 'red' 42 | // }) 43 | 44 | cellStore.setActiveCell({ 45 | ...first, 46 | width: last!.x - first!.x + last!.width, 47 | height: last!.y - first!.y + last!.height, 48 | } as CellAttrs) 49 | 50 | cellStore.setSelectArea(null) 51 | } 52 | 53 | @action.bound 54 | splitCell(cellStore: CellStore) { 55 | if (cellStore.selectArea) { 56 | let cells = getCurrentCellsByArea( 57 | cellStore.selectArea, 58 | cellStore.cellsMap 59 | ) 60 | 61 | cellStore.splitCell(cells) 62 | } else if (cellStore.activeCell && cellStore.activeCell.isMerge) { 63 | let cells = getCellsByMergeKey( 64 | cellStore.activeCell.isMerge, 65 | cellStore.cellsMap 66 | ) 67 | cellStore.splitCell(cells) 68 | } 69 | } 70 | 71 | dealWithBorderStyle(obj: BorderStyle, cellStore: CellStore, hide = false) { 72 | if (cellStore.selectArea) { 73 | let cells = getCurrentCellsByArea( 74 | cellStore.selectArea, 75 | cellStore.cellsMap 76 | ) 77 | cells.forEach((i) => { 78 | i!.borderStyle = hide 79 | ? undefined 80 | : { 81 | ...i!.borderStyle, 82 | ...obj, 83 | } 84 | }) 85 | } else if (cellStore.activeCell) { 86 | let cell = getCurrentCellByOwnKey( 87 | cellStore.activeCell.ownKey, 88 | cellStore.cellsMap 89 | ) 90 | cell!.borderStyle = hide 91 | ? undefined 92 | : { 93 | ...cell!.borderStyle, 94 | ...obj, 95 | } 96 | } 97 | } 98 | 99 | @action.bound 100 | colorBorderCell(color: string, cellStore: CellStore) { 101 | this.dealWithBorderStyle( 102 | { color: color, strokeDash: this.currentBorderStyle?.strokeDash }, 103 | cellStore 104 | ) 105 | this.currentBorderStyle = { 106 | ...this.currentBorderStyle, 107 | color: color, 108 | } 109 | } 110 | 111 | @action.bound 112 | dashBorderCell(dash: number[], cellStore: CellStore) { 113 | this.dealWithBorderStyle( 114 | { strokeDash: dash, color: this.currentBorderStyle?.color }, 115 | cellStore 116 | ) 117 | this.currentBorderStyle = { 118 | ...this.currentBorderStyle, 119 | strokeDash: dash, 120 | } 121 | } 122 | 123 | @observable 124 | currentBorderStyle: BorderStyle = defaultBorderStyle 125 | 126 | @action.bound 127 | toggleBorderCell(flag: boolean, cellStore: CellStore) { 128 | this.dealWithBorderStyle(this.currentBorderStyle, cellStore, !flag) 129 | 130 | if (flag == false) { 131 | this.currentBorderStyle = defaultBorderStyle 132 | } 133 | } 134 | 135 | @action.bound 136 | fillCell(color: string, cellStore: CellStore) { 137 | if (cellStore.selectArea) { 138 | let cells = getCurrentCellsByArea( 139 | cellStore.selectArea, 140 | cellStore.cellsMap 141 | ) 142 | cells.forEach((i) => { 143 | i!.fill = color 144 | }) 145 | } else if (cellStore.activeCell) { 146 | var isMerge = cellStore.activeCell.isMerge 147 | if (isMerge) { 148 | let cells = getCellsByMergeKey(isMerge, cellStore.cellsMap) 149 | cellStore.fillCell(color, cells) 150 | } else { 151 | let cell = getCurrentCellByOwnKey( 152 | cellStore.activeCell.ownKey, 153 | cellStore.cellsMap 154 | ) 155 | cell!.fill = color 156 | } 157 | } 158 | } 159 | 160 | @observable 161 | currentTextFillBold: boolean | string = false 162 | 163 | @action.bound 164 | textBoldCell(cellStore: CellStore) { 165 | if (this.currentTextFillBold) { 166 | this.currentTextFillBold = false 167 | } else { 168 | this.currentTextFillBold = 'bold' 169 | } 170 | if (cellStore.selectArea) { 171 | let cells = getCurrentCellsByArea( 172 | cellStore.selectArea, 173 | cellStore.cellsMap 174 | ) 175 | if (this.currentTextFillBold) { 176 | cells.forEach((i) => { 177 | i!.fontWeight = 'bold' 178 | }) 179 | } else { 180 | cells.forEach((i) => { 181 | i!.fontWeight = false 182 | }) 183 | } 184 | } else if (cellStore.activeCell) { 185 | var isMerge = cellStore.activeCell.isMerge 186 | let cell = null 187 | if (isMerge) { 188 | cell = getCurrentCellByOwnKey(isMerge[1], cellStore.cellsMap) 189 | } else { 190 | cell = getCurrentCellByOwnKey( 191 | cellStore.activeCell.ownKey, 192 | cellStore.cellsMap 193 | ) 194 | } 195 | cell!.fontWeight = this.currentTextFillBold ? 'bold' : false 196 | } 197 | } 198 | 199 | @action.bound 200 | textColorCell(color: string, cellStore: CellStore) { 201 | if (cellStore.selectArea) { 202 | let cells = getCurrentCellsByArea( 203 | cellStore.selectArea, 204 | cellStore.cellsMap 205 | ) 206 | cells.forEach((i) => { 207 | if (i?.value) { 208 | i!.textColor = color 209 | } 210 | }) 211 | } else if (cellStore.activeCell) { 212 | var isMerge = cellStore.activeCell.isMerge 213 | if (isMerge) { 214 | let cell = getCurrentCellByOwnKey( 215 | isMerge[1], 216 | cellStore.cellsMap 217 | ) 218 | cell!.textColor = color 219 | } else { 220 | let cell = getCurrentCellByOwnKey( 221 | cellStore.activeCell.ownKey, 222 | cellStore.cellsMap 223 | ) 224 | 225 | cell!.textColor = color 226 | } 227 | } 228 | } 229 | 230 | @action.bound 231 | verticalAlignCell(align: string, cellStore: CellStore) { 232 | if (cellStore.selectArea) { 233 | let cells = getCurrentCellsByArea( 234 | cellStore.selectArea, 235 | cellStore.cellsMap 236 | ) 237 | cells.forEach((i) => { 238 | if (i?.value) { 239 | i!.verticalAlign = align 240 | } 241 | }) 242 | } else if (cellStore.activeCell) { 243 | var isMerge = cellStore.activeCell.isMerge 244 | if (isMerge) { 245 | let cell = getCurrentCellByOwnKey( 246 | isMerge[1], 247 | cellStore.cellsMap 248 | ) 249 | cell!.verticalAlign = align 250 | } else { 251 | let cell = getCurrentCellByOwnKey( 252 | cellStore.activeCell.ownKey, 253 | cellStore.cellsMap 254 | ) 255 | 256 | cell!.verticalAlign = align 257 | } 258 | } 259 | } 260 | 261 | @action.bound 262 | alignCell(align: string, cellStore: CellStore) { 263 | if (cellStore.selectArea) { 264 | let cells = getCurrentCellsByArea( 265 | cellStore.selectArea, 266 | cellStore.cellsMap 267 | ) 268 | cells.forEach((i) => { 269 | if (i?.value) { 270 | i!.align = align 271 | } 272 | }) 273 | } else if (cellStore.activeCell) { 274 | var isMerge = cellStore.activeCell.isMerge 275 | if (isMerge) { 276 | let cell = getCurrentCellByOwnKey( 277 | isMerge[1], 278 | cellStore.cellsMap 279 | ) 280 | cell!.align = align 281 | } else { 282 | let cell = getCurrentCellByOwnKey( 283 | cellStore.activeCell.ownKey, 284 | cellStore.cellsMap 285 | ) 286 | 287 | cell!.align = align 288 | } 289 | } 290 | } 291 | 292 | @action.bound 293 | fontFamaiyCell(str: string, cellStore: CellStore) { 294 | if (cellStore.selectArea) { 295 | let cells = getCurrentCellsByArea( 296 | cellStore.selectArea, 297 | cellStore.cellsMap 298 | ) 299 | cells.forEach((i) => { 300 | if (i?.value) { 301 | i!.fontFamily = str 302 | } 303 | }) 304 | } else if (cellStore.activeCell) { 305 | var isMerge = cellStore.activeCell.isMerge 306 | if (isMerge) { 307 | let cell = getCurrentCellByOwnKey( 308 | isMerge[1], 309 | cellStore.cellsMap 310 | ) 311 | cell!.fontFamily = str 312 | } else { 313 | let cell = getCurrentCellByOwnKey( 314 | cellStore.activeCell.ownKey, 315 | cellStore.cellsMap 316 | ) 317 | 318 | cell!.fontFamily = str 319 | } 320 | } 321 | } 322 | 323 | @action.bound 324 | fontSizeCell(size: number, cellStore: CellStore) { 325 | if (cellStore.selectArea) { 326 | let cells = getCurrentCellsByArea( 327 | cellStore.selectArea, 328 | cellStore.cellsMap 329 | ) 330 | cells.forEach((i) => { 331 | if (i?.value) { 332 | i!.fontSize = size 333 | } 334 | }) 335 | } else if (cellStore.activeCell) { 336 | var isMerge = cellStore.activeCell.isMerge 337 | if (isMerge) { 338 | let cell = getCurrentCellByOwnKey( 339 | isMerge[1], 340 | cellStore.cellsMap 341 | ) 342 | cell!.fontSize = size 343 | } else { 344 | let cell = getCurrentCellByOwnKey( 345 | cellStore.activeCell.ownKey, 346 | cellStore.cellsMap 347 | ) 348 | 349 | cell!.fontSize = size 350 | } 351 | } 352 | } 353 | 354 | @observable 355 | currentTextFillItalic: boolean | string = false 356 | 357 | @action.bound 358 | textItalicCell(cellStore: CellStore) { 359 | if (this.currentTextFillItalic) { 360 | this.currentTextFillItalic = false 361 | } else { 362 | this.currentTextFillItalic = 'italic' 363 | } 364 | if (cellStore.selectArea) { 365 | let cells = getCurrentCellsByArea( 366 | cellStore.selectArea, 367 | cellStore.cellsMap 368 | ) 369 | if (this.currentTextFillItalic) { 370 | cells.forEach((i) => { 371 | i!.fontItalic = 'italic' 372 | }) 373 | } else { 374 | cells.forEach((i) => { 375 | i!.fontItalic = false 376 | }) 377 | } 378 | } else if (cellStore.activeCell) { 379 | var isMerge = cellStore.activeCell.isMerge 380 | let cell = null 381 | if (isMerge) { 382 | cell = getCurrentCellByOwnKey(isMerge[1], cellStore.cellsMap) 383 | } else { 384 | cell = getCurrentCellByOwnKey( 385 | cellStore.activeCell.ownKey, 386 | cellStore.cellsMap 387 | ) 388 | } 389 | cell!.fontItalic = this.currentTextFillItalic ? 'bold' : false 390 | } 391 | } 392 | 393 | @observable 394 | currentTextFillUnderline: string = '' 395 | 396 | @action.bound 397 | textUnderlineCell(cellStore: CellStore) { 398 | if (this.currentTextFillUnderline) { 399 | this.currentTextFillUnderline = '' 400 | } else { 401 | this.currentTextFillUnderline = 'underline' 402 | } 403 | if (cellStore.selectArea) { 404 | let cells = getCurrentCellsByArea( 405 | cellStore.selectArea, 406 | cellStore.cellsMap 407 | ) 408 | if (this.currentTextFillUnderline) { 409 | cells.forEach((i) => { 410 | i!.textDecoration = 'underline' 411 | }) 412 | } else { 413 | cells.forEach((i) => { 414 | i!.textDecoration = '' 415 | }) 416 | } 417 | } else if (cellStore.activeCell) { 418 | var isMerge = cellStore.activeCell.isMerge 419 | let cell = null 420 | if (isMerge) { 421 | cell = getCurrentCellByOwnKey(isMerge[1], cellStore.cellsMap) 422 | } else { 423 | cell = getCurrentCellByOwnKey( 424 | cellStore.activeCell.ownKey, 425 | cellStore.cellsMap 426 | ) 427 | } 428 | cell!.textDecoration = this.currentTextFillUnderline 429 | ? 'underline' 430 | : '' 431 | } 432 | } 433 | 434 | @action.bound 435 | uploadImgCell(img: string, cellStore: CellStore) { 436 | if (!cellStore.activeCell) return 437 | var isMerge = cellStore.activeCell.isMerge 438 | let cell = null 439 | if (isMerge) { 440 | cell = getCurrentCellByOwnKey(isMerge[1], cellStore.cellsMap) 441 | } else { 442 | cell = getCurrentCellByOwnKey( 443 | cellStore.activeCell.ownKey, 444 | cellStore.cellsMap 445 | ) 446 | } 447 | 448 | cell!.imgUrl = img 449 | } 450 | 451 | @action.bound 452 | clearCell(cellStore: CellStore){ 453 | if (cellStore.selectArea) { 454 | let cells = getCurrentCellsByArea( 455 | cellStore.selectArea, 456 | cellStore.cellsMap 457 | ) 458 | cells.forEach((i) => { 459 | clearCellFromat(i) 460 | }) 461 | } else if (cellStore.activeCell) { 462 | var isMerge = cellStore.activeCell.isMerge 463 | let cell = null 464 | if (isMerge) { 465 | cell = getCurrentCellByOwnKey(isMerge[1], cellStore.cellsMap) 466 | } else { 467 | cell = getCurrentCellByOwnKey( 468 | cellStore.activeCell.ownKey, 469 | cellStore.cellsMap 470 | ) 471 | } 472 | clearCellFromat(cell) 473 | 474 | } 475 | } 476 | 477 | @observable 478 | currentBigImg: [] = [] 479 | 480 | // @computed 481 | // get getcells() { 482 | 483 | // } 484 | } 485 | 486 | export const ToolBarStoreContext = createContext(new ToolBarStore()) 487 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { GridProps } from '@/Grid' 2 | import { 3 | BorderStyle, 4 | CellAttrs, 5 | CellMap, 6 | CellStoreContext, 7 | } from '@/stores/CellStore' 8 | 9 | export const headerCell = { 10 | fill: '#f8f9fa', 11 | width: 70, 12 | height: 20, 13 | } 14 | 15 | export const leftCell = { 16 | fill: '#f8f9fa', 17 | width: 40, 18 | height: 20, 19 | } 20 | 21 | export const normalCell = { 22 | fill: '#fff', 23 | width: 70, 24 | height: 20, 25 | fontSize: 12, 26 | fontFamily: 'Arial', 27 | } 28 | export const singleCell = { 29 | fill: '#fff', 30 | width: 40, 31 | height: 20, 32 | } 33 | 34 | // export const rowStartIndex: number = 0 35 | 36 | export const rowStopIndex: number = 40 37 | 38 | // export const columnStartIndex: number = 0 39 | 40 | export const columnStopIndex: number = 26 41 | 42 | export var containerWidth: number = 861 43 | 44 | export var containerHeight: number = 621 45 | 46 | export const dragMinWidth: number = 40 47 | 48 | export const dragMinHeight: number = 18 49 | 50 | export const dragHandleHeight: number = 3 51 | 52 | export const dragHandleWidth: number = 3 53 | 54 | export const cellDash: { 55 | [key: string]: number[] 56 | } = { 57 | solid: [], 58 | dashed: [5, 5], 59 | dotted: [2, 2], 60 | } 61 | 62 | export const defaultBorderStyle: BorderStyle = { 63 | color: '#000', 64 | strokeDash: [], 65 | } 66 | 67 | export const floatImageStyle = { 68 | initWidth: 160, 69 | initHeight: 160, 70 | initX: 10, 71 | initY: 10, 72 | } 73 | 74 | export const initConstants = (props: GridProps) => { 75 | containerHeight = props.height || containerHeight 76 | containerWidth = props.width || containerWidth 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CellAttrs, 3 | CellMap, 4 | CellStoreContext, 5 | SelectArea, 6 | } from '@/stores/CellStore' 7 | import _ from 'lodash' 8 | import { useContext } from 'react' 9 | 10 | type PrevFunc = { 11 | getPrevK?: (rowIndex: number, columnIndex: number) => string 12 | getMerge?: (ov: any, rowIndex: number, columnIndex: number) => any 13 | getPrevV?: (ov: any, rowIndex: number, columnIndex: number) => any 14 | } 15 | 16 | import { 17 | headerCell, 18 | leftCell, 19 | normalCell, 20 | singleCell, 21 | // rowStopIndex, 22 | // columnStopIndex, 23 | } from './constants' 24 | 25 | export const getCurrentCellByXY = (x: number, y: number, cellsMap: CellMap) => { 26 | var _cells = _.values(cellsMap) 27 | return _cells[ 28 | _.findIndex(_cells, { 29 | x: x, 30 | y: y, 31 | }) 32 | ] 33 | } 34 | 35 | export const getCurrentCellByOwnKey = ( 36 | key: string, 37 | cellsMap: CellMap, 38 | useMerge: boolean = false 39 | ) => { 40 | var obj = cellsMap[key] 41 | 42 | if (obj?.isMerge && useMerge) { 43 | const [firstkey, endkey] = obj?.isMerge 44 | const o = { 45 | x: cellsMap[firstkey]!.x, 46 | y: cellsMap[firstkey]!.y, 47 | width: 48 | cellsMap[endkey]!.x - 49 | cellsMap[firstkey]!.x + 50 | cellsMap[endkey]!.width, 51 | height: 52 | cellsMap[endkey]!.y - 53 | cellsMap[firstkey]!.y + 54 | cellsMap[endkey]!.height, 55 | } 56 | return { 57 | ...obj, 58 | ...o, 59 | } 60 | } else { 61 | return obj 62 | } 63 | } 64 | export const getCurrentCellsByCol = (colkey: string, cellsMap: CellMap) => { 65 | var _cells = _.values(cellsMap) 66 | 67 | return _cells.filter((i) => { 68 | return i?.ownKey.split(':')[1] == colkey 69 | }) 70 | } 71 | export const getCurrentCellsByRow = (rowkey: string, cellsMap: CellMap) => { 72 | var _cells = _.values(cellsMap) 73 | 74 | return _cells.filter((i) => { 75 | return i?.ownKey.split(':')[0] == rowkey 76 | }) 77 | } 78 | 79 | export const getCellsByMergeKey = (isMerge: string[], cellsMap: CellMap) => { 80 | let first = getCurrentCellByOwnKey(isMerge[0], cellsMap) 81 | let last = getCurrentCellByOwnKey(isMerge[1], cellsMap) 82 | let o = { 83 | left: first?.x, 84 | top: first?.y, 85 | bottom: last!.y + last!.height, 86 | right: last!.x + last!.width, 87 | } 88 | let cells = getCurrentCellsByArea(o as SelectArea, cellsMap) 89 | 90 | return cells 91 | } 92 | 93 | export const getCurrentCellsByArea = (o: SelectArea, cellsMap: CellMap) => { 94 | var _cells = _.values(cellsMap) 95 | if (!o) return [] 96 | 97 | return _cells.filter((i) => { 98 | if (i && o) { 99 | return ( 100 | i.x >= o.left && i.x < o.right && i.y >= o.top && i.y < o.bottom 101 | ) 102 | } else { 103 | return false 104 | } 105 | }) 106 | } 107 | 108 | export const getCellCopyAttr = (cur?: CellAttrs) => { 109 | const o = { 110 | verticalAlign: cur!.verticalAlign, 111 | textColor: cur!.textColor, 112 | textDecoration: cur!.textDecoration, 113 | value: cur!.value, 114 | borderStyle: cur!.borderStyle, 115 | align: cur!.align, 116 | fill: cur!.fill, 117 | fontFamily: cur!.fontFamily, 118 | fontItalic: cur!.fontItalic, 119 | fontSize: cur!.fontSize, 120 | fontWeight: cur!.fontWeight, 121 | // imgLoaded:cur!.imgLoaded, 122 | isMerge:cur!.isMerge, 123 | imgUrl: cur!.imgUrl, 124 | ownKey: cur!.ownKey, 125 | } 126 | 127 | return o 128 | } 129 | 130 | // 二维矩阵cell数据 131 | export const getCurrentCellsRectByArea = (o: SelectArea, cellsMap: CellMap) => { 132 | let list = getCurrentCellsByArea(o, cellsMap) 133 | let first = list[0], 134 | last = list[list.length - 1] 135 | 136 | let firstRow = Number(first!.ownKey.split(':')[0]) 137 | let firstCol = Number(first!.ownKey.split(':')[1]) 138 | 139 | let lastRow = Number(last!.ownKey.split(':')[0]) 140 | 141 | let lastCol = Number(last!.ownKey.split(':')[1]) 142 | 143 | let m = lastRow - firstRow 144 | let n = lastCol - firstCol 145 | let arr = [] 146 | 147 | for (var i = 0; i <= m; i++) { 148 | var k = [] 149 | for (var j = 0; j <= n; j++) { 150 | k.push( 151 | getCellCopyAttr( 152 | _.find(list, { 153 | ownKey: i + firstRow + ':' + (j + firstCol), 154 | }) 155 | ) 156 | ) 157 | } 158 | arr.push(k) 159 | } 160 | 161 | return arr 162 | } 163 | 164 | export const getScrollWidthAndHeight = ( 165 | cellsMap: CellMap, 166 | rowStopIndex: number, 167 | columnStopIndex: number 168 | ) => { 169 | var key1 = '0:' + columnStopIndex 170 | var key2 = rowStopIndex + ':0' 171 | var w: any = cellsMap[key1] 172 | var h: any = cellsMap[key2] 173 | if (w && h) { 174 | return { 175 | swidth: w.x + w.width, 176 | sheight: h.y + h.height, 177 | } 178 | } else { 179 | return { 180 | swidth: 0, 181 | sheight: 0, 182 | } 183 | } 184 | } 185 | 186 | export const getCurrentCellByNextRight = ( 187 | cell: CellAttrs, 188 | cellsMap: CellMap 189 | ) => { 190 | var r = cell?.ownKey.split(':')[0] 191 | var c = Number(cell?.ownKey.split(':')[1]) + 1 192 | 193 | return cellsMap[r + ':' + c] 194 | } 195 | export const getCurrentCellByNextBottom = ( 196 | cell: CellAttrs, 197 | cellsMap: CellMap 198 | ) => { 199 | var r = Number(cell?.ownKey.split(':')[0]) + 1 200 | var c = Number(cell?.ownKey.split(':')[1]) 201 | 202 | return cellsMap[r + ':' + c] 203 | } 204 | export const getCurrentCellByPrevLeft = ( 205 | cell: CellAttrs, 206 | cellsMap: CellMap 207 | ) => { 208 | var r = Number(cell?.ownKey.split(':')[0]) 209 | var c = Number(cell?.ownKey.split(':')[1]) - 1 210 | 211 | return cellsMap[r + ':' + c] 212 | } 213 | const checkLeftByKey = (ownKey: string) => { 214 | var c = Number(ownKey.split(':')[1]) 215 | 216 | return c == 0 217 | } 218 | const checkHeaderByKey = (ownKey: string) => { 219 | var r = Number(ownKey.split(':')[0]) 220 | 221 | return r == 0 222 | } 223 | export const getCurrentCellByPrevTop = (cell: CellAttrs, cellsMap: CellMap) => { 224 | var r = Number(cell?.ownKey.split(':')[0]) - 1 225 | var c = Number(cell?.ownKey.split(':')[1]) 226 | 227 | return cellsMap[r + ':' + c] 228 | } 229 | export const clearCellFromat = (cell: CellAttrs) => { 230 | cell!.value = undefined 231 | cell!.borderStyle = undefined 232 | cell!.imgUrl = undefined 233 | cell!.fontFamily = undefined 234 | cell!.textColor = undefined 235 | cell!.verticalAlign = undefined 236 | cell!.fill = undefined 237 | cell!.align = undefined 238 | cell!.fontSize = undefined 239 | cell!.fontItalic = undefined 240 | cell!.textDecoration = undefined 241 | } 242 | 243 | export const getLastCell = (cellsMap: CellMap)=>{ 244 | let arr = _.values(cellsMap) 245 | 246 | return arr[arr.length-1] 247 | 248 | } 249 | // export const copyToClipboard = (text:string) => { 250 | // // 创建一个文本域 251 | // const textArea = document.createElement('textarea') 252 | // // 隐藏掉这个文本域,使其在页面上不显示 253 | // textArea.style.position = 'fixed' 254 | // textArea.style.visibility = '-10000px' 255 | // // 将需要复制的内容赋值给文本域 256 | // textArea.value = text 257 | // // 将这个文本域添加到页面上 258 | // document.body.appendChild(textArea) 259 | // // 添加聚焦事件,写了可以鼠标选取要复制的内容 260 | // textArea.focus() 261 | // // 选取文本域内容 262 | // textArea.select() 263 | 264 | // if (!document.execCommand('copy')) { // 检测浏览器是否支持这个方法 265 | // console.warn('浏览器不支持 document.execCommand("copy")') 266 | // // 复制失败将构造的标签 移除 267 | // document.body.removeChild(textArea) 268 | // return false 269 | // } else { 270 | // console.log("复制成功") 271 | // // 复制成功后再将构造的标签 移除 272 | // document.body.removeChild(textArea) 273 | // return true 274 | // } 275 | // } 276 | 277 | export const generaCell = ( 278 | prev: CellMap = {}, 279 | rowStopIndex: number, 280 | columnStopIndex: number, 281 | prevFunc: PrevFunc = {} 282 | ) => { 283 | const getRowOffset = ( 284 | rowIndex: number, 285 | columnIndex: number, 286 | map: CellMap 287 | ) => { 288 | const _ownKey = rowIndex - 1 + ':' + columnIndex 289 | const cur = map[_ownKey] 290 | 291 | return cur ? cur.y + (cur.height || 0) : 0 292 | } 293 | const getColumnOffset = ( 294 | rowIndex: number, 295 | columnIndex: number, 296 | map: CellMap 297 | ) => { 298 | const _ownKey = rowIndex + ':' + (columnIndex - 1) 299 | 300 | const cur = map[_ownKey] 301 | 302 | return cur ? cur.x + (cur.width || 0) : 0 303 | } 304 | 305 | const getRowHeight = (type: string, k: string) => { 306 | let v = 0 307 | if (type == 'header') { 308 | v = headerCell.height 309 | } else if (type == 'left') { 310 | v = leftCell.height 311 | } else if (type == 'single') { 312 | v = singleCell.height 313 | } else { 314 | v = normalCell.height 315 | } 316 | 317 | const cur = prev[k.split(':')[0] + ':0'] 318 | return cur ? cur.height : v 319 | } 320 | 321 | const getColumnWidth = (type: string, k: string, isFirst?: boolean) => { 322 | let v = 0 323 | if (type == 'header') { 324 | v = headerCell.width 325 | } else if (type == 'left') { 326 | v = leftCell.width 327 | } else if (type == 'single') { 328 | v = singleCell.width 329 | } else { 330 | v = normalCell.width 331 | } 332 | if (checkLeftByKey(k) && isFirst) { 333 | return v 334 | } 335 | 336 | const cur = prev['0:' + k.split(':')[1]] 337 | return cur ? cur.width : v 338 | } 339 | 340 | const getType = (rowIndex: number, columnIndex: number) => { 341 | if (rowIndex == 0 && columnIndex == 0) return 'single' 342 | if (rowIndex == 0) return 'header' 343 | if (columnIndex == 0) return 'left' 344 | return 'normal' 345 | } 346 | 347 | const getFill = (type: string) => { 348 | if (type == 'header') { 349 | return headerCell.fill 350 | } 351 | if (type == 'left') { 352 | return leftCell.fill 353 | } 354 | if (type == 'normal') { 355 | return undefined 356 | } 357 | 358 | return singleCell.fill 359 | } 360 | 361 | var map: CellMap = {} 362 | for (let rowIndex: number = 0; rowIndex <= rowStopIndex; rowIndex++) { 363 | for ( 364 | let columnIndex: number = 0; 365 | columnIndex <= columnStopIndex; 366 | columnIndex++ 367 | ) { 368 | const type = getType(rowIndex, columnIndex) 369 | 370 | const x = getColumnOffset(rowIndex, columnIndex, map) 371 | 372 | const y = getRowOffset(rowIndex, columnIndex, map) 373 | 374 | const ownKey = rowIndex + ':' + columnIndex 375 | 376 | let k = ownKey 377 | if (prevFunc.getPrevK) { 378 | k = prevFunc.getPrevK(rowIndex, columnIndex) 379 | } 380 | 381 | const dealFirst = 382 | prevFunc.getPrevV && (checkHeaderByKey(k) || checkLeftByKey(k)) 383 | 384 | const width = getColumnWidth(type, k, dealFirst) 385 | 386 | const height = getRowHeight(type, k) 387 | 388 | let isMerge = prev[k]?.isMerge || undefined 389 | if (prevFunc.getMerge) { 390 | isMerge = prevFunc.getMerge(isMerge, rowIndex, columnIndex) 391 | } 392 | 393 | // 增加单元格时不需要复制得内容 394 | let noCopyAttr: any = { 395 | verticalAlign: prev[k]?.verticalAlign || undefined, 396 | textColor: prev[k]?.textColor || undefined, 397 | fontWeight: prev[k]?.fontWeight || undefined, 398 | align: prev[k]?.align || undefined, 399 | fontSize: prev[k]?.fontSize || undefined, 400 | fontFamily: prev[k]?.fontFamily || undefined, 401 | fontItalic: prev[k]?.fontItalic || undefined, 402 | textDecoration: prev[k]?.textDecoration || undefined, 403 | imgUrl: prev[k]?.imgUrl || undefined, 404 | imgLoaded: prev[k]?.imgLoaded || undefined, 405 | noEdit: prev[k]?.noEdit || undefined, 406 | value: prev[k]?.value || undefined, 407 | } 408 | 409 | if (prevFunc.getPrevV) { 410 | for (var key in noCopyAttr) { 411 | noCopyAttr[key] = prevFunc.getPrevV( 412 | noCopyAttr[key], 413 | rowIndex, 414 | columnIndex 415 | ) 416 | } 417 | } 418 | 419 | map[ownKey] = { 420 | x, 421 | y, 422 | width, 423 | height, 424 | type: type, 425 | ownKey: ownKey, 426 | isMerge: dealFirst ? undefined : isMerge, 427 | fill: dealFirst 428 | ? getFill(type) 429 | : prev[k]?.fill || getFill(type), 430 | borderStyle: dealFirst 431 | ? undefined 432 | : prev[k]?.borderStyle || undefined, 433 | ...noCopyAttr, 434 | } 435 | } 436 | } 437 | 438 | return map 439 | } 440 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "declaration":true, 21 | "declarationDir":"types", 22 | "noEmit": true, 23 | "jsx": "react-jsx", 24 | "noFallthroughCasesInSwitch": true, 25 | "paths":{ 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": [ 30 | "src" 31 | ] 32 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const {CleanWebpackPlugin} = require('clean-webpack-plugin') 4 | // const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | 6 | 7 | module.exports = { 8 | mode: "development", 9 | entry: ["./example/src/index.js"], 10 | output: { 11 | path: path.resolve(__dirname, "example/dist"), 12 | filename: "[name].[contenthash].js", 13 | chunkFilename: "[name].[contenthash].js", 14 | publicPath: process.env.NODE_ENV ? "/" : "https://www.nihaoshijie.com.cn/mypro/simple-sheet/", 15 | }, 16 | devtool: 'source-map', 17 | devServer: { 18 | // contentBase: path.resolve(__dirname, "examples/src"), 19 | host: "0.0.0.0", 20 | port: 8001, 21 | historyApiFallback: true, 22 | 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(js|mjs|jsx|ts|tsx)$/, 28 | loader: 'ts-loader', 29 | options: { 30 | transpileOnly: true, 31 | }, 32 | exclude: /dist/, 33 | }, 34 | { 35 | test: /\.css$/i, 36 | use: ["style-loader", "css-loader"], 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | alias: { 42 | "@": path.resolve(__dirname, "src"), 43 | }, 44 | extensions: ['*', '.js', '.tsx','.ts'], 45 | }, 46 | 47 | plugins: [ 48 | new CleanWebpackPlugin(), 49 | new HtmlWebpackPlugin({ 50 | filename: "index.html", 51 | template: path.resolve(__dirname, "example/src/index.html"), 52 | }), 53 | ], 54 | }; --------------------------------------------------------------------------------