├── .env.development ├── .env.production ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── config-overrides.js ├── guide.gif ├── logo.png ├── package-lock.json ├── package.json ├── pay.png ├── public ├── favicon.ico └── index.html ├── src ├── App.js ├── api │ ├── excel.js │ ├── login.js │ ├── monitor.js │ ├── remoteSearch.js │ ├── table.js │ └── user.js ├── assets │ └── images │ │ ├── 404.png │ │ ├── bg.jpg │ │ ├── draggable.gif │ │ ├── githubCorner.png │ │ ├── logo.svg │ │ ├── reward.jpg │ │ └── wechat.jpg ├── components │ ├── BreadCrumb │ │ ├── index.jsx │ │ └── index.less │ ├── FullScreen │ │ ├── index.jsx │ │ └── index.less │ ├── Hamburger │ │ ├── index.jsx │ │ └── index.less │ ├── Loading │ │ └── index.jsx │ ├── Mallki │ │ ├── index.jsx │ │ └── index.less │ ├── Markdown │ │ └── index.jsx │ ├── PanThumb │ │ ├── index.jsx │ │ └── index.less │ ├── RichTextEditor │ │ ├── index.jsx │ │ └── index.less │ ├── Settings │ │ ├── index.jsx │ │ └── index.less │ ├── TypingCard │ │ └── index.jsx │ └── UploadExcel │ │ └── index.jsx ├── config │ ├── menuConfig.js │ └── routeMap.js ├── defaultSettings.js ├── index.js ├── lib │ ├── Export2Excel.js │ ├── Export2Zip.js │ ├── echarts.js │ └── monitor │ │ ├── index.js │ │ ├── lib │ │ ├── blankScreen.js │ │ ├── jsError.js │ │ ├── timing.js │ │ └── xhr.js │ │ └── utils │ │ ├── getLastEvent.js │ │ ├── getSelector.js │ │ ├── onload.js │ │ └── tracker.js ├── mock │ ├── excel.js │ ├── index.js │ ├── login.js │ ├── monitor.js │ ├── remoteSearch.js │ └── table.js ├── router │ └── index.js ├── store │ ├── action-types.js │ ├── actions │ │ ├── app.js │ │ ├── auth.js │ │ ├── index.js │ │ ├── monitor.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js │ ├── index.js │ └── reducers │ │ ├── app.js │ │ ├── index.js │ │ ├── monitor.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── index.less │ └── transition.less ├── utils │ ├── auth.js │ ├── clipboard.js │ ├── index.js │ ├── request.js │ └── typing.js └── views │ ├── about │ └── index.jsx │ ├── bug │ └── index.jsx │ ├── charts │ ├── keyboard.jsx │ ├── line.jsx │ └── mixChart.jsx │ ├── clipboard │ └── index.jsx │ ├── components-demo │ ├── Markdown.jsx │ ├── draggable.jsx │ └── richTextEditor.jsx │ ├── dashboard │ ├── components │ │ ├── BarChart │ │ │ └── index.jsx │ │ ├── BoxCard │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── LineChart │ │ │ └── index.jsx │ │ ├── PanelGroup │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── PieChart │ │ │ └── index.jsx │ │ ├── RaddarChart │ │ │ └── index.jsx │ │ └── TransactionTable │ │ │ └── index.jsx │ ├── index.jsx │ └── index.less │ ├── doc │ └── index.jsx │ ├── error │ └── 404 │ │ ├── index.jsx │ │ └── index.less │ ├── excel │ ├── exportExcel │ │ └── index.jsx │ └── uploadExcel │ │ └── index.jsx │ ├── guide │ ├── index.jsx │ └── steps.js │ ├── layout │ ├── Content │ │ └── index.jsx │ ├── Header │ │ ├── index.jsx │ │ └── index.less │ ├── RightPanel │ │ └── index.jsx │ ├── Sider │ │ ├── Logo │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── Menu │ │ │ ├── index.jsx │ │ │ └── index.less │ │ └── index.jsx │ ├── TagsView │ │ ├── components │ │ │ └── TagList.jsx │ │ ├── index.jsx │ │ └── index.less │ └── index.jsx │ ├── login │ ├── index.jsx │ └── index.less │ ├── nested │ └── menu1 │ │ ├── menu1-1 │ │ └── index.jsx │ │ └── menu1-2 │ │ └── menu1-2-1 │ │ └── index.jsx │ ├── permission │ ├── adminPage.jsx │ ├── editorPage.jsx │ ├── guestPage.jsx │ └── index.jsx │ ├── table │ ├── forms │ │ └── editForm.jsx │ └── index.jsx │ ├── user │ ├── forms │ │ ├── add-user-form.jsx │ │ └── edit-user-form.jsx │ └── index.jsx │ └── zip │ └── index.jsx └── wechat.jpg /.env.development: -------------------------------------------------------------------------------- 1 | # base api 2 | REACT_APP_BASE_API = '/dev-api' 3 | 4 | PUBLIC_URL = '/' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # base api 2 | REACT_APP_BASE_API = '/prod-api' 3 | 4 | # 生产环境下不产生sourceMap 5 | GENERATE_SOURCEMAP=false 6 | 7 | PUBLIC_URL = '/react-antd-admin-template/' -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | # 拉取代码 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | with: 24 | persist-credentials: false 25 | 26 | # 生成静态文件 27 | - name: Build 28 | run: npm install && npm run build 29 | # 部署到 GitHub Pages 30 | - name: Deploy 31 | uses: JamesIves/github-pages-deploy-action@releases/v3 32 | with: 33 | GITHUB_TOKEN: ${{ secrets.CI_SECRET }} 34 | BRANCH: gh-pages 35 | FOLDER: build 36 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 难凉热血 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | 6 |

7 | 8 | # 简介 9 | 10 | [react-antd-admin-template](https://nlrx-wjc.github.io/react-antd-admin-template/) 是一个基于 `React` 和 `Ant Design` 的后台管理系统模板。它内置了用户登录/登出,动态路由,权限校验,用户管理等典型的业务模型,可以帮助你快速搭建企业级中后台产品原型,是你接私活的不二之选。 11 | 12 | 本系统的开发灵感来自 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin/) ,这是一个基于 `Vue` 和 `ElementUI` 的优秀的后台管理系统模板,在这里向大佬致敬! 13 | 14 | 其实我的主技术栈一直是 `Vue`,只是最近入坑了 `React` ,看了大半个月文档,就想牛刀小试一下,哈哈。不是有那句话么:检验学习成果最好的方式就是造轮子。所以就造了这么个轮子,哈哈。对于 `React` ,我还是个小白,项目中肯定有做的不够好的地方,欢迎各位同好提 `pr` 或 `issue` 。 15 | 16 | - [在线预览](https://nlrx-wjc.github.io/react-antd-admin-template/) 17 | - [Gitee在线预览(国内用户可访问该地址)](https://nlrx.gitee.io/react-antd-admin-template/) 18 | - [开发文档](https://nlrx-wjc.github.io/react-antd-admin-template-doc/) 目前还在持续编写完善中... 19 | 20 | # 功能 21 | 22 | ```bash 23 | - 登录 / 注销 24 | 25 | - 权限验证 26 | - 页面权限 27 | - 路由权限 28 | 29 | - 全局功能 30 | - 动态侧边栏(支持多级路由嵌套) 31 | - 动态面包屑 32 | - 本地/后端 mock 数据 33 | - Screenfull全屏 34 | - 自适应收缩侧边栏 35 | 36 | - 编辑器 37 | - 富文本 38 | - Markdown 39 | 40 | - Excel 41 | - 导出excel 42 | - 导入excel 43 | - 前端可视化excel 44 | 45 | - Zip 46 | - 导出zip 47 | 48 | - 错误页面 49 | - 404 50 | 51 | - 组件 52 | - 拖拽列表 53 | 54 | - 表格 55 | - Dashboard 56 | - 引导页 57 | - ECharts 图表 58 | - 剪贴板 59 | ``` 60 | 61 | # 目录结构 62 | 63 | ```bash 64 | ├─ public # 静态资源 65 | │ ├─ favicon.ico # favicon图标 66 | │ └─ index.html # html模板 67 | ├─ src # 项目源代码 68 | │ ├─ api # 所有请求 69 | │ ├─ assets # 图片 字体等静态资源 70 | │ ├─ components # 全局公用组件 71 | │ ├─ config # 全局配置 72 | │ │ ├─ menuConfig.js # 导航菜单配置 73 | │ │ └─ routeMap.js # 路由配置 74 | │ ├─ lib # 第三方库按需加载 75 | │ ├─ mock # 项目mock 模拟数据 76 | │ ├─ store # 全局 store管理 77 | │ ├─ styles # 全局样式 78 | │ ├─ utils # 全局公用方法 79 | │ ├─ views # views 所有页面 80 | │ ├─ App.js # 入口页面 81 | │ ├─ defaultSettings.js # 全局默认配置 82 | │ └─index.js # 源码入口 83 | ├── .env.development # 开发环境变量配置 84 | ├── .env.production # 生产环境变量配置 85 | ├── config-overrides.js # 对cra的webpack自定义配置 86 | ├── deploy.sh # CI部署脚本 87 | ├── .travis.yml # 自动化CI配置 88 | └── package.json # package.json 89 | ``` 90 | 91 | # 安装 92 | 93 | ```shell 94 | # 克隆项目 95 | git clone https://github.com/NLRX-WJC/react-antd-admin-template.git 96 | 97 | # 进入项目目录 98 | cd react-antd-admin-template 99 | 100 | # 安装依赖 101 | npm install 102 | 103 | # 切换淘宝源,解决 npm 下载速度慢的问题 104 | npm install --registry=https://registry.npm.taobao.org 105 | 106 | # 启动服务 107 | npm start 108 | ``` 109 | 110 | 启动完成后会自动打开浏览器访问 [http://localhost:3000](http://localhost:3000), 你看到下面的页面就代表操作成功了。 111 | 112 | ![](./guide.gif) 113 | 114 | 接下来你可以修改代码进行业务开发了。 115 | 116 | # 关于作者 117 | 118 | 大家好,我是难凉热血。 119 | 120 | 终南山下码农一枚,师从道长王重阳,酷爱打码,崇尚开源精神,乐于分享。 121 | 122 | 2005年服役于中国人民解放军东南战区狼牙特种大队,担任狙击手。 123 | 124 | 2008年受俄罗斯阿尔法特种部队邀请,执教于该特种部队第一大队教授其队员学习中国特色社会主义理论及毛泽东思想。 125 | 126 | 2011年竞选美国总统落选,遂心灰意冷,放下所有荣誉,隐居终南山下。 127 | 128 | 2015年受道长王重阳委托,为道观开发香火管理系统,遂沉迷IT,无法自拔。 129 | 130 | 喜欢折腾和搞机,追求新鲜技术。 131 | 132 | 下边是我的微信,欢迎同好伙伴一起树(tree)新(new)风(bee)!!! 133 | 134 | ![](./wechat.jpg) 135 | 136 | # 鼓励作者 137 | 138 | 作为个人开发者,维护开源实属不易。如果您觉得本项目对你有些许帮助的话,还请帮忙点个 star 哈~~ 139 | 如果您有余力的话也非常感谢您对我的赞赏,您的赞赏,是对我创作最大的认可和鼓励。 140 | 141 | ![](./pay.png) -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const { 2 | override, 3 | fixBabelImports, 4 | addLessLoader, 5 | addWebpackAlias, 6 | } = require("customize-cra"); 7 | const path = require("path"); 8 | function resolve(dir) { 9 | return path.join(__dirname, dir); 10 | } 11 | process.env.CI = "false"; 12 | const addCustomize = () => (config) => { 13 | if (config.output.publicPath) { 14 | config.output.publicPath = 15 | process.env.NODE_ENV === "production" 16 | ? "/react-antd-admin-template/" 17 | : "/"; 18 | } 19 | if (config.resolve) { 20 | config.resolve.extensions.push(".jsx"); 21 | } 22 | return config; 23 | }; 24 | module.exports = override( 25 | // 针对antd实现按需打包: 根据import来打包(使用babel-plugin-import) 26 | fixBabelImports("import", { 27 | libraryName: "antd", 28 | libraryDirectory: "es", 29 | style: true, // 自动打包相关的样式 30 | }), 31 | 32 | // 使用less-loader对源码中的less的变量进行重新指定 33 | addLessLoader({ 34 | javascriptEnabled: true, 35 | modifyVars: { "@primary-color": "#1DA57A" }, 36 | }), 37 | 38 | // 配置路径别名 39 | addWebpackAlias({ 40 | "@": resolve("src"), 41 | }), 42 | addCustomize() 43 | ); 44 | -------------------------------------------------------------------------------- /guide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/guide.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react_antd_admin_template", 3 | "version": "1.0.0", 4 | "author": "NLRX <414961362@qq.com>", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.5.0", 10 | "@testing-library/user-event": "^7.2.1", 11 | "@toast-ui/react-editor": "^2.1.0", 12 | "antd": "^3.17.0", 13 | "axios": "^0.19.2", 14 | "clipboard": "^2.0.6", 15 | "draft-js": "^0.11.5", 16 | "draftjs-to-html": "^0.9.1", 17 | "draftjs-to-markdown": "^0.6.0", 18 | "driver.js": "^0.9.8", 19 | "echarts": "^4.7.0", 20 | "file-saver": "^2.0.2", 21 | "js-cookie": "^2.2.1", 22 | "jszip": "^3.4.0", 23 | "less": "^3.9.0", 24 | "less-loader": "^5.0.0", 25 | "nprogress": "^0.2.0", 26 | "prop-types": "^15.7.2", 27 | "react": "^16.13.1", 28 | "react-beautiful-dnd": "^13.0.0", 29 | "react-countup": "^4.3.3", 30 | "react-custom-scrollbars": "^4.2.1", 31 | "react-document-title": "^2.0.3", 32 | "react-dom": "^16.13.1", 33 | "react-draft-wysiwyg": "^1.14.5", 34 | "react-loadable": "^5.5.0", 35 | "react-redux": "^7.2.0", 36 | "react-router-dom": "^5.1.2", 37 | "react-scripts": "3.4.1", 38 | "react-transition-group": "^4.4.1", 39 | "redux": "^4.0.5", 40 | "redux-thunk": "^2.3.0", 41 | "screenfull": "^5.0.2", 42 | "script-loader": "^0.7.2", 43 | "xlsx": "^0.16.0" 44 | }, 45 | "scripts": { 46 | "start": "react-app-rewired start", 47 | "build": "react-app-rewired --max-old-space-size=4096 build ", 48 | "test": "react-app-rewired test", 49 | "eject": "react-scripts eject", 50 | "commit": "git cz" 51 | }, 52 | "eslintConfig": { 53 | "extends": "react-app" 54 | }, 55 | "browserslist": { 56 | "production": [ 57 | ">0.2%", 58 | "not dead", 59 | "not op_mini all" 60 | ], 61 | "development": [ 62 | "last 1 chrome version", 63 | "last 1 firefox version", 64 | "last 1 safari version" 65 | ] 66 | }, 67 | "devDependencies": { 68 | "user-agent": "^1.0.4", 69 | "babel-plugin-import": "^1.13.0", 70 | "commitizen": "^4.0.3", 71 | "customize-cra": "^0.9.1", 72 | "cz-conventional-changelog": "^3.0.2", 73 | "mockjs": "^1.1.0", 74 | "react-app-rewired": "^2.1.5" 75 | }, 76 | "config": { 77 | "commitizen": { 78 | "path": "./node_modules/cz-conventional-changelog" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/pay.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react-antd-admin-template 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from "react-redux"; 3 | import { ConfigProvider } from "antd"; 4 | import zhCN from "antd/es/locale/zh_CN"; 5 | import store from "./store"; 6 | import Router from "./router"; 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/api/excel.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | export function excelList() { 3 | return request({ 4 | url: '/excel/list', 5 | method: 'get' 6 | }) 7 | } -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function reqLogin(data) { 4 | return request({ 5 | url: '/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function reqLogout(data) { 12 | return request({ 13 | url: '/logout', 14 | method: 'post', 15 | data 16 | }) 17 | } -------------------------------------------------------------------------------- /src/api/monitor.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export function tracker(data) { 4 | return request({ 5 | url: "/monitor", 6 | method: "post", 7 | data, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/api/remoteSearch.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | export function transactionList() { 3 | return request({ 4 | url: '/transaction/list', 5 | method: 'get' 6 | }) 7 | } -------------------------------------------------------------------------------- /src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | export function tableList(data) { 3 | return request({ 4 | url: '/table/list', 5 | method: 'post', 6 | data 7 | }) 8 | } 9 | 10 | export function deleteItem(data) { 11 | return request({ 12 | url: '/table/delete', 13 | method: 'post', 14 | data 15 | }) 16 | } 17 | export function editItem(data) { 18 | return request({ 19 | url: '/table/edit', 20 | method: 'post', 21 | data 22 | }) 23 | } -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function reqUserInfo(data) { 4 | return request({ 5 | url: '/userInfo', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getUsers() { 12 | return request({ 13 | url: '/user/list', 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function deleteUser(data) { 19 | return request({ 20 | url: '/user/delete', 21 | method: 'post', 22 | data 23 | }) 24 | } 25 | 26 | export function editUser(data) { 27 | return request({ 28 | url: '/user/edit', 29 | method: 'post', 30 | data 31 | }) 32 | } 33 | 34 | export function reqValidatUserID(data) { 35 | return request({ 36 | url: '/user/validatUserID', 37 | method: 'post', 38 | data 39 | }) 40 | } 41 | 42 | export function addUser(data) { 43 | return request({ 44 | url: '/user/add', 45 | method: 'post', 46 | data 47 | }) 48 | } -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/src/assets/images/bg.jpg -------------------------------------------------------------------------------- /src/assets/images/draggable.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/src/assets/images/draggable.gif -------------------------------------------------------------------------------- /src/assets/images/githubCorner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/src/assets/images/githubCorner.png -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/reward.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/src/assets/images/reward.jpg -------------------------------------------------------------------------------- /src/assets/images/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NLRX-WJC/react-antd-admin-template/2bf6517630daa7d68aeb7a71b4c86f99f48160ac/src/assets/images/wechat.jpg -------------------------------------------------------------------------------- /src/components/BreadCrumb/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { Breadcrumb } from "antd"; 4 | import menuList from "@/config/menuConfig"; 5 | import "./index.less"; 6 | /** 7 | * 根据当前浏览器地址栏的路由地址,在menuConfig中查找路由跳转的路径 8 | * 如路由地址为/charts/keyboard,则查找到的路径为[{title: "图表",...},{title: "键盘图表",...}] 9 | */ 10 | const getPath = (menuList, pathname) => { 11 | let temppath = []; 12 | try { 13 | function getNodePath(node) { 14 | temppath.push(node); 15 | //找到符合条件的节点,通过throw终止掉递归 16 | if (node.path === pathname) { 17 | throw new Error("GOT IT!"); 18 | } 19 | if (node.children && node.children.length > 0) { 20 | for (var i = 0; i < node.children.length; i++) { 21 | getNodePath(node.children[i]); 22 | } 23 | //当前节点的子节点遍历完依旧没找到,则删除路径中的该节点 24 | temppath.pop(); 25 | } else { 26 | //找到叶子节点时,删除路径当中的该叶子节点 27 | temppath.pop(); 28 | } 29 | } 30 | for (let i = 0; i < menuList.length; i++) { 31 | getNodePath(menuList[i]); 32 | } 33 | } catch (e) { 34 | return temppath; 35 | } 36 | }; 37 | 38 | const BreadCrumb = (props) => { 39 | const { location } = props; 40 | const { pathname } = location; 41 | let path = getPath(menuList, pathname); 42 | const first = path && path[0]; 43 | if (first && first.title.trim() !== "首页") { 44 | path = [{ title: "首页", path: "/dashboard" }].concat(path); 45 | } 46 | return ( 47 |
48 | 49 | {path && 50 | path.map((item) => 51 | item.title === "首页" ? ( 52 | 53 | {item.title} 54 | 55 | ) : ( 56 | {item.title} 57 | ) 58 | )} 59 | 60 |
61 | ); 62 | }; 63 | 64 | export default withRouter(BreadCrumb); 65 | -------------------------------------------------------------------------------- /src/components/BreadCrumb/index.less: -------------------------------------------------------------------------------- 1 | .Breadcrumb-container { 2 | display: inline-block; 3 | line-height: 64px; 4 | margin-left: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/FullScreen/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import screenfull from "screenfull"; 3 | import { Icon, message, Tooltip } from "antd"; 4 | import "./index.less"; 5 | 6 | const click = () => { 7 | if (!screenfull.isEnabled) { 8 | message.warning("you browser can not work"); 9 | return false; 10 | } 11 | screenfull.toggle(); 12 | }; 13 | 14 | const FullScreen = () => { 15 | const [isFullscreen, setIsFullscreen] = useState(false); 16 | 17 | const change = () => { 18 | setIsFullscreen(screenfull.isFullscreen); 19 | }; 20 | 21 | useEffect(() => { 22 | screenfull.isEnabled && screenfull.on("change", change); 23 | return () => { 24 | screenfull.isEnabled && screenfull.off("change", change); 25 | }; 26 | }, []); 27 | 28 | const title = isFullscreen ? "取消全屏" : "全屏"; 29 | const type = isFullscreen ? "fullscreen-exit" : "fullscreen"; 30 | return ( 31 |
32 | 33 | 34 | 35 |
36 | ); 37 | }; 38 | 39 | export default FullScreen; 40 | -------------------------------------------------------------------------------- /src/components/FullScreen/index.less: -------------------------------------------------------------------------------- 1 | .fullScreen-container { 2 | display: inline-block; 3 | font-size: 25px; 4 | margin-right: 15px; 5 | height: 100%; 6 | cursor: pointer; 7 | vertical-align: middle; 8 | } -------------------------------------------------------------------------------- /src/components/Hamburger/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Icon } from "antd"; 4 | import { toggleSiderBar } from "@/store/actions"; 5 | import "./index.less"; 6 | const Hamburger = (props) => { 7 | const { sidebarCollapsed, toggleSiderBar } = props; 8 | return ( 9 |
10 | 14 |
15 | ); 16 | }; 17 | 18 | export default connect((state) => state.app, { toggleSiderBar })(Hamburger); 19 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.less: -------------------------------------------------------------------------------- 1 | .hamburger-container { 2 | display: inline-block; 3 | font-size: 20px; 4 | line-height: 64px; 5 | cursor: pointer; 6 | margin-left: -30px; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Loading/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Spin } from "antd"; 3 | import NProgress from "nprogress"; // progress bar 4 | import "nprogress/nprogress.css"; // progress bar style 5 | 6 | NProgress.configure({ showSpinner: false }); // NProgress Configuration 7 | 8 | const Loading = () => { 9 | useEffect(() => { 10 | NProgress.start(); 11 | return () => { 12 | NProgress.done(); 13 | }; 14 | }, []); 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }; 22 | 23 | export default Loading; 24 | -------------------------------------------------------------------------------- /src/components/Mallki/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import "./index.less"; 4 | const Mallki = (props) => { 5 | const { className, text } = props; 6 | return ( 7 | 8 | {text} 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | Mallki.propTypes = { 16 | className: PropTypes.string, 17 | text: PropTypes.string, 18 | }; 19 | 20 | Mallki.defaultProps = { 21 | className: "", 22 | text: "vue-element-admin", 23 | }; 24 | 25 | export default Mallki; 26 | -------------------------------------------------------------------------------- /src/components/Mallki/index.less: -------------------------------------------------------------------------------- 1 | .mallki { 2 | font-weight: 800; 3 | color: #4dd9d5; 4 | font-family: 'Dosis', sans-serif; 5 | -webkit-transition: color 0.5s 0.25s; 6 | transition: color 0.5s 0.25s; 7 | overflow: hidden; 8 | position: relative; 9 | display: inline-block; 10 | line-height: 1; 11 | outline: none; 12 | text-decoration: none; 13 | } 14 | 15 | .mallki:hover { 16 | -webkit-transition: none; 17 | transition: none; 18 | color: transparent; 19 | } 20 | 21 | .mallki::before { 22 | content: ''; 23 | width: 100%; 24 | height: 6px; 25 | margin: -3px 0 0 0; 26 | background: #3888fa; 27 | position: absolute; 28 | left: 0; 29 | top: 50%; 30 | -webkit-transform: translate3d(-100%, 0, 0); 31 | transform: translate3d(-100%, 0, 0); 32 | -webkit-transition: -webkit-transform 0.4s; 33 | transition: transform 0.4s; 34 | -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); 35 | transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); 36 | } 37 | 38 | .mallki:hover::before { 39 | -webkit-transform: translate3d(100%, 0, 0); 40 | transform: translate3d(100%, 0, 0); 41 | } 42 | 43 | .mallki span { 44 | position: absolute; 45 | height: 50%; 46 | width: 100%; 47 | left: 0; 48 | top: 0; 49 | overflow: hidden; 50 | } 51 | 52 | .mallki span::before { 53 | content: attr(data-letters); 54 | color: red; 55 | position: absolute; 56 | left: 0; 57 | width: 100%; 58 | color: #3888fa; 59 | -webkit-transition: -webkit-transform 0.5s; 60 | transition: transform 0.5s; 61 | } 62 | 63 | .mallki span:nth-child(2) { 64 | top: 50%; 65 | } 66 | 67 | .mallki span:first-child::before { 68 | top: 0; 69 | -webkit-transform: translate3d(0, 100%, 0); 70 | transform: translate3d(0, 100%, 0); 71 | } 72 | 73 | .mallki span:nth-child(2)::before { 74 | bottom: 0; 75 | -webkit-transform: translate3d(0, -100%, 0); 76 | transform: translate3d(0, -100%, 0); 77 | } 78 | 79 | .mallki:hover span::before { 80 | -webkit-transition-delay: 0.3s; 81 | transition-delay: 0.3s; 82 | -webkit-transform: translate3d(0, 0, 0); 83 | transform: translate3d(0, 0, 0); 84 | -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1); 85 | transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1); 86 | } -------------------------------------------------------------------------------- /src/components/Markdown/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "codemirror/lib/codemirror.css"; 3 | import "@toast-ui/editor/dist/toastui-editor.css"; 4 | import { Editor } from "@toast-ui/react-editor"; 5 | const Markdown = () => { 6 | return ( 7 | 14 | ); 15 | }; 16 | 17 | export default Markdown; 18 | -------------------------------------------------------------------------------- /src/components/PanThumb/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import "./index.less"; 4 | 5 | const PanThumb = (props) => { 6 | const { image, zIndex, width, height, className } = props; 7 | return ( 8 |
16 |
17 |
{props.children}
18 |
19 | 20 |
21 | ); 22 | }; 23 | 24 | PanThumb.propTypes = { 25 | image: PropTypes.string.isRequired, 26 | zIndex: PropTypes.number, 27 | width: PropTypes.string, 28 | height: PropTypes.string, 29 | className: PropTypes.string, 30 | }; 31 | 32 | PanThumb.defaultProps = { 33 | width: "150px", 34 | height: "150px", 35 | zIndex: 1, 36 | className: "", 37 | }; 38 | 39 | export default PanThumb; 40 | -------------------------------------------------------------------------------- /src/components/PanThumb/index.less: -------------------------------------------------------------------------------- 1 | .pan-item { 2 | width: 200px; 3 | height: 200px; 4 | border-radius: 50%; 5 | display: inline-block; 6 | position: relative; 7 | cursor: default; 8 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 9 | } 10 | 11 | .pan-info-roles-container { 12 | padding: 20px; 13 | text-align: center; 14 | } 15 | 16 | .pan-thumb { 17 | width: 100%; 18 | height: 100%; 19 | background-size: 100%; 20 | border-radius: 50%; 21 | overflow: hidden; 22 | position: absolute; 23 | transform-origin: 95% 40%; 24 | transition: all 0.3s ease-in-out; 25 | } 26 | 27 | .pan-thumb:after { 28 | content: ''; 29 | width: 8px; 30 | height: 8px; 31 | position: absolute; 32 | border-radius: 50%; 33 | top: 40%; 34 | left: 95%; 35 | margin: -4px 0 0 -4px; 36 | background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%); 37 | box-shadow: 0 0 1px rgba(255, 255, 255, 0.9); 38 | } 39 | 40 | .pan-info { 41 | position: absolute; 42 | width: inherit; 43 | height: inherit; 44 | border-radius: 50%; 45 | overflow: hidden; 46 | box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05); 47 | } 48 | 49 | .pan-info h3 { 50 | color: #fff; 51 | text-transform: uppercase; 52 | position: relative; 53 | letter-spacing: 2px; 54 | font-size: 18px; 55 | margin: 0 60px; 56 | padding: 22px 0 0 0; 57 | height: 85px; 58 | font-family: 'Open Sans', Arial, sans-serif; 59 | text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3); 60 | } 61 | 62 | .pan-info p { 63 | color: #fff; 64 | padding: 10px 5px; 65 | font-style: italic; 66 | margin: 0 30px; 67 | font-size: 12px; 68 | border-top: 1px solid rgba(255, 255, 255, 0.5); 69 | } 70 | 71 | .pan-info p a { 72 | display: block; 73 | color: #333; 74 | width: 80px; 75 | height: 80px; 76 | background: rgba(255, 255, 255, 0.3); 77 | border-radius: 50%; 78 | color: #fff; 79 | font-style: normal; 80 | font-weight: 700; 81 | text-transform: uppercase; 82 | font-size: 9px; 83 | letter-spacing: 1px; 84 | padding-top: 24px; 85 | margin: 7px auto 0; 86 | font-family: 'Open Sans', Arial, sans-serif; 87 | opacity: 0; 88 | transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s; 89 | transform: translateX(60px) rotate(90deg); 90 | } 91 | 92 | .pan-info p a:hover { 93 | background: rgba(255, 255, 255, 0.5); 94 | } 95 | 96 | .pan-item:hover .pan-thumb { 97 | transform: rotate(-110deg); 98 | } 99 | 100 | .pan-item:hover .pan-info p a { 101 | opacity: 1; 102 | transform: translateX(0px) rotate(0deg); 103 | } -------------------------------------------------------------------------------- /src/components/RichTextEditor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Card, Row, Col } from "antd"; 3 | import { EditorState, convertToRaw } from "draft-js"; 4 | import { Editor } from "react-draft-wysiwyg"; 5 | import draftToHtml from "draftjs-to-html"; 6 | import draftToMarkdown from "draftjs-to-markdown"; 7 | import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; 8 | import "./index.less"; 9 | 10 | const RichTextEditor = () => { 11 | const [editorState, setEditorState] = useState(EditorState.createEmpty()); 12 | 13 | const onEditorStateChange = (editorState) => setEditorState(editorState); 14 | 15 | return ( 16 |
17 | 18 | 26 | 27 |
28 | 29 | 30 | 35 | {editorState && 36 | draftToHtml(convertToRaw(editorState.getCurrentContent()))} 37 | 38 | 39 | 40 | 45 | {editorState && 46 | draftToMarkdown(convertToRaw(editorState.getCurrentContent()))} 47 | 48 | 49 | 50 |
51 | ); 52 | }; 53 | 54 | export default RichTextEditor; 55 | -------------------------------------------------------------------------------- /src/components/RichTextEditor/index.less: -------------------------------------------------------------------------------- 1 | .editor-class{ 2 | min-height: 300px; 3 | border: 1px solid #F1F1F1; 4 | border-top: none; 5 | margin-top: -5px; 6 | padding: 0 10px; 7 | } -------------------------------------------------------------------------------- /src/components/Settings/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Icon, Tooltip } from "antd"; 4 | import { toggleSettingPanel } from "@/store/actions"; 5 | import "./index.less"; 6 | const Settings = (props) => { 7 | const { toggleSettingPanel } = props; 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default connect(null, { toggleSettingPanel })(Settings); 18 | -------------------------------------------------------------------------------- /src/components/Settings/index.less: -------------------------------------------------------------------------------- 1 | .settings-container { 2 | display: inline-block; 3 | font-size: 25px; 4 | margin-right: 15px; 5 | height: 100%; 6 | cursor: pointer; 7 | vertical-align: middle; 8 | } -------------------------------------------------------------------------------- /src/components/TypingCard/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import { Card } from "antd"; 3 | import { PropTypes } from "prop-types"; 4 | import Typing from "@/utils/typing"; 5 | 6 | const TypingCard = (props) => { 7 | const { title, source } = props; 8 | 9 | const sourceEl = useRef(); 10 | const outputEl = useRef(); 11 | 12 | useEffect(() => { 13 | const typing = new Typing({ 14 | source: sourceEl.current, 15 | output: outputEl.current, 16 | delay: 30, 17 | }); 18 | typing.start(); 19 | }, []); 20 | return ( 21 | 22 |
27 |
28 | 29 | ); 30 | }; 31 | 32 | TypingCard.propTypes = { 33 | title: PropTypes.string, 34 | source: PropTypes.string, 35 | }; 36 | 37 | TypingCard.defaultProps = { 38 | title: "", 39 | source: "", 40 | }; 41 | 42 | export default TypingCard; 43 | -------------------------------------------------------------------------------- /src/components/UploadExcel/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import { Upload, Icon, message } from "antd"; 4 | import XLSX from "xlsx"; 5 | const { Dragger } = Upload; 6 | 7 | const getHeaderRow = (sheet) => { 8 | const headers = []; 9 | const range = XLSX.utils.decode_range(sheet["!ref"]); 10 | let C; 11 | const R = range.s.r; 12 | /* start in the first row */ 13 | for (C = range.s.c; C <= range.e.c; ++C) { 14 | /* walk every column in the range */ 15 | const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]; 16 | /* find the cell in the first row */ 17 | let hdr = "UNKNOWN " + C; // <-- replace with your desired default 18 | if (cell && cell.t) hdr = XLSX.utils.format_cell(cell); 19 | headers.push(hdr); 20 | } 21 | return headers; 22 | }; 23 | const isExcel = (file) => { 24 | return /\.(xlsx|xls|csv)$/.test(file.name); 25 | }; 26 | class UploadExcel extends Component { 27 | static propTypes = { 28 | uploadSuccess: PropTypes.func.isRequired, 29 | }; 30 | state = { 31 | loading: false, 32 | excelData: { 33 | header: null, 34 | results: null, 35 | }, 36 | }; 37 | draggerProps = () => { 38 | let _this = this; 39 | return { 40 | name: "file", 41 | multiple: false, 42 | accept: ".xlsx, .xls", 43 | onChange(info) { 44 | const { status } = info.file; 45 | if (status === "done") { 46 | message.success(`${info.file.name} 文件上传成功`); 47 | } else if (status === "error") { 48 | message.error(`${info.file.name} 文件上传失败`); 49 | } 50 | }, 51 | beforeUpload(file, fileList) { 52 | if (!isExcel(file)) { 53 | message.error("仅支持上传.xlsx, .xls, .csv 文件"); 54 | return false; 55 | } 56 | }, 57 | customRequest(e) { 58 | _this.readerData(e.file).then(() => { 59 | e.onSuccess(); 60 | }); 61 | } 62 | }; 63 | }; 64 | readerData = (rawFile) => { 65 | return new Promise((resolve, reject) => { 66 | const reader = new FileReader(); 67 | reader.onload = (e) => { 68 | const data = e.target.result; 69 | const workbook = XLSX.read(data, { type: "array" }); 70 | const firstSheetName = workbook.SheetNames[0]; 71 | const worksheet = workbook.Sheets[firstSheetName]; 72 | const header = getHeaderRow(worksheet); 73 | const results = XLSX.utils.sheet_to_json(worksheet); 74 | this.generateData({ header, results }); 75 | resolve(); 76 | }; 77 | reader.readAsArrayBuffer(rawFile); 78 | }); 79 | }; 80 | generateData = ({ header, results }) => { 81 | this.setState({ 82 | excelData: { header, results }, 83 | }); 84 | this.props.uploadSuccess && this.props.uploadSuccess(this.state.excelData); 85 | }; 86 | render() { 87 | return ( 88 |
89 | 90 |

91 | 92 |

93 |

94 | Click or drag file to this area to upload 95 |

96 |
97 |
98 | ); 99 | } 100 | } 101 | 102 | export default UploadExcel; 103 | -------------------------------------------------------------------------------- /src/config/menuConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * icon:菜单项图标 3 | * roles:标明当前菜单项在何种角色下可以显示,如果不写此选项,表示该菜单项完全公开,在任何角色下都显示 4 | */ 5 | const menuList = [ 6 | { 7 | title: "首页", 8 | path: "/dashboard", 9 | icon: "home", 10 | roles:["admin","editor","guest"] 11 | }, 12 | { 13 | title: "作者博客", 14 | path: "/doc", 15 | icon: "file", 16 | roles:["admin","editor","guest"] 17 | }, 18 | { 19 | title: "引导页", 20 | path: "/guide", 21 | icon: "key", 22 | roles:["admin","editor"] 23 | }, 24 | { 25 | title: "权限测试", 26 | path: "/permission", 27 | icon: "lock", 28 | children: [ 29 | { 30 | title: "权限说明", 31 | path: "/permission/explanation", 32 | roles:["admin"] 33 | }, 34 | { 35 | title: "admin页面", 36 | path: "/permission/adminPage", 37 | roles:["admin"] 38 | }, 39 | { 40 | title: "guest页面", 41 | path: "/permission/guestPage", 42 | roles:["guest"] 43 | }, 44 | { 45 | title: "editor页面", 46 | path: "/permission/editorPage", 47 | roles:["editor"] 48 | }, 49 | ], 50 | }, 51 | { 52 | title: "组件", 53 | path: "/components", 54 | icon: "appstore", 55 | roles:["admin","editor"], 56 | children: [ 57 | { 58 | title: "富文本", 59 | path: "/components/richTextEditor", 60 | roles:["admin","editor"], 61 | }, 62 | { 63 | title: "Markdown", 64 | path: "/components/Markdown", 65 | roles:["admin","editor"], 66 | }, 67 | { 68 | title: "拖拽列表", 69 | path: "/components/draggable", 70 | roles:["admin","editor"], 71 | }, 72 | ], 73 | }, 74 | { 75 | title: "图表", 76 | path: "/charts", 77 | icon: "area-chart", 78 | roles:["admin","editor"], 79 | children: [ 80 | { 81 | title: "键盘图表", 82 | path: "/charts/keyboard", 83 | roles:["admin","editor"], 84 | }, 85 | { 86 | title: "折线图", 87 | path: "/charts/line", 88 | roles:["admin","editor"], 89 | }, 90 | { 91 | title: "混合图表", 92 | path: "/charts/mix-chart", 93 | roles:["admin","editor"], 94 | }, 95 | ], 96 | }, 97 | { 98 | title: "路由嵌套", 99 | path: "/nested", 100 | icon: "cluster", 101 | roles:["admin","editor"], 102 | children: [ 103 | { 104 | title: "菜单1", 105 | path: "/nested/menu1", 106 | children: [ 107 | { 108 | title: "菜单1-1", 109 | path: "/nested/menu1/menu1-1", 110 | roles:["admin","editor"], 111 | }, 112 | { 113 | title: "菜单1-2", 114 | path: "/nested/menu1/menu1-2", 115 | children: [ 116 | { 117 | title: "菜单1-2-1", 118 | path: "/nested/menu1/menu1-2/menu1-2-1", 119 | roles:["admin","editor"], 120 | }, 121 | ], 122 | }, 123 | ], 124 | }, 125 | ], 126 | }, 127 | { 128 | title: "表格", 129 | path: "/table", 130 | icon: "table", 131 | roles:["admin","editor"] 132 | }, 133 | { 134 | title: "Excel", 135 | path: "/excel", 136 | icon: "file-excel", 137 | roles:["admin","editor"], 138 | children: [ 139 | { 140 | title: "导出Excel", 141 | path: "/excel/export", 142 | roles:["admin","editor"] 143 | }, 144 | { 145 | title: "上传Excel", 146 | path: "/excel/upload", 147 | roles:["admin","editor"] 148 | } 149 | ], 150 | }, 151 | { 152 | title: "Zip", 153 | path: "/zip", 154 | icon: "file-zip", 155 | roles:["admin","editor"] 156 | }, 157 | { 158 | title: "剪贴板", 159 | path: "/clipboard", 160 | icon: "copy", 161 | roles:["admin","editor"] 162 | }, 163 | { 164 | title: "用户管理", 165 | path: "/user", 166 | icon: "usergroup-add", 167 | roles:["admin"] 168 | }, 169 | { 170 | title: "关于作者", 171 | path: "/about", 172 | icon: "user", 173 | roles:["admin","editor","guest"] 174 | }, 175 | { 176 | title: "Bug收集", 177 | path: "/bug", 178 | icon: "bug", 179 | roles:["admin"] 180 | }, 181 | ]; 182 | export default menuList; 183 | -------------------------------------------------------------------------------- /src/config/routeMap.js: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | import Loading from '@/components/Loading' 3 | const Dashboard = Loadable({loader: () => import(/*webpackChunkName:'Dashboard'*/'@/views/dashboard'),loading: Loading}); 4 | const Doc = Loadable({loader: () => import(/*webpackChunkName:'Doc'*/'@/views/doc'),loading: Loading}); 5 | const Guide = Loadable({loader: () => import(/*webpackChunkName:'Guide'*/'@/views/guide'),loading: Loading}); 6 | const Explanation = Loadable({loader: () => import(/*webpackChunkName:'Explanation'*/'@/views/permission'),loading: Loading}); 7 | const AdminPage = Loadable({loader: () => import(/*webpackChunkName:'AdminPage'*/'@/views/permission/adminPage'),loading: Loading}); 8 | const GuestPage = Loadable({loader: () => import(/*webpackChunkName:'GuestPage'*/'@/views/permission/guestPage'),loading: Loading}); 9 | const EditorPage = Loadable({loader: () => import(/*webpackChunkName:'EditorPage'*/'@/views/permission/editorPage'),loading: Loading}); 10 | const RichTextEditor = Loadable({loader: () => import(/*webpackChunkName:'RichTextEditor'*/'@/views/components-demo/richTextEditor'),loading: Loading}); 11 | const Markdown = Loadable({loader: () => import(/*webpackChunkName:'Markdown'*/'@/views/components-demo/Markdown'),loading: Loading}); 12 | const Draggable = Loadable({loader: () => import(/*webpackChunkName:'Draggable'*/'@/views/components-demo/draggable'),loading: Loading}); 13 | const KeyboardChart = Loadable({loader: () => import(/*webpackChunkName:'KeyboardChart'*/'@/views/charts/keyboard'),loading: Loading}); 14 | const LineChart = Loadable({loader: () => import(/*webpackChunkName:'LineChart'*/'@/views/charts/line'),loading: Loading}); 15 | const MixChart = Loadable({loader: () => import(/*webpackChunkName:'MixChart'*/'@/views/charts/mixChart'),loading: Loading}); 16 | const Menu1_1 = Loadable({loader: () => import(/*webpackChunkName:'Menu1_1'*/'@/views/nested/menu1/menu1-1'),loading: Loading}); 17 | const Menu1_2_1 = Loadable({loader: () => import(/*webpackChunkName:'Menu1_2_1'*/'@/views/nested/menu1/menu1-2/menu1-2-1'),loading: Loading}); 18 | const Table = Loadable({loader: () => import(/*webpackChunkName:'Table'*/'@/views/table'),loading: Loading}); 19 | const ExportExcel = Loadable({loader: () => import(/*webpackChunkName:'ExportExcel'*/'@/views/excel/exportExcel'),loading: Loading}); 20 | const UploadExcel = Loadable({ loader: () => import(/*webpackChunkName:'UploadExcel'*/'@/views/excel/uploadExcel'),loading: Loading }); 21 | const Zip = Loadable({loader: () => import(/*webpackChunkName:'Zip'*/'@/views/zip'),loading: Loading}); 22 | const Clipboard = Loadable({loader: () => import(/*webpackChunkName:'Clipboard'*/'@/views/clipboard'),loading: Loading}); 23 | const Error404 = Loadable({loader: () => import(/*webpackChunkName:'Error404'*/'@/views/error/404'),loading: Loading}); 24 | const User = Loadable({loader: () => import(/*webpackChunkName:'User'*/'@/views/user'),loading: Loading}); 25 | const About = Loadable({loader: () => import(/*webpackChunkName:'About'*/'@/views/about'),loading: Loading}); 26 | const Bug = Loadable({loader: () => import(/*webpackChunkName:'Bug'*/'@/views/bug'),loading: Loading}); 27 | 28 | export default [ 29 | { path: "/dashboard", component: Dashboard, roles: ["admin","editor","guest"] }, 30 | { path: "/doc", component: Doc, roles: ["admin","editor","guest"] }, 31 | { path: "/guide", component: Guide, roles: ["admin","editor"] }, 32 | { path: "/permission/explanation", component: Explanation, roles: ["admin"] }, 33 | { path: "/permission/adminPage", component: AdminPage, roles: ["admin"] }, 34 | { path: "/permission/guestPage", component: GuestPage, roles: ["guest"] }, 35 | { path: "/permission/editorPage", component: EditorPage, roles: ["editor"] }, 36 | { path: "/components/richTextEditor", component: RichTextEditor, roles: ["admin","editor"] }, 37 | { path: "/components/Markdown", component: Markdown, roles: ["admin","editor"] }, 38 | { path: "/components/draggable", component: Draggable, roles: ["admin","editor"] }, 39 | { path: "/charts/keyboard", component: KeyboardChart, roles: ["admin","editor"] }, 40 | { path: "/charts/line", component: LineChart, roles: ["admin","editor"] }, 41 | { path: "/charts/mix-chart", component: MixChart, roles: ["admin","editor"] }, 42 | { path: "/nested/menu1/menu1-1", component: Menu1_1, roles: ["admin","editor"] }, 43 | { path: "/nested/menu1/menu1-2/menu1-2-1", component: Menu1_2_1, roles: ["admin","editor"] }, 44 | { path: "/table", component: Table, roles: ["admin","editor"] }, 45 | { path: "/excel/export", component: ExportExcel, roles: ["admin","editor"] }, 46 | { path: "/excel/upload", component: UploadExcel, roles: ["admin","editor"] }, 47 | { path: "/zip", component: Zip, roles: ["admin","editor"] }, 48 | { path: "/clipboard", component: Clipboard, roles: ["admin","editor"] }, 49 | { path: "/user", component: User, roles: ["admin"] }, 50 | { path: "/about", component: About, roles: ["admin", "editor", "guest"] }, 51 | { path: "/bug", component: Bug, roles: ["admin"] }, 52 | { path: "/error/404", component: Error404 }, 53 | ]; 54 | -------------------------------------------------------------------------------- /src/defaultSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * @type {boolean} true | false 4 | * @description Whether show the settings right-panel 5 | */ 6 | showSettings: true, 7 | // 如果只想在开发环境下显示系统设置面板,生产环境下不显示,那么请打开下面这行代码 8 | // showSettings: process.env.NODE_ENV === "development", 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether show the logo in sidebar 13 | */ 14 | sidebarLogo: true, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether fix the header 19 | */ 20 | fixedHeader: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether need tagsView 25 | */ 26 | tagsView: true, 27 | }; 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "antd/dist/antd.less"; 5 | import "@/styles/index.less"; 6 | import "./mock"; 7 | import '@/lib/monitor'; 8 | 9 | ReactDOM.render(, document.getElementById("root")); 10 | -------------------------------------------------------------------------------- /src/lib/Export2Excel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('script-loader!file-saver'); 3 | import XLSX from 'xlsx' 4 | 5 | function generateArray(table) { 6 | var out = []; 7 | var rows = table.querySelectorAll('tr'); 8 | var ranges = []; 9 | for (var R = 0; R < rows.length; ++R) { 10 | var outRow = []; 11 | var row = rows[R]; 12 | var columns = row.querySelectorAll('td'); 13 | for (var C = 0; C < columns.length; ++C) { 14 | var cell = columns[C]; 15 | var colspan = cell.getAttribute('colspan'); 16 | var rowspan = cell.getAttribute('rowspan'); 17 | var cellValue = cell.innerText; 18 | if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue; 19 | 20 | //Skip ranges 21 | ranges.forEach(function (range) { 22 | if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) { 23 | for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null); 24 | } 25 | }); 26 | 27 | //Handle Row Span 28 | if (rowspan || colspan) { 29 | rowspan = rowspan || 1; 30 | colspan = colspan || 1; 31 | ranges.push({ 32 | s: { 33 | r: R, 34 | c: outRow.length 35 | }, 36 | e: { 37 | r: R + rowspan - 1, 38 | c: outRow.length + colspan - 1 39 | } 40 | }); 41 | }; 42 | 43 | //Handle Value 44 | outRow.push(cellValue !== "" ? cellValue : null); 45 | 46 | //Handle Colspan 47 | if (colspan) 48 | for (var k = 0; k < colspan - 1; ++k) outRow.push(null); 49 | } 50 | out.push(outRow); 51 | } 52 | return [out, ranges]; 53 | }; 54 | 55 | function datenum(v, date1904) { 56 | if (date1904) v += 1462; 57 | var epoch = Date.parse(v); 58 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); 59 | } 60 | 61 | function sheet_from_array_of_arrays(data, opts) { 62 | var ws = {}; 63 | var range = { 64 | s: { 65 | c: 10000000, 66 | r: 10000000 67 | }, 68 | e: { 69 | c: 0, 70 | r: 0 71 | } 72 | }; 73 | for (var R = 0; R != data.length; ++R) { 74 | for (var C = 0; C != data[R].length; ++C) { 75 | if (range.s.r > R) range.s.r = R; 76 | if (range.s.c > C) range.s.c = C; 77 | if (range.e.r < R) range.e.r = R; 78 | if (range.e.c < C) range.e.c = C; 79 | var cell = { 80 | v: data[R][C] 81 | }; 82 | if (cell.v == null) continue; 83 | var cell_ref = XLSX.utils.encode_cell({ 84 | c: C, 85 | r: R 86 | }); 87 | 88 | if (typeof cell.v === 'number') cell.t = 'n'; 89 | else if (typeof cell.v === 'boolean') cell.t = 'b'; 90 | else if (cell.v instanceof Date) { 91 | cell.t = 'n'; 92 | cell.z = XLSX.SSF._table[14]; 93 | cell.v = datenum(cell.v); 94 | } else cell.t = 's'; 95 | 96 | ws[cell_ref] = cell; 97 | } 98 | } 99 | if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); 100 | return ws; 101 | } 102 | 103 | function Workbook() { 104 | if (!(this instanceof Workbook)) return new Workbook(); 105 | this.SheetNames = []; 106 | this.Sheets = {}; 107 | } 108 | 109 | function s2ab(s) { 110 | var buf = new ArrayBuffer(s.length); 111 | var view = new Uint8Array(buf); 112 | for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; 113 | return buf; 114 | } 115 | 116 | export function export_table_to_excel(id) { 117 | var theTable = document.getElementById(id); 118 | var oo = generateArray(theTable); 119 | var ranges = oo[1]; 120 | 121 | /* original data */ 122 | var data = oo[0]; 123 | var ws_name = "SheetJS"; 124 | 125 | var wb = new Workbook(), 126 | ws = sheet_from_array_of_arrays(data); 127 | 128 | /* add ranges to worksheet */ 129 | // ws['!cols'] = ['apple', 'banan']; 130 | ws['!merges'] = ranges; 131 | 132 | /* add worksheet to workbook */ 133 | wb.SheetNames.push(ws_name); 134 | wb.Sheets[ws_name] = ws; 135 | 136 | var wbout = XLSX.write(wb, { 137 | bookType: 'xlsx', 138 | bookSST: false, 139 | type: 'binary' 140 | }); 141 | 142 | saveAs(new Blob([s2ab(wbout)], { 143 | type: "application/octet-stream" 144 | }), "test.xlsx") 145 | } 146 | 147 | export function export_json_to_excel({ 148 | multiHeader = [], 149 | header, 150 | data, 151 | filename, 152 | merges = [], 153 | autoWidth = true, 154 | bookType= 'xlsx' 155 | } = {}) { 156 | /* original data */ 157 | filename = filename || 'excel-list' 158 | data = [...data] 159 | data.unshift(header); 160 | 161 | for (let i = multiHeader.length-1; i > -1; i--) { 162 | data.unshift(multiHeader[i]) 163 | } 164 | 165 | var ws_name = "SheetJS"; 166 | var wb = new Workbook(), 167 | ws = sheet_from_array_of_arrays(data); 168 | 169 | if (merges.length > 0) { 170 | if (!ws['!merges']) ws['!merges'] = []; 171 | merges.forEach(item => { 172 | ws['!merges'].push(XLSX.utils.decode_range(item)) 173 | }) 174 | } 175 | 176 | if (autoWidth) { 177 | /*设置worksheet每列的最大宽度*/ 178 | const colWidth = data.map(row => row.map(val => { 179 | /*先判断是否为null/undefined*/ 180 | if (val == null) { 181 | return { 182 | 'wch': 10 183 | }; 184 | } 185 | /*再判断是否为中文*/ 186 | else if (val.toString().charCodeAt(0) > 255) { 187 | return { 188 | 'wch': val.toString().length * 2 189 | }; 190 | } else { 191 | return { 192 | 'wch': val.toString().length 193 | }; 194 | } 195 | })) 196 | /*以第一行为初始值*/ 197 | let result = colWidth[0]; 198 | for (let i = 1; i < colWidth.length; i++) { 199 | for (let j = 0; j < colWidth[i].length; j++) { 200 | if (result[j]['wch'] < colWidth[i][j]['wch']) { 201 | result[j]['wch'] = colWidth[i][j]['wch']; 202 | } 203 | } 204 | } 205 | ws['!cols'] = result; 206 | } 207 | 208 | /* add worksheet to workbook */ 209 | wb.SheetNames.push(ws_name); 210 | wb.Sheets[ws_name] = ws; 211 | 212 | var wbout = XLSX.write(wb, { 213 | bookType: bookType, 214 | bookSST: false, 215 | type: 'binary' 216 | }); 217 | saveAs(new Blob([s2ab(wbout)], { 218 | type: "application/octet-stream" 219 | }), `${filename}.${bookType}`); 220 | } 221 | -------------------------------------------------------------------------------- /src/lib/Export2Zip.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('script-loader!file-saver'); 3 | import JSZip from 'jszip' 4 | 5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 6 | const zip = new JSZip() 7 | const txt_name = txtName || 'file' 8 | const zip_name = zipName || 'file' 9 | const data = jsonData 10 | let txtData = `${th}\r\n` 11 | data.forEach((row) => { 12 | let tempStr = '' 13 | tempStr = row.toString() 14 | txtData += `${tempStr}\r\n` 15 | }) 16 | zip.file(`${txt_name}.txt`, txtData) 17 | zip.generateAsync({ 18 | type: "blob" 19 | }).then((blob) => { 20 | saveAs(blob, `${zip_name}.zip`) 21 | }, (err) => { 22 | alert('导出失败') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/echarts.js: -------------------------------------------------------------------------------- 1 | // 引入 ECharts 主模块 2 | import echarts from 'echarts/lib/echarts' 3 | // 引入提示框和标题组件 4 | import 'echarts/lib/component/legend' 5 | import 'echarts/lib/component/title' 6 | 7 | import 'echarts/lib/chart/bar' // 引入柱状图 8 | import 'echarts/lib/chart/radar' // 引入雷达图 9 | import 'echarts/lib/chart/pie' // 引入饼状图 10 | import 'echarts/lib/chart/line' // 引入折线图 11 | 12 | 13 | require('echarts/theme/macarons') // echarts theme 14 | export default echarts -------------------------------------------------------------------------------- /src/lib/monitor/index.js: -------------------------------------------------------------------------------- 1 | import { injectJsError } from './lib/jsError'; 2 | import { injectXHR } from './lib/xhr'; 3 | import { blankScreen } from './lib/blankScreen'; 4 | import { timing } from './lib/timing'; 5 | injectJsError(); 6 | // injectXHR(); 7 | // blankScreen(); 8 | // timing(); -------------------------------------------------------------------------------- /src/lib/monitor/lib/blankScreen.js: -------------------------------------------------------------------------------- 1 | import tracker from '../utils/tracker'; 2 | import onload from '../utils/onload'; 3 | export function blankScreen() { 4 | let wrapperElements = ['html', 'body', '#container', '.content']; 5 | let emptyPoints = 0; 6 | function getSelector(element) { 7 | if (element.id) { 8 | return "#" + element.id; 9 | } else if (element.className) {// a b c => .a.b.c 10 | return "." + element.className.split(' ').filter(item => !!item).join('.'); 11 | } else { 12 | return element.nodeName.toLowerCase(); 13 | } 14 | } 15 | function isWrapper(element) { 16 | let selector = getSelector(element); 17 | if (wrapperElements.indexOf(selector) !== -1) { 18 | emptyPoints++; 19 | } 20 | } 21 | onload(function () { 22 | for (let i = 1; i <= 9; i++) { 23 | let xElements = document.elementsFromPoint( 24 | window.innerWidth * i / 10, window.innerHeight / 2); 25 | let yElements = document.elementsFromPoint( 26 | window.innerWidth / 2, window.innerHeight * i / 10); 27 | isWrapper(xElements[0]); 28 | isWrapper(yElements[0]); 29 | } 30 | 31 | if (emptyPoints >= 18) { 32 | let centerElements = document.elementsFromPoint( 33 | window.innerWidth / 2, window.innerHeight / 2 34 | ); 35 | tracker.send({ 36 | kind: 'stability', 37 | type: 'blank', 38 | emptyPoints, 39 | screen: window.screen.width + "X" + window.screen.height, 40 | viewPoint: window.innerWidth + "X" + window.innerHeight, 41 | selector: getSelector(centerElements[0]) 42 | }); 43 | } 44 | }); 45 | 46 | } -------------------------------------------------------------------------------- /src/lib/monitor/lib/jsError.js: -------------------------------------------------------------------------------- 1 | 2 | import getLastEvent from '../utils/getLastEvent'; 3 | import getSelector from '../utils/getSelector'; 4 | import tracker from '../utils/tracker'; 5 | 6 | // 定义的错误类型码 7 | const ERROR_RUNTIME = 1 8 | const ERROR_SCRIPT = 2 9 | const ERROR_STYLE = 3 10 | const ERROR_IMAGE = 4 11 | const ERROR_AUDIO = 5 12 | const ERROR_VIDEO = 6 13 | const ERROR_CONSOLE = 7 14 | const ERROR_TRY_CATHC = 8 15 | 16 | const LOAD_ERROR_TYPE = { 17 | SCRIPT: ERROR_SCRIPT, 18 | LINK: ERROR_STYLE, 19 | IMG: ERROR_IMAGE, 20 | AUDIO: ERROR_AUDIO, 21 | VIDEO: ERROR_VIDEO 22 | } 23 | 24 | const JS_TRACKER_ERROR_DISPLAY_MAP = { 25 | 1: 'JS_RUNTIME_ERROR', 26 | 2: 'SCRIPT_LOAD_ERROR', 27 | 3: 'CSS_LOAD_ERROR', 28 | 4: 'IMAGE_LOAD_ERROR', 29 | 5: 'AUDIO_LOAD_ERROR', 30 | 6: 'VIDEO_LOAD_ERROR', 31 | 7: 'CONSOLE_ERROR', 32 | 8: 'TRY_CATCH_ERROR' 33 | } 34 | 35 | export function injectJsError() { 36 | //监听全局未捕获的错误 37 | window.addEventListener('error', function (event) {//错误事件对象 38 | let lastEvent = getLastEvent();//最后一个交互事件 39 | //这是一个脚本加载错误 40 | const errorTarget = event.target 41 | if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]) { 42 | tracker.send({ 43 | kind: 'stability',//监控指标的大类 44 | errorType: JS_TRACKER_ERROR_DISPLAY_MAP[LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]],//js或css资源加载错误 45 | desc: errorTarget.baseURI + '@' + (errorTarget.src || errorTarget.href), 46 | stack: 'no stack', 47 | selector: getSelector(errorTarget) //代表最后一个操作的元素 48 | }); 49 | } else { 50 | const { message, filename, lineno, colno, error } = event; 51 | tracker.send({ 52 | kind: 'stability',//监控指标的大类 53 | errorType: JS_TRACKER_ERROR_DISPLAY_MAP[ERROR_RUNTIME],//JS执行错误 54 | desc:`${message} at ${filename}:${lineno}:${colno}`, 55 | stack: error && error.stack ? error.stack : 'no stack', 56 | selector: lastEvent ? getSelector(lastEvent.path) : '' //代表最后一个操作的元素 57 | }); 58 | } 59 | }, true); 60 | window.addEventListener('unhandledrejection', (event) => { 61 | let lastEvent = getLastEvent();//最后一个交互事件 62 | let message; 63 | let filename; 64 | let lineno = 0; 65 | let colno = 0; 66 | let stack = ''; 67 | let reason = event.reason; 68 | if (typeof reason === 'string') { 69 | message = reason; 70 | } else if (typeof reason === 'object') {//说明是一个错误对象 71 | message = reason.message; 72 | if (reason.stack) { 73 | let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/); 74 | filename = matchResult[1]; 75 | lineno = matchResult[2]; 76 | colno = matchResult[3]; 77 | } 78 | stack = reason.stack; 79 | } 80 | tracker.send({ 81 | kind: 'stability',//监控指标的大类 82 | errorType: JS_TRACKER_ERROR_DISPLAY_MAP[ERROR_RUNTIME],//JS执行错误 83 | desc:`${message} at ${filename}:${lineno}:${colno}`, 84 | stack, 85 | selector: lastEvent ? getSelector(lastEvent.path) : '' //代表最后一个操作的元素 86 | }); 87 | }, true); 88 | } -------------------------------------------------------------------------------- /src/lib/monitor/lib/timing.js: -------------------------------------------------------------------------------- 1 | import tracker from '../utils/tracker'; 2 | import onload from '../utils/onload'; 3 | import getLastEvent from '../utils/getLastEvent'; 4 | import getSelector from '../utils/getSelector'; 5 | export function timing() { 6 | let FMP, LCP; 7 | // 增加一个性能条目的观察者 8 | if (PerformanceObserver) { 9 | new PerformanceObserver((entryList, observer) => { 10 | let perfEntries = entryList.getEntries(); 11 | FMP = perfEntries[0];//startTime 2000以后 12 | observer.disconnect();//不再观察了 13 | }).observe({ entryTypes: ['element'] });//观察页面中的意义的元素 14 | 15 | new PerformanceObserver((entryList, observer) => { 16 | let perfEntries = entryList.getEntries(); 17 | LCP = perfEntries[0]; 18 | observer.disconnect();//不再观察了 19 | }).observe({ entryTypes: ['largest-contentful-paint'] });//观察页面中的意义的元素 20 | 21 | new PerformanceObserver((entryList, observer) => { 22 | let lastEvent = getLastEvent(); 23 | let firstInput = entryList.getEntries()[0]; 24 | console.log('FID', firstInput); 25 | if (firstInput) { 26 | //processingStart开始处理的时间 startTime开点击的时间 差值就是处理的延迟 27 | let inputDelay = firstInput.processingStart - firstInput.startTime; 28 | let duration = firstInput.duration;//处理的耗时 29 | if (inputDelay > 0 || duration > 0) { 30 | tracker.send({ 31 | kind: 'experience',//用户体验指标 32 | type: 'firstInputDelay',//首次输入延迟 33 | inputDelay,//延时的时间 34 | duration,//处理的时间 35 | startTime: firstInput.startTime, 36 | selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : '' 37 | }); 38 | } 39 | 40 | } 41 | observer.disconnect();//不再观察了 42 | }).observe({ type: 'first-input', buffered: true });//观察页面中的意义的元素 43 | } 44 | 45 | //用户的第一次交互 点击页面 46 | onload(function () { 47 | setTimeout(() => { 48 | const { 49 | fetchStart, 50 | connectStart, 51 | connectEnd, 52 | requestStart, 53 | responseStart, 54 | responseEnd, 55 | domLoading, 56 | domInteractive, 57 | domContentLoadedEventStart, 58 | domContentLoadedEventEnd, 59 | loadEventStart 60 | } = performance.timing; 61 | tracker.send({ 62 | kind: 'experience',//用户体验指标 63 | type: 'timing',//统计每个阶段的时间 64 | connectTime: connectEnd - connectStart,//连接时间 65 | ttfbTime: responseStart - requestStart,//首字节到达时间 66 | responseTime: responseEnd - responseStart,//响应的读取时间 67 | parseDOMTime: loadEventStart - domLoading,//DOM解析的时间 68 | domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, 69 | timeToInteractive: domInteractive - fetchStart,//首次可交互时间 70 | loadTIme: loadEventStart - fetchStart //完整的加载时间 71 | }); 72 | 73 | 74 | let FP = performance.getEntriesByName('first-paint')[0]; 75 | let FCP = performance.getEntriesByName('first-contentful-paint')[0]; 76 | //开始发送性能指标 77 | console.log('FP', FP); 78 | console.log('FCP', FCP); 79 | console.log('FMP', FMP); 80 | console.log('LCP', LCP); 81 | tracker.send({ 82 | kind: 'experience',//用户体验指标 83 | type: 'paint',//统计每个阶段的时间 84 | firstPaint: FP.startTime, 85 | firstContentfulPaint: FCP.startTime, 86 | firstMeaningfulPaint: FMP.startTime, 87 | largestContentfulPaint: LCP.startTime 88 | }); 89 | }, 3000); 90 | }); 91 | 92 | } -------------------------------------------------------------------------------- /src/lib/monitor/lib/xhr.js: -------------------------------------------------------------------------------- 1 | import tracker from '../utils/tracker'; 2 | export function injectXHR() { 3 | let XMLHttpRequest = window.XMLHttpRequest; 4 | let oldOpen = XMLHttpRequest.prototype.open; 5 | XMLHttpRequest.prototype.open = function (method, url, async) { 6 | if (!url.match(/logstores/) && !url.match(/sockjs/)) { 7 | this.logData = { method, url, async }; 8 | } 9 | return oldOpen.apply(this, arguments); 10 | } 11 | //axios 背后有两种 如果 browser XMLHttpRequest node http 12 | let oldSend = XMLHttpRequest.prototype.send; 13 | //fetch怎么监听 14 | XMLHttpRequest.prototype.send = function (body) { 15 | if (this.logData) { 16 | let startTime = Date.now();//在发送之前记录一下开始的时间 17 | //XMLHttpRequest readyState 0 1 2 3 4 18 | //status 2xx 304 成功 其它 就是失败 19 | let handler = (type) => (event) => { 20 | let duration = Date.now() - startTime; 21 | let status = this.status;//200 500 22 | let statusText = this.statusText;// OK Server Error 23 | tracker.send({ 24 | kind: 'stability', 25 | type: 'xhr', 26 | eventType: type,//load error abort 27 | pathname: this.logData.url,//请求路径 28 | status: status + '-' + statusText,//状态码 29 | duration,//持续时间 30 | response: this.response ? JSON.stringify(this.response) : '',//响应体 31 | params: body || '' 32 | }); 33 | } 34 | this.addEventListener('load', handler('load'), false); 35 | this.addEventListener('error', handler('error'), false); 36 | this.addEventListener('abort', handler('abort'), false); 37 | } 38 | return oldSend.apply(this, arguments); 39 | } 40 | } -------------------------------------------------------------------------------- /src/lib/monitor/utils/getLastEvent.js: -------------------------------------------------------------------------------- 1 | let lastEvent; 2 | ['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(eventType => { 3 | document.addEventListener(eventType, (event) => { 4 | lastEvent = event; 5 | }, { 6 | capture: true,//捕获阶段 7 | passive: true//默认不阻止默认事件 8 | }); 9 | }); 10 | export default function () { 11 | return lastEvent; 12 | } -------------------------------------------------------------------------------- /src/lib/monitor/utils/getSelector.js: -------------------------------------------------------------------------------- 1 | 2 | function getSelectors(path) { 3 | return path.reverse().filter(element => { 4 | return element !== document && element !== window; 5 | }).map(element => { 6 | let selector = ""; 7 | if (element.id) { 8 | return `${element.nodeName.toLowerCase()}#${element.id}`; 9 | } else if (element.className && typeof element.className === 'string') { 10 | return `${element.nodeName.toLowerCase()}.${element.className}`; 11 | } else { 12 | selector = element.nodeName.toLowerCase(); 13 | } 14 | return selector; 15 | }).join(' '); 16 | } 17 | export default function (pathsOrTarget) { 18 | if (Array.isArray(pathsOrTarget)) {//可能是一个数组 19 | return getSelectors(pathsOrTarget); 20 | } else {//也有可有是一个对象 21 | let path = []; 22 | while (pathsOrTarget) { 23 | path.push(pathsOrTarget); 24 | pathsOrTarget = pathsOrTarget.parentNode; 25 | } 26 | return getSelectors(path); 27 | } 28 | } -------------------------------------------------------------------------------- /src/lib/monitor/utils/onload.js: -------------------------------------------------------------------------------- 1 | export default function (callback) { 2 | if (document.readyState === 'complete') { 3 | callback(); 4 | } else { 5 | window.addEventListener('load', callback); 6 | } 7 | } -------------------------------------------------------------------------------- /src/lib/monitor/utils/tracker.js: -------------------------------------------------------------------------------- 1 | import { tracker } from "@/api/monitor"; 2 | import userAgent from "user-agent"; 3 | import store from "@/store"; 4 | import { addBug } from "@/store/actions" 5 | 6 | function getExtraData() { 7 | return { 8 | title: document.title, 9 | url: window.location.href, 10 | timestamp: Date.now(), 11 | userAgent: userAgent.parse(navigator.userAgent).name, 12 | }; 13 | } 14 | //gif图片做上传 图片速度 快没有跨域 问题, 15 | class SendTracker { 16 | // send(data = {}) { 17 | // let extraData = getExtraData(); 18 | // let logInfo = { ...extraData, ...data }; 19 | 20 | // // 图片打点 21 | // const img = new window.Image(); 22 | // img.src = `${feeTarget}?d=${encodeURIComponent(JSON.stringify(logInfo))}`; 23 | // } 24 | send(data = {}) { 25 | let extraData = getExtraData(); 26 | let logInfo = { ...extraData, ...data }; 27 | tracker(logInfo); 28 | store.dispatch(addBug(logInfo)); 29 | } 30 | } 31 | 32 | export default new SendTracker(); 33 | -------------------------------------------------------------------------------- /src/mock/excel.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | const list = [] 3 | const count = 20 4 | 5 | for (let i = 0; i < count; i++) { 6 | list.push(Mock.mock({ 7 | id: '@increment', 8 | title: '@ctitle(5, 10)', 9 | author: '@cname', 10 | readings: '@integer(300, 5000)', 11 | date: '@datetime' 12 | })) 13 | } 14 | export default { 15 | excelList: (_) => { 16 | return { 17 | code: 20000, 18 | data: { items: list } 19 | } 20 | }, 21 | }; -------------------------------------------------------------------------------- /src/mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from "mockjs"; 2 | import loginAPI from "./login"; 3 | import remoteSearchAPI from "./remoteSearch"; 4 | import excelAPI from "./excel"; 5 | import tableAPI from "./table"; 6 | import monitor from "./monitor"; 7 | 8 | // 登录与用户相关 9 | Mock.mock(/\/login/, "post", loginAPI.login); 10 | Mock.mock(/\/logout/, "post", loginAPI.logout); 11 | Mock.mock(/\/userInfo/, "post", loginAPI.userInfo); 12 | Mock.mock(/\/user\/list/, "get", loginAPI.getUsers); 13 | Mock.mock(/\/user\/delete/, "post", loginAPI.deleteUser); 14 | Mock.mock(/\/user\/edit/, "post", loginAPI.editUser); 15 | Mock.mock(/\/user\/validatUserID/, "post", loginAPI.ValidatUserID); 16 | Mock.mock(/\/user\/add/, "post", loginAPI.addUser); 17 | 18 | 19 | // dashboard 20 | Mock.mock(/\/transaction\/list/, "get", remoteSearchAPI.transactionList); 21 | 22 | // excel 23 | Mock.mock(/\/excel\/list/, "get", excelAPI.excelList); 24 | 25 | // table 26 | Mock.mock(/\/table\/list/, "post", tableAPI.tableList); 27 | Mock.mock(/\/table\/delete/, "post", tableAPI.deleteItem); 28 | Mock.mock(/\/table\/edit/, "post", tableAPI.editItem); 29 | 30 | // monitor 31 | Mock.mock(/\/monitor/, "post", monitor.monitor); 32 | 33 | export default Mock; 34 | -------------------------------------------------------------------------------- /src/mock/login.js: -------------------------------------------------------------------------------- 1 | const tokens = { 2 | admin: "admin-token", 3 | guest: "guest-token", 4 | editor: "editor-token", 5 | }; 6 | 7 | const users = { 8 | "admin-token": { 9 | id: "admin", 10 | role: "admin", 11 | name: "难凉热血", 12 | avatar: "https://s1.ax1x.com/2020/04/28/J5hUaT.jpg", 13 | description: "拥有系统内所有菜单和路由权限", 14 | }, 15 | "editor-token": { 16 | id: "editor", 17 | role: "editor", 18 | name: "编辑员", 19 | avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png", 20 | description:"可以看到除户管理页面之外的所有页面", 21 | }, 22 | "guest-token": { 23 | id: "guest", 24 | role: "guest", 25 | name: "游客", 26 | avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png", 27 | description:"仅能看到Dashboard、作者博客、权限测试和关于作者四个页面", 28 | }, 29 | }; 30 | 31 | export default { 32 | login: (config) => { 33 | const { username } = JSON.parse(config.body); 34 | const token = tokens[username]; 35 | if (!token) { 36 | return { 37 | status: 1, 38 | message: "用户名或密码错误", 39 | }; 40 | } 41 | return { 42 | status: 0, 43 | token, 44 | }; 45 | }, 46 | userInfo: (config) => { 47 | const token = config.body; 48 | const userInfo = users[token]; 49 | if (!userInfo) { 50 | return { 51 | status: 1, 52 | message: "获取用户信息失败", 53 | }; 54 | } 55 | return { 56 | status: 0, 57 | userInfo, 58 | }; 59 | }, 60 | getUsers: () => { 61 | return { 62 | status: 0, 63 | users: Object.values(users), 64 | }; 65 | }, 66 | deleteUser: (config) => { 67 | const { id } = JSON.parse(config.body); 68 | const token = tokens[id]; 69 | if (token) { 70 | delete tokens[id]; 71 | delete users[token]; 72 | } 73 | return { 74 | status: 0, 75 | }; 76 | }, 77 | editUser: (config) => { 78 | const data = JSON.parse(config.body); 79 | const { id } = data; 80 | const token = tokens[id]; 81 | if (token) { 82 | users[token] = { ...users[token], ...data }; 83 | } 84 | return { 85 | status: 0, 86 | }; 87 | }, 88 | ValidatUserID: (config) => { 89 | const userID = config.body; 90 | const token = tokens[userID]; 91 | if (token) { 92 | return { 93 | status: 1, 94 | }; 95 | } else { 96 | return { 97 | status: 0 98 | }; 99 | } 100 | }, 101 | addUser: (config) => { 102 | const data = JSON.parse(config.body); 103 | const { id } = data; 104 | tokens[id] = `${id}-token` 105 | users[`${id}-token`] = { 106 | ...users["guest-token"], 107 | ...data 108 | } 109 | return { 110 | status: 0, 111 | }; 112 | }, 113 | logout: (_) => { 114 | return { 115 | status: 0, 116 | data: "success", 117 | }; 118 | }, 119 | }; 120 | -------------------------------------------------------------------------------- /src/mock/monitor.js: -------------------------------------------------------------------------------- 1 | export default { 2 | monitor: (config) => { 3 | return { 4 | status: 1, 5 | message: "monitor", 6 | }; 7 | } 8 | } -------------------------------------------------------------------------------- /src/mock/remoteSearch.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | const list = [] 3 | const count = 20 4 | 5 | for (let i = 0; i < count; i++) { 6 | list.push(Mock.mock({ 7 | key: '@increment', 8 | order_no: '@guid()', 9 | price: '@float(1000, 15000, 0, 2)', 10 | 'tag|1': ['success', 'pending'] 11 | })) 12 | } 13 | export default { 14 | transactionList: (_) => { 15 | return { 16 | code: 20000, 17 | data: { items: list } 18 | } 19 | }, 20 | }; -------------------------------------------------------------------------------- /src/mock/table.js: -------------------------------------------------------------------------------- 1 | import Mock from "mockjs"; 2 | let List = []; 3 | const count = 100; 4 | 5 | for (let i = 0; i < count; i++) { 6 | List.push( 7 | Mock.mock({ 8 | id: i, 9 | title: "@ctitle(5, 10)", 10 | author: "@cname", 11 | readings: "@integer(300, 5000)", 12 | "star|1-3": "★", 13 | "status|1": ["published", "draft"], 14 | date: "@datetime", 15 | }) 16 | ); 17 | } 18 | export default { 19 | tableList: (config) => { 20 | const { pageNumber, pageSize, title, status, star } = JSON.parse( 21 | config.body 22 | ); 23 | let start = (pageNumber - 1) * pageSize; 24 | let end = pageNumber * pageSize; 25 | let mockList = List.filter((item) => { 26 | if (star && item.star.length !== star) return false; 27 | if (status && item.status !== status) return false; 28 | if (title && item.title.indexOf(title) < 0) return false; 29 | return true; 30 | }); 31 | let pageList = mockList.slice(start, end); 32 | return { 33 | code: 20000, 34 | data: { 35 | total: mockList.length, 36 | items: pageList, 37 | }, 38 | }; 39 | }, 40 | deleteItem: (config) => { 41 | const { id } = JSON.parse(config.body); 42 | const item = List.filter((item) => item.id === id); 43 | const index = List.indexOf(item[0]); 44 | List.splice(index, 1); 45 | return { 46 | code: 20000, 47 | }; 48 | }, 49 | editItem: (config) => { 50 | const data = JSON.parse(config.body); 51 | const { id } = data; 52 | const item = List.filter((item) => item.id === id); 53 | const index = List.indexOf(item[0]); 54 | List.splice(index, 1, data); 55 | return { 56 | code: 20000, 57 | }; 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HashRouter, Route, Switch, Redirect } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { getUserInfo } from "@/store/actions"; 5 | import Layout from "@/views/layout"; 6 | import Login from "@/views/login"; 7 | class Router extends React.Component { 8 | render() { 9 | const { token, role, getUserInfo } = this.props; 10 | return ( 11 | 12 | 13 | 14 | { 17 | if (!token) { 18 | return ; 19 | } else { 20 | if (role) { 21 | return ; 22 | } else { 23 | getUserInfo(token).then(() => ); 24 | } 25 | } 26 | }} 27 | /> 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default connect((state) => state.user, { getUserInfo })(Router); 35 | -------------------------------------------------------------------------------- /src/store/action-types.js: -------------------------------------------------------------------------------- 1 | export const USER_SET_USER_TOKEN = "USER_SET_USER_TOKEN"; 2 | export const USER_SET_USER_INFO = "USER_SET_USER_INFO"; 3 | export const USER_RESET_USER = "USER_RESET_USER"; 4 | 5 | // app 6 | export const APP_TOGGLE_SIDEBAR = "APP_TOGGLE_SIDEBAR"; 7 | export const APP_TOGGLE_SETTINGPANEL = "APP_TOGGLE_SETTINGPANEL"; 8 | 9 | // settings 10 | export const SETTINGS_CHANGE_SETTINGS = "SETTINGS_CHANGE_SETTINGS"; 11 | 12 | // tagsView 13 | export const TAGSVIEW_ADD_TAG = "TAGSVIEW_ADD_TAG"; 14 | export const TAGSVIEW_DELETE_TAG = "TAGSVIEW_DELETE_TAG"; 15 | export const TAGSVIEW_EMPTY_TAGLIST = "TAGSVIEW_EMPTY_TAGLIST"; 16 | export const TAGSVIEW_CLOSE_OTHER_TAGS = "TAGSVIEW_CLOSE_OTHER_TAGS"; 17 | 18 | // monitor 19 | export const BUG_ADD_BUG = "BUG_ADD_BUG"; 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/store/actions/app.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | export const toggleSiderBar = () => { 3 | return { 4 | type: types.APP_TOGGLE_SIDEBAR 5 | }; 6 | }; 7 | 8 | export const toggleSettingPanel = () => { 9 | return { 10 | type: types.APP_TOGGLE_SETTINGPANEL 11 | }; 12 | }; -------------------------------------------------------------------------------- /src/store/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { setUserToken, resetUser } from "./user"; 2 | import { reqLogin, reqLogout } from "@/api/login"; 3 | import { setToken, removeToken } from "@/utils/auth"; 4 | export const login = (username, password) => (dispatch) => { 5 | return new Promise((resolve, reject) => { 6 | reqLogin({ username: username.trim(), password: password }) 7 | .then((response) => { 8 | const { data } = response; 9 | if (data.status === 0) { 10 | const token = data.token; 11 | dispatch(setUserToken(token)); 12 | setToken(token); 13 | resolve(data); 14 | } else { 15 | const msg = data.message; 16 | reject(msg); 17 | } 18 | }) 19 | .catch((error) => { 20 | reject(error); 21 | }); 22 | }); 23 | }; 24 | 25 | export const logout = (token) => (dispatch) => { 26 | return new Promise((resolve, reject) => { 27 | reqLogout(token) 28 | .then((response) => { 29 | const { data } = response; 30 | if (data.status === 0) { 31 | dispatch(resetUser()); 32 | removeToken(); 33 | resolve(data); 34 | } else { 35 | const msg = data.message; 36 | reject(msg); 37 | } 38 | }) 39 | .catch((error) => { 40 | reject(error); 41 | }); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | import { login, logout } from "./auth"; 2 | import { getUserInfo, setUserToken, setUserInfo, resetUser } from "./user"; 3 | import { toggleSiderBar, toggleSettingPanel } from "./app"; 4 | import { changeSetting } from "./settings"; 5 | import { addTag, emptyTaglist, deleteTag, closeOtherTags } from "./tagsView"; 6 | import { addBug } from "./monitor"; 7 | 8 | export { 9 | login, 10 | logout, 11 | getUserInfo, 12 | setUserToken, 13 | setUserInfo, 14 | resetUser, 15 | toggleSiderBar, 16 | toggleSettingPanel, 17 | changeSetting, 18 | addTag, 19 | emptyTaglist, 20 | deleteTag, 21 | closeOtherTags, 22 | addBug 23 | }; 24 | -------------------------------------------------------------------------------- /src/store/actions/monitor.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | export const addBug = (bug) => { 3 | return { 4 | type: types.BUG_ADD_BUG, 5 | bug 6 | }; 7 | }; -------------------------------------------------------------------------------- /src/store/actions/settings.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | export const changeSetting = (data) => { 3 | return { 4 | type: types.SETTINGS_CHANGE_SETTINGS, 5 | ...data, 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/actions/tagsView.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | export const addTag = (tag) => { 3 | return { 4 | type: types.TAGSVIEW_ADD_TAG, 5 | tag 6 | }; 7 | }; 8 | 9 | export const emptyTaglist = () => { 10 | return { 11 | type: types.TAGSVIEW_EMPTY_TAGLIST 12 | }; 13 | }; 14 | 15 | export const deleteTag = (tag) => { 16 | return { 17 | type: types.TAGSVIEW_DELETE_TAG, 18 | tag 19 | }; 20 | }; 21 | 22 | export const closeOtherTags = (tag) => { 23 | return { 24 | type: types.TAGSVIEW_CLOSE_OTHER_TAGS, 25 | tag 26 | }; 27 | }; -------------------------------------------------------------------------------- /src/store/actions/user.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | import { reqUserInfo } from "@/api/user"; 3 | 4 | export const getUserInfo = (token) => (dispatch) => { 5 | return new Promise((resolve, reject) => { 6 | reqUserInfo(token) 7 | .then((response) => { 8 | const { data } = response; 9 | if (data.status === 0) { 10 | const userInfo = data.userInfo; 11 | dispatch(setUserInfo(userInfo)); 12 | resolve(data); 13 | } else { 14 | const msg = data.message; 15 | reject(msg); 16 | } 17 | }) 18 | .catch((error) => { 19 | reject(error); 20 | }); 21 | }); 22 | }; 23 | 24 | export const setUserToken = (token) => { 25 | return { 26 | type: types.USER_SET_USER_TOKEN, 27 | token, 28 | }; 29 | }; 30 | 31 | export const setUserInfo = (userInfo) => { 32 | return { 33 | type: types.USER_SET_USER_INFO, 34 | ...userInfo, 35 | }; 36 | }; 37 | 38 | export const resetUser = () => { 39 | return { 40 | type: types.USER_RESET_USER, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import reduxThunk from 'redux-thunk' 3 | import reducer from './reducers' 4 | 5 | const store = createStore(reducer, applyMiddleware(reduxThunk)); 6 | 7 | export default store -------------------------------------------------------------------------------- /src/store/reducers/app.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | const initState = { 3 | sidebarCollapsed: false, 4 | settingPanelVisible: false, 5 | }; 6 | export default function app(state = initState, action) { 7 | switch (action.type) { 8 | case types.APP_TOGGLE_SIDEBAR: 9 | return { 10 | ...state, 11 | sidebarCollapsed: !state.sidebarCollapsed, 12 | }; 13 | case types.APP_TOGGLE_SETTINGPANEL: 14 | return { 15 | ...state, 16 | settingPanelVisible: !state.settingPanelVisible, 17 | }; 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import user from "./user"; 3 | import app from "./app"; 4 | import settings from "./settings"; 5 | import tagsView from "./tagsView"; 6 | import monitor from "./monitor"; 7 | 8 | export default combineReducers({ 9 | user, 10 | app, 11 | settings, 12 | tagsView, 13 | monitor 14 | }); 15 | -------------------------------------------------------------------------------- /src/store/reducers/monitor.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | const initState = { 3 | bugList: [], 4 | }; 5 | export default function app(state = initState, action) { 6 | switch (action.type) { 7 | case types.BUG_ADD_BUG: 8 | return { 9 | bugList: [...state.bugList, action.bug], 10 | }; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/store/reducers/settings.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | import defaultSettings from "@/defaultSettings"; 3 | const { showSettings, sidebarLogo, fixedHeader, tagsView } = defaultSettings; 4 | 5 | const initState = { 6 | showSettings: showSettings, 7 | sidebarLogo: sidebarLogo, 8 | fixedHeader: fixedHeader, 9 | tagsView: tagsView, 10 | }; 11 | export default function settings(state = initState, action) { 12 | switch (action.type) { 13 | case types.SETTINGS_CHANGE_SETTINGS: 14 | const { key, value } = action; 15 | if (state.hasOwnProperty(key)) { 16 | return { 17 | ...state, 18 | [key]: value, 19 | }; 20 | } 21 | return state; 22 | default: 23 | return state; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/store/reducers/tagsView.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | const initState = { 3 | taglist: [], 4 | }; 5 | export default function app(state = initState, action) { 6 | switch (action.type) { 7 | case types.TAGSVIEW_ADD_TAG: 8 | const tag = action.tag; 9 | if (state.taglist.includes(tag)) { 10 | return state; 11 | } else { 12 | return { 13 | ...state, 14 | taglist: [...state.taglist, tag], 15 | }; 16 | } 17 | case types.TAGSVIEW_DELETE_TAG: 18 | return { 19 | ...state, 20 | taglist: [...state.taglist.filter((item) => item !== action.tag)], 21 | }; 22 | case types.TAGSVIEW_EMPTY_TAGLIST: 23 | return { 24 | ...state, 25 | taglist: [ 26 | ...state.taglist.filter((item) => item.path === "/dashboard"), 27 | ], 28 | }; 29 | case types.TAGSVIEW_CLOSE_OTHER_TAGS: 30 | return { 31 | ...state, 32 | taglist: [ 33 | ...state.taglist.filter((item) => item.path === "/dashboard" || item === action.tag), 34 | ], 35 | }; 36 | default: 37 | return state; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/store/reducers/user.js: -------------------------------------------------------------------------------- 1 | import * as types from "../action-types"; 2 | import { getToken } from "@/utils/auth"; 3 | const initUserInfo = { 4 | name: "", 5 | role: "", 6 | avatar:"", 7 | token: getToken(), 8 | }; 9 | export default function user(state = initUserInfo, action) { 10 | switch (action.type) { 11 | case types.USER_SET_USER_TOKEN: 12 | return { 13 | ...state, 14 | token: action.token 15 | }; 16 | case types.USER_SET_USER_INFO: 17 | return { 18 | ...state, 19 | name: action.name, 20 | role: action.role, 21 | avatar: action.avatar, 22 | }; 23 | case types.USER_RESET_USER: 24 | return {}; 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './transition.less'; 2 | 3 | body { 4 | height: 100%; 5 | -moz-osx-font-smoothing: grayscale; 6 | -webkit-font-smoothing: antialiased; 7 | text-rendering: optimizeLegibility; 8 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 9 | Microsoft YaHei, Arial, sans-serif; 10 | } 11 | 12 | html { 13 | height: 100%; 14 | box-sizing: border-box; 15 | } 16 | #root { 17 | height: 100%; 18 | } 19 | .ant-layout { 20 | height: 100%; 21 | } 22 | *, 23 | *:before, 24 | *:after { 25 | box-sizing: inherit; 26 | } 27 | 28 | .no-padding { 29 | padding: 0px !important; 30 | } 31 | 32 | .padding-content { 33 | padding: 4px 0; 34 | } 35 | 36 | a:focus, 37 | a:active { 38 | outline: none; 39 | } 40 | 41 | a, 42 | a:focus, 43 | a:hover { 44 | cursor: pointer; 45 | // color: inherit; 46 | text-decoration: none; 47 | } 48 | 49 | div:focus { 50 | outline: none; 51 | } 52 | 53 | .fr { 54 | float: right; 55 | } 56 | 57 | .fl { 58 | float: left; 59 | } 60 | 61 | .pr-5 { 62 | padding-right: 5px; 63 | } 64 | 65 | .pl-5 { 66 | padding-left: 5px; 67 | } 68 | 69 | .block { 70 | display: block; 71 | } 72 | 73 | .pointer { 74 | cursor: pointer; 75 | } 76 | 77 | .inlineBlock { 78 | display: block; 79 | } 80 | 81 | .clearfix { 82 | &:after { 83 | visibility: hidden; 84 | display: block; 85 | font-size: 0; 86 | content: " "; 87 | clear: both; 88 | height: 0; 89 | } 90 | } 91 | 92 | .app-container { 93 | padding: 20px; 94 | } 95 | -------------------------------------------------------------------------------- /src/styles/transition.less: -------------------------------------------------------------------------------- 1 | // 路由转场动画 2 | .fade-enter { 3 | opacity: 0; 4 | transform: translateX(-30px); 5 | } 6 | 7 | .fade-enter-active, 8 | .fade-exit-active { 9 | opacity: 1; 10 | transition: all 500ms ease-out; 11 | transform: translateX(0); 12 | } 13 | 14 | .fade-exit { 15 | opacity: 0; 16 | transform: translateX(30px); 17 | } -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Clipboard from "clipboard"; 2 | import { message } from "antd"; 3 | 4 | function clipboardSuccess() { 5 | message.success("复制成功"); 6 | } 7 | 8 | function clipboardError() { 9 | message.error("复制失败"); 10 | } 11 | 12 | export default function handleClipboard(text, event) { 13 | const clipboard = new Clipboard(event.target, { 14 | text: () => text, 15 | }); 16 | clipboard.on("success", () => { 17 | clipboardSuccess(); 18 | clipboard.destroy(); 19 | }); 20 | clipboard.on("error", () => { 21 | clipboardError(); 22 | clipboard.destroy(); 23 | }); 24 | clipboard.onClick(event); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, wait, immediate) { 2 | let timeout, args, context, timestamp, result; 3 | 4 | const later = function () { 5 | // 据上一次触发时间间隔 6 | const last = +new Date() - timestamp; 7 | 8 | // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait 9 | if (last < wait && last > 0) { 10 | timeout = setTimeout(later, wait - last); 11 | } else { 12 | timeout = null; 13 | // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 14 | if (!immediate) { 15 | result = func.apply(context, args); 16 | if (!timeout) context = args = null; 17 | } 18 | } 19 | }; 20 | 21 | return function (...args) { 22 | context = this; 23 | timestamp = +new Date(); 24 | const callNow = immediate && !timeout; 25 | // 如果延时不存在,重新设定延时 26 | if (!timeout) timeout = setTimeout(later, wait); 27 | if (callNow) { 28 | result = func.apply(context, args); 29 | context = args = null; 30 | } 31 | 32 | return result; 33 | }; 34 | } 35 | // 根据某个属性值从MenuList查找拥有该属性值的menuItem 36 | export function getMenuItemInMenuListByProperty(menuList, key, value) { 37 | let stack = []; 38 | stack = stack.concat(menuList); 39 | let res; 40 | while (stack.length) { 41 | let cur = stack.shift(); 42 | if (cur.children && cur.children.length > 0) { 43 | stack = cur.children.concat(stack); 44 | } 45 | if (value === cur[key]) { 46 | res = cur; 47 | } 48 | } 49 | return res; 50 | } 51 | 52 | /** 53 | * @description 将时间戳转换为年-月-日-时-分-秒格式 54 | * @param {String} timestamp 55 | * @returns {String} 年-月-日-时-分-秒 56 | */ 57 | 58 | export function timestampToTime(timestamp) { 59 | var date = new Date(timestamp); 60 | var Y = date.getFullYear() + '-'; 61 | var M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-'; 62 | var D = (date.getDate() < 10 ? '0'+date.getDate() : date.getDate()) + ' '; 63 | var h = (date.getHours() < 10 ? '0'+date.getHours() : date.getHours()) + ':'; 64 | var m = (date.getMinutes() < 10 ? '0'+date.getMinutes() : date.getMinutes()) + ':'; 65 | var s = (date.getSeconds() < 10 ? '0'+date.getSeconds() : date.getSeconds()); 66 | 67 | let strDate = Y+M+D+h+m+s; 68 | return strDate; 69 | } -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "@/store"; 3 | import { Modal } from "antd"; 4 | import { getToken } from "@/utils/auth"; 5 | import { logout } from "@/store/actions"; 6 | 7 | //创建一个axios示例 8 | const service = axios.create({ 9 | baseURL: process.env.REACT_APP_BASE_API, // api 的 base_url 10 | timeout: 5000, // request timeout 11 | }); 12 | 13 | // 请求拦截器 14 | service.interceptors.request.use( 15 | (config) => { 16 | // Do something before request is sent 17 | if (store.getState().user.token) { 18 | // 让每个请求携带token-- ['Authorization']为自定义key 请根据实际情况自行修改 19 | config.headers.Authorization = getToken(); 20 | } 21 | return config; 22 | }, 23 | (error) => { 24 | // Do something with request error 25 | console.log(error); // for debug 26 | Promise.reject(error); 27 | } 28 | ); 29 | 30 | // 响应拦截器 31 | service.interceptors.response.use( 32 | (response) => response, 33 | /** 34 | * 下面的注释为通过在response里,自定义code来标示请求状态 35 | * 当code返回如下情况则说明权限有问题,登出并返回到登录页 36 | * 如想通过 xmlhttprequest 来状态码标识 逻辑可写在下面error中 37 | * 以下代码均为样例,请结合自生需求加以修改,若不需要,则可删除 38 | */ 39 | // response => { 40 | // const res = response.data 41 | // if (res.code !== 20000) { 42 | // Message({ 43 | // message: res.message, 44 | // type: 'error', 45 | // duration: 5 * 1000 46 | // }) 47 | // // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; 48 | // if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 49 | // // 请自行在引入 MessageBox 50 | // // import { Message, MessageBox } from 'element-ui' 51 | // MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', { 52 | // confirmButtonText: '重新登录', 53 | // cancelButtonText: '取消', 54 | // type: 'warning' 55 | // }).then(() => { 56 | // store.dispatch('FedLogOut').then(() => { 57 | // location.reload() // 为了重新实例化vue-router对象 避免bug 58 | // }) 59 | // }) 60 | // } 61 | // return Promise.reject('error') 62 | // } else { 63 | // return response.data 64 | // } 65 | // }, 66 | (error) => { 67 | console.log("err" + error); // for debug 68 | const { status } = error.response; 69 | if (status === 403) { 70 | Modal.confirm({ 71 | title: "确定登出?", 72 | content: 73 | "由于长时间未操作,您已被登出,可以取消继续留在该页面,或者重新登录", 74 | okText: "重新登录", 75 | cancelText: "取消", 76 | onOk() { 77 | let token = store.getState().user.token; 78 | store.dispatch(logout(token)); 79 | }, 80 | onCancel() { 81 | console.log("Cancel"); 82 | }, 83 | }); 84 | } 85 | return Promise.reject(error); 86 | } 87 | ); 88 | 89 | export default service; 90 | -------------------------------------------------------------------------------- /src/utils/typing.js: -------------------------------------------------------------------------------- 1 | class Typing { 2 | constructor(opts) { 3 | this.opts = opts || {}; 4 | this.source = opts.source; 5 | this.output = opts.output; 6 | this.delay = opts.delay || 120; 7 | this.chain = { 8 | parent: null, 9 | dom: this.output, 10 | val: [] 11 | }; 12 | if (!(typeof this.opts.done === 'function')) this.opts.done = function () { 13 | }; 14 | } 15 | 16 | init() { 17 | //初始化函数 18 | this.chain.val = this.convert(this.source, this.chain.val); 19 | } 20 | 21 | convert(dom, arr) { 22 | //将dom节点的子节点转换成数组, 23 | let children = Array.from(dom.childNodes) 24 | for (let i = 0; i < children.length; i++) { 25 | let node = children[i] 26 | if (node.nodeType === 3) { 27 | arr = arr.concat(node.nodeValue.split('')) //将字符串转换成字符串数组,后面打印时才会一个一个的打印 28 | } else if (node.nodeType === 1) { 29 | let val = [] 30 | val = this.convert(node, val) 31 | arr.push({ 32 | 'dom': node, 33 | 'val': val 34 | }) 35 | } 36 | } 37 | return arr 38 | } 39 | 40 | print(dom, val, callback) { 41 | setTimeout(function () { 42 | dom.appendChild(document.createTextNode(val)); 43 | callback(); 44 | }, this.delay); 45 | } 46 | 47 | play(ele) { 48 | //当打印最后一个字符时,动画完毕,执行done 49 | if (!ele.val.length) { 50 | if (ele.parent) this.play(ele.parent); 51 | else this.opts.done(); 52 | return; 53 | } 54 | let current = ele.val.shift() //获取第一个元素,同时删除数组中的第一个元素 55 | if (typeof current === 'string') { 56 | this.print(ele.dom, current, () => { 57 | this.play(ele); //继续打印下一个字符 58 | }) 59 | } else { 60 | let dom = current.dom.cloneNode() //克隆节点,不克隆节点的子节点,所以不用加参数true 61 | ele.dom.appendChild(dom) 62 | this.play({ 63 | parent: ele, 64 | dom, 65 | val: current.val 66 | }) 67 | } 68 | } 69 | 70 | start() { 71 | this.init(); 72 | this.play(this.chain); 73 | } 74 | } 75 | 76 | export default Typing -------------------------------------------------------------------------------- /src/views/about/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TypingCard from "@/components/TypingCard"; 3 | import wechat from "@/assets/images/wechat.jpg"; 4 | import reward from "@/assets/images/reward.jpg"; 5 | const About = () => { 6 | const cardContent = ` 7 |

