├── .DS_Store ├── .gitignore ├── .vscode └── launch.json ├── client ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── hash.js │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── reset.css │ ├── robots.txt │ ├── service-worker.js │ ├── spark-md5.min.js │ └── static │ │ ├── css │ │ ├── 0.371969c2.chunk.css │ │ ├── 0.371969c2.chunk.css.gz │ │ ├── 1.b695c2c7.chunk.css │ │ ├── 1.b695c2c7.chunk.css.gz │ │ ├── 10.39541017.chunk.css │ │ ├── 10.39541017.chunk.css.gz │ │ ├── 11.ba4d032b.chunk.css │ │ ├── 11.ba4d032b.chunk.css.gz │ │ ├── 12.5d77b4aa.chunk.css │ │ ├── 12.5d77b4aa.chunk.css.gz │ │ ├── 13.4c2f88d6.chunk.css │ │ ├── 13.4c2f88d6.chunk.css.gz │ │ ├── 14.e93f3129.chunk.css │ │ ├── 14.e93f3129.chunk.css.gz │ │ ├── 15.038ab382.chunk.css │ │ ├── 16.a46e298c.chunk.css │ │ ├── 17.a1fdeba1.chunk.css │ │ ├── 2.19b6ca2f.chunk.css │ │ ├── 3.76bf3c25.chunk.css │ │ ├── 3.76bf3c25.chunk.css.gz │ │ ├── 7.efba28c3.chunk.css │ │ ├── 7.efba28c3.chunk.css.gz │ │ ├── 8.c5223997.chunk.css │ │ ├── 8.c5223997.chunk.css.gz │ │ ├── 9.34994217.chunk.css │ │ └── 9.34994217.chunk.css.gz │ │ └── media │ │ ├── 404.9161e238.png │ │ └── bg.96c8b37a.png ├── config-overrides.js ├── jsconfig.json ├── package.json ├── public │ ├── favicon.ico │ ├── hash.js │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── reset.css │ ├── robots.txt │ └── spark-md5.min.js ├── src │ ├── App.js │ ├── api │ │ └── index.js │ ├── assets │ │ └── gray.css │ ├── components │ │ ├── 404 │ │ │ ├── images │ │ │ │ └── 404.png │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── Attachment │ │ │ ├── Attachment.js │ │ │ ├── AttachmentAttr.js │ │ │ └── index.js │ │ ├── Auth │ │ │ └── index.js │ │ ├── AuthRouter │ │ │ └── index.js │ │ ├── BaseFields │ │ │ └── index.js │ │ ├── BigTable │ │ │ ├── index.js │ │ │ └── style.css │ │ ├── CascadeDrop │ │ │ ├── CascadeDrop.js │ │ │ ├── CascadeDropAttr.js │ │ │ └── index.js │ │ ├── CellBase │ │ │ └── index.js │ │ ├── Date │ │ │ ├── Date.js │ │ │ ├── DateAttr.js │ │ │ └── index.js │ │ ├── DateSection │ │ │ ├── DateSection.js │ │ │ ├── DateSectionAttr.js │ │ │ └── index.js │ │ ├── DragAndDrop │ │ │ ├── DropContainer.js │ │ │ ├── SimpleFields.js │ │ │ ├── WrapperDrag.js │ │ │ └── WrapperDrop.js │ │ ├── DropDown │ │ │ ├── DropDownField.js │ │ │ ├── DropDownFieldAttr.js │ │ │ └── index.js │ │ ├── Dustbin.js │ │ ├── FormHidden │ │ │ ├── FormHidden.js │ │ │ ├── FormHiddenAttr.js │ │ │ └── index.js │ │ ├── FormLayout │ │ │ ├── FormLayout.js │ │ │ ├── FormLayoutAttr.js │ │ │ └── index.js │ │ ├── GridCol │ │ │ └── index.js │ │ ├── LayoutFields │ │ │ └── index.js │ │ ├── MultipleBox │ │ │ ├── MultipleBox.js │ │ │ ├── MultipleBoxAttr.js │ │ │ └── index.js │ │ ├── RadioBox │ │ │ ├── RadioBox.js │ │ │ ├── RadioBoxAttr.js │ │ │ └── index.js │ │ ├── ResizeDemo │ │ │ ├── index.js │ │ │ ├── styles.css │ │ │ ├── styles.css.map │ │ │ └── styles.scss │ │ ├── TextAreaField │ │ │ ├── TextAreaField.js │ │ │ ├── TextAreaFieldAttr.js │ │ │ └── index.js │ │ ├── TextField │ │ │ ├── TextField.js │ │ │ ├── TextFieldAttr.js │ │ │ └── index.js │ │ ├── TextMoneyField │ │ │ ├── TextMoneyField.js │ │ │ ├── TextMoneyFieldAttr.js │ │ │ └── index.js │ │ ├── TextNumberField │ │ │ ├── TextNumberField.js │ │ │ ├── TextNumberFieldAttr.js │ │ │ └── index.js │ │ ├── box │ │ │ └── box.js │ │ ├── footer │ │ │ └── index.js │ │ ├── header │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ └── index.less │ │ ├── left-nav │ │ │ ├── action-type.js │ │ │ ├── action.js │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── reducer.js │ │ ├── link-a │ │ │ └── index.js │ │ ├── loading │ │ │ └── index.js │ │ ├── test.js │ │ └── test │ │ │ ├── TestField.js │ │ │ ├── TestFieldAttr.js │ │ │ └── index.js │ ├── config │ │ ├── index.js │ │ ├── menuConfig.js │ │ └── type.js │ ├── directive │ │ └── dragdropdirective.js │ ├── images │ │ ├── angledown.png │ │ ├── attachment.png │ │ ├── cascadedrop.png │ │ ├── date.png │ │ ├── datesection.png │ │ ├── formhidden.png │ │ ├── formsection.png │ │ ├── money.png │ │ ├── multiplebox.png │ │ ├── number.png │ │ ├── radiobox.png │ │ ├── textareafield.png │ │ └── textfield.png │ ├── index.js │ ├── logo.svg │ ├── mock │ │ ├── map.json │ │ └── world.json │ ├── pages │ │ ├── VirtualDom │ │ │ ├── element.js │ │ │ └── index.js │ │ ├── admin │ │ │ └── admin.js │ │ ├── category │ │ │ ├── add-form.js │ │ │ ├── category.js │ │ │ ├── test.js │ │ │ └── update-form.js │ │ ├── charts │ │ │ ├── D3 │ │ │ │ └── graph.js │ │ │ ├── bar.js │ │ │ ├── line.js │ │ │ ├── map.js │ │ │ └── pie.js │ │ ├── drag │ │ │ ├── Native.js │ │ │ ├── Rxjs.js │ │ │ └── index.js │ │ ├── form-design │ │ │ ├── FormDesign │ │ │ │ ├── index.css │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ │ ├── FormDisplay │ │ │ │ ├── index.css │ │ │ │ ├── index.css.map │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ │ ├── containers │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── github │ │ │ └── index.js │ │ ├── home │ │ │ ├── bar.jsx │ │ │ ├── home.css │ │ │ ├── home.js │ │ │ ├── home.less │ │ │ └── line.jsx │ │ ├── hooks │ │ │ ├── calculate-hash.js │ │ │ ├── example.js │ │ │ ├── fileUpload.js │ │ │ └── hooks.js │ │ ├── login │ │ │ ├── action-type.js │ │ │ ├── action.js │ │ │ ├── images │ │ │ │ └── bg.png │ │ │ ├── login.css │ │ │ ├── login.js │ │ │ ├── login.less │ │ │ └── reducer.js │ │ ├── order │ │ │ ├── calendar.js │ │ │ └── index.js │ │ ├── product │ │ │ ├── add.js │ │ │ ├── detail.js │ │ │ ├── index.js │ │ │ ├── product.js │ │ │ ├── richTextEdit.js │ │ │ └── upload.js │ │ ├── role │ │ │ ├── auth.js │ │ │ ├── index.js │ │ │ └── role.js │ │ └── user │ │ │ └── user.js │ ├── redux │ │ ├── index.js │ │ └── reducer.js │ └── utils │ │ ├── DragDropService.js │ │ ├── ajax.js │ │ ├── axios.js │ │ ├── common.js │ │ ├── field-cor-attr.js │ │ ├── field-images.js │ │ ├── request.js │ │ ├── storeUtils.js │ │ └── utils-form-design.js ├── stylelint.config.js ├── yarn.lock └── 笔记.md ├── doc └── img │ ├── 1572075655846.jpg │ ├── 1572075835814.jpg │ ├── 1572077173164.jpg │ ├── 1572077222666.gif │ ├── 1572077222666.jpg │ ├── 1572508162034.gif │ ├── 1572508162034.jpg │ ├── 1572938533169.jpg │ ├── 1573007074794.gif │ ├── 1573007074794.jpg │ ├── permission.gif │ ├── permission.jpg │ ├── roleList.gif │ ├── roleList.jpg │ ├── test.gif │ ├── test.png │ ├── userList.gif │ └── userList.jpg ├── readme.md └── server ├── .DS_Store ├── cluster.js ├── config ├── config.default.js ├── rsa_private_key.pem └── rsa_public_key.pem ├── controller ├── CacheLRU.js └── bigupload.js ├── db └── connect.js ├── models ├── CategoryModel.js ├── ProductModel.js ├── RoleModel.js └── UserModel.js ├── package.json ├── routers ├── file-upload.js └── index.js ├── server.js └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | *.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # 秘钥 65 | *.pem -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "nodemon", 11 | "runtimeExecutable": "nodemon", 12 | "program": "${workspaceFolder}/server/server.js", 13 | "restart": true, 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "airbnb/hooks"] 3 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.conf 26 | .stylelintcache -------------------------------------------------------------------------------- /client/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Composure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/hash.js: -------------------------------------------------------------------------------- 1 | // /public/hash.js 2 | self.importScripts("/spark-md5.min.js"); // 导入脚本 3 | 4 | // 根据文件的内容生成文件 hash 就算修改用户名依旧可以判断是否是同一个文件,hash 值是唯一的 5 | self.onmessage = e => { 6 | const { fileChunkList } = e.data; 7 | const spark = new self.SparkMD5.ArrayBuffer(); 8 | let percentage = 0; 9 | let count = 0; 10 | const loadNext = index => { 11 | const reader = new FileReader(); 12 | reader.readAsArrayBuffer(fileChunkList[index].file); 13 | reader.onload = e => { 14 | count++; 15 | spark.append(e.target.result); 16 | if (count === fileChunkList.length) { 17 | self.postMessage({ 18 | percentage: 100, 19 | hash: spark.end() 20 | }); 21 | self.close(); 22 | } else { 23 | percentage += 100 / fileChunkList.length; 24 | self.postMessage({ 25 | percentage 26 | }); 27 | // 递归计算下一个切片 28 | loadNext(count); 29 | } 30 | }; 31 | }; 32 | loadNext(0); 33 | }; 34 | -------------------------------------------------------------------------------- /client/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/logo192.png -------------------------------------------------------------------------------- /client/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/logo512.png -------------------------------------------------------------------------------- /client/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/build/reset.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.5 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | font-size: 100%; 36 | font-weight: normal; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | button, 44 | input, 45 | select, 46 | textarea { 47 | margin: 0; 48 | } 49 | 50 | html { 51 | box-sizing: border-box; 52 | } 53 | 54 | 55 | *, *:before, *:after { 56 | box-sizing: inherit; 57 | } 58 | 59 | img, 60 | video { 61 | height: auto; 62 | max-width: 100%; 63 | } 64 | 65 | iframe { 66 | border: 0; 67 | } 68 | 69 | table { 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | } 73 | 74 | td, 75 | th { 76 | padding: 0; 77 | text-align: left; 78 | } 79 | #root{ 80 | width: 100%; 81 | height: 100%; 82 | } 83 | -------------------------------------------------------------------------------- /client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/build/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.698622eb27f13657b0800e4d8046d773.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /client/build/static/css/0.371969c2.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/0.371969c2.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/1.b695c2c7.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/1.b695c2c7.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/10.39541017.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/10.39541017.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/11.ba4d032b.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/11.ba4d032b.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/12.5d77b4aa.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/12.5d77b4aa.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/13.4c2f88d6.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/13.4c2f88d6.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/14.e93f3129.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/14.e93f3129.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/15.038ab382.chunk.css: -------------------------------------------------------------------------------- 1 | .not-found{background-color:#f0f2f5;height:100%}.not-found .left{height:100%;background:url(/static/media/404.9161e238.png) no-repeat 50%}.not-found .right{padding-left:50px;margin-top:150px}.not-found .right h1{font-size:35px}.not-found .right h2{margin-bottom:20px;font-size:20px} -------------------------------------------------------------------------------- /client/build/static/css/17.a1fdeba1.chunk.css: -------------------------------------------------------------------------------- 1 | .home{padding:24px;background:#fff;min-height:850px}.home .home-card{float:left}.home .home-content{position:absolute;top:420px;width:76%;border:1px solid #e8e8e8}.home .home-content .home-menu{font-size:20px}.home .home-content .home-menu span{cursor:pointer}.home .home-content .home-menu .home-menu-active{border-bottom:2px solid;padding:0 0 16px}.home .home-content .home-menu .home-menu-visited{margin-right:40px}.home .home-content .home-table-left{float:left;width:60%}.home .home-content .home-table-right{float:right;width:330px} -------------------------------------------------------------------------------- /client/build/static/css/3.76bf3c25.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/3.76bf3c25.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/7.efba28c3.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/7.efba28c3.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/8.c5223997.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/8.c5223997.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/css/9.34994217.chunk.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/css/9.34994217.chunk.css.gz -------------------------------------------------------------------------------- /client/build/static/media/404.9161e238.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/media/404.9161e238.png -------------------------------------------------------------------------------- /client/build/static/media/bg.96c8b37a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/build/static/media/bg.96c8b37a.png -------------------------------------------------------------------------------- /client/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, fixBabelImports,addLessLoader,addWebpackAlias} = require('customize-cra'); 2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 3 | const StyleLintPlugin = require('stylelint-webpack-plugin') 4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 5 | const path = require("path"); 6 | const CompressionPlugin = require('compression-webpack-plugin'); 7 | 8 | const addMyPlugin = config => { 9 | 10 | let plugins = [ 11 | new ProgressBarPlugin({ 12 | format: 'Build [:bar] :percent (:elapsed seconds)', 13 | clear: false, 14 | }), 15 | new CompressionPlugin({ 16 | filename: "[path].gz[query]", 17 | algorithm: "gzip", 18 | test: /\.js$|\.css$|\.html$/, 19 | threshold: 10240, 20 | minRatio: 0.8 21 | }), 22 | new StyleLintPlugin({ 23 | 'files': ['**/*.{html,vue,css,sass,scss}'], 24 | 'fix': false, 25 | 'cache': true, 26 | 'emitErrors': true, 27 | 'failOnError': false 28 | }), 29 | // new BundleAnalyzerPlugin(), 30 | ] 31 | 32 | config.plugins = [...config.plugins,...plugins] 33 | 34 | return config 35 | } 36 | 37 | // const addWebpackModules = () => config => { 38 | // const loaders = config.module.rules.find(rule => Array.isArray(rule.oneOf)).oneOf 39 | // loaders[loaders.length - 4] = Object.assign( 40 | // loaders[loaders.length - 4], 41 | // webpackConfig.module.rules[0] 42 | // ) 43 | // return config 44 | // } 45 | 46 | 47 | 48 | // 关闭map 49 | process.env.GENERATE_SOURCEMAP = "false"; 50 | module.exports = override( 51 | // 按需打包,根据import(使用babel-plugin-import) 52 | fixBabelImports('import', { 53 | libraryName: 'antd', 54 | libraryDirectory: 'es', 55 | style: true, //自动打包相关样式 56 | }), 57 | // 自定义样式 58 | addLessLoader({ 59 | javascriptEnabled: true, 60 | modifyVars: { 61 | '@primary-color': '#00A5E4' 62 | }, //主题颜色 63 | }), 64 | addWebpackAlias({ 65 | ["@"]: path.resolve(__dirname, "src"), 66 | ["components"]: path.resolve(__dirname, "src/components"), 67 | ["api"]: path.resolve(__dirname, "src/api"), 68 | ["config"]: path.resolve(__dirname, "src/config"), 69 | }), 70 | addMyPlugin, 71 | ); -------------------------------------------------------------------------------- /client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["client/src/*"] 6 | } 7 | }, 8 | "exclude": ["client/node_modules", "client/dist","server"] 9 | } 10 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@antv/data-set": "^0.10.2", 7 | "antd": "^3.23.6", 8 | "axios": "^0.19.0", 9 | "babel-loader": "^8.0.6", 10 | "babel-plugin-import": "^1.13.0", 11 | "bizcharts": "^3.5.6", 12 | "customize-cra": "^0.8.0", 13 | "d3": "v4.*", 14 | "draft-js-export-html": "^1.4.1", 15 | "draftjs-to-html": "^0.8.4", 16 | "echarts": "^4.4.0", 17 | "echarts-for-react": "^2.0.15-beta.1", 18 | "jsonp": "^0.2.1", 19 | "less": "^3.10.3", 20 | "less-loader": "^5.0.0", 21 | "md5": "^2.2.1", 22 | "moment": "^2.24.0", 23 | "node-sass": "^4.13.1", 24 | "react": "^16.10.2", 25 | "react-app-rewired": "^2.1.3", 26 | "react-dnd-html5-backend": "^10.0.2", 27 | "react-dom": "^16.10.2", 28 | "react-draft-wysiwyg": "^1.13.2", 29 | "react-loadable": "^5.5.0", 30 | "react-redux": "^7.1.3", 31 | "react-router-dom": "^5.1.2", 32 | "react-scripts": "3.2.0", 33 | "react-sortablejs": "^2.0.11", 34 | "react-virtualized-auto-sizer": "^1.0.2", 35 | "react-window": "^1.8.5", 36 | "redux": "^4.0.4", 37 | "redux-devtools": "^3.5.0", 38 | "redux-devtools-extension": "^2.13.8", 39 | "redux-thunk": "^2.3.0", 40 | "rxjs": "^6.5.5", 41 | "rxjs-hooks": "^0.6.2", 42 | "store": "^2.0.12" 43 | }, 44 | "scripts": { 45 | "start": "react-app-rewired start", 46 | "dev": "yarn start & yarn server", 47 | "server": "nodemon ../server/server.js ", 48 | "build": "react-app-rewired build", 49 | "test": "react-app-rewired test", 50 | "eject": "react-app-rewired eject", 51 | "lint": "eslint --ext .js,.css,.scss src --fix" 52 | }, 53 | "eslintConfig": { 54 | "extends": "react-app", 55 | "plugins": [ 56 | "react-hooks" 57 | ], 58 | "rules": { 59 | "react-hooks/rules-of-hooks": "error", 60 | "react-hooks/exhaustive-deps": "warn" 61 | } 62 | }, 63 | "browserslist": { 64 | "production": [ 65 | ">0.2%", 66 | "not dead", 67 | "not op_mini all" 68 | ], 69 | "development": [ 70 | "last 1 chrome version", 71 | "last 1 firefox version", 72 | "last 1 safari version" 73 | ] 74 | }, 75 | "proxy": "http://localhost:8081", 76 | "devDependencies": { 77 | "compression-webpack-plugin": "^3.0.0", 78 | "envify": "^4.1.0", 79 | "eslint": "6.8.0", 80 | "eslint-config-airbnb": "18.1.0", 81 | "eslint-plugin-import": "^2.20.1", 82 | "eslint-plugin-jsx-a11y": "^6.2.3", 83 | "eslint-plugin-react": "^7.19.0", 84 | "eslint-plugin-react-hooks": "2.5.0", 85 | "image-webpack-loader": "^6.0.0", 86 | "progress-bar-webpack-plugin": "^1.12.1", 87 | "rollup-plugin-commonjs": "^10.1.0", 88 | "rollup-plugin-replace": "^2.2.0", 89 | "rollup-plugin-terser": "^5.1.2", 90 | "stylelint": "^13.6.1", 91 | "stylelint-config-recommended-scss": "^4.2.0", 92 | "stylelint-config-standard": "^20.0.0", 93 | "stylelint-scss": "^3.18.0", 94 | "stylelint-webpack-plugin": "^2.1.0", 95 | "terser": "^4.4.2", 96 | "terser-brunch": "^3.0.0", 97 | "thread-loader": "^2.1.3", 98 | "uglifyify": "^5.0.2", 99 | "webpack-bundle-analyzer": "^3.6.0" 100 | }, 101 | "babel": { 102 | "presets": [ 103 | "module:metro-react-native-babel-preset" 104 | ], 105 | "env": { 106 | "production": {} 107 | }, 108 | "plugins": [ 109 | [ 110 | "@babel/plugin-proposal-decorators", 111 | { 112 | "legacy": true 113 | } 114 | ], 115 | [ 116 | "transform-inline-environment-variables", 117 | { 118 | "include": [ 119 | "NODE_ENV", 120 | "API" 121 | ] 122 | } 123 | ], 124 | [ 125 | "@babel/plugin-proposal-optional-catch-binding" 126 | ] 127 | ] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/hash.js: -------------------------------------------------------------------------------- 1 | // /public/hash.js 2 | self.importScripts("/spark-md5.min.js"); // 导入脚本 3 | 4 | // 根据文件的内容生成文件 hash 就算修改用户名依旧可以判断是否是同一个文件,hash 值是唯一的 5 | self.onmessage = e => { 6 | const { fileChunkList } = e.data; 7 | const spark = new self.SparkMD5.ArrayBuffer(); 8 | let percentage = 0; 9 | let count = 0; 10 | const loadNext = index => { 11 | const reader = new FileReader(); 12 | reader.readAsArrayBuffer(fileChunkList[index].file); 13 | reader.onload = e => { 14 | count++; 15 | spark.append(e.target.result); 16 | if (count === fileChunkList.length) { 17 | self.postMessage({ 18 | percentage: 100, 19 | hash: spark.end() 20 | }); 21 | self.close(); 22 | } else { 23 | percentage += 100 / fileChunkList.length; 24 | self.postMessage({ 25 | percentage 26 | }); 27 | // 递归计算下一个切片 28 | loadNext(count); 29 | } 30 | }; 31 | }; 32 | loadNext(0); 33 | }; 34 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 28 | 管理系统 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/reset.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.5 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | font-size: 100%; 36 | font-weight: normal; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | button, 44 | input, 45 | select, 46 | textarea { 47 | margin: 0; 48 | } 49 | 50 | html { 51 | box-sizing: border-box; 52 | } 53 | 54 | 55 | *, *:before, *:after { 56 | box-sizing: inherit; 57 | } 58 | 59 | img, 60 | video { 61 | height: auto; 62 | max-width: 100%; 63 | } 64 | 65 | iframe { 66 | border: 0; 67 | } 68 | 69 | table { 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | } 73 | 74 | td, 75 | th { 76 | padding: 0; 77 | text-align: left; 78 | } 79 | #root{ 80 | width: 100%; 81 | height: 100%; 82 | } 83 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 应用根组件 3 | */ 4 | import React, { Component } from "react"; 5 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 6 | import { Provider } from "react-redux"; 7 | import Login from "./pages/login/login"; 8 | import Admin from "./pages/admin/admin"; 9 | import store from "./utils/storeUtils"; 10 | import reduxStore from "./redux"; 11 | import Loading from "components/loading"; 12 | // 国际化配置 13 | import { ConfigProvider } from "antd"; 14 | import zh_CN from "antd/es/locale-provider/zh_CN"; 15 | import moment from "moment"; 16 | import "moment/locale/zh-cn"; 17 | moment.locale("zh-cn"); 18 | 19 | class App extends Component { 20 | state = { 21 | renderError: false 22 | }; 23 | // 和下面一样 24 | static getDerivedStateFromError() { 25 | return { 26 | renderError: true 27 | }; 28 | } 29 | // 捕获 Suspense 渲染出错 30 | // componentDidCatch(){ 31 | // this.setState({ 32 | // renderError:true 33 | // }) 34 | render() { 35 | const { renderError } = this.state; 36 | if (renderError) { 37 | return ; 38 | } 39 | const user = store.get("user_key"); 40 | store.user = user; //每次渲染先取出来放到内存中,各个组件取的时候不用store再去取 41 | return ( 42 | 43 | 44 | 45 | 46 | {/* switch只匹配一个path */} 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default App; 58 | -------------------------------------------------------------------------------- /client/src/assets/gray.css: -------------------------------------------------------------------------------- 1 | html { 2 | filter: grayscale(100%); 3 | -webkit-filter: grayscale(100%); 4 | -moz-filter: grayscale(100%); 5 | -ms-filter: grayscale(100%); 6 | -o-filter: grayscale(100%); 7 | filter: url("data:image/svg+xml;utf8,#grayscale"); 8 | filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); 9 | -webkit-filter: grayscale(1); 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/404/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/components/404/images/404.png -------------------------------------------------------------------------------- /client/src/components/404/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Button, Row, Col} from 'antd' 3 | import {connect} from 'react-redux' 4 | 5 | import './index.less' 6 | 7 | /* 8 | 前台404页面 9 | */ 10 | class NotFound extends Component { 11 | 12 | goHome = () => { 13 | this.props.history.replace('/home') 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 22 |

