├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ └── autofix.yml ├── .gitignore ├── .npmignore ├── README.md ├── SECURITY.md ├── example └── app.js ├── package-lock.json ├── package.json ├── public └── index.html ├── scripts ├── webpack.base.config.js ├── webpack.dev.config.js └── webpack.prod.config.js ├── src ├── App.js ├── App.less ├── App.test.js ├── assets │ ├── images │ │ ├── iconpriority.png │ │ ├── iconpriority2.png │ │ ├── icons.png │ │ └── template.png │ └── kityminder-core │ │ ├── kityminder.core.css │ │ └── kityminder.core.js ├── command │ └── EditorCommand.js ├── common │ └── helpers │ │ ├── axios.js │ │ ├── events.js │ │ ├── jsondiff.js │ │ ├── uploadFile.js │ │ └── utils.js ├── constant │ └── config.minder.js ├── index.css ├── index.js ├── logo.svg ├── mode │ └── Kityminder.js ├── model │ ├── ClipBoard.js │ ├── History.js │ ├── HotBox.js │ ├── Navigator.js │ ├── Runtime.js │ └── ToolBox.js ├── pages │ ├── EditUsers │ │ ├── index.js │ │ └── style.less │ ├── Exterior │ │ ├── index.js │ │ └── style.less │ ├── Mind │ │ ├── ColorPicker │ │ │ ├── index.js │ │ │ └── style.less │ │ ├── HyperLink │ │ │ └── index.js │ │ ├── Image │ │ │ ├── index.js │ │ │ └── style.less │ │ ├── NodeLink │ │ │ └── index.js │ │ ├── index.js │ │ └── style.less │ ├── NavigatorRender │ │ ├── index.js │ │ └── style.less │ ├── NoteRender │ │ ├── index.js │ │ └── style.less │ ├── SearchRenderV2 │ │ ├── ContentSearch │ │ │ └── index.js │ │ ├── RecordStatusSearch │ │ │ └── index.js │ │ ├── index.js │ │ └── style.less │ ├── ShotCut │ │ ├── index.js │ │ └── style.less │ └── ToolBox │ │ ├── Note │ │ ├── index.js │ │ └── style.less │ │ ├── index.js │ │ └── style.less ├── reportWebVitals.js ├── setupTests.js ├── store │ ├── actions │ │ └── index.js │ ├── index.js │ └── reducer │ │ └── index.js └── websocket │ └── Websocket.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "amd": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: ["main"] 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | autofix: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | 18 | - run: npm install --package-lock-only 19 | - run: npm ci 20 | - run: npx prettier --write . 21 | 22 | - uses: autofix-ci/action@8bc06253bec489732e5f9c52884c7cace15c0160 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /node_modules 3 | yarn-error.log 4 | .DS_Store 5 | /dist 6 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # .npmignore 2 | src 3 | examples 4 | .babelrc 5 | .gitignore 6 | webpack.config.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-agiletc-minder-editor 2 | 3 | 基于 react 的 脑图编辑器 🎉 4 | 5 | # 开发和运行 6 | 7 | UI 框架使用 [Ant Design](https://github.com/ant-design/ant-design) 8 | 9 | 将项目克隆到本地在根目录下执行 10 | 11 | ``` 12 | npm install 13 | npm run dev 14 | ``` 15 | 16 | 即可运行项目 17 | 18 | 请配置 `example/app.js` 下的 wsUrl 地址. 19 | 20 | # 集成 21 | 22 | 1. 进入到前端项目的目录下,执行 23 | 24 | ``` 25 | npm install react-agiletc-minder-editor --save 26 | ``` 27 | 28 | 2. 替换项目中引用的组件 29 | 30 | 替换原有的 31 | 32 | `import AgileTCEditor from 'react-agiletc-editor';` 33 | 34 | 为 35 | 36 | `import AgileTCEditor from 'react-agiletc-minder-editor';` 37 | 38 | 3. 重新编译前端即可,最好提前把 server 下 dist 目录先清理掉。 39 | 40 | # API 41 | 42 | | 属性 | 说明 | 类型 | 默认值 | 43 | | -------------- | ------------------------------------------------------------------------ | ---------------- | ------ | 44 | | readOnly | 脑图是否可编辑 | boolean | false | 45 | | editorStyle | 编辑器样式(高度等) | object | | 46 | | baseUrl | 图片上传请求域名 | string | | 47 | | uploadUrl | 图片上传请求接口 | string | | 48 | | wsUrl | websocket 请求地址 | string | | 49 | | onSave | 保存快捷键方法回调,回传脑图全部数据 | Function(object) | | 50 | | type | 是否为只查看 xmind 数据,type 为 compare 时只读 | string | | 51 | | onResultChange | 用例执行的结果状态有变更时会回调,用于实时更新顶部的执行结果占比的进度条 | Function(object) | | 52 | 53 | # FAQ 54 | 55 | 若出现页面的高度问题,即有滚动条的情况,可适当修改下 `editorStyle` 属性高度的值 56 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import ReactDemo from "../src/App"; // 引入组件 4 | 5 | const App = () => ( 6 | { 10 | console.log("o1nResultChange callback"); 11 | }} 12 | editorStyle={{ height: "calc(100vh - 100px)" }} 13 | readOnly={false} 14 | editorRef={(editorNode) => { 15 | console.log(editorNode); 16 | }} 17 | onSave={() => { 18 | console.log("sss"); 19 | }} 20 | type="record" 21 | /> 22 | ); 23 | render(, document.getElementById("root")); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-agiletc-minder-editor", 3 | "version": "0.0.7", 4 | "description": "react脑图编辑器", 5 | "main": "dist/bundle.js", 6 | "scripts": { 7 | "build": "webpack --config ./scripts/webpack.prod.config.js", 8 | "dev": "webpack-dev-server --config ./scripts/webpack.dev.config.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.10.4", 15 | "@babel/plugin-proposal-class-properties": "^7.0.0", 16 | "@babel/plugin-transform-runtime": "^7.16.0", 17 | "@babel/preset-env": "^7.10.4", 18 | "@babel/preset-react": "^7.10.4", 19 | "@hot-loader/react-dom": "^16.13.0", 20 | "autoprefixer": "^9.8.4", 21 | "babel-loader": "^8.1.0", 22 | "babel-plugin-import": "^1.13.0", 23 | "clean-webpack-plugin": "^3.0.0", 24 | "copy-to-clipboard": "^3.3.1", 25 | "css-loader": "^3.6.0", 26 | "file-loader": "6.1.1", 27 | "html-webpack-plugin": "^4.3.0", 28 | "less": "^2.7.2", 29 | "less-loader": "6.0.0", 30 | "optimize-css-assets-webpack-plugin": "^5.0.3", 31 | "postcss-loader": "^3.0.0", 32 | "style-loader": "^1.2.1", 33 | "terser-webpack-plugin": "^3.0.6", 34 | "url-loader": "4.1.1", 35 | "web-vitals": "^1.0.1", 36 | "webpack": "^4.43.0", 37 | "webpack-cli": "^3.3.12", 38 | "webpack-dev-server": "^3.11.0", 39 | "webpack-merge": "^5.0.9", 40 | "webpack-node-externals": "^2.5.0" 41 | }, 42 | "dependencies": { 43 | "antd": "3.26.16", 44 | "axios": "^0.19.0", 45 | "hotbox-ui": "^1.0.0", 46 | "jquery": "^3.6.0", 47 | "kity": "^2.0.4", 48 | "kityminder-core": "^1.4.50", 49 | "lodash": "^4.17.21", 50 | "marked": "^4.0.0", 51 | "prettier": "^2.8.8", 52 | "react": "^16.13.1", 53 | "react-dom": "^16.13.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const webpackConfigBase = { 2 | //module此处为loader区域,一般文件内容解析,处理放在此处,如babel,less,postcss转换等 3 | module: { 4 | rules: [ 5 | { 6 | test: /\.js$/, 7 | exclude: [/node_modules/, /assets/], 8 | use: { 9 | loader: "babel-loader", 10 | options: { 11 | presets: ["@babel/preset-react", "@babel/preset-env"], 12 | plugins: [ 13 | "@babel/plugin-transform-runtime", 14 | ["import", { libraryName: "antd", style: true }], // `style: true` 会加载 less 文件 15 | ], 16 | }, 17 | }, 18 | }, 19 | { 20 | test: /\.css$/, 21 | use: [ 22 | "style-loader", 23 | "css-loader", 24 | { 25 | loader: "postcss-loader", 26 | options: { 27 | ident: "postcss", 28 | plugins: (loader) => [require("autoprefixer")()], 29 | }, 30 | }, 31 | ], 32 | }, 33 | { 34 | test: /\.less$/, 35 | use: [ 36 | { 37 | loader: "style-loader", // creates style nodes from JS strings 38 | }, 39 | { 40 | loader: "css-loader", // translates CSS into CommonJS 41 | }, 42 | { 43 | loader: "less-loader", // compiles Less to CSS 44 | options: { 45 | lessOptions: { 46 | javascriptEnabled: true, 47 | }, 48 | }, 49 | }, 50 | ], 51 | }, 52 | { 53 | test: /.(png|jpg|gif|jpeg)$/, 54 | use: [ 55 | { 56 | loader: "url-loader", 57 | options: { 58 | limit: 40960, // 限制为10k,小于就会base64 59 | }, 60 | }, 61 | ], 62 | }, 63 | ], 64 | }, 65 | }; 66 | module.exports = webpackConfigBase; 67 | -------------------------------------------------------------------------------- /scripts/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const webpackConfigBase = require("./webpack.base.config"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const { merge } = require("webpack-merge"); 6 | 7 | function resolve(relatedPath) { 8 | return path.join(__dirname, relatedPath); 9 | } 10 | 11 | const webpackConfigDev = { 12 | mode: "development", 13 | 14 | entry: { 15 | app: [resolve("../example/app.js")], 16 | }, 17 | 18 | output: { 19 | path: resolve("../lib"), 20 | filename: "editor.js", 21 | }, 22 | 23 | devtool: "cheap-module-eval-source-map", 24 | 25 | devServer: { 26 | contentBase: resolve("../lib"), 27 | hot: true, 28 | open: true, 29 | host: "localhost", 30 | port: 8080, 31 | proxy: { 32 | "/api": "http://localhost:8094", 33 | }, 34 | }, 35 | 36 | plugins: [ 37 | new HtmlWebpackPlugin({ template: "./public/index.html" }), 38 | new webpack.NamedModulesPlugin(), 39 | new webpack.HotModuleReplacementPlugin(), 40 | ], 41 | }; 42 | 43 | module.exports = merge(webpackConfigBase, webpackConfigDev); 44 | -------------------------------------------------------------------------------- /scripts/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | const webpackConfigBase = require("./webpack.base.config"); 5 | const TerserJSPlugin = require("terser-webpack-plugin"); 6 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 7 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 8 | const { merge } = require("webpack-merge"); 9 | 10 | function resolve(relatedPath) { 11 | return path.join(__dirname, relatedPath); 12 | } 13 | 14 | const webpackConfigProd = { 15 | mode: "production", 16 | 17 | entry: { 18 | app: [resolve("../src/App.js")], 19 | }, 20 | 21 | output: { 22 | filename: "bundle.js", 23 | path: resolve("../dist"), 24 | libraryTarget: "commonjs2", 25 | }, 26 | 27 | devtool: "source-map", //或使用'cheap-module-source-map'、'none' 28 | optimization: { 29 | minimizer: [ 30 | // 压缩js代码 31 | new TerserJSPlugin({ 32 | // 多进程压缩 33 | parallel: 4, // 开启多进程压缩 34 | terserOptions: { 35 | compress: { 36 | // drop_console: true, // 删除所有的 `console` 语句 37 | }, 38 | }, 39 | }), 40 | //压缩css代码 41 | new OptimizeCSSAssetsPlugin(), 42 | ], 43 | }, 44 | externals: [nodeExternals()], 45 | 46 | plugins: [ 47 | new CleanWebpackPlugin(), //每次执行都将清空一下./dist目录 48 | ], 49 | }; 50 | module.exports = merge(webpackConfigBase, webpackConfigProd); 51 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./App.less"; 3 | import "kity"; 4 | import "./assets/kityminder-core/kityminder.core.js"; 5 | import "./assets/kityminder-core/kityminder.core.css"; 6 | import * as editorCommand from "./command/EditorCommand"; 7 | import HotBox from "./model/HotBox"; 8 | import History from "./model/History"; 9 | import ToolBox from "./model/ToolBox"; 10 | import Runtime from "./model/Runtime"; 11 | import Navigator from "./model/Navigator"; 12 | import NoteRender from "./pages/NoteRender"; 13 | import SearchRenderV2 from "./pages/SearchRenderV2"; 14 | import ToolBoxRender from "./pages/ToolBox"; 15 | import EditUsersRender from "./pages/EditUsers"; 16 | import NavigatorRender from "./pages/NavigatorRender"; 17 | import ClipBoard from "./model/ClipBoard"; 18 | import Mind from "./pages/Mind"; 19 | import Exterior from "./pages/Exterior"; 20 | import ShotCutModal from "./pages/ShotCut"; 21 | import { 22 | Input, 23 | Tabs, 24 | notification, 25 | Button, 26 | Icon, 27 | Spin, 28 | Tooltip, 29 | Switch, 30 | Drawer, 31 | } from "antd"; 32 | import { isUndefined, isArray, endsWith } from "lodash"; 33 | import Websocket from "./websocket/Websocket"; 34 | 35 | class App extends React.Component { 36 | constructor(props) { 37 | super(props); 38 | this.myRef = React.createRef(); 39 | (this.state = { 40 | url: "", 41 | editText: "", 42 | // 使用的资源 43 | usedResource: [], 44 | // 节点信息 45 | nodeInfo: { 46 | id: "", 47 | text: "", 48 | note: "", 49 | hyperlink: {}, 50 | image: {}, 51 | timeStamp: String(new Date().getTime()), 52 | }, 53 | // 是否选中节点 54 | isNode: false, 55 | // 主题 56 | theme: "fresh-blue-compat", 57 | // 模板 58 | template: "", 59 | // 是否存在撤销 60 | hasUndo: false, 61 | // 是否存在重做 62 | hasRedo: false, 63 | // 工具箱的显示状态 64 | toolbox: false, 65 | // 工具箱的 66 | toolboxTab: "review", 67 | 68 | showTip: false, //是否显示结果文字 69 | curIndex: 0, // 当前处于第一条 70 | resultNum: 0, // 搜索结果共几条 71 | zoom: 100, 72 | triggerActive: true, 73 | fullScreen: false, 74 | tags: ["前置条件", "执行步骤", "预期结果"], 75 | expand: true, 76 | websocketStatus: 0, // 0 异常, 1 正常 77 | readOnly: props.readOnly || false, 78 | spinning: true, // 加载中 79 | caseId: "", 80 | userName: "", 81 | recordId: "", 82 | editUsers: [], 83 | isScore: "0", 84 | minderStatus: "normal", 85 | searchDrawerVisible: false, 86 | }), 87 | (this.baseVersion = 0); 88 | window.editor = {}; 89 | this.timeoutObj = null; 90 | this.SPLITOR = "\uFEFF"; 91 | this.userName = ""; 92 | this.isFirst = true; 93 | } 94 | 95 | componentWillUnmount = () => { 96 | // 清除掉定时上报心跳的定时任务 97 | if (this.timeoutObj != null) { 98 | clearInterval(this.timeoutObj); 99 | this.timeoutObj = null; 100 | } 101 | // 去掉复制粘贴的listener 102 | window.editor.clipBoard.unMount(); 103 | }; 104 | 105 | componentDidMount = () => { 106 | // 处理caseId 以及userName; 107 | // ws://xwcase.gz.cvte.cn/api/case/2245/undefined/0/zsx 108 | // this.initData(this.props.wsUrl); 109 | 110 | let urls = this.props.wsUrl.split("/"); 111 | this.userName = urls[urls.length - 1]; 112 | window.minder.editor = this.userName; 113 | this.setState({ 114 | caseId: urls[5], 115 | recordId: urls[6], 116 | isScore: urls[7], 117 | userName: this.userName, 118 | }); 119 | // 将实例化对象 传回给父组件 120 | if (this.props.editorRef) { 121 | this.props.editorRef(this); 122 | } 123 | 124 | if ( 125 | this.props.type === "" && 126 | this.props.editor.length !== 0 && 127 | this.props.editor.indexOf(this.userName) === -1 128 | ) { 129 | this.disableMinder(); 130 | } 131 | 132 | if (!isUndefined(this.props.tags)) { 133 | this.setState({ 134 | tags: this.props.tags, 135 | }); 136 | } 137 | 138 | // 139 | window.editor = { 140 | runtime: new Runtime(this), 141 | hotbox: new HotBox(this), 142 | history: new History(this), 143 | clipBoard: new ClipBoard(this), 144 | toolbox: new ToolBox(this), 145 | navigator: new Navigator(this), 146 | }; 147 | }; 148 | 149 | componentWillReceiveProps = (nextProps) => { 150 | // 处理当前用户不是编辑人的情况 151 | if ( 152 | nextProps.type === "" && 153 | JSON.stringify(nextProps.editor) !== JSON.stringify(this.props.editor) 154 | ) { 155 | if (nextProps.editor.indexOf(this.userName) === -1) { 156 | window.minder.disable(); 157 | this.setState({ 158 | minderStatus: "disabled", 159 | }); 160 | } 161 | } 162 | }; 163 | 164 | /** 165 | * 处理所有state状态 166 | */ 167 | handleState = (type, value) => { 168 | if (type === "nodeInfo") { 169 | this.setState({ 170 | nodeInfo: value, 171 | isNode: value.id === "" ? false : true, 172 | }); 173 | } else { 174 | this.setState({ 175 | [type]: value, 176 | }); 177 | } 178 | }; 179 | 180 | handleChange = (value) => { 181 | this.setState({ 182 | editText: value, 183 | }); 184 | }; 185 | 186 | getEditText = () => { 187 | return this.state.editText; 188 | }; 189 | 190 | /** 191 | * 返回所有的数据 192 | */ 193 | getAllData = () => { 194 | let root = editorCommand.exportJson(); 195 | root.base = this.baseVersion; 196 | return root; 197 | }; 198 | 199 | /** 200 | * 设置编辑器的数据 201 | * @param {*} value 202 | */ 203 | setEditerData = (value) => { 204 | editorCommand.importJson(value); 205 | }; 206 | 207 | start = () => { 208 | this.timeoutObj = setInterval(() => { 209 | this.sendMessage("0ping ping ping"); 210 | }, 10000); 211 | }; 212 | 213 | handleWsOpen = () => { 214 | if (this.timeoutObj != null) { 215 | clearInterval(this.timeoutObj); 216 | } 217 | 218 | this.setState({ 219 | websocketStatus: 1, 220 | }); 221 | this.start(); 222 | }; 223 | 224 | sendMessage = (message) => { 225 | if (!isUndefined(this.refWebSocket) && this.refWebSocket !== null) { 226 | this.refWebSocket.sendMessage(message); 227 | } 228 | }; 229 | 230 | /** 231 | * 处理服务端的ws的数据 232 | * @param {} message 233 | * @returns 234 | */ 235 | handleWsData = (message) => { 236 | // 收到当前用户的数据 237 | if (message.substring(0, 4) === "当前用户") { 238 | notification.warn({ 239 | message, 240 | }); 241 | } else if (message.substring(0, 1) === "2") { 242 | // 消息回复的信息处理 243 | } else { 244 | if (message === "ping ping ping") { 245 | this.sendMessage("0pong pong pong"); 246 | return; 247 | } 248 | try { 249 | let minderData = JSON.parse(message); 250 | // 区别是更新 还是加载一个新的文件 251 | if (isArray(minderData)) { 252 | if (isArray(minderData[0])) { 253 | if (minderData[0][0].value > parseInt(this.baseVersion)) { 254 | this.baseVersion = minderData[0][0].value; 255 | } 256 | } else { 257 | let temp = minderData.filter((item) => item.path === "/base"); 258 | if (temp[0].value !== parseInt(this.baseVersion) + 1) { 259 | alert( 260 | "版本信息不正确, 请刷新页面同步数据!!!, 否则会导致数据丢失" 261 | ); 262 | } else { 263 | this.baseVersion = temp[0].value; 264 | window.editor.history.isSync = true; 265 | editorCommand.applyPatches(minderData); 266 | // 这里如果是任务执行的情况下 还需要同步任务结果数据 267 | if (this.props.type === "record") { 268 | // 如果是任务执行,需要去回调这个结果 269 | this.onResultChange(); 270 | } 271 | } 272 | } 273 | } else { 274 | if (minderData.type === "all_users") { 275 | // 这里处理用户信息的地方 276 | this.setState({ 277 | editUsers: minderData.data, 278 | }); 279 | } else { 280 | // websocket 链接后的第一次数据传输 281 | this.baseVersion = minderData.base; 282 | editorCommand.importJson(minderData, this.isFirst); 283 | this.isFirst = false; 284 | } 285 | } 286 | } catch (e) { 287 | // 这里需要处理异常 288 | } 289 | } 290 | }; 291 | 292 | /** 293 | * 发送补丁数据 294 | */ 295 | sendPatch = (diff) => { 296 | if (this.state.editUsers.length > 1) { 297 | // 如果说编辑的人超过了1人的话 部分的样式修改 ,需要把它过滤掉 298 | diff = diff.filter((item) => { 299 | return ( 300 | !endsWith(item.path, "layout") && 301 | !endsWith(item.path, "layout_right_offset") && 302 | !endsWith(item.path, "expandState") && 303 | !endsWith(item.path, "layout_right_offset/y") && 304 | !endsWith(item.path, "layout_right_offset/x") 305 | ); 306 | }); 307 | } 308 | 309 | this.sendMessage( 310 | "1" + JSON.stringify({ case: this.getAllData(), patch: [diff] }) 311 | ); 312 | }; 313 | 314 | /** 315 | * 使脑图不可用 316 | */ 317 | disableMinder = () => { 318 | editorCommand.disableMinder(); 319 | this.setState({ 320 | minderStatus: "disabled", 321 | }); 322 | }; 323 | 324 | /** 325 | * 脑图可用 326 | */ 327 | enableMinder = () => { 328 | editorCommand.enableMinder(); 329 | this.setState({ 330 | minderStatus: "normal", 331 | }); 332 | }; 333 | 334 | /** 335 | * ws 连接断开时 336 | * @param {*} e 337 | */ 338 | handleWsClose = () => { 339 | this.setState({ 340 | websocketStatus: 0, 341 | }); 342 | this.refWebSocket = null; 343 | // this.disableMinder() 344 | }; 345 | 346 | handleEditPaste = (e) => { 347 | var clipBoardEvent = e; 348 | try { 349 | // 提取出里面的文本内容 350 | let pasteText = ""; 351 | var textData = clipBoardEvent.clipboardData.getData("text/plain"); 352 | if (textData.indexOf(this.SPLITOR) !== -1) { 353 | const data = textData.split(this.SPLITOR); 354 | let nodes = JSON.parse(data[1]); 355 | 356 | for (const item of nodes) { 357 | if (pasteText !== "") { 358 | pasteText += "\n"; 359 | } 360 | pasteText += item.data.text; 361 | } 362 | this.setState({ 363 | editText: 364 | this.state.editText === "" 365 | ? pasteText 366 | : this.state.editText + "\n" + pasteText, 367 | }); 368 | e.preventDefault(); 369 | } 370 | } catch (e) { 371 | console.log(e); 372 | } 373 | }; 374 | 375 | onMindRef = (ref) => { 376 | this.mindRef = ref; 377 | }; 378 | 379 | /** 380 | * 设置mind的超链接弹出框可见 381 | */ 382 | setMindHyperLinkVisible = () => { 383 | this.mindRef.setHyperlink(true); 384 | }; 385 | 386 | /** 387 | * 获取到record搜索数据的上下文 388 | */ 389 | onRecordSearchRef = (ref) => { 390 | this.recordStatusSearchRef = ref; 391 | }; 392 | 393 | /** 394 | * 执行结果数据发生改变时 395 | */ 396 | onResultChange = () => { 397 | if (this.props.onResultChange) { 398 | this.props.onResultChange(); 399 | } 400 | 401 | if ( 402 | this.state.searchDrawerVisible && 403 | this.props.type === "record" && 404 | this.recordStatusSearchRef.state.recordStatus !== "" 405 | ) { 406 | this.recordStatusSearchRef.handleChange( 407 | this.recordStatusSearchRef.state.recordStatus 408 | ); 409 | } 410 | }; 411 | 412 | render() { 413 | const operations = ( 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | {this.props.readOnly ? null : ( 422 | 432 | { 434 | if (checked) { 435 | this.disableMinder(); 436 | } else { 437 | this.enableMinder(); 438 | } 439 | }} 440 | disabled={ 441 | this.state.websocketStatus === 0 || 442 | (this.props.editor && 443 | this.props.editor.indexOf(this.userName) === -1) 444 | } 445 | checkedChildren="只读" 446 | unCheckedChildren="编辑" 447 | checked={this.state.minderStatus !== "normal"} 448 | /> 449 | 450 | )} 451 | 452 | 453 | {this.state.websocketStatus === 0 && 454 | this.props.type !== "compare" && 455 | this.props.type !== "backup" 456 | ? "连接房间异常,请保存数据" 457 | : ""} 458 | 459 | 477 | 478 | ); 479 | return ( 480 |
485 | {this.props.type !== "compare" && this.props.type !== "backup" && ( 486 | { 493 | notification.warn({ 494 | message: "websocket连接错误", 495 | }); 496 | }} 497 | ref={(Websocket) => { 498 | this.refWebSocket = Websocket; 499 | window.websocket = Websocket; 500 | }} 501 | /> 502 | )} 503 | 504 | 505 | {this.state.searchDrawerVisible && ( 506 | 514 | )} 515 | 516 | { 517 | // 在线用户的逻辑不一样,暂时先隐藏起来 518 | // 522 | } 523 | 524 | {this.props.readOnly ? null : ( 525 | 536 | )} 537 | 538 | 543 | { 544 | 552 | {this.props.type === "compare" ? null : ( 553 | 554 | 572 | 573 | )} 574 | 575 | 576 | 584 | 585 | 586 | } 587 | 588 |
{ 593 | if (!this.minder) { 594 | this.minder = new window.kityminder.Minder({ 595 | renderTo: input, 596 | enableAnimation: false, 597 | defaultTheme: "fresh-blue-compat", 598 | }); 599 | window.minder = this.minder; 600 | window.minder.editor = this.userName; 601 | window.minder.type = 602 | this.props.type === "compare" || 603 | this.props.type === "record" || 604 | this.props.type === "backup" 605 | ? "disable" 606 | : ""; 607 | } 608 | }} 609 | > 610 | 619 | 620 |
625 | this.handleChange(e.target.value)} 629 | onPaste={(e) => this.handleEditPaste(e)} 630 | autoSize={{ minRows: 1, maxRows: 10 }} 631 | /> 632 |
633 |
634 |
635 | ); 636 | } 637 | } 638 | 639 | export default App; 640 | -------------------------------------------------------------------------------- /src/App.less: -------------------------------------------------------------------------------- 1 | .kityminder-editor-container { 2 | position: relative; 3 | height: 100vh; 4 | 5 | .editor-tabs { 6 | background-color: #fff; 7 | .ant-tabs-bar { 8 | margin: 0px !important; 9 | } 10 | } 11 | 12 | .enter-input { 13 | opacity: 1 !important; 14 | } 15 | 16 | .m-input { 17 | position: absolute; 18 | z-index: 999; 19 | } 20 | 21 | .kityminder-core-container { 22 | height: calc(~"100% - 40px"); 23 | } 24 | 25 | .editor-tabs .ant-tabs-content > .ant-tabs-tabpane .ant-btn-link { 26 | color: #333; 27 | } 28 | 29 | .editor-tabs .ant-tabs-content > .ant-tabs-tabpane .ant-btn-link[disabled] { 30 | color: rgba(0, 0, 0, 0.3); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | test("renders learn react link", () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/assets/images/iconpriority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pactortester/react-agiletc-minder-editor/6af891e84d86fd73bad5594dfb9f5c955a40170f/src/assets/images/iconpriority.png -------------------------------------------------------------------------------- /src/assets/images/iconpriority2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pactortester/react-agiletc-minder-editor/6af891e84d86fd73bad5594dfb9f5c955a40170f/src/assets/images/iconpriority2.png -------------------------------------------------------------------------------- /src/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pactortester/react-agiletc-minder-editor/6af891e84d86fd73bad5594dfb9f5c955a40170f/src/assets/images/icons.png -------------------------------------------------------------------------------- /src/assets/images/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pactortester/react-agiletc-minder-editor/6af891e84d86fd73bad5594dfb9f5c955a40170f/src/assets/images/template.png -------------------------------------------------------------------------------- /src/assets/kityminder-core/kityminder.core.css: -------------------------------------------------------------------------------- 1 | .km-view { 2 | font-family: "STHeitiSC-Light", "STHeiti", "Hei", "Heiti SC", 3 | "Microsoft Yahei", Arial, sans-serif; 4 | -webkit-user-select: none; 5 | user-select: none; 6 | position: relative; 7 | } 8 | 9 | .km-view .km-receiver { 10 | position: absolute; 11 | left: -99999px; 12 | top: -99999px; 13 | width: 20px; 14 | height: 20px; 15 | outline: none; 16 | margin: 0; 17 | } 18 | 19 | .km-view image { 20 | cursor: zoom-in; 21 | } 22 | 23 | .km-image-viewer { 24 | position: fixed; 25 | z-index: 99999; 26 | top: 0; 27 | bottom: 0; 28 | left: 0; 29 | right: 0; 30 | background: rgba(0, 0, 0, 0.75); 31 | } 32 | 33 | .km-image-viewer .km-image-viewer-container { 34 | position: absolute; 35 | top: 0; 36 | bottom: 0; 37 | left: 0; 38 | right: 0; 39 | text-align: center; 40 | white-space: nowrap; 41 | overflow: auto; 42 | } 43 | 44 | .km-image-viewer .km-image-viewer-container::before { 45 | content: ""; 46 | display: inline-block; 47 | height: 100%; 48 | width: 0; 49 | font-size: 0; 50 | vertical-align: middle; 51 | } 52 | 53 | .km-image-viewer .km-image-viewer-container img { 54 | cursor: zoom-out; 55 | vertical-align: middle; 56 | } 57 | 58 | .km-image-viewer .km-image-viewer-container img.limited { 59 | cursor: zoom-in; 60 | max-width: 100%; 61 | max-height: 100%; 62 | } 63 | 64 | .km-image-viewer .km-image-viewer-toolbar { 65 | z-index: 1; 66 | background: rgba(0, 0, 0, 0.75); 67 | text-align: right; 68 | transition: all 0.25s; 69 | } 70 | 71 | .km-image-viewer .km-image-viewer-toolbar.hidden { 72 | transform: translate(0, -100%); 73 | opacity: 0; 74 | } 75 | 76 | .km-image-viewer .km-image-viewer-btn { 77 | cursor: pointer; 78 | outline: 0; 79 | border: 0; 80 | width: 44px; 81 | height: 44px; 82 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjY0IiBoZWlnaHQ9Ijg4IiB2aWV3Qm94PSIwIDAgMjY0IDg4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5kZWZhdWx0LXNraW4gMjwvdGl0bGU+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Zz48cGF0aCBkPSJNNjcuMDAyIDU5LjV2My43NjhjLTYuMzA3Ljg0LTkuMTg0IDUuNzUtMTAuMDAyIDkuNzMyIDIuMjItMi44MyA1LjU2NC01LjA5OCAxMC4wMDItNS4wOThWNzEuNUw3MyA2NS41ODUgNjcuMDAyIDU5LjV6IiBpZD0iU2hhcGUiIGZpbGw9IiNmZmYiLz48ZyBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMTMgMjl2LTVoMnYzaDN2MmgtNXpNMTMgMTVoNXYyaC0zdjNoLTJ2LTV6TTMxIDE1djVoLTJ2LTNoLTN2LTJoNXpNMzEgMjloLTV2LTJoM3YtM2gydjV6IiBpZD0iU2hhcGUiLz48L2c+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTYyIDI0djVoLTJ2LTNoLTN2LTJoNXpNNjIgMjBoLTV2LTJoM3YtM2gydjV6TTcwIDIwdi01aDJ2M2gzdjJoLTV6TTcwIDI0aDV2MmgtM3YzaC0ydi01eiIvPjwvZz48cGF0aCBkPSJNMjAuNTg2IDY2bC01LjY1Ni01LjY1NiAxLjQxNC0xLjQxNEwyMiA2NC41ODZsNS42NTYtNS42NTYgMS40MTQgMS40MTRMMjMuNDE0IDY2bDUuNjU2IDUuNjU2LTEuNDE0IDEuNDE0TDIyIDY3LjQxNGwtNS42NTYgNS42NTYtMS40MTQtMS40MTRMMjAuNTg2IDY2eiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0xMTEuNzg1IDY1LjAzTDExMCA2My41bDMtMy41aC0xMHYtMmgxMGwtMy0zLjUgMS43ODUtMS40NjhMMTE3IDU5bC01LjIxNSA2LjAzeiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0xNTIuMjE1IDY1LjAzTDE1NCA2My41bC0zLTMuNWgxMHYtMmgtMTBsMy0zLjUtMS43ODUtMS40NjhMMTQ3IDU5bDUuMjE1IDYuMDN6IiBmaWxsPSIjZmZmIi8+PGc+PHBhdGggaWQ9IlJlY3RhbmdsZS0xMSIgZmlsbD0iI2ZmZiIgZD0iTTE2MC45NTcgMjguNTQzbC0zLjI1LTMuMjUtMS40MTMgMS40MTQgMy4yNSAzLjI1eiIvPjxwYXRoIGQ9Ik0xNTIuNSAyN2MzLjAzOCAwIDUuNS0yLjQ2MiA1LjUtNS41cy0yLjQ2Mi01LjUtNS41LTUuNS01LjUgMi40NjItNS41IDUuNSAyLjQ2MiA1LjUgNS41IDUuNXoiIGlkPSJPdmFsLTEiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTUwIDIxaDV2MWgtNXoiLz48L2c+PGc+PHBhdGggZD0iTTExNi45NTcgMjguNTQzbC0xLjQxNCAxLjQxNC0zLjI1LTMuMjUgMS40MTQtMS40MTQgMy4yNSAzLjI1eiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0xMDguNSAyN2MzLjAzOCAwIDUuNS0yLjQ2MiA1LjUtNS41cy0yLjQ2Mi01LjUtNS41LTUuNS01LjUgMi40NjItNS41IDUuNSAyLjQ2MiA1LjUgNS41IDUuNXoiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA2IDIxaDV2MWgtNXoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA5LjA0MyAxOS4wMDhsLS4wODUgNS0xLS4wMTcuMDg1LTV6Ii8+PC9nPjwvZz48L2c+PC9zdmc+); 83 | } 84 | 85 | .km-image-viewer .km-image-viewer-toolbar { 86 | position: absolute; 87 | top: 0; 88 | left: 0; 89 | right: 0; 90 | } 91 | 92 | .km-image-viewer .km-image-viewer-close { 93 | background-position: 0 -44px; 94 | } 95 | -------------------------------------------------------------------------------- /src/command/EditorCommand.js: -------------------------------------------------------------------------------- 1 | import { isEmpty, isUndefined, isNull } from "lodash"; 2 | 3 | /** 4 | * @desc 判断当前是否有节点选中 5 | * @param window.minder 思维导图实例 6 | * 7 | */ 8 | export function isNode() { 9 | return !!window.minder.getSelectedNode(); 10 | } 11 | 12 | export function getSelectedNode() { 13 | return window.minder.getSelectedNode(); 14 | } 15 | 16 | /** 17 | * 直接刷新当前的节点数据 18 | */ 19 | export function renderCurrentNode() { 20 | window.minder.getSelectedNode().render(); 21 | window.minder.fire("contentchange"); 22 | } 23 | 24 | /** 25 | * @desc 导入JSON 26 | * @param window.minder 思维导图实例 27 | * @param flag 是否第一次 28 | */ 29 | export function importJson(data, flag) { 30 | const currentNode = window.minder.getSelectedNode(); 31 | 32 | window.minder.importJson(data); 33 | 34 | if (flag) { 35 | cameraRoot(); 36 | } 37 | if (currentNode == null) { 38 | selectRoot(); 39 | } else { 40 | window.minder.selectById(currentNode.getData("id"), true); 41 | } 42 | } 43 | 44 | /** 45 | * @desc 导出JSON 46 | * @param window.minder 思维导图实例 47 | * 48 | */ 49 | export function exportJson() { 50 | return window.minder.exportJson(); 51 | } 52 | 53 | /** 54 | * @desc 选中根节点 55 | * @param window.minder 思维导图实例 56 | * 57 | */ 58 | export function selectRoot() { 59 | window.minder.select(window.minder.getRoot(), true); 60 | } 61 | 62 | /** 63 | * @desc 插入主题 64 | * @param window.minder 思维导图实例 65 | * @param type {string} 插入的主题类型 66 | */ 67 | export function handleAppend(type, text) { 68 | switch (type) { 69 | case "childNode": 70 | isNode() && window.minder.execCommand("AppendChildNode", text); 71 | break; 72 | case "parentNode": 73 | isNode() && window.minder.execCommand("AppendParentNode", text); 74 | break; 75 | case "siblingNode": 76 | isNode() && window.minder.execCommand("AppendSiblingNode", text); 77 | break; 78 | default: 79 | break; 80 | } 81 | } 82 | 83 | /** 84 | * @desc 编辑优先级 85 | * @param window.minder 思维导图实例 86 | * @param key {string} 优先级 87 | */ 88 | export function handlePriority(key) { 89 | isNode() && window.minder.execCommand("priority", key); 90 | } 91 | 92 | /** 93 | * @desc 编辑完成情况 94 | * @param window.minder 思维导图实例 95 | * @param key {string} 完成情况 96 | */ 97 | export function handleProgress(key) { 98 | isNode() && window.minder.execCommand("progress", key); 99 | } 100 | 101 | /** 102 | * 设置用例执行结果的情况 103 | * @param {}} key 104 | */ 105 | export function handleResult(key) { 106 | let indexTocommandValueMap = { 107 | 0: 0, // 去掉结果 108 | 1: 1, // 不通过 109 | 2: 9, // 通过 110 | 3: 5, // 阻塞 111 | 4: 4, // 不执行 112 | }; 113 | 114 | window.minder.execCommand("result", indexTocommandValueMap[parseInt(key)]); 115 | } 116 | 117 | /** 118 | * 更新节点的执行人 119 | * @param {*} name 120 | */ 121 | export function handleExecutor(name) { 122 | const nodes = window.minder.getSelectedNodes(); 123 | for (const item of nodes) { 124 | item.setData("executor", name); 125 | } 126 | } 127 | 128 | /** 129 | * @desc 插入/移除文本 130 | * @param window.minder 思维导图实例 131 | * @param note {string} 备注信息 132 | */ 133 | export function handleText(text, nodeId) { 134 | window.minder.execCommand("text", text, nodeId); 135 | } 136 | 137 | /** 138 | * @desc 设置标签-非当前选中的节点 139 | * @param window.minder 思维导图实例 140 | * @param tag {string} 标签列表 141 | */ 142 | export function handleResourceById(tag, nodeId) { 143 | const nodes = window.minder.getNodesById(nodeId); // 注:此方法更新时貌似不触发contentChange事件,故注释 144 | !isEmpty(nodes) && nodes[0].setData("resource", tag).render(); 145 | window.minder.layout(100); 146 | window.minder.fire("contentchange"); 147 | } 148 | 149 | /** 150 | * @desc 设置标签-当前选中的节点 151 | * @param window.minder 思维导图实例 152 | * @param tag {string} 标签列表 153 | */ 154 | export function handleResource(tag, flag) { 155 | window.minder.execCommand("Resource", tag, flag); 156 | } 157 | 158 | /** 159 | * 获取选中节点的标签列表 160 | * @returns 161 | */ 162 | export function getResource() { 163 | return window.minder.queryCommandValue("resource"); 164 | } 165 | 166 | /** 167 | * 获取所有的标签列表 168 | */ 169 | export function getAllResource() { 170 | return window.minder.getUsedResource(); 171 | } 172 | 173 | /** 174 | * @desc 通过节点id获取当前节点的标签 175 | * @param window.minder 思维导图实例 176 | * @param tag {string} 标签列表 177 | */ 178 | export function getResourceById(nodeId) { 179 | const nodes = window.minder.getNodesById(nodeId); 180 | let resource = []; 181 | if (!isEmpty(nodes) && nodes[0].getData("resource")) 182 | resource = nodes[0].getData("resource"); 183 | return resource; 184 | } 185 | 186 | /** 187 | * @desc 插入/移除备注 188 | * @param window.minder 思维导图实例 189 | * @param note {string} 备注信息 190 | */ 191 | export function handleNote(note, nodeId) { 192 | const currentNode = window.minder.getSelectedNode(); 193 | window.minder.selectById(nodeId, true); 194 | window.minder.execCommand("Note", note); 195 | window.minder.selectById( 196 | isNull(currentNode) ? null : currentNode.getData("id"), 197 | true 198 | ); 199 | 200 | /* window.minder.selectById(isNull(currentNode) ? null : currentNode.getData('id'), true); 201 | const nodes = window.minder.getNodesById(nodeId); 202 | !isEmpty(nodes) && nodes[0].setData('note', note).render(); 203 | window.minder.layout(100); */ 204 | } 205 | 206 | /** 207 | * @desc 获取备注 208 | * @param window.minder 思维导图实例 209 | * @param note {string} 备注信息 210 | */ 211 | export function getNote() { 212 | const note = window.minder.queryCommandValue("Note"); 213 | if (isUndefined(note) || isNull(note)) { 214 | return ""; 215 | } 216 | return note; 217 | } 218 | 219 | /** 220 | * @desc 获取文本 221 | * @param window.minder 思维导图实例 222 | * @param text {string} 文本 223 | */ 224 | export function getText() { 225 | return window.minder.queryCommandValue("Text"); 226 | } 227 | 228 | /** 229 | * @desc 移除节点 230 | * @param window.minder 思维导图实例 231 | * 232 | */ 233 | let enable = true; 234 | export function handleRemove() { 235 | const nodes = window.minder.getSelectedNodes(); 236 | if (isEmpty(nodes)) return; 237 | enable = true; 238 | checkSourceId(nodes); 239 | enable && window.minder.execCommand("RemoveNode"); 240 | } 241 | 242 | function checkSourceId(nodes) { 243 | nodes.map((node) => { 244 | if (!isNull(node.data.sourceId) && !isUndefined(node.data.sourceId)) { 245 | // message.error('所选节点或其子节点中包含关联接口,请先删除接口关联关系!'); 246 | enable = false; 247 | return; 248 | } 249 | if (node.children !== []) { 250 | checkSourceId(node.children); 251 | } 252 | }); 253 | } 254 | 255 | /** 256 | * @desc 撤销重做 257 | * @param window.minder 思维导图实例 258 | * 259 | */ 260 | export function applyPatches(diff) { 261 | window.minder.applyPatches(diff); 262 | } 263 | 264 | /** 265 | * @desc 设置根节点到视野中心 266 | * @param window.minder 思维导图实例 267 | * 268 | */ 269 | export function cameraRoot() { 270 | window.minder.execCommand("camera", window.minder.getRoot(), 600); 271 | } 272 | 273 | /** 274 | * @desc 放大视野到下一个百分比 275 | * @param window.minder 思维导图实例 276 | * 277 | */ 278 | export function zoomIn() { 279 | window.minder.execCommand("ZoomIn"); 280 | } 281 | 282 | /** 283 | * @desc 缩小视野到上一个百分比 284 | * @param window.minder 思维导图实例 285 | * 286 | */ 287 | export function zoomOut() { 288 | window.minder.execCommand("ZoomOut"); 289 | } 290 | 291 | /** 292 | * @desc 缩放视野到一定比例 293 | * @param window.minder 思维导图实例 294 | * 295 | */ 296 | export function zoom(value) { 297 | window.minder.execCommand("Zoom", value); 298 | } 299 | 300 | /** 301 | * @desc 切换脑图的抓手状态 302 | * @param window.minder 思维导图实例 303 | * 304 | */ 305 | export function hand() { 306 | window.minder.execCommand("Hand"); 307 | if (window.minder._status === "hand") { 308 | window.minder.selectById(null, true); 309 | } 310 | } 311 | 312 | /** 313 | * @desc 获取节点超链接 314 | * @param window.minder 思维导图实例 315 | * 316 | */ 317 | export function getHyperlink() { 318 | const hyperLink = window.minder.queryCommandValue("HyperLink"); 319 | if (isUndefined(hyperLink.url) || isNull(hyperLink.url)) { 320 | return {}; 321 | } 322 | return hyperLink; 323 | } 324 | 325 | /** 326 | * @desc 设置节点超链接 327 | * @param window.minder 思维导图实例 328 | * 329 | */ 330 | export function handleHyperlink(params) { 331 | window.minder.execCommand("HyperLink", params.url, params.title); 332 | } 333 | 334 | /** 335 | * 设置节点的主题链接 336 | * @param {*} value 337 | */ 338 | export function handleNodeLink(value) { 339 | window.minder.execCommand("nodeLink", value); 340 | } 341 | 342 | /** 343 | * @desc 获取节点图片 344 | * @param window.minder 思维导图实例 345 | * 346 | */ 347 | export function getImage() { 348 | const image = window.minder.queryCommandValue("Image"); 349 | if (isUndefined(image.url) || isNull(image.url)) { 350 | return {}; 351 | } 352 | return image; 353 | } 354 | 355 | /** 356 | * @desc 设置节点图片 357 | * @param window.minder 思维导图实例 358 | * 359 | */ 360 | export function handleImage(params) { 361 | window.minder.execCommand("Image", params.url, params.title); 362 | } 363 | 364 | /** 365 | * @desc 展开节点 366 | * @param window.minder 思维导图实例 367 | * 368 | */ 369 | export function handleExpand(level) { 370 | window.minder.execCommand("ExpandToLevel", level); 371 | } 372 | 373 | /** 374 | * @desc 切换脑图模版 375 | * @param window.minder 思维导图实例 376 | * 377 | */ 378 | export function handleTemplate(name) { 379 | window.minder.execCommand("Template", name); 380 | } 381 | 382 | /** 383 | * @desc 获取当前脑图模版主题 384 | * @param window.minder 思维导图实例 385 | * 386 | */ 387 | export function getTemplate() { 388 | return window.minder.queryCommandValue("Template"); 389 | } 390 | 391 | /** 392 | * @desc 切换脑图主题 393 | * @param window.minder 思维导图实例 394 | * 395 | */ 396 | export function handleTheme(name) { 397 | window.minder.execCommand("Theme", name); 398 | } 399 | 400 | /** 401 | * @desc 获取当前脑图主题 402 | * @param window.minder 思维导图实例 403 | * 404 | */ 405 | export function getTheme() { 406 | return window.minder.queryCommandValue("Theme"); 407 | } 408 | 409 | /** 410 | * @desc 获取脑图当前图片预览状态 411 | * @param window.minder 思维导图实例 412 | * 413 | */ 414 | export function getImageViewActive() { 415 | return window.minder.viewer.actived; 416 | } 417 | 418 | /** 419 | * @desc 重设整个脑图布局 420 | * @param window.minder 思维导图实例 421 | * 422 | */ 423 | export function resetLayout() { 424 | const currentNode = window.minder.getSelectedNode(); 425 | window.minder.selectById(window.minder.getRoot(), true); 426 | window.minder.execCommand("ResetLayout"); 427 | window.minder.selectById( 428 | isNull(currentNode) ? null : currentNode.getData("id"), 429 | true 430 | ); 431 | } 432 | 433 | /** 434 | * @desc 节点上移 435 | * @param window.minder 思维导图实例 436 | * 437 | */ 438 | export function handleUp() { 439 | window.minder.execCommand("ArrangeUp"); 440 | } 441 | 442 | /** 443 | * @desc 节点下移 444 | * @param window.minder 思维导图实例 445 | * 446 | */ 447 | export function handleDown() { 448 | window.minder.execCommand("ArrangeDown"); 449 | } 450 | 451 | /** 452 | * @desc 复制 453 | * @param window.minder 思维导图实例 454 | * 455 | */ 456 | export function handleCopy() { 457 | window.minder.execCommand("Copy"); 458 | } 459 | 460 | /** 461 | * @desc 粘贴 462 | * @param window.minder 思维导图实例 463 | * 464 | */ 465 | export function handlePaste() { 466 | window.minder.execCommand("Paste"); 467 | } 468 | 469 | /** 470 | * @desc 设置节点的字体颜色 471 | * @param window.minder 思维导图实例 472 | * 473 | */ 474 | export function handleForeColor(color) { 475 | window.minder.execCommand("ForeColor", color); 476 | } 477 | 478 | /** 479 | * @desc 设置节点的背景颜色 480 | * @param window.minder 思维导图实例 481 | * 482 | */ 483 | export function handleBgColor(color) { 484 | window.minder.execCommand("Background", color); 485 | } 486 | 487 | /** 488 | * @desc 清除节点样式 489 | * @param window.minder 思维导图实例 490 | * 491 | */ 492 | export function handleClear() { 493 | window.minder.execCommand("ClearStyle"); 494 | } 495 | 496 | /** 497 | * @desc 通过nodeId定位节点 498 | * @param window.minder 思维导图实例 499 | * 500 | */ 501 | export function focusNodeById(nodeId) { 502 | window.minder.selectById(nodeId, true); 503 | const nodes = window.minder.getNodesById(nodeId); 504 | if (!isEmpty(nodes)) { 505 | window.minder.execCommand("Camera", nodes[0], 0); 506 | if (!nodes[0].isExpanded()) window.minder.execCommand("Expand", true); 507 | } 508 | // window.editor.hotbox.setInterface(true); 509 | } 510 | 511 | /** 512 | * 选中节点 513 | * @param {*} nodId 514 | */ 515 | export function selectNode(nodeId) { 516 | window.minder.selectById(nodeId, true); 517 | } 518 | 519 | /** 520 | * @desc 设置节点的sourceId 521 | * @param window.minder 思维导图实例 522 | * 523 | */ 524 | export function setSourceId(sourceId) { 525 | window.minder.getSelectedNode().setData("sourceId", sourceId).render(); 526 | } 527 | 528 | /** 529 | * 剪切节点 530 | */ 531 | export function cutNodes() { 532 | window.minder.execCommand("cut"); 533 | } 534 | 535 | /** 536 | * 使脑图不可用 537 | */ 538 | export function disableMinder() { 539 | window.minder.disable(); 540 | } 541 | 542 | /** 543 | * 脑图可用 544 | */ 545 | export function enableMinder() { 546 | window.minder.enable(); 547 | } 548 | -------------------------------------------------------------------------------- /src/common/helpers/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { notification } from "antd"; 3 | 4 | /** 5 | * 一、功能: 6 | * 1. 统一拦截http错误请求码; 7 | * 2. 统一拦截业务错误代码; 8 | * 3. 统一设置请求前缀 9 | * |-- 每个 http 加前缀 baseURL = /api/v1,从配置文件中获取 apiPrefix 10 | 11 | * 12 | * 二、引包: 13 | * |-- axios:http 请求工具库 14 | * |-- notification:Antd组件 > 处理错误响应码提示信息 15 | * |-- routerRedux:dva/router对象,用于路由跳转,错误响应码跳转相应页面 16 | * |-- store:dva中对象,使用里面的 dispatch 对象,用于触发路由跳转 17 | */ 18 | // const { NODE_ENV } = process.env 19 | // window.apiPrefix = '/api' 20 | // 设置全局参数,如响应超时时间5min,请求前缀等。 21 | axios.defaults.timeout = 1000 * 60 * 5; 22 | // axios.defaults.baseURL = window.apiPrefix 23 | axios.defaults.withCredentials = true; 24 | // 状态码错误信息 25 | const codeMessage = { 26 | 200: "服务器成功返回请求的数据。", 27 | 201: "新建或修改数据成功。", 28 | 202: "一个请求已经进入后台排队(异步任务)。", 29 | 204: "删除数据成功。", 30 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。", 31 | 401: "用户没有权限(令牌、用户名、密码错误)。", 32 | 403: "用户得到授权,但是访问是被禁止的。", 33 | 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。", 34 | 406: "请求的格式不可得。", 35 | 410: "请求的资源被永久删除,且不会再得到的。", 36 | 422: "当创建一个对象时,发生一个验证错误。", 37 | 500: "服务器发生错误,请检查服务器。", 38 | 502: "网关错误。", 39 | 503: "服务不可用,服务器暂时过载或维护。", 40 | 504: "网关超时。", 41 | }; 42 | 43 | // 添加一个请求拦截器,用于设置请求过渡状态 44 | axios.interceptors.request.use( 45 | (config) => { 46 | // config.baseURL = window.apiPrefix 47 | return config; 48 | }, 49 | (error) => { 50 | return Promise.reject(error); 51 | } 52 | ); 53 | 54 | // 添加一个返回拦截器 55 | axios.interceptors.response.use( 56 | (response) => { 57 | return response; 58 | }, 59 | (error) => { 60 | // 即使出现异常,也要调用关闭方法,否则一直处于加载状态很奇怪 61 | return Promise.reject(error); 62 | } 63 | ); 64 | 65 | export default function request(url, opt) { 66 | // 调用 axios api,统一拦截 67 | const options = {}; 68 | options.method = opt !== undefined ? opt.method : "get"; 69 | if (opt) { 70 | if (opt.body) { 71 | options.data = 72 | typeof opt.body === "string" ? JSON.parse(opt.body) : opt.body; 73 | } 74 | 75 | if (opt.headers) options.headers = opt.headers; 76 | 77 | if (opt.data) options.data = opt.data; 78 | 79 | if (typeof opt.withCredentials !== "undefined") 80 | options.withCredentials = opt.withCredentials; 81 | 82 | if (opt.params !== undefined) { 83 | url += "?"; 84 | for (let key in opt.params) { 85 | if (opt.params[key] !== undefined && opt.params[key] !== "") { 86 | url = url + key + "=" + opt.params[key] + "&"; 87 | } 88 | } 89 | url = url.substring(0, url.length - 1); 90 | } 91 | } 92 | return axios({ 93 | url, 94 | ...options, 95 | }) 96 | .then((response) => { 97 | // >>>>>>>>>>>>>> 请求成功 <<<<<<<<<<<<<< 98 | // console.log(`【${opt.method} ${opt.url}】请求成功,响应数据:`, response) 99 | 100 | // 打印业务错误提示 101 | // if (response.data && response.data.code != '0000') { 102 | // message.error(response.data.message) 103 | // } 104 | // eslint-disable-next-line 105 | if (response.data.code === 401 || response.code === 401) { 106 | let loginPath = `${response.data.data.login_url}?app_id=${response.data.data.app_id}`; 107 | let pagePath = encodeURIComponent(window.location.href); 108 | const redirectURL = `${loginPath}&version=1.0&jumpto=${pagePath}`; 109 | window.location.href = redirectURL; 110 | return Promise.reject(new Error("服务不可用,请联系管理员")); 111 | } 112 | // >>>>>>>>>>>>>> 当前未登录 <<<<<<<<<<<<<< 113 | if (response.data.code === 99993 || response.code === 99993) { 114 | const redirectURL = `/login`; 115 | window.location.href = redirectURL; 116 | return Promise.reject(new Error("服务不可用,请联系管理员")); 117 | } 118 | return { ...response.data }; 119 | }) 120 | .catch((error) => { 121 | // >>>>>>>>>>>>>> 请求失败 <<<<<<<<<<<<<< 122 | // 请求配置发生的错误 123 | if (!error.response) { 124 | // eslint-disable-next-line 125 | return console.log("Error", error.message); 126 | } 127 | 128 | // 响应时状态码处理 129 | const status = error.response.status; 130 | const errortext = codeMessage[status] || error.response.statusText; 131 | 132 | notification.error({ 133 | message: `请求错误 ${status}`, 134 | description: errortext, 135 | }); 136 | 137 | return { code: status, message: errortext }; 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /src/common/helpers/events.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | export default new EventEmitter(); 3 | -------------------------------------------------------------------------------- /src/common/helpers/jsondiff.js: -------------------------------------------------------------------------------- 1 | var _objectKeys = (function () { 2 | if (Object.keys) return Object.keys; 3 | 4 | return function (o) { 5 | var keys = []; 6 | for (var i in o) { 7 | if (o.hasOwnProperty(i)) { 8 | keys.push(i); 9 | } 10 | } 11 | return keys; 12 | }; 13 | })(); 14 | function escapePathComponent(str) { 15 | if (str.indexOf("/") === -1 && str.indexOf("~") === -1) return str; 16 | return str.replace(/~/g, "~0").replace(/\//g, "~1"); 17 | } 18 | function deepClone(obj) { 19 | if (typeof obj === "object") { 20 | return JSON.parse(JSON.stringify(obj)); 21 | } else { 22 | return obj; 23 | } 24 | } 25 | 26 | // Dirty check if obj is different from mirror, generate patches and update mirror 27 | function _generate(mirror, obj, patches, path) { 28 | var newKeys = _objectKeys(obj); 29 | var oldKeys = _objectKeys(mirror); 30 | var changed = false; 31 | var deleted = false; 32 | 33 | for (var t = oldKeys.length - 1; t >= 0; t--) { 34 | var key = oldKeys[t]; 35 | var oldVal = mirror[key]; 36 | if (obj.hasOwnProperty(key)) { 37 | var newVal = obj[key]; 38 | if ( 39 | typeof oldVal == "object" && 40 | oldVal != null && 41 | typeof newVal == "object" && 42 | newVal != null 43 | ) { 44 | _generate( 45 | oldVal, 46 | newVal, 47 | patches, 48 | path + "/" + escapePathComponent(key) 49 | ); 50 | } else { 51 | if (oldVal != newVal) { 52 | changed = true; 53 | patches.push({ 54 | op: "replace", 55 | path: path + "/" + escapePathComponent(key), 56 | value: deepClone(newVal), 57 | }); 58 | } 59 | } 60 | } else { 61 | patches.push({ 62 | op: "remove", 63 | path: path + "/" + escapePathComponent(key), 64 | }); 65 | deleted = true; // property has been deleted 66 | } 67 | } 68 | 69 | if (!deleted && newKeys.length == oldKeys.length) { 70 | return; 71 | } 72 | 73 | for (var t = 0; t < newKeys.length; t++) { 74 | var key = newKeys[t]; 75 | if (!mirror.hasOwnProperty(key)) { 76 | patches.push({ 77 | op: "add", 78 | path: path + "/" + escapePathComponent(key), 79 | value: deepClone(obj[key]), 80 | }); 81 | } 82 | } 83 | } 84 | 85 | export function compare(tree1, tree2) { 86 | var patches = []; 87 | _generate(tree1, tree2, patches, ""); 88 | return patches; 89 | } 90 | -------------------------------------------------------------------------------- /src/common/helpers/uploadFile.js: -------------------------------------------------------------------------------- 1 | import request from "./axios"; 2 | import { startsWith } from "lodash"; 3 | /** 4 | * @desc 将文件上传七牛 5 | * @param {string} file 需要上传的文件 6 | */ 7 | export function uploadFile(file, uploadUrl) { 8 | uploadUrl = startsWith(uploadUrl, "/api") 9 | ? uploadUrl.substring(4) 10 | : uploadUrl; 11 | return new Promise((resolve, reject) => { 12 | var formData = new FormData(); 13 | formData.append("file", file); 14 | request(uploadUrl, { 15 | method: "POST", 16 | processData: false, 17 | data: formData, 18 | withCredentials: false, 19 | }).then((response) => { 20 | resolve(response.data[0].url); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/common/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import { cloneDeep, isEmpty, isUndefined } from "lodash"; 2 | 3 | /** 4 | * @desc 获取地址栏的参数 5 | * @param {string} name 要获取的参数名称 6 | */ 7 | export function getQueryString(name) { 8 | var reg = new RegExp(name + "=([^&]*)", "i"); 9 | var r = window.location.href.match(reg); 10 | if (r !== null) { 11 | return r[1]; 12 | } else { 13 | return null; 14 | } 15 | } 16 | 17 | /** 18 | * @desc 判断数据是否为JSON字符串 19 | * @param {string} name 要判断的字符串 20 | */ 21 | export function isJsonString(name) { 22 | try { 23 | if (typeof JSON.parse(name) == "object") { 24 | return true; 25 | } 26 | return false; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | /** 33 | * @desc 生成随机字符串 34 | * @param {string} name 要判断的字符串 35 | */ 36 | export function randomString(len) { 37 | len = len || 32; 38 | const $chars = 39 | "abcdefghijklmnopqrstuvwxyz123456789"; /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/ 40 | const maxPos = $chars.length; 41 | var pwd = ""; 42 | for (let i = 0; i < len; i++) { 43 | pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); 44 | } 45 | return pwd; 46 | } 47 | 48 | // /** 49 | // * @desc 模拟键盘事件 50 | // * @param {string} name 要判断的字符串 51 | // */ 52 | // export function fireKeyEvent(el, keyCode) { 53 | // var evtObj; 54 | // if (document.createEvent) { 55 | // if (window.KeyEvent) { //firefox 浏览器下模拟事件 56 | // evtObj = document.createEvent('KeyEvents'); 57 | // evtObj.initKeyEvent(evtType, true, true, window, true, false, false, false, keyCode, 0); 58 | // } else { //chrome 浏览器下模拟事件 59 | // evtObj = document.createEvent('HTMLEvents'); 60 | // evtObj.initEvent('input', true, true); 61 | // evtObj.keyCode = keyCode; 62 | // } 63 | // el.dispatchEvent(evtObj); 64 | 65 | // } else if (document.createEventObject) { //IE 浏览器下模拟事件 66 | // evtObj = document.createEventObject(); 67 | // evtObj.keyCode = keyCode 68 | // el.fireEvent('on' + evtType, evtObj); 69 | // } 70 | // } 71 | 72 | /** 73 | * [格式化路由:将路由中的projectId -> projectName] 74 | * @param {Array} routes [路由] 75 | * @param {Object} projectInfo [项目信息] 76 | * @return {[type]} [description] 77 | */ 78 | export function parseRouteWithInfo( 79 | routes = [], 80 | projectInfo = {}, 81 | componentInfo = {} 82 | ) { 83 | const projectReg = /:projectId/g; 84 | const componentReg = /:componentId/g; 85 | const parsedRoute = routes.map(function (item) { 86 | const itemCopy = cloneDeep(item); 87 | if (projectReg.test(item.path) && !isEmpty(projectInfo)) { 88 | itemCopy.breadcrumbName = projectInfo.name; 89 | } 90 | if (componentReg.test(item.path) && !isUndefined(componentInfo.name)) { 91 | itemCopy.breadcrumbName = componentInfo.name; 92 | } 93 | return itemCopy; 94 | }); 95 | return parsedRoute; 96 | } 97 | 98 | /** 99 | * [格式化路由:将路由中breadcrumbName为空的过滤掉] 100 | * @param {Array} routes [description] 101 | * @param {Object} classroomInfo [description] 102 | * @return {[type]} [description] 103 | */ 104 | export function parseRouteForBreadcrumb(routes = []) { 105 | // 将url拼接 106 | const parsedUrl = routes.map((item, index) => { 107 | const newRoute = routes.slice(0, index + 1).map((newItem) => { 108 | return newItem.url; 109 | }); 110 | return { 111 | url: "/" + newRoute.join("/"), 112 | breadcrumbName: item.breadcrumbName, 113 | }; 114 | }); 115 | // 过滤breadcrumbName为空 116 | const parsedRoute = parsedUrl.filter((item) => { 117 | if (!isEmpty(item.breadcrumbName) && !isUndefined(item.breadcrumbName)) 118 | return item; 119 | }); 120 | return parsedRoute; 121 | } 122 | 123 | /** 124 | * [获取图片url对应的宽高] 125 | * @param {string} url [description] 126 | */ 127 | export function loadImageSize(url) { 128 | return new Promise((resolve, reject) => { 129 | var img = document.createElement("img"); 130 | img.onload = function () { 131 | resolve({ width: img.width, height: img.height }); 132 | }; 133 | img.onerror = function () { 134 | reject(null); 135 | }; 136 | img.src = url; 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /src/constant/config.minder.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | // 放大缩小比例 3 | zoom: [10, 20, 30, 50, 80, 100, 120, 150, 200], 4 | tag: { 5 | 0: { 6 | text: "未处理", 7 | color: "#EE2C2C", 8 | }, 9 | 1: { 10 | text: "已处理", 11 | color: "#9ACD32", 12 | }, 13 | 2: { 14 | text: "无需处理", 15 | color: "#8B8989", 16 | }, 17 | }, 18 | commentType: { 19 | 1: "需求不明确", 20 | 2: "需求合理性", 21 | 3: "遗漏异常情况", 22 | 4: "遗漏场景测试", 23 | 5: "边界值", 24 | 6: "测试数据", 25 | 7: "安全性相关", 26 | 8: "体验优化改进", 27 | 9: "测试点编写改进", 28 | 0: "其他", 29 | }, 30 | template: [ 31 | "default", 32 | "structure", 33 | "filetree", 34 | "right", 35 | "fish-bone", 36 | "tianpan", 37 | ], 38 | theme: { 39 | classic: { 40 | key: "脑图经典", 41 | background: "rgb(233, 223, 152)", 42 | color: "rgb(68, 51, 0)", 43 | radius: "15px", 44 | }, 45 | "classic-compact": { 46 | key: "紧凑经典", 47 | background: "rgb(233, 223, 152)", 48 | color: "rgb(68, 51, 0)", 49 | radius: "15px", 50 | }, 51 | "fresh-blue": { 52 | key: "天空蓝", 53 | background: "rgb(115, 161, 191)", 54 | color: "#fff", 55 | radius: "2.5px", 56 | }, 57 | "fresh-blue-compat": { 58 | key: "紧凑蓝", 59 | background: "rgb(115, 161, 191)", 60 | color: "#fff", 61 | radius: "2.5px", 62 | }, 63 | "fresh-green": { 64 | key: "文艺绿", 65 | background: "rgb(115, 191, 118)", 66 | color: "#fff", 67 | radius: "2.5px", 68 | }, 69 | "fresh-green-compat": { 70 | key: "紧凑绿", 71 | background: "rgb(115, 191, 118)", 72 | color: "#fff", 73 | radius: "2.5px", 74 | }, 75 | "fresh-pink": { 76 | key: "脑残粉", 77 | background: "rgb(191, 115, 148)", 78 | color: "#fff", 79 | radius: "2.5px", 80 | }, 81 | "fresh-pink-compat": { 82 | key: "紧凑粉", 83 | background: "rgb(191, 115, 148)", 84 | color: "#fff", 85 | radius: "2.5px", 86 | }, 87 | "fresh-purple": { 88 | key: "浪漫紫", 89 | background: "rgb(123, 115, 191)", 90 | color: "#fff", 91 | radius: "2.5px", 92 | }, 93 | "fresh-purple-compat": { 94 | key: "紧凑紫", 95 | background: "rgb(123, 115, 191)", 96 | color: "#fff", 97 | radius: "2.5px", 98 | }, 99 | "fresh-red": { 100 | key: "清新红", 101 | background: "rgb(191, 115, 115)", 102 | color: "#fff", 103 | radius: "2.5px", 104 | }, 105 | "fresh-red-compat": { 106 | key: "紧凑红", 107 | background: "rgb(191, 115, 115)", 108 | color: "#fff", 109 | radius: "2.5px", 110 | }, 111 | "fresh-soil": { 112 | key: "泥土黄", 113 | background: "rgb(191, 147, 115)", 114 | color: "#fff", 115 | radius: "2.5px", 116 | }, 117 | "fresh-soil-compat": { 118 | key: "紧凑黄", 119 | background: "rgb(191, 147, 115)", 120 | color: "#fff", 121 | radius: "2.5px", 122 | }, 123 | snow: { 124 | key: "温柔冷光", 125 | background: "rgb(233, 223, 152)", 126 | color: "rgb(68, 51, 0)", 127 | radius: "2.5px", 128 | }, 129 | "snow-compact": { 130 | key: "紧凑冷光", 131 | background: "rgb(233, 223, 152)", 132 | color: "rgb(68, 51, 0)", 133 | radius: "2.5px", 134 | }, 135 | tianpan: { 136 | key: "经典天盘", 137 | background: "rgb(233, 223, 152)", 138 | color: "rgb(68, 51, 0)", 139 | radius: "15px", 140 | }, 141 | "tianpan-compact": { 142 | key: "紧凑天盘", 143 | background: "rgb(233, 223, 152)", 144 | color: "rgb(68, 51, 0)", 145 | radius: "15px", 146 | }, 147 | fish: { 148 | key: "鱼骨图", 149 | background: "rgb(233, 223, 152)", 150 | color: "rgb(68, 51, 0)", 151 | radius: "15px", 152 | }, 153 | wire: { 154 | key: "线框", 155 | background: "#fff", 156 | color: "rgb(68, 51, 0)", 157 | radius: "15px", 158 | }, 159 | }, 160 | commonColor: [ 161 | [ 162 | "#ffffff", 163 | "#000000", 164 | "#eeece1", 165 | "#1f497d", 166 | "#4f81bd", 167 | "#c0504d", 168 | "#9bbb59", 169 | "#8064a2", 170 | "#4bacc6", 171 | "#f79646", 172 | ], 173 | [ 174 | "#f2f2f2", 175 | "#808080", 176 | "#ddd8c2", 177 | "#c6d9f1", 178 | "#dbe5f1", 179 | "#f2dbdb", 180 | "#eaf1dd", 181 | "#e5dfec", 182 | "#daeef3", 183 | "#fde9d9", 184 | ], 185 | [ 186 | "#d9d9d9", 187 | "#595959", 188 | "#c4bc96", 189 | "#8db3e2", 190 | "#b8cce4", 191 | "#e5b8b7", 192 | "#d6e3bc", 193 | "#ccc0d9", 194 | "#b6dde8", 195 | "#fbd4b4", 196 | ], 197 | [ 198 | "#bfbfbf", 199 | "#404040", 200 | "#938953", 201 | "#548dd4", 202 | "#95b3d7", 203 | "#d99594", 204 | "#c2d69b", 205 | "#b2a1c7", 206 | "#92cddc", 207 | "#fabf8f", 208 | ], 209 | [ 210 | "#a6a6a6", 211 | "#262626", 212 | "#4a442a", 213 | "#17365d", 214 | "#365f91", 215 | "#943634", 216 | "#76923c", 217 | "#5f497a", 218 | "#31849b", 219 | "#e36c0a", 220 | ], 221 | [ 222 | "#7f7f7f", 223 | "#0d0d0d", 224 | "#1c1a10", 225 | "#0f243e", 226 | "#243f60", 227 | "#622423", 228 | "#4e6128", 229 | "#3f3151", 230 | "#205867", 231 | "#974706", 232 | ], 233 | ], 234 | standardColor: [ 235 | "#c00000", 236 | "#ff0000", 237 | "#ffc000", 238 | "#ffff00", 239 | "#92d050", 240 | "#00b050", 241 | "#00b0f0", 242 | "#0070c0", 243 | "#002060", 244 | "#7030a0", 245 | ], 246 | reviewTag: "待审", 247 | reviewedTag: "已审", 248 | resourceColor: [ 249 | { bgColor: "#BF1E1B", fontColor: "#FFF" }, 250 | { bgColor: "#7a00f2", fontColor: "#FFF" }, 251 | { bgColor: "#2760f2", fontColor: "#FFF" }, 252 | { bgColor: "#63ABF7", fontColor: "#FFF" }, 253 | { bgColor: "#71CB2D", fontColor: "#FFF" }, 254 | { bgColor: "#50c28b", fontColor: "#FFF" }, 255 | { bgColor: "#FF9F1A", fontColor: "#FFF" }, 256 | { bgColor: "#30BFBF", fontColor: "#FFF" }, 257 | { bgColor: "#444444", fontColor: "#FFF" }, 258 | { bgColor: "#6d6d6d", fontColor: "#FFF" }, 259 | { bgColor: "#F4F4F4", fontColor: "#333333" }, 260 | { bgColor: "#D6F0F8", fontColor: "#276F86" }, 261 | ], 262 | }; 263 | 264 | export default config; 265 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | ReactDOM.render( 7 | <> 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mode/Kityminder.js: -------------------------------------------------------------------------------- 1 | import { ActionModeBase } from "@/common/modes/Base/ActionModeBase"; 2 | import types from "@/app/constants/kityminderTypes"; 3 | import { fromJS } from "immutable"; 4 | 5 | export default class EditorMode extends ActionModeBase { 6 | // 获取项目列表 7 | async set(params) { 8 | const type = types.SET_STATE; 9 | this.dispatch({ 10 | type: type, 11 | data: params, 12 | }); 13 | } 14 | } 15 | 16 | const initialState = fromJS({ 17 | loading: false, 18 | editable: true, 19 | isNode: false, 20 | isRoot: false, 21 | isEdit: false, 22 | editText: "", 23 | hasUndo: false, 24 | hasRedo: false, 25 | hotbox: false, 26 | toolbox: false, 27 | toolboxTab: "review", 28 | nodeInfo: { 29 | id: "", 30 | text: "", 31 | note: "", 32 | hyperlink: {}, 33 | image: {}, 34 | timeStamp: "", 35 | }, 36 | zoom: 100, 37 | triggerActive: true, 38 | fullScreen: false, 39 | theme: "fresh-blue-compat", 40 | template: "", 41 | showTip: false, //是否显示结果文字 42 | curIndex: 0, // 当前处于第一条 43 | resultNum: 0, // 搜索结果共几条 44 | allResource: [], // 所有标签 45 | currentResource: [], // 当前选中节点的标签 46 | interfaceSettingVisible: false, 47 | }); 48 | 49 | export function editorInfo(state = initialState, action) { 50 | const oldState = state.toJS(); 51 | switch (action.type) { 52 | case types.SET_STATE: 53 | state = fromJS({ ...oldState, ...action.data }); 54 | return state; 55 | default: 56 | return state; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/model/ClipBoard.js: -------------------------------------------------------------------------------- 1 | import { isEmpty, remove, isEqual, get, isNull } from "lodash"; 2 | import { randomString } from "./../common/helpers/utils"; 3 | 4 | import * as copy from "copy-to-clipboard"; 5 | import { uploadFile } from "./../common/helpers/uploadFile"; 6 | import { loadImageSize } from "./../common/helpers/utils"; 7 | import config from "../constant/config.minder"; 8 | import * as editorCommand from "./../command/EditorCommand"; 9 | class Clipboard { 10 | // 构造函数 11 | constructor(props) { 12 | this.setProps(props); 13 | // 字段 14 | this.lock = false; 15 | this.SPLITOR = "\uFEFF"; 16 | this.MIMETYPE = { 17 | "application/km": "\uFFFF", 18 | }; 19 | this.SIGN = { 20 | "\uFEFF": "SPLITOR", 21 | "\uFFFF": "application/km", 22 | }; 23 | 24 | /** 25 | * 实际复制的文本 26 | */ 27 | this.copyText = ""; 28 | // 监听复制事件 29 | document.addEventListener("copy", this.beforeCopy, false); 30 | 31 | // 监听粘贴事件 32 | document.addEventListener("paste", this.beforePaste, false); 33 | } 34 | 35 | setProps = (props) => { 36 | this.props = props; 37 | }; 38 | 39 | unMount = () => { 40 | document.removeEventListener("copy", this.beforeCopy); 41 | document.removeEventListener("paste", this.beforePaste); 42 | }; 43 | 44 | beforeCopy = (e) => { 45 | if (document.activeElement.id != "kityminder-core") return; 46 | e.stopPropagation(); 47 | var nodes = [].concat(window.minder.getSelectedNodes()); 48 | if (nodes.length) { 49 | // 这里由于被粘贴复制的节点的id信息也都一样,故做此算法 50 | // 这里有个疑问,使用node.getParent()或者node.parent会离奇导致出现非选中节点被渲染成选中节点,因此使用isAncestorOf,而没有使用自行回溯的方式 51 | if (nodes.length > 1) { 52 | var targetLevel; 53 | nodes.sort(function (a, b) { 54 | return a.getLevel() - b.getLevel(); 55 | }); 56 | targetLevel = nodes[0].getLevel(); 57 | if (targetLevel !== nodes[nodes.length - 1].getLevel()) { 58 | var pnode, 59 | idx = 0, 60 | l = nodes.length, 61 | pidx = l - 1; 62 | 63 | pnode = nodes[pidx]; 64 | 65 | while (pnode.getLevel() !== targetLevel) { 66 | idx = 0; 67 | while (idx < l && nodes[idx].getLevel() === targetLevel) { 68 | if (nodes[idx].isAncestorOf(pnode)) { 69 | nodes.splice(pidx, 1); 70 | break; 71 | } 72 | idx++; 73 | } 74 | pidx--; 75 | pnode = nodes[pidx]; 76 | } 77 | } 78 | } 79 | 80 | var str = ""; 81 | for (const item of nodes) { 82 | if (str === "") { 83 | str += item.data.text; 84 | } else { 85 | str += "\n" + item.data.text; 86 | } 87 | } 88 | copy(str, { format: "text/plain" }); 89 | localStorage.setItem("copyText", this.encode(nodes)); 90 | this.copyText = this.encode(nodes); 91 | } 92 | }; 93 | 94 | beforePaste = async (e) => { 95 | if (window.minder.getStatus() === "normal") { 96 | if (this.props.props.type === "record") { 97 | await this.handlePicture(e); 98 | // 直接修改节点的图片内容 不会触发更新,所以需要手动触发更新的逻辑 99 | setTimeout(() => { 100 | window.minder.fire("contentchange"); 101 | }, 500); 102 | } 103 | 104 | if (!this.props.state.readOnly) { 105 | if (document.activeElement.id === "kityminder-core") { 106 | this.handleNode(e); 107 | } 108 | if (document.activeElement.id === "core-node-input-disableKeydown") { 109 | this.handlePicture(e); 110 | } 111 | } 112 | } 113 | }; 114 | 115 | handlePicture = async (e) => { 116 | const item = get(e, "clipboardData.items[0]", null); 117 | if (!isNull(item) && item.kind === "file" && item.type.match(/^image\//i)) { 118 | const imgFile = item.getAsFile(); 119 | const imgUrl = await uploadFile(imgFile, this.props.props.uploadUrl); 120 | editorCommand.handleImage({ url: imgUrl }); 121 | } 122 | }; 123 | 124 | handleNode = async (e) => { 125 | this.lock = true; 126 | e.preventDefault(); 127 | e.stopPropagation(); 128 | var _selectedNodes = []; 129 | var clipBoardEvent = e; 130 | var textData = clipBoardEvent.clipboardData.getData("text/plain"); 131 | 132 | this.copyText = localStorage.getItem("copyText"); 133 | if (this.copyText !== null && this.copyText !== "") { 134 | let copyMemText = ""; 135 | let nodeData = this.copyText.split(this.SPLITOR)[1]; 136 | for (const item of JSON.parse(nodeData)) { 137 | if (copyMemText === "") { 138 | copyMemText += item.data.text; 139 | } else { 140 | copyMemText += "\n" + item.data.text; 141 | } 142 | } 143 | if (copyMemText === textData) { 144 | textData = this.copyText; 145 | } else { 146 | // 清掉复制的内容 147 | this.copyText = ""; 148 | localStorage.setItem("copyText", ""); 149 | } 150 | } 151 | 152 | /* 153 | * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理 154 | */ 155 | var sNodes = window.minder.getSelectedNodes(); 156 | const data = textData.split(this.SPLITOR); 157 | 158 | if (this.SIGN[data[0]] == "application/km") { 159 | var nodes = JSON.parse(data[1]); 160 | var _node; 161 | sNodes.map((node) => { 162 | // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来 163 | for (var i = nodes.length - 1; i >= 0; i--) { 164 | _node = window.minder.createNode(null, node); 165 | const newNode = this.createNodes([nodes[i]])[0]; 166 | window.minder.importNode(_node, newNode); 167 | _selectedNodes.push(_node); 168 | node.appendChild(_node); 169 | } 170 | }); 171 | window.minder.select(_selectedNodes, true); 172 | _selectedNodes = []; 173 | 174 | window.minder.refresh(); 175 | } else { 176 | sNodes.forEach(function (node) { 177 | window.minder.Text2Children(node, textData); 178 | }); 179 | } 180 | 181 | /* 182 | * 针对normal状态下通过对选中节点粘贴导入图片进行单独处理 183 | */ 184 | const item = get(clipBoardEvent, "clipboardData.items[0]", null); 185 | if (!isNull(item) && item.kind === "file" && item.type.match(/^image\//i)) { 186 | const imgFile = item.getAsFile(); 187 | const imgUrl = await uploadFile(imgFile, this.props.props.uploadUrl); 188 | let imgInfo = await loadImageSize(imgUrl); 189 | var _node; 190 | sNodes.map((node) => { 191 | let width = imgInfo.width; 192 | let height = imgInfo.height; 193 | if (width > 200 && width > height) { 194 | height = (200 * height) / width; 195 | width = 200; 196 | } else if (height > 200 && height > width) { 197 | width = (200 * width) / height; 198 | height = 200; 199 | } 200 | window.minder.createNode( 201 | { image: imgUrl, imageSize: { width, height } }, 202 | node 203 | ); 204 | window.minder.refresh(); 205 | }); 206 | } 207 | 208 | this.lock = false; 209 | }; 210 | 211 | /* 212 | * 粘贴时处理每一个节点的id,避免出现不同节点的id相同的情况 213 | */ 214 | createNodes = (nodes) => { 215 | return nodes.map((node) => { 216 | node.data.id = randomString(12); 217 | node.data.creator = this.props.userName; 218 | // 去掉元数据关联 219 | delete node.data.sourceId; 220 | 221 | // 去掉已审与待审标签 222 | remove(node.data.resource, (item) => { 223 | return ( 224 | isEqual(item, config.reviewTag) || isEqual(item, config.reviewedTag) 225 | ); 226 | }); 227 | 228 | isEmpty(node.children) || this.createNodes(node.children); 229 | return node; 230 | }); 231 | }; 232 | 233 | /* 234 | * 增加对多节点复制粘贴的处理 235 | */ 236 | encode = (nodes) => { 237 | var _nodes = []; 238 | for (var i = 0, l = nodes.length; i < l; i++) { 239 | _nodes.push(window.minder.exportNode(nodes[i])); 240 | } 241 | return ( 242 | this.MIMETYPE["application/km"] + this.SPLITOR + JSON.stringify(_nodes) 243 | ); 244 | }; 245 | } 246 | 247 | export default Clipboard; 248 | -------------------------------------------------------------------------------- /src/model/History.js: -------------------------------------------------------------------------------- 1 | import * as editorCommand from "./../command/EditorCommand"; 2 | import { compare } from "../common/helpers/jsondiff"; 3 | import { isUndefined } from "lodash"; 4 | 5 | class History { 6 | // 构造函数 7 | constructor(props) { 8 | // true代表这个是一个同步过来的动作 我在前端改变的时候 不需要发送patch 9 | this.isSync = false; 10 | this.setProps(props); 11 | this.hasUndo = false; 12 | this.hasRedo = false; 13 | this.reset(); 14 | window.minder.on("contentchange", () => { 15 | this.change(); 16 | }); 17 | 18 | // 如果新导入一个脑图,就重置所有的undo以及redo 19 | window.minder.on("import", (e) => { 20 | this.reset(); 21 | }); 22 | } 23 | 24 | setProps = (props) => { 25 | this.props = props; 26 | }; 27 | 28 | reset() { 29 | this.lastContent = editorCommand.exportJson(); 30 | this.undoList = []; 31 | this.setUndo(); 32 | this.redoList = []; 33 | this.setRedo(); 34 | } 35 | 36 | makeUndoDiff() { 37 | var headSnap = editorCommand.exportJson(); 38 | var diff = compare(headSnap, this.lastContent); 39 | if (diff.length) { 40 | this.undoList.push(diff); 41 | this.setUndo(); 42 | while (this.undoList.length > this.MAX_HISTORY) { 43 | this.undoList.shift(); 44 | } 45 | this.lastContent = headSnap; 46 | return true; 47 | } 48 | return false; 49 | } 50 | 51 | makeRedoDiff() { 52 | var revertSnap = editorCommand.exportJson(); 53 | let diff = compare(revertSnap, this.lastContent); 54 | this.redoList.push(diff); 55 | this.setRedo(); 56 | this.lastContent = revertSnap; 57 | } 58 | 59 | /** 60 | * 生成这次操作与上次的区别的diff内容 61 | */ 62 | makePatch = () => { 63 | var headSnap = editorCommand.exportJson(); 64 | if (!isUndefined(this.lastContent.root.data.text)) { 65 | var diff = compare(this.lastContent, headSnap); 66 | if (diff.length) { 67 | if (!this.isSync) { 68 | this.props.sendPatch(diff); 69 | } else { 70 | this.isSync = false; 71 | } 72 | } 73 | } 74 | }; 75 | 76 | setUndo = () => { 77 | this.hasUndo = !!this.undoList.length; 78 | this.props.handleState("hasUndo", this.hasUndo); 79 | }; 80 | 81 | setRedo = () => { 82 | this.hasRedo = !!this.redoList.length; 83 | this.props.handleState("hasRedo", this.hasRedo); 84 | }; 85 | 86 | undo() { 87 | this.patchLock = true; 88 | var undoDiff = this.undoList.pop(); 89 | this.setUndo(); 90 | if (undoDiff) { 91 | this.props.sendPatch(undoDiff); 92 | editorCommand.applyPatches(undoDiff); 93 | this.makeRedoDiff(); 94 | } 95 | this.patchLock = false; 96 | } 97 | 98 | redo() { 99 | this.patchLock = true; 100 | var redoDiff = this.redoList.pop(); 101 | this.setRedo(); 102 | if (redoDiff) { 103 | this.props.sendPatch(redoDiff); 104 | editorCommand.applyPatches(redoDiff); 105 | this.makeUndoDiff(); 106 | } 107 | this.patchLock = false; 108 | } 109 | 110 | change() { 111 | if (this.patchLock) return; 112 | this.makePatch(); 113 | if (this.makeUndoDiff()) { 114 | this.redoList = []; 115 | this.setRedo(); 116 | } 117 | } 118 | 119 | hasUndo() { 120 | return !!this.undoList.length; 121 | } 122 | 123 | hasRedo() { 124 | return !!this.redoList.length; 125 | } 126 | } 127 | 128 | export default History; 129 | -------------------------------------------------------------------------------- /src/model/HotBox.js: -------------------------------------------------------------------------------- 1 | import * as editorCommand from "./../command/EditorCommand"; 2 | import "hotbox-ui"; 3 | import "hotbox-ui/hotbox.css"; 4 | 5 | class HotBoxs { 6 | constructor(props) { 7 | this.setProps(props); 8 | 9 | this.hotbox = new window.HotBox("#kityminder-core"); 10 | var main = this.hotbox.state("main"); 11 | 12 | const vm = this; 13 | 14 | if (!props.state.readOnly) { 15 | main.button({ 16 | position: "center", 17 | action: function () { 18 | // 编辑动作 19 | window.editor.runtime.handleEdit(); 20 | }, 21 | label: "编辑", 22 | key: "F2", 23 | next: "idle", 24 | }); 25 | 26 | main.button({ 27 | position: "ring", 28 | action: function () { 29 | editorCommand.handleUp(); 30 | }, 31 | label: "前移", 32 | key: "Alt+Up", 33 | next: "idle", 34 | }); 35 | 36 | main.button({ 37 | position: "ring", 38 | action: function () { 39 | window.editor.runtime.handleAppend("childNode"); 40 | }, 41 | label: "下级", 42 | key: "Tab", 43 | next: "idle", 44 | }); 45 | 46 | main.button({ 47 | position: "ring", 48 | action: function () { 49 | window.editor.runtime.handleAppend("siblingNode"); 50 | }, 51 | label: "同级", 52 | key: "Enter", 53 | next: "idle", 54 | }); 55 | 56 | main.button({ 57 | position: "ring", 58 | action: function () { 59 | editorCommand.handleDown(); 60 | }, 61 | label: "后移", 62 | key: "Alt+Down", 63 | next: "idle", 64 | }); 65 | 66 | main.button({ 67 | position: "ring", 68 | action: function () { 69 | editorCommand.handleRemove(); 70 | }, 71 | label: "删除", 72 | key: "Delete", 73 | next: "idle", 74 | }); 75 | 76 | main.button({ 77 | position: "ring", 78 | action: function () { 79 | window.editor.runtime.handleAppend("parentNode"); 80 | }, 81 | label: "上级", 82 | key: "Shift+Tab", 83 | next: "idle", 84 | }); 85 | 86 | main.button({ 87 | position: "bottom", 88 | action: function () { 89 | props.handleState("toolbox", true); 90 | props.handleState("toolboxTab", "note"); 91 | }, 92 | label: "备注", 93 | key: "备注", 94 | next: "idle", 95 | }); 96 | } else { 97 | main.button({ 98 | position: "ring", 99 | action: function () { 100 | vm.handleExecuteResult(2); 101 | }, 102 | label: "通过", 103 | key: "通过", 104 | next: "idle", 105 | }); 106 | 107 | main.button({ 108 | position: "ring", 109 | action: function () { 110 | vm.handleExecuteResult(1); 111 | }, 112 | label: "失败", 113 | key: "失败", 114 | next: "idle", 115 | }); 116 | 117 | main.button({ 118 | position: "ring", 119 | action: function () { 120 | vm.handleExecuteResult(4); 121 | }, 122 | label: "不适用", 123 | key: "不适用", 124 | next: "idle", 125 | }); 126 | 127 | main.button({ 128 | position: "ring", 129 | action: function () { 130 | vm.handleExecuteResult(3); 131 | }, 132 | label: "阻塞", 133 | key: "阻塞", 134 | next: "idle", 135 | }); 136 | 137 | main.button({ 138 | position: "top", 139 | action: function () { 140 | vm.handleExecuteResult(0); 141 | }, 142 | label: "移除", 143 | key: "Del", 144 | next: "idle", 145 | }); 146 | } 147 | } 148 | 149 | /** 150 | * 设置执行结果 151 | * @param {*} key 152 | */ 153 | handleExecuteResult = (key) => { 154 | editorCommand.handleResult(key); 155 | if (this.props.props.onResultChange) { 156 | this.props.props.onResultChange(); 157 | } 158 | }; 159 | 160 | active = (x, y) => { 161 | this.hotbox.active("main", { x, y }); 162 | }; 163 | 164 | idle = () => { 165 | this.hotbox.active("idle"); 166 | }; 167 | 168 | setProps = (props) => { 169 | this.props = props; 170 | }; 171 | } 172 | 173 | export default HotBoxs; 174 | -------------------------------------------------------------------------------- /src/model/Navigator.js: -------------------------------------------------------------------------------- 1 | import config from "../constant/config.minder"; 2 | 3 | class Navigator { 4 | // 构造函数 5 | constructor(props) { 6 | this.props = props; 7 | // 字段 8 | this.zoom = 100; // 初始缩放比例 9 | this.triggerActive = true; 10 | this.paper; // 承载缩略图的画布 11 | this.pathHandler; 12 | this.nodeThumb; 13 | this.connectionThumb; 14 | this.visibleRect; 15 | this.contentView; 16 | this.visibleView; 17 | this.isMove = false; // 判断是否在导航盘的拖拽状态 18 | this.fullScreen = false; // 记录浏览器全屏模式,以供其他editor下的类使用 19 | window.minder.setDefaultOptions({ zoom: config.zoom }); // 自定义缩放的比例 20 | window.minder.on("zoom", (e) => { 21 | this.zoom = e.zoom; 22 | this.setZoom(); 23 | }); 24 | // document.getElementById('project-component-container').addEventListener('mousemove', (e) => { // 阻止鼠标拖拽的默认事件 25 | // if (this.isMove) e.preventDefault(); 26 | // }, false) 27 | window.minder.on("themechange", (e) => { 28 | // 主题切换事件 29 | this.pathHandler = this.getPathHandler(e.theme); 30 | }); 31 | this.pathHandler = this.getPathHandler(window.minder.getTheme()); 32 | this.setPaper(); 33 | } 34 | 35 | setPaper = () => { 36 | this.paper = new window.window.kity.Paper("nav-previewer"); 37 | // 用两个路径绘制节点和连线的缩略图 38 | this.nodeThumb = this.paper.put(new window.kity.Path()); 39 | this.connectionThumb = this.paper.put(new window.kity.Path()); 40 | 41 | // 表示可视区域的矩形 42 | this.visibleRect = this.paper.put( 43 | new window.kity.Rect(100, 100).stroke("red", "1%") 44 | ); 45 | 46 | this.contentView = new window.kity.Box(); 47 | this.visibleView = new window.kity.Box(); 48 | 49 | if (this.triggerActive) { 50 | this.bind(); 51 | this.updateContentView(); 52 | this.updateVisibleView(); 53 | } else { 54 | this.unbind(); 55 | } 56 | 57 | this.navigate(); 58 | }; 59 | 60 | navigate = () => { 61 | var dragging = false; 62 | 63 | this.paper.on("mousedown", (e) => { 64 | this.isMove = true; 65 | dragging = true; 66 | this.moveView(e.getPosition("top"), 200); 67 | document.getElementById("nav-previewer").className = "nav-previewer grab"; 68 | }); 69 | 70 | this.paper.on("mousemove", (e) => { 71 | if (dragging) { 72 | this.moveView(e.getPosition("top"), 0); 73 | } 74 | }); 75 | 76 | this.paper.on("mouseup", (e) => { 77 | this.isMove = false; 78 | dragging = false; 79 | document.getElementById("nav-previewer").className = "nav-previewer"; 80 | }); 81 | 82 | // document.getElementById('kityminder-core').addEventListener('mouseup', () => { 83 | // this.isMove = false; 84 | // dragging = false; 85 | // document.getElementById('nav-previewer').className = 'nav-previewer'; 86 | // }, false) 87 | }; 88 | 89 | handleTriggerClick = () => { 90 | // 点击导航栏的trigger时 91 | this.triggerActive = !this.triggerActive; 92 | this.setTriggerActive(); 93 | if (this.triggerActive) { 94 | this.bind(); 95 | this.updateContentView(); 96 | this.updateVisibleView(); 97 | } else { 98 | this.unbind(); 99 | } 100 | }; 101 | 102 | bind = () => { 103 | window.minder.on("layout layoutallfinish", this.updateContentView); 104 | window.minder.on("viewchange", this.updateVisibleView); 105 | }; 106 | 107 | unbind = () => { 108 | window.minder.off("layout layoutallfinish", this.updateContentView); 109 | window.minder.off("viewchange", this.updateVisibleView); 110 | }; 111 | 112 | moveView = (center, duration) => { 113 | var box = this.visibleView; 114 | center.x = -center.x; 115 | center.y = -center.y; 116 | 117 | var viewMatrix = window.minder.getPaper().getViewPortMatrix(); 118 | box = viewMatrix.transformBox(box); 119 | 120 | var targetPosition = center.offset(box.width / 2, box.height / 2); 121 | 122 | window.minder.getViewDragger().moveTo(targetPosition, duration); 123 | }; 124 | 125 | getPathHandler = (theme) => { 126 | switch (theme) { 127 | case "tianpan": 128 | case "tianpan-compact": 129 | return function (nodePathData, x, y, width, height) { 130 | var r = width >> 1; 131 | nodePathData.push("M", x, y + r, "a", r, r, 0, 1, 1, 0, 0.01, "z"); 132 | }; 133 | default: { 134 | return function (nodePathData, x, y, width, height) { 135 | nodePathData.push( 136 | "M", 137 | x, 138 | y, 139 | "h", 140 | width, 141 | "v", 142 | height, 143 | "h", 144 | -width, 145 | "z" 146 | ); 147 | }; 148 | } 149 | } 150 | }; 151 | 152 | updateContentView = () => { 153 | var view = window.minder.getRenderContainer().getBoundaryBox(); 154 | 155 | this.contentView = view; 156 | 157 | var padding = 30; 158 | 159 | this.paper.setViewBox( 160 | view.x - padding - 0.5, 161 | view.y - padding - 0.5, 162 | view.width + padding * 2 + 1, 163 | view.height + padding * 2 + 1 164 | ); 165 | 166 | var nodePathData = []; 167 | var connectionThumbData = []; 168 | 169 | window.minder.getRoot().traverse((node) => { 170 | var box = node.getLayoutBox(); 171 | this.pathHandler(nodePathData, box.x, box.y, box.width, box.height); 172 | if (node.getConnection() && node.parent && node.parent.isExpanded()) { 173 | connectionThumbData.push(node.getConnection().getPathData()); 174 | } 175 | }); 176 | 177 | this.paper.setStyle("background", window.minder.getStyle("background")); 178 | 179 | if (nodePathData.length) { 180 | this.nodeThumb 181 | .fill(window.minder.getStyle("root-background")) 182 | .setPathData(nodePathData); 183 | } else { 184 | this.nodeThumb.setPathData(null); 185 | } 186 | 187 | if (connectionThumbData.length) { 188 | this.connectionThumb 189 | .stroke(window.minder.getStyle("connect-color"), "0.5%") 190 | .setPathData(connectionThumbData); 191 | } else { 192 | this.connectionThumb.setPathData(null); 193 | } 194 | 195 | this.updateVisibleView(); 196 | }; 197 | 198 | updateVisibleView = () => { 199 | this.visibleView = window.minder.getViewDragger().getView(); 200 | this.visibleRect.setBox(this.visibleView.intersect(this.contentView)); 201 | }; 202 | 203 | setZoom = () => { 204 | this.props.handleState("zoom", this.zoom); 205 | }; 206 | 207 | setTriggerActive = () => { 208 | this.props.handleState("triggerActive", this.triggerActive); 209 | }; 210 | } 211 | 212 | export default Navigator; 213 | -------------------------------------------------------------------------------- /src/model/Runtime.js: -------------------------------------------------------------------------------- 1 | // import EditorMode from '@/app/modes/Kityminder'; 2 | import * as editorCommand from "./../command/EditorCommand"; 3 | import { isNull, isUndefined, isEmpty } from "lodash"; 4 | 5 | import $ from "jquery"; 6 | 7 | class Runtime { 8 | // 构造函数 9 | constructor(props) { 10 | this.editNodeId = ""; 11 | // this._EditorMode = new EditorMode(props); 12 | this.setProps(props); 13 | this.reset(); 14 | 15 | window.minder.on("selectionchange", this.updateNodeInfo); 16 | 17 | // 脑图重新导入数据时,数据初始化 18 | window.minder.on("import", () => { 19 | this.isInit = true; 20 | this.updateNodeInfo(); 21 | props.handleState("theme", editorCommand.getTheme()); 22 | props.handleState("template", editorCommand.getTemplate()); 23 | // this._EditorMode.set({ theme: editorCommand.getTheme(), template: editorCommand.getTemplate() }); 24 | }); 25 | 26 | window.minder.on("viewchange", () => { 27 | // if (this.isEdit) { 28 | // this.handleEdit(props.state.editText); 29 | // } 30 | }); 31 | 32 | // 脑图导入完成时,将loading状态去掉 33 | window.minder.on("layoutallfinish", () => { 34 | if (this.isInit) { 35 | // 获取到所有的标签节点 36 | let usedResource = window.minder.getUsedResource(); 37 | props.handleState("usedResource", usedResource); 38 | props.handleState("spinning", false); 39 | this.isInit = false; 40 | } 41 | 42 | // console.log(this.usedResouces, 'usedResouces') 43 | // isEmpty(this.props.minderData) || isEmpty(this.props.minderData.data) || this._EditorMode.set({ loading: false }); // 避免初次进来数据还没回来,脑图初始化完成就把loading状态置为false 44 | }); 45 | 46 | window.minder.on("mousewheel", () => { 47 | // console.log("鼠标滚轮滚动"); 48 | if (this.isEdit) { 49 | this.handleEdit(props.state.editText); 50 | } 51 | }); 52 | 53 | // 由于kityminder-core屏蔽了所有原生事件,故需要手动处理kityminder-core无法获取焦点的问题,坑! 54 | window.minder.on("click", (e) => { 55 | document.getElementById("kityminder-core").focus(); 56 | }); 57 | 58 | if (props.props.type !== "compare" && props.props.type !== "backup") { 59 | window.minder.on("contextmenu", (e) => { 60 | if (window.minder.getStatus() === "normal") { 61 | if (editorCommand.isNode()) { 62 | const position = window.minder 63 | .getSelectedNode() 64 | .getRenderBox("TextRenderer"); 65 | window.editor.hotbox.active(position.cx, position.cy); 66 | } 67 | } 68 | }); 69 | } 70 | 71 | // 监听快捷键 72 | document 73 | .getElementById("kityminder-core") 74 | .addEventListener("keydown", this.handleListenerForEditor, false); 75 | 76 | if (!props.state.readOnly) { 77 | // 双击节点进入编辑状态 78 | window.minder.on("dblclick", (e) => { 79 | if (window.minder.getStatus() === "normal") { 80 | // 只是选中单个节点 , 这里需要延迟处理,因为kityminder-core会响应双击的操作然后将节点做选中,所以这里需要稍等下 81 | setTimeout(() => { 82 | this.handleEdit(); 83 | }, 50); 84 | } 85 | }); 86 | } 87 | 88 | // 监听输入框的键盘事件 89 | document 90 | .getElementById("core-node-input-disableKeydown") 91 | .addEventListener("keydown", this.handleListenerForInput, false); 92 | 93 | // 鼠标按下前,退出编辑并更新节点text 94 | window.minder.on("beforemousedown", (e) => { 95 | if (this.isEdit) { 96 | this.saveAndExit(); 97 | } 98 | }); 99 | } 100 | 101 | handleListenerForInput = (e) => { 102 | if (!this.isEdit) { 103 | return; 104 | } 105 | if (!e.altKey && e.keyCode === 13) { 106 | // Enter 保存并退出 107 | e.stopPropagation(); 108 | e.preventDefault(); 109 | this.saveAndExit(); 110 | return; 111 | } 112 | 113 | if (e.altKey && e.keyCode === 13) { 114 | // Alt + Enter 换行 115 | e.stopPropagation(); 116 | e.preventDefault(); 117 | const value = $("#core-node-input-disableKeydown").val(); 118 | let posReal = document.getElementById("core-node-input-disableKeydown")[ 119 | "selectionStart" 120 | ]; 121 | let pos = posReal; //? posReal : String(value).length - 1; 122 | const newValue = 123 | String(value).substr(0, pos) + 124 | "\r\n" + 125 | String(value).substr(pos, String(value).length); 126 | this.props.handleChange(newValue); 127 | document.getElementById("core-node-input-disableKeydown")[ 128 | "setSelectionRange" 129 | ] && 130 | document 131 | .getElementById("core-node-input-disableKeydown") 132 | ["setSelectionRange"](pos + 1, pos + 1); 133 | return; 134 | } 135 | }; 136 | 137 | setProps = (props) => { 138 | this.props = props; 139 | }; 140 | 141 | handleEdit = (text) => { 142 | // if (!this.props.editable || !editorCommand.isNode()) return; 143 | 144 | if (window.minder.getStatus() !== "normal") return; 145 | let selectedNode = editorCommand.getSelectedNode(); 146 | 147 | if (selectedNode === null) return; 148 | // 选中节点 149 | this.editNodeId = selectedNode.getData("id"); 150 | editorCommand.selectNode(this.editNodeId); 151 | const position = selectedNode.getRenderBox(); 152 | // const element = document.getElementById('kityminder-editor'); 153 | let inputElement = document.getElementById("node-input-container"); 154 | inputElement.style.width = String(position.width + 100) + "px"; 155 | inputElement.style.left = String(position.x) + "px"; // - position.width / 2 156 | inputElement.style.top = String(position.y) + "px"; //- position.height / 2 157 | inputElement.style.display = "block"; 158 | const value = isUndefined(text) ? selectedNode.data.text : text; 159 | document.getElementById("core-node-input-disableKeydown").focus(); 160 | this.setEdit(true, value); 161 | }; 162 | 163 | saveAndExit = () => { 164 | const value = $("#core-node-input-disableKeydown").val(); 165 | editorCommand.handleText(value, this.editNodeId); 166 | this.editExit(); 167 | }; 168 | 169 | editExit = () => { 170 | document.getElementById("node-input-container").style.display = "none"; 171 | this.setEdit(false, ""); 172 | document.getElementById("kityminder-core").focus(); 173 | }; 174 | 175 | /** 176 | * 设置执行结果 177 | * @param {*} key 178 | */ 179 | handleExecuteResult = (key) => { 180 | editorCommand.handleResult(key); 181 | if (this.props.props.onResultChange) { 182 | this.props.props.onResultChange(); 183 | } 184 | }; 185 | 186 | /** 187 | * 添加标签 188 | * @param {} value 189 | */ 190 | addTag = (value) => { 191 | if (editorCommand.isNode()) { 192 | let temp = editorCommand.getResource(); 193 | if (temp.indexOf(value) === -1) { 194 | editorCommand.handleResource(value, 0); 195 | } 196 | } 197 | }; 198 | 199 | handleListenerForEditor = (e) => { 200 | // if (!this.props.editable) return; 201 | if (this.isEdit) { 202 | return; 203 | } 204 | 205 | if (!this.props.state.readOnly) { 206 | if (e.keyCode === 27) { 207 | // 退出编辑、右键菜单Esc 208 | e.stopPropagation(); 209 | e.preventDefault(); 210 | this.editExit(); 211 | window.editor.hotbox.idle(); 212 | return; 213 | } 214 | 215 | // 需要判断某些情况下不响应快捷键 216 | if (document.activeElement.id.indexOf("disableKeydown") !== -1) return; 217 | if (e.shiftKey && e.keyCode === 9) { 218 | // 父级主题shift + Tab 219 | this.handleAppend("parentNode"); 220 | e.stopPropagation(); 221 | e.preventDefault(); 222 | window.editor.hotbox.idle(); 223 | return; 224 | } 225 | 226 | if (e.keyCode === 9 && !e.shiftKey) { 227 | // 下级主题Tab 228 | this.handleAppend("childNode"); 229 | e.stopPropagation(); 230 | e.preventDefault(); 231 | window.editor.hotbox.idle(); 232 | return; 233 | } 234 | if (e.keyCode === 13) { 235 | // 同级主题Enter 236 | this.handleAppend("siblingNode"); 237 | e.stopPropagation(); 238 | e.preventDefault(); 239 | window.editor.hotbox.idle(); 240 | return; 241 | } 242 | 243 | if (e.keyCode === 113) { 244 | // F2 245 | this.handleEdit(); 246 | e.stopPropagation(); 247 | e.preventDefault(); 248 | return; 249 | } 250 | 251 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 49) { 252 | // 前置条件 ctrl + 1 253 | this.addTag("前置条件"); 254 | e.stopPropagation(); 255 | e.preventDefault(); 256 | return; 257 | } 258 | 259 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 50) { 260 | // 操作步骤 ctrl + 2 261 | this.addTag("执行步骤"); 262 | e.stopPropagation(); 263 | e.preventDefault(); 264 | return; 265 | } 266 | 267 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 51) { 268 | // 预期结果 ctrl + 3 269 | this.addTag("预期结果"); 270 | e.stopPropagation(); 271 | e.preventDefault(); 272 | return; 273 | } 274 | 275 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 75) { 276 | // Ctrl + K 调出链接的添加菜单 277 | let selectedNode = window.minder.getSelectedNode(); 278 | if (selectedNode === null) return; 279 | this.props.setMindHyperLinkVisible(); 280 | e.stopPropagation(); 281 | e.preventDefault(); 282 | return; 283 | } 284 | 285 | if (e.altKey && e.keyCode === 38) { 286 | // 节点上移alt + Up 287 | editorCommand.handleUp(); 288 | e.stopPropagation(); 289 | e.preventDefault(); 290 | return; 291 | } 292 | if (e.altKey && e.keyCode === 40) { 293 | // 节点下移alt + Down 294 | editorCommand.handleDown(); 295 | e.stopPropagation(); 296 | e.preventDefault(); 297 | return; 298 | } 299 | 300 | if (e.keyCode === 8 || e.keyCode === 46) { 301 | // 删除Delete 302 | editorCommand.handleRemove(); 303 | e.stopPropagation(); 304 | e.preventDefault(); 305 | return; 306 | } 307 | 308 | if (e.keyCode === 32) { 309 | // 空格键 310 | this.handleEdit(); 311 | e.stopPropagation(); 312 | e.preventDefault(); 313 | return; 314 | } 315 | 316 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 88) { 317 | // 撤销Ctrl + X 318 | window.editor.clipBoard.beforeCopy(e); 319 | editorCommand.cutNodes(); 320 | e.stopPropagation(); 321 | e.preventDefault(); 322 | return; 323 | } 324 | } 325 | 326 | // 如果是任务执行 327 | if (this.props.props.type === "record") { 328 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 48) { 329 | // 清除执行结果 ctrl + 0 330 | this.handleExecuteResult(0); 331 | e.stopPropagation(); 332 | e.preventDefault(); 333 | return; 334 | } 335 | 336 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 49) { 337 | //执行通过 ctrl + 1 338 | this.handleExecuteResult(2); 339 | e.stopPropagation(); 340 | e.preventDefault(); 341 | return; 342 | } 343 | 344 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 50) { 345 | //执行失败 ctrl + 2 346 | this.handleExecuteResult(1); 347 | e.stopPropagation(); 348 | e.preventDefault(); 349 | return; 350 | } 351 | 352 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 51) { 353 | //阻塞 ctrl+3 354 | this.handleExecuteResult(3); 355 | e.stopPropagation(); 356 | e.preventDefault(); 357 | return; 358 | } 359 | 360 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 52) { 361 | // 不适用 预期结果 ctrl + 4 362 | this.handleExecuteResult(4); 363 | e.stopPropagation(); 364 | e.preventDefault(); 365 | return; 366 | } 367 | } 368 | 369 | if (e.ctrlKey && e.keyCode === 65) { 370 | // 全选Ctrl + a 371 | let selectedNodes = []; 372 | window.minder.getRoot().traverse((node) => { 373 | selectedNodes.push(node); 374 | }); 375 | window.minder.select(selectedNodes, true); 376 | e.stopPropagation(); 377 | e.preventDefault(); 378 | return; 379 | } 380 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 90) { 381 | // 撤销Ctrl + z 382 | window.editor.history.undo(); 383 | e.stopPropagation(); 384 | e.preventDefault(); 385 | return; 386 | } 387 | 388 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 70) { 389 | // 撤销Ctrl + F 390 | // window.editor.search.setSearch(true); 391 | this.props.handleState("searchDrawerVisible", true); 392 | e.stopPropagation(); 393 | e.preventDefault(); 394 | return; 395 | } 396 | 397 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 187) { 398 | // 缩小 399 | editorCommand.zoomIn(); 400 | e.stopPropagation(); 401 | e.preventDefault(); 402 | return; 403 | } 404 | 405 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 189) { 406 | // 放大 407 | editorCommand.zoomOut(); 408 | e.stopPropagation(); 409 | e.preventDefault(); 410 | return; 411 | } 412 | 413 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 89) { 414 | // 重做Ctrl + y 415 | window.editor.history.redo(); 416 | e.stopPropagation(); 417 | e.preventDefault(); 418 | return; 419 | } 420 | 421 | if ((e.ctrlKey || e.metaKey) && e.keyCode === 83) { 422 | // ctrl + s 423 | this.props.props.onSave && this.props.props.onSave(); 424 | e.stopPropagation(); 425 | e.preventDefault(); 426 | return; 427 | } 428 | 429 | if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === 76) { 430 | // Ctrl +shift + L 431 | editorCommand.resetLayout(); 432 | e.stopPropagation(); 433 | e.preventDefault(); 434 | return; 435 | } 436 | 437 | window.minder.fire("normal.keydown", e); 438 | }; 439 | 440 | isIntendToInput(e) { 441 | if (e.ctrlKey || e.metaKey || e.altKey) return false; 442 | 443 | // a-zA-Z 444 | if (e.keyCode >= 65 && e.keyCode <= 90) return true; 445 | 446 | // 0-9 以及其上面的符号 447 | if (e.keyCode >= 48 && e.keyCode <= 57) return true; 448 | 449 | // 小键盘区域 (除回车外) 450 | if (e.keyCode !== 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 451 | 452 | // 输入法 453 | if (e.keyCode === 229 || e.keyCode === 0) return true; 454 | 455 | return false; 456 | } 457 | 458 | handleAppend = (type) => { 459 | this.AppendLock++; 460 | editorCommand.handleAppend(type, ""); 461 | setTimeout(this.afterAppend, 300); 462 | // window.minder.on('layoutallfinish', this.afterAppend); 463 | }; 464 | 465 | afterAppend = () => { 466 | if (!--this.AppendLock) { 467 | this.handleEdit(); 468 | } 469 | // window.minder.off('layoutallfinish', this.afterAppend); 470 | }; 471 | 472 | reset() { 473 | this.listenDisable = true; 474 | this.nodeInfo = { 475 | id: "", 476 | text: "", 477 | note: "", 478 | hyperlink: {}, 479 | image: {}, 480 | timeStamp: String(new Date().getTime()), 481 | }; 482 | this.isInit = false; 483 | this.setEdit(false, ""); 484 | this.setNode(); 485 | } 486 | 487 | updateNodeInfo = () => { 488 | if (this.infoLock) return; 489 | this.infoLock = true; 490 | const vm = this; 491 | setTimeout(function () { 492 | let selectedNode = window.minder.getSelectedNode(); 493 | if (selectedNode !== null) { 494 | vm.nodeInfo = { 495 | id: selectedNode.data.id, 496 | text: selectedNode.data.text, 497 | note: selectedNode.data.note, 498 | hyperlink: { 499 | url: selectedNode.data.hyperlink, 500 | title: selectedNode.data.hyperlinkTitle, 501 | }, 502 | nodeLink: selectedNode.data.nodeLink, 503 | image: { 504 | url: selectedNode.data.image, 505 | title: "", 506 | }, 507 | background: selectedNode.data.background, 508 | timeStamp: String(new Date().getTime()), 509 | }; 510 | } else { 511 | vm.nodeInfo = { 512 | id: "", 513 | text: "", 514 | note: "", 515 | hyperlink: {}, 516 | nodeLink: "", 517 | image: {}, 518 | background: "", 519 | timeStamp: String(new Date().getTime()), 520 | }; 521 | window.editor.hotbox.idle(); 522 | } 523 | // vm.setIsNode(); 524 | vm.setNode(); 525 | vm.infoLock = false; 526 | }); 527 | }; 528 | 529 | updateNote = (value) => { 530 | this.nodeInfo.note = value; 531 | this.setNode(); 532 | }; 533 | 534 | setListenDisable = (value) => { 535 | this.listenDisable = value; 536 | }; 537 | 538 | setEdit = (isEdit, text) => { 539 | this.isEdit = isEdit; 540 | // this.editText = text; 541 | if (isEdit) { 542 | this.props.handleChange(text); 543 | } 544 | 545 | // document.getElementById('node-input-container').value = text; 546 | // this._EditorMode.set({ isEdit: isEdit, editText: text }); 547 | }; 548 | 549 | setNode = () => { 550 | this.props.handleState("nodeInfo", this.nodeInfo); 551 | }; 552 | 553 | setIsNode = () => { 554 | this.props.handleState("isNode", this.isNode); 555 | }; 556 | 557 | setIsRoot = () => { 558 | // const selectedNode = window.minder.getSelectedNode(); 559 | // this.isRoot = selectedNode && isNull(selectedNode.parent); 560 | }; 561 | } 562 | 563 | export default Runtime; 564 | -------------------------------------------------------------------------------- /src/model/ToolBox.js: -------------------------------------------------------------------------------- 1 | import { isEqual, unionWith, remove, isNull, isUndefined } from "lodash"; 2 | import $ from "jquery"; 3 | import marked from "marked/lib/marked"; 4 | 5 | class ToolBox { 6 | // 构造函数 7 | constructor(props) { 8 | this.setProps(props); 9 | 10 | this.toolbox = false; 11 | // 调起备注预览 12 | window.minder.on("shownoterequest", this.enterNotePreview); 13 | window.minder.on("hidenoterequest", () => clearTimeout(this.previewTimer)); 14 | 15 | // 退出备注预览 16 | $("#kityminder-core").on( 17 | "mousedown mousewheel DOMMouseScroll", 18 | this.exitNotePreview 19 | ); 20 | } 21 | 22 | /** 23 | * 24 | */ 25 | setProps = (props) => { 26 | this.props = props; 27 | }; 28 | 29 | enterNotePreview = (e) => { 30 | const vm = this; 31 | this.previewTimer = setTimeout(function () { 32 | vm.preview(e.node); 33 | }, 300); 34 | }; 35 | 36 | exitNotePreview = (e) => { 37 | // 退出预览 38 | if (!this.previewLive) return; 39 | document.getElementById("note-previewer").style.display = "none"; 40 | }; 41 | 42 | setToolbox = (visible, tab) => { 43 | this.toolbox = visible; 44 | this.props.handleState("toolbox", this.toolbox); 45 | this.props.handleState("toolboxTab", tab); 46 | if (!visible) document.getElementById("kityminder-editor").focus(); 47 | }; 48 | 49 | preview = (node) => { 50 | let icon = node 51 | .getRenderer("NoteIconRenderer") 52 | .getRenderShape() 53 | .getRenderBox("screen"); 54 | let note = node.getData("note"); 55 | document.getElementById("note-preview-content").innerHTML = marked(note); 56 | let editors = document.getElementsByClassName("kityminder-editor"); 57 | let x = 58 | icon.left - 59 | (document.documentElement.clientWidth - editors[0].clientWidth); 60 | let y = icon.bottom + 8 - editors[0].getBoundingClientRect().top; 61 | if (x < 0) x = 10; 62 | 63 | $("#note-previewer").css({ 64 | left: Math.round(x), 65 | top: Math.round(y), 66 | display: "block", 67 | }); 68 | this.previewLive = true; 69 | }; 70 | } 71 | 72 | export default ToolBox; 73 | -------------------------------------------------------------------------------- /src/pages/EditUsers/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Collapse } from "antd"; 3 | const { Panel } = Collapse; 4 | import "./style.less"; 5 | class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | 11 | componentWillReceiveProps = (nextProps) => {}; 12 | 13 | render() { 14 | const { userName, editUsers } = this.props; 15 | 16 | return ( 17 |
18 | 19 | 22 | {editUsers.length}人正在查看 23 |
24 | } 25 | key="1" 26 | > 27 | {editUsers.map((item) => { 28 | return ( 29 |
30 |
{item}
31 | {userName === item ? (我) : null} 32 |
33 | ); 34 | })} 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /src/pages/EditUsers/style.less: -------------------------------------------------------------------------------- 1 | .edit-users-container { 2 | position: absolute; 3 | background: #fff; 4 | top: 170px; 5 | left: 10px; 6 | float: left; 7 | z-index: 99; 8 | 9 | .ant-collapse-header { 10 | padding: 5px 10px !important; 11 | padding-left: 30px !important; 12 | } 13 | 14 | .user-container { 15 | margin-top: 3px; 16 | display: flex; 17 | 18 | .name { 19 | font-size: 12px; 20 | color: rgb(114, 128, 141); 21 | } 22 | 23 | .me { 24 | font-size: 12px; 25 | color: rgb(114, 128, 141); 26 | margin-left: 5px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/Exterior/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Icon, Popover, Divider, Menu, Dropdown } from "antd"; 4 | import * as editorComand from "../../command/EditorCommand"; 5 | import { partial } from "lodash"; 6 | import config from "../../constant/config.minder"; 7 | 8 | class App extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | componentWillMount() { 14 | // window.editor['search'] = new Search(this.props); 15 | } 16 | 17 | setTemplate = (value) => { 18 | editorComand.handleTemplate(value); 19 | this.props.handleState("template", value); 20 | }; 21 | 22 | setTheme = (value) => { 23 | editorComand.handleTheme(value); 24 | this.props.handleState("theme", value); 25 | }; 26 | 27 | setSearch = (visible) => { 28 | this.props.handleState("searchDrawerVisible", visible); 29 | }; 30 | 31 | render() { 32 | const { template, theme, minderStatus } = this.props; 33 | let editable = minderStatus === "normal"; 34 | 35 | const templateMenu = ( 36 |
37 | {config.template.map((item) => ( 38 |
47 | 52 |
53 | ))} 54 |
55 | ); 56 | var themeMenu = []; 57 | for (var key in config.theme) { 58 | const item = config.theme[key]; 59 | themeMenu.push( 60 | 76 | ); 77 | } 78 | const expandMenu = ( 79 | 80 | 81 | 展开到一级节点 82 | 83 | 84 | 展开到二级节点 85 | 86 | 87 | 展开到三级节点 88 | 89 | 90 | 展开到四级节点 91 | 92 | 93 | 展开到五级节点 94 | 95 | 96 | 展开到六级节点 97 | 98 | 99 | 全部展开 100 | 101 | 102 | ); 103 | return ( 104 | } 137 | > 138 | 156 | 157 | )} 158 | 159 | 160 | 169 | 170 | 171 |
175 | 176 | 整理布局 177 |
178 | 179 | {this.props.type === "compare" ? ( 180 |
184 | 195 | 搜索 196 |
197 | ) : null} 198 | 199 | ); 200 | } 201 | } 202 | 203 | export default App; 204 | -------------------------------------------------------------------------------- /src/pages/Exterior/style.less: -------------------------------------------------------------------------------- 1 | .exterior-container { 2 | height: 60px; 3 | padding: 5px 8px; 4 | .m-icon-disabled { 5 | opacity: 0.5; 6 | } 7 | .caret { 8 | vertical-align: bottom; 9 | } 10 | .block { 11 | display: block; 12 | } 13 | .km-btn-item { 14 | background: url(../../assets/images/icons.png) no-repeat; 15 | width: 20px; 16 | height: 20px; 17 | padding: 2px; 18 | margin: 1px; 19 | vertical-align: middle; 20 | margin: 1px auto; 21 | } 22 | .expand { 23 | background-position: 0 -995px; 24 | } 25 | 26 | .reset-layout { 27 | background-position: 0 -150px; 28 | height: 25px; 29 | } 30 | } 31 | 32 | .inline-block-pointer { 33 | display: inline-block; 34 | cursor: pointer; 35 | &:hover { 36 | background-color: #f5f5f5; 37 | } 38 | } 39 | 40 | .inline-block { 41 | display: inline-block; 42 | } 43 | 44 | .selected { 45 | background-color: #87a9da; 46 | } 47 | .default { 48 | background-position: 0px 0px !important; 49 | } 50 | .structure { 51 | background-position: -50px 0px !important; 52 | } 53 | .filetree { 54 | background-position: -100px 0px !important; 55 | } 56 | .right { 57 | background-position: -150px 0px !important; 58 | } 59 | .fish-bone { 60 | background-position: -200px 0px !important; 61 | } 62 | .tianpan { 63 | background-position: -250px 0px !important; 64 | } 65 | 66 | .template-list { 67 | width: 120px; 68 | } 69 | .template-item { 70 | margin: 5px; 71 | } 72 | .km-btn-item-template { 73 | background: url(../../assets/images/template.png) no-repeat; 74 | width: 50px; 75 | height: 40px; 76 | } 77 | .theme-list { 78 | width: 160px; 79 | } 80 | .theme-item { 81 | display: inline-block; 82 | cursor: pointer; 83 | margin: 5px; 84 | width: 70px; 85 | height: 30px; 86 | text-align: center; 87 | line-height: 30px; 88 | } 89 | 90 | /*.exterior-popover { 91 | .ant-popover-arrow { 92 | display: none !important; 93 | } 94 | .ant-popover-placement-bottom { 95 | padding-top: 0px !important; 96 | } 97 | }*/ 98 | -------------------------------------------------------------------------------- /src/pages/Mind/ColorPicker/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import config from "../../../constant/config.minder"; 4 | import { partial } from "lodash"; 5 | 6 | class App extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | const { set } = this.props; 13 | return ( 14 |
15 |
主题颜色
16 |
17 | {config.commonColor.map((citem, cindex) => { 18 | return ( 19 |
20 | {citem.map((item, index) => { 21 | return ( 22 | 28 | ); 29 | })} 30 |
31 | ); 32 | })} 33 |
34 |
标准颜色
35 |
36 | {config.standardColor.map((item, index) => { 37 | return ( 38 | 44 | ); 45 | })} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /src/pages/Mind/ColorPicker/style.less: -------------------------------------------------------------------------------- 1 | .colorpicker-container { 2 | width: 170px; 3 | .color-line { 4 | line-height: 0px; 5 | } 6 | .color-item { 7 | display: inline-block; 8 | margin: 0 2px; 9 | width: 13px; 10 | height: 13px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/Mind/HyperLink/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // import './style.less'; 3 | import { Modal, Form, Input } from "antd"; 4 | import { partial, isUndefined } from "lodash"; 5 | import * as editorCommand from "../../../command/EditorCommand"; 6 | 7 | class App extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | handleOk = () => { 13 | this.props.form.validateFields((err, values) => { 14 | if (err) return; 15 | editorCommand.handleHyperlink(values); 16 | const { nodeInfo } = this.props; 17 | nodeInfo.hyperlink = values; 18 | this.onCancel(); 19 | }); 20 | }; 21 | 22 | onCancel = () => { 23 | this.props.form.resetFields(); 24 | this.props.onCancel(false); 25 | }; 26 | 27 | handleFocus = (value) => { 28 | // window.editor.runtime.setListenDisable(value); 29 | }; 30 | 31 | render() { 32 | const { visible, nodeInfo } = this.props; 33 | const { getFieldDecorator } = this.props.form; 34 | const formItemLayout = { labelCol: { span: 7 }, wrapperCol: { span: 13 } }; 35 | return ( 36 | 45 |
46 | 47 | {getFieldDecorator("url", { 48 | initialValue: isUndefined(nodeInfo.hyperlink.url) 49 | ? "" 50 | : nodeInfo.hyperlink.url, 51 | rules: [{ required: true, message: "请输入链接地址" }], 52 | })( 53 | 57 | )} 58 | 59 | 60 | {getFieldDecorator("title", { 61 | initialValue: isUndefined(nodeInfo.hyperlink.title) 62 | ? "" 63 | : nodeInfo.hyperlink.title, 64 | })( 65 | 69 | )} 70 | 71 |
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default Form.create()(App); 78 | -------------------------------------------------------------------------------- /src/pages/Mind/Image/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Modal, Form, Upload, Button, notification, message } from "antd"; 4 | import { partial, isUndefined } from "lodash"; 5 | import * as editorComand from "../../../command/EditorCommand"; 6 | 7 | class App extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | loading: false, 12 | }; 13 | } 14 | 15 | componentWillReceiveProps = (nextProps) => {}; 16 | 17 | componentWillMount() {} 18 | 19 | handleOk = () => { 20 | this.props.form.validateFields((err, values) => { 21 | if (err) return; 22 | editorComand.handleImage(values); 23 | const { nodeInfo } = this.props; 24 | nodeInfo.image = values; 25 | // this._EditorMode.set({ nodeInfo: nodeInfo }); 26 | this.onCancel(); 27 | setTimeout(() => { 28 | window.minder.fire("contentchange"); 29 | }, 500); 30 | }); 31 | }; 32 | 33 | onCancel = () => { 34 | this.props.form.resetFields(); 35 | this.props.onCancel(false); 36 | }; 37 | 38 | handleFocus = (value) => { 39 | window.editor.runtime.setListenDisable(value); 40 | }; 41 | 42 | render() { 43 | const { loading } = this.state; 44 | const { visible, nodeInfo, uploadUrl } = this.props; 45 | const { getFieldDecorator, getFieldValue, setFieldsValue } = 46 | this.props.form; 47 | const props = { 48 | name: "file", 49 | showUploadList: false, 50 | accept: "image/*", 51 | action: uploadUrl, 52 | data: (file) => { 53 | var formData = {}; 54 | formData["file"] = file; 55 | return formData; 56 | }, 57 | beforeUpload: (file) => { 58 | if (file.type.indexOf("image") == -1) { 59 | message.warning("仅允许上传图片"); 60 | return false; 61 | } else { 62 | return true; 63 | } 64 | }, 65 | onChange: (info) => { 66 | this.setState({ loading: true }); 67 | const status = info.file.status; 68 | if (status !== "uploading") { 69 | this.setState({ loading: false }); 70 | } 71 | if (status === "done") { 72 | setFieldsValue({ url: info.file.response.data[0].url }); 73 | } else if (status === "error") { 74 | notification.error({ 75 | message: "提示", 76 | description: "图片上传失败!", 77 | duration: 4, 78 | }); 79 | } 80 | }, 81 | }; 82 | return ( 83 | 92 |
93 | 96 | 图片 97 | (仅支持jpg、png图片) 98 | 99 | } 100 | > 101 | {getFieldDecorator("url", { 102 | initialValue: nodeInfo.image.url, 103 | trigger: "", 104 | rules: [{ required: true, message: "请选择图片" }], 105 | })( 106 | 107 | 111 | 112 | )} 113 | 114 |
115 | {!isUndefined(getFieldValue("url")) && ( 116 | 117 | )} 118 |
119 | ); 120 | } 121 | } 122 | 123 | export default Form.create()(App); 124 | -------------------------------------------------------------------------------- /src/pages/Mind/Image/style.less: -------------------------------------------------------------------------------- 1 | .image-container { 2 | .ui-logo { 3 | max-width: 100%; 4 | } 5 | .ant-form-item { 6 | margin-bottom: 15px !important; 7 | } 8 | .ant-form-item-label { 9 | line-height: 30px !important; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Mind/NodeLink/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Modal, Tree, Input } from "antd"; 3 | const { TreeNode } = Tree; 4 | const { Search } = Input; 5 | import { partial } from "lodash"; 6 | import * as editorCommand from "../../../command/EditorCommand"; 7 | 8 | class App extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | expandedKeys: [], 13 | searchValue: "", 14 | autoExpandParent: true, 15 | data: [], 16 | dataList: [], 17 | selectedNodeId: "", 18 | }; 19 | } 20 | 21 | componentDidMount = () => {}; 22 | 23 | componentWillReceiveProps = (nextProps) => { 24 | if (nextProps.visible !== this.props.visible && nextProps.visible) { 25 | let json = editorCommand.exportJson(); 26 | 27 | this.setState({ 28 | data: [json.root], 29 | }); 30 | } 31 | }; 32 | 33 | handleOk = () => { 34 | if (this.state.selectedNodeId !== "") { 35 | editorCommand.handleNodeLink(this.state.selectedNodeId); 36 | } 37 | this.setState({ 38 | searchValue: "", 39 | }); 40 | this.props.onCancel(false); 41 | }; 42 | 43 | onCancel = () => { 44 | this.props.onCancel(false); 45 | }; 46 | 47 | onExpand = (expandedKeys) => { 48 | this.setState({ 49 | expandedKeys, 50 | autoExpandParent: false, 51 | }); 52 | }; 53 | 54 | getParentKey = (key, tree) => { 55 | let parentKey; 56 | for (let i = 0; i < tree.length; i++) { 57 | const node = tree[i]; 58 | if (node.children) { 59 | if (node.children.some((item) => item.key === key)) { 60 | parentKey = node.key; 61 | } else if (this.getParentKey(key, node.children)) { 62 | parentKey = this.getParentKey(key, node.children); 63 | } 64 | } 65 | } 66 | return parentKey; 67 | }; 68 | 69 | onChange = (e) => { 70 | const { value } = e.target; 71 | 72 | let dataList = []; 73 | window.minder.getRoot().traverse((node) => { 74 | if (node.data.text.indexOf(value) !== -1) { 75 | dataList.push(node); 76 | } 77 | }); 78 | 79 | this.setState({ 80 | searchValue: value, 81 | dataList, 82 | }); 83 | }; 84 | 85 | onSelect = (selectedKeys) => { 86 | this.setState({ 87 | selectedNodeId: selectedKeys.length > 0 ? selectedKeys[0] : "", 88 | }); 89 | }; 90 | 91 | render() { 92 | const { visible } = this.props; 93 | const { searchValue, autoExpandParent } = this.state; 94 | 95 | const loop = (data) => 96 | data.map((item) => { 97 | const index = item.data.text.indexOf(searchValue); 98 | const beforeStr = item.data.text.substr(0, index); 99 | const afterStr = item.data.text.substr(index + searchValue.length); 100 | const text = 101 | index > -1 ? ( 102 | 103 | {beforeStr} 104 | {searchValue} 105 | {afterStr} 106 | 107 | ) : ( 108 | {item.data.text} 109 | ); 110 | if (item.children) { 111 | return ( 112 | 113 | {loop(item.children)} 114 | 115 | ); 116 | } 117 | return ; 118 | }); 119 | 120 | return ( 121 | 130 | 135 | {this.state.data.length > 0 && ( 136 | 144 | {this.state.searchValue !== "" 145 | ? loop(this.state.dataList) 146 | : loop(this.state.data)} 147 | 148 | )} 149 | 150 | ); 151 | } 152 | } 153 | 154 | export default App; 155 | -------------------------------------------------------------------------------- /src/pages/Mind/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Icon, 4 | Menu, 5 | Dropdown, 6 | Popover, 7 | Tooltip, 8 | Button, 9 | Select, 10 | Tag, 11 | } from "antd"; 12 | import "./style.less"; 13 | import { partial, isEmpty, isUndefined, isString } from "lodash"; 14 | import * as editorCommand from "../../command/EditorCommand"; 15 | import HyperLink from "./HyperLink"; 16 | import NodeLink from "./NodeLink"; 17 | import ImageUpload from "./Image"; 18 | import ColorPicker from "./ColorPicker"; 19 | 20 | const { Option } = Select; 21 | 22 | class App extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | hyperlink: false, 27 | nodeLink: false, 28 | image: false, 29 | fontColor: "#000", 30 | bgColor: "rgb(115, 161, 191)", 31 | hasNodeSelected: false, 32 | nodeInfo: { 33 | hyperlink: "", 34 | }, 35 | currentResource: [], 36 | image: false, 37 | }; 38 | } 39 | 40 | componentDidMount = () => { 41 | this.props.onRef(this); 42 | }; 43 | 44 | componentWillReceiveProps = (nextProps) => { 45 | if (nextProps.nodeInfo.id !== this.props.nodeInfo.id) { 46 | nextProps.handleState("usedResource", window.minder.getUsedResource()); 47 | this.setState({ 48 | currentResource: nextProps.isNode 49 | ? editorCommand.getResource() === null 50 | ? [] 51 | : editorCommand.getResource() 52 | : [], 53 | }); 54 | } 55 | }; 56 | 57 | setHyperlink = (visible) => { 58 | this.setState({ 59 | hyperlink: visible, 60 | }); 61 | }; 62 | 63 | setNodeLink = (visible) => { 64 | this.setState({ 65 | nodeLink: visible, 66 | }); 67 | }; 68 | 69 | setImage = (visible) => { 70 | this.setState({ image: visible }); 71 | }; 72 | 73 | handleHyperlink = () => { 74 | editorCommand.handleHyperlink({ url: null, title: "" }); 75 | //更新其中的值 76 | // window.editor.runtime.nodeInfo = { url: null, title: '' }; 77 | }; 78 | 79 | handleNodeLink = () => { 80 | editorCommand.handleNodeLink(""); 81 | }; 82 | 83 | handlePriority = (value) => { 84 | editorCommand.handlePriority(value); 85 | }; 86 | 87 | /** 88 | * 设置背景颜色 89 | * @param {*} color 90 | */ 91 | setBgColor = (color) => { 92 | // if (!this.props.editable) return; 93 | this.setState({ bgColor: color }); 94 | editorCommand.handleBgColor(color); 95 | }; 96 | 97 | /** 98 | * 设置字体颜色 99 | * @param {}} color 100 | */ 101 | setFontColor = (color) => { 102 | this.setState({ fontColor: color }); 103 | editorCommand.handleForeColor(color); 104 | }; 105 | 106 | /** 107 | * 标签发生变化时 108 | */ 109 | onTagChange = (value) => {}; 110 | 111 | /** 112 | * 清除样式 113 | */ 114 | handleClear = () => { 115 | editorCommand.handleClear(); 116 | }; 117 | 118 | onTagSelect = (value) => { 119 | let temp = JSON.parse(JSON.stringify(this.state.currentResource)); 120 | let isMatch = false; 121 | for (const item of temp) { 122 | let name = isString(item) ? item : item.name; 123 | isMatch = value == name || value.name == name; 124 | if (isMatch) { 125 | break; 126 | } 127 | } 128 | 129 | if (isMatch) { 130 | this.onTagDeSelect(value); 131 | } else { 132 | temp.push(value); 133 | editorCommand.handleResource(value, 0); 134 | this.setState({ 135 | currentResource: temp, 136 | }); 137 | } 138 | 139 | // if (this.state.usedResource.indexOf(value) === -1) { 140 | // let temp = JSON.parse(JSON.stringify(this.state.usedResource)); 141 | // temp.push(value); 142 | // this.setState({ 143 | // currentResource: temp 144 | // }) 145 | // } 146 | }; 147 | 148 | onTagDeSelect = (value) => { 149 | let temp = JSON.parse(JSON.stringify(this.state.currentResource)); 150 | temp = temp.filter((item) => { 151 | if (isString(value)) { 152 | return item !== value && item.name !== value; 153 | } else { 154 | return item !== value.name && item.name !== value.name; 155 | } 156 | }); 157 | editorCommand.handleResource(value, 1); 158 | this.setState({ 159 | currentResource: temp, 160 | }); 161 | }; 162 | 163 | onTagFouce = () => {}; 164 | 165 | renderTags = () => { 166 | const { isNode, tags } = this.props; 167 | if (!isUndefined(window.minder)) { 168 | return tags.map((item) => { 169 | let color = window.minder.getResourceColor(item).toHEX(); 170 | return ( 171 | { 174 | this.onTagSelect(item); 175 | }} 176 | className={`resource-tag ${isNode ? "" : "disabled"}`} 177 | color={color} 178 | > 179 | {item} 180 | 181 | ); 182 | }); 183 | } 184 | }; 185 | 186 | setSearch = (visible) => { 187 | this.props.handleState("searchDrawerVisible", visible); 188 | }; 189 | 190 | handleImage = () => { 191 | editorCommand.handleImage({ url: null, title: "" }); 192 | setTimeout(() => { 193 | window.minder.fire("contentchange"); 194 | }, 500); 195 | }; 196 | 197 | /** 198 | * 打开备注 199 | */ 200 | setNote = () => { 201 | this.props.handleState("toolbox", true); 202 | this.props.handleState("toolboxTab", "note"); 203 | }; 204 | 205 | /** 206 | * 设置执行结果 207 | * @param {*} key 208 | */ 209 | handleExecuteResult = (key) => { 210 | editorCommand.handleExecutor(key === 0 ? "" : this.props.userName); 211 | editorCommand.handleResult(key); 212 | if (this.props.onResultChange) { 213 | this.props.onResultChange(); 214 | } 215 | }; 216 | 217 | render() { 218 | const { isNode, nodeInfo, hasUndo, hasRedo } = this.props; 219 | 220 | const { hyperlink, image, fontColor, bgColor, nodeLink } = this.state; 221 | 222 | var priorityList = []; 223 | for (let i = 0; i < 3; i++) { 224 | priorityList.push( 225 | 234 | ); 235 | } 236 | 237 | // const addChild = () => { 238 | // return 239 | // } 240 | 241 | // const addParent = () => { 242 | // return 243 | // } 244 | 245 | // const addSibling = () => { 246 | // return 247 | // } 248 | 249 | const hyperlinkMenu = ( 250 |
251 | 252 | 253 | {isEmpty(nodeInfo.hyperlink) || isEmpty(nodeInfo.hyperlink.url) 254 | ? "插入超链接" 255 | : "打开超链接"} 256 | 257 | 258 | {isEmpty(nodeInfo.hyperlink) || 259 | isEmpty(nodeInfo.hyperlink.url) ? null : ( 260 | 261 | 移除超链接 262 | 263 | )} 264 | 265 | 266 | 267 | {isEmpty(nodeInfo.nodeLink) ? "插入主题链接" : "打开主题链接"} 268 | 269 | 270 | 271 | {isEmpty(nodeInfo.nodeLink) ? null : ( 272 | 273 | 移除主题链接 274 | 275 | )} 276 | 277 | ); 278 | 279 | const imageMenu = isEmpty(nodeInfo.image) ? ( 280 | 281 | 282 | 插入图片 283 | 284 | 285 | ) : ( 286 | 287 | 288 | 打开图片 289 | 290 | 291 | 移除图片 292 | 293 | 294 | ); 295 | 296 | const commentMenu = ( 297 | 298 | 299 | 打开备注 300 | 301 | 302 | ); 303 | 304 | return ( 305 |
309 |
310 | 321 | 332 |
333 | {/* { 334 | this.props.readOnly ? null :
335 | 344 | 353 | 362 |
363 | } */} 364 | 365 | { 366 | /*
367 | 378 | 389 |
390 | 391 | 392 | */ 393 | // 394 | // 404 | // 405 | } 406 | 407 |
408 | {!this.props.readOnly ? null : ( 409 | 414 | 420 | 421 | )} 422 | 423 | 428 | 434 | 435 | { 436 | 441 | 447 | 448 | } 449 |
450 | { 451 | // 执行用例的结果 452 | this.props.readOnly ? ( 453 |
454 | 455 | 481 | 482 | 483 | 484 | 524 | 525 | 526 | 558 | 559 | 560 | 561 | 599 | 600 | 601 | 602 | 630 | 631 |
632 | ) : null 633 | } 634 | 635 | {this.props.readOnly ? null : ( 636 | <> 637 |
638 |
  • 644 | {priorityList} 645 |
    646 | 647 |
    648 |
    649 | 661 | 665 | 666 |
    667 | } 668 | > 669 | 670 | 674 | 675 | 676 |
    677 |
    678 | 686 | } 689 | > 690 | 691 | 695 | 696 | 697 |
    698 |
    699 | 700 |
    701 | 712 |
    713 | 714 | )} 715 | 716 |
    717 | 727 |
    728 | {this.props.readOnly ? null : ( 729 |
    730 |
    733 |
    734 | {this.renderTags()} 735 |
    736 | 737 |
    738 | 767 |
    768 |
    769 |
    770 | )} 771 | 772 | 777 | 778 | 783 | 784 | 790 | 791 | ); 792 | } 793 | } 794 | 795 | export default App; 796 | -------------------------------------------------------------------------------- /src/pages/Mind/style.less: -------------------------------------------------------------------------------- 1 | .minder-container { 2 | display: flex; 3 | align-items: center; 4 | flex-wrap: wrap; 5 | height: 110px; 6 | padding: 5px; 7 | 8 | .resource-tag.ant-tag-has-color { 9 | color: rgba(0, 0, 0, 0.65); 10 | } 11 | 12 | .resource-tag.ant-tag-has-color.disabled { 13 | cursor: not-allowed; 14 | opacity: 0.5; 15 | } 16 | 17 | .icon-disabled { 18 | color: rgba(0, 0, 0, 0.3); 19 | } 20 | 21 | .m-icon { 22 | cursor: pointer; 23 | 24 | &:hover { 25 | background: #dcdcdc; 26 | } 27 | } 28 | 29 | .big-icon { 30 | height: 60px !important; 31 | } 32 | 33 | .bg-icon { 34 | margin-top: 3px; 35 | } 36 | 37 | .m-icon-disabled { 38 | opacity: 0.5; 39 | } 40 | 41 | .m-margin { 42 | margin-left: 7px; 43 | } 44 | 45 | .inline { 46 | // display: inline-block; 47 | border-right: 1px dashed #eee; 48 | } 49 | 50 | .block { 51 | display: block; 52 | } 53 | 54 | // .km-btn-item { 55 | // display: inline-block; 56 | // background: url(../../../../../../../images/icons.png) no-repeat; 57 | // background-position: 0 20px; 58 | // width: 20px; 59 | // height: 20px; 60 | // padding: 2px; 61 | // margin: 1px; 62 | // vertical-align: middle; 63 | // } 64 | 65 | .ant-btn.priority-btn[disabled] { 66 | opacity: 0.4; 67 | } 68 | 69 | .priority-btn.p1 { 70 | background-color: #ff1200; 71 | border-bottom: 3px solid #840023; 72 | } 73 | 74 | .priority-btn.p2 { 75 | background-color: #0074ff; 76 | border-bottom: 3px solid #01467f; 77 | } 78 | 79 | .priority-btn.p3 { 80 | background-color: #00af00; 81 | border-bottom: 3px solid #006300; 82 | } 83 | 84 | .priority-btn { 85 | color: #fff !important; 86 | border-radius: 8px; 87 | padding: 0px 2px; 88 | margin-left: 2px; 89 | font-style: italic; 90 | font-size: 12px; 91 | height: 22px; 92 | margin-left: 4px; 93 | } 94 | 95 | .km-priority-item { 96 | display: inline-block; 97 | background: url(../../assets/images/iconpriority.png) no-repeat; 98 | background-position: 0 20px; 99 | width: 20px; 100 | height: 20px; 101 | padding: 2px; 102 | margin: 1px; 103 | vertical-align: middle; 104 | } 105 | 106 | .km-priority-item2 { 107 | display: inline-block; 108 | background: url(../../assets/images/iconpriority2.png) no-repeat; 109 | background-position: 0 20px; 110 | width: 20px; 111 | height: 20px; 112 | padding: 2px; 113 | margin: 1px; 114 | vertical-align: middle; 115 | } 116 | 117 | // .km-progress-item { 118 | // display: inline-block; 119 | // background: url(../../../../../../../images/iconprogress.png) no-repeat; 120 | // background-position: 0 20px; 121 | // width: 20px; 122 | // height: 20px; 123 | // padding: 2px; 124 | // margin: 1px; 125 | // vertical-align: middle; 126 | // } 127 | .m-append { 128 | width: 165px; 129 | } 130 | 131 | .append-child-node { 132 | background-position: 0 0; 133 | } 134 | 135 | .append-sibling-node { 136 | background-position: 0 -20px; 137 | } 138 | 139 | .append-parent-node { 140 | background-position: 0 -40px; 141 | } 142 | 143 | .up { 144 | background-position: 0 -280px; 145 | } 146 | 147 | .down { 148 | background-position: 0 -300px; 149 | } 150 | 151 | .undo { 152 | background-position: 0 -1240px; 153 | } 154 | 155 | .redo { 156 | background-position: 0 -1220px; 157 | } 158 | 159 | .m-priority { 160 | width: 120px; 161 | } 162 | 163 | .m-progress { 164 | width: 120px; 165 | } 166 | 167 | .priority-icon { 168 | margin: 0 2px; 169 | padding: 1px; 170 | } 171 | 172 | .progress-icon { 173 | margin: 0 2px; 174 | padding: 1px; 175 | } 176 | 177 | .priority-0 { 178 | background-position: 0 -180px; 179 | } 180 | 181 | .priority-1 { 182 | background-position: 0 0px; 183 | } 184 | 185 | .priority-2 { 186 | background-position: 0 -20px; 187 | } 188 | 189 | .priority-3 { 190 | background-position: 0 -40px; 191 | } 192 | 193 | .priority-4 { 194 | background-position: 0 -60px; 195 | } 196 | 197 | .priority-5 { 198 | background-position: 0 -80px; 199 | } 200 | 201 | .priority-6 { 202 | background-position: 0 -100px; 203 | } 204 | 205 | .priority-7 { 206 | background-position: 0 -120px; 207 | } 208 | 209 | .priority-8 { 210 | background-position: 0 -140px; 211 | } 212 | 213 | .priority-9 { 214 | background-position: 0 -160px; 215 | } 216 | 217 | .progress-0 { 218 | background-position: 0 -180px; 219 | } 220 | 221 | .progress-1 { 222 | background-position: 0 0px; 223 | } 224 | 225 | .progress-2 { 226 | background-position: 0 -20px; 227 | } 228 | 229 | .progress-3 { 230 | background-position: 0 -40px; 231 | } 232 | 233 | .progress-4 { 234 | background-position: 0 -60px; 235 | } 236 | 237 | .progress-5 { 238 | background-position: 0 -80px; 239 | } 240 | 241 | .progress-6 { 242 | background-position: 0 -100px; 243 | } 244 | 245 | .progress-7 { 246 | background-position: 0 -120px; 247 | } 248 | 249 | .progress-8 { 250 | background-position: 0 -140px; 251 | } 252 | 253 | .progress-9 { 254 | background-position: 0 -160px; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/pages/NavigatorRender/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { partial } from "lodash"; 4 | import * as editorCommand from "../../command/EditorCommand"; 5 | import config from "./../../constant/config.minder"; 6 | import { Icon } from "antd"; 7 | 8 | // interface IProps { 9 | // zoom: number, 10 | // triggerActive: boolean, 11 | // fullScreen: boolean 12 | // } 13 | 14 | class App extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.height = 70; // 缩放条的长度 18 | this.state = { 19 | handActive: false, 20 | realFullScreen: false, 21 | }; 22 | } 23 | 24 | zoomIn = () => { 25 | editorCommand.zoomIn(); 26 | }; 27 | 28 | getHeight = (value) => { 29 | const zoomList = config.zoom; 30 | const maxZoom = zoomList[zoomList.length - 1]; 31 | const minZoom = zoomList[0]; 32 | return (this.height * (maxZoom - value)) / (maxZoom - minZoom); 33 | }; 34 | 35 | zoom = (value) => { 36 | editorCommand.zoom(value); 37 | }; 38 | 39 | zoomOut = () => { 40 | editorCommand.zoomOut(); 41 | }; 42 | 43 | handClick = () => { 44 | this.setState({ handActive: !this.state.handActive }); 45 | editorCommand.hand(); 46 | }; 47 | 48 | triggerClick = () => { 49 | window.editor.navigator.handleTriggerClick(); 50 | }; 51 | 52 | fullScreenClick = () => { 53 | if (this.props.fullScreen || document.fullscreenElement) { 54 | this.closeFullScreen(); 55 | } else { 56 | this.fullScreen(); 57 | } 58 | }; 59 | 60 | fullScreen = () => { 61 | if (document.getElementsByTagName("body")[0].requestFullscreen) { 62 | // 如果浏览器不支持屏幕全屏 63 | this.setState({ realFullScreen: true }); 64 | document.getElementsByTagName("body")[0].requestFullscreen(); 65 | window.addEventListener("resize", this.reflashFullScreen, false); 66 | } 67 | window.editor.navigator.fullScreen = true; 68 | }; 69 | 70 | reflashFullScreen = () => { 71 | this.setState({ realFullScreen: !!document.fullscreenElement }); 72 | if (!document.fullscreenElement) { 73 | window.editor.navigator.fullScreen = false; 74 | window.removeEventListener("resize", this.reflashFullScreen); 75 | } 76 | }; 77 | 78 | closeFullScreen = () => { 79 | if (document.exitFullscreen) { 80 | document.exitFullscreen(); 81 | this.setState({ realFullScreen: false }); 82 | } 83 | window.editor.navigator.fullScreen = false; 84 | }; 85 | 86 | render() { 87 | const { handActive, realFullScreen } = this.state; 88 | const { zoom, triggerActive, fullScreen } = this.props; 89 | return ( 90 |
    91 | 96 |
    97 | 98 |
    99 |
    100 |
    105 |
    112 |
    113 |
    114 | 115 |
    116 |
    120 | 121 |
    122 |
    123 | 124 |
    125 |
    129 | 130 |
    131 | 136 |
    137 | ); 138 | } 139 | } 140 | 141 | export default App; 142 | -------------------------------------------------------------------------------- /src/pages/NavigatorRender/style.less: -------------------------------------------------------------------------------- 1 | .navigator-container { 2 | position: absolute; 3 | width: 35px; 4 | height: 243px; 5 | padding: 5px 0; 6 | left: 0px; 7 | bottom: 0px; 8 | background: #fc8383; 9 | color: #fff; 10 | border-radius: 4px; 11 | z-index: 10; 12 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2); 13 | 14 | .nav-icon { 15 | font-size: 25px; 16 | width: 35px; 17 | } 18 | 19 | .nav-btn { 20 | width: 35px; 21 | height: 24px; 22 | line-height: 24px; 23 | text-align: center; 24 | 25 | &.active { 26 | background-color: #5a6378; 27 | } 28 | } 29 | 30 | .icon { 31 | background: url(../../assets/images/icons.png) no-repeat; 32 | display: block; 33 | } 34 | 35 | .zoom-in { 36 | background-position: 0 -730px; 37 | width: 20px; 38 | height: 20px; 39 | margin: 2px auto; 40 | } 41 | 42 | .zoom-pan { 43 | width: 2px; 44 | //height: 70px; 45 | box-shadow: 0 1px #e50000; 46 | position: relative; 47 | background: white; 48 | margin: 3px auto; 49 | overflow: visible; 50 | 51 | .origin { 52 | position: absolute; 53 | width: 20px; 54 | height: 8px; 55 | left: -9px; 56 | margin-top: -4px; 57 | background: transparent; 58 | 59 | &:after { 60 | content: " "; 61 | display: block; 62 | width: 6px; 63 | height: 2px; 64 | background: white; 65 | left: 7px; 66 | top: 3px; 67 | position: absolute; 68 | } 69 | } 70 | 71 | .indicator { 72 | position: absolute; 73 | width: 8px; 74 | height: 8px; 75 | left: -3px; 76 | background: white; 77 | border-radius: 100%; 78 | margin-top: -4px; 79 | } 80 | } 81 | 82 | .zoom-out { 83 | background-position: 0 -750px; 84 | width: 20px; 85 | height: 20px; 86 | margin: 2px auto; 87 | } 88 | 89 | .hand { 90 | background-position: 0 -770px; 91 | width: 25px; 92 | height: 25px; 93 | margin: 0 auto; 94 | } 95 | 96 | .camera { 97 | background-position: 0 -870px; 98 | width: 25px; 99 | height: 25px; 100 | margin: 0 auto; 101 | } 102 | 103 | .trigger { 104 | background-position: 0 -845px; 105 | width: 25px; 106 | height: 25px; 107 | margin: 0 auto; 108 | } 109 | } 110 | 111 | .nav-previewer { 112 | background: #fff; 113 | width: 140px; 114 | height: 120px; 115 | position: absolute; 116 | left: 40px; 117 | bottom: 20px; 118 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 119 | border-radius: 0 2px 2px 0; 120 | padding: 1px; 121 | z-index: 9; 122 | cursor: crosshair; 123 | transition: -webkit-transform 0.7s 0.1s ease; 124 | transition: transform 0.7s 0.1s ease; 125 | 126 | &.grab { 127 | cursor: move; 128 | cursor: -webkit-grabbing; 129 | cursor: -moz-grabbing; 130 | cursor: grabbing; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/pages/NoteRender/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | 4 | class App extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 |
    16 |

    17 |
    18 | ); 19 | } 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/pages/NoteRender/style.less: -------------------------------------------------------------------------------- 1 | .note-preview-container { 2 | z-index: 99; 3 | position: absolute; 4 | background: #ffd; 5 | padding: 5px 15px; 6 | max-width: 400px; 7 | max-height: 200px; 8 | overflow: auto; 9 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); 10 | color: #333; 11 | p { 12 | white-space: pre-wrap; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/SearchRenderV2/ContentSearch/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Input, Tree, Icon } from "antd"; 3 | import * as editorCommand from "../../../command/EditorCommand"; 4 | const { TreeNode } = Tree; 5 | import { isString, cloneDeep, isUndefined } from "lodash"; 6 | 7 | class App extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | data: [], 12 | autoExpandParent: true, 13 | searchValue: "", 14 | }; 15 | } 16 | 17 | componentDidMount = () => { 18 | window.minder.on("contentchange", this.contentReSearch); 19 | // let json = editorCommand.exportJson(); 20 | // this.setState({ 21 | // data: [json.root], 22 | // }) 23 | 24 | if (this.props.visible) { 25 | if (this.props.type === "compare") { 26 | this.onChange({ target: { value: "待审" } }); 27 | } else { 28 | let json = editorCommand.exportJson(); 29 | this.setState({ 30 | data: [json.root], 31 | }); 32 | } 33 | } 34 | }; 35 | 36 | componentWillReceiveProps = (nextProps) => { 37 | if (nextProps.visible !== this.props.visible) { 38 | if (nextProps.visible) { 39 | if (nextProps.type === "compare") { 40 | this.onChange({ target: { value: "待审" } }); 41 | } else { 42 | let json = editorCommand.exportJson(); 43 | this.setState({ 44 | data: [json.root], 45 | }); 46 | } 47 | } else { 48 | this.setState({ 49 | searchValue: "", 50 | }); 51 | } 52 | } 53 | }; 54 | 55 | contentReSearch = () => { 56 | // 只有search可见的时候才处理这个逻辑 57 | if (this.props.visible) { 58 | this.onChange({ target: { value: this.state.searchValue } }); 59 | } 60 | }; 61 | 62 | onSelect = (selectedKeys, event) => { 63 | let key = event.node.props.eventKey.split("_")[0]; 64 | editorCommand.focusNodeById(key); 65 | }; 66 | 67 | /** 68 | * 文本框发生变化时 69 | */ 70 | onChange = (e) => { 71 | const { value } = e.target; 72 | 73 | let data = []; 74 | if (value !== "") { 75 | window.minder.getRoot().traverse((node) => { 76 | if ( 77 | !isUndefined(node.data.text) && 78 | node.data.text.indexOf(value) !== -1 79 | ) { 80 | data.push(node); 81 | } 82 | 83 | // 也需要去过滤resource这块的内容 84 | let nodeResource = node.getData("resource"); 85 | // let isFind = false; 86 | nodeResource && 87 | nodeResource.map((resource) => { 88 | if (resource !== null) { 89 | if (isString(resource)) { 90 | if (resource.toLowerCase().indexOf(value) != -1) { 91 | let nodeTemp = cloneDeep(node); 92 | nodeTemp.data.id = nodeTemp.data.id + "_tag"; 93 | nodeTemp.data.text = resource; 94 | data.push(nodeTemp); 95 | } 96 | } else { 97 | if (resource.name.toLowerCase().indexOf(value) != -1) { 98 | let nodeTemp = cloneDeep(node); 99 | nodeTemp.data.id = nodeTemp.data.id + "_tag"; 100 | nodeTemp.data.text = resource.name; 101 | data.push(nodeTemp); 102 | } 103 | } 104 | } 105 | }); 106 | }); 107 | } else { 108 | data = [editorCommand.exportJson().root]; 109 | } 110 | 111 | this.setState({ 112 | searchValue: value, 113 | data, 114 | }); 115 | }; 116 | 117 | render() { 118 | const { autoExpandParent, searchValue } = this.state; 119 | 120 | const loop = (data) => 121 | data.map((item) => { 122 | const index = isUndefined(item.data.text) 123 | ? -1 124 | : item.data.text.indexOf(searchValue); 125 | const beforeStr = isUndefined(item.data.text) 126 | ? "" 127 | : item.data.text.substr(0, index); 128 | const afterStr = isUndefined(item.data.text) 129 | ? "" 130 | : item.data.text.substr(index + searchValue.length); 131 | const text = 132 | index > -1 ? ( 133 | 134 | {beforeStr} 135 | {searchValue} 136 | {afterStr} 137 | 138 | ) : ( 139 | 140 | {isUndefined(item.data.text) 141 | ? "[图片]" 142 | : item.data.text === "" 143 | ? "[无文本]" 144 | : item.data.text} 145 | 146 | ); 147 | 148 | if (searchValue === "" && item.children) { 149 | return ( 150 | 151 | {loop(item.children)} 152 | 153 | ); 154 | } 155 | return ( 156 | : null 159 | } 160 | key={item.data.id} 161 | title={text} 162 | /> 163 | ); 164 | }); 165 | 166 | return ( 167 |
    168 |
    169 | 174 |
    175 | 176 | {this.state.data.length > 0 && ( 177 |
    178 | 185 | {loop(this.state.data)} 186 | 187 |
    188 | )} 189 |
    190 | ); 191 | } 192 | } 193 | 194 | export default App; 195 | -------------------------------------------------------------------------------- /src/pages/SearchRenderV2/RecordStatusSearch/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Select, Tree } from "antd"; 3 | const { Option } = Select; 4 | const { TreeNode } = Tree; 5 | import { isUndefined } from "lodash"; 6 | 7 | import * as editorCommand from "../../../command/EditorCommand"; 8 | 9 | class App extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | data: [], 14 | autoExpandParent: true, 15 | recordStatus: "", 16 | }; 17 | } 18 | 19 | componentDidMount = () => { 20 | this.props.onRecordSearchRef(this); 21 | }; 22 | 23 | componentWillReceiveProps = (nextProps) => { 24 | if (nextProps.visible !== this.props.visible) { 25 | if (!nextProps.visible) { 26 | this.setState({ 27 | data: [], 28 | recordStatus: "", 29 | }); 30 | } 31 | } 32 | }; 33 | 34 | /** 35 | * 添加叶子节点的数据 36 | */ 37 | addLeaf = (node, nodeList) => { 38 | if (node.children.length > 0) { 39 | for (const item of node.children) { 40 | this.addLeaf(item, nodeList); 41 | } 42 | } else { 43 | nodeList.push(node); 44 | } 45 | }; 46 | 47 | /** 48 | * 过滤出带有备注的节点 49 | * @param {*} rootData 50 | * @param {*} nodeList 51 | */ 52 | filterNote = (rootData, nodeList) => { 53 | let currNode = rootData.data; 54 | let childNodes = rootData.children; 55 | 56 | if (!isUndefined(currNode.note) && currNode.note !== "") { 57 | nodeList.push(rootData); 58 | } 59 | 60 | if (childNodes.length !== 0) { 61 | for (const childNode of childNodes) { 62 | this.filterNote(childNode, nodeList); 63 | } 64 | } 65 | }; 66 | 67 | /** 68 | * 用例遍历 69 | */ 70 | caseDFS = (rootData, nodeList, type) => { 71 | let currNode = rootData.data; 72 | let childNodes = rootData.children; 73 | 74 | if (childNodes.length !== 0) { 75 | if (isUndefined(currNode.progress)) { 76 | for (const childNode of childNodes) { 77 | this.caseDFS(childNode, nodeList, type); 78 | } 79 | } else { 80 | // progress是有被赋值的,那就要判断值是否是我们需要过滤的结果 81 | if (type !== -1 && currNode.progress === type) { 82 | this.addLeaf(rootData, nodeList); 83 | } 84 | } 85 | } else { 86 | if (type === -1) { 87 | if (isUndefined(currNode.progress)) { 88 | nodeList.push(rootData); 89 | } 90 | } else { 91 | if (currNode.progress === type) { 92 | nodeList.push(rootData); 93 | } 94 | } 95 | } 96 | }; 97 | 98 | handleChange = (value) => { 99 | let data = []; 100 | if (value === "note") { 101 | // 处理执行结果过滤的 102 | this.filterNote(editorCommand.exportJson().root, data); 103 | } else { 104 | // 处理执行结果过滤的 105 | this.caseDFS(editorCommand.exportJson().root, data, value); 106 | } 107 | 108 | this.setState({ 109 | data, 110 | recordStatus: value, 111 | }); 112 | }; 113 | 114 | onSelect = (selectedKeys, event) => { 115 | editorCommand.focusNodeById(event.node.props.eventKey); 116 | }; 117 | 118 | render() { 119 | const { autoExpandParent, recordStatus } = this.state; 120 | 121 | const loop = (data) => 122 | data.map((item) => { 123 | const text = ( 124 | 125 | {isUndefined(item.data.text) 126 | ? "[图片]" 127 | : item.data.text === "" 128 | ? "[无文本]" 129 | : item.data.text} 130 | 131 | ); 132 | return ; 133 | }); 134 | 135 | return ( 136 |
    137 | 150 | {this.state.data.length > 0 && ( 151 | 157 | {loop(this.state.data)} 158 | 159 | )} 160 |
    161 | ); 162 | } 163 | } 164 | 165 | export default App; 166 | -------------------------------------------------------------------------------- /src/pages/SearchRenderV2/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Drawer, Tabs, Icon, Select } from "antd"; 3 | const { Option } = Select; 4 | const { TabPane } = Tabs; 5 | import ContentSearch from "./ContentSearch"; 6 | import RecordSearch from "./RecordStatusSearch"; 7 | import "./style.less"; 8 | class App extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | activeKey: "1", 13 | }; 14 | } 15 | 16 | componentWillReceiveProps = (nextProps) => { 17 | if (nextProps.visible !== this.props.visible && !nextProps.visible) { 18 | this.setState({ 19 | activeKey: "1", 20 | }); 21 | } 22 | }; 23 | 24 | onChange = (activeKey) => { 25 | this.setState({ 26 | activeKey, 27 | }); 28 | }; 29 | 30 | render() { 31 | const { visible, expand } = this.props; 32 | return ( 33 | 34 |
    41 |
    42 | 导航 43 | { 47 | this.props.handleState("searchDrawerVisible", false); 48 | }} 49 | /> 50 |
    51 | 52 | 53 | 56 | 57 | 节点查询 58 | 59 | } 60 | key="1" 61 | > 62 | 67 | 68 | {this.props.recordId !== "" && 69 | this.props.recordId !== "undefined" ? ( 70 | 73 | 74 | 状态 75 | 76 | } 77 | key="2" 78 | > 79 | 83 | 84 | ) : null} 85 | 86 |
    87 |
    88 | ); 89 | } 90 | } 91 | 92 | export default App; 93 | -------------------------------------------------------------------------------- /src/pages/SearchRenderV2/style.less: -------------------------------------------------------------------------------- 1 | .search-v2-container { 2 | position: absolute; 3 | background: #fff; 4 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); 5 | left: 2px; 6 | width: 350px; 7 | height: calc(~"99% - 25px"); 8 | overflow-y: auto; 9 | z-index: 101; 10 | .drawer-header { 11 | display: flex; 12 | justify-content: space-between; 13 | padding: 16px 24px; 14 | border-bottom: 1px solid #e8e8e8; 15 | 16 | .drawer-title { 17 | color: rgba(0, 0, 0, 0.85); 18 | font-weight: 500; 19 | font-size: 16px; 20 | line-height: 22px; 21 | } 22 | } 23 | 24 | .ant-tabs { 25 | height: calc(100% - 55px) !important; 26 | 27 | .ant-tabs-content { 28 | height: calc(100% - 60px) !important; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/ShotCut/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Modal } from "antd"; 3 | import {} from "lodash"; 4 | import "./style.less"; 5 | 6 | class App extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | visible: false, 11 | }; 12 | } 13 | 14 | handleCancel = () => { 15 | this.setState({ 16 | visible: false, 17 | }); 18 | }; 19 | 20 | render() { 21 | return ( 22 | 23 | { 25 | this.setState({ visible: true }); 26 | }} 27 | className="shotcut-icon" 28 | > 29 | {this.props.children} 30 | 31 | 39 |

    节点操作

    40 |
    41 | 42 | Enter 43 | 44 | 插入兄弟节点 45 |
    46 | 47 |
    48 | 49 | Tab 50 | 51 | 插入子节点 52 |
    53 | 54 |
    55 | 56 | Shift 57 |  +  58 | Tab 59 | 60 | 插入父节点 61 |
    62 | 63 |
    64 | 65 | Delete 66 | 67 | 删除节点 68 |
    69 | 70 |
    71 | 72 | Up 73 |  ,  74 | Down 75 |  ,  76 | Left 77 |  ,  78 | Right 79 | 80 | 节点导航 81 |
    82 | 83 |
    84 | 85 | Alt 86 |  +  87 | Up 88 |  ,  89 | Down 90 | 91 | 向上/下调整顺序 92 |
    93 | 94 |
    95 | 96 | / 97 | 98 | 展开/收起节点 99 |
    100 | 101 |
    102 | 103 | F2 104 | 105 | 编辑文本 106 |
    107 | 108 |
    109 | 110 | Alt 111 |  +  112 | Enter 113 | 114 | 文本换行 115 |
    116 | 117 |
    118 | 119 | Ctrl 120 |  +  121 | K 122 | 123 | 链接-网页 124 |
    125 | 126 |
    127 | 128 | Ctrl 129 |  +  130 | A 131 | 132 | 全选节点 133 |
    134 | 135 |
    136 | 137 | Shift 138 |  +  139 | 鼠标左键 140 | 141 | 选中兄弟节点 142 |
    143 | 144 |
    145 | 146 | Ctrl 147 |  +  148 | C 149 | 150 | 复制节点 151 |
    152 | 153 |
    154 | 155 | Ctrl 156 |  +  157 | X 158 | 159 | 剪切节点 160 |
    161 | 162 |
    163 | 164 | Ctrl 165 |  +  166 | V 167 | 168 | 粘贴节点 169 |
    170 | 171 |
    172 | 173 | Ctrl 174 |  +  175 | F 176 | 177 | 查找节点 178 |
    179 | 180 |
    181 | 182 | Ctrl 183 |  +  184 | 1 185 |  ,  186 | 2 187 |  ,  188 | 3 189 | 190 | 添加前置条件,执行步骤, 预期结果 191 |
    192 | 193 |
    194 | 195 | Ctrl 196 |  +  197 | 0 198 |  ,  199 | 1 200 |  ,  201 | 2 202 | 203 | 204 | (执行任务)添加执行结果清除,✅,❎ 205 | 206 |
    207 | 208 |

    视野控制

    209 |
    210 | 211 | Alt 212 |  +  213 | 拖动 214 |  ,  215 | 右键拖动 216 | 217 | 拖动视野 218 |
    219 | 220 |
    221 | 222 | 滚轮 223 |  ,  224 | 触摸板 225 | 226 | 移动视野 227 |
    228 | 229 |
    230 | 231 | 双击空白处 232 | 233 | 居中根节点 234 |
    235 | 236 |
    237 | 238 | Ctrl 239 |  +  240 | + 241 |  ,  242 | - 243 | 244 | 放到/缩小视野 245 |
    246 | 247 |

    布局

    248 |
    249 | 250 | Ctrl 251 |  +  252 | Shift 253 |  +  254 | L 255 | 256 | 整理布局 257 |
    258 | 259 |

    后悔药

    260 |
    261 | 262 | Ctrl 263 |  +  264 | Z 265 | 266 | 撤销 267 |
    268 | 269 |
    270 | 271 | Ctrl 272 |  +  273 | Y 274 | 275 | 重做 276 |
    277 |
    278 |
    279 | ); 280 | } 281 | } 282 | 283 | export default App; 284 | -------------------------------------------------------------------------------- /src/pages/ShotCut/style.less: -------------------------------------------------------------------------------- 1 | .shotcut-icon { 2 | margin-right: 10px; 3 | cursor: pointer; 4 | } 5 | 6 | .shotcutModal { 7 | h3 { 8 | border-bottom: 1px solid #eee; 9 | font-size: 16px; 10 | font-weight: 700; 11 | padding-bottom: 5px; 12 | margin-top: 0; 13 | } 14 | 15 | .shortcut-group { 16 | margin: 5px auto; 17 | 18 | .shotcut { 19 | display: inline-block; 20 | width: 220px; 21 | text-align: right; 22 | margin-right: 10px; 23 | 24 | .shotcut-key { 25 | display: inline-block; 26 | padding: 3px 8px 5px; 27 | font-size: 12px; 28 | font-weight: 400; 29 | line-height: 14px; 30 | color: #6e6e6e; 31 | white-space: nowrap; 32 | vertical-align: middle; 33 | background-color: #fcfcfc; 34 | border-radius: 3px; 35 | border: 1px solid #ccc; 36 | text-transform: capitalize; 37 | box-shadow: inset 0 -2px #ebebeb, inset 0 -3px #fff, 38 | 0 1px 2px rgb(255 255 255 / 30%); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/ToolBox/Note/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Input } from "antd"; 3 | import * as editorCommand from "./../../../command/EditorCommand"; 4 | import "./style.less"; 5 | 6 | class App extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | hasEdit: false, 11 | }; 12 | } 13 | 14 | componentWillReceiveProps(nextProps) { 15 | const { nodeInfo } = this.props; 16 | if (nodeInfo.id != nextProps.nodeInfo.id) 17 | this.handleNote(nodeInfo.note, nodeInfo.id); 18 | } 19 | 20 | updateNote = (value) => { 21 | window.editor.runtime.updateNote(value); 22 | this.setState({ hasEdit: true }); 23 | }; 24 | 25 | handleNote = (note, id) => { 26 | if (!this.state.hasEdit) return; 27 | editorCommand.handleNote(note, id); 28 | this.setState({ hasEdit: false }); 29 | }; 30 | 31 | render() { 32 | const { nodeInfo } = this.props; 33 | let editable = true; 34 | return ( 35 |
    36 | {editable && ( 37 | { 43 | this.handleNote(e.target.value, nodeInfo.id); 44 | }} 45 | onChange={(e) => this.updateNote(e.target.value)} 46 | /> 47 | )} 48 | {editable ||

    {nodeInfo.note}

    } 49 |
    50 | ); 51 | } 52 | } 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /src/pages/ToolBox/Note/style.less: -------------------------------------------------------------------------------- 1 | .note-container { 2 | overflow-y: auto; 3 | height: calc(~"100% - 37px"); 4 | p { 5 | padding: 5px; 6 | white-space: pre-wrap; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/ToolBox/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tabs, Icon, Tooltip } from "antd"; 3 | import "./style.less"; 4 | import Note from "./Note"; 5 | import { partial } from "lodash"; 6 | class App extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | lock: false, 11 | }; 12 | } 13 | 14 | componentWillReceiveProps = (nextProps) => { 15 | // 支持锁定状态 16 | if (this.state.lock) return; 17 | // 更新满足任一:1、显示状态切换 2、可见时,切换节点 18 | if ( 19 | nextProps.toolbox != this.props.toolbox || 20 | (nextProps.toolbox && nextProps.nodeInfo.id != this.props.nodeInfo.id) 21 | ) { 22 | this.initData(nextProps); 23 | } 24 | }; 25 | 26 | /** 27 | * 选中tab 28 | * @param {*} toolboxTab 29 | */ 30 | setTab = (toolboxTab) => { 31 | this.props.handleState("toolboxTab", toolboxTab); 32 | }; 33 | 34 | setToolbox = (visible, tab) => { 35 | window.editor.toolbox.setToolbox(visible, tab); 36 | }; 37 | 38 | setLock = (value) => { 39 | this.setState({ lock: value }, () => { 40 | if (!value) this.initData(this.props); 41 | }); 42 | }; 43 | 44 | initData = (nextProps) => { 45 | const { nodeInfo } = nextProps; 46 | this.setState({ nodeId: nodeInfo.id }); // 防止锁定状态下使用的节点数据错误 47 | }; 48 | 49 | render() { 50 | const { toolboxTab, toolbox, readOnly, type } = this.props; 51 | const { lock } = this.state; 52 | 53 | return ( 54 |
    55 |
    64 | 65 |
    66 |
    70 |
    71 | 72 | 77 | 78 | 89 |
    90 | this.setTab(key)} 96 | > 97 | {type === "compare" ? null : } 98 | 99 | {toolboxTab == "note" && } 100 |
    101 |
    102 | ); 103 | } 104 | } 105 | 106 | export default App; 107 | -------------------------------------------------------------------------------- /src/pages/ToolBox/style.less: -------------------------------------------------------------------------------- 1 | .icon-container { 2 | position: absolute; 3 | background: #fff; 4 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); 5 | top: 130px; 6 | right: 2px; 7 | float: right; 8 | z-index: 99; 9 | border-radius: 2px; 10 | width: 36px; 11 | height: 34px; 12 | line-height: 32px; 13 | text-align: center; 14 | cursor: pointer; 15 | .m-icon { 16 | } 17 | } 18 | .toolbox-container { 19 | position: absolute; 20 | background: #fff; 21 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); 22 | top: 140px; 23 | right: 2px; 24 | float: right; 25 | width: 340px; 26 | height: calc(~"99% - 100px"); 27 | overflow-y: auto; 28 | z-index: 99; 29 | .toolbox-icon { 30 | position: absolute; 31 | line-height: 37px; 32 | z-index: 999; 33 | right: 2px; 34 | .m-close { 35 | margin-left: 5px; 36 | } 37 | } 38 | .m-h4 { 39 | margin-top: 3px; 40 | border-left: 3px solid #2395f1; 41 | padding-left: 8px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | const login = (user) => { 2 | return { 3 | type: "LOGIN", 4 | user, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | //引入Reducer 3 | import Reducer from "./reducer/index"; 4 | //引入中间件 5 | import thunkMiddleware from "redux-thunk"; 6 | 7 | const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore); 8 | const configureStore = (initialState) => { 9 | const store = createStoreWithMiddleware(Reducer, initialState); 10 | return store; 11 | }; 12 | export default configureStore(); 13 | -------------------------------------------------------------------------------- /src/store/reducer/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | const initialState = { 4 | // 节点信息 5 | nodeInfo: { 6 | id: "", 7 | text: "", 8 | note: "", 9 | hyperlink: {}, 10 | image: {}, 11 | timeStamp: String(new Date().getTime()), 12 | }, 13 | // 是否有选中节点 14 | isNode: false, 15 | // 撤销 16 | hasRedo: false, 17 | // 重做 18 | hasUndo: false, 19 | // 工具箱的默认tab key 20 | toolboxTab: "review", 21 | // toolbox 是否可见 22 | toolbox: false, 23 | // 主题 24 | theme: "fresh-blue-compat", 25 | // 模板 26 | template: "", 27 | 28 | showTip: false, //是否显示结果文字 29 | curIndex: 0, // 当前处于第一条 30 | resultNum: 0, // 搜索结果共几条 31 | }; 32 | 33 | const kityMinder = (state = initialState, action) => { 34 | switch (action.type) { 35 | case "UPDATE_NODE": 36 | return { 37 | ...state, 38 | nodeInfo: action.nodeInfo, 39 | }; 40 | case "NODE_ROOT": 41 | return { 42 | ...state, 43 | isNode: action.isNode, 44 | }; 45 | case "UNDO": 46 | return { 47 | ...state, 48 | hasUndo: action.hasUndo, 49 | }; 50 | case "REDO": 51 | return { 52 | ...state, 53 | hasRedo: action.hasRedo, 54 | }; 55 | 56 | case "TOOLBOX-TAB": 57 | return { 58 | ...state, 59 | toolboxTab: action.toolboxTab, 60 | }; 61 | case "TOOLBOX": { 62 | return { 63 | ...state, 64 | toolboxTab: action.toolboxTab, 65 | toolbox: action.toolbox, 66 | }; 67 | } 68 | case "THEME": { 69 | return { 70 | ...state, 71 | theme: action.theme, 72 | }; 73 | } 74 | case "TEMPLATE": 75 | return { 76 | ...state, 77 | template: action.template, 78 | }; 79 | 80 | case "SET_DATA": 81 | return { 82 | ...state, 83 | ...action.data, 84 | }; 85 | default: 86 | return state; 87 | } 88 | }; 89 | 90 | export default combineReducers({ 91 | kityMinder, 92 | }); 93 | -------------------------------------------------------------------------------- /src/websocket/Websocket.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class Websocket extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | ws: window.WebSocket 9 | ? new window.WebSocket(this.props.url, this.props.protocol) 10 | : new window.MozWebSocket(this.props.url, this.props.protocol), 11 | attempts: 1, 12 | }; 13 | this.sendMessage = this.sendMessage.bind(this); 14 | this.setupWebsocket = this.setupWebsocket.bind(this); 15 | } 16 | 17 | logging(logline) { 18 | if (this.props.debug === true) { 19 | console.log(logline); 20 | } 21 | } 22 | 23 | generateInterval(k) { 24 | if (this.props.reconnectIntervalInMilliSeconds > 0) { 25 | return this.props.reconnectIntervalInMilliSeconds; 26 | } 27 | return Math.min(30, Math.pow(2, k) - 1) * 1000; 28 | } 29 | 30 | setupWebsocket() { 31 | let websocket = this.state.ws; 32 | 33 | websocket.onopen = () => { 34 | this.logging("Websocket connected"); 35 | if (typeof this.props.onOpen === "function") this.props.onOpen(); 36 | }; 37 | 38 | websocket.onerror = (e) => { 39 | console.log(e, "error"); 40 | if (typeof this.props.onError === "function") this.props.onError(e); 41 | }; 42 | 43 | websocket.onmessage = (evt) => { 44 | this.props.onMessage(evt.data); 45 | }; 46 | 47 | this.shouldReconnect = this.props.reconnect; 48 | websocket.onclose = (evt) => { 49 | this.logging( 50 | `Websocket disconnected,the reason: ${evt.reason},the code: ${evt.code}` 51 | ); 52 | if (typeof this.props.onClose === "function") 53 | this.props.onClose(evt.code, evt.reason); 54 | if (this.shouldReconnect) { 55 | let time = this.generateInterval(this.state.attempts); 56 | this.timeoutID = setTimeout(() => { 57 | this.setState({ attempts: this.state.attempts + 1 }); 58 | this.setState({ 59 | ws: window.WebSocket 60 | ? new window.WebSocket(this.props.url, this.props.protocol) 61 | : new window.MozWebSocket(this.props.url, this.props.protocol), 62 | }); 63 | this.setupWebsocket(); 64 | }, time); 65 | } 66 | }; 67 | } 68 | 69 | componentDidMount() { 70 | this.setupWebsocket(); 71 | } 72 | 73 | componentWillUnmount() { 74 | this.shouldReconnect = false; 75 | clearTimeout(this.timeoutID); 76 | let websocket = this.state.ws; 77 | websocket.close(); 78 | } 79 | 80 | sendMessage(message) { 81 | let websocket = this.state.ws; 82 | websocket.send(message); 83 | } 84 | 85 | render() { 86 | return
    ; 87 | } 88 | } 89 | 90 | Websocket.defaultProps = { 91 | debug: false, 92 | reconnect: true, 93 | }; 94 | 95 | Websocket.propTypes = { 96 | url: PropTypes.string.isRequired, 97 | onMessage: PropTypes.func.isRequired, 98 | onOpen: PropTypes.func, 99 | onClose: PropTypes.func, 100 | onError: PropTypes.func, 101 | debug: PropTypes.bool, 102 | reconnect: PropTypes.bool, 103 | protocol: PropTypes.string, 104 | reconnectIntervalInMilliSeconds: PropTypes.number, 105 | }; 106 | 107 | export default Websocket; 108 | --------------------------------------------------------------------------------