大家好,我是难凉热血。

8 |

终南山下码农一枚,师从道长王重阳,酷爱打码,崇尚开源精神,乐于分享。

9 |

2005年服役于中国人民解放军东南战区狼牙特种大队,担任狙击手。

10 |

2008年受俄罗斯阿尔法特种部队邀请,执教于该特种部队第一大队教授其队员学习中国特色社会主义理论及毛泽东思想。

11 |

2011年竞选美国总统落选,遂心灰意冷,放下所有荣誉,隐居终南山下。

12 |

2015年受道长王重阳委托,为道观开发香火管理系统,遂沉迷IT,无法自拔。

13 |

喜欢折腾和搞机,追求新鲜技术。

14 |

下边是我的微信,欢迎同好伙伴一起树(tree)新(new)风(bee)!!!

15 |

如果你觉得这个项目对你有些许帮助的话,欢迎赞赏哈。

16 |

您的赞赏,是我不断前进的动力!

17 |

Ps:最近好多朋友加我微信问我一些问题,结果问完连个 star 也不给我点,好心塞啊~~~

18 |

求大佬们点个 star 啦,感谢感谢~~

19 | wechat 20 | reward 21 | `; 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }; 28 | 29 | export default About; 30 | -------------------------------------------------------------------------------- /src/views/bug/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Table, Collapse, Button,} from "antd"; 4 | import TypingCard from "@/components/TypingCard"; 5 | import { timestampToTime } from "@/utils" 6 | 7 | const { Column } = Table; 8 | const { Panel } = Collapse; 9 | 10 | const obj = {}; 11 | 12 | class Bug extends Component { 13 | jsError = () => { 14 | console.log(obj.a.length); 15 | }; 16 | loadResourceError = () => { 17 | let img = document.createElement("img"); 18 | img.src = "/images/notExist.jpg"; 19 | let parent = document.querySelector(".app-container") 20 | parent.appendChild(img); 21 | } 22 | render() { 23 | const cardContent = `此页面是用来展示通过项目内埋点收集到的异常信息。你可以点击不同种类的异常按钮,来观察捕获到的异常信息。`; 24 | const { bugList } = this.props 25 | return ( 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | record.timestamp} 39 | dataSource={bugList} 40 | pagination={false} 41 | > 42 | index+1}/> 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | timestampToTime(value)}/> 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default connect((state) => state.monitor)(Bug); 58 | -------------------------------------------------------------------------------- /src/views/charts/keyboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import echarts from "@/lib/echarts"; 4 | import { debounce } from "@/utils"; 5 | class KeyboardChart extends Component { 6 | state = { 7 | chart: null, 8 | }; 9 | 10 | componentDidMount() { 11 | debounce(this.initChart.bind(this), 300)(); 12 | window.addEventListener("resize", () => this.resize()); 13 | } 14 | componentWillReceiveProps(nextProps) { 15 | if (nextProps.sidebarCollapsed !== this.props.sidebarCollapsed) { 16 | this.resize(); 17 | } 18 | } 19 | 20 | componentWillUnmount() { 21 | this.dispose(); 22 | } 23 | 24 | resize() { 25 | const chart = this.state.chart; 26 | if (chart) { 27 | debounce(chart.resize.bind(this), 300)(); 28 | } 29 | } 30 | 31 | dispose() { 32 | if (!this.state.chart) { 33 | return; 34 | } 35 | window.removeEventListener("resize", () => this.resize()); // 移除窗口,变化时重置图表 36 | this.setState({ chart: null }); 37 | } 38 | 39 | setOptions() { 40 | const xAxisData = []; 41 | const data = []; 42 | const data2 = []; 43 | for (let i = 0; i < 50; i++) { 44 | xAxisData.push(i); 45 | data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5); 46 | data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3); 47 | } 48 | this.state.chart.setOption({ 49 | backgroundColor: "#08263a", 50 | grid: { 51 | left: "5%", 52 | right: "5%", 53 | }, 54 | xAxis: [ 55 | { 56 | show: false, 57 | data: xAxisData, 58 | }, 59 | { 60 | show: false, 61 | data: xAxisData, 62 | }, 63 | ], 64 | visualMap: { 65 | show: false, 66 | min: 0, 67 | max: 50, 68 | dimension: 0, 69 | inRange: { 70 | color: [ 71 | "#4a657a", 72 | "#308e92", 73 | "#b1cfa5", 74 | "#f5d69f", 75 | "#f5898b", 76 | "#ef5055", 77 | ], 78 | }, 79 | }, 80 | yAxis: { 81 | axisLine: { 82 | show: false, 83 | }, 84 | axisLabel: { 85 | textStyle: { 86 | color: "#4a657a", 87 | }, 88 | }, 89 | splitLine: { 90 | show: true, 91 | lineStyle: { 92 | color: "#08263f", 93 | }, 94 | }, 95 | axisTick: { 96 | show: false, 97 | }, 98 | }, 99 | series: [ 100 | { 101 | name: "back", 102 | type: "bar", 103 | data: data2, 104 | z: 1, 105 | itemStyle: { 106 | normal: { 107 | opacity: 0.4, 108 | barBorderRadius: 5, 109 | shadowBlur: 3, 110 | shadowColor: "#111", 111 | }, 112 | }, 113 | }, 114 | { 115 | name: "Simulate Shadow", 116 | type: "line", 117 | data, 118 | z: 2, 119 | showSymbol: false, 120 | animationDelay: 0, 121 | animationEasing: "linear", 122 | animationDuration: 1200, 123 | lineStyle: { 124 | normal: { 125 | color: "transparent", 126 | }, 127 | }, 128 | areaStyle: { 129 | normal: { 130 | color: "#08263a", 131 | shadowBlur: 50, 132 | shadowColor: "#000", 133 | }, 134 | }, 135 | }, 136 | { 137 | name: "front", 138 | type: "bar", 139 | data, 140 | xAxisIndex: 1, 141 | z: 3, 142 | itemStyle: { 143 | normal: { 144 | barBorderRadius: 5, 145 | }, 146 | }, 147 | }, 148 | ], 149 | animationEasing: "elasticOut", 150 | animationEasingUpdate: "elasticOut", 151 | animationDelay(idx) { 152 | return idx * 20; 153 | }, 154 | animationDelayUpdate(idx) { 155 | return idx * 20; 156 | }, 157 | }); 158 | } 159 | 160 | initChart() { 161 | if (!this.el) return; 162 | this.setState({ chart: echarts.init(this.el, "macarons") }, () => { 163 | this.setOptions(); 164 | }); 165 | } 166 | render() { 167 | return ( 168 |
172 |
(this.el = el)} 175 | >
176 |
177 | ); 178 | } 179 | } 180 | 181 | export default connect((state) => state.app)(KeyboardChart); 182 | -------------------------------------------------------------------------------- /src/views/charts/mixChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import echarts from "@/lib/echarts"; 4 | import { debounce } from "@/utils"; 5 | class MixChart extends Component { 6 | state = { 7 | chart: null, 8 | }; 9 | 10 | componentDidMount() { 11 | debounce(this.initChart.bind(this), 300)(); 12 | window.addEventListener("resize", () => this.resize()); 13 | } 14 | componentWillReceiveProps(nextProps) { 15 | if (nextProps.sidebarCollapsed !== this.props.sidebarCollapsed) { 16 | this.resize(); 17 | } 18 | } 19 | 20 | componentWillUnmount() { 21 | this.dispose(); 22 | } 23 | 24 | resize() { 25 | const chart = this.state.chart; 26 | if (chart) { 27 | debounce(chart.resize.bind(this), 300)(); 28 | } 29 | } 30 | 31 | dispose() { 32 | if (!this.state.chart) { 33 | return; 34 | } 35 | window.removeEventListener("resize", () => this.resize()); // 移除窗口,变化时重置图表 36 | this.setState({ chart: null }); 37 | } 38 | 39 | setOptions() { 40 | const xData = (function () { 41 | const data = []; 42 | for (let i = 1; i < 13; i++) { 43 | data.push(i + "month"); 44 | } 45 | return data; 46 | })(); 47 | this.state.chart.setOption({ 48 | backgroundColor: "#344b58", 49 | title: { 50 | text: "statistics", 51 | x: "20", 52 | top: "20", 53 | textStyle: { 54 | color: "#fff", 55 | fontSize: "22", 56 | }, 57 | subtextStyle: { 58 | color: "#90979c", 59 | fontSize: "16", 60 | }, 61 | }, 62 | tooltip: { 63 | trigger: "axis", 64 | axisPointer: { 65 | textStyle: { 66 | color: "#fff", 67 | }, 68 | }, 69 | }, 70 | grid: { 71 | left: "5%", 72 | right: "5%", 73 | borderWidth: 0, 74 | top: 150, 75 | bottom: 95, 76 | textStyle: { 77 | color: "#fff", 78 | }, 79 | }, 80 | legend: { 81 | x: "5%", 82 | top: "10%", 83 | textStyle: { 84 | color: "#90979c", 85 | }, 86 | data: ["female", "male", "average"], 87 | }, 88 | calculable: true, 89 | xAxis: [ 90 | { 91 | type: "category", 92 | axisLine: { 93 | lineStyle: { 94 | color: "#90979c", 95 | }, 96 | }, 97 | splitLine: { 98 | show: false, 99 | }, 100 | axisTick: { 101 | show: false, 102 | }, 103 | splitArea: { 104 | show: false, 105 | }, 106 | axisLabel: { 107 | interval: 0, 108 | }, 109 | data: xData, 110 | }, 111 | ], 112 | yAxis: [ 113 | { 114 | type: "value", 115 | splitLine: { 116 | show: false, 117 | }, 118 | axisLine: { 119 | lineStyle: { 120 | color: "#90979c", 121 | }, 122 | }, 123 | axisTick: { 124 | show: false, 125 | }, 126 | axisLabel: { 127 | interval: 0, 128 | }, 129 | splitArea: { 130 | show: false, 131 | }, 132 | }, 133 | ], 134 | dataZoom: [ 135 | { 136 | show: true, 137 | height: 30, 138 | xAxisIndex: [0], 139 | bottom: 30, 140 | start: 10, 141 | end: 80, 142 | handleIcon: 143 | "path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z", 144 | handleSize: "110%", 145 | handleStyle: { 146 | color: "#d3dee5", 147 | }, 148 | textStyle: { 149 | color: "#fff", 150 | }, 151 | borderColor: "#90979c", 152 | }, 153 | { 154 | type: "inside", 155 | show: true, 156 | height: 15, 157 | start: 1, 158 | end: 35, 159 | }, 160 | ], 161 | series: [ 162 | { 163 | name: "female", 164 | type: "bar", 165 | stack: "total", 166 | barMaxWidth: 35, 167 | barGap: "10%", 168 | itemStyle: { 169 | normal: { 170 | color: "rgba(255,144,128,1)", 171 | label: { 172 | show: true, 173 | textStyle: { 174 | color: "#fff", 175 | }, 176 | position: "insideTop", 177 | formatter(p) { 178 | return p.value > 0 ? p.value : ""; 179 | }, 180 | }, 181 | }, 182 | }, 183 | data: [ 184 | 709, 185 | 1917, 186 | 2455, 187 | 2610, 188 | 1719, 189 | 1433, 190 | 1544, 191 | 3285, 192 | 5208, 193 | 3372, 194 | 2484, 195 | 4078, 196 | ], 197 | }, 198 | 199 | { 200 | name: "male", 201 | type: "bar", 202 | stack: "total", 203 | itemStyle: { 204 | normal: { 205 | color: "rgba(0,191,183,1)", 206 | barBorderRadius: 0, 207 | label: { 208 | show: true, 209 | position: "top", 210 | formatter(p) { 211 | return p.value > 0 ? p.value : ""; 212 | }, 213 | }, 214 | }, 215 | }, 216 | data: [ 217 | 327, 218 | 1776, 219 | 507, 220 | 1200, 221 | 800, 222 | 482, 223 | 204, 224 | 1390, 225 | 1001, 226 | 951, 227 | 381, 228 | 220, 229 | ], 230 | }, 231 | { 232 | name: "average", 233 | type: "line", 234 | stack: "total", 235 | symbolSize: 10, 236 | symbol: "circle", 237 | itemStyle: { 238 | normal: { 239 | color: "rgba(252,230,48,1)", 240 | barBorderRadius: 0, 241 | label: { 242 | show: true, 243 | position: "top", 244 | formatter(p) { 245 | return p.value > 0 ? p.value : ""; 246 | }, 247 | }, 248 | }, 249 | }, 250 | data: [ 251 | 1036, 252 | 3693, 253 | 2962, 254 | 3810, 255 | 2519, 256 | 1915, 257 | 1748, 258 | 4675, 259 | 6209, 260 | 4323, 261 | 2865, 262 | 4298, 263 | ], 264 | }, 265 | ], 266 | }); 267 | } 268 | 269 | initChart() { 270 | if (!this.el) return; 271 | this.setState({ chart: echarts.init(this.el, "macarons") }, () => { 272 | this.setOptions(); 273 | }); 274 | } 275 | render() { 276 | return ( 277 |
281 |
(this.el = el)} 284 | >
285 |
286 | ); 287 | } 288 | } 289 | 290 | export default connect((state) => state.app)(MixChart); 291 | -------------------------------------------------------------------------------- /src/views/clipboard/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clip from "@/utils/clipboard"; 3 | import { Button, Row, Col } from "antd"; 4 | 5 | const text = ` 6 | 我是要被复制的文字, 7 | 我是要被复制的文字, 8 | 我是要被复制的文字, 9 | 我是要被复制的文字, 10 | 我是要被复制的文字, 11 | 我是要被复制的文字, 12 | 我是要被复制的文字, 13 | 我是要被复制的文字, 14 | 我是要被复制的文字, 15 | 我是要被复制的文字, 16 | 我是要被复制的文字, 17 | 我是要被复制的文字, 18 | 我是要被复制的文字, 19 | 我是要被复制的文字, 20 | 我是要被复制的文字, 21 | 我是要被复制的文字 22 | `; 23 | const handleCopy = (text, event) => { 24 | clip(text, event); 25 | }; 26 | const Clipboard = () => { 27 | return ( 28 |
29 |

点击下方的Copy按钮,可将以下文字复制到剪贴板

30 |
31 | 32 | {text} 33 | 34 |
35 | 36 | 37 | 46 | 47 | 48 |
49 | ); 50 | }; 51 | 52 | export default Clipboard; 53 | -------------------------------------------------------------------------------- /src/views/components-demo/Markdown.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card } from "antd"; 3 | import Markdown from "@/components/Markdown"; 4 | import TypingCard from "@/components/TypingCard"; 5 | 6 | const MarkdownDemo = () => { 7 | const cardContent = ` 8 | 此页面用到的Markdown编辑器是tui.editor(React版)。 9 | `; 10 | return ( 11 |
12 | 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | export default MarkdownDemo; 22 | -------------------------------------------------------------------------------- /src/views/components-demo/draggable.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TypingCard from "@/components/TypingCard"; 3 | import draggable from "@/assets/images/draggable.gif"; 4 | const Draggable = () => { 5 | const cardContent = ` 6 | 你可以试着拖拽一下左侧导航菜单栏的某一项,它是可以拖拽的哦。 7 | 本Demo是基于react-beautiful-dnd。 8 |