404

23 |

抱歉,你访问的页面不存在

24 |
25 | 28 |
29 | 30 |
31 | ) 32 | } 33 | } 34 | 35 | export default connect( 36 | null, 37 | null, 38 | )(NotFound) -------------------------------------------------------------------------------- /client/src/components/404/index.less: -------------------------------------------------------------------------------- 1 | .not-found{ 2 | background-color: #f0f2f5; 3 | height: 100%; 4 | .left { 5 | height: 100%; 6 | background: url('./images/404.png') no-repeat center; 7 | } 8 | .right { 9 | padding-left: 50px; 10 | margin-top: 150px; 11 | h1 { 12 | font-size: 35px; 13 | } 14 | h2 { 15 | margin-bottom: 20px; 16 | font-size: 20px; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /client/src/components/Attachment/Attachment.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class Attachment extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue = '附件', verifyValue = false }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status += ' active'; 19 | } 20 | if (isDragging) { 21 | status += ' draging'; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | 43 | 附件 44 | {verifyValue ? '(必选)' : ''} 45 | 46 | 47 |
48 |
49 |
50 | 51 | ); 52 | } 53 | } 54 | 55 | export default WrapperDrop(WrapperDrag(Attachment)); 56 | -------------------------------------------------------------------------------- /client/src/components/Attachment/AttachmentAttr.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { Checkbox } from "antd"; 3 | export default class AttachmentAttr extends PureComponent { 4 | handleChange = item => { 5 | const { activeItem, onSave } = this.props; 6 | const { index, attrInfo, cellIndex } = activeItem; 7 | const updateAttrInfo = { 8 | ...attrInfo, 9 | ...item 10 | }; 11 | const updateActiveItem = { ...activeItem, attrInfo: updateAttrInfo }; 12 | onSave(updateActiveItem, index, cellIndex); 13 | }; 14 | render() { 15 | const { 16 | activeItem: { 17 | attrInfo: { titleValue, name, verifyValue } 18 | } 19 | } = this.props; 20 | return ( 21 |
22 |
附件
23 |
24 |
25 |
26 |
27 | 标题最多20字 28 |
29 |
30 | { 34 | this.handleChange({ titleValue: e.target.value }); 35 | }} 36 | /> 37 |
38 |
39 |
40 |
41 | 字段名 42 | 20 ? "error-toolong" : "" 45 | }`} 46 | > 47 | 最多20字 48 | 49 |
50 |
51 | { 55 | this.handleChange({ name: e.target.value }); 56 | }} 57 | /> 58 |
59 |
60 |
61 |
验证
62 | 72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /client/src/components/Attachment/index.js: -------------------------------------------------------------------------------- 1 | import Attachment from './Attachment'; 2 | import AttachmentAttr from './AttachmentAttr'; 3 | 4 | Attachment.AttachmentAttr = AttachmentAttr; 5 | export default Attachment; 6 | -------------------------------------------------------------------------------- /client/src/components/Auth/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | class Auth extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | hasPermission: false, 10 | superAdmin: false, 11 | }; 12 | // 调试字段,为true时表示取消权限控制 13 | this.cancelPermission = false; 14 | } 15 | 16 | verification(authLists, permissionPath) { 17 | if (authLists.indexOf(permissionPath) !== -1) { 18 | this.setState({ 19 | hasPermission: true, 20 | }); 21 | } 22 | } 23 | 24 | componentWillMount() { 25 | const { permissionPath, userInfo } = this.props; 26 | if (userInfo.username === 'admin') { 27 | this.setState({ 28 | superAdmin: true, 29 | }); 30 | } 31 | this.verification(userInfo.role.menus, permissionPath); 32 | } 33 | 34 | UNSAFE_componentWillReceiveProps({ userInfo }) { 35 | this.verification( 36 | userInfo.role.menus, 37 | this.props.history.location.pathname, 38 | ); 39 | } 40 | 41 | render() { 42 | const { hasPermission, superAdmin } = this.state; 43 | const { noCheck = false, children } = this.props; 44 | return hasPermission || noCheck || superAdmin || this.cancelPermission ? ( 45 | children 46 | ) : ( 47 |
没有查看该模块的权限
48 | ); 49 | } 50 | } 51 | const mapStateToProps = (state) => ({ 52 | userInfo: state.loginUserInfo, 53 | }); 54 | export default connect(mapStateToProps, null)(withRouter(Auth)); 55 | -------------------------------------------------------------------------------- /client/src/components/AuthRouter/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import Auth from '../Auth'; 5 | 6 | export default class AuthRouter extends Component { 7 | constructor(props) { 8 | super(); 9 | this.state = { 10 | 11 | }; 12 | } 13 | 14 | render(h) { 15 | const { path, noCheck } = this.props; 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | AuthRouter.propTypes = { 24 | path: PropTypes.string, 25 | noCheck: PropTypes.bool, 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/components/BaseFields/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 3 | 4 | class BaseFields extends Component { 5 | render() { 6 | const { dataSet, dragStart, draggable } = this.props; 7 | return ( 8 | //
14 |
15 | 16 | 图片 17 |
18 | //
19 | ); 20 | } 21 | } 22 | export default WrapperDrag(BaseFields, 'base'); 23 | -------------------------------------------------------------------------------- /client/src/components/BigTable/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FixedSizeList as List } from 'react-window'; 3 | import AntoSize from 'react-virtualized-auto-sizer'; 4 | import './style.css'; 5 | 6 | const Row = ({ style, index }) => ( 7 |
8 | Row 9 | {' '} 10 | {index} 11 | {' '} 12 | {index & 1 ? 'even' : 'odd'} 13 |
14 | ); 15 | const Example = () => ( 16 | 17 | {({ width, height }) => ( 18 | 25 | {Row} 26 | 27 | )} 28 | 29 | ); 30 | export default class BigTable extends Component { 31 | render() { 32 | return ( 33 |
34 | 35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/BigTable/style.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .odd,.even{ 7 | text-align: center; 8 | background: #5CC9F5; 9 | 10 | } 11 | .even:hover{ 12 | box-shadow: inset 0 0 10px #5CC9F5; 13 | } 14 | .even{ 15 | background: #ffff; 16 | } -------------------------------------------------------------------------------- /client/src/components/CascadeDrop/CascadeDrop.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class CascadeDrop extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status = `${status} active`; 19 | } 20 | if (isDragging) { 21 | status = `${status} draging`; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | {tipValue + (verifyValue ? '(必填)' : '')} 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | export default WrapperDrop(WrapperDrag(CascadeDrop)); 51 | -------------------------------------------------------------------------------- /client/src/components/CascadeDrop/index.js: -------------------------------------------------------------------------------- 1 | import CascadeDrop from './CascadeDrop'; 2 | import CascadeDropAttr from './CascadeDropAttr'; 3 | 4 | CascadeDrop.CascadeDropAttr = CascadeDropAttr; 5 | export default CascadeDrop; 6 | -------------------------------------------------------------------------------- /client/src/components/CellBase/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import FieldCorAttr from "../../utils/field-cor-attr.js"; 3 | import emitter from "../../directive/dragdropdirective"; 4 | import Util from "../../utils/utils-form-design"; 5 | import { take } from "rxjs/operators"; 6 | import PropTypes from "prop-types"; 7 | const emitter$ = emitter; 8 | const data$ = emitter$.getDragData().pipe(take(1)); 9 | 10 | class CellBase extends PureComponent { 11 | state = { 12 | dropTags: ['base'], 13 | dragenter: false 14 | }; 15 | generateField = () => { 16 | const { currentCell } = this.props; 17 | console.log(currentCell) 18 | return ( 19 |
20 |
21 | 22 |
23 |
24 | ) 25 | // return FieldCorAttr[type].showField({ 26 | // dataSet: { ...data, active }, 27 | // activeField: this.activeFields, 28 | // removeField: this.removeFields 29 | // }); 30 | }; 31 | onDragEnter = event => { 32 | const { 33 | dataSet: { active}, 34 | cells, 35 | cellIndex 36 | } = this.props; 37 | console.log(cells,cellIndex) 38 | const { dropTags } = this.state; 39 | event.preventDefault(); 40 | event.stopPropagation(); 41 | data$.subscribe(dragData => { 42 | if (dropTags.indexOf(dragData.tag) > -1) { 43 | if (!active) { 44 | return; 45 | } 46 | if(!cells[cellIndex].item){ 47 | this.setState({dragenter: true }) 48 | } 49 | } 50 | }); 51 | }; 52 | onDragOver = event => { 53 | event.preventDefault(); 54 | event.stopPropagation(); 55 | }; 56 | onDragLeave = () => { 57 | const { 58 | dataSet: { active }, 59 | cells, 60 | cellIndex 61 | } = this.props; 62 | const { dropTags } = this.state; 63 | data$.subscribe(dragData => { 64 | if (dropTags.indexOf(dragData.tag) > -1) { 65 | if (!active) { 66 | return; 67 | } 68 | if(!cells[cellIndex].item){ 69 | this.setState({ dragenter: false }); 70 | } 71 | } 72 | }); 73 | }; 74 | onDrops = (event) => { 75 | const { 76 | dataSet: { active,gridIndex}, 77 | onDropFormLayout, 78 | cellIndex, 79 | cells, 80 | } = this.props; 81 | event.stopPropagation(); 82 | event.preventDefault(); 83 | const { dropTags } = this.state; 84 | data$.subscribe(dragData => { 85 | const tag = dragData.tag; 86 | if (dropTags.indexOf(tag) > -1) { 87 | if (!active) { 88 | return; 89 | } 90 | if (!cells[cellIndex].item) { 91 | this.setState({ dragenter: false }); 92 | const baseInfo = Util.deepClone({ 93 | ...FieldCorAttr[dragData.data.type].initValues 94 | }); 95 | const attrInfo = { 96 | titleValue: dragData.data.name, 97 | ...baseInfo, 98 | 99 | }; 100 | const item = { ...dragData.data, attrInfo,gridIndex,cellIndex }; 101 | onDropFormLayout(item); 102 | } 103 | } 104 | }); 105 | }; 106 | render() { 107 | const { dragenter } = this.state; 108 | return ( 109 |
this.onDrops(event)} 114 | onDragOver={this.onDragOver} 115 | style={{ height: "100%" ,display:'flex',flex:1,minHeight:'50px'}} 116 | > 117 | {this.generateField()} 118 |
119 | ); 120 | } 121 | } 122 | CellBase.propTypes = { 123 | dataSet: PropTypes.object 124 | }; 125 | export default CellBase; 126 | -------------------------------------------------------------------------------- /client/src/components/Date/Date.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class Date extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | let status = ''; 16 | if (active) { 17 | status += ' active'; 18 | } 19 | if (isDragging) { 20 | status += ' draging'; 21 | } 22 | return ( 23 |
24 |
{ 27 | event.stopPropagation(); 28 | removeField(dataSet, cellIndex); 29 | }} 30 | /> 31 |
{ 34 | event.stopPropagation(); 35 | activeField(dataSet, active, cellIndex); 36 | }} 37 | /> 38 |
39 |
40 | 41 | 42 | {tipValue + (verifyValue ? '(必填)' : '')} 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export default WrapperDrop(WrapperDrag(Date)); 52 | -------------------------------------------------------------------------------- /client/src/components/Date/index.js: -------------------------------------------------------------------------------- 1 | import DateFormat from './Date'; 2 | import DateAttr from './DateAttr'; 3 | 4 | DateFormat.DateAttr = DateAttr; 5 | 6 | export default DateFormat; 7 | -------------------------------------------------------------------------------- /client/src/components/DateSection/DateSection.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class DateSection extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | let status = ''; 16 | if (active) { 17 | status += ' active'; 18 | } 19 | if (isDragging) { 20 | status += ' draging'; 21 | } 22 | return ( 23 |
24 |
{ 27 | event.stopPropagation(); 28 | removeField(dataSet, cellIndex); 29 | }} 30 | /> 31 |
{ 34 | event.stopPropagation(); 35 | activeField(dataSet, active, cellIndex); 36 | }} 37 | /> 38 |
39 |
40 | 41 | 42 | {tipValue + (verifyValue ? '(必填)' : '')} 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export default WrapperDrop(WrapperDrag(DateSection)); 52 | -------------------------------------------------------------------------------- /client/src/components/DateSection/index.js: -------------------------------------------------------------------------------- 1 | import DateSection from './DateSection'; 2 | import DateSectionAttr from './DateSectionAttr'; 3 | 4 | DateSection.DateSectionAttr = DateSectionAttr; 5 | 6 | export default DateSection; 7 | -------------------------------------------------------------------------------- /client/src/components/DragAndDrop/DropContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from './WrapperDrop.js'; 3 | 4 | class DropContainer extends PureComponent { 5 | render() { 6 | const { children, currentDropIndex } = this.props; 7 | return ( 8 |
9 | {currentDropIndex === -1 ?
: null} 10 | {children} 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default WrapperDrop(DropContainer); 17 | -------------------------------------------------------------------------------- /client/src/components/DragAndDrop/SimpleFields.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import WrapperDrag from './WrapperDrag.js'; 3 | 4 | class SimpleFields extends Component { 5 | render() { 6 | const { isDragging, dataSet } = this.props; 7 | return ( 8 |
14 |
15 | 16 | 图片 17 |
18 |
19 | ); 20 | } 21 | } 22 | export default WrapperDrag(SimpleFields); 23 | -------------------------------------------------------------------------------- /client/src/components/DragAndDrop/WrapperDrag.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import PropTypes from "prop-types"; 3 | import emitter from "../../directive/dragdropdirective"; 4 | const emitter$ = emitter; 5 | // 拖动元素的高阶组件 6 | export default function WrapperDrag(Component, dragTag) { 7 | return class DragElement extends PureComponent { 8 | state = { 9 | draggable: true, 10 | dragStart: false 11 | }; 12 | // 开始拖动 13 | onDragStart = () => { 14 | const { dataSet} = this.props; 15 | // 设置拖动的数据 16 | emitter$.setDragData({ tag: dragTag, type: "new", data: dataSet }); 17 | this.setState({ dragStart: true }); 18 | }; 19 | // 结束拖动 20 | onDragEnd = () => { 21 | this.setState({ dragStart: false }); 22 | }; 23 | render() { 24 | const { dragStart } = this.state; 25 | const { className } = this.props 26 | // 三个原生拖动事件 drag dragstart dragend 27 | return ( 28 |
{ 33 | this.dragComp = comp; 34 | }} 35 | > 36 | 43 |
44 | ); 45 | } 46 | }; 47 | } 48 | WrapperDrag.propTypes = { 49 | dataSet: PropTypes.object 50 | }; 51 | -------------------------------------------------------------------------------- /client/src/components/DragAndDrop/WrapperDrop.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from "react"; 2 | import emitter from "../../directive/dragdropdirective"; 3 | import { take } from "rxjs/operators"; 4 | const emitter$ = emitter; 5 | const data$ = emitter$.getDragData().pipe(take(1));//发出接收源最初的一个值 6 | // 定义-释放区域的高阶组件 7 | export default function WrapperDrop(Component) { 8 | return class DropElement extends PureComponent { 9 | // 设置被释放区域-拖拽元素携带的信息 10 | onDragStart = event => { 11 | event.stopPropagation(); 12 | 13 | const {dataSet} = this.props; 14 | const tag = dataSet.type ==='grid'?dataSet.type:'base'; 15 | emitter$.setDragData({ 16 | tag: tag, 17 | type: "move", 18 | data: dataSet, 19 | index: dataSet.gridIndex 20 | }); 21 | } 22 | // 定义被释放区域,需要先绑定 onDragEnter、onDragOver 并且阻止默认事件 23 | onDragEnter = event =>{ 24 | event.preventDefault(); 25 | event.stopPropagation(); 26 | } 27 | onDragOver = event => { 28 | event.preventDefault(); 29 | event.stopPropagation(); 30 | const { onDragOver } = this.props; 31 | if (onDragOver) { 32 | data$.subscribe(dragData => { 33 | if (dragData.tag !== "base") { 34 | onDragOver(event, this.props.dataSet, this.containerComp); 35 | } 36 | }); 37 | } 38 | }; 39 | onDragLeave = event => { 40 | const { onDragLeave } = this.props; 41 | if (onDragLeave) { 42 | data$.subscribe(dragData => { 43 | if (dragData.tag !== "base") { 44 | onDragLeave(event, this.containerComp); 45 | } 46 | }); 47 | } 48 | }; 49 | //数组项内排序 50 | moveJudge = (index) => { 51 | const { moveField} = this.props; 52 | const dragIndex = index; 53 | const curIndex = this.props.currentDropIndex; 54 | if (curIndex >= dragIndex) { 55 | //往下拖拽 56 | if(!moveField){ 57 | return 58 | } 59 | moveField(dragIndex, curIndex); 60 | } else { 61 | if(!moveField){ 62 | return 63 | } 64 | moveField(dragIndex, curIndex + 1); 65 | //往上拖拽 66 | } 67 | }; 68 | //放入的组件-插入新项并排序 69 | dropJudge = data => { 70 | const { onDrop } = this.props; 71 | if (!onDrop) { 72 | return; 73 | } 74 | onDrop(data); 75 | }; 76 | // drop 事件中获取元素携带的信息,并进行展示 77 | onDrop = event => { 78 | event.preventDefault(); 79 | event.stopPropagation(); 80 | data$.subscribe(dragData => { 81 | if (dragData.tag !== "base") { 82 | if (dragData.type === "move") { 83 | this.moveJudge(dragData.index); 84 | } else { 85 | debugger 86 | this.dropJudge(dragData.data, event); 87 | } 88 | }else{ 89 | const { onDrop ,dataSet} = this.props; 90 | onDrop(dragData.data,dataSet) 91 | } 92 | }); 93 | }; 94 | render() { 95 | const { currentDropIndex, dataSet, gridIndex } = this.props; 96 | return ( 97 | 98 |
this.onDragEnter(event)} 101 | onDragOver={this.onDragOver} 102 | onDragLeave={this.onDragLeave} 103 | onDrop={this.onDrop} 104 | ref={comp => { 105 | this.containerComp = comp; 106 | }} 107 | style={dataSet ? {} : { height: "100%" }} 108 | > 109 | 110 | {currentDropIndex === gridIndex ? ( 111 |
112 | ) : ( 113 | "" 114 | )} 115 |
116 | 117 | ); 118 | } 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /client/src/components/DropDown/DropDownField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class TextAreaField extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status += ' active'; 19 | } 20 | if (isDragging) { 21 | status += ' draging'; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | 43 | {tipValue + (verifyValue ? '(必填)' : '')} 44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default WrapperDrop(WrapperDrag(TextAreaField)); 53 | -------------------------------------------------------------------------------- /client/src/components/DropDown/index.js: -------------------------------------------------------------------------------- 1 | import DropDownField from './DropDownField'; 2 | import DropDownFieldAttr from './DropDownFieldAttr'; 3 | 4 | DropDownField.DropDownFieldAttr = DropDownFieldAttr; 5 | export default DropDownField; 6 | -------------------------------------------------------------------------------- /client/src/components/Dustbin.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | 4 | // import { DropTarget } from 'react-dnd'; 5 | // import ItemTypes from '../types'; 6 | 7 | // const style = { 8 | // height: '12rem', 9 | // width: '12rem', 10 | // marginRight: '1.5rem', 11 | // marginBottom: '1.5rem', 12 | // color: 'white', 13 | // padding: '1rem', 14 | // textAlign: 'center', 15 | // fontSize: '1rem', 16 | // lineHeight: 'normal', 17 | // float: 'left', 18 | // } 19 | 20 | // const boxTarget = { 21 | // // 当有对应的 drag source 放在当前组件区域时,会返回一个对象,可以在 monitor.getDropResult() 中获取到 22 | // drop: () => ({ name: 'Dustbin' }) 23 | // } 24 | 25 | // @DropTarget( 26 | // // type 标识,这里是字符串 'box' 27 | // ItemTypes.BOX, 28 | // // 接收拖拽的事件对象 29 | // boxTarget, 30 | // // 收集功能函数,包含 connect 和 monitor 参数 31 | // // connect 里面的函数用来将 DOM 节点与 react-dnd 的 backend 建立联系 32 | // (connect, monitor) => ({ 33 | // // 包裹住 DOM 节点,使其可以接收对应的拖拽组件 34 | // connectDropTarget: connect.dropTarget(), 35 | // // drag source是否在 drop target 区域 36 | // isOver: monitor.isOver(), 37 | // // 是否可以被放置 38 | // canDrop: monitor.canDrop(), 39 | // }) 40 | // ) 41 | // class Dustbin extends React.Component { 42 | 43 | // static propTypes = { 44 | // canDrop: PropTypes.bool.isRequired, 45 | // isOver: PropTypes.bool.isRequired, 46 | // connectDropTarget: PropTypes.func.isRequired 47 | // } 48 | 49 | // render() { 50 | // const { canDrop, isOver, connectDropTarget } = this.props; 51 | // const isActive = canDrop && isOver; 52 | 53 | // let backgroundColor = '#222'; 54 | // // 拖拽组件此时正处于 drag target 区域时,当前组件背景色变为 darkgreen 55 | // if (isActive) { 56 | // backgroundColor = 'darkgreen'; 57 | // } 58 | // // 当前组件可以放置 drag source 时,背景色变为 pink 59 | // else if (canDrop) { 60 | // backgroundColor = 'darkkhaki'; 61 | // } 62 | 63 | // // 使用 connectDropTarget 包裹住 DOM 节点,使其可以接收对应的 drag source 组件 64 | // // connectDropTarget 包裹住的 DOM 节点才能接收 drag source 组件 65 | // return connectDropTarget && connectDropTarget( 66 | //
67 | // {isActive ? 'Release to drop' : 'Drag a box here'} 68 | //
69 | // ); 70 | // } 71 | // } 72 | 73 | // export default Dustbin; 74 | -------------------------------------------------------------------------------- /client/src/components/FormHidden/FormHidden.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class FormHidden extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status += ' active'; 19 | } 20 | if (isDragging) { 21 | status += ' draging'; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | 43 | 必填 44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default WrapperDrop(WrapperDrag(FormHidden)); 53 | -------------------------------------------------------------------------------- /client/src/components/FormHidden/FormHiddenAttr.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | export default class FormHiddenAttr extends PureComponent { 3 | handleChange = item => { 4 | const { activeItem, onSave } = this.props; 5 | const{ 6 | index, 7 | attrInfo, 8 | cellIndex 9 | } = activeItem; 10 | const updateAttrInfo = { 11 | ...attrInfo, 12 | ...item 13 | }; 14 | const updateActiveItem = {...activeItem,attrInfo:updateAttrInfo} 15 | onSave(updateActiveItem, index, cellIndex); 16 | }; 17 | render() { 18 | const { 19 | activeItem: { 20 | attrInfo: { titleValue, name} 21 | } 22 | } = this.props; 23 | return ( 24 |
25 |
{titleValue}
26 |
27 |
28 |
29 |
30 | 字段名 31 | 20 ? "error-toolong" : "" 34 | }`} 35 | > 36 | 最多20字 37 | 38 |
39 |
40 | { 44 | this.handleChange({ name: e.target.value }); 45 | }} 46 | /> 47 |
48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/FormHidden/index.js: -------------------------------------------------------------------------------- 1 | import FormHidden from './FormHidden'; 2 | import FormHiddenAttr from './FormHiddenAttr'; 3 | 4 | FormHidden.FormHiddenAttr = FormHiddenAttr; 5 | export default FormHidden; 6 | -------------------------------------------------------------------------------- /client/src/components/FormLayout/FormLayoutAttr.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { Select, Button } from "antd"; 3 | import Util from "../../utils/utils-form-design"; 4 | 5 | const Option = Select.Option; 6 | export default class FormLayoutAttr extends PureComponent { 7 | handleChange = ({ rowoptionvalue, coloptionvalue }) => { 8 | const { activeItem, onSave } = this.props; 9 | const { gridIndex, attrInfo } = activeItem; 10 | const updateAttrInfo = { 11 | ...attrInfo, 12 | rowoptionvalue:rowoptionvalue, 13 | coloptionvalue:coloptionvalue, 14 | grid:{ 15 | row:rowoptionvalue, 16 | col:coloptionvalue, 17 | rowtem:Util.initGridRowOrColumn(rowoptionvalue), 18 | coltem:Util.initGridRowOrColumn(coloptionvalue), 19 | cells:Util.initArray(rowoptionvalue * coloptionvalue) 20 | } 21 | }; 22 | const updateActiveItem = { ...activeItem, attrInfo: updateAttrInfo }; 23 | onSave(updateActiveItem, gridIndex); 24 | }; 25 | onClick = ()=>{ 26 | const { activeItem, onSave } = this.props; 27 | const { attrInfo ,attrInfo:{grid:{row,col,cells}}} = activeItem; 28 | const updateAttrInfo = { 29 | ...attrInfo, 30 | grid:{ 31 | row, 32 | col, 33 | rowtem:Util.initGridRowOrColumn(row), 34 | coltem:Util.initGridRowOrColumn(col), 35 | cells 36 | } 37 | }; 38 | const updateActiveItem = { ...activeItem, attrInfo: updateAttrInfo }; 39 | onSave(updateActiveItem); 40 | } 41 | 42 | render() { 43 | const { 44 | activeItem: { 45 | attrInfo: { coloptions, coloptionvalue, rowoptions, rowoptionvalue } 46 | } 47 | } = this.props; 48 | return ( 49 |
50 |
布局设置
51 |
52 |
53 |
54 |
55 |
56 | 74 |
75 |
76 |
77 | 95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 |
103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /client/src/components/FormLayout/index.js: -------------------------------------------------------------------------------- 1 | import FormLayout from './FormLayout'; 2 | import FormLayoutAttr from './FormLayoutAttr'; 3 | 4 | FormLayout.FormLayoutAttr = FormLayoutAttr; 5 | export default FormLayout; 6 | -------------------------------------------------------------------------------- /client/src/components/LayoutFields/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 3 | 4 | class LayoutFields extends Component { 5 | render() { 6 | const { dataSet, dragStart, draggable } = this.props; 7 | return ( 8 | //
15 |
16 | 17 | 图片 18 |
19 | //
20 | ); 21 | } 22 | } 23 | export default WrapperDrag(LayoutFields, 'grid'); 24 | -------------------------------------------------------------------------------- /client/src/components/MultipleBox/MultipleBox.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class MultipleBox extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status = `${status} active`; 19 | } 20 | if (isDragging) { 21 | status = `${status} draging`; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | {tipValue + (verifyValue ? '(必填)' : '')} 43 |
44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | export default WrapperDrop(WrapperDrag(MultipleBox)); 51 | -------------------------------------------------------------------------------- /client/src/components/MultipleBox/index.js: -------------------------------------------------------------------------------- 1 | import MultipleBox from './MultipleBox'; 2 | import MultipleBoxAttr from './MultipleBoxAttr'; 3 | 4 | MultipleBox.MultipleBoxAttr = MultipleBoxAttr; 5 | 6 | export default MultipleBox; 7 | -------------------------------------------------------------------------------- /client/src/components/RadioBox/RadioBox.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class RadioBox extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status += ' active'; 19 | } 20 | if (isDragging) { 21 | status += ' draging'; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | 43 | {tipValue + (verifyValue ? '(必填)' : '')} 44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default WrapperDrop(WrapperDrag(RadioBox)); 53 | -------------------------------------------------------------------------------- /client/src/components/RadioBox/index.js: -------------------------------------------------------------------------------- 1 | import RadioBox from './RadioBox'; 2 | import RadioBoxAttr from './RadioBoxAttr'; 3 | 4 | RadioBox.RadioBoxAttr = RadioBoxAttr; 5 | 6 | export default RadioBox; 7 | -------------------------------------------------------------------------------- /client/src/components/ResizeDemo/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useEventCallback } from 'rxjs-hooks'; 3 | import { fromEvent } from 'rxjs'; 4 | import './styles.scss'; 5 | import { 6 | map, switchMap, takeUntil, withLatestFrom, 7 | } from 'rxjs/operators'; 8 | 9 | function Resize() { 10 | const leftEle = useRef(null); 11 | // const [onMouseDown, leftX] = useEventCallback( 12 | // (event$, inputs$) => 13 | // event$.pipe( 14 | // withLatestFrom(inputs$.pipe(map(([leftEle]) => leftEle))), 15 | // switchMap(([event, leftEle]) => { 16 | // const leftStyle = getComputedStyle(leftEle.current); 17 | // const width0 = parseFloat(leftStyle.getPropertyValue("width")); 18 | // const startX = event.clientX; 19 | // return fromEvent(window, "mousemove").pipe( 20 | // map((moveEvent) => moveEvent.clientX - startX + width0), 21 | // takeUntil(fromEvent(window, "mouseup")) 22 | // ); 23 | // }) 24 | // ), 25 | // null, 26 | // [leftEle] 27 | // ); 28 | const [onMouseDown, leftX] = useEventCallback(() => (event$, inputs$) => event$.pipe( 29 | withLatestFrom(inputs$.pipe(map(([leftEle]) => leftEle))), 30 | switchMap(([event, leftEle]) => { 31 | const leftStyle = getComputedStyle(leftEle.current); 32 | const width0 = parseFloat(leftStyle.getPropertyValue('width')); 33 | const startX = event.clientX; 34 | return fromEvent(window, 'mousemove').pipe( 35 | // 计算元素的位置并设置样式改变元素位置 36 | map((moveEvent) => moveEvent.clientX - startX + width0), 37 | takeUntil(fromEvent(window, 'mouseup')), 38 | ); 39 | }, null), 40 | ), [leftEle]); 41 | 42 | const leftStyle = { 43 | flexBasis: leftX === null ? 0 : leftX, 44 | flexGrow: leftX === null ? 1 : 0, 45 | flexShrink: 0, 46 | }; 47 | 48 | return ( 49 |
50 |
51 |

52 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui dicta 53 | minus molestiae vel beatae natus eveniet ratione temporibus aperiam 54 | harum alias officiis assumenda officia quibusdam deleniti eos 55 | cupiditate dolore doloribus! 56 |

57 |

58 | Ad dolore dignissimos asperiores dicta facere optio quod commodi nam 59 | tempore recusandae. Rerum sed nulla eum vero expedita ex delectus 60 | voluptates rem at neque quos facere sequi unde optio aliquam! 61 |

62 |
63 |
64 | drag me 65 |
66 |
67 |

68 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui dicta 69 | minus molestiae vel beatae natus eveniet ratione temporibus aperiam 70 | harum alias officiis assumenda officia quibusdam deleniti eos 71 | cupiditate dolore doloribus! 72 |

73 |

74 | Ad dolore dignissimos asperiores dicta facere optio quod commodi nam 75 | tempore recusandae. Rerum sed nulla eum vero expedita ex delectus 76 | voluptates rem at neque quos facere sequi unde optio aliquam! 77 |

78 |
79 |
80 | ); 81 | } 82 | export default Resize; 83 | -------------------------------------------------------------------------------- /client/src/components/ResizeDemo/styles.css: -------------------------------------------------------------------------------- 1 | /* No CSS *//* # sourceMappingURL=styles.css.map */ 2 | -------------------------------------------------------------------------------- /client/src/components/ResizeDemo/styles.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "", 4 | "sources": [ 5 | "styles.scss" 6 | ], 7 | "names": [], 8 | "file": "styles.css" 9 | } -------------------------------------------------------------------------------- /client/src/components/ResizeDemo/styles.scss: -------------------------------------------------------------------------------- 1 | // body { 2 | // margin: 0; 3 | // } 4 | 5 | // .App { 6 | // background: lightblue; 7 | // display: flex; 8 | // } 9 | 10 | // .resizer { 11 | // position: relative; 12 | // display: flex; 13 | // flex: 0 0 50px; 14 | // padding: 10px; 15 | // background: orange; 16 | // cursor: col-resize; 17 | // user-select: none; 18 | // align-items: center; 19 | // color: white; 20 | // } 21 | 22 | // .left, 23 | // .right { 24 | // flex: 1 1 0; 25 | // //flex-grow flex-shrink flex-basis 26 | // padding: 0 20px; 27 | // } 28 | -------------------------------------------------------------------------------- /client/src/components/TextAreaField/TextAreaField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class TextAreaField extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | 16 | let status = ''; 17 | if (active) { 18 | status += ' active'; 19 | } 20 | if (isDragging) { 21 | status += ' draging'; 22 | } 23 | return ( 24 |
25 |
{ 28 | event.stopPropagation(); 29 | removeField(dataSet, cellIndex); 30 | }} 31 | /> 32 |
{ 35 | event.stopPropagation(); 36 | activeField(dataSet, active, cellIndex); 37 | }} 38 | /> 39 |
40 |
41 | 42 | 43 | {tipValue + (verifyValue ? '(必填)' : '')} 44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default WrapperDrop(WrapperDrag(TextAreaField)); 53 | -------------------------------------------------------------------------------- /client/src/components/TextAreaField/TextAreaFieldAttr.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { Checkbox } from "antd"; 3 | 4 | export default class TextAreaFieldAttr extends PureComponent { 5 | handleChange = item => { 6 | const { activeItem, onSave } = this.props; 7 | const{ 8 | index, 9 | attrInfo, 10 | cellIndex 11 | } = activeItem; 12 | const updateAttrInfo = { 13 | ...attrInfo, 14 | ...item 15 | }; 16 | const updateActiveItem = {...activeItem,attrInfo:updateAttrInfo} 17 | onSave(updateActiveItem, index, cellIndex); 18 | }; 19 | render() { 20 | const { 21 | activeItem: { 22 | attrInfo: { titleValue, tipValue, name, verifyValue } 23 | } 24 | } = this.props; 25 | return ( 26 |
27 |
多行输入框
28 |
29 |
30 |
31 |
32 | 标题 33 | 20 ? "error-toolong" : "" 36 | }`} 37 | > 38 | 最多20字 39 | 40 |
41 |
42 | { 46 | this.handleChange({ titleValue: e.target.value }); 47 | }} 48 | /> 49 |
50 |
51 |
52 |
53 | 字段名 54 | 20 ? "error-toolong" : "" 57 | }`} 58 | > 59 | 最多20字 60 | 61 |
62 |
63 | { 67 | this.handleChange({ name: e.target.value }); 68 | }} 69 | /> 70 |
71 |
72 |
73 |
74 | 提示文字 75 | 50 ? "error-toolong" : "" 78 | }`} 79 | > 80 | 最多50字 81 | 82 |
83 |
84 | { 88 | this.handleChange({ tipValue: e.target.value }); 89 | }} 90 | /> 91 |
内容最多可填写8000个字
92 |
93 |
94 |
95 |
验证
96 | 106 |
107 |
108 |
109 |
110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /client/src/components/TextAreaField/index.js: -------------------------------------------------------------------------------- 1 | import TextAreaField from './TextAreaField.js'; 2 | import TextAreaFieldAttr from './TextAreaFieldAttr.js'; 3 | 4 | TextAreaField.TextAreaFieldAttr = TextAreaFieldAttr; 5 | export default TextAreaField; 6 | -------------------------------------------------------------------------------- /client/src/components/TextField/TextField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class TextField extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, 9 | isDragging, 10 | activeField, 11 | removeField, 12 | draggabled, 13 | } = this.props; 14 | const { 15 | attrInfo: { titleValue, tipValue, verifyValue }, 16 | active, 17 | cellIndex, 18 | } = dataSet; 19 | let status = ''; 20 | if (active) { 21 | status += ' active'; 22 | } 23 | if (isDragging) { 24 | status += ' draging'; 25 | } 26 | return ( 27 |
31 |
{ 34 | event.stopPropagation(); 35 | removeField(dataSet, cellIndex); 36 | }} 37 | /> 38 |
{ 41 | event.stopPropagation(); 42 | activeField(dataSet, active, cellIndex); 43 | }} 44 | /> 45 |
46 |
47 | 48 | 49 | {tipValue + (verifyValue ? '(必填)' : '')} 50 | 51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | export default WrapperDrop(WrapperDrag(TextField)); 58 | -------------------------------------------------------------------------------- /client/src/components/TextField/index.js: -------------------------------------------------------------------------------- 1 | import TextField from './TextField.js'; 2 | import TextFieldAttr from './TextFieldAttr.js'; 3 | 4 | TextField.TextFieldAttr = TextFieldAttr; 5 | export default TextField; 6 | -------------------------------------------------------------------------------- /client/src/components/TextMoneyField/TextMoneyField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class TextNumberField extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | let status = ''; 16 | if (active) { 17 | status += ' active'; 18 | } 19 | if (isDragging) { 20 | status += ' draging'; 21 | } 22 | return ( 23 |
24 |
{ 27 | event.stopPropagation(); 28 | removeField(dataSet, cellIndex); 29 | }} 30 | /> 31 |
{ 34 | event.stopPropagation(); 35 | activeField(dataSet, active, cellIndex); 36 | }} 37 | /> 38 |
39 |
40 | 41 | 42 | {tipValue + (verifyValue ? '(必填)' : '')} 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | export default WrapperDrop(WrapperDrag(TextNumberField)); 51 | -------------------------------------------------------------------------------- /client/src/components/TextMoneyField/index.js: -------------------------------------------------------------------------------- 1 | import TextMoneyField from './TextMoneyField.js'; 2 | import TextMoneyFieldAttr from './TextMoneyFieldAttr.js'; 3 | 4 | TextMoneyField.TextMoneyFieldAttr = TextMoneyFieldAttr; 5 | export default TextMoneyField; 6 | -------------------------------------------------------------------------------- /client/src/components/TextNumberField/TextNumberField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | class TextNumberField extends PureComponent { 6 | render() { 7 | const { 8 | dataSet, isDragging, activeField, removeField, draggabled, 9 | } = this.props; 10 | const { 11 | attrInfo: { titleValue, tipValue, verifyValue }, 12 | active, 13 | cellIndex, 14 | } = dataSet; 15 | let status = ''; 16 | if (active) { 17 | status += ' active'; 18 | } 19 | if (isDragging) { 20 | status += ' draging'; 21 | } 22 | return ( 23 |
24 |
{ 27 | event.stopPropagation(); 28 | removeField(dataSet, cellIndex); 29 | }} 30 | /> 31 |
{ 34 | event.stopPropagation(); 35 | activeField(dataSet, active, cellIndex); 36 | }} 37 | /> 38 |
39 |
40 | 41 | 42 | {tipValue + (verifyValue ? '(必填)' : '')} 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | export default WrapperDrop(WrapperDrag(TextNumberField)); 51 | -------------------------------------------------------------------------------- /client/src/components/TextNumberField/index.js: -------------------------------------------------------------------------------- 1 | import TextNumberField from './TextNumberField.js'; 2 | import TextNumberFieldAttr from './TextNumberFieldAttr.js'; 3 | 4 | TextNumberField.TextNumberFieldAttr = TextNumberFieldAttr; 5 | export default TextNumberField; 6 | -------------------------------------------------------------------------------- /client/src/components/box/box.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | // import { DragSource } from 'react-dnd'; 4 | 5 | // import ItemTypes from '../../config/type'; 6 | 7 | // const style = { 8 | // border: '1px dashed gray', 9 | // backgroundColor: 'white', 10 | // padding: '0.5rem 1rem', 11 | // marginRight: '1.5rem', 12 | // marginBottom: '1.5rem', 13 | // cursor: 'move', 14 | // float: 'left', 15 | // } 16 | 17 | // const boxSource = { 18 | // /** 19 | // * 开始拖拽时触发当前函数 20 | // * @param {*} props 组件的 props 21 | // */ 22 | // beginDrag(props) { 23 | // // 返回的对象可以在 monitor.getItem() 中获取到 24 | // return { 25 | // name: props.name, 26 | // } 27 | // }, 28 | 29 | // /** 30 | // * 拖拽结束时触发当前函数 31 | // * @param {*} props 当前组件的 props 32 | // * @param {*} monitor DragSourceMonitor 对象 33 | // */ 34 | // endDrag(props, monitor) { 35 | // // 当前拖拽的 item 组件 36 | // const item = monitor.getItem() 37 | // // 拖拽元素放下时,drop 结果 38 | // const dropResult = monitor.getDropResult() 39 | 40 | // // 如果 drop 结果存在,就弹出 alert 提示 41 | // if (dropResult) { 42 | // alert(`You dropped ${item.name} into ${dropResult.name}!`) 43 | // } 44 | // }, 45 | // } 46 | 47 | // @DragSource( 48 | // // type 标识,这里是字符串 'box' 49 | // ItemTypes.BOX, 50 | // // 拖拽事件对象 51 | // boxSource, 52 | // // 收集功能函数,包含 connect 和 monitor 参数 53 | // // connect 里面的函数用来将 DOM 节点与 react-dnd 的 backend 建立联系 54 | // (connect, monitor) => ({ 55 | // // 包裹住 DOM 节点,使其可以进行拖拽操作 56 | // connectDragSource: connect.dragSource(), 57 | // // 是否处于拖拽状态 58 | // isDragging: monitor.isDragging(), 59 | // }), 60 | // ) 61 | // class Box extends React.Component { 62 | 63 | // static propTypes = { 64 | // name: PropTypes.string.isRequired, 65 | // isDragging: PropTypes.bool.isRequired, 66 | // connectDragSource: PropTypes.func.isRequired 67 | // } 68 | 69 | // render() { 70 | // const { isDragging, connectDragSource } = this.props 71 | // const { name } = this.props 72 | // const opacity = isDragging ? 0.4 : 1 73 | 74 | // // 使用 connectDragSource 包裹住 DOM 节点,使其可以接受各种拖动 API 75 | // // connectDragSource 包裹住的 DOM 节点才可以被拖动 76 | // return connectDragSource && connectDragSource( 77 | //
78 | // {name} 79 | //
80 | // ); 81 | // } 82 | // } 83 | 84 | // export default Box; 85 | -------------------------------------------------------------------------------- /client/src/components/footer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LinkA from '../../components/link-a' 3 | export default class extends Component { 4 | state = { 5 | params:{ 6 | href:'http://beian.miit.gov.cn/', 7 | target:'_black', 8 | text:'粤ICP备19121998号' 9 | } 10 | } 11 | render() { 12 | return ( 13 |
14 | Made with ❤ by XT 15 |
16 | ); 17 | } 18 | } -------------------------------------------------------------------------------- /client/src/components/header/index.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | z-index: 1; 7 | background: #fff; 8 | overflow: hidden; 9 | } 10 | .header .border { 11 | display: flex; 12 | align-items: center; 13 | justify-content: flex-end; 14 | padding: 8px; 15 | } 16 | .header .header-top { 17 | display: flex; 18 | align-items: center; 19 | justify-content: flex-end; 20 | padding: 8px; 21 | border-bottom: 1px solid; 22 | } 23 | .header .header-top .header-exit-btn { 24 | margin: 0 2px 0 4px; 25 | } 26 | .header .header-buttom { 27 | display: flex; 28 | align-items: center; 29 | justify-content: flex-end; 30 | padding: 8px; 31 | } 32 | .header .header-buttom span { 33 | margin: 0 1px; 34 | } 35 | .header .header-buttom .header-buttom-weather-img { 36 | width: 30px; 37 | } 38 | .header .header-buttom .header-buttom-weather-img img { 39 | width: 100%; 40 | height: 100%; 41 | } 42 | -------------------------------------------------------------------------------- /client/src/components/header/index.less: -------------------------------------------------------------------------------- 1 | .header{ 2 | position: fixed; 3 | top:0; 4 | left: 0; 5 | width: 100%; 6 | z-index: 1; 7 | background: #fff; 8 | overflow: hidden; 9 | .border{ 10 | display: flex; 11 | align-items: center; 12 | justify-content: flex-end; 13 | padding: 8px; 14 | } 15 | .header-top{ 16 | .border; 17 | border-bottom: 1px solid; 18 | .header-exit-btn{ 19 | margin: 0 2px 0 4px; 20 | } 21 | } 22 | .header-buttom{ 23 | .getSource{ 24 | 25 | } 26 | .border; 27 | span{ 28 | margin: 0 1px; 29 | } 30 | .header-buttom-weather-img{ 31 | width: 30px; 32 | img{ 33 | width: 100%; 34 | height: 100%; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/left-nav/action-type.js: -------------------------------------------------------------------------------- 1 | export const SETHEADTITLE = 'set_head_title'; 2 | -------------------------------------------------------------------------------- /client/src/components/left-nav/action.js: -------------------------------------------------------------------------------- 1 | import { SETHEADTITLE } from './action-type'; 2 | 3 | // 设置title名称的同步action 4 | export const setHeadTitle = (title) => ({ 5 | type: SETHEADTITLE, 6 | data: title, 7 | }); 8 | -------------------------------------------------------------------------------- /client/src/components/left-nav/index.less: -------------------------------------------------------------------------------- 1 | .left-nav{ 2 | position: fixed; 3 | min-width:200px; 4 | height: 100%; 5 | overflow-y: auto; 6 | z-index: 1000; 7 | .left-nav-header{ 8 | margin-top: 10px; 9 | z-index: 2; 10 | .left-nav-header-content{ 11 | height: 30px; 12 | line-height: 30px; 13 | font-size: 20px; 14 | text-align: center; 15 | color: #fff; 16 | overflow: hidden; 17 | vertical-align: middle; 18 | } 19 | } 20 | .left-nav-content{ 21 | height: 100%; 22 | overflow: scroll; 23 | } 24 | } 25 | @media screen and (max-width: 960px){ 26 | .left-nav-header-content{ 27 | display: none; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/left-nav/reducer.js: -------------------------------------------------------------------------------- 1 | import { SETHEADTITLE } from './action-type'; 2 | 3 | 4 | export default (state = '首页', action) => { 5 | switch (action.type) { 6 | case SETHEADTITLE: 7 | return action.data; 8 | default: 9 | return state; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /client/src/components/link-a/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * 5 | * @param {*} @param: params object 6 | * @description 7 | */ 8 | export default function (props) { 9 | const { href, text, target } = props.params || {}; 10 | return ( 11 | {text} 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/loading/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | 4 | // export default class extends Component { 5 | // state = { } 6 | 7 | // render() { 8 | // return ( 9 | //
10 | // 11 | //
12 | // ); 13 | // } 14 | // } 15 | 16 | export default function () { 17 | return ( 18 |
19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client/src/components/test.js: -------------------------------------------------------------------------------- 1 | class ExampleComponent extends React.Component { 2 | // 用于初始化 state 3 | constructor() {} 4 | 5 | // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用 6 | // 因为该函数是静态函数,所以取不到 `this` 7 | // 如果需要对比 `prevProps` 需要单独在 `state` 中维护 8 | static getDerivedStateFromProps(nextProps, prevState) {} 9 | 10 | // 判断是否需要更新组件,多用于组件性能优化 11 | shouldComponentUpdate(nextProps, nextState) {} 12 | 13 | // 组件挂载后调用 14 | // 可以在该函数中进行请求或者订阅 15 | componentDidMount() {} 16 | 17 | // 用于获得最新的 DOM 数据 18 | getSnapshotBeforeUpdate() {} 19 | 20 | // 组件即将销毁 21 | // 可以在此处移除订阅,定时器等等 22 | componentWillUnmount() {} 23 | 24 | // 组件销毁后调用 25 | componentDidUnMount() {} 26 | 27 | // 组件更新后调用 28 | componentDidUpdate() {} 29 | 30 | // 渲染组件函数 31 | render() {} 32 | 33 | // 以下函数不建议使用 34 | UNSAFE_componentWillMount() {} 35 | 36 | UNSAFE_componentWillUpdate(nextProps, nextState) {} 37 | 38 | UNSAFE_componentWillReceiveProps(nextProps) {} 39 | } 40 | -------------------------------------------------------------------------------- /client/src/components/test/TestField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import WrapperDrop from '../DragAndDrop/WrapperDrop.js'; 3 | import WrapperDrag from '../DragAndDrop/WrapperDrag.js'; 4 | 5 | // 改变 组件在布局中的位置 6 | class TextField extends PureComponent { 7 | render() { 8 | const { 9 | dataSet, 10 | isDragging, 11 | activeField, 12 | removeField, 13 | draggabled, 14 | } = this.props; 15 | const { 16 | attrInfo: { titleValue, tipValue, verifyValue }, 17 | active, 18 | cellIndex, 19 | } = dataSet; 20 | let status = ''; 21 | if (active) { 22 | status += ' active'; 23 | } 24 | if (isDragging) { 25 | status += ' draging'; 26 | } 27 | return ( 28 |
32 |
{ 35 | event.stopPropagation(); 36 | removeField(dataSet, cellIndex); 37 | }} 38 | /> 39 |
{ 42 | event.stopPropagation(); 43 | activeField(dataSet, active, cellIndex); 44 | }} 45 | /> 46 |
47 |
48 | 49 | 50 | {tipValue + (verifyValue ? '(必填)' : '')} 51 | 52 |
53 |
54 |
55 | ); 56 | } 57 | } 58 | export default WrapperDrop(WrapperDrag(TextField)); 59 | -------------------------------------------------------------------------------- /client/src/components/test/index.js: -------------------------------------------------------------------------------- 1 | import TestField from './TestField'; 2 | import TextFieldAttr from './TestFieldAttr'; 3 | 4 | TestField.TextFieldAttr = TextFieldAttr; 5 | export default TestField; 6 | -------------------------------------------------------------------------------- /client/src/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | serverAddress: 'http://localhost:8081', 3 | baseURl: '/api', 4 | imgUrl: 'http://localhost:8081/upload/', 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/config/menuConfig.js: -------------------------------------------------------------------------------- 1 | const menuList = [ 2 | { 3 | title: '首页', // 菜单标题名称 4 | key: '/home', // 对应的path 5 | icon: 'home', // 图标名称 6 | isPublic: true, // 公开的 7 | }, 8 | { 9 | title: 'CRUD', 10 | key: '/products', 11 | icon: 'appstore', 12 | children: [ 13 | // 子菜单列表 14 | { 15 | title: '增删改查', 16 | key: '/category', 17 | icon: 'bars', 18 | }, 19 | { 20 | title: '富文本', 21 | key: '/product', 22 | icon: 'tool', 23 | }, 24 | { 25 | title: 'tabs', 26 | key: '/order', 27 | icon: 'windows', 28 | }, 29 | ], 30 | }, 31 | 32 | { 33 | title: '用户管理', 34 | key: '/user', 35 | icon: 'user', 36 | }, 37 | { 38 | title: '角色管理', 39 | key: '/role', 40 | icon: 'safety', 41 | }, 42 | { 43 | title: '虚拟dom', 44 | key: '/virtualDom', 45 | icon: 'safety', 46 | }, 47 | 48 | { 49 | title: '图表可视化', 50 | key: '/charts', 51 | icon: 'area-chart', 52 | children: [ 53 | { 54 | title: '柱形图', 55 | key: '/charts/bar', 56 | icon: 'bar-chart', 57 | }, 58 | { 59 | title: '折线图', 60 | key: '/charts/line', 61 | icon: 'line-chart', 62 | }, 63 | { 64 | title: '饼图', 65 | key: '/charts/pie', 66 | icon: 'pie-chart', 67 | }, 68 | { 69 | title: '地图', 70 | key: '/charts/map', 71 | icon: 'radar-chart', 72 | }, 73 | ], 74 | }, 75 | 76 | { 77 | title: '组件拖拽', 78 | key: '/drag', 79 | icon: 'drag', 80 | children: [ 81 | { 82 | title: '元素拖拽', 83 | key: '/drag/native', 84 | icon: 'drag', 85 | }, 86 | { 87 | title: 'Rxjs拖拽', 88 | key: '/drag/rxjs', 89 | icon: 'drag', 90 | }, 91 | { 92 | title: '自定义组件', 93 | key: '/drag/form-design', 94 | icon: 'form', 95 | }, 96 | ], 97 | }, 98 | { 99 | title: 'Hooks', 100 | key: '/hooks', 101 | icon: 'smile', 102 | children: [ 103 | { 104 | title: '切片上传', 105 | key: '/hooks/slice', 106 | icon: 'cloud-upload', 107 | }, 108 | { 109 | title: '实践', 110 | key: '/hooks/test', 111 | icon: 'loading', 112 | }, 113 | ], 114 | }, 115 | { 116 | title: '长列表渲染', 117 | key: '/bigTable', 118 | icon: 'table', 119 | }, 120 | { 121 | title: 'GitHub', 122 | key: '/github', 123 | icon: 'github', 124 | }, 125 | ]; 126 | 127 | export default menuList; 128 | -------------------------------------------------------------------------------- /client/src/config/type.js: -------------------------------------------------------------------------------- 1 | export default { 2 | BOX: 'box', 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/directive/dragdropdirective.js: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from "rxjs"; 2 | 3 | export class DragDropService { 4 | _dragData = new BehaviorSubject(); 5 | setDragData(data){ 6 | this._dragData.next(data); 7 | } 8 | getDragData(){ 9 | return this._dragData.asObservable(); 10 | } 11 | clearDragData(){ 12 | this._dragData.next(null); 13 | } 14 | } 15 | export default new DragDropService() -------------------------------------------------------------------------------- /client/src/images/angledown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/angledown.png -------------------------------------------------------------------------------- /client/src/images/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/attachment.png -------------------------------------------------------------------------------- /client/src/images/cascadedrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/cascadedrop.png -------------------------------------------------------------------------------- /client/src/images/date.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/date.png -------------------------------------------------------------------------------- /client/src/images/datesection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/datesection.png -------------------------------------------------------------------------------- /client/src/images/formhidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/formhidden.png -------------------------------------------------------------------------------- /client/src/images/formsection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/formsection.png -------------------------------------------------------------------------------- /client/src/images/money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/money.png -------------------------------------------------------------------------------- /client/src/images/multiplebox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/multiplebox.png -------------------------------------------------------------------------------- /client/src/images/number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/number.png -------------------------------------------------------------------------------- /client/src/images/radiobox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/radiobox.png -------------------------------------------------------------------------------- /client/src/images/textareafield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/textareafield.png -------------------------------------------------------------------------------- /client/src/images/textfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/images/textfield.png -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | // 全站置灰色 5 | // import './assets/gray.css' 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | -------------------------------------------------------------------------------- /client/src/pages/VirtualDom/element.js: -------------------------------------------------------------------------------- 1 | function Element(tagName, props, children) { 2 | this.tagName = tagName; 3 | this.props = props; 4 | this.children = children; 5 | } 6 | 7 | Element.prototype.render = function () { 8 | const el = document.createElement(this.tagName); // 根据tagName构建 9 | const { props } = this; 10 | 11 | for (const propName in props) { 12 | // 设置节点的DOM属性 13 | const propValue = props[propName]; 14 | el.setAttribute(propName, propValue); 15 | } 16 | 17 | const children = this.children || []; 18 | 19 | children.forEach((child) => { 20 | const childEl = child instanceof Element 21 | ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点 22 | : document.createTextNode(child); // 如果字符串,只构建文本节点 23 | el.appendChild(childEl); 24 | }); 25 | 26 | return el; 27 | }; 28 | module.exports = function (tagName, props, children) { 29 | return new Element(tagName, props, children); 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/pages/VirtualDom/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Element from './element'; 3 | 4 | const ul = Element('ul', { id: 'list' }, [ 5 | Element('li', { class: 'item' }, ['Item 1']), 6 | Element('li', { class: 'item' }, ['Item 2']), 7 | Element('li', { class: 'item' }, ['Item 3']), 8 | ]); 9 | 10 | export default class VirtualDom extends Component { 11 | constructor() { 12 | super(); 13 | this.state = { 14 | render: ul.render(), 15 | val: 0, 16 | }; 17 | } 18 | 19 | // 1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。 20 | // 2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。 21 | // 3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。 22 | // 输出: 0 0 2 3 23 | // 在React的setState函数实现中,会根据一个变量 isBatchingUpdate 来判断是直接同步更新this.state还是放到队列中异步更新 。React使用了事务的机制,React的每个生命周期和合成事件都处在一个大的事务当中。在事务的前置钩子中调用batchedUpdates方法修改isBatchingUpdates变量为true,在后置钩子中将变量置为false。原生绑定事件和setTimeout异步的函数没有进入到React的事务当中,或者当他们执行时,刚刚的事务已近结束了,后置钩子触发了,所以此时的setState会直接进入非批量更新模式,表现在我们看来成为了同步SetState。 24 | componentDidMount() { 25 | this.setState({ val: this.state.val + 1 }); 26 | console.log(this.state.val); // 第 1 次 log 0 27 | 28 | this.setState({ val: this.state.val + 1 }); 29 | console.log(this.state.val); // 第 2 次 log 0 30 | 31 | setTimeout(() => { 32 | this.setState({ val: this.state.val + 1 }); 33 | console.log(this.state.val); // 第 3 次 log 2 34 | 35 | this.setState({ val: this.state.val + 1 }); 36 | console.log(this.state.val); // 第 4 次 log 3 37 | }, 0); 38 | } 39 | 40 | render() { 41 | // console.log(this.state.render); 42 | return ( 43 | <> 44 |

45 | 用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 46 | DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了 47 |

48 | {/*
{this.state.render}
*/} 49 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/pages/category/add-form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Form,Select,Input} from 'antd' 3 | import PropTypes from 'prop-types' 4 | const { Option } = Select; 5 | class addForm extends Component { 6 | static propTypes={ 7 | categoryList:PropTypes.array.isRequired, 8 | setForm:PropTypes.func.isRequired, 9 | currentRowData:PropTypes.object.isRequired //父级分类id 10 | } 11 | componentDidMount(){ 12 | this.props.setForm(this.props.form) 13 | } 14 | render() { 15 | const { getFieldDecorator } = this.props.form; 16 | const { categoryList,currentRowData}= this.props 17 | return ( 18 |
19 | 20 | {getFieldDecorator('categoryId', { 21 | rules: [{ required: true, message: '请选择分类!' }], 22 | initialValue:currentRowData._id 23 | })( 24 | 33 | )} 34 | 35 | 36 | 37 | {getFieldDecorator('categoryName', { 38 | rules: [{ required: true, message: '请输入分类!' }], 39 | })()} 40 | 41 | 42 |
43 | ); 44 | } 45 | } 46 | const WrappedApp = Form.create({ name: 'coordinated' })(addForm); 47 | export default (WrappedApp); -------------------------------------------------------------------------------- /client/src/pages/category/test.js: -------------------------------------------------------------------------------- 1 | const twoSum = function (nums, target) { 2 | for (let i = 0; i < nums.length; i++) { 3 | const diff = target - nums[i]; 4 | for (let j = 0; j < nums.length; j++) { 5 | if (diff === nums[j]) { 6 | return [i, j]; 7 | } 8 | } 9 | } 10 | }; 11 | // var twoSum = function(nums, target) { 12 | // for (var i = 0; i < nums.length; i++) { 13 | // var dif = target - nums[i]; 14 | // // j = i + 1 的目的是减少重复计算和避免两个元素下标相同 15 | // for (var j = i + 1; j < nums.length; j++) { 16 | // if(nums[j] == dif) 17 | // return [i,j]; 18 | // } 19 | // } 20 | // }; 21 | 22 | 23 | const arr = twoSum([2, 7, 11, 15], 9); 24 | console.log(arr); 25 | -------------------------------------------------------------------------------- /client/src/pages/category/update-form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types' 3 | 4 | import {Form,Input} from 'antd' 5 | class UpdateForm extends Component { 6 | static propTypes={ 7 | currentRowData:PropTypes.object.isRequired, 8 | setForm:PropTypes.func.isRequired 9 | } 10 | componentDidMount(){ 11 | this.props.setForm(this.props.form) 12 | } 13 | render() { 14 | const {name}=this.props.currentRowData 15 | const { getFieldDecorator } = this.props.form; 16 | return ( 17 |
18 | 19 | 20 | {getFieldDecorator('categoryName', { 21 | rules: [{ required: true, message: '请输入分类名称!' }], 22 | initialValue:name 23 | })()} 24 | 25 | 26 |
27 | ); 28 | } 29 | } 30 | const WrappedApp = Form.create()(UpdateForm); 31 | export default (WrappedApp); -------------------------------------------------------------------------------- /client/src/pages/charts/D3/graph.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/pages/charts/D3/graph.js -------------------------------------------------------------------------------- /client/src/pages/charts/bar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Card, Button} from 'antd' 3 | import ReactEcharts from 'echarts-for-react' 4 | 5 | /* 6 | 后台管理的柱状图路由组件 7 | */ 8 | export default class Bar extends Component { 9 | 10 | state = { 11 | sales: [5, 20, 36, 10, 10, 20], // 销量的数组 12 | stores: [6, 10, 25, 20, 15, 10], // 库存的数组 13 | } 14 | 15 | update = () => { 16 | this.setState(state => ({ 17 | sales: state.sales.map(sale => sale + 1), 18 | stores: state.stores.reduce((pre, store) => { 19 | pre.push(store-1) 20 | return pre 21 | }, []), 22 | })) 23 | } 24 | 25 | /* 26 | 返回柱状图的配置对象 27 | */ 28 | getOption = (sales, stores) => { 29 | return { 30 | title: { 31 | text: '库存销量' 32 | }, 33 | tooltip: {}, 34 | legend: { 35 | data:['销量', '库存'] 36 | }, 37 | xAxis: { 38 | data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"] 39 | }, 40 | yAxis: {}, 41 | series: [{ 42 | name: '销量', 43 | type: 'bar', 44 | data: sales 45 | }, { 46 | name: '库存', 47 | type: 'bar', 48 | data: stores 49 | }] 50 | } 51 | } 52 | 53 | render() { 54 | const {sales, stores} = this.state 55 | return ( 56 |
57 | 更新)}> 58 | 59 | 60 |
61 | ) 62 | } 63 | } -------------------------------------------------------------------------------- /client/src/pages/charts/line.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Card, Button} from 'antd' 3 | import ReactEcharts from 'echarts-for-react' 4 | 5 | /* 6 | 后台管理的折线图路由组件 7 | */ 8 | export default class Line extends Component { 9 | 10 | state = { 11 | sales: [5, 20, 36, 10, 10, 20], // 销量的数组 12 | stores: [6, 10, 25, 20, 15, 10], // 库存的数组 13 | } 14 | 15 | update = () => { 16 | this.setState(state => ({ 17 | sales: state.sales.map(sale => sale + 1), 18 | stores: state.stores.reduce((pre, store) => { 19 | pre.push(store-1) 20 | return pre 21 | }, []), 22 | })) 23 | } 24 | 25 | /* 26 | 返回柱状图的配置对象 27 | */ 28 | getOption = (sales, stores) => { 29 | return { 30 | title: { 31 | text: '折线' 32 | }, 33 | tooltip: {}, 34 | legend: { 35 | data:['销量', '库存'] 36 | }, 37 | xAxis: { 38 | data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"] 39 | }, 40 | yAxis: {}, 41 | series: [{ 42 | name: '销量', 43 | type: 'line', 44 | data: sales 45 | }, { 46 | name: '库存', 47 | type: 'line', 48 | data: stores 49 | }] 50 | } 51 | } 52 | 53 | render() { 54 | const {sales, stores} = this.state 55 | return ( 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /client/src/pages/charts/map.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import echarts from "echarts"; 3 | // import world from 'echarts/map/js/world.js'; 4 | // 有两种用法 5 | import world from "echarts/map/json/world.json"; 6 | import options from "../../mock/map.json"; 7 | // import china from 'echarts/map/json/china.json'; 8 | 9 | // echarts.registerMap('world', world);/* 注册world地图 */ 10 | // echarts.registerMap('china', china);/* 注册world地图 */ 11 | // 或者 12 | // import world from 'echarts/map/js/world.js'; 13 | // import china from 'echarts/map/js/china.js'; 14 | // 没什么不同的, js文件里是帮我们做了注册json 15 | import { deepClone } from "../../utils/common"; 16 | import ReactEcharts from "echarts-for-react"; 17 | import { reqMapOptions, reqMapWorld } from "../../api"; 18 | export default class Map extends Component { 19 | constructor() { 20 | super(); 21 | this.state = { 22 | options, 23 | world: {}, 24 | }; 25 | } 26 | getRandomData(data) { 27 | let result = data.slice(); 28 | return result.slice(0, Math.round(Math.random() * (data.length - 1))); 29 | } 30 | componentDidMount() { 31 | // this.getOptions(); 32 | } 33 | refreshData() { 34 | const { options } = this.state; 35 | if (!options || !options.series) { 36 | return; 37 | } 38 | 39 | let option = deepClone(this.rawOptions); 40 | 41 | let data4 = option.series[4].data; 42 | let data5 = option.series[5].data; 43 | let data6 = option.series[6].data; 44 | 45 | option.series[4].data = this.getRandomData(data4); 46 | option.series[5].data = this.getRandomData(data5); 47 | option.series[6].data = this.getRandomData(data6); 48 | 49 | this.setState({ 50 | options: option, 51 | }); 52 | } 53 | getOptions = async () => { 54 | const option = await reqMapOptions(); 55 | // const option = options 56 | option.geo.zlevel = 1; 57 | option.series.forEach((s, index) => { 58 | s.zlevel = index + 2; 59 | if (s.effect) { 60 | s.effect.zlevel = index + option.series.length + 2; 61 | } 62 | if (s.rippleEffect) { 63 | s.rippleEffect.zlevel = index + option.series.length + 2; 64 | } 65 | }); 66 | this.rawOptions = option; 67 | this.options = option; 68 | const world = await reqMapWorld(); 69 | echarts.registerMap("world", world); 70 | this.setState({ 71 | options: option, 72 | world, 73 | }); 74 | var chart = echarts.init(document.getElementById("map"), null, { 75 | devicePixelRatio: 1, 76 | }); 77 | 78 | chart.setOption(option); 79 | window.addEventListener("resize", chart.resize); 80 | setInterval(() => { 81 | this.refreshData(); 82 | }, 5000); 83 | }; 84 | getOption() { 85 | return this.state.options; 86 | } 87 | render() { 88 | return ( 89 | <> 90 | 91 | 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /client/src/pages/charts/pie.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Card} from 'antd' 3 | import ReactEcharts from 'echarts-for-react' 4 | 5 | /* 6 | 后台管理的饼图路由组件 7 | */ 8 | export default class Pie extends Component { 9 | 10 | getOption = () => { 11 | return { 12 | title : { 13 | text: '某站点用户访问来源', 14 | subtext: '纯属虚构', 15 | x:'center' 16 | }, 17 | tooltip : { 18 | trigger: 'item', 19 | formatter: "{a}
{b} : {c} ({d}%)" 20 | }, 21 | legend: { 22 | orient: 'vertical', 23 | left: 'left', 24 | data: ['直接访问','邮件营销','联盟广告','视频广告','搜索引擎'] 25 | }, 26 | series : [ 27 | { 28 | name: '访问来源', 29 | type: 'pie', 30 | radius : '55%', 31 | center: ['50%', '60%'], 32 | data:[ 33 | {value:335, name:'直接访问'}, 34 | {value:310, name:'邮件营销'}, 35 | {value:234, name:'联盟广告'}, 36 | {value:135, name:'视频广告'}, 37 | {value:1548, name:'搜索引擎'} 38 | ], 39 | itemStyle: { 40 | emphasis: { 41 | shadowBlur: 10, 42 | shadowOffsetX: 0, 43 | shadowColor: 'rgba(0, 0, 0, 0.5)' 44 | } 45 | } 46 | } 47 | ] 48 | }; 49 | 50 | } 51 | 52 | getOption2 = () => { 53 | return { 54 | backgroundColor: '#2c343c', 55 | 56 | title: { 57 | text: '测试数据', 58 | left: 'center', 59 | top: 20, 60 | textStyle: { 61 | color: '#ccc' 62 | } 63 | }, 64 | 65 | tooltip : { 66 | trigger: 'item', 67 | formatter: "{a}
{b} : {c} ({d}%)" 68 | }, 69 | 70 | visualMap: { 71 | show: false, 72 | min: 80, 73 | max: 600, 74 | inRange: { 75 | colorLightness: [0, 1] 76 | } 77 | }, 78 | series : [ 79 | { 80 | name:'访问来源', 81 | type:'pie', 82 | radius : '55%', 83 | center: ['50%', '50%'], 84 | data:[ 85 | {value:335, name:'直接访问'}, 86 | {value:310, name:'邮件营销'}, 87 | {value:274, name:'联盟广告'}, 88 | {value:235, name:'视频广告'}, 89 | {value:400, name:'搜索引擎'} 90 | ].sort(function (a, b) { return a.value - b.value; }), 91 | roseType: 'radius', 92 | label: { 93 | normal: { 94 | textStyle: { 95 | color: 'rgba(255, 255, 255, 0.3)' 96 | } 97 | } 98 | }, 99 | labelLine: { 100 | normal: { 101 | lineStyle: { 102 | color: 'rgba(255, 255, 255, 0.3)' 103 | }, 104 | smooth: 0.2, 105 | length: 10, 106 | length2: 20 107 | } 108 | }, 109 | itemStyle: { 110 | normal: { 111 | color: '#c23531', 112 | shadowBlur: 200, 113 | shadowColor: 'rgba(0, 0, 0, 0.5)' 114 | } 115 | }, 116 | 117 | animationType: 'scale', 118 | animationEasing: 'elasticOut', 119 | animationDelay: function (idx) { 120 | return Math.random() * 200; 121 | } 122 | } 123 | ] 124 | }; 125 | } 126 | 127 | render() { 128 | return ( 129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 |
137 | ) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /client/src/pages/drag/Native.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | // import Resize from '../../components/ResizeDemo' 3 | const style = { 4 | position: 'absolute', 5 | width: '100px', 6 | height: '100px', 7 | background: 'red', 8 | cursor: 'pointer', 9 | zIndex: 1, 10 | }; 11 | 12 | export default class NativeDrag extends PureComponent { 13 | constructor() { 14 | super(); 15 | this.state = { 16 | isDrag: false, 17 | initialX: null, 18 | initialY: null, 19 | }; 20 | } 21 | 22 | componentDidMount() { 23 | } 24 | 25 | mouseDown(e) { 26 | this.setState({ 27 | isDrag: true, 28 | }); 29 | const { left, top } = this.el.getBoundingClientRect(); 30 | this.setState({ 31 | initialX: e.clientX - left, 32 | initialY: e.clientY - top, 33 | }); 34 | } 35 | 36 | mouseMove(e) { 37 | const { isDrag } = this.state; 38 | if (isDrag) { 39 | const { initialX, initialY } = this.state; 40 | this.el.style.left = `${e.clientX - initialX}px`; 41 | this.el.style.top = `${e.clientY - initialY}px`; 42 | } 43 | } 44 | 45 | mouseUp() { 46 | this.setState({ 47 | isDrag: false, 48 | }); 49 | } 50 | 51 | render() { 52 | return ( 53 | <> 54 |
(this.el = el)} 57 | onMouseDown={(e) => this.mouseDown(e)} 58 | onMouseMove={(e) => this.mouseMove(e)} 59 | onMouseUp={(e) => this.mouseUp(e)} 60 | > 61 | 原生拖拽 62 |
63 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/src/pages/drag/Rxjs.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { findDOMNode } from "react-dom"; 3 | import { Button, Input } from "antd"; 4 | import { fromEvent } from "rxjs"; 5 | function btn(){ 6 | return ( 7 | 8 | ) 9 | } 10 | export default class RxjsDrag extends PureComponent { 11 | click = (e) => { 12 | console.log("one-btn", e); 13 | fromEvent(findDOMNode(this.btn), "click").subscribe((ee) => { 14 | console.log("two-btn", ee); 15 | }); 16 | }; 17 | onMouseDown = (e) => { 18 | const div = fromEvent(findDOMNode(this.div), "mousemove"); 19 | console.log(div); 20 | }; 21 | onDragStart = (event)=>{ 22 | event.dataTransfer.setData('text/plain', 'drag info'); 23 | } 24 | onDragEnter = (event) => { 25 | event.preventDefault(); 26 | event.stopPropagation(); 27 | }; 28 | onDragOver = (event) => { 29 | event.preventDefault(); 30 | event.stopPropagation(); 31 | }; 32 | onDrop = (event) => { 33 | event.preventDefault(); 34 | const data = event.dataTransfer.getData("text/plain") || 'test'; 35 | const div = document.createElement("div"); 36 | div.textContent = data; 37 | event.target.appendChild(div); 38 | 39 | }; 40 | render() { 41 | return ( 42 | <> 43 | 49 | 52 |
this.onDrop(e)} 57 | style={{ width: "300px", height: "300px", border: "1px solid red" }} 58 | onMouseDown={(e) => this.onMouseDown(e)} 59 | ref={(el) => (this.div = el)} 60 | > 61 | onMouseDown 62 |
63 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/src/pages/drag/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { List } from "antd"; 3 | import RxjsDrag from "./Native"; 4 | const data = []; 5 | for (let i = 0; i < 5; i++) { 6 | data.push(`item${i}`); 7 | } 8 | 9 | const maskStyle = { 10 | position: "fixed", 11 | left: 0, 12 | right: 0, 13 | top: 0, 14 | bottom: 0, 15 | backgorund: "rgba(0,0,0,0.5)", 16 | }; 17 | // 从新计算数组 18 | const move = (arr = [], startIndex, toIndex) => { 19 | arr = arr.slice(); 20 | arr.splice(toIndex, 0, arr.splice(startIndex, 1)); 21 | return arr; 22 | }; 23 | const lineHeight = 42; 24 | export default class Drag extends Component { 25 | state = { 26 | a:0, 27 | list: data, 28 | dragging: false, 29 | draggingIndex: -1, 30 | startPageY: 0, 31 | offsetPageY: 0, 32 | }; 33 | // 点击的时候记录 Y 轴的位置 34 | dragging = (e, index) => { 35 | this.setState({ 36 | dragging: true, 37 | draggingIndex: index, 38 | currentPageY: e.pageY, 39 | startPageY: e.pageY, 40 | }); 41 | }; 42 | // 松开鼠标的时候,从新初始化 startPageY、draggingIndex, 43 | onMouseUp = (e) => { 44 | this.setState({ 45 | dragging: false, 46 | startPageY: 0, 47 | draggingIndex: -1, 48 | }); 49 | }; 50 | // 移动的轨迹 51 | onMouseMove = (e) => { 52 | let offset = e.pageY - this.state.startPageY; 53 | const draggingIndex = this.state.draggingIndex; 54 | if (offset > lineHeight && draggingIndex < this.state.list.length) { 55 | // 向下移动 56 | offset -= lineHeight; 57 | // move 期间,state 的数据是动态变化的,draggingIndex 始终比上一个多 1 58 | this.setState({ 59 | list: move(this.state.list, draggingIndex, draggingIndex + 1), // 按照移动的方向进行数据交换 60 | draggingIndex: draggingIndex + 1, 61 | startPageY: this.state.startPageY + lineHeight, 62 | }); 63 | } else if (offset < -lineHeight && draggingIndex > 0) { 64 | // 向上移动 65 | offset += lineHeight; 66 | this.setState({ 67 | list: move(this.state.list, draggingIndex, draggingIndex - 1), 68 | draggingIndex: draggingIndex - 1, 69 | startPageY: this.state.startPageY - lineHeight, 70 | }); 71 | } 72 | // item 移动的距离 73 | this.setState({ offsetPageY: offset }); 74 | }; 75 | // 移动动画 76 | getDraggingStyle = (index) => { 77 | if (index !== this.state.draggingIndex) return; 78 | return { 79 | backgorundColor: "#eee", 80 | transform: `translate(10px,${this.state.offsetPageY}px)`, 81 | opacity: 0.5, 82 | }; 83 | }; 84 | componentDidMount(){ 85 | // this.setState(function(state,props){ 86 | // return {a:state.a+1} 87 | // }) 88 | // this.setState(function(state,props){ 89 | // return {a:state.a+1} 90 | // }) 91 | // console.log(this.state.a) 92 | } 93 | render() { 94 | return ( 95 |
96 |

原生事件实现拖动上下组件变换index

97 | 列表头部
} 99 | footer={
列表尾部
} 100 | bordered 101 | dataSource={this.state.list} 102 | renderItem={(item, index) => ( 103 | this.dragging(e, index)} 105 | style={this.getDraggingStyle(index)} 106 | key={item} 107 | > 108 | {item} 109 | 110 | )} 111 | /> 112 | {/* 用一个遮罩监听事件,也可以监听整个 document */} 113 | {this.state.dragging && ( 114 |
this.onMouseUp(e)} 117 | onMouseMove={(e) => this.onMouseMove(e)} 118 | >
119 | )} 120 | 121 |
122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /client/src/pages/form-design/FormDisplay/index.css: -------------------------------------------------------------------------------- 1 | .form-display .flex-row { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | } 6 | 7 | .form-display .flex-item { 8 | -webkit-box-flex: 1; 9 | -ms-flex: 1 1 0px; 10 | flex: 1 1 0; 11 | } 12 | 13 | .form-display .flex-item .ant-input-number { 14 | width: 100%; 15 | } 16 | /*# sourceMappingURL=index.css.map */ -------------------------------------------------------------------------------- /client/src/pages/form-design/FormDisplay/index.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AACA,AACI,aADS,CACT,SAAS,CAAC;EACN,OAAO,EAAE,IAAI;CACd;;AAHP,AAIM,aAJO,CAIP,UAAU,CAAC;EACT,IAAI,EAAE,KAAK;CAIZ;;AATP,AAMQ,aANK,CAIP,UAAU,CAER,iBAAiB,CAAA;EACf,KAAK,EAAC,IAAI;CACX", 4 | "sources": [ 5 | "index.scss" 6 | ], 7 | "names": [], 8 | "file": "index.css" 9 | } -------------------------------------------------------------------------------- /client/src/pages/form-design/FormDisplay/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Form } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | import './index.scss'; 5 | import FieldCorAttr from '../../../utils/field-cor-attr.js'; 6 | 7 | const FormItem = Form.Item; 8 | 9 | export default class FormDisplay extends PureComponent { 10 | getFields(item) { 11 | const { getFieldDecorator } = this.props.form; 12 | const { attrInfo, type } = item; 13 | const { name, titleValue, verifyValue } = attrInfo; 14 | return ( 15 | 16 | {getFieldDecorator(name, { 17 | rules: [ 18 | { 19 | required: verifyValue, 20 | message: '必填项!', 21 | }, 22 | ], 23 | })(FieldCorAttr[type].getReallyField(attrInfo))} 24 | 25 | ); 26 | } 27 | 28 | render() { 29 | const { fieldsData } = this.props; 30 | return ( 31 |
32 | {fieldsData.map((items, index) => { 33 | const { grid } = items.attrInfo; 34 | const { rowtem, coltem, cells } = grid; 35 | const GridStyle = { 36 | display: 'grid', 37 | gridTemplateRows: `${rowtem.join(' ')}`, 38 | gridTemplateColumns: `${coltem.join(' ')}`, 39 | }; 40 | return ( 41 |
42 | {cells.map((item, idx) => { 43 | const cellitem = item.item; 44 | const type = cellitem && cellitem.type; 45 | switch (type) { 46 | case 'grid': 47 | const { grid } = cellitem.attrInfo; 48 | const { rowtem, coltem, cells } = grid; 49 | const GridStyle = { 50 | display: 'grid', 51 | gridTemplateRows: `${rowtem.join(' ')}`, 52 | gridTemplateColumns: `${coltem.join(' ')}`, 53 | }; 54 | return ( 55 |
56 |
57 | {cells.map((item, ix) => { 58 | const cellitem = item.item; 59 | 60 | return ( 61 |
62 | {cellitem ? this.getFields(cellitem) : ''} 63 |
64 | ); 65 | })} 66 |
67 |
68 | ); 69 | default: 70 | return ( 71 |
72 | {cellitem ? this.getFields(item.item) : ''} 73 |
74 | ); 75 | } 76 | })} 77 |
78 | ); 79 | })} 80 |
81 | ); 82 | } 83 | } 84 | 85 | FormDisplay.propTypes = { 86 | form: PropTypes.object.isRequired, 87 | fieldsData: PropTypes.array.isRequired, 88 | formItemLayout: PropTypes.object, 89 | }; 90 | -------------------------------------------------------------------------------- /client/src/pages/form-design/FormDisplay/index.scss: -------------------------------------------------------------------------------- 1 | 2 | .form-display{ 3 | .flex-row { 4 | display: flex; 5 | } 6 | .flex-item { 7 | flex: 1 1 0; 8 | .ant-input-number{ 9 | width:100%; 10 | } 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /client/src/pages/form-design/containers/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import FormDesign from '../FormDesign'; 3 | import FormDisplay from '../FormDisplay'; 4 | 5 | FormDesign.FormDisplay = FormDisplay; 6 | export default FormDesign; 7 | -------------------------------------------------------------------------------- /client/src/pages/form-design/containers/index.scss: -------------------------------------------------------------------------------- 1 | body, 2 | p, 3 | html { 4 | font-size: 12px; 5 | color: #191f25; 6 | background: #f6f6f6; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | @font-face { 11 | font-family: Chinese Quote; 12 | src: local("PingFang SC"), local("SimSun"); 13 | unicode-range: u+2018, u+2019, u+201c, u+201d; 14 | } 15 | input::-ms-clear, 16 | input::-ms-reveal { 17 | display: none; 18 | } 19 | 20 | *, 21 | :after, 22 | :before { 23 | box-sizing: border-box; 24 | } 25 | 26 | html { 27 | font-family: sans-serif; 28 | line-height: 1.15; 29 | -webkit-text-size-adjust: 100%; 30 | -ms-text-size-adjust: 100%; 31 | -ms-overflow-style: scrollbar; 32 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 33 | } 34 | 35 | @-ms-viewport { 36 | width: device-width; 37 | } 38 | 39 | article, 40 | aside, 41 | dialog, 42 | figcaption, 43 | figure, 44 | footer, 45 | header, 46 | hgroup, 47 | main, 48 | nav, 49 | section { 50 | display: block; 51 | } 52 | 53 | input[type="number"], 54 | input[type="password"], 55 | input[type="text"], 56 | textarea { 57 | -webkit-appearance: none; 58 | } 59 | 60 | ::selection { 61 | background: #1890ff; 62 | color: #fff; 63 | } 64 | 65 | .clearfix { 66 | zoom: 1; 67 | } 68 | 69 | .clearfix:after, 70 | .clearfix:before { 71 | content: ""; 72 | display: table; 73 | } 74 | 75 | .clearfix:after { 76 | clear: both; 77 | } 78 | -------------------------------------------------------------------------------- /client/src/pages/form-design/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { Button, Form, Tabs } from "antd"; 3 | import {Link} from 'react-router-dom' 4 | import FormDesign from "./containers"; 5 | import "./index.scss"; 6 | const TabPane = Tabs.TabPane; 7 | const FormItem = Form.Item; 8 | const { FormDisplay } = FormDesign; 9 | 10 | class FormShow extends PureComponent { 11 | handleSubmit = e => { 12 | e.preventDefault(); 13 | this.props.form.validateFields((err, values) => { 14 | if (!err) { 15 | console.log("Received values of form: ", values); 16 | } 17 | }); 18 | }; 19 | 20 | render() { 21 | const { fieldsData, form } = this.props; 22 | return ( 23 |
28 | 40 | 41 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | const WrappedFormShow = Form.create({ 51 | mapPropsToFields(props) { 52 | console.log(props) 53 | let obj = {}; 54 | let { initValues } = props; 55 | if (initValues) { 56 | Object.keys(initValues).forEach(key => { 57 | obj[key] = Form.createFormField({ 58 | value: initValues[key] 59 | }); 60 | }); 61 | } 62 | return obj; 63 | } 64 | })(FormShow); 65 | export default class extends PureComponent { 66 | state = { fieldsData: [] }; 67 | save = data => { 68 | this.setState({ fieldsData: data },()=>{ 69 | console.log(this.state.fieldsData) 70 | }); 71 | }; 72 | onSubmit = values => { 73 | console.log(values); 74 | }; 75 | submit = () => { 76 | this.props.form.validateFields((err, values) => { 77 | if (!err) { 78 | const onSubmit = this.props; 79 | if (onSubmit) { 80 | onSubmit(values); 81 | } 82 | } 83 | }); 84 | }; 85 | render() { 86 | return ( 87 | 88 | 89 | 90 | 91 | 92 |
93 | 94 |
95 | {/* 预览 */} 96 |
97 | 98 | 102 | 103 |
104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/src/pages/form-design/index.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~antd/dist/antd.min.css'; 3 | .ant-tabs{ 4 | overflow-y: auto; 5 | } 6 | 7 | body{ 8 | background-color: #f6f6f6; 9 | } 10 | .form-display .ant-form-item { 11 | display: flex; 12 | margin-bottom: 16px; 13 | } 14 | .form-display .ant-form-item-label { 15 | text-overflow: ellipsis; 16 | } 17 | .form-display .ant-form-item-control-wrapper { 18 | flex: 1 1 auto; 19 | } 20 | -------------------------------------------------------------------------------- /client/src/pages/github/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Card, Col, Row } from 'antd'; 3 | import LinkA from 'components/link-a' 4 | export default class extends Component { 5 | state = { } 6 | params={ 7 | href:'https://github.com/Composur/react-manage', 8 | target:'_black', 9 | text:'源码' 10 | } 11 | chatHref={ 12 | href:'https://github.com/Composur/react-practice/tree/master/react-chat', 13 | target:'_black', 14 | text:'源码' 15 | } 16 | blogHref={ 17 | href:'https://github.com/Composur/vue-project/tree/master/vue-blog2', 18 | target:'_black', 19 | text:'源码' 20 | } 21 | render() { 22 | return ( 23 |
24 | 25 | 26 | }> 27 |

* 包括前端 PC 应用和后端应用,包括用户管理 / 商品分类管理 / 商品管理 / 权限管理等功能模块

28 |

* 前端: 使用 React 全家桶 + Antd + Axios + ES6 + Webpack 等技术

29 |

* 后端: 使用 Node + Express + Mongodb 等技术

30 |
31 | 32 | 33 | }> 34 |

* 包括前端应用和后端应用注册,包括用户注册/登陆, 管理员/普通用户列表, 实时聊天,消息等模块

35 |

* 前端: 使用 React 全家桶+ES6+Webpack 等技术

36 |

* 后端: 使用 Node + express + mongodb + socketIO 等技术

37 |
38 | 39 | 40 | }> 41 |

前端加后台

42 |
43 | 44 |
45 |
46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /client/src/pages/home/bar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { 3 | Chart, 4 | Geom, 5 | Axis, 6 | Tooltip, 7 | } from "bizcharts" 8 | 9 | export default class Bar extends React.Component { 10 | render() { 11 | const data = [ 12 | { 13 | year: "1月", 14 | sales: 38 15 | }, 16 | { 17 | year: "2月", 18 | sales: 52 19 | }, 20 | { 21 | year: "3月", 22 | sales: 61 23 | }, 24 | { 25 | year: "4月", 26 | sales: 145 27 | }, 28 | { 29 | year: "5月", 30 | sales: 48 31 | }, 32 | { 33 | year: "6月", 34 | sales: 38 35 | }, 36 | { 37 | year: "7月", 38 | sales: 28 39 | }, 40 | { 41 | year: "8月", 42 | sales: 38 43 | }, 44 | { 45 | year: "59月", 46 | sales: 68 47 | }, 48 | { 49 | year: "10月", 50 | sales: 38 51 | }, 52 | { 53 | year: "11月", 54 | sales: 58 55 | }, 56 | { 57 | year: "12月", 58 | sales: 38 59 | } 60 | ] 61 | const cols = { 62 | sales: { 63 | tickInterval: 20 64 | } 65 | } 66 | return ( 67 |
68 | 69 | 70 | 71 | 76 | 77 | 78 |
79 | ) 80 | } 81 | } -------------------------------------------------------------------------------- /client/src/pages/home/home.css: -------------------------------------------------------------------------------- 1 | .home { 2 | padding: 24px; 3 | background: #fff; 4 | min-height: 850px; 5 | } 6 | .home .home-card { 7 | float: left; 8 | } 9 | .home .home-content { 10 | position: absolute; 11 | top: 420px; 12 | width: 76%; 13 | border: 1px solid #e8e8e8; 14 | } 15 | .home .home-content .home-menu { 16 | font-size: 20px; 17 | } 18 | .home .home-content .home-menu span { 19 | cursor: pointer; 20 | } 21 | .home .home-content .home-menu .home-menu-active { 22 | border-bottom: 2px solid; 23 | padding: 0 0 16px 0; 24 | } 25 | .home .home-content .home-menu .home-menu-visited { 26 | margin-right: 40px; 27 | } 28 | .home .home-content .home-table-left { 29 | float: left; 30 | width: 60%; 31 | } 32 | .home .home-content .home-table-right { 33 | float: right; 34 | width: 330px; 35 | } 36 | -------------------------------------------------------------------------------- /client/src/pages/home/home.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import { 3 | Icon, 4 | Card, 5 | Statistic, 6 | DatePicker, 7 | Timeline, 8 | } from 'antd' 9 | import moment from 'moment' 10 | 11 | import Line from './line' 12 | import Bar from './bar' 13 | import './home.less' 14 | 15 | const dateFormat = 'YYYY/MM/DD' 16 | const {RangePicker} = DatePicker 17 | 18 | export default class Home extends Component { 19 | 20 | state = { 21 | isVisited: true 22 | } 23 | 24 | handleChange = (isVisited) => { 25 | return () => this.setState({isVisited}) 26 | } 27 | componentDidMount(){ 28 | } 29 | render() { 30 | const {isVisited} = this.state 31 | 32 | return ( 33 |
34 | } 38 | style={{width: 250}} 39 | headStyle={{color: 'rgba(0,0,0,.45)'}} 40 | > 41 | 46 | %
} 51 | /> 52 | %
} 57 | /> 58 | 59 | 60 | 61 | 62 | 65 | 访问量 67 | 销售量 68 |
} 69 | extra={} 73 | > 74 | } 79 | > 80 | 81 | 82 | 83 | } className="home-table-right"> 84 | 85 | 新版本迭代会 86 | 完成网站设计初版 87 | 88 |

联调接口

89 |

功能验收

90 |
91 | 92 |

登录功能设计

93 |

权限验证

94 |

页面排版

95 |
96 |
97 |
98 | 99 |
100 | ) 101 | } 102 | } -------------------------------------------------------------------------------- /client/src/pages/home/home.less: -------------------------------------------------------------------------------- 1 | .home { 2 | padding: 24px; 3 | background: #fff; 4 | min-height: 850px; 5 | .home-card { 6 | float: left; 7 | } 8 | .home-content { 9 | position: absolute; 10 | top: 420px; 11 | width: 76%; 12 | border: 1px solid #e8e8e8; 13 | .home-menu { 14 | font-size: 20px; 15 | span { 16 | cursor: pointer; 17 | } 18 | .home-menu-active { 19 | border-bottom: 2px solid; 20 | padding: 0 0 16px 0; 21 | } 22 | .home-menu-visited { 23 | margin-right: 40px; 24 | } 25 | } 26 | .home-table-left { 27 | float: left; 28 | width: 60%; 29 | } 30 | .home-table-right { 31 | float: right; 32 | width: 330px; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/pages/home/line.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { 3 | Chart, 4 | Geom, 5 | Axis, 6 | Tooltip, 7 | Legend, 8 | } from "bizcharts" 9 | import DataSet from "@antv/data-set" 10 | 11 | export default class Line extends React.Component { 12 | render() { 13 | const data = [ 14 | { 15 | month: "Jan", 16 | a: 7.0, 17 | b: 3.9, 18 | c: 5.9 19 | }, 20 | { 21 | month: "Feb", 22 | a: 6.9, 23 | b: 4.2, 24 | c: 1.9 25 | }, 26 | { 27 | month: "Mar", 28 | a: 9.5, 29 | b: 5.7, 30 | c: 3.9 31 | }, 32 | { 33 | month: "Apr", 34 | a: 14.5, 35 | b: 8.5, 36 | c: 5.5 37 | }, 38 | { 39 | month: "May", 40 | a: 18.4, 41 | b: 11.9, 42 | c: 8.9 43 | }, 44 | { 45 | month: "Jun", 46 | a: 21.5, 47 | b: 15.2, 48 | c: 10.0 49 | }, 50 | { 51 | month: "Jul", 52 | a: 25.2, 53 | b: 17.0, 54 | c: 12.9 55 | }, 56 | { 57 | month: "Aug", 58 | a: 26.5, 59 | b: 16.6, 60 | c: 15.9 61 | }, 62 | { 63 | month: "Sep", 64 | a: 23.3, 65 | b: 14.2, 66 | c: 20.7 67 | }, 68 | { 69 | month: "Oct", 70 | a: 18.3, 71 | b: 10.3, 72 | c: 25.9 73 | }, 74 | { 75 | month: "Nov", 76 | a: 13.9, 77 | b: 6.6, 78 | c: 30.9 79 | }, 80 | { 81 | month: "Dec", 82 | a: 9.6, 83 | b: 4.8, 84 | c: 35.9 85 | } 86 | ] 87 | const ds = new DataSet() 88 | const dv = ds.createView().source(data) 89 | dv.transform({ 90 | type: "fold", 91 | fields: ["a", "b", "c"], 92 | // 展开字段集 93 | key: "city", 94 | // key字段 95 | value: "temperature" // value字段 96 | }) 97 | const cols = { 98 | month: { 99 | range: [0, 1] 100 | } 101 | } 102 | return ( 103 |
104 | 105 | 106 | 107 | `${val}万个` 111 | }} 112 | /> 113 | 118 | 125 | 136 | 137 |
138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /client/src/pages/hooks/calculate-hash.js: -------------------------------------------------------------------------------- 1 | // 根据传进来的文件切片进行 hash 值的计算 2 | export const calculateHash = (fileChunkList, setHashPercentage) => new Promise((resolve) => { 3 | const worker = new Worker('/hash.js'); 4 | worker.postMessage({ fileChunkList }); 5 | worker.onmessage = (e) => { 6 | const { percentage, hash } = e.data; 7 | setHashPercentage(percentage); // 计算总文件的 hash 8 | if (hash) { 9 | resolve(hash); // 每个切片的 hash 10 | } 11 | }; 12 | }); 13 | 14 | // 文件切片 15 | export const createFileChunk = (file, size) => { 16 | const fileChunkList = []; 17 | let cur = 0; 18 | while (cur < file.size) { 19 | fileChunkList.push({ file: file.slice(cur, cur + size) }); 20 | cur += size; 21 | } 22 | return fileChunkList; 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/pages/hooks/example.js: -------------------------------------------------------------------------------- 1 | import React,{useState,useEffect,memo, useRef} from 'react' 2 | class ExampleComponent extends React.Component { 3 | 4 | // 用于初始化 state 5 | constructor() {} 6 | useState() 7 | 8 | // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用 9 | // 因为该函数是静态函数,所以取不到 `this` 10 | // 如果需要对比 `prevProps` 需要单独在 `state` 中维护 11 | static getDerivedStateFromProps(nextProps, prevState) {} 12 | useState() 13 | 14 | static getDerivedStateFromError(){} 15 | // Hooks 暂未实现组件错误处理的功能 16 | 17 | // 判断是否需要更新组件,多用于组件性能优化 18 | shouldComponentUpdate(nextProps, nextState) {} 19 | memo() // memo 用于函数组件和 Hooks 关系不大 20 | 21 | // 组件挂载后调用 22 | // 可以在该函数中进行请求或者订阅 23 | componentDidMount() {} 24 | useEffect(()=>{ 25 | componentDidMount() {} 26 | return ()=>{ 27 | componentWillUnmount() {} 28 | } 29 | },[]) 30 | 31 | // 用于获得最新的 DOM 数据 32 | getSnapshotBeforeUpdate() {} 33 | useEffect() 结合 useRef() 用 useRef() 保存上传的状态 34 | 35 | // 组件即将销毁 36 | // 可以在此处移除订阅,定时器等等 37 | componentWillUnmount() {} 38 | useEffect() 39 | 40 | // 组件销毁后调用 41 | componentDidUnMount() {} 42 | 43 | // 组件更新后调用 44 | componentDidUpdate() {} 45 | useEffect() 46 | 47 | // 渲染组件函数 48 | render() {} 49 | 50 | 51 | 52 | // 以下函数不建议使用 53 | UNSAFE_componentWillMount() {} 54 | UNSAFE_componentWillUpdate(nextProps, nextState) {} 55 | UNSAFE_componentWillReceiveProps(nextProps) {} 56 | } -------------------------------------------------------------------------------- /client/src/pages/login/action-type.js: -------------------------------------------------------------------------------- 1 | export const LOGIN_USER_INFO = 'login_user_info'; 2 | export const LOG_OUT = 'log_out'; 3 | -------------------------------------------------------------------------------- /client/src/pages/login/action.js: -------------------------------------------------------------------------------- 1 | 2 | import { reqLogin } from 'api/index'; 3 | import { message } from 'antd'; 4 | import { LOGIN_USER_INFO } from './action-type'; 5 | import store from '../../utils/storeUtils'; 6 | 7 | export const getLoginUserInfo = (loginInfo) => async (dispatch, getState) => { 8 | // console.log(getState()) 9 | const res = await reqLogin(loginInfo); 10 | if (res.status === 0) { 11 | message.success('登录成功!'); 12 | // 分发一个同步action 13 | dispatch( 14 | { 15 | type: LOGIN_USER_INFO, 16 | data: res.data, 17 | }, 18 | ); 19 | store.set('user_key', res.data); // 全局存储 20 | store.set('token', res.token); // 全局存储 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/pages/login/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/pages/login/images/bg.png -------------------------------------------------------------------------------- /client/src/pages/login/login.css: -------------------------------------------------------------------------------- 1 | .login { 2 | width: 100%; 3 | height: 100%; 4 | background-image: url('images/bg.png'); 5 | background-size: cover; 6 | } 7 | .login .header { 8 | width: 100%; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | position: relative; 13 | height: 64px; 14 | padding: 0; 15 | opacity: 0.3; 16 | background-color: transparent; 17 | } 18 | .login .content { 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | margin-top: 30px; 23 | } 24 | .login .content .login-form { 25 | width: 500px; 26 | height: 300px; 27 | padding: 30px; 28 | background-color: #fff; 29 | } 30 | .login .content .login-form .login-label { 31 | font-size: 20px; 32 | font-weight: bold; 33 | text-align: center; 34 | } 35 | .login .content .login-form .login-btn { 36 | width: 100%; 37 | } 38 | .login .footer { 39 | position: fixed; 40 | bottom: 0; 41 | color: #fff; 42 | left: 0; 43 | right: 0; 44 | } 45 | .login .footer a { 46 | color: #fff; 47 | } 48 | -------------------------------------------------------------------------------- /client/src/pages/login/login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Icon, Input, Button} from 'antd'; 3 | import {Redirect} from 'react-router-dom' 4 | import {connect} from 'react-redux' 5 | import {getLoginUserInfo} from './action' 6 | import LinkA from '../../components/link-a' 7 | import './login.less' 8 | class Login extends Component { 9 | state={ 10 | params:{ 11 | href:'http://beian.miit.gov.cn/', 12 | target:'_black', 13 | text:'粤ICP备19121998号' 14 | } 15 | } 16 | handleSubmit = e => { 17 | e.preventDefault(); 18 | this.props.form.validateFields( 19 | async (err, values) => { //可以对所有结果校验,并返回结果 20 | if (!err) { 21 | values=Object.assign(values,{username:btoa(values.username),password:btoa(values.password)}) 22 | this.props.getLoginUserInfo(values) 23 | } 24 | }); 25 | }; 26 | // 自定义密码验证规则 27 | validatorPwd=(rule, value, callback)=>{ 28 | // 无论验证成功与否callback()必须调用 29 | if(!value){ 30 | callback('请输入密码!') 31 | }else if(value.length<4||value.length>12){ 32 | callback('密码长度应大于4小于12位!') //验证不通过传入错误提示 33 | } 34 | callback()//验证成功无提示 35 | } 36 | render() { 37 | // 如果检测到登录信息自动登录 38 | const {userInfo} = this.props 39 | if(userInfo._id){ 40 | return 41 | } 42 | const { getFieldDecorator } = this.props.form; 43 | return ( 44 |
45 |
46 |
47 |
48 |
49 |
管理平台
50 |
51 | 52 | {getFieldDecorator('username', { 53 | rules: [ 54 | { required: true,message: '请输入用户名!',min:4}, 55 | { message: '用户名长度应大于4小于12位!',min:4,max:12}, 56 | { message: '用户名只能含有数字、英文、下划线!',pattern:/^[a-zA-Z0-9_]+$/ },//+号可以匹配任意多个字符 57 | ], 58 | initialValue: 'admin', // 初始值 59 | })( 60 | } 62 | placeholder="用户名" 63 | />, 64 | )} 65 | 66 | 67 | {getFieldDecorator('password', { 68 | rules: [ 69 | { validator:this.validatorPwd},//自定义校验规则 70 | ], 71 | initialValue: 'admin', // 初始值 72 | })( 73 | } 75 | type="password" 76 | placeholder="密码" 77 | />, 78 | )} 79 | 80 | 81 | 84 | 85 |
86 |
87 |
88 |
89 |
90 | Made with ❤ by XT  91 |
92 |
93 |
94 | ); 95 | } 96 | } 97 | 98 | const WrappeLoginForm = Form.create({ name: 'normal_login' })(Login); 99 | 100 | 101 | const mapStateToProps=(state)=>({ 102 | userInfo:state.loginUserInfo 103 | }) 104 | 105 | const mapDispatchToProps={getLoginUserInfo} 106 | 107 | export default connect(mapStateToProps,mapDispatchToProps)(WrappeLoginForm) 108 | -------------------------------------------------------------------------------- /client/src/pages/login/login.less: -------------------------------------------------------------------------------- 1 | .login{ 2 | width: 100%; 3 | height: 100%; 4 | background-image: url('./images/bg.png'); 5 | background-size: cover; 6 | .header{ 7 | width: 100%; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | position: relative; 12 | height: 64px; 13 | padding: 0; 14 | opacity: 0.3; 15 | background-color: transparent; 16 | // box-shadow: 0 1px 4px rgba(0,21,41,.08); 17 | } 18 | .content{ 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | margin-top: 30px; 23 | .login-form{ 24 | width: 500px; 25 | height: 300px; 26 | padding: 30px; 27 | background-color: #fff; 28 | .login-label{ 29 | font-size: 20px; 30 | font-weight: bold; 31 | text-align: center; 32 | } 33 | .login-btn{ 34 | width: 100%; 35 | } 36 | } 37 | } 38 | .footer{ 39 | position: fixed; 40 | bottom: 0; 41 | color: #fff; 42 | left: 0; 43 | right: 0; 44 | a{ 45 | color: #fff; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /client/src/pages/login/reducer.js: -------------------------------------------------------------------------------- 1 | import store from 'store'; 2 | import { LOGIN_USER_INFO, LOG_OUT } from './action-type'; 3 | 4 | const user = store.get('user_key') || {}; 5 | export default (state = user, action) => { 6 | switch (action.type) { 7 | case LOGIN_USER_INFO: 8 | return action.data; 9 | case LOG_OUT: 10 | return {}; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /client/src/pages/order/ calendar.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import moment from 'moment'; 4 | import { Calendar, Badge,Alert, } from 'antd'; 5 | 6 | // 提醒事件数据集合 7 | function getListData(value) { 8 | let listData; 9 | switch (value.date()) { 10 | case 8: 11 | listData = [ 12 | { type: 'success', content: '生日!' }, 13 | ]; 14 | break; 15 | case 20: 16 | listData = [ 17 | { type: 'warning', content: '打卡!' }, 18 | ]; 19 | break; 20 | default: 21 | } 22 | return listData || []; 23 | } 24 | 25 | // 渲染提醒事件 26 | function dateCellRender(value) { 27 | const listData = getListData(value); 28 | return ( 29 |
    30 | {listData.map(item => ( 31 |
  • 32 | 33 |
  • 34 | ))} 35 |
36 | ); 37 | } 38 | 39 | function getMonthData(value) { 40 | if (value.month() === 8) { 41 | return '追加的内容'; 42 | } 43 | } 44 | 45 | // 自定义渲染月单元格,返回内容会被追加到单元格 46 | function monthCellRender(value) { 47 | const num = getMonthData(value); 48 | return num ? ( 49 |
50 |
{num}
51 | 追加的内容 52 |
53 | ) : null; 54 | } 55 | export default class extends Component { 56 | state = { 57 | value: moment(), 58 | selectedValue: moment(), 59 | }; 60 | onSelect = value => { 61 | this.setState({ 62 | value, 63 | selectedValue: value, 64 | }); 65 | }; 66 | 67 | onPanelChange = value => { 68 | this.setState({ value }); 69 | }; 70 | componentDidMount(){ 71 | console.log('子组件') 72 | } 73 | render() { 74 | const { value, selectedValue } = this.state; 75 | return ( 76 |
77 | 80 | 84 |
85 | ); 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /client/src/pages/order/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Card } from 'antd'; 3 | import Calendar from './ calendar' 4 | const tabList = [ 5 | { 6 | key: 'tab1', 7 | tab: '日历', 8 | }, 9 | { 10 | key: 'tab2', 11 | tab: 'tab2', 12 | }, 13 | ]; 14 | 15 | const contentList = { 16 | tab1: , 17 | tab2:

content2

, 18 | }; 19 | 20 | export default class order extends Component { 21 | state = { 22 | key: 'tab1', 23 | noTitleKey: 'app', 24 | }; 25 | 26 | onTabChange = (key, type) => { 27 | this.setState({ [type]: key }); 28 | }; 29 | componentDidMount(){ 30 | console.log('父') 31 | } 32 | render() { 33 | return ( 34 | { 41 | this.onTabChange(key, 'key'); 42 | }} 43 | > 44 | {contentList[this.state.key]} 45 | 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /client/src/pages/product/detail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {withRouter} from 'react-router-dom' 3 | import {Card,List,Icon,Typography} from 'antd' 4 | const { Text } = Typography; 5 | const listStyle={fontSize:15,marginRight:'1rem'} 6 | class ProductDetail extends Component { 7 | state = { } 8 | constructor(){ 9 | super() 10 | this.title=( 11 | 12 | ) 13 | } 14 | goBack=(e)=>{ 15 | this.props.history.goBack() 16 | } 17 | render() { 18 | const data=this.props.location.state //路由传进来的数据 19 | const listTitle=[ 20 | '商品名称','商品描述','商品价格','所属分类','商品图片','商品详情' 21 | ] 22 | return ( 23 | 24 | 商品详情
} 26 | bordered 27 | // dataSource={data} 28 | // renderItem={(item,index) => ( 29 | // 30 | // {listTitle[index]}:{item} 31 | // 32 | // )} 33 | > 34 | 35 | {listTitle[0]}:{data.name} 36 | 37 | 38 | {listTitle[1]}:{data.desc} 39 | 40 | 41 | {listTitle[2]}:{data.price} 42 | 43 | 44 | {listTitle[3]}:{data.categoryId}-{data.pCategoryId} 45 | 46 | 47 | {listTitle[4]}:{'图片'} 48 | 49 | 50 | {listTitle[5]}:{data.detail} 51 | 52 | 53 | 54 | ); 55 | } 56 | } 57 | export default withRouter(ProductDetail) -------------------------------------------------------------------------------- /client/src/pages/product/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Switch,Route,Redirect} from 'react-router-dom' 3 | import Product from './product' 4 | import ProductAdd from './add' 5 | import ProductDetail from './detail' 6 | export default class extends Component { 7 | state = { } 8 | render() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | } -------------------------------------------------------------------------------- /client/src/pages/product/richTextEdit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { EditorState, convertToRaw, ContentState } from 'draft-js'; 3 | import { Editor } from 'react-draft-wysiwyg'; 4 | import draftToHtml from 'draftjs-to-html'; 5 | import htmlToDraft from 'html-to-draftjs'; 6 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; 7 | /** 8 | * @description 富文本编辑器 9 | */ 10 | export default class richTextEdit extends Component { 11 | state = { 12 | editorState: EditorState.createEmpty() 13 | }; 14 | constructor(props){ 15 | super(props) 16 | const html = this.props.preDetail 17 | if(!html){return} 18 | const contentBlock = htmlToDraft(html); 19 | if (contentBlock) { 20 | const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); 21 | const editorState = EditorState.createWithContent(contentState); 22 | this.state = { 23 | editorState, 24 | }; 25 | } 26 | } 27 | onEditorStateChange = (editorState) => { 28 | this.setState({ 29 | editorState, 30 | }); 31 | }; 32 | getInputData=()=>{ 33 | const { editorState } = this.state; 34 | return draftToHtml(convertToRaw(editorState.getCurrentContent())) 35 | } 36 | uploadImageCallBack = (file)=> { 37 | return new Promise( 38 | (resolve, reject) => { 39 | const xhr = new XMLHttpRequest(); 40 | xhr.open('POST', '/manage/img/upload'); 41 | // xhr.setRequestHeader('Authorization', 'Client-ID XXXXX'); 42 | const data = new FormData(); 43 | data.append('image', file); 44 | xhr.send(data); 45 | xhr.addEventListener('load', () => { 46 | const response = JSON.parse(xhr.responseText); 47 | const {status,data}=response 48 | if(status===0){ 49 | resolve({data:{link:data.url}}); 50 | } 51 | }); 52 | xhr.addEventListener('error', () => { 53 | const error = JSON.parse(xhr.responseText); 54 | reject(error); 55 | }); 56 | } 57 | ); 58 | } 59 | render() { 60 | const { editorState } = this.state; 61 | return ( 62 |
63 | 76 |
77 | ); 78 | } 79 | } -------------------------------------------------------------------------------- /client/src/pages/product/upload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Upload, Icon, Modal,message } from 'antd'; 3 | import {reqDelUploadImg} from 'api' 4 | import {imgUrl} from 'config' 5 | 6 | function getBase64(file) { 7 | return new Promise((resolve, reject) => { 8 | const reader = new FileReader(); 9 | reader.readAsDataURL(file); 10 | reader.onload = () => resolve(reader.result); 11 | reader.onerror = error => reject(error); 12 | }); 13 | } 14 | 15 | export default class PicturesWall extends React.Component { 16 | state = { 17 | previewVisible: false, 18 | previewImage: '', 19 | fileList: [ 20 | // { 21 | // uid: '-1', 22 | // name: 'image.png', 23 | // status: 'done', 24 | // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', 25 | // }, 26 | ], 27 | }; 28 | constructor(props){ 29 | super(props) 30 | const {imgSrc=[]} = this.props 31 | if(imgSrc.length){ 32 | const fileList = imgSrc.map((img,index)=>{ 33 | return { 34 | uid: -index, 35 | name: img, 36 | status: 'done', 37 | url: imgUrl+img, 38 | } 39 | }) 40 | this.state={ 41 | fileList 42 | } 43 | } 44 | } 45 | 46 | handleCancel = () => this.setState({ previewVisible: false }); 47 | 48 | handlePreview = async file => { 49 | if (!file.url && !file.preview) { 50 | file.preview = await getBase64(file.originFileObj); 51 | } 52 | this.setState({ 53 | previewImage: file.url || file.preview, 54 | previewVisible: true, 55 | }); 56 | }; 57 | getImgs=()=>{ 58 | return this.state.fileList.map(file=>file.name) 59 | } 60 | /** 61 | * @param file 当前操作的文件对象。file.response为当前请求的返回 62 | * @param fileList 当前的文件列表。 63 | * @param event 上传中的服务端响应内容,包含了上传进度等信息,高级浏览器支持。 64 | */ 65 | handleChange = async ({ file,fileList,event }) => { //上传中、完成、失败都会调用这个函数。 66 | const {response,status} = file 67 | // 上传成功 68 | if (status === 'done') { 69 | const {data} = response 70 | const {name,url}=data 71 | const currentFile = fileList[fileList.length - 1] 72 | currentFile.name = name 73 | currentFile.url = url 74 | } else if (status === 'removed') { 75 | const res = await reqDelUploadImg({name:file.name}) 76 | if(res.status===0){ 77 | message.success('删除成功!') 78 | } 79 | } 80 | this.setState({ fileList }) 81 | }; 82 | render() { 83 | const { previewVisible, previewImage, fileList } = this.state; 84 | const uploadButton = ( 85 |
86 | 87 |
图片上传
88 |
89 | ); 90 | return ( 91 |
92 | 103 | {fileList.length >= 4 ? null : uploadButton} 104 | 105 | 106 | upload_img 107 | 108 |
109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /client/src/pages/role/auth.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {Modal,Tree,Input,Form,Icon,message} from 'antd' 4 | import store from '../../utils/storeUtils' 5 | import {reqSettingRole} from 'api' 6 | import menuList from 'config/menuConfig' 7 | const iconStyle={ color: 'rgba(0,0,0,.25)' } 8 | const { TreeNode } = Tree; 9 | class Auth extends Component { 10 | state = { 11 | visible:false, 12 | confirmLoading:false, 13 | menus:[],//路由权限 14 | } 15 | showModal=()=>{ 16 | this.setState({ 17 | visible:true 18 | }) 19 | } 20 | // 设置权限 21 | handelAddUser= async ()=>{ 22 | this.setState({ 23 | confirmLoading:true 24 | }) 25 | const {role}= this.props 26 | role.menus=this.state.menus //自动更新了父组件的状态,改变了props的状态 27 | const {username,role_id} = store.get('user_key') 28 | role.auth_name = username 29 | const res = await reqSettingRole(role) 30 | if(res){ 31 | message.success('更新成功!') 32 | // 如果用户更新的是自己的权限,重新登录 33 | if(role._id===role_id){ 34 | store.user=null 35 | store.clearAll() 36 | // 在这里还需要reset redux 上的数据 37 | // this.props.logout() 38 | this.props.history.replace('/login') 39 | }else { 40 | this.setState({ 41 | confirmLoading:false, 42 | visible:false, 43 | }) 44 | this.props.getUserList() 45 | } 46 | } 47 | } 48 | handleCancel=()=>{ 49 | this.setState({ 50 | visible:false, 51 | confirmLoading:false, 52 | menus:this.props.role.menus 53 | }) 54 | } 55 | 56 | /** 57 | * @description menus 得到checkbox选中的集合 58 | * @description info 点击事件 59 | */ 60 | // 更新了menus 61 | onCheck = (menus, info) => { 62 | this.setState({menus}) 63 | }; 64 | // 权限控件渲染 65 | treeNodeRender=(menuList)=>( 66 | menuList.map(item=>{ 67 | return ( 68 | 69 | { item.children?this.treeNodeRender(item.children):null} 70 | 71 | ) 72 | }) 73 | ) 74 | componentDidMount() { 75 | this.treeNodes=this.treeNodeRender(menuList) 76 | } 77 | // 这个方法已经不建议使用 78 | UNSAFE_componentWillReceiveProps(next){ 79 | this.setState({ 80 | menus:next.role.menus 81 | }) 82 | } 83 | // shouldComponentUpdate(nextProps,nextState){ 84 | // // 比较新旧props和state的数据 85 | // // if( this.props.role===nextProps.role || this.state.menus===nextState.menus){ 86 | // // return false 87 | // // } 88 | // return true 89 | // } 90 | render() { 91 | // 每次需要拿到最新的role 92 | const {role} = this.props 93 | const {visible,confirmLoading,menus} = this.state 94 | return ( 95 | 106 | 107 | } disabled value={role.name}/> 108 | 109 | 116 | 117 | {this.treeNodes } 118 | 119 | 120 | 121 | ); 122 | } 123 | } 124 | Auth.propTypes={ 125 | role:PropTypes.object.isRequired, 126 | } 127 | 128 | export default Auth -------------------------------------------------------------------------------- /client/src/pages/role/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/client/src/pages/role/index.js -------------------------------------------------------------------------------- /client/src/redux/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import ReduxThunk from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | // 生成环境配置 5 | // import { devToolsEnhancer } from 'redux-devtools-extension/logOnlyInProduction'; 6 | import appReducer from './reducer'; 7 | 8 | /** 9 | * @description createStore(reducer,initState,applyMiddle) 10 | * @description 如果没有initState 默认第二项是中间件函数 11 | */ 12 | 13 | // const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 14 | // export default createStore(reducer, /* preloadedState, */ composeEnhancers(applyMiddleware(ReduxThunk))) 15 | 16 | 17 | /** 18 | * @description 可以编写中间件增强dispatch的功能,例如redux-thunk等 19 | * @description 也可以编写 Store Enhancer 增强store的功能 20 | */ 21 | 22 | // 增强dispatch的功能实现日志打印,返回了一个增强的store 23 | // const doNothingEnhancer = (createStore) => (reducer, initStore, enhancer) => { 24 | // const store = createStore(reducer, initStore, enhancer) 25 | // const enhancerDispatch = store.dispatch; 26 | // store.dispatch = (action) => { 27 | // console.log('dispatch action', action) 28 | // enhancerDispatch(action) 29 | // } 30 | // return store 31 | // } 32 | // export default doNothingEnhancer(createStore)(reducer,{},composeWithDevTools(applyMiddleware(ReduxThunk))) 33 | 34 | export default createStore(appReducer, process.env.NODE_ENV === 'development' ? composeWithDevTools(applyMiddleware(ReduxThunk)) : (applyMiddleware(ReduxThunk))); 35 | -------------------------------------------------------------------------------- /client/src/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import getHeadTitle from 'components/left-nav/reducer'; 3 | import loginUserInfo from '../pages/login/reducer'; 4 | 5 | /** 6 | * @description 接收多个reducer合并成一个reducer 7 | * @default 接收的 reducer 返回的 state对象不能是 undefined 8 | */ 9 | export default combineReducers({ 10 | getHeadTitle, 11 | loginUserInfo, 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/utils/DragDropService.js: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from "rxjs"; 2 | 3 | export class DragDropService { 4 | _dragData = new BehaviorSubject(); 5 | setDragData(data){ 6 | this._dragData.next(data); 7 | } 8 | getDragData(){ 9 | return this._dragData.asObservable(); 10 | } 11 | clearDragData(){ 12 | this._dragData.next(null); 13 | } 14 | } 15 | export default new DragDropService() -------------------------------------------------------------------------------- /client/src/utils/axios.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 封装axios 3 | */ 4 | // import React from 'react'; 5 | import { message } from 'antd'; 6 | import store from 'store'; 7 | import axios from 'axios'; 8 | import config from '../config'; 9 | import { refreshToken } from '../api'; 10 | // import {Redirect} from 'react-router-dom' 11 | 12 | // 把 Token 存在localStroage,每次请求在 Axios 请求头上进行携带 13 | // function redirect(){ 14 | // return () 15 | // } 16 | 17 | // 请求拦截 18 | axios.interceptors.request.use(); 19 | 20 | // 响应拦截 21 | axios.interceptors.response.use( 22 | (response) => { 23 | const { data } = response; 24 | console.log(data); 25 | return response; 26 | }, 27 | (error) => { 28 | if (error.response) { 29 | switch (error.response.status) { 30 | case 303: 31 | message.error(`请求出错${error.response.msg}`); 32 | break; 33 | case 401: 34 | // console.log(error) 35 | break; 36 | default: 37 | return; 38 | } 39 | } 40 | return Promise.reject(error.response); 41 | }, 42 | ); 43 | 44 | export default function (url, type = 'GET', data = {}) { 45 | axios.defaults.headers.common.Authorization = store.get('token'); 46 | let promise; 47 | url = config.baseURl + url; 48 | // 返回一个promise,统一处理错误 49 | return new Promise((resolve, reject) => { 50 | // 1.执行异步请求 51 | if (type === 'GET') { 52 | let paramStr = ''; 53 | Object.keys(data).forEach((key) => { 54 | paramStr += `${key}=${data[key]}&`; 55 | }); 56 | if (paramStr) { 57 | paramStr = paramStr.substring(0, paramStr.length - 1); 58 | } 59 | promise = axios.get(`${url}?${paramStr}&t=${new Date()}`); 60 | } else { 61 | promise = axios.post(url, data); 62 | } 63 | promise.then((res) => { 64 | // 2.成功调用resolve 65 | if (res.data && res.data.status === 0) { 66 | resolve(res.data); 67 | } else { 68 | message.error(res.data.msg); 69 | resolve(res.data); 70 | } 71 | }).catch((err) => { 72 | const { data } = err; 73 | const { status } = data; 74 | debugger; 75 | if (status === 1) { 76 | // refreshToken().then(data=>{ 77 | // console.log(data) 78 | // }) 79 | } 80 | if (data && data.msg) { 81 | message.error(`请求出错${data.msg}`); 82 | // window.location.href = '/' 83 | store.clearAll(); 84 | window.location.href = '/login'; 85 | return; 86 | } 87 | // 3.失败调用reject,但是不能调用,调用就进入外层catch里了,为了不在外层用try...catch这里显式的返回error 88 | message.error(`请求出错${err.message}`); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /client/src/utils/field-images.js: -------------------------------------------------------------------------------- 1 | import img_textfield from '../images/textfield.png'; 2 | import img_textareafield from '../images/textareafield.png'; 3 | import img_date from '../images/date.png'; 4 | import img_datesection from '../images/datesection.png'; 5 | import img_formhidden from '../images/formhidden.png'; 6 | import img_attachment from '../images/attachment.png'; 7 | import img_angledown from '../images/angledown.png'; 8 | import img_cascadedrop from '../images/cascadedrop.png'; 9 | import img_radiobox from '../images/radiobox.png'; 10 | import img_multiplebox from '../images/multiplebox.png'; 11 | import img_formsection from '../images/formsection.png'; 12 | import img_number from '../images/number.png'; 13 | import img_money from '../images/money.png'; 14 | 15 | 16 | const images = {}; 17 | images.img_textfield = img_textfield; 18 | images.img_textareafield = img_textareafield; 19 | images.img_date = img_date; 20 | images.img_datesection = img_datesection; 21 | images.img_formhidden = img_formhidden; 22 | images.img_attachment = img_attachment; 23 | images.img_angledown = img_angledown; 24 | images.img_cascadedrop = img_cascadedrop; 25 | images.img_radiobox = img_radiobox; 26 | images.img_multiplebox = img_multiplebox; 27 | images.img_formsection = img_formsection; 28 | images.img_img_number = img_number; 29 | images.img_img_money = img_money; 30 | export default images; 31 | -------------------------------------------------------------------------------- /client/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import { fetch } from 'whatwg-fetch'; 2 | import { notification } from 'antd'; 3 | 4 | const codeMessage = { 5 | 200: '服务器成功返回请求的数据。', 6 | 201: '新建或修改数据成功。', 7 | 202: '一个请求已经进入后台排队(异步任务)。', 8 | 204: '删除数据成功。', 9 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 10 | 401: '用户没有权限(令牌、用户名、密码错误)。', 11 | 403: '用户得到授权,但是访问是被禁止的。', 12 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 13 | 406: '请求的格式不可得。', 14 | 410: '请求的资源被永久删除,且不会再得到的。', 15 | 422: '当创建一个对象时,发生一个验证错误。', 16 | 500: '服务器发生错误,请检查服务器。', 17 | 502: '网关错误。', 18 | 503: '服务不可用,服务器暂时过载或维护。', 19 | 504: '网关超时。', 20 | }; 21 | function checkStatus(response) { 22 | if (response.status >= 200 && response.status < 300) { 23 | return response; 24 | } 25 | const errortext = codeMessage[response.status] || response.statusText; 26 | notification.error({ 27 | message: `请求错误 ${response.status}: ${response.url}`, 28 | description: errortext, 29 | duration: 2, 30 | }); 31 | } 32 | 33 | /** 34 | * Requests a URL, returning a promise. 35 | * 36 | * @param {string} url The URL we want to request 37 | * @param {object} [options] The options we want to pass to "fetch" 38 | * @return {object} An object containing either "data" or "err" 39 | */ 40 | export default function request(url, options) { 41 | const defaultOptions = { 42 | method: 'POST', 43 | credentials: 'include', 44 | }; 45 | const newOptions = { ...defaultOptions, ...options }; 46 | if (newOptions.method === 'POST' || newOptions.method === 'PUT') { 47 | if (!(newOptions.body instanceof FormData)) { 48 | newOptions.headers = { 49 | Accept: 'application/json', 50 | 'Content-Type': 'application/json; charset=utf-8', 51 | ...newOptions.headers, 52 | }; 53 | newOptions.body = JSON.stringify(newOptions.body); 54 | } else { 55 | // newOptions.body is FormData 56 | newOptions.headers = { 57 | Accept: 'application/json', 58 | 'Content-Type': 'multipart/form-data', 59 | ...newOptions.headers, 60 | }; 61 | } 62 | } 63 | 64 | return fetch(url, newOptions) 65 | .then(checkStatus) 66 | .then((response) => { 67 | if (newOptions.method === 'DELETE' || response.status === 204) { 68 | return response.text(); 69 | } 70 | const data = response.json(); 71 | if (!data.success) { 72 | data.message = data.respMsg || '发生未知错误'; 73 | notification.error({ 74 | message: '操作失败', 75 | description: data.message, 76 | duration: 2, 77 | }); 78 | } 79 | return data; 80 | }) 81 | .catch((e) => {}); 82 | } 83 | -------------------------------------------------------------------------------- /client/src/utils/storeUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description localStore 3 | */ 4 | import store from 'store'; 5 | 6 | export default { 7 | user: null, 8 | set(key, value) { 9 | // localStorage.setItem(key, JSON.stringify(value)) 10 | store.set(key, value); 11 | }, 12 | get(key) { 13 | // return JSON.parse(localStorage.getItem(key) || '{}') 14 | return store.get(key); 15 | }, 16 | remove(key) { 17 | // localStorage.removeItem(key) 18 | store.remove(key); 19 | }, 20 | clearAll() { 21 | store.clearAll(); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /client/stylelint.config.js: -------------------------------------------------------------------------------- 1 | // stylelint.config.js 2 | module.exports = { 3 | 'defaultSeverity': 'warning', 4 | 'extends': ['stylelint-config-standard', 'stylelint-config-recommended-scss'], 5 | 'plugins': ['stylelint-scss'], 6 | 'rules': { 7 | // 不要使用已被 autoprefixer 支持的浏览器前缀 8 | 'unit-case': null, 9 | 'media-feature-name-no-vendor-prefix': true, 10 | 'at-rule-no-vendor-prefix': true, 11 | 'selector-no-vendor-prefix': true, 12 | 'property-no-vendor-prefix': true, 13 | 'value-no-vendor-prefix': true, 14 | 'block-no-empty': null, 15 | 'color-no-invalid-hex': true, 16 | 'comment-empty-line-before': ['always', { 17 | 'ignore': ['stylelint-commands', 'after-comment'] 18 | }], 19 | 'declaration-colon-space-after': 'always', 20 | 'indentation': ['tab', { 21 | 'except': ['value'] 22 | }], 23 | 'max-empty-lines': 2, 24 | 'rule-empty-line-before': ['always', { 25 | 'except': ['first-nested'], 26 | 'ignore': ['after-comment'] 27 | }], 28 | 'unit-whitelist': ['em', 'rem', '%', 's'] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /doc/img/1572075655846.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572075655846.jpg -------------------------------------------------------------------------------- /doc/img/1572075835814.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572075835814.jpg -------------------------------------------------------------------------------- /doc/img/1572077173164.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572077173164.jpg -------------------------------------------------------------------------------- /doc/img/1572077222666.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572077222666.gif -------------------------------------------------------------------------------- /doc/img/1572077222666.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572077222666.jpg -------------------------------------------------------------------------------- /doc/img/1572508162034.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572508162034.gif -------------------------------------------------------------------------------- /doc/img/1572508162034.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572508162034.jpg -------------------------------------------------------------------------------- /doc/img/1572938533169.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1572938533169.jpg -------------------------------------------------------------------------------- /doc/img/1573007074794.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1573007074794.gif -------------------------------------------------------------------------------- /doc/img/1573007074794.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/1573007074794.jpg -------------------------------------------------------------------------------- /doc/img/permission.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/permission.gif -------------------------------------------------------------------------------- /doc/img/permission.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/permission.jpg -------------------------------------------------------------------------------- /doc/img/roleList.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/roleList.gif -------------------------------------------------------------------------------- /doc/img/roleList.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/roleList.jpg -------------------------------------------------------------------------------- /doc/img/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/test.gif -------------------------------------------------------------------------------- /doc/img/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/test.png -------------------------------------------------------------------------------- /doc/img/userList.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/userList.gif -------------------------------------------------------------------------------- /doc/img/userList.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/doc/img/userList.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## 技术栈 2 | >项目运行之前,请确保系统已经安装以下应用 3 | 1. node (6.0 及以上版本) 4 | 2. mongodb (开启状态) [安装参考](https://blog.csdn.net/composurext/article/details/79543271) 5 | 3. 全局安装nodemon(为了node服务热启动) 6 | ``` 7 | yarn global add nodemon 8 | ``` 9 | 4. [可以参考这里安装](https://blog.csdn.net/composurext/article/details/79543271) 10 | ## 运行启动 11 | ``` 12 | # 克隆到本地 13 | 1. git clone 14 | 15 | # 进入后台应用目录 16 | 2. cd react-manage/server 17 | 18 | # 安装后台应用依赖 19 | 3. yarn install 20 | 21 | # 启动后台应用 22 | 4. yarn start 23 | 24 | # 进人前端应用目录 25 | 5. cd react-manage/client 26 | 27 | # 安装前端应用依赖 28 | 6. yarn install 29 | 30 | # 启动前端应用 31 | 7. yarn start 32 | 33 | # 启动浏览器 34 | 8. 访问: http://localhost:3000 35 | ``` 36 | ## 应用基本结构 37 | ### client/src 38 | |文件目录 |含义 | 39 | | ----- | ------ | 40 | | api | ajax相关 | 41 | | assets | 公用资源 | 42 | | components | 公共组件(非路由组件) | 43 | | pages | 路由组件 | 44 | | config | 配置文件 | 45 | | utils |工具模块 | 46 | | App.js | 应用根模块 | 47 | | index.js| 应用入口 | 48 | ### server/src 49 | |文件目录 |含义 | 50 | | ----- | ------ | 51 | | config | 配置参数 | 52 | | db | 数据库连接 | 53 | | logs | 日志文件 | 54 | | modals | 数据库表结构 | 55 | | public | 可以放前端静态资源 | 56 | | router | 路由 | 57 | | server.js| 应用入口 | 58 | 59 | 60 | ## 功能 61 | + 首页 62 | + 登陆 63 | + node实现基于token的身份验证 64 | + 登出 65 | + 保持登录 66 | + 页面刷新时的store状态存储 67 | + 商品 68 | + 品类管理 69 | + 添加 70 | + 修改分类 71 | + 子分类 72 | + 商品管理 73 | + 搜索商品 74 | + 添加商品 75 | + 上架/下架 76 | + 修改/详情 77 | + 用户管理 78 | + 添加/删除 79 | + 角色管理 80 | + 权限控制 81 | + 订单管理 82 | + 进、入 83 | + 数据可视化 84 | + 柱形、折线、饼图等 85 | + GitHub 86 | + 其它项目展示 87 | 88 | 89 | ### 登录 90 | ![登录](./doc/img/1572077222666.gif) 91 | ### 品类管理 92 | ![管理](./doc/img/1572508162034.gif) 93 | ### 商品管理 94 | ![管理](./doc/img/1573007074794.gif) 95 | ![添加](./doc/img/test.gif) 96 | ### 用户管理 97 | ![用户管理](./doc/img/userList.gif) 98 | ### 角色管理 99 | ![角色管理](./doc/img/roleList.gif) 100 | ### 权限控制 101 | ![权限控制](./doc/img/permission.gif) 102 | 103 | -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Composur/react-manage/7368193a2bc81768e7d4a31d2cb5766e8544a864/server/.DS_Store -------------------------------------------------------------------------------- /server/cluster.js: -------------------------------------------------------------------------------- 1 | const cluster = require("cluster"); 2 | const os = require("os"); 3 | const cpus = os.cpus().length; 4 | if (cluster.isMaster) { 5 | // 启动的时候根据 CPU 的核心数开启多个进程 6 | for (let i = 0; i < cpus / 2 ; i++) { 7 | cluster.fork(); 8 | } 9 | } else { 10 | require("./server") // 启动 http 服务 11 | } -------------------------------------------------------------------------------- /server/config/config.default.js: -------------------------------------------------------------------------------- 1 | module.exports={ 2 | server_port:8081, 3 | dataBase: "mongodb://localhost/react-manage", 4 | socketAddress:'' 5 | // dataBase: "mongodb://username:password@localhost:27017/blog", 6 | } -------------------------------------------------------------------------------- /server/config/rsa_private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA56DBeGJnJyFvh7QN1OyoTbHYmh5AO3deGp5NyEvTdBXguCrI 3 | pTLWRP8UAeUDFUL6V0dJuBj/aismRq+QRlYzT9bn4WaM9ZZyNiVILkWZL+WFOBRB 4 | BOA+GbEe+ulbEj5GJLQUekWW4k21OJbxTygJqvE2d98WPScumLOa0I27rfGzF9Ur 5 | nY/lRgPi0+HyZV/v/Zd0NJxjd9zIEhgaHOvIUXC7SEgOoX5fHDScotcgyYcJiUrJ 6 | oOr9oUSB8sNtS5eQuepFa1t/H1GCehCHQxeXZOdJnUcFys1+a9G4SLqQG0hWazVT 7 | PdPP/l/4GQswSDZS9JDrfyOyS5zSEzwX98PPzwIDAQABAoIBADqplhY9Jc7dQvfl 8 | fBNPFJkIz8jInI/y/VxUd3Kg8Z7W46EBBOwB4g57Y7/PFmbOHcT5AwyjGBz1bK4+ 9 | TJXjwv8XeIa3iD+yqJ5Z9tvxOIjO4lx/lK/9cvzixOvbKfuG1hD5oq4M7dfGwnzg 10 | jaD0jO5ERP06/NDkGYMHCdCD9OJqdv/KRgNzTfIn0YQhUa6bfOt/ODcErwReu8hJ 11 | s3h3ytR8Rx2x6BOhU/qRk6hyOjMAdBLDK6JWzx046QTl0cZyl8+BZDYt5YXGjm+Q 12 | fx7tSGNUFw/czBZlXP8RUuxXLwZioPKRbjRuBbsuulbQV+kLMZO0hixBgYW27hU+ 13 | ECxCpgECgYEA+ykb9p/NPorulw+84K6Ivmr+OdvElV27b1hgrVEdsytRuS5ieLCH 14 | 0WR7SAOL4caLCGuJGt4jcAPR1kYys0TP8VyQuCDebdyccxW1B6P6d9RXiIGifwxe 15 | BzAicVu8E5o+TWZjqK7/ZLMOFHZz+eQh0N5W9P4BR4MIDGtJRa+1j4ECgYEA7BdM 16 | cXSvuHICk6vqvYC3fXjeszPIIlwtf3wHwSkJSfxWStbJ43eWx4bWkfImpssMbeUp 17 | 8JC1NSUk1vSceBI/zH4PXnjB7Dfg88gx79omHEEvQjQrFQXqpcZWYDm9cIrC6YFn 18 | kjjDS+d19PKJKnZqxyO8YuOaclJ90zH8q6/QB08CgYBARbXdN+mq/pyt1Z4tFRzz 19 | NagXIFE/M/xaMxCSabai+gvwypjyb6JiCEewFxA917QOlI5CbOQ+rwuMvOwOiiZ4 20 | ioqSQ/8HuDPlE4H/EYxwC8vWp+3weza4ui2mOqZ3kbotzmzkZtv+Zf/NZr6pDSNG 21 | Mw+npDSpmzN9EhtvFNbugQKBgDL4cLuJ6qoSizXghuR01qiTINllk46/gd8lIvNz 22 | 7Zp4jRTJPHsMhZP8K3UE541ZBwzuzdgvFcAsjcCOvP07S2TVznGh9pQOGFXpYcab 23 | vWCaPh637pkyVs+Fe3542MpkWuJY2sh50sgfZ5sDhdOSD0mBEJn085I89wdSom9a 24 | nLoHAoGBAKcaQEgiEtaqkbncmIBmGtoEFTs/YF3L3Si+T0jFocJ8e5Cc7upYl87U 25 | 485nyKmHlWZT7LS+wwRjRftS2YdUulcZ39n9QtsRGwggt7dRIWa7oj2+40ieFAw+ 26 | 7w2c1oyXOarxfu5LMPmlVUAf0s+jpVMwjI7fYJK0PRWUhCl5YIZ5 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /server/config/rsa_public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA56DBeGJnJyFvh7QN1Oyo 3 | TbHYmh5AO3deGp5NyEvTdBXguCrIpTLWRP8UAeUDFUL6V0dJuBj/aismRq+QRlYz 4 | T9bn4WaM9ZZyNiVILkWZL+WFOBRBBOA+GbEe+ulbEj5GJLQUekWW4k21OJbxTygJ 5 | qvE2d98WPScumLOa0I27rfGzF9UrnY/lRgPi0+HyZV/v/Zd0NJxjd9zIEhgaHOvI 6 | UXC7SEgOoX5fHDScotcgyYcJiUrJoOr9oUSB8sNtS5eQuepFa1t/H1GCehCHQxeX 7 | ZOdJnUcFys1+a9G4SLqQG0hWazVTPdPP/l/4GQswSDZS9JDrfyOyS5zSEzwX98PP 8 | zwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /server/controller/CacheLRU.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Node 缓存数据 3 | * @param {} capacity 4 | */ 5 | // 使用Buffer构建的读写都要快一点,容量也不受V8限制,只是不适合在客户端浏览器使用 6 | function CacheLRU(capacity) { 7 | /* 利用Buffer写的一个LRU缓存,capacity为缓存容量,为0时构造一般缓存。 8 | myCache = new CacheLRU(capacity); //构造缓存 9 | myCache.get(key); //读取名为key的缓存值 10 | myCache.put(key, value); //写入名为key的缓存值 11 | myCache.remove(key); //删除名为key的缓存值 12 | myCache.removeAll(); //删除所有缓存值 13 | myCache.info(); //返回myCache缓存信息,包括: 14 | {capacity: 缓存容量, 15 | length: 当前已使用容量, 16 | size: 缓存数据大小bytes, 17 | ratio: 缓存命中率, 18 | keys: 当前已缓存的数据名称(key)数组 19 | } 20 | */ 21 | this.capacity = capacity || 0; 22 | this.cache = {}; 23 | this.hash = {}; 24 | this.miss = 0; 25 | if(capacity < 0) this.capacity = 0; 26 | }; 27 | //为了提高取值效率,get方法只有取值和取值计数操作,key为string类型。 28 | CacheLRU.prototype.get = function(key) { 29 | key = '_' + key; 30 | if(this.hash[key]) this.hash[key] += 1; 31 | else this.miss += 1; 32 | return JSON.parse(this.cache[key].toString()); 33 | }; 34 | //LRU cache由存值put方法实现 35 | CacheLRU.prototype.put = function(key, value) { 36 | key = '_' + key; 37 | if(this.capacity === 0) { 38 | this.cache[key] = new Buffer(JSON.stringify(value)); 39 | this.hash[key] = 1; 40 | } else { 41 | var r = Object.keys(this.hash); 42 | if(r.length < this.capacity) { 43 | this.cache[key] = new Buffer(JSON.stringify(value)); 44 | this.hash[key] = 1; 45 | } else { 46 | that = this; 47 | r.sort(function(a, b) { 48 | return that.hash[a] - that.hash[b]; 49 | }); 50 | delete this.cache[r[0]]; 51 | delete this.hash[r[0]]; 52 | this.cache[key] = new Buffer(JSON.stringify(value)); 53 | this.hash[key] = 1; 54 | } 55 | } 56 | return this; 57 | }; 58 | CacheLRU.prototype.info = function() { 59 | var keys = Object.keys(this.hash); 60 | var hit = 0, size = 0; 61 | keys.forEach(function(key, i) { 62 | if(this.hash[key]) hit += this.hash[key]; 63 | size += this.cache[key].length; 64 | keys[i] = key.slice(1); 65 | }, this); 66 | return { 67 | capacity: this.capacity, 68 | length: keys.length, 69 | size: size, 70 | ratio: hit / (this.miss + hit), 71 | keys: keys 72 | }; 73 | }; 74 | CacheLRU.prototype.remove = function(key) { 75 | key = '_' + key; 76 | delete this.cache[key]; 77 | delete this.hash[key]; 78 | return this; 79 | }; 80 | CacheLRU.prototype.removeAll = function() { 81 | this.cache = {}; 82 | this.hash = {}; 83 | return this; 84 | }; 85 | 86 | module.exports = CacheLRU; 87 | 88 | // test: 89 | /*var user = new CacheLRU(10); 90 | user.put('user1', {name:'admin', age: 30}); 91 | user.put('user2', {name:'user', age: 31}); 92 | user.put('user3', {name:'guest', age: 32}); 93 | console.log(user.get('user1')); 94 | console.log(user.get('user2')); 95 | console.log(user.get('user3')); 96 | console.log(user.info());*/ -------------------------------------------------------------------------------- /server/controller/bigupload.js: -------------------------------------------------------------------------------- 1 | const fse = require('fs-extra') 2 | const multiparty = require("multiparty"); 3 | const path = require('path'); 4 | // 大文件存储目录 5 | const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); 6 | // 提取后缀名 7 | const extractExt = filename =>filename.slice(filename.lastIndexOf("."), filename.length); 8 | // 移除后缀名 9 | const extractRemove = filename =>filename.slice(0, filename.lastIndexOf(".")); 10 | 11 | 12 | const verify = function(req,res){ 13 | const {filename,fileHash} = req.body 14 | const filePath = path.resolve(UPLOAD_DIR,fileHash+extractExt(filename)) 15 | 16 | // 文件存在不上传 17 | if(fse.existsSync(filePath)){ 18 | res.send({status:1,shouldUpload:false}) 19 | }else { 20 | res.send({status:0,shouldUpload:true}) 21 | } 22 | return 23 | } 24 | 25 | const upload = function(req, res) { 26 | const multipart = new multiparty.Form(); 27 | // fields: 其它formData字段 28 | // files:二进制文件 29 | multipart.parse(req, async (err, fileds, files) => { 30 | if (err) return; 31 | const [chunk] = files.chunk; 32 | const [hash] = fileds.hash; 33 | const [filename] = fileds.filename; 34 | const [fileHash] = fileds.fileHash; 35 | const chunkDir = path.resolve(UPLOAD_DIR,fileHash); 36 | const filePath = path.resolve( 37 | UPLOAD_DIR, 38 | `${fileHash}${extractExt(filename)}` 39 | ); 40 | // 文件切片存在直接返回 41 | if (fse.existsSync(filePath)) { 42 | res.end("file exist"); 43 | return; 44 | } 45 | if (!fse.existsSync(chunkDir)) { 46 | await fse.mkdirp(chunkDir); 47 | } 48 | await fse.move(chunk.path, path.resolve(chunkDir, hash)); 49 | res.send({ status: 0,msg: "上传成功" }); 50 | }); 51 | } 52 | const pipeStream = (path, writeStream) => 53 | new Promise( resolve => { 54 | const readStream = fse.createReadStream(path); 55 | readStream.on("end", () => { 56 | fse.unlinkSync(path); 57 | resolve(); 58 | }); 59 | readStream.pipe(writeStream); 60 | }); 61 | 62 | const mergeFileChunk = async (filePath, fileHash, size) => { 63 | // 找到文件夹 文件路径 64 | const chunkDir = path.resolve(UPLOAD_DIR, fileHash); 65 | // 遍历文件夹里面的切片名 66 | const chunkPaths = await fse.readdir(chunkDir); 67 | // 根据切片下标进行排序 68 | // 否则直接读取目录的获得的顺序可能会错乱 69 | chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]); 70 | await Promise.all( 71 | chunkPaths.map((chunkPath, index) => 72 | pipeStream( 73 | path.resolve(chunkDir, chunkPath), 74 | // 指定位置创建可写流 75 | fse.createWriteStream(filePath, { 76 | start: index * size, 77 | end: (index + 1) * size 78 | }) 79 | ) 80 | ) 81 | ); 82 | fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录 83 | }; 84 | const mergeFile = async function(req,res){ 85 | let {filename,size,fileHash} = req.body 86 | const ext = extractExt(filename); 87 | const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`); 88 | await mergeFileChunk(filePath,fileHash,size); 89 | res.send({ status: 0, msg: "合并完成" }); 90 | } 91 | module.exports = { 92 | verify, 93 | upload, 94 | mergeFile, 95 | }; 96 | -------------------------------------------------------------------------------- /server/db/connect.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | // file: simplest.js var log4js = require('log4js'); var logger = 3 | // log4js.getLogger(); logger.debug("Time:", new Date()); 4 | const config = require('../config/config.default'); 5 | 6 | var isConnectedBefore = false; 7 | const options = { 8 | autoIndex: false, // Don't build indexes 9 | reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect 10 | reconnectInterval: 500, // Reconnect every 500ms 11 | poolSize: 10, // Maintain up to 10 socket connections 12 | // If not connected, return errors immediately rather than waiting for reconnect 13 | bufferMaxEntries: 0, 14 | autoReconnect: true, 15 | useNewUrlParser: true, 16 | useCreateIndex:true, 17 | useFindAndModify:false, 18 | useUnifiedTopology:true 19 | } 20 | 21 | // mongoose.connect(uri, options, function(error) { // Check error in 22 | // initial connection. There is no 2nd param to the callback. }); // Or 23 | // using promises mongoose.connect(uri, options).then( () => { /** ready 24 | // to use. The `mongoose.connect()` promise resolves to undefined. */ }, err 25 | // => { /** handle initial connection error */ } ); 26 | 27 | var connect = function (callback) { 28 | mongoose 29 | .connect(config.dataBase, options, function (error) { 30 | if(!error){ 31 | callback() 32 | } 33 | }); 34 | }; 35 | // connect(); 36 | 37 | mongoose 38 | .connection 39 | .on('error', function () { 40 | console.log('Could not connect to MongoDB','请确保启动MongoDB服务'); 41 | }); 42 | 43 | mongoose 44 | .connection 45 | .on('disconnected', function () { 46 | console.log('Lost MongoDB connection...'); 47 | if (!isConnectedBefore) 48 | connect(); 49 | }); 50 | mongoose 51 | .connection 52 | .on('connected', function () { 53 | isConnectedBefore = true; 54 | console.log('Connection established to MongoDB'); 55 | }); 56 | 57 | mongoose 58 | .connection 59 | .on('reconnected', function () { 60 | console.log('Reconnected to MongoDB'); 61 | }); 62 | 63 | // Close the Mongoose connection, when receiving SIGINT 64 | process.on('SIGINT', function () { 65 | mongoose 66 | .connection 67 | .close(function () { 68 | console.log('Force to close the MongoDB conection'); 69 | process.exit(0); 70 | }); 71 | }); 72 | 73 | module.exports = connect; -------------------------------------------------------------------------------- /server/models/CategoryModel.js: -------------------------------------------------------------------------------- 1 | /* 2 | 能操作categorys集合数据的Model 3 | */ 4 | // 1.引入mongoose 5 | const mongoose = require('mongoose') 6 | // 2.字义Schema(描述文档结构) 7 | const categorySchema = new mongoose.Schema({ 8 | name: {type: String, required: true}, 9 | parentId: {type: String, required: true, default: '0'}, 10 | parentId: {type: String, required: true, default: '0'}, 11 | update_at: { 12 | type: Date, 13 | default:Date.now 14 | } 15 | }) 16 | 17 | // 3. 定义Model(与集合对应, 可以操作集合) 18 | const CategoryModel = mongoose.model('categorys', categorySchema) 19 | 20 | // 4. 向外暴露Model 21 | module.exports = CategoryModel -------------------------------------------------------------------------------- /server/models/ProductModel.js: -------------------------------------------------------------------------------- 1 | /* 2 | 能操作products集合数据的Model 3 | */ 4 | // 1.引入mongoose 5 | const mongoose = require('mongoose') 6 | 7 | // 2.字义Schema(描述文档结构) 8 | const productSchema = new mongoose.Schema({ 9 | categoryId: {type: String, required: true}, // 所属分类的id 10 | pCategoryId: {type: String, required: true}, // 所属分类的父分类id 11 | name: {type: String, required: true}, // 名称 12 | price: {type: Number, required: true}, // 价格 13 | desc: {type: String}, 14 | status: {type: Number, default: 1}, // 商品状态: 1:在售, 2: 下架了 15 | imgs: {type: Array, default: []}, // n个图片文件名的json字符串 16 | detail: {type: String} 17 | }) 18 | 19 | 20 | // 3. 定义Model(与集合对应, 可以操作集合) 21 | const ProductModel = mongoose.model('products', productSchema) 22 | 23 | // 4. 向外暴露Model 24 | module.exports = ProductModel -------------------------------------------------------------------------------- /server/models/RoleModel.js: -------------------------------------------------------------------------------- 1 | /* 2 | 能操作roles集合数据的Model 3 | */ 4 | // 1.引入mongoose 5 | const mongoose = require('mongoose') 6 | 7 | // 2.字义Schema(描述文档结构) 8 | const roleSchema = new mongoose.Schema({ 9 | name: {type: String, required: true}, // 角色名称 10 | auth_name: String, // 授权人 11 | auth_time: Number, // 授权时间 12 | create_time: {type: Number, default: Date.now}, // 创建时间 13 | menus: Array // 所有有权限操作的菜单path的数组 14 | }) 15 | 16 | // 3. 定义Model(与集合对应, 可以操作集合) 17 | const RoleModel = mongoose.model('roles', roleSchema) 18 | 19 | // 4. 向外暴露Model 20 | module.exports = RoleModel 21 | -------------------------------------------------------------------------------- /server/models/UserModel.js: -------------------------------------------------------------------------------- 1 | /* 2 | 能操作users集合数据的Model 3 | */ 4 | // 1.引入mongoose 5 | const mongoose = require('mongoose') 6 | const md5 = require('blueimp-md5') 7 | 8 | // 2.字义Schema(描述文档结构) 9 | const userSchema = new mongoose.Schema({ 10 | username: {type: String, required: true}, // 用户名 11 | password: {type: String, required: true}, // 密码 12 | phone: String, 13 | email: String, 14 | create_time: {type: Number, default: Date.now}, 15 | role_id: String 16 | }) 17 | 18 | // 3. 定义Model(与集合对应, 可以操作集合) 19 | const UserModel = mongoose.model('users', userSchema) 20 | 21 | // 初始化默认超级管理员用户: admin/admin 22 | UserModel.findOne({username: 'admin'}).then(user => { 23 | if(!user) { 24 | UserModel.create({username: 'admin', password: md5('admin')}) 25 | .then(user => { 26 | console.log('初始化用户: 用户名: admin 密码为: admin') 27 | }) 28 | } 29 | }) 30 | 31 | // 4. 向外暴露Model 32 | module.exports = UserModel -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-server_final", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "atob": "^2.1.2", 14 | "blueimp-md5": "^2.10.0", 15 | "cookie-parser": "^1.4.3", 16 | "cors": "^2.8.5", 17 | "express": "^4.16.4", 18 | "express-pino-logger": "^5.0.0", 19 | "fs-extra": "^8.1.0", 20 | "global": "^4.4.0", 21 | "jsonwebtoken": "^8.5.1", 22 | "mongoose": "^5.3.7", 23 | "morgan": "^1.9.1", 24 | "multer": "^1.4.1", 25 | "multiparty": "^4.2.1", 26 | "nodemon": "^1.19.4", 27 | "pino": "^6.2.1" 28 | }, 29 | "devDependencies": { 30 | "pino-pretty": "^4.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/routers/file-upload.js: -------------------------------------------------------------------------------- 1 | /* 2 | 处理文件上传的路由 3 | */ 4 | const multer = require('multer') 5 | const path = require('path') 6 | const fs = require('fs') 7 | 8 | const dirPath = path.join(__dirname, '..', 'public/upload') 9 | const storage = multer.diskStorage({ 10 | // destination: 'upload', //string时,服务启动将会自动创建文件夹 11 | destination: function (req, file, cb) { //函数需手动创建文件夹 12 | // console.log('destination()', file) 13 | if (!fs.existsSync(dirPath)) { 14 | fs.mkdir(dirPath, function (err) { 15 | if (err) { 16 | console.log(err) 17 | } else { 18 | cb(null, dirPath) 19 | } 20 | }) 21 | } else { 22 | cb(null, dirPath) 23 | } 24 | }, 25 | filename: function (req, file, cb) { 26 | // console.log('filename()', file) 27 | var ext = path.extname(file.originalname) 28 | cb(null, file.fieldname + '-' + Date.now() + ext) 29 | } 30 | }) 31 | const upload = multer({storage}) 32 | const uploadSingle = upload.single('image') 33 | 34 | module.exports = function fileUpload(router) { 35 | 36 | // 上传图片 37 | router.post('/manage/img/upload', (req, res) => { 38 | uploadSingle(req, res, function (err) { //错误处理 39 | if (err) { 40 | return res.send({ 41 | status: 1, 42 | msg: '上传文件失败' 43 | }) 44 | } 45 | var file = req.file 46 | res.send({ 47 | status: 0, 48 | data: { 49 | name: file.filename, 50 | url: 'http://localhost:8081/upload/' + file.filename 51 | } 52 | }) 53 | 54 | }) 55 | }) 56 | 57 | // 删除图片 58 | router.post('/manage/img/delete', (req, res) => { 59 | const {name} = req.body 60 | fs.unlink(path.join(dirPath, name), (err) => { 61 | if (err) { 62 | console.log(err) 63 | res.send({ 64 | status: 1, 65 | msg: '删除文件失败' 66 | }) 67 | } else { 68 | res.send({ 69 | status: 0 70 | }) 71 | } 72 | }) 73 | }) 74 | } 75 | --------------------------------------------------------------------------------