9 | `; 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Draggable; 18 | -------------------------------------------------------------------------------- /src/views/components-demo/richTextEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RichTextEditor from "@/components/RichTextEditor"; 3 | import TypingCard from "@/components/TypingCard"; 4 | 5 | const RichTextEditorDemo = () => { 6 | const cardContent = ` 7 | 此页面用到的富文本编辑器是react-draft-wysiwyg。 8 | ` 9 | return ( 10 |
11 | 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | export default RichTextEditorDemo; 19 | -------------------------------------------------------------------------------- /src/views/dashboard/components/BarChart/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import echarts from "@/lib/echarts"; 5 | import { debounce } from "@/utils"; 6 | 7 | class BarChart extends Component { 8 | static propTypes = { 9 | width: PropTypes.string, 10 | height: PropTypes.string, 11 | className: PropTypes.string, 12 | styles: PropTypes.object, 13 | }; 14 | static defaultProps = { 15 | width: "100%", 16 | height: "300px", 17 | styles: {}, 18 | className: "", 19 | }; 20 | state = { 21 | chart: null, 22 | }; 23 | 24 | componentDidMount() { 25 | debounce(this.initChart.bind(this), 300)(); 26 | window.addEventListener("resize", () => this.resize()); 27 | } 28 | componentWillReceiveProps(nextProps) { 29 | if (nextProps.sidebarCollapsed !== this.props.sidebarCollapsed) { 30 | this.resize(); 31 | } 32 | if (nextProps.chartData !== this.props.chartData) { 33 | debounce(this.initChart.bind(this), 300)(); 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | this.dispose(); 39 | } 40 | 41 | resize() { 42 | const chart = this.state.chart; 43 | if (chart) { 44 | debounce(chart.resize.bind(this), 300)(); 45 | } 46 | } 47 | 48 | dispose() { 49 | if (!this.state.chart) { 50 | return; 51 | } 52 | window.removeEventListener("resize", () => this.resize()); // 移除窗口,变化时重置图表 53 | this.setState({ chart: null }); 54 | } 55 | 56 | setOptions() { 57 | const animationDuration = 3000; 58 | this.state.chart.setOption({ 59 | tooltip: { 60 | trigger: "axis", 61 | axisPointer: { 62 | // 坐标轴指示器,坐标轴触发有效 63 | type: "shadow", // 默认为直线,可选为:'line' | 'shadow' 64 | }, 65 | }, 66 | grid: { 67 | top: 10, 68 | left: "2%", 69 | right: "2%", 70 | bottom: "3%", 71 | containLabel: true, 72 | }, 73 | xAxis: [ 74 | { 75 | type: "category", 76 | data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 77 | axisTick: { 78 | alignWithLabel: true, 79 | }, 80 | }, 81 | ], 82 | yAxis: [ 83 | { 84 | type: "value", 85 | axisTick: { 86 | show: false, 87 | }, 88 | }, 89 | ], 90 | series: [ 91 | { 92 | name: "pageA", 93 | type: "bar", 94 | stack: "vistors", 95 | barWidth: "60%", 96 | data: [79, 52, 200, 334, 390, 330, 220], 97 | animationDuration, 98 | }, 99 | { 100 | name: "pageB", 101 | type: "bar", 102 | stack: "vistors", 103 | barWidth: "60%", 104 | data: [80, 52, 200, 334, 390, 330, 220], 105 | animationDuration, 106 | }, 107 | { 108 | name: "pageC", 109 | type: "bar", 110 | stack: "vistors", 111 | barWidth: "60%", 112 | data: [30, 52, 200, 334, 390, 330, 220], 113 | animationDuration, 114 | }, 115 | ], 116 | }); 117 | } 118 | 119 | initChart() { 120 | if (!this.el) return; 121 | this.setState({ chart: echarts.init(this.el, "macarons") }, () => { 122 | this.setOptions(this.props.chartData); 123 | }); 124 | } 125 | 126 | render() { 127 | const { className, height, width, styles } = this.props; 128 | return ( 129 |
(this.el = el)} 132 | style={{ 133 | ...styles, 134 | height, 135 | width, 136 | }} 137 | /> 138 | ); 139 | } 140 | } 141 | 142 | export default connect((state) => state.app)(BarChart); 143 | -------------------------------------------------------------------------------- /src/views/dashboard/components/BoxCard/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Card, Progress } from "antd"; 3 | import { connect } from "react-redux"; 4 | import PanThumb from '@/components/PanThumb' 5 | import Mallki from '@/components//Mallki' 6 | import './index.less' 7 | class BoxCard extends Component { 8 | state = {}; 9 | render() { 10 | const {avatar} = this.props 11 | return ( 12 |
13 | 20 | } 21 | > 22 |
23 | 24 | 25 |
26 | Vue 27 | 28 |
29 |
30 | JavaScript 31 | 32 |
33 |
34 | Css 35 | 36 |
37 |
38 | ESLint 39 | 40 |
41 |
42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | export default connect((state) => state.user)(BoxCard); -------------------------------------------------------------------------------- /src/views/dashboard/components/BoxCard/index.less: -------------------------------------------------------------------------------- 1 | .box-card-component { 2 | position: relative; 3 | .mallki-text { 4 | position: absolute; 5 | top: 0px; 6 | right: 0px; 7 | font-size: 25px; 8 | font-weight: bold; 9 | } 10 | .panThumb { 11 | z-index: 100; 12 | height: 70px!important; 13 | width: 70px!important; 14 | position: absolute!important; 15 | top: -45px; 16 | left: 0px; 17 | border: 5px solid #ffffff; 18 | background-color: #fff; 19 | margin: auto; 20 | box-shadow: none!important; 21 | } 22 | .progress-item { 23 | margin-bottom: 10px; 24 | font-size: 14px; 25 | } 26 | @media only screen and (max-width: 1510px){ 27 | .mallki-text{ 28 | display: none; 29 | } 30 | } 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/views/dashboard/components/LineChart/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import echarts from "@/lib/echarts"; 5 | import { debounce } from "@/utils"; 6 | 7 | class LineChart extends Component { 8 | static propTypes = { 9 | width: PropTypes.string, 10 | height: PropTypes.string, 11 | className: PropTypes.string, 12 | styles: PropTypes.object, 13 | chartData: PropTypes.object.isRequired, 14 | }; 15 | static defaultProps = { 16 | width: "100%", 17 | height: "350px", 18 | styles: {}, 19 | className: "", 20 | }; 21 | state = { 22 | chart: null, 23 | }; 24 | 25 | componentDidMount() { 26 | debounce(this.initChart.bind(this), 300)(); 27 | window.addEventListener("resize", () => this.resize()); 28 | } 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps.sidebarCollapsed !== this.props.sidebarCollapsed) { 31 | this.resize(); 32 | } 33 | if (nextProps.chartData !== this.props.chartData) { 34 | debounce(this.initChart.bind(this), 300)(); 35 | } 36 | } 37 | 38 | componentWillUnmount() { 39 | this.dispose(); 40 | } 41 | 42 | resize() { 43 | const chart = this.state.chart; 44 | if (chart) { 45 | debounce(chart.resize.bind(this), 300)(); 46 | } 47 | } 48 | 49 | dispose() { 50 | if (!this.state.chart) { 51 | return; 52 | } 53 | window.removeEventListener("resize", () => this.resize()); // 移除窗口,变化时重置图表 54 | this.setState({ chart: null }); 55 | } 56 | 57 | setOptions({ expectedData, actualData } = {}) { 58 | this.state.chart.setOption({ 59 | backgroundColor: "#fff", 60 | xAxis: { 61 | data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 62 | boundaryGap: false, 63 | axisTick: { 64 | show: false, 65 | }, 66 | }, 67 | grid: { 68 | left: 10, 69 | right: 10, 70 | bottom: 10, 71 | top: 30, 72 | containLabel: true, 73 | }, 74 | tooltip: { 75 | trigger: "axis", 76 | axisPointer: { 77 | type: "cross", 78 | }, 79 | padding: [5, 10], 80 | }, 81 | yAxis: { 82 | axisTick: { 83 | show: false, 84 | }, 85 | }, 86 | legend: { 87 | data: ["expected", "actual"], 88 | }, 89 | series: [ 90 | { 91 | name: "expected", 92 | itemStyle: { 93 | normal: { 94 | color: "#FF005A", 95 | lineStyle: { 96 | color: "#FF005A", 97 | width: 2, 98 | }, 99 | }, 100 | }, 101 | smooth: true, 102 | type: "line", 103 | data: expectedData, 104 | animationDuration: 2800, 105 | animationEasing: "cubicInOut", 106 | }, 107 | { 108 | name: "actual", 109 | smooth: true, 110 | type: "line", 111 | itemStyle: { 112 | normal: { 113 | color: "#3888fa", 114 | lineStyle: { 115 | color: "#3888fa", 116 | width: 2, 117 | }, 118 | areaStyle: { 119 | color: "#f3f8ff", 120 | }, 121 | }, 122 | }, 123 | data: actualData, 124 | animationDuration: 2800, 125 | animationEasing: "quadraticOut", 126 | }, 127 | ], 128 | }); 129 | } 130 | 131 | initChart() { 132 | if (!this.el) return; 133 | this.setState({ chart: echarts.init(this.el,"macarons") }, () => { 134 | this.setOptions(this.props.chartData); 135 | }); 136 | } 137 | 138 | render() { 139 | const { className, height, width,styles } = this.props; 140 | return ( 141 |
(this.el = el)} 144 | style={{ 145 | ...styles, 146 | height, 147 | width, 148 | }} 149 | /> 150 | ); 151 | } 152 | } 153 | 154 | export default connect(state=>state.app)(LineChart); 155 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PanelGroup/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Row, Col, Icon } from "antd"; 3 | import CountUp from "react-countup"; 4 | import "./index.less"; 5 | 6 | const chartList = [ 7 | { 8 | type: "New Visits", 9 | icon: "user", 10 | num: 102400, 11 | color: "#40c9c6", 12 | }, 13 | { 14 | type: "Messages", 15 | icon: "message", 16 | num: 81212, 17 | color: "#36a3f7", 18 | }, 19 | { 20 | type: "Purchases", 21 | icon: "pay-circle", 22 | num: 9280, 23 | color: "#f4516c", 24 | }, 25 | { 26 | type: "Shoppings", 27 | icon: "shopping-cart", 28 | num: 13600, 29 | color: "#f6ab40", 30 | }, 31 | ]; 32 | 33 | const PanelGroup = (props) => { 34 | const { handleSetLineChartData } = props; 35 | return ( 36 |
37 | 38 | {chartList.map((chart, i) => ( 39 | 47 |
48 |
49 | 54 |
55 |
56 |

{chart.type}

57 | 58 |
59 |
60 | 61 | ))} 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default PanelGroup; 68 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PanelGroup/index.less: -------------------------------------------------------------------------------- 1 | .panel-group-container { 2 | .panel-group { 3 | .card-panel-col{ 4 | margin-bottom: 20px; 5 | } 6 | .card-panel { 7 | height: 108px; 8 | cursor: pointer; 9 | font-size: 12px; 10 | position: relative; 11 | overflow: hidden; 12 | color: #666; 13 | background: #fff; 14 | box-shadow: 4px 4px 40px rgba(0, 0, 0, .05); 15 | border-color: rgba(0, 0, 0, .05); 16 | &:hover { 17 | .card-panel-icon-wrapper { 18 | background: #ccc; 19 | } 20 | } 21 | .card-panel-icon-wrapper { 22 | float: left; 23 | margin: 14px 0 0 14px; 24 | padding: 16px; 25 | transition: all 0.38s ease-out; 26 | border-radius: 6px; 27 | } 28 | .card-panel-icon { 29 | float: left; 30 | font-size: 48px; 31 | } 32 | .card-panel-description { 33 | float: right; 34 | font-weight: bold; 35 | margin: 26px; 36 | margin-left: 0px; 37 | .card-panel-text { 38 | line-height: 18px; 39 | color: rgba(0, 0, 0, 0.45); 40 | font-size: 16px; 41 | margin-bottom: 12px; 42 | } 43 | .card-panel-num { 44 | font-size: 20px; 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/views/dashboard/components/PieChart/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import echarts from "@/lib/echarts"; 5 | import { debounce } from "@/utils"; 6 | 7 | class PieChart extends Component { 8 | static propTypes = { 9 | width: PropTypes.string, 10 | height: PropTypes.string, 11 | className: PropTypes.string, 12 | styles: PropTypes.object, 13 | }; 14 | static defaultProps = { 15 | width: "100%", 16 | height: "300px", 17 | styles: {}, 18 | className: "", 19 | }; 20 | state = { 21 | chart: null, 22 | }; 23 | 24 | componentDidMount() { 25 | debounce(this.initChart.bind(this), 300)(); 26 | window.addEventListener("resize", () => this.resize()); 27 | } 28 | componentWillReceiveProps(nextProps) { 29 | if (nextProps.sidebarCollapsed !== this.props.sidebarCollapsed) { 30 | this.resize(); 31 | } 32 | if (nextProps.chartData !== this.props.chartData) { 33 | debounce(this.initChart.bind(this), 300)(); 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | this.dispose(); 39 | } 40 | 41 | resize() { 42 | const chart = this.state.chart; 43 | if (chart) { 44 | debounce(chart.resize.bind(this), 300)(); 45 | } 46 | } 47 | 48 | dispose() { 49 | if (!this.state.chart) { 50 | return; 51 | } 52 | window.removeEventListener("resize", () => this.resize()); // 移除窗口,变化时重置图表 53 | this.setState({ chart: null }); 54 | } 55 | 56 | setOptions() { 57 | const animationDuration = 3000; 58 | this.state.chart.setOption({ 59 | tooltip: { 60 | trigger: "item", 61 | formatter: "{a}
{b} : {c} ({d}%)", 62 | }, 63 | legend: { 64 | left: "center", 65 | bottom: "10", 66 | data: ["Industries", "Technology", "Forex", "Gold", "Forecasts"], 67 | }, 68 | calculable: true, 69 | series: [ 70 | { 71 | name: "WEEKLY WRITE ARTICLES", 72 | type: "pie", 73 | roseType: "radius", 74 | radius: [15, 95], 75 | center: ["50%", "38%"], 76 | data: [ 77 | { value: 320, name: "Industries" }, 78 | { value: 240, name: "Technology" }, 79 | { value: 149, name: "Forex" }, 80 | { value: 100, name: "Gold" }, 81 | { value: 59, name: "Forecasts" }, 82 | ], 83 | animationEasing: "cubicInOut", 84 | animationDuration 85 | }, 86 | ], 87 | }); 88 | } 89 | 90 | initChart() { 91 | if (!this.el) return; 92 | this.setState({ chart: echarts.init(this.el, "macarons") }, () => { 93 | this.setOptions(this.props.chartData); 94 | }); 95 | } 96 | 97 | render() { 98 | const { className, height, width, styles } = this.props; 99 | return ( 100 |
(this.el = el)} 103 | style={{ 104 | ...styles, 105 | height, 106 | width, 107 | }} 108 | /> 109 | ); 110 | } 111 | } 112 | 113 | export default connect((state) => state.app)(PieChart); 114 | -------------------------------------------------------------------------------- /src/views/dashboard/components/RaddarChart/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { PropTypes } from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import echarts from "@/lib/echarts"; 5 | import { debounce } from "@/utils"; 6 | 7 | class RaddarChart extends Component { 8 | static propTypes = { 9 | width: PropTypes.string, 10 | height: PropTypes.string, 11 | className: PropTypes.string, 12 | styles: PropTypes.object, 13 | }; 14 | static defaultProps = { 15 | width: "100%", 16 | height: "300px", 17 | styles: {}, 18 | className: "", 19 | }; 20 | state = { 21 | chart: null, 22 | }; 23 | 24 | componentDidMount() { 25 | debounce(this.initChart.bind(this), 300)(); 26 | window.addEventListener("resize", () => this.resize()); 27 | } 28 | componentWillReceiveProps(nextProps) { 29 | if (nextProps.sidebarCollapsed !== this.props.sidebarCollapsed) { 30 | this.resize(); 31 | } 32 | if (nextProps.chartData !== this.props.chartData) { 33 | debounce(this.initChart.bind(this), 300)(); 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | this.dispose(); 39 | } 40 | 41 | resize() { 42 | const chart = this.state.chart; 43 | if (chart) { 44 | debounce(chart.resize.bind(this), 300)(); 45 | } 46 | } 47 | 48 | dispose() { 49 | if (!this.state.chart) { 50 | return; 51 | } 52 | window.removeEventListener("resize", () => this.resize()); // 移除窗口,变化时重置图表 53 | this.setState({ chart: null }); 54 | } 55 | 56 | setOptions() { 57 | const animationDuration = 3000; 58 | this.state.chart.setOption({ 59 | tooltip: { 60 | trigger: "axis", 61 | axisPointer: { 62 | // 坐标轴指示器,坐标轴触发有效 63 | type: "shadow", // 默认为直线,可选为:'line' | 'shadow' 64 | }, 65 | }, 66 | radar: { 67 | radius: "66%", 68 | center: ["50%", "42%"], 69 | splitNumber: 8, 70 | splitArea: { 71 | areaStyle: { 72 | color: "rgba(127,95,132,.3)", 73 | opacity: 1, 74 | shadowBlur: 45, 75 | shadowColor: "rgba(0,0,0,.5)", 76 | shadowOffsetX: 0, 77 | shadowOffsetY: 15, 78 | }, 79 | }, 80 | indicator: [ 81 | { name: "Sales", max: 10000 }, 82 | { name: "Administration", max: 20000 }, 83 | { name: "Information Techology", max: 20000 }, 84 | { name: "Customer Support", max: 20000 }, 85 | { name: "Development", max: 20000 }, 86 | { name: "Marketing", max: 20000 }, 87 | ], 88 | }, 89 | legend: { 90 | left: "center", 91 | bottom: "10", 92 | data: ["Allocated Budget", "Expected Spending", "Actual Spending"], 93 | }, 94 | series: [ 95 | { 96 | type: "radar", 97 | symbolSize: 0, 98 | areaStyle: { 99 | normal: { 100 | shadowBlur: 13, 101 | shadowColor: "rgba(0,0,0,.2)", 102 | shadowOffsetX: 0, 103 | shadowOffsetY: 10, 104 | opacity: 1, 105 | }, 106 | }, 107 | data: [ 108 | { 109 | value: [5000, 7000, 12000, 11000, 15000, 14000], 110 | name: "Allocated Budget", 111 | }, 112 | { 113 | value: [4000, 9000, 15000, 15000, 13000, 11000], 114 | name: "Expected Spending", 115 | }, 116 | { 117 | value: [5500, 11000, 12000, 15000, 12000, 12000], 118 | name: "Actual Spending", 119 | }, 120 | ], 121 | animationDuration, 122 | }, 123 | ], 124 | }); 125 | } 126 | 127 | initChart() { 128 | if (!this.el) return; 129 | this.setState({ chart: echarts.init(this.el, "macarons") }, () => { 130 | this.setOptions(this.props.chartData); 131 | }); 132 | } 133 | 134 | render() { 135 | const { className, height, width, styles } = this.props; 136 | return ( 137 |
(this.el = el)} 140 | style={{ 141 | ...styles, 142 | height, 143 | width, 144 | }} 145 | /> 146 | ); 147 | } 148 | } 149 | 150 | export default connect((state) => state.app)(RaddarChart); 151 | -------------------------------------------------------------------------------- /src/views/dashboard/components/TransactionTable/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Table, Tag } from "antd"; 3 | import { transactionList } from "@/api/remoteSearch"; 4 | 5 | const columns = [ 6 | { 7 | title: "Order_No", 8 | dataIndex: "order_no", 9 | key: "order_no", 10 | width: 200, 11 | }, 12 | { 13 | title: "Price", 14 | dataIndex: "price", 15 | key: "price", 16 | width: 195, 17 | render: text => (`$${text}`), 18 | }, 19 | { 20 | title: "Status", 21 | key: "tag", 22 | dataIndex: "tag", 23 | width: 100, 24 | render: (tag) => ( 25 | 26 | {tag} 27 | 28 | ), 29 | }, 30 | ]; 31 | 32 | class TransactionTable extends Component { 33 | _isMounted = false; // 这个变量是用来标志当前组件是否挂载 34 | state = { 35 | list: [], 36 | }; 37 | fetchData = () => { 38 | transactionList().then((response) => { 39 | const list = response.data.data.items.slice(0, 13); 40 | if (this._isMounted) { 41 | this.setState({ list }); 42 | } 43 | }); 44 | }; 45 | componentDidMount() { 46 | this._isMounted = true; 47 | this.fetchData(); 48 | } 49 | componentWillUnmount() { 50 | this._isMounted = false; 51 | } 52 | render() { 53 | return ( 54 | 59 | ); 60 | } 61 | } 62 | 63 | export default TransactionTable; 64 | -------------------------------------------------------------------------------- /src/views/dashboard/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Row, Col } from "antd"; 3 | import "./index.less"; 4 | import PanelGroup from "./components/PanelGroup"; 5 | import LineChart from "./components/LineChart"; 6 | import BarChart from "./components/BarChart"; 7 | import RaddarChart from "./components/RaddarChart"; 8 | import PieChart from "./components/PieChart"; 9 | import TransactionTable from "./components/TransactionTable"; 10 | import BoxCard from "./components/BoxCard"; 11 | 12 | const lineChartDefaultData = { 13 | "New Visits": { 14 | expectedData: [100, 120, 161, 134, 105, 160, 165], 15 | actualData: [120, 82, 91, 154, 162, 140, 145], 16 | }, 17 | Messages: { 18 | expectedData: [200, 192, 120, 144, 160, 130, 140], 19 | actualData: [180, 160, 151, 106, 145, 150, 130], 20 | }, 21 | Purchases: { 22 | expectedData: [80, 100, 121, 104, 105, 90, 100], 23 | actualData: [120, 90, 100, 138, 142, 130, 130], 24 | }, 25 | Shoppings: { 26 | expectedData: [130, 140, 141, 142, 145, 150, 160], 27 | actualData: [120, 82, 91, 154, 162, 140, 130], 28 | }, 29 | }; 30 | 31 | const Dashboard = () => { 32 | const [lineChartData, setLineChartData] = useState( 33 | lineChartDefaultData["New Visits"] 34 | ); 35 | 36 | const handleSetLineChartData = (type) => setLineChartData(lineChartDefaultData[type]); 37 | 38 | return ( 39 |
40 | 46 | 47 | 48 | 49 | 57 | 58 | 59 |
60 |
61 | 62 |
63 | 64 | 65 |
66 | 67 |
68 | 69 | 70 |
71 | 72 |
73 | 74 | 75 | 76 | 77 | 85 | 86 | 87 | 95 | 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | export default Dashboard; 103 | -------------------------------------------------------------------------------- /src/views/dashboard/index.less: -------------------------------------------------------------------------------- 1 | .app-container { 2 | background-color: rgb(240, 242, 245); 3 | position: relative; 4 | .github-corner { 5 | position: absolute; 6 | top: 0px; 7 | right: 0px; 8 | background-image: url('~@/assets/images/githubCorner.png'); 9 | background-size: 100% 100%; 10 | width: 120px; 11 | height: 120px; 12 | z-index: 9; 13 | cursor: pointer; 14 | } 15 | .chart-wrapper { 16 | background: #fff; 17 | padding: 16px 16px 0; 18 | margin-bottom: 32px; 19 | } 20 | } -------------------------------------------------------------------------------- /src/views/doc/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TypingCard from '@/components/TypingCard' 3 | const Doc = () => { 4 | const cardContent = ` 5 | 作者博客请戳这里 难凉热血的博客。 6 | 欢迎大家与我交流,如果觉得博客不错,也麻烦给博客赏个 star 哈。 7 | ` 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | 15 | export default Doc; -------------------------------------------------------------------------------- /src/views/error/404/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Row, Col } from "antd"; 3 | import errImg from "@/assets/images/404.png"; 4 | import "./index.less"; 5 | 6 | const NotFound = (props) => { 7 | const { history } = props; 8 | const goHome = () => history.replace("/"); 9 | return ( 10 | 11 |
12 | 404 13 | 14 | 15 |

404

16 |

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

17 |
18 | 21 |
22 | 23 | 24 | ); 25 | }; 26 | 27 | export default NotFound; 28 | -------------------------------------------------------------------------------- /src/views/error/404/index.less: -------------------------------------------------------------------------------- 1 | .not-found{ 2 | background-color: #f0f2f5; 3 | height: 100%; 4 | .right { 5 | padding-left: 50px; 6 | margin-top: 150px; 7 | h1 { 8 | font-size: 35px; 9 | } 10 | h2 { 11 | margin-bottom: 20px; 12 | font-size: 20px; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/views/excel/exportExcel/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | Table, 4 | Tag, 5 | Form, 6 | Icon, 7 | Button, 8 | Input, 9 | Radio, 10 | Select, 11 | message, 12 | Collapse, 13 | } from "antd"; 14 | 15 | import { excelList } from "@/api/excel"; 16 | const { Panel } = Collapse; 17 | const columns = [ 18 | { 19 | title: "Id", 20 | dataIndex: "id", 21 | key: "id", 22 | width: 200, 23 | align: "center", 24 | }, 25 | { 26 | title: "Title", 27 | dataIndex: "title", 28 | key: "title", 29 | width: 200, 30 | align: "center", 31 | }, 32 | { 33 | title: "Author", 34 | key: "author", 35 | dataIndex: "author", 36 | width: 100, 37 | align: "center", 38 | render: (author) => {author}, 39 | }, 40 | { 41 | title: "Readings", 42 | dataIndex: "readings", 43 | key: "readings", 44 | width: 195, 45 | align: "center", 46 | }, 47 | { 48 | title: "Date", 49 | dataIndex: "date", 50 | key: "date", 51 | width: 195, 52 | align: "center", 53 | }, 54 | ]; 55 | class Excel extends Component { 56 | _isMounted = false; // 这个变量是用来标志当前组件是否挂载 57 | state = { 58 | list: [], 59 | filename: "excel-file", 60 | autoWidth: true, 61 | bookType: "xlsx", 62 | downloadLoading: false, 63 | selectedRows: [], 64 | selectedRowKeys: [], 65 | }; 66 | fetchData = () => { 67 | excelList().then((response) => { 68 | const list = response.data.data.items; 69 | if (this._isMounted) { 70 | this.setState({ list }); 71 | } 72 | }); 73 | }; 74 | componentDidMount() { 75 | this._isMounted = true; 76 | this.fetchData(); 77 | } 78 | componentWillUnmount() { 79 | this._isMounted = false; 80 | } 81 | onSelectChange = (selectedRowKeys, selectedRows) => { 82 | this.setState({ selectedRows, selectedRowKeys }); 83 | }; 84 | handleDownload = (type) => { 85 | if (type === "selected" && this.state.selectedRowKeys.length === 0) { 86 | message.error("至少选择一项进行导出"); 87 | return; 88 | } 89 | this.setState({ 90 | downloadLoading: true, 91 | }); 92 | import("@/lib/Export2Excel").then((excel) => { 93 | const tHeader = ["Id", "Title", "Author", "Readings", "Date"]; 94 | const filterVal = ["id", "title", "author", "readings", "date"]; 95 | const list = type === "all" ? this.state.list : this.state.selectedRows; 96 | const data = this.formatJson(filterVal, list); 97 | excel.export_json_to_excel({ 98 | header: tHeader, 99 | data, 100 | filename: this.state.filename, 101 | autoWidth: this.state.autoWidth, 102 | bookType: this.state.bookType, 103 | }); 104 | this.setState({ 105 | selectedRowKeys: [], // 导出完成后将多选框清空 106 | downloadLoading: false, 107 | }); 108 | }); 109 | }; 110 | formatJson(filterVal, jsonData) { 111 | return jsonData.map(v => filterVal.map(j => v[j])) 112 | } 113 | filenameChange = (e) => { 114 | this.setState({ 115 | filename: e.target.value, 116 | }); 117 | }; 118 | autoWidthChange = (e) => { 119 | this.setState({ 120 | autoWidth: e.target.value, 121 | }); 122 | }; 123 | bookTypeChange = (value) => { 124 | this.setState({ 125 | bookType: value, 126 | }); 127 | }; 128 | render() { 129 | const { selectedRowKeys } = this.state; 130 | const rowSelection = { 131 | selectedRowKeys, 132 | onChange: this.onSelectChange, 133 | }; 134 | return ( 135 |
136 | 137 | 138 |
139 | 140 | 144 | } 145 | placeholder="请输入文件名(默认excel-file)" 146 | onChange={this.filenameChange} 147 | /> 148 | 149 | 150 | 154 | 155 | 156 | 157 | 158 | 159 | 168 | 169 | 170 | 177 | 178 | 179 | 186 | 187 | 188 |
189 |
190 |
191 |
record.id} 195 | dataSource={this.state.list} 196 | pagination={false} 197 | rowSelection={rowSelection} 198 | loading={this.state.downloadLoading} 199 | /> 200 | 201 | ); 202 | } 203 | } 204 | 205 | export default Excel; 206 | -------------------------------------------------------------------------------- /src/views/excel/uploadExcel/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Table } from "antd"; 3 | import UploadExcelComponent from "@/components/UploadExcel"; 4 | class UploadExcel extends Component { 5 | state = { 6 | tableData: [], 7 | tableHeader: [], 8 | }; 9 | handleSuccess = ({ results, header }) => { 10 | this.setState({ 11 | tableData: results, 12 | tableHeader: header, 13 | }); 14 | }; 15 | render() { 16 | return ( 17 |
18 | 19 |
20 |
({ 23 | title: item, 24 | dataIndex: item, 25 | key: item, 26 | width: 195, 27 | align: "center", 28 | }))} 29 | dataSource={this.state.tableData} 30 | /> 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default UploadExcel; 37 | -------------------------------------------------------------------------------- /src/views/guide/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Driver from "driver.js"; // import driver.js 3 | import "driver.js/dist/driver.min.css"; // import driver.js css 4 | import { Button } from "antd"; 5 | import TypingCard from '@/components/TypingCard' 6 | import steps from "./steps"; 7 | const driver = new Driver({ 8 | animate: true, // 在更改突出显示的元素时是否设置动画, 9 | // 当header的position为fixed时,会覆盖元素,这是driver.js的bug, 10 | // 详细内容见https://github.com/kamranahmedse/driver.js/issues/97 11 | opacity: 0.75, // 背景不透明度(0表示只有弹出窗口,没有覆盖) 12 | doneBtnText: "完成", // 最后一个按钮上的文本 13 | closeBtnText: "关闭", // 此步骤的“关闭”按钮上的文本 14 | nextBtnText: "下一步", // 此步骤的下一步按钮文本 15 | prevBtnText: "上一步", // 此步骤的上一个按钮文本 16 | }); 17 | 18 | const guide = function () { 19 | driver.defineSteps(steps); 20 | driver.start(); 21 | }; 22 | const Guide = function () { 23 | const cardContent = `引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。 24 | 本Demo是基于driver.js` 25 | return ( 26 |
27 | 28 | 31 |
32 | ); 33 | }; 34 | 35 | export default Guide; 36 | -------------------------------------------------------------------------------- /src/views/guide/steps.js: -------------------------------------------------------------------------------- 1 | const steps = [ 2 | { 3 | element: '.ant-btn-primary', 4 | popover: { 5 | title: '打开引导', 6 | description: '打开页面引导', 7 | position: 'bottom' 8 | } 9 | }, 10 | { 11 | element: '.hamburger-container', 12 | popover: { 13 | title: 'Hamburger', 14 | description: '打开/收起左侧导航栏', 15 | position: 'bottom' 16 | } 17 | }, 18 | { 19 | element: '.fullScreen-container', 20 | popover: { 21 | title: 'Screenfull', 22 | description: '全屏', 23 | position: 'left' 24 | } 25 | }, 26 | { 27 | element: '.settings-container', 28 | popover: { 29 | title: 'Settings', 30 | description: '系统设置', 31 | position: 'left' 32 | } 33 | }, 34 | 35 | ] 36 | 37 | export default steps 38 | -------------------------------------------------------------------------------- /src/views/layout/Content/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect, withRouter, Route, Switch } from "react-router-dom"; 3 | import DocumentTitle from "react-document-title"; 4 | import { connect } from "react-redux"; 5 | import { CSSTransition, TransitionGroup } from "react-transition-group"; 6 | import { Layout } from "antd"; 7 | import { getMenuItemInMenuListByProperty } from "@/utils"; 8 | import routeList from "@/config/routeMap"; 9 | import menuList from "@/config/menuConfig"; 10 | const { Content } = Layout; 11 | 12 | const getPageTitle = (menuList, pathname) => { 13 | let title = "Ant Design Pro"; 14 | let item = getMenuItemInMenuListByProperty(menuList, "path", pathname); 15 | if (item) { 16 | title = `${item.title} - Ant Design Pro`; 17 | } 18 | return title; 19 | }; 20 | 21 | const LayoutContent = (props) => { 22 | const { role, location } = props; 23 | const { pathname } = location; 24 | const handleFilter = (route) => { 25 | // 过滤没有权限的页面 26 | return role === "admin" || !route.roles || route.roles.includes(role); 27 | }; 28 | return ( 29 | 30 | 31 | 32 | 38 | 39 | 40 | {routeList.map((route) => { 41 | return ( 42 | handleFilter(route) && ( 43 | 48 | ) 49 | ); 50 | })} 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default connect((state) => state.user)(withRouter(LayoutContent)); 61 | -------------------------------------------------------------------------------- /src/views/layout/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Icon, Menu, Dropdown, Modal, Layout, Avatar } from "antd"; 4 | import { Link } from "react-router-dom"; 5 | import { logout, getUserInfo } from "@/store/actions"; 6 | import FullScreen from "@/components/FullScreen"; 7 | import Settings from "@/components/Settings"; 8 | import Hamburger from "@/components/Hamburger"; 9 | import BreadCrumb from "@/components/BreadCrumb"; 10 | import "./index.less"; 11 | const { Header } = Layout; 12 | 13 | const LayoutHeader = (props) => { 14 | const { 15 | token, 16 | avatar, 17 | sidebarCollapsed, 18 | logout, 19 | getUserInfo, 20 | showSettings, 21 | fixedHeader, 22 | } = props; 23 | token && getUserInfo(token); 24 | const handleLogout = (token) => { 25 | Modal.confirm({ 26 | title: "注销", 27 | content: "确定要退出系统吗?", 28 | okText: "确定", 29 | cancelText: "取消", 30 | onOk: () => { 31 | logout(token); 32 | }, 33 | }); 34 | }; 35 | const onClick = ({ key }) => { 36 | switch (key) { 37 | case "logout": 38 | handleLogout(token); 39 | break; 40 | default: 41 | break; 42 | } 43 | }; 44 | const menu = ( 45 | 46 | 47 | 首页 48 | 49 | 50 | 55 | 项目地址 56 | 57 | 58 | 59 | 注销 60 | 61 | ); 62 | const computedStyle = () => { 63 | let styles; 64 | if (fixedHeader) { 65 | if (sidebarCollapsed) { 66 | styles = { 67 | width: "calc(100% - 80px)", 68 | }; 69 | } else { 70 | styles = { 71 | width: "calc(100% - 200px)", 72 | }; 73 | } 74 | } else { 75 | styles = { 76 | width: "100%", 77 | }; 78 | } 79 | return styles; 80 | }; 81 | return ( 82 | <> 83 | {/* 这里是仿照antd pro的做法,如果固定header, 84 | 则header的定位变为fixed,此时需要一个定位为relative的header把原来的header位置撑起来 */} 85 | {fixedHeader ?
: null} 86 |
90 | 91 | 92 |
93 | 94 | {showSettings ? : null} 95 |
96 | 97 |
98 | 99 | 100 |
101 |
102 |
103 |
104 |
105 | 106 | ); 107 | }; 108 | 109 | const mapStateToProps = (state) => { 110 | return { 111 | ...state.app, 112 | ...state.user, 113 | ...state.settings, 114 | }; 115 | }; 116 | export default connect(mapStateToProps, { logout, getUserInfo })(LayoutHeader); 117 | -------------------------------------------------------------------------------- /src/views/layout/Header/index.less: -------------------------------------------------------------------------------- 1 | .ant-layout-header { 2 | transition: width 0.2s; 3 | z-index: 9; 4 | position: relative; 5 | height: 64px; 6 | background: #fff !important; 7 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 8 | .right-menu { 9 | float: right; 10 | .dropdown-wrap { 11 | cursor: pointer; 12 | display: inline-block; 13 | .ant-dropdown-menu-item { 14 | padding: 6px 20px 8px 15px; 15 | i { 16 | padding-right: 15px; 17 | } 18 | } 19 | .anticon-caret-down { 20 | vertical-align: bottom; 21 | padding: 0 0 12px 6px; 22 | } 23 | } 24 | } 25 | } 26 | .fix-header { 27 | position: fixed; 28 | top: 0; 29 | right: 0; 30 | } 31 | -------------------------------------------------------------------------------- /src/views/layout/RightPanel/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Drawer, Switch, Row, Col, Divider, Alert, Icon, Button } from "antd"; 4 | import { toggleSettingPanel, changeSetting } from "@/store/actions"; 5 | import clip from "@/utils/clipboard"; 6 | 7 | const RightPanel = (props) => { 8 | const { 9 | settingPanelVisible, 10 | toggleSettingPanel, 11 | changeSetting, 12 | sidebarLogo: defaultSidebarLogo, 13 | fixedHeader: defaultFixedHeader, 14 | tagsView: defaultTagsView, 15 | } = props; 16 | 17 | const [sidebarLogo, setSidebarLogo] = useState(defaultSidebarLogo); 18 | const [fixedHeader, setFixedHeader] = useState(defaultFixedHeader); 19 | const [tagsView, setTagsView] = useState(defaultTagsView); 20 | 21 | const sidebarLogoChange = (checked) => { 22 | setSidebarLogo(checked); 23 | changeSetting({ key: "sidebarLogo", value: checked }); 24 | }; 25 | 26 | const fixedHeaderChange = (checked) => { 27 | setFixedHeader(checked); 28 | changeSetting({ key: "fixedHeader", value: checked }); 29 | }; 30 | 31 | const tagsViewChange = (checked) => { 32 | setTagsView(checked); 33 | changeSetting({ key: "tagsView", value: checked }); 34 | }; 35 | 36 | const handleCopy = (e) => { 37 | let config = ` 38 | export default { 39 | showSettings: true, 40 | sidebarLogo: ${sidebarLogo}, 41 | fixedHeader: ${fixedHeader}, 42 | tagsView: ${tagsView}, 43 | } 44 | `; 45 | clip(config, e); 46 | }; 47 | 48 | return ( 49 |
50 | 57 | 58 |
59 | 侧边栏 Logo 60 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 固定 Header 74 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 开启 Tags-View 88 | 89 | 90 | 96 | 97 | 98 | 99 | 100 | 101 | } 107 | style={{ marginBottom: "16px" }} 108 | /> 109 | 112 | 113 | 114 | 115 | 116 | ); 117 | }; 118 | 119 | const mapStateToProps = (state) => { 120 | return { 121 | ...state.app, 122 | ...state.settings, 123 | }; 124 | }; 125 | 126 | export default connect(mapStateToProps, { toggleSettingPanel, changeSetting })( 127 | RightPanel 128 | ); 129 | -------------------------------------------------------------------------------- /src/views/layout/Sider/Logo/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "@/assets/images/logo.svg"; 3 | import "./index.less"; 4 | const Logo = () => { 5 | return ( 6 |
7 | logo 8 |

难凉热血

9 |
10 | ); 11 | }; 12 | 13 | export default Logo; 14 | -------------------------------------------------------------------------------- /src/views/layout/Sider/Logo/index.less: -------------------------------------------------------------------------------- 1 | .sidebar-logo-container { 2 | position: relative; 3 | width: 100%; 4 | height: 64px; 5 | line-height: 64px; 6 | background: #2b2f3a; 7 | text-align: center; 8 | overflow: hidden; 9 | 10 | & .sidebar-logo { 11 | width: 60px; 12 | animation: sidebar-logo-spin infinite 10s linear; 13 | } 14 | 15 | & .sidebar-title { 16 | display: inline-block; 17 | margin: 0; 18 | color: #fff; 19 | font-weight: 600; 20 | line-height: 50px; 21 | font-size: 14px; 22 | font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; 23 | vertical-align: middle; 24 | } 25 | } 26 | 27 | @keyframes sidebar-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/views/layout/Sider/Menu/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Menu, Icon } from "antd"; 3 | import { Link, withRouter } from "react-router-dom"; 4 | import { Scrollbars } from "react-custom-scrollbars"; 5 | import { connect } from "react-redux"; 6 | import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; 7 | import { addTag } from "@/store/actions"; 8 | import { getMenuItemInMenuListByProperty } from "@/utils"; 9 | import menuList from "@/config/menuConfig"; 10 | import "./index.less"; 11 | const SubMenu = Menu.SubMenu; 12 | // 重新记录数组顺序 13 | const reorder = (list, startIndex, endIndex) => { 14 | const result = Array.from(list); 15 | const [removed] = result.splice(startIndex, 1); 16 | result.splice(endIndex, 0, removed); 17 | return result; 18 | }; 19 | 20 | class Meun extends Component { 21 | state = { 22 | menuTreeNode: null, 23 | openKey: [], 24 | }; 25 | 26 | // filterMenuItem用来根据配置信息筛选可以显示的菜单项 27 | filterMenuItem = (item) => { 28 | const { roles } = item; 29 | const { role } = this.props; 30 | if (role === "admin" || !roles || roles.includes(role)) { 31 | return true; 32 | } else if (item.children) { 33 | // 如果当前用户有此item的某个子item的权限 34 | return !!item.children.find((child) => roles.includes(child.role)); 35 | } 36 | return false; 37 | }; 38 | // 菜单渲染 39 | getMenuNodes = (menuList) => { 40 | // 得到当前请求的路由路径 41 | const path = this.props.location.pathname; 42 | return menuList.reduce((pre, item) => { 43 | if (this.filterMenuItem(item)) { 44 | if (!item.children) { 45 | pre.push( 46 | 47 | 48 | {item.icon ? : null} 49 | {item.title} 50 | 51 | 52 | ); 53 | } else { 54 | // 查找一个与当前请求路径匹配的子Item 55 | const cItem = item.children.find( 56 | (cItem) => path.indexOf(cItem.path) === 0 57 | ); 58 | // 如果存在, 说明当前item的子列表需要打开 59 | if (cItem) { 60 | this.setState((state) => ({ 61 | openKey: [...state.openKey, item.path], 62 | })); 63 | } 64 | 65 | // 向pre添加 66 | pre.push( 67 | 71 | {item.icon ? : null} 72 | {item.title} 73 | 74 | } 75 | > 76 | {this.getMenuNodes(item.children)} 77 | 78 | ); 79 | } 80 | } 81 | 82 | return pre; 83 | }, []); 84 | }; 85 | 86 | onDragEnd = (result) => { 87 | if (!result.destination) { 88 | return; 89 | } 90 | const _items = reorder( 91 | this.state.menuTreeNode, 92 | result.source.index, 93 | result.destination.index 94 | ); 95 | this.setState({ 96 | menuTreeNode: _items, 97 | }); 98 | }; 99 | 100 | handleMenuSelect = ({ key = "/dashboard" }) => { 101 | let menuItem = getMenuItemInMenuListByProperty(menuList, "path", key); 102 | this.props.addTag(menuItem); 103 | }; 104 | 105 | componentWillMount() { 106 | const menuTreeNode = this.getMenuNodes(menuList); 107 | this.setState({ 108 | menuTreeNode, 109 | }); 110 | this.handleMenuSelect(this.state.openKey); 111 | } 112 | render() { 113 | const path = this.props.location.pathname; 114 | const openKey = this.state.openKey; 115 | return ( 116 |
117 | 118 | 119 | 120 | {(provided, snapshot) => ( 121 |
122 | {this.state.menuTreeNode.map((item, index) => ( 123 | 128 | {(provided, snapshot) => ( 129 |
134 | 141 | {item} 142 | 143 |
144 | )} 145 |
146 | ))} 147 |
148 | )} 149 |
150 |
151 |
152 |
153 | ); 154 | } 155 | } 156 | 157 | export default connect((state) => state.user, { addTag })(withRouter(Meun)); 158 | -------------------------------------------------------------------------------- /src/views/layout/Sider/Menu/index.less: -------------------------------------------------------------------------------- 1 | .sidebar-menu-container { 2 | height:calc(100% - 64px) 3 | } -------------------------------------------------------------------------------- /src/views/layout/Sider/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Layout } from "antd"; 4 | import Logo from "./Logo"; 5 | import Menu from "./Menu"; 6 | const { Sider } = Layout; 7 | 8 | const LayoutSider = (props) => { 9 | const { sidebarCollapsed, sidebarLogo } = props; 10 | return ( 11 | 17 | {sidebarLogo ? : null} 18 | 19 | 20 | ); 21 | }; 22 | 23 | const mapStateToProps = (state) => { 24 | return { 25 | ...state.app, 26 | ...state.settings, 27 | }; 28 | }; 29 | export default connect(mapStateToProps)(LayoutSider); 30 | -------------------------------------------------------------------------------- /src/views/layout/TagsView/components/TagList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { withRouter } from "react-router-dom"; 4 | import { Scrollbars } from "react-custom-scrollbars"; 5 | import { Tag } from "antd"; 6 | import { deleteTag, emptyTaglist, closeOtherTags } from "@/store/actions"; 7 | class TagList extends Component { 8 | tagListContainer = React.createRef(); 9 | contextMenuContainer = React.createRef(); 10 | state = { 11 | left: 0, 12 | top: 0, 13 | menuVisible: false, 14 | }; 15 | handleClose = (tag) => { 16 | const { history, deleteTag, taglist } = this.props; 17 | const path = tag.path; 18 | const currentPath = history.location.pathname; 19 | const length = taglist.length; 20 | // 如果关闭的是当前页,跳转到最后一个tag 21 | if (path === currentPath) { 22 | history.push(taglist[length - 1].path); 23 | } 24 | // 如果关闭的是最后的tag ,且当前显示的也是最后的tag对应的页面,才做路由跳转 25 | if ( 26 | path === taglist[length - 1].path && 27 | currentPath === taglist[length - 1].path 28 | ) { 29 | // 因为cutTaglist在最后执行,所以跳转到上一个tags的对应的路由,应该-2 30 | if (length - 2 > 0) { 31 | history.push(taglist[length - 2].path); 32 | } else if (length === 2) { 33 | history.push(taglist[0].path); 34 | } 35 | } 36 | 37 | // 先跳转路由,再修改state树的taglist 38 | deleteTag(tag); 39 | }; 40 | handleClick = (path) => { 41 | this.props.history.push(path); 42 | }; 43 | openContextMenu = (tag, event) => { 44 | event.preventDefault(); 45 | const menuMinWidth = 105; 46 | const clickX = event.clientX; 47 | const clickY = event.clientY; //事件发生时鼠标的Y坐标 48 | const clientWidth = this.tagListContainer.current.clientWidth; // container width 49 | const maxLeft = clientWidth - menuMinWidth; // left boundary 50 | 51 | // 当鼠标点击位置大于左侧边界时,说明鼠标点击的位置偏右,将菜单放在左边 52 | if (clickX > maxLeft) { 53 | this.setState({ 54 | left: clickX - menuMinWidth + 15, 55 | top: clickY, 56 | menuVisible: true, 57 | currentTag: tag, 58 | }); 59 | } else { 60 | // 反之,当鼠标点击的位置偏左,将菜单放在右边 61 | this.setState({ 62 | left: clickX, 63 | top: clickY, 64 | menuVisible: true, 65 | currentTag: tag, 66 | }); 67 | } 68 | }; 69 | handleClickOutside = (event) => { 70 | const { menuVisible } = this.state; 71 | const isOutside = !( 72 | this.contextMenuContainer.current && 73 | this.contextMenuContainer.current.contains(event.target) 74 | ); 75 | if (isOutside && menuVisible) { 76 | this.closeContextMenu(); 77 | } 78 | }; 79 | closeContextMenu() { 80 | this.setState({ 81 | menuVisible: false, 82 | }); 83 | } 84 | componentDidMount() { 85 | document.body.addEventListener("click", this.handleClickOutside); 86 | } 87 | componentWillUnmount() { 88 | document.body.removeEventListener("click", this.handleClickOutside); 89 | } 90 | handleCloseAllTags = () => { 91 | this.props.emptyTaglist(); 92 | this.props.history.push("/dashboard"); 93 | this.closeContextMenu(); 94 | }; 95 | handleCloseOtherTags = () => { 96 | const currentTag = this.state.currentTag; 97 | const { path } = currentTag; 98 | this.props.closeOtherTags(currentTag) 99 | this.props.history.push(path); 100 | this.closeContextMenu(); 101 | }; 102 | render() { 103 | const { left, top, menuVisible } = this.state; 104 | const { taglist, history } = this.props; 105 | const currentPath = history.location.pathname; 106 | return ( 107 | <> 108 | ( 114 |
115 | )} 116 | renderTrackVertical={(props) => ( 117 |
118 | )} 119 | > 120 |
    121 | {taglist.map((tag) => ( 122 |
  • 123 | 130 | {tag.title} 131 | 132 |
  • 133 | ))} 134 |
135 | 136 | {menuVisible ? ( 137 |
    142 |
  • 关闭其他
  • 143 |
  • 关闭所有
  • 144 |
145 | ) : null} 146 | 147 | ); 148 | } 149 | } 150 | export default withRouter( 151 | connect((state) => state.tagsView, { 152 | deleteTag, 153 | emptyTaglist, 154 | closeOtherTags, 155 | })(TagList) 156 | ); 157 | -------------------------------------------------------------------------------- /src/views/layout/TagsView/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TagList from "./components/TagList"; 3 | import "./index.less"; 4 | 5 | const TagsView = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default TagsView; 14 | -------------------------------------------------------------------------------- /src/views/layout/TagsView/index.less: -------------------------------------------------------------------------------- 1 | .tagsView-container { 2 | white-space: nowrap; 3 | width: 100%; 4 | height: 36px; 5 | line-height: 36px; 6 | border-bottom: 1px solid #d8dce5; 7 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); 8 | background: #fff; 9 | .tags-wrap { 10 | padding: 0; 11 | & > li { 12 | display: inline-block; 13 | &:first-of-type { 14 | margin-left: 15px; 15 | } 16 | &:last-of-type { 17 | margin-right: 15px; 18 | } 19 | } 20 | } 21 | } 22 | .scrollbar-container { 23 | overflow-y: hidden !important; 24 | } 25 | .scrollbar-track-vertical { 26 | visibility: hidden !important; 27 | } 28 | .contextmenu { 29 | margin: 0; 30 | background: #fff; 31 | z-index: 3000; 32 | position: absolute; 33 | list-style-type: none; 34 | padding: 5px 0; 35 | border-radius: 4px; 36 | font-size: 12px; 37 | font-weight: 400; 38 | color: #333; 39 | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); 40 | li { 41 | margin: 0; 42 | padding: 0px 16px; 43 | height: 30px; 44 | cursor: pointer; 45 | &:hover { 46 | background: #eee; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/views/layout/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import Content from "./Content"; 4 | import Header from "./Header"; 5 | import RightPanel from "./RightPanel"; 6 | import Sider from "./Sider"; 7 | import TagsView from "./TagsView"; 8 | import { Layout } from "antd"; 9 | const Main = (props) => { 10 | const { tagsView } = props; 11 | return ( 12 | 13 | 14 | 15 |
16 | {tagsView ? : null} 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | export default connect((state) => state.settings)(Main); 24 | -------------------------------------------------------------------------------- /src/views/login/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import { Form, Icon, Input, Button, message, Spin } from "antd"; 4 | import { connect } from "react-redux"; 5 | import DocumentTitle from "react-document-title"; 6 | import "./index.less"; 7 | import { login, getUserInfo } from "@/store/actions"; 8 | 9 | const Login = (props) => { 10 | const { form, token, login, getUserInfo } = props; 11 | const { getFieldDecorator } = form; 12 | 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleLogin = (username, password) => { 16 | // 登录完成后 发送请求 调用接口获取用户信息 17 | setLoading(true); 18 | login(username, password) 19 | .then((data) => { 20 | message.success("登录成功"); 21 | handleUserInfo(data.token); 22 | }) 23 | .catch((error) => { 24 | setLoading(false); 25 | message.error(error); 26 | }); 27 | }; 28 | 29 | // 获取用户信息 30 | const handleUserInfo = (token) => { 31 | getUserInfo(token) 32 | .then((data) => {}) 33 | .catch((error) => { 34 | message.error(error); 35 | }); 36 | }; 37 | 38 | const handleSubmit = (event) => { 39 | // 阻止事件的默认行为 40 | event.preventDefault(); 41 | 42 | // 对所有表单字段进行检验 43 | form.validateFields((err, values) => { 44 | // 检验成功 45 | if (!err) { 46 | const { username, password } = values; 47 | handleLogin(username, password); 48 | } else { 49 | console.log("检验失败!"); 50 | } 51 | }); 52 | }; 53 | 54 | if (token) { 55 | return ; 56 | } 57 | return ( 58 | 59 |
60 |
61 |
62 |

用户登录

63 |
64 | 65 | 66 | {getFieldDecorator("username", { 67 | rules: [ 68 | { 69 | required: true, 70 | whitespace: true, 71 | message: "请输入用户名", 72 | }, 73 | ], 74 | initialValue: "admin", // 初始值 75 | })( 76 | 79 | } 80 | placeholder="用户名" 81 | /> 82 | )} 83 | 84 | 85 | {getFieldDecorator("password", { 86 | rules: [ 87 | { 88 | required: true, 89 | whitespace: true, 90 | message: "请输入密码", 91 | }, 92 | ], 93 | initialValue: "123456", // 初始值 94 | })( 95 | 98 | } 99 | type="password" 100 | placeholder="密码" 101 | /> 102 | )} 103 | 104 | 105 | 112 | 113 | 114 | 账号 : admin 密码 : 随便填 115 |
116 | 账号 : editor 密码 : 随便填 117 |
118 | 账号 : guest 密码 : 随便填 119 |
120 |
121 | 122 |
123 |
124 | ); 125 | }; 126 | 127 | const WrapLogin = Form.create()(Login); 128 | 129 | export default connect((state) => state.user, { login, getUserInfo })( 130 | WrapLogin 131 | ); 132 | -------------------------------------------------------------------------------- /src/views/login/index.less: -------------------------------------------------------------------------------- 1 | .login-container { 2 | height: 100%; 3 | overflow: hidden; 4 | position: relative; 5 | // background: #2d3a4b; 6 | background-image: url('~@/assets/images/bg.jpg'); 7 | background-size: 100% 100%; 8 | & > .title { 9 | color: #eee; 10 | font-size: 26px; 11 | font-weight: 400; 12 | margin: 0px auto 30px auto; 13 | text-align: center; 14 | font-weight: bold; 15 | letter-spacing: 1px; 16 | } 17 | .spin-wrap { 18 | text-align: center; 19 | background-color: rgba(255, 255, 255, 0.3); 20 | border-radius: 4px; 21 | position: absolute; 22 | left: 0; 23 | top: 0; 24 | width: 100vw; 25 | height: 100vh; 26 | line-height: 90vh; 27 | z-index: 999; 28 | } 29 | .content { 30 | position: absolute; 31 | background-color: #fff; 32 | left: 50%; 33 | top: 50%; 34 | width: 320px; 35 | padding: 30px 30px 0 30px; 36 | transform: translate(-50%, -60%); 37 | box-shadow: 0 0 10px 2px rgba(40, 138, 204, 0.16); 38 | border-radius: 3px; 39 | 40 | .title { 41 | text-align: center; 42 | margin: 0 0 30px 0; 43 | } 44 | .two-button-wrap { 45 | display: flex; 46 | justify-content: space-between; 47 | } 48 | 49 | .row-container { 50 | display: flex; 51 | justify-content: space-between; 52 | } 53 | 54 | .user-type-button { 55 | width: 45%; 56 | } 57 | 58 | .login-form-button { 59 | width: 100%; 60 | } 61 | .login-form-forgot { 62 | float: right; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-1/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const Menu1_1 = () => { 3 | return

Menu1-1

; 4 | }; 5 | 6 | export default Menu1_1; 7 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-1/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const Menu1_2_1 = () => { 3 | return

Menu1-2-1

; 4 | }; 5 | 6 | export default Menu1_2_1; -------------------------------------------------------------------------------- /src/views/permission/adminPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TypingCard from '@/components/TypingCard' 3 | const AdminPage = () => { 4 | const cardContent = `这个页面只有admin角色才可以访问,guest和editor角色看不到` 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default AdminPage; -------------------------------------------------------------------------------- /src/views/permission/editorPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TypingCard from '@/components/TypingCard' 3 | const GuestPage = () => { 4 | const cardContent = `这个页面只有admin和editor角色才可以访问,guest角色看不到` 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default GuestPage; -------------------------------------------------------------------------------- /src/views/permission/guestPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TypingCard from '@/components/TypingCard' 3 | const GuestPage = () => { 4 | const cardContent = `这个页面只有admin和guest角色才可以访问,editor角色看不到` 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default GuestPage; -------------------------------------------------------------------------------- /src/views/permission/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TypingCard from "@/components/TypingCard"; 3 | export default () => { 4 | const cardContent = ` 5 | 本项目中的菜单权限和路由权限都是基于用户所属角色来分配的,本项目中内置了三种角色,分别是: 6 | 7 |
    8 |
  • 管理员 admin:该角色拥有系统内所有菜单和路由的权限。
  • 9 |
  • 编辑员 editor:该角色拥有系统内除用户管理页之外的所有菜单和路由的权限。
  • 10 |
  • 游客 guest:该角色仅拥有Dashboard、作者博客、权限测试和关于作者三个页面的权限。
  • 11 |
12 | 13 | 你可以通过用户管理页面,动态的添加或删除用户,以及编辑某个已经存在的用户,例如修改其权限等操作。 14 | `; 15 | return ( 16 |
17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/views/table/forms/editForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Form, Input, DatePicker, Select, Rate, Modal } from "antd"; 3 | import moment from "moment"; 4 | import "moment/locale/zh-cn"; 5 | moment.locale("zh-cn"); 6 | class EditForm extends Component { 7 | render() { 8 | const { 9 | visible, 10 | onCancel, 11 | onOk, 12 | form, 13 | confirmLoading, 14 | currentRowData, 15 | } = this.props; 16 | const { getFieldDecorator } = form; 17 | const { id, author, date, readings, star, status, title } = currentRowData; 18 | const formItemLayout = { 19 | labelCol: { 20 | sm: { span: 4 }, 21 | }, 22 | wrapperCol: { 23 | sm: { span: 16 }, 24 | }, 25 | }; 26 | return ( 27 | 34 |
35 | 36 | {getFieldDecorator("id", { 37 | initialValue: id, 38 | })()} 39 | 40 | 41 | {getFieldDecorator("title", { 42 | rules: [{ required: true, message: "请输入标题!" }], 43 | initialValue: title, 44 | })()} 45 | 46 | 47 | {getFieldDecorator("author", { 48 | initialValue: author, 49 | })()} 50 | 51 | 52 | {getFieldDecorator("readings", { 53 | initialValue: readings, 54 | })()} 55 | 56 | 57 | {getFieldDecorator("star", { 58 | initialValue: star.length, 59 | })()} 60 | 61 | 62 | {getFieldDecorator("status", { 63 | initialValue: status, 64 | })( 65 | 69 | )} 70 | 71 | 72 | {getFieldDecorator("date", { 73 | rules: [{ type: 'object', required: true, message: '请选择时间!' }], 74 | initialValue: moment(date || "YYYY-MM-DD HH:mm:ss"), 75 | })()} 76 | 77 | 78 |
79 | ); 80 | } 81 | } 82 | 83 | export default Form.create({ name: "EditForm" })(EditForm); 84 | -------------------------------------------------------------------------------- /src/views/table/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | Table, 4 | Tag, 5 | Form, 6 | Button, 7 | Input, 8 | Collapse, 9 | Pagination, 10 | Divider, 11 | message, 12 | Select 13 | } from "antd"; 14 | import { tableList, deleteItem,editItem } from "@/api/table"; 15 | import EditForm from "./forms/editForm" 16 | const { Column } = Table; 17 | const { Panel } = Collapse; 18 | class TableComponent extends Component { 19 | _isMounted = false; // 这个变量是用来标志当前组件是否挂载 20 | state = { 21 | list: [], 22 | loading: false, 23 | total: 0, 24 | listQuery: { 25 | pageNumber: 1, 26 | pageSize: 10, 27 | title: "", 28 | star: "", 29 | status:"" 30 | }, 31 | editModalVisible: false, 32 | editModalLoading: false, 33 | currentRowData: { 34 | id: 0, 35 | author: "", 36 | date: "", 37 | readings: 0, 38 | star: "★", 39 | status: "published", 40 | title: "" 41 | } 42 | }; 43 | fetchData = () => { 44 | this.setState({ loading: true }); 45 | tableList(this.state.listQuery).then((response) => { 46 | this.setState({ loading: false }); 47 | const list = response.data.data.items; 48 | const total = response.data.data.total; 49 | if (this._isMounted) { 50 | this.setState({ list, total }); 51 | } 52 | }); 53 | }; 54 | componentDidMount() { 55 | this._isMounted = true; 56 | this.fetchData(); 57 | } 58 | componentWillUnmount() { 59 | this._isMounted = false; 60 | } 61 | filterTitleChange = (e) => { 62 | let value = e.target.value 63 | this.setState((state) => ({ 64 | listQuery: { 65 | ...state.listQuery, 66 | title:value, 67 | } 68 | })); 69 | }; 70 | filterStatusChange = (value) => { 71 | this.setState((state) => ({ 72 | listQuery: { 73 | ...state.listQuery, 74 | status:value, 75 | } 76 | })); 77 | }; 78 | filterStarChange = (value) => { 79 | this.setState((state) => ({ 80 | listQuery: { 81 | ...state.listQuery, 82 | star:value, 83 | } 84 | })); 85 | }; 86 | changePage = (pageNumber, pageSize) => { 87 | this.setState( 88 | (state) => ({ 89 | listQuery: { 90 | ...state.listQuery, 91 | pageNumber, 92 | }, 93 | }), 94 | () => { 95 | this.fetchData(); 96 | } 97 | ); 98 | }; 99 | changePageSize = (current, pageSize) => { 100 | this.setState( 101 | (state) => ({ 102 | listQuery: { 103 | ...state.listQuery, 104 | pageNumber: 1, 105 | pageSize, 106 | }, 107 | }), 108 | () => { 109 | this.fetchData(); 110 | } 111 | ); 112 | }; 113 | handleDelete = (row) => { 114 | deleteItem({id:row.id}).then(res => { 115 | message.success("删除成功") 116 | this.fetchData(); 117 | }) 118 | } 119 | handleEdit = (row) => { 120 | this.setState({ 121 | currentRowData:Object.assign({}, row), 122 | editModalVisible: true, 123 | }); 124 | }; 125 | 126 | handleOk = _ => { 127 | const { form } = this.formRef.props; 128 | form.validateFields((err, fieldsValue) => { 129 | if (err) { 130 | return; 131 | } 132 | const values = { 133 | ...fieldsValue, 134 | 'star': "".padStart(fieldsValue['star'], '★'), 135 | 'date': fieldsValue['date'].format('YYYY-MM-DD HH:mm:ss'), 136 | }; 137 | this.setState({ editModalLoading: true, }); 138 | editItem(values).then((response) => { 139 | form.resetFields(); 140 | this.setState({ editModalVisible: false, editModalLoading: false }); 141 | message.success("编辑成功!") 142 | this.fetchData() 143 | }).catch(e => { 144 | message.success("编辑失败,请重试!") 145 | }) 146 | 147 | }); 148 | }; 149 | 150 | handleCancel = _ => { 151 | this.setState({ 152 | editModalVisible: false, 153 | }); 154 | }; 155 | render() { 156 | return ( 157 |
158 | 159 | 160 |
161 | 162 | 163 | 164 | 165 | 171 | 172 | 173 | 180 | 181 | 182 | 185 | 186 | 187 |
188 |
189 |
190 |
record.id} 193 | dataSource={this.state.list} 194 | loading={this.state.loading} 195 | pagination={false} 196 | > 197 | a.id - b.id}/> 198 | 199 | 200 | 201 | 202 | { 203 | let color = 204 | status === "published" ? "green" : status === "deleted" ? "red" : ""; 205 | return ( 206 | 207 | {status} 208 | 209 | ); 210 | }}/> 211 | 212 | ( 213 | 214 |
220 |
221 | `共${total}条数据`} 225 | onChange={this.changePage} 226 | current={this.state.listQuery.pageNumber} 227 | onShowSizeChange={this.changePageSize} 228 | showSizeChanger 229 | showQuickJumper 230 | hideOnSinglePage={true} 231 | /> 232 | this.formRef = formRef} 235 | visible={this.state.editModalVisible} 236 | confirmLoading={this.state.editModalLoading} 237 | onCancel={this.handleCancel} 238 | onOk={this.handleOk} 239 | /> 240 |
241 | ); 242 | } 243 | } 244 | 245 | export default TableComponent; 246 | -------------------------------------------------------------------------------- /src/views/user/forms/add-user-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Form, Input, Select, Modal } from "antd"; 3 | import { reqValidatUserID } from "@/api/user"; 4 | const { TextArea } = Input; 5 | class AddUserForm extends Component { 6 | validatUserID = async (rule, value, callback) => { 7 | if (value) { 8 | if (!/^[a-zA-Z0-9]{1,6}$/.test(value)) { 9 | callback("用户ID必须为1-6位数字或字母组合"); 10 | } 11 | let res = await reqValidatUserID(value); 12 | const { status } = res.data; 13 | if (status) { 14 | callback("该用户ID已存在"); 15 | } 16 | } else { 17 | callback("请输入用户ID"); 18 | } 19 | callback(); 20 | }; 21 | render() { 22 | const { visible, onCancel, onOk, form, confirmLoading } = this.props; 23 | const { getFieldDecorator } = form; 24 | const formItemLayout = { 25 | labelCol: { 26 | sm: { span: 4 }, 27 | }, 28 | wrapperCol: { 29 | sm: { span: 16 }, 30 | }, 31 | }; 32 | return ( 33 | 40 |
41 | 42 | {getFieldDecorator("id", { 43 | rules: [{ required: true, validator: this.validatUserID }], 44 | })()} 45 | 46 | 47 | {getFieldDecorator("name", { 48 | rules: [{ required: true, message: "请输入用户名称!" }], 49 | })()} 50 | 51 | 52 | {getFieldDecorator("role", { 53 | initialValue: "admin", 54 | })( 55 | 59 | )} 60 | 61 | 62 | {getFieldDecorator("description", { 63 | })(