├── .commitlintrc.js ├── .env ├── .env.ra.dev ├── .env.ra.production ├── .env.ra.starandsea ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── package.json ├── public ├── assets │ └── less.min.js ├── favicon.ico ├── images │ └── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png ├── index.html ├── logo.png ├── manifest.json ├── robots.txt └── theme.less ├── screenshots ├── logo.png ├── menu_draggable.gif ├── pwa.png └── themepicker.png ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── App.test.tsx ├── App.tsx ├── Page.tsx ├── components │ ├── HeaderCustom.tsx │ ├── Page.tsx │ ├── SiderCustom.tsx │ ├── SiderMenu.tsx │ ├── animation │ │ ├── BasicAnimations.tsx │ │ └── ExampleAnimations.tsx │ ├── auth │ │ ├── Basic.tsx │ │ └── RouterEnter.tsx │ ├── charts │ │ ├── Echarts.tsx │ │ ├── EchartsArea.tsx │ │ ├── EchartsEffectScatter.tsx │ │ ├── EchartsForce.tsx │ │ ├── EchartsGraphnpm.tsx │ │ ├── EchartsPie.tsx │ │ ├── EchartsScatter.tsx │ │ ├── Recharts.tsx │ │ ├── RechartsBarChart.tsx │ │ ├── RechartsRadarChart.tsx │ │ ├── RechartsRadialBarChart.tsx │ │ └── RechartsSimpleLineChart.tsx │ ├── cssmodule │ │ ├── index.module.less │ │ └── index.tsx │ ├── dashboard │ │ ├── Dashboard.tsx │ │ ├── EchartsProjects.tsx │ │ └── EchartsViews.tsx │ ├── env │ │ └── index.tsx │ ├── extension │ │ ├── MultipleMenu.tsx │ │ ├── QueryParams.tsx │ │ └── Visitor.tsx │ ├── index.tsx │ ├── pages │ │ ├── Login.tsx │ │ └── NotFound.tsx │ ├── smenu │ │ ├── Sub1.tsx │ │ └── Sub2.tsx │ ├── tables │ │ ├── AdvancedTables.tsx │ │ ├── AsynchronousTable.tsx │ │ ├── BasicTable.tsx │ │ ├── BasicTables.tsx │ │ ├── ExpandedTable.tsx │ │ ├── FixedTable.tsx │ │ ├── SearchTable.tsx │ │ ├── SelectTable.tsx │ │ └── SortTable.tsx │ ├── ui │ │ ├── Buttons.tsx │ │ ├── Draggable.tsx │ │ ├── Gallery.tsx │ │ ├── Icons.tsx │ │ ├── Modals.tsx │ │ ├── Notifications.tsx │ │ ├── Spins.tsx │ │ ├── Tabs.tsx │ │ ├── Wysiwyg.tsx │ │ ├── banners │ │ │ ├── AutoPlay.tsx │ │ │ ├── Basic.tsx │ │ │ ├── Custom.tsx │ │ │ └── index.tsx │ │ ├── emoji │ │ │ ├── iconfont.ts │ │ │ └── index.tsx │ │ └── map │ │ │ ├── Tencent.tsx │ │ │ └── index.tsx │ └── widget │ │ ├── AuthWidget.tsx │ │ ├── BreadcrumbCustom.tsx │ │ ├── Copyright.tsx │ │ ├── Loading.tsx │ │ ├── PwaInstaller.tsx │ │ ├── ThemePicker.tsx │ │ └── index.tsx ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── routes │ ├── RouteWrapper.tsx │ ├── config.ts │ └── index.tsx ├── service │ ├── config.ts │ ├── index.ts │ └── tools.ts ├── serviceWorker.ts ├── style │ ├── antd │ │ ├── header.less │ │ ├── index.less │ │ ├── layout.less │ │ ├── menu.less │ │ ├── reset.less │ │ ├── utils.less │ │ └── variables.less │ ├── app.less │ ├── banner.less │ ├── button.less │ ├── card.less │ ├── font │ │ └── y6oxFxU60dYw9khW6q8jGw.woff2 │ ├── global.less │ ├── icons.less │ ├── img.less │ ├── imgs │ │ ├── 404.png │ │ ├── b1.jpg │ │ ├── beauty.jpg │ │ ├── installer.png │ │ ├── logo.png │ │ ├── mobile.gif │ │ └── spot_location.png │ ├── index.less │ ├── lib │ │ └── animate.css │ ├── login.less │ ├── menu.less │ ├── modal.less │ ├── scroll.less │ ├── table.less │ ├── theme │ │ ├── index.js │ │ ├── theme-danger.json │ │ ├── theme-grey.json │ │ ├── theme-info.json │ │ └── theme-warn.json │ ├── utils-border.less │ ├── utils-color.less │ ├── utils-size.less │ ├── utils-spacing.less │ ├── utils-text.less │ └── variables.less └── utils │ ├── hooks.ts │ └── index.ts ├── theme.js ├── tsconfig.json └── yarn.lock /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=3006 -------------------------------------------------------------------------------- /.env.ra.dev: -------------------------------------------------------------------------------- 1 | REACT_ADMIN_ENV=我是本地开发环境配置 -------------------------------------------------------------------------------- /.env.ra.production: -------------------------------------------------------------------------------- 1 | REACT_ADMIN_ENV=我是打包线上环境配置 2 | REACT_ADMIN_TEST=我是其他环境配置 -------------------------------------------------------------------------------- /.env.ra.starandsea: -------------------------------------------------------------------------------- 1 | REACT_ADMIN_ENV=我是星辰大海,你运行yarn starandsea试试 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "plugins": [ 4 | "react-hooks" 5 | ], 6 | "rules": { 7 | "react-hooks/rules-of-hooks": "error", 8 | "react-hooks/exhaustive-deps": "warn", 9 | "no-multi-spaces": 1, 10 | "react/jsx-tag-spacing": 1, // 总是在自动关闭的标签前加一个空格,正常情况下也不需要换行 11 | "jsx-quotes": 1, 12 | "react/jsx-closing-bracket-location": 1, // 遵循JSX语法缩进/格式 13 | "react/jsx-boolean-value": 1, // 如果属性值为 true, 可以直接省略 14 | "react/no-string-refs": 1, // 总是在Refs里使用回调函数 15 | "react/self-closing-comp": 1, // 对于没有子元素的标签来说总是自己关闭标签 16 | "react/sort-comp": 1, // 按照具体规范的React.createClass 的生命周期函数书写代码 17 | "react/jsx-pascal-case": 1 // React模块名使用帕斯卡命名,实例使用骆驼式命名 18 | } 19 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/theme.less -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "jsxBracketSameLine": false, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | script: 5 | - yarn 6 | - yarn build -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 更新日志 2 | 3 | #### 2017-07-08 4 | 5 | - 依赖包版本升级 6 | - react@15.6.1 7 | - antd@2.11.2 8 | - webpack@2.6.1 9 | - 等等 10 | 11 | #### 2017-08-01 12 | 13 | - 引入 redux 系列 14 | - redux@3.7.2 15 | - redux-thunk@2.2.0 16 | - react-redux@5.0.5 17 | - 增加权限管理模块 18 | - 使用 easy-mock 模拟数据模拟登录接口 19 | - 使用 redux 系列将登录用户数据传递给权限组件 20 | - 权限组件采用 Render Callback 的方式传递权限给需要受控制的组件(具体做法请查看源代码。) 21 | - 用户状态保存在 localStorage 中 22 | - 具体做法请运行项目查看 23 | - PS:以上管理权限只是一种方式,但这绝对不是唯一的方式,也不是最好的方式。如果你有更好的方式,不妨加上面的群和大家一起分享下。😄😄 24 | - 增加路径别名 25 | - 使用@别名处理引入组件相对路径过长问题。 26 | - 缺点:编辑器不能使用快捷提示和快捷跳转到相应的文件 27 | 28 | #### 2017-08-13 29 | 30 | - 权限管理模块增加页面跳转权限验证 31 | - 点击权限管理的路由拦截,若没有访问权限则会跳转到 404 页面。 32 | - 大致实现方式(非常简单):通过向自定义 router 组件传入 store,登录之后可获取到 redux 中的权限 state 数据,并通过判断是否包含权限进行跳转。ps: 该 demo 的效果是管理员登录之后才能跳转到路由拦截页面。具体操作请拉取代码尝试。 33 | 34 | #### 2017-08-26 35 | 36 | - 增加响应式布局 - 替换 antd Col 组件的响应式栅格为 md(具体参数用法请查看 antd 官方文档) - 初始化页面是获取当前浏览器宽度设置菜单显示类型 - 监听 window 的 onresize 函数,设置菜单显示类型。PS:浏览器宽度存入 redux 中,方便组件之间传递。 37 | ![截图](https://raw.githubusercontent.com/yezihaohao/react-admin/master/src/style/imgs/mobile.gif) 38 | 39 | #### 2017-09-13 40 | 41 | - 依赖包版本升级 42 | - antd@2.13.1(目前最新版) 43 | 44 | #### 2017-10-21 45 | 46 | - 开发环境增加 react-hot-loader-保持状态刷新组件(译:实时调整组件),可参考以下相关项目 47 | - [react-hot-loader](https://github.com/gaearon/react-hot-loader) 48 | 49 | #### 2017-12-12 50 | 51 | - 依赖包版本升级 52 | - antd@3.0.1(目前最新版) 53 | - react-router-dom@4.2.2 54 | - 大改动 55 | - react-router 切换 4.x 版本,切换响应的版本路由写法(具体见代码更新日志) 56 | - ps: react-router 3.x 的版本请查看代码分支 router3.x 57 | 58 | #### 2018-01-12 59 | 60 | - 增加 cssmodule 的支持(css, less) 61 | 62 | - 建议用 css 预处理器,文件名为 xxx.module.less,引入相应组件即可使用。 63 | 64 | - 具体做法参见新增模块,路由后缀:/app/cssModule。[点击访问](http://cheng_haohao.oschina.io/reactadmin/#/app/cssModule) 65 | 66 | #### 2018-10-13 67 | 68 | - 重大更新 :sparkles: 69 | - 升级 create-react-app 2.x,详情文档见[官方文档](https://reactjs.org/blog/2018/10/01/create-react-app-v2.html) 70 | - 升级大部分第三方库,升级版本见[commit](https://github.com/yezihaohao/react-admin/commit/d8dc0ff0c6517c57a46d731adba69112a55145a9#diff-b9cfc7f2cdf78a7f4b91a753d10865a2) 71 | - 增加自定义主题功能 - 主题基础样式配置见[variables.less](https://github.com/yezihaohao/react-admin/blob/master/src/style/antd/variables.less) - 修改主题基础样式后执行`yarn theme 或 npm run theme`,默认主题即可生效 - 页面上可自定义主题颜色配置(根据此可添加字体大小等其他 antd 的默认样式) 72 | ![自定义主题](https://raw.githubusercontent.com/yezihaohao/react-admin/master/screenshots/themepicker.png) 73 | 74 | #### 2018-11-07 75 | 76 | - 完善 PWA 的 manifest.json 文件,增加按钮手动触发安装 PWA 应用 77 | - 最新版的 chrome 浏览器访问[ReactAdmin](https://admiring-dijkstra-34cb29.netlify.com/)即可体验 78 | 79 | ![PWA](https://raw.githubusercontent.com/yezihaohao/react-admin/master/screenshots/pwa.png) 80 | 81 | #### 2018-11-26 82 | 83 | - 增加问号形式的路由参数扩展 84 | 85 | #### 2018-12-28 86 | 87 | - 增加[react-document-title](https://github.com/gaearon/react-document-title)组件,根据路由设置页面 title 88 | 89 | #### 2019-03-20 90 | 91 | - 增加[redux-alita](https://github.com/yezihaohao/redux-alita),极简的 redux 工具用法,详情见其代码仓库 92 | 93 | #### 2019-05-10 94 | 95 | - 升级 react,react-dom,增加 hooks 支持(去掉 react-hot-loader,老版本 hot-loader 使用 hook 有点问题) 96 | - 增加菜单可拖拽 97 | 98 | ![截图](https://raw.githubusercontent.com/yezihaohao/react-admin/master/screenshots/menu_draggable.gif) 99 | 100 | #### 2019-09-04 101 | 102 | - 增加 Git 提交 message 规范约束工具[commitizen](https://github.com/commitizen/cz-cli) 103 | - Git 提交规范往往是团队编码必需,借助工具能形成更好的约束,如果你不喜欢用,可参照提交记录去掉[bd426fd](https://github.com/yezihaohao/react-admin/commit/a9401d191edd077bc3e59c8dbeeb61e5029cde95) 104 | 105 | #### 2019-09-26 106 | 107 | - 更新 create-react-app3.x 版本,升级部分依赖 lib,详情请查看提交记录(有问题请提 issue) 108 | 109 | #### 2019-10-26 110 | 111 | - 新增访客模式的路由配置+demo(主路由配置) 112 | - [在线 Demo](https://admiring-dijkstra-34cb29.netlify.com/#/app/extension/visitor) 113 | 114 | #### 2019-12-18 115 | 116 | - 新增多级菜单配置功能(菜单可配置成无限的树状菜单,菜单嵌套过多时,样式问题可能需要你调整) 117 | 118 | #### 2020-01-21 119 | 120 | - 新增服务端异步菜单功能 121 | 122 | #### 2020-08-02 123 | 124 | - 新增多环境配置方案,环境配置任你加,源码详情请查看[提交记录](https://github.com/yezihaohao/react-admin/commit/d2cb53dca7e7179c794dc9e699d057ed549aec62) 125 | - 根目录增加 .env.ra.xxx,其中 xxx 是 package.json 中运行的脚本命令第三个参数,请结合项目查看 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 yezihaohao 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 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | const REACT_ADMIN = process.env.REACT_ADMIN; 12 | if (!NODE_ENV) { 13 | throw new Error('The NODE_ENV environment variable is required but was not specified.'); 14 | } 15 | 16 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 17 | const dotenvFiles = [ 18 | `${paths.dotenv}.${NODE_ENV}.local`, 19 | `${paths.dotenv}.${NODE_ENV}`, 20 | `${paths.dotenv}.ra.${REACT_ADMIN}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach((dotenvFile) => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter((folder) => folder && !path.isAbsolute(folder)) 56 | .map((folder) => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | const REACT_ADMIN_REG = /^REACT_ADMIN_/i; 63 | 64 | function getClientEnvironment(publicUrl) { 65 | const raw = Object.keys(process.env) 66 | .filter((key) => REACT_APP.test(key) || REACT_ADMIN_REG.test(key)) 67 | .reduce( 68 | (env, key) => { 69 | env[key] = process.env[key]; 70 | return env; 71 | }, 72 | { 73 | // Useful for determining whether we’re running in production mode. 74 | // Most importantly, it switches React into the correct mode. 75 | NODE_ENV: process.env.NODE_ENV || 'development', 76 | // Useful for resolving the correct path to static assets in `public`. 77 | // For example, . 78 | // This should only be used as an escape hatch. Normally you would put 79 | // images into the `src` and `import` them in code to get their paths. 80 | PUBLIC_URL: publicUrl, 81 | } 82 | ); 83 | // Stringify all values so we can feed into Webpack DefinePlugin 84 | const stringified = { 85 | 'process.env': Object.keys(raw).reduce((env, key) => { 86 | env[key] = JSON.stringify(raw[key]); 87 | return env; 88 | }, {}), 89 | }; 90 | 91 | return { raw, stringified }; 92 | } 93 | 94 | module.exports = getClientEnvironment; 95 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | // We need to explicitly check for null and undefined (and not a falsy value) because 18 | // TypeScript treats an empty string as `.`. 19 | if (baseUrl == null) { 20 | // If there's no baseUrl set we respect NODE_PATH 21 | // Note that NODE_PATH is deprecated and will be removed 22 | // in the next major release of create-react-app. 23 | 24 | const nodePath = process.env.NODE_PATH || ''; 25 | return nodePath.split(path.delimiter).filter(Boolean); 26 | } 27 | 28 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 29 | 30 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 31 | // the default behavior. 32 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 33 | return null; 34 | } 35 | 36 | // Allow the user set the `baseUrl` to `appSrc`. 37 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 38 | return [paths.appSrc]; 39 | } 40 | 41 | // If the path is equal to the root directory we ignore it here. 42 | // We don't want to allow importing from the root directly as source files are 43 | // not transpiled outside of `src`. We do allow importing them with the 44 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 45 | // an alias. 46 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 47 | return null; 48 | } 49 | 50 | // Otherwise, throw an error. 51 | throw new Error( 52 | chalk.red.bold( 53 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 54 | ' Create React App does not support other values at this time.' 55 | ) 56 | ); 57 | } 58 | 59 | /** 60 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 61 | * 62 | * @param {*} options 63 | */ 64 | function getWebpackAliases(options = {}) { 65 | const baseUrl = options.baseUrl; 66 | 67 | if (!baseUrl) { 68 | return {}; 69 | } 70 | 71 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 72 | 73 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 74 | return { 75 | src: paths.appSrc, 76 | }; 77 | } 78 | } 79 | 80 | /** 81 | * Get jest aliases based on the baseUrl of a compilerOptions object. 82 | * 83 | * @param {*} options 84 | */ 85 | function getJestAliases(options = {}) { 86 | const baseUrl = options.baseUrl; 87 | 88 | if (!baseUrl) { 89 | return {}; 90 | } 91 | 92 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 93 | 94 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 95 | return { 96 | 'src/(.*)$': '/src/$1', 97 | }; 98 | } 99 | } 100 | 101 | function getModules() { 102 | // Check if TypeScript is setup 103 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 104 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 105 | 106 | if (hasTsConfig && hasJsConfig) { 107 | throw new Error( 108 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 109 | ); 110 | } 111 | 112 | let config; 113 | 114 | // If there's a tsconfig.json we assume it's a 115 | // TypeScript project and set up the config 116 | // based on tsconfig.json 117 | if (hasTsConfig) { 118 | const ts = require(resolve.sync('typescript', { 119 | basedir: paths.appNodeModules, 120 | })); 121 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 122 | // Otherwise we'll check if there is jsconfig.json 123 | // for non TS projects. 124 | } else if (hasJsConfig) { 125 | config = require(paths.appJsConfig); 126 | } 127 | 128 | config = config || {}; 129 | const options = config.compilerOptions || {}; 130 | 131 | const additionalModulePaths = getAdditionalModulePaths(options); 132 | 133 | return { 134 | additionalModulePaths: additionalModulePaths, 135 | webpackAliases: getWebpackAliases(options), 136 | jestAliases: getJestAliases(options), 137 | hasTsConfig, 138 | }; 139 | } 140 | 141 | module.exports = getModules(); 142 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 28 | 35 | 38 |
39 | 49 | 50 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/public/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactAdmin", 3 | "short_name": "ReactAdmin", 4 | "theme_color": "#313653", 5 | "background_color": "#313653", 6 | "display": "fullscreen", 7 | "start_url": ".", 8 | "icons": [ 9 | { 10 | "src": "images/icons/icon-72x72.png", 11 | "sizes": "72x72", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "images/icons/icon-96x96.png", 16 | "sizes": "96x96", 17 | "type": "image/png" 18 | }, 19 | { 20 | "src": "images/icons/icon-128x128.png", 21 | "sizes": "128x128", 22 | "type": "image/png" 23 | }, 24 | { 25 | "src": "images/icons/icon-144x144.png", 26 | "sizes": "144x144", 27 | "type": "image/png" 28 | }, 29 | { 30 | "src": "images/icons/icon-152x152.png", 31 | "sizes": "152x152", 32 | "type": "image/png" 33 | }, 34 | { 35 | "src": "images/icons/icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image/png" 38 | }, 39 | { 40 | "src": "images/icons/icon-384x384.png", 41 | "sizes": "384x384", 42 | "type": "image/png" 43 | }, 44 | { 45 | "src": "images/icons/icon-512x512.png", 46 | "sizes": "512x512", 47 | "type": "image/png" 48 | } 49 | ], 50 | "splash_pages": null 51 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /screenshots/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/screenshots/logo.png -------------------------------------------------------------------------------- /screenshots/menu_draggable.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/screenshots/menu_draggable.gif -------------------------------------------------------------------------------- /screenshots/pwa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/screenshots/pwa.png -------------------------------------------------------------------------------- /screenshots/themepicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/screenshots/themepicker.png -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | process.env.REACT_ADMIN = process.argv.slice(2)[0]; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', (err) => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const paths = require('../config/paths'); 32 | const configFactory = require('../config/webpack.config'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log(`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`); 59 | console.log(); 60 | } 61 | 62 | // We require that you explicitly set browsers and do not fall back to 63 | // browserslist defaults. 64 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 65 | checkBrowsers(paths.appPath, isInteractive) 66 | .then(() => { 67 | // We attempt to use the default port but if it is busy, we offer the user to 68 | // run on a different port. `choosePort()` Promise resolves to the next free port. 69 | return choosePort(HOST, DEFAULT_PORT); 70 | }) 71 | .then((port) => { 72 | if (port == null) { 73 | // We have not found a port. 74 | return; 75 | } 76 | const config = configFactory('development'); 77 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 78 | const appName = require(paths.appPackageJson).name; 79 | const useTypeScript = fs.existsSync(paths.appTsConfig); 80 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; 81 | const urls = prepareUrls(protocol, HOST, port); 82 | const devSocket = { 83 | warnings: (warnings) => devServer.sockWrite(devServer.sockets, 'warnings', warnings), 84 | errors: (errors) => devServer.sockWrite(devServer.sockets, 'errors', errors), 85 | }; 86 | // Create a webpack compiler that is configured with custom messages. 87 | const compiler = createCompiler({ 88 | appName, 89 | config, 90 | devSocket, 91 | urls, 92 | useYarn, 93 | useTypeScript, 94 | tscCompileOnError, 95 | webpack, 96 | }); 97 | // Load proxy config 98 | const proxySetting = require(paths.appPackageJson).proxy; 99 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 100 | // Serve webpack assets generated by the compiler over a web server. 101 | const serverConfig = createDevServerConfig(proxyConfig, urls.lanUrlForConfig); 102 | const devServer = new WebpackDevServer(compiler, serverConfig); 103 | // Launch WebpackDevServer. 104 | devServer.listen(port, HOST, (err) => { 105 | if (err) { 106 | return console.log(err); 107 | } 108 | if (isInteractive) { 109 | clearConsole(); 110 | } 111 | 112 | // We used to support resolving modules according to `NODE_PATH`. 113 | // This now has been deprecated in favor of jsconfig/tsconfig.json 114 | // This lets you use absolute paths in imports inside large monorepos: 115 | if (process.env.NODE_PATH) { 116 | console.log( 117 | chalk.yellow( 118 | 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' 119 | ) 120 | ); 121 | console.log(); 122 | } 123 | 124 | console.log(chalk.cyan('Starting the development server...\n')); 125 | openBrowser(urls.localUrlForBrowser); 126 | }); 127 | 128 | ['SIGINT', 'SIGTERM'].forEach(function (sig) { 129 | process.on(sig, function () { 130 | devServer.close(); 131 | process.exit(); 132 | }); 133 | }); 134 | }) 135 | .catch((err) => { 136 | if (err && err.message) { 137 | console.log(err.message); 138 | } 139 | process.exit(1); 140 | }); 141 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Layout, notification } from 'antd'; 3 | import umbrella from 'umbrella-storage'; 4 | import { useAlita } from 'redux-alita'; 5 | import Routes from './routes'; 6 | import SiderCustom from './components/SiderCustom'; 7 | import HeaderCustom from './components/HeaderCustom'; 8 | import { ThemePicker, Copyright } from './components/widget'; 9 | import { checkLogin } from './utils'; 10 | import { fetchMenu } from './service'; 11 | import classNames from 'classnames'; 12 | import { SmileOutlined } from '@ant-design/icons'; 13 | 14 | const { Content, Footer } = Layout; 15 | 16 | type AppProps = {}; 17 | 18 | function checkIsMobile() { 19 | const clientWidth = window.innerWidth; 20 | return clientWidth <= 992; 21 | } 22 | 23 | let _resizeThrottled = false; 24 | function resizeListener(handler: (isMobile: boolean) => void) { 25 | const delay = 250; 26 | if (!_resizeThrottled) { 27 | _resizeThrottled = true; 28 | const timer = setTimeout(() => { 29 | handler(checkIsMobile()); 30 | _resizeThrottled = false; 31 | clearTimeout(timer); 32 | }, delay); 33 | } 34 | } 35 | function handleResize(handler: (isMobile: boolean) => void) { 36 | window.addEventListener('resize', resizeListener.bind(null, handler)); 37 | } 38 | 39 | function openFNotification() { 40 | const openNotification = () => { 41 | notification.open({ 42 | message: '博主-yezihaohao', 43 | description: ( 44 |
45 |

46 | GitHub地址: 47 | 52 | https://github.com/yezihaohao 53 | 54 |

55 |

56 | 博客地址: 57 | 62 | https://yezihaohao.github.io/ 63 | 64 |

65 |
66 | ), 67 | icon: , 68 | duration: 0, 69 | }); 70 | umbrella.setLocalStorage('hideBlog', true); 71 | }; 72 | const storageFirst = umbrella.getLocalStorage('hideBlog'); 73 | if (!storageFirst) { 74 | openNotification(); 75 | } 76 | } 77 | 78 | /** 79 | * 获取服务端异步菜单 80 | * @param handler 执行回调 81 | */ 82 | function fetchSmenu(handler: any) { 83 | const setAlitaMenu = (menus: any) => { 84 | handler(menus); 85 | // this.props.setAlitaState({ stateName: 'smenus', data: menus }); 86 | }; 87 | setAlitaMenu(umbrella.getLocalStorage('smenus') || []); 88 | fetchMenu().then((smenus) => { 89 | setAlitaMenu(smenus); 90 | umbrella.setLocalStorage('smenus', smenus); 91 | }); 92 | } 93 | 94 | const App = (props: AppProps) => { 95 | const [collapsed, setCollapsed] = useState(false); 96 | const [auth, responsive, setAlita] = useAlita( 97 | { auth: { permissions: null } }, 98 | { responsive: { isMobile: false } }, 99 | { light: true } 100 | ); 101 | 102 | useEffect(() => { 103 | let user = umbrella.getLocalStorage('user'); 104 | user && setAlita('auth', user); 105 | setAlita('responsive', { isMobile: checkIsMobile() }); 106 | 107 | handleResize((isMobile: boolean) => setAlita('responsive', { isMobile })); 108 | openFNotification(); 109 | fetchSmenu((smenus: any[]) => setAlita('smenus', smenus)); 110 | }, [setAlita]); 111 | 112 | function toggle() { 113 | setCollapsed(!collapsed); 114 | } 115 | return ( 116 | 117 | {!responsive.isMobile && checkLogin(auth.permissions) && ( 118 | 119 | )} 120 | 121 | 124 | 125 | 126 | 127 | 128 |
129 | 130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default App; 137 | -------------------------------------------------------------------------------- /src/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; 3 | import NotFound from './components/pages/NotFound'; 4 | import Login from './components/pages/Login'; 5 | import App from './App'; 6 | 7 | export default () => ( 8 | 9 | 10 | } /> 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/HeaderCustom.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/13. 3 | */ 4 | import React, { useEffect, useState } from 'react'; 5 | import screenfull from 'screenfull'; 6 | import avater from '../style/imgs/b1.jpg'; 7 | import SiderCustom from './SiderCustom'; 8 | import { Menu, Layout, Badge, Popover } from 'antd'; 9 | import { gitOauthToken, gitOauthInfo } from '../service'; 10 | import { parseQuery } from '../utils'; 11 | import { useHistory } from 'react-router-dom'; 12 | import { PwaInstaller } from './widget'; 13 | import { useAlita } from 'redux-alita'; 14 | import umbrella from 'umbrella-storage'; 15 | import { useSwitch } from '../utils/hooks'; 16 | import { 17 | ArrowsAltOutlined, 18 | BarsOutlined, 19 | MenuFoldOutlined, 20 | MenuUnfoldOutlined, 21 | NotificationOutlined, 22 | } from '@ant-design/icons'; 23 | const { Header } = Layout; 24 | const SubMenu = Menu.SubMenu; 25 | const MenuItemGroup = Menu.ItemGroup; 26 | 27 | type HeaderCustomProps = { 28 | toggle: () => void; 29 | collapsed: boolean; 30 | user: any; 31 | responsive?: any; 32 | path?: string; 33 | }; 34 | 35 | const HeaderCustom = (props: HeaderCustomProps) => { 36 | const [user, setUser] = useState(); 37 | const [responsive] = useAlita('responsive', { light: true }); 38 | const [visible, turn] = useSwitch(); 39 | const history = useHistory(); 40 | 41 | useEffect(() => { 42 | const query = parseQuery(); 43 | let storageUser = umbrella.getLocalStorage('user'); 44 | 45 | if (!storageUser && query.code) { 46 | gitOauthToken(query.code as string).then((res: any) => { 47 | gitOauthInfo(res.access_token).then((info: any) => { 48 | setUser({ 49 | user: info, 50 | }); 51 | umbrella.setLocalStorage('user', info); 52 | }); 53 | }); 54 | } else { 55 | setUser({ 56 | user: storageUser, 57 | }); 58 | } 59 | }, []); 60 | 61 | const screenFull = () => { 62 | if (screenfull.isEnabled) { 63 | screenfull.toggle(); 64 | } 65 | }; 66 | const menuClick = (e: any) => { 67 | e.key === 'logout' && logout(); 68 | }; 69 | const logout = () => { 70 | umbrella.removeLocalStorage('user'); 71 | history.push('/login'); 72 | }; 73 | return ( 74 |
75 | {responsive?.isMobile ? ( 76 | } 78 | trigger="click" 79 | placement="bottomLeft" 80 | visible={visible} 81 | onVisibleChange={(visible) => (visible ? turn.turnOn() : turn.turnOff())} 82 | > 83 | 84 | 85 | ) : props.collapsed ? ( 86 | 90 | ) : ( 91 | 95 | )} 96 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 115 | 头像 116 | 117 | 118 | } 119 | > 120 | 121 | 你好 - {user?.userName} 122 | 个人信息 123 | 124 | 退出登录 125 | 126 | 127 | 128 | 个人设置 129 | 系统设置 130 | 131 | 132 | 133 |
134 | ); 135 | }; 136 | 137 | export default HeaderCustom; 138 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React, { ReactNode } from 'react'; 5 | 6 | interface PageProps { 7 | children?: ReactNode; 8 | } 9 | 10 | const Page = (props: PageProps) => { 11 | return
{props.children}
; 12 | }; 13 | 14 | export default Page; 15 | -------------------------------------------------------------------------------- /src/components/SiderCustom.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/13. 3 | */ 4 | import React, { useState, useEffect } from 'react'; 5 | import { Layout } from 'antd'; 6 | import { withRouter, RouteComponentProps } from 'react-router-dom'; 7 | import routes from '../routes/config'; 8 | import SiderMenu from './SiderMenu'; 9 | import { useAlita } from 'redux-alita'; 10 | import { useSwitch } from '../utils/hooks'; 11 | import { usePrevious } from 'ahooks'; 12 | const { Sider } = Layout; 13 | 14 | type SiderCustomProps = RouteComponentProps & { 15 | popoverHide?: () => void; 16 | collapsed?: boolean; 17 | smenus?: any; 18 | }; 19 | interface IMenu { 20 | openKeys: string[]; 21 | selectedKey: string; 22 | } 23 | 24 | const SiderCustom = (props: SiderCustomProps) => { 25 | const [collapsed, tCollapsed] = useSwitch(); 26 | const [firstHide, tFirstHide] = useSwitch(); 27 | const [menu, setMenu] = useState({ openKeys: [''], selectedKey: '' }); 28 | // 异步菜单 29 | const [smenus] = useAlita({ smenus: [] }, { light: true }); 30 | const { location, collapsed: pCollapsed } = props; 31 | const prePathname = usePrevious(props.location.pathname); 32 | 33 | useEffect(() => { 34 | const recombineOpenKeys = (openKeys: string[]) => { 35 | let i = 0; 36 | let strPlus = ''; 37 | let tempKeys: string[] = []; 38 | // 多级菜单循环处理 39 | while (i < openKeys.length) { 40 | strPlus += openKeys[i]; 41 | tempKeys.push(strPlus); 42 | i++; 43 | } 44 | return tempKeys; 45 | }; 46 | const getOpenAndSelectKeys = () => { 47 | return { 48 | openKeys: recombineOpenKeys(location.pathname.match(/[/](\w+)/gi) || []), 49 | selectedKey: location.pathname, 50 | }; 51 | }; 52 | 53 | if (pCollapsed !== collapsed) { 54 | setMenu(getOpenAndSelectKeys()); 55 | tCollapsed.setSwitcher(!!pCollapsed); 56 | tFirstHide.setSwitcher(!!pCollapsed); 57 | } 58 | 59 | if (prePathname !== location.pathname) { 60 | setMenu(getOpenAndSelectKeys()); 61 | } 62 | }, [prePathname, location.pathname, collapsed, tFirstHide, tCollapsed, pCollapsed]); 63 | 64 | const menuClick = (e: any) => { 65 | setMenu((state) => ({ ...state, selectedKey: e.key })); 66 | props.popoverHide?.(); // 响应式布局控制小屏幕点击菜单时隐藏菜单操作 67 | }; 68 | 69 | const openMenu: any = (v: string[]) => { 70 | setMenu((state) => ({ ...state, openKeys: v })); 71 | tFirstHide.turnOff(); 72 | }; 73 | 74 | return ( 75 | 82 |
83 | 91 | 99 | 100 | ); 101 | }; 102 | 103 | export default withRouter(SiderCustom); 104 | -------------------------------------------------------------------------------- /src/components/SiderMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Menu } from 'antd'; 3 | import { Link } from 'react-router-dom'; 4 | import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; 5 | import { IFMenu } from '../routes/config'; 6 | import { MenuProps } from 'antd/lib/menu'; 7 | 8 | const renderMenuItem = ( 9 | item: IFMenu // item.route 菜单单独跳转的路由 10 | ) => ( 11 | 12 | 13 | {/* {item.icon && } */} 14 | {item.title} 15 | 16 | 17 | ); 18 | 19 | const renderSubMenu = (item: IFMenu) => { 20 | return ( 21 | 25 | {/* {item.icon && } */} 26 | {item.title} 27 | 28 | } 29 | > 30 | {item.subs!.map((sub) => (sub.subs ? renderSubMenu(sub) : renderMenuItem(sub)))} 31 | 32 | ); 33 | }; 34 | 35 | type SiderMenuProps = MenuProps & { 36 | menus: any; 37 | onClick: (e: any) => void; 38 | selectedKeys: string[]; 39 | openKeys?: string[]; 40 | onOpenChange: (v: string[]) => void; 41 | }; 42 | 43 | const SiderMenu = ({ menus, ...props }: SiderMenuProps) => { 44 | const [dragItems, setDragItems] = useState([]); 45 | 46 | useEffect(() => { 47 | setDragItems(menus); 48 | }, [menus]); 49 | 50 | const reorder = (list: any, startIndex: number, endIndex: number) => { 51 | const result = Array.from(list); 52 | const [removed] = result.splice(startIndex, 1); 53 | result.splice(endIndex, 0, removed); 54 | return result; 55 | }; 56 | const onDragEnd = (result: any) => { 57 | // dropped outside the list 58 | if (!result.destination) { 59 | return; 60 | } 61 | 62 | const _items = reorder(dragItems, result.source.index, result.destination.index); 63 | setDragItems(_items); 64 | }; 65 | return ( 66 | 67 | 68 | {(provided, snapshot) => ( 69 |
70 | {dragItems.map((item: IFMenu, index: number) => ( 71 | 72 | {(provided, snapshot) => ( 73 |
) => 78 | provided.dragHandleProps && 79 | provided.dragHandleProps.onDragStart(e as any) 80 | } 81 | > 82 | 83 | {item.subs! 84 | ? renderSubMenu(item) 85 | : renderMenuItem(item)} 86 | 87 |
88 | )} 89 |
90 | ))} 91 | {provided.placeholder} 92 |
93 | )} 94 |
95 |
96 | ); 97 | }; 98 | 99 | export default React.memo(SiderMenu); 100 | -------------------------------------------------------------------------------- /src/components/animation/BasicAnimations.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/5/8. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card, Switch } from 'antd'; 6 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 7 | 8 | class BasicAnimations extends React.Component { 9 | state = { 10 | animated: false, 11 | animatedOne: -1, 12 | }; 13 | animatedAll = (checked: boolean) => { 14 | checked && this.setState({ animated: true }); 15 | !checked && this.setState({ animated: false }); 16 | }; 17 | animatedOne = (i: number) => { 18 | this.setState({ animatedOne: i }); 19 | }; 20 | animatedOneOver = () => { 21 | this.setState({ animatedOne: -1 }); 22 | }; 23 | render() { 24 | const animations = [ 25 | 'bounce', 26 | 'flash', 27 | 'rubberBand', 28 | 'shake', 29 | 'headShake', 30 | 'swing', 31 | 'tada', 32 | 'wobble', 33 | 'jello', 34 | 'bounceIn', 35 | 'bounceInDown', 36 | 'bounceInLeft', 37 | 'bounceInRight', 38 | 'bounceOut', 39 | 'bounceOutDown', 40 | 'bounceOutLeft', 41 | 'bounceOutLeft', 42 | 'bounceOutUp', 43 | 'fadeIn', 44 | 'fadeInDown', 45 | 'fadeInDownBig', 46 | 'fadeInLeft', 47 | 'fadeInLeftBig', 48 | 'fadeInRight', 49 | 'fadeInRightBig', 50 | 'fadeInUp', 51 | 'fadeInUpBig', 52 | 'fadeOut', 53 | 'fadeOutDown', 54 | 'fadeOutDownBig', 55 | 'fadeOutLeft', 56 | 'fadeOutLeftBig', 57 | 'fadeOutRight', 58 | 'fadeOutRightBig', 59 | 'fadeOutUp', 60 | 'fadeOutUpBig', 61 | 'flipInX', 62 | 'flipInY', 63 | 'flipOutX', 64 | 'flipOutY', 65 | 'lightSpeedIn', 66 | 'lightSpeedOut', 67 | 'rotateIn', 68 | 'rotateInDownLeft', 69 | 'rotateInDownRight', 70 | 'rotateInUpLeft', 71 | 'rotateInUpRight', 72 | 'rotateOut', 73 | 'rotateOutDownLeft', 74 | 'rotateOutDownRight', 75 | 'rotateOutUpLeft', 76 | 'rotateOutUpRight', 77 | 'hinge', 78 | 'jackInTheBox', 79 | 'rollIn', 80 | 'rollOut', 81 | 'zoomIn', 82 | 'zoomInDown', 83 | 'zoomInLeft', 84 | 'zoomInRight', 85 | 'zoomInUp', 86 | 'zoomOut', 87 | 'zoomOutDown', 88 | 'zoomOutLeft', 89 | 'zoomOutRight', 90 | 'zoomOutUp', 91 | 'slideInDown', 92 | 'slideInLeft', 93 | 'slideInRight', 94 | 'slideInUp', 95 | 'slideOutDown', 96 | 'slideOutLeft', 97 | 'slideOutRight', 98 | 'slideOutUp', 99 | ]; 100 | return ( 101 |
102 | 103 | 104 | 全部动画(单个动画请移动鼠标) 105 | 106 | 107 | 108 | {animations.map((v, i) => ( 109 | 110 |
111 | this.animatedOne(i)} 122 | onMouseLeave={() => this.animatedOneOver()} 123 | > 124 |
125 |

{v}

126 |
127 |
128 |
129 | 130 | ))} 131 |
132 |
133 | ); 134 | } 135 | } 136 | 137 | export default BasicAnimations; 138 | -------------------------------------------------------------------------------- /src/components/animation/ExampleAnimations.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/5/8. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card, Table, Popconfirm, Button } from 'antd'; 6 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 7 | 8 | type ExampleAnimationsProps = {}; 9 | type ExampleAnimationsState = { 10 | dataSource: any; 11 | count: number; 12 | deleteIndex: number; 13 | }; 14 | 15 | class ExampleAnimations extends React.Component { 16 | constructor(props: any) { 17 | super(props); 18 | this.columns = [ 19 | { 20 | title: 'name', 21 | dataIndex: 'name', 22 | width: '30%', 23 | }, 24 | { 25 | title: 'age', 26 | dataIndex: 'age', 27 | }, 28 | { 29 | title: 'address', 30 | dataIndex: 'address', 31 | }, 32 | { 33 | title: 'operation', 34 | dataIndex: 'operation', 35 | render: (text: any, record: any, index: number) => { 36 | return this.state.dataSource.length > 1 ? ( 37 | this.onDelete(record, index)} 40 | > 41 | Delete 42 | 43 | ) : null; 44 | }, 45 | }, 46 | ]; 47 | this.state = { 48 | dataSource: [ 49 | { 50 | key: '0', 51 | name: 'Edward King 0', 52 | age: '32', 53 | address: 'London, Park Lane no. 0', 54 | }, 55 | { 56 | key: '1', 57 | name: 'Edward King 1', 58 | age: '32', 59 | address: 'London, Park Lane no. 1', 60 | }, 61 | ], 62 | count: 2, 63 | deleteIndex: -1, 64 | }; 65 | } 66 | columns: any; 67 | onDelete = (record: any, index: number) => { 68 | const dataSource = [...this.state.dataSource]; 69 | dataSource.splice(index, 1); 70 | this.setState({ deleteIndex: record.key }); 71 | setTimeout(() => { 72 | this.setState({ dataSource }); 73 | }, 500); 74 | }; 75 | handleAdd = () => { 76 | const { count, dataSource } = this.state; 77 | const newData = { 78 | key: count, 79 | name: `Edward King ${count}`, 80 | age: 32, 81 | address: `London, Park Lane no. ${count}`, 82 | }; 83 | this.setState({ 84 | dataSource: [newData, ...dataSource], 85 | count: count + 1, 86 | }); 87 | }; 88 | render() { 89 | const { dataSource } = this.state; 90 | const columns = this.columns; 91 | return ( 92 |
93 | 94 | 95 | 96 |
97 | 98 | 101 | { 106 | if (this.state.deleteIndex === record.key) 107 | return 'animated zoomOutLeft min-black'; 108 | return 'animated fadeInRight'; 109 | }} 110 | /> 111 | 112 | 113 | 114 | 115 | 116 | ); 117 | } 118 | } 119 | 120 | export default ExampleAnimations; 121 | -------------------------------------------------------------------------------- /src/components/auth/Basic.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 叶子 on 2017/7/31. 3 | */ 4 | import React, { Component } from 'react'; 5 | import { Row, Col, Card } from 'antd'; 6 | import beauty from '@/style/imgs/beauty.jpg'; 7 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 8 | import { AuthWidget } from '../widget'; 9 | 10 | class Basic extends Component { 11 | render() { 12 | return ( 13 |
14 | 15 | ( 17 | 18 |
19 | 20 | {!auth.uid && ( 21 |

22 | 登录之后你将看到一张美女图 23 |

24 | )} 25 | {auth.permissions && 26 | auth.permissions.includes('auth/authPage/visit') && ( 27 |
28 | 29 | {(auth.permissions.includes( 30 | 'auth/authPage/edit' 31 | ) && ( 32 |
33 |

34 | 看啥子美女,看点美景就行啦~ 35 | 40 | 😄😄 41 | 42 |

43 |

管理员身份登录才能看到这两段话

44 |
45 | )) || ( 46 |
47 |

管理员登录将看到不一样的效果

48 |
49 | )} 50 |
51 | )} 52 |
53 | 54 | 55 | )} 56 | /> 57 | 58 | ); 59 | } 60 | } 61 | 62 | export default Basic; 63 | -------------------------------------------------------------------------------- /src/components/auth/RouterEnter.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 叶子 on 2017/8/1. 3 | */ 4 | /** 5 | * Created by 叶子 on 2017/7/31. 6 | */ 7 | import React, { Component } from 'react'; 8 | import { Row, Col, Card } from 'antd'; 9 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 10 | import { AuthWidget } from '../widget'; 11 | 12 | class RouterEnter extends Component { 13 | componentDidMount() { 14 | console.log('RouterEnter'); 15 | } 16 | render() { 17 | return ( 18 |
19 | 20 | ( 22 | 23 |
24 | 25 |

26 | 只有管理员登录才能看到该页面,否则跳转到404页面 27 |

28 |
29 | 30 | 31 | )} 32 | /> 33 | 34 | ); 35 | } 36 | } 37 | 38 | export default RouterEnter; 39 | -------------------------------------------------------------------------------- /src/components/charts/Echarts.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/17. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card } from 'antd'; 6 | import EchartsArea from './EchartsArea'; 7 | import EchartsPie from './EchartsPie'; 8 | import EchartsEffectScatter from './EchartsEffectScatter'; 9 | import EchartsForce from './EchartsForce'; 10 | 11 | class Echarts extends React.Component { 12 | render() { 13 | return ( 14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | {/**/} 29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | ) 52 | } 53 | } 54 | 55 | export default Echarts; -------------------------------------------------------------------------------- /src/components/charts/EchartsArea.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/17. 3 | */ 4 | import React from 'react'; 5 | import ReactEcharts from 'echarts-for-react'; 6 | import echarts from 'echarts'; 7 | 8 | let base = +new Date(1968, 9, 3); 9 | let oneDay = 24 * 3600 * 1000; 10 | let date = []; 11 | 12 | let data = [Math.random() * 300]; 13 | 14 | for (var i = 1; i < 20000; i++) { 15 | var now = new Date((base += oneDay)); 16 | date.push([now.getFullYear(), now.getMonth() + 1, now.getDate()].join('/')); 17 | data.push(Math.round((Math.random() - 0.5) * 20 + data[i - 1])); 18 | } 19 | 20 | const option = { 21 | tooltip: { 22 | trigger: 'axis', 23 | position: function(pt: any) { 24 | return [pt[0], '10%']; 25 | }, 26 | }, 27 | title: { 28 | left: 'center', 29 | text: '大数据量面积图', 30 | }, 31 | toolbox: { 32 | feature: { 33 | dataZoom: { 34 | yAxisIndex: 'none', 35 | }, 36 | restore: {}, 37 | saveAsImage: {}, 38 | }, 39 | }, 40 | xAxis: { 41 | type: 'category', 42 | boundaryGap: false, 43 | data: date, 44 | }, 45 | yAxis: { 46 | type: 'value', 47 | boundaryGap: [0, '100%'], 48 | }, 49 | dataZoom: [ 50 | { 51 | type: 'inside', 52 | start: 0, 53 | end: 10, 54 | }, 55 | { 56 | start: 0, 57 | end: 10, 58 | handleIcon: 59 | 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z', 60 | handleSize: '80%', 61 | handleStyle: { 62 | color: '#fff', 63 | shadowBlur: 3, 64 | shadowColor: 'rgba(0, 0, 0, 0.6)', 65 | shadowOffsetX: 2, 66 | shadowOffsetY: 2, 67 | }, 68 | }, 69 | ], 70 | series: [ 71 | { 72 | name: '模拟数据', 73 | type: 'line', 74 | smooth: true, 75 | symbol: 'none', 76 | sampling: 'average', 77 | itemStyle: { 78 | normal: { 79 | color: 'rgb(255, 70, 131)', 80 | }, 81 | }, 82 | areaStyle: { 83 | normal: { 84 | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ 85 | { 86 | offset: 0, 87 | color: 'rgb(255, 158, 68)', 88 | }, 89 | { 90 | offset: 1, 91 | color: 'rgb(255, 70, 131)', 92 | }, 93 | ]), 94 | }, 95 | }, 96 | data: data, 97 | }, 98 | ], 99 | }; 100 | 101 | class EchartsArea extends React.Component { 102 | render() { 103 | return ( 104 | 109 | ); 110 | } 111 | } 112 | 113 | export default EchartsArea; 114 | -------------------------------------------------------------------------------- /src/components/charts/EchartsGraphnpm.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/21. 3 | */ 4 | import React from 'react'; 5 | import ReactEcharts from 'echarts-for-react'; 6 | import { npmDependencies } from '../../service'; 7 | 8 | class EchartsGraphnpm extends React.Component { 9 | state = { 10 | option: { 11 | title: { 12 | text: 'NPM Dependencies', 13 | }, 14 | animationDurationUpdate: 1500, 15 | animationEasingUpdate: 'quinticInOut', 16 | series: [ 17 | { 18 | type: 'graph', 19 | layout: 'none', 20 | // progressiveThreshold: 700, 21 | data: [], 22 | edges: [], 23 | label: { 24 | emphasis: { 25 | position: 'right', 26 | show: true, 27 | }, 28 | }, 29 | roam: true, 30 | focusNodeAdjacency: true, 31 | lineStyle: { 32 | normal: { 33 | width: 0.5, 34 | curveness: 0.3, 35 | opacity: 0.7, 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | }; 42 | componentDidMount() { 43 | npmDependencies().then((npm) => { 44 | this.setState({ 45 | option: { 46 | series: [ 47 | { 48 | data: npm.nodes.map(function (node: any) { 49 | return { 50 | x: node.x, 51 | y: node.y, 52 | id: node.id, 53 | name: node.label, 54 | symbolSize: node.size, 55 | itemStyle: { 56 | normal: { 57 | color: node.color, 58 | }, 59 | }, 60 | }; 61 | }), 62 | edges: npm.edges.map(function (edge: any) { 63 | return { 64 | source: edge.sourceID, 65 | target: edge.targetID, 66 | }; 67 | }), 68 | }, 69 | ], 70 | }, 71 | }); 72 | }); 73 | } 74 | render() { 75 | return ( 76 | 81 | ); 82 | } 83 | } 84 | 85 | export default EchartsGraphnpm; 86 | -------------------------------------------------------------------------------- /src/components/charts/EchartsPie.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/21. 3 | */ 4 | import React from 'react'; 5 | import ReactEcharts from 'echarts-for-react'; 6 | 7 | const option = { 8 | title: { 9 | text: 'Customized Pie', 10 | left: 'center', 11 | top: 20, 12 | textStyle: { 13 | color: '#777', 14 | }, 15 | }, 16 | 17 | tooltip: { 18 | trigger: 'item', 19 | formatter: '{a}
{b} : {c} ({d}%)', 20 | }, 21 | 22 | visualMap: { 23 | show: false, 24 | min: 80, 25 | max: 600, 26 | inRange: { 27 | colorLightness: [0, 1], 28 | }, 29 | }, 30 | series: [ 31 | { 32 | name: '访问来源', 33 | type: 'pie', 34 | radius: '55%', 35 | center: ['50%', '50%'], 36 | data: [ 37 | { value: 335, name: '直接访问' }, 38 | { value: 310, name: '邮件营销' }, 39 | { value: 274, name: '联盟广告' }, 40 | { value: 235, name: '视频广告' }, 41 | { value: 400, name: '搜索引擎' }, 42 | ].sort(function(a, b) { 43 | return a.value - b.value; 44 | }), 45 | roseType: 'angle', 46 | label: { 47 | normal: { 48 | textStyle: { 49 | color: '#777', 50 | }, 51 | }, 52 | }, 53 | labelLine: { 54 | normal: { 55 | lineStyle: { 56 | color: '#777', 57 | }, 58 | smooth: 0.2, 59 | length: 10, 60 | length2: 20, 61 | }, 62 | }, 63 | itemStyle: { 64 | normal: { 65 | color: '#c23531', 66 | shadowBlur: 200, 67 | shadowColor: '#777', 68 | }, 69 | }, 70 | 71 | animationType: 'scale', 72 | animationEasing: 'elasticOut', 73 | animationDelay: function(idx: any) { 74 | return Math.random() * 200; 75 | }, 76 | }, 77 | ], 78 | }; 79 | 80 | const EchartsPie = () => ( 81 | 86 | ); 87 | 88 | export default EchartsPie; 89 | -------------------------------------------------------------------------------- /src/components/charts/EchartsScatter.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/21. 3 | */ 4 | import React from 'react'; 5 | import ReactEcharts from 'echarts-for-react'; 6 | import { weibo } from '../../service'; 7 | require('echarts/map/js/china.js'); 8 | 9 | class EchartsScatter extends React.Component { 10 | state = { 11 | option: { 12 | backgroundColor: '#404a59', 13 | title: { 14 | text: '微博签到数据点亮中国', 15 | subtext: 'From ThinkGIS', 16 | sublink: 'http://www.thinkgis.cn/public/sina', 17 | left: 'center', 18 | top: 'top', 19 | textStyle: { 20 | color: '#fff', 21 | }, 22 | }, 23 | tooltip: {}, 24 | legend: { 25 | left: 'left', 26 | data: ['强', '中', '弱'], 27 | textStyle: { 28 | color: '#ccc', 29 | }, 30 | }, 31 | geo: { 32 | map: 'china', 33 | label: { 34 | emphasis: { 35 | show: false, 36 | }, 37 | }, 38 | itemStyle: { 39 | normal: { 40 | areaColor: '#323c48', 41 | borderColor: '#111', 42 | }, 43 | emphasis: { 44 | areaColor: '#2a333d', 45 | }, 46 | }, 47 | }, 48 | series: [ 49 | { 50 | name: '弱', 51 | type: 'scatter', 52 | coordinateSystem: 'geo', 53 | symbolSize: 1, 54 | large: true, 55 | itemStyle: { 56 | normal: { 57 | shadowBlur: 2, 58 | shadowColor: 'rgba(37, 140, 249, 0.8)', 59 | color: 'rgba(37, 140, 249, 0.8)', 60 | }, 61 | }, 62 | data: [], 63 | }, 64 | { 65 | name: '中', 66 | type: 'scatter', 67 | coordinateSystem: 'geo', 68 | symbolSize: 1, 69 | large: true, 70 | itemStyle: { 71 | normal: { 72 | shadowBlur: 2, 73 | shadowColor: 'rgba(14, 241, 242, 0.8)', 74 | color: 'rgba(14, 241, 242, 0.8)', 75 | }, 76 | }, 77 | data: [], 78 | }, 79 | { 80 | name: '强', 81 | type: 'scatter', 82 | coordinateSystem: 'geo', 83 | symbolSize: 1, 84 | large: true, 85 | itemStyle: { 86 | normal: { 87 | shadowBlur: 2, 88 | shadowColor: 'rgba(255, 255, 255, 0.8)', 89 | color: 'rgba(255, 255, 255, 0.8)', 90 | }, 91 | }, 92 | data: [], 93 | }, 94 | ], 95 | }, 96 | }; 97 | componentDidMount() { 98 | weibo().then((weiboData) => { 99 | weiboData = weiboData.map(function (serieData: any) { 100 | var px = serieData[0] / 1000; 101 | var py = serieData[1] / 1000; 102 | var res = [[px, py]]; 103 | 104 | for (var i = 2; i < serieData.length; i += 2) { 105 | var dx = serieData[i] / 1000; 106 | var dy = serieData[i + 1] / 1000; 107 | var x = px + dx; 108 | var y = py + dy; 109 | res.push([parseInt(x.toFixed(2), 10), parseInt(y.toFixed(2), 10), 1]); 110 | 111 | px = x; 112 | py = y; 113 | } 114 | return res; 115 | }); 116 | this.setState({ 117 | option: { 118 | series: [ 119 | { data: weiboData[0] }, 120 | { data: weiboData[1] }, 121 | { data: weiboData[2] }, 122 | ], 123 | }, 124 | }); 125 | }); 126 | } 127 | render() { 128 | return ( 129 | 134 | ); 135 | } 136 | } 137 | 138 | export default EchartsScatter; 139 | -------------------------------------------------------------------------------- /src/components/charts/Recharts.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/21. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card } from 'antd'; 6 | import RechartsSimpleLineChart from './RechartsSimpleLineChart'; 7 | import RechartsBarChart from './RechartsBarChart'; 8 | import RechartsRadialBarChart from './RechartsRadialBarChart'; 9 | import RechartsRadarChart from './RechartsRadarChart'; 10 | 11 | class Recharts extends React.Component { 12 | render() { 13 | return ( 14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 |
40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | ) 51 | } 52 | } 53 | 54 | export default Recharts; -------------------------------------------------------------------------------- /src/components/charts/RechartsBarChart.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/21. 3 | */ 4 | import React from 'react'; 5 | import {BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer} from 'recharts'; 6 | 7 | const data = [ 8 | {name: 'Page A', uv: 4000, pv: 2400, amt: 2400}, 9 | {name: 'Page B', uv: 3000, pv: 1398, amt: 2210}, 10 | {name: 'Page C', uv: 2000, pv: 9800, amt: 2290}, 11 | {name: 'Page D', uv: 2780, pv: 3908, amt: 2000}, 12 | {name: 'Page E', uv: 1890, pv: 4800, amt: 2181}, 13 | {name: 'Page F', uv: 2390, pv: 3800, amt: 2500}, 14 | {name: 'Page G', uv: 3490, pv: 4300, amt: 2100}, 15 | ]; 16 | 17 | const RechartsBarChart = () => ( 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | export default RechartsBarChart; -------------------------------------------------------------------------------- /src/components/charts/RechartsRadarChart.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/22. 3 | */ 4 | import React from 'react'; 5 | import {Radar, RadarChart, PolarGrid, Legend, 6 | PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer} from 'recharts'; 7 | 8 | const data = [ 9 | { subject: 'Math', A: 120, B: 110, fullMark: 150 }, 10 | { subject: 'Chinese', A: 98, B: 130, fullMark: 150 }, 11 | { subject: 'English', A: 86, B: 130, fullMark: 150 }, 12 | { subject: 'Geography', A: 99, B: 100, fullMark: 150 }, 13 | { subject: 'Physics', A: 85, B: 90, fullMark: 150 }, 14 | { subject: 'History', A: 65, B: 85, fullMark: 150 }, 15 | ]; 16 | 17 | const RechartsRadarChart = () => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | export default RechartsRadarChart; -------------------------------------------------------------------------------- /src/components/charts/RechartsRadialBarChart.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/22. 3 | */ 4 | import React from 'react'; 5 | import { RadialBarChart, Legend, Tooltip, ResponsiveContainer } from 'recharts'; 6 | 7 | const data = [ 8 | { name: '18-24', uv: 31.47, pv: 2400, fill: '#8884d8' }, 9 | { name: '25-29', uv: 26.69, pv: 4567, fill: '#83a6ed' }, 10 | { name: '30-34', uv: 15.69, pv: 1398, fill: '#8dd1e1' }, 11 | { name: '35-39', uv: 8.22, pv: 9800, fill: '#82ca9d' }, 12 | { name: '40-49', uv: 8.63, pv: 3908, fill: '#a4de6c' }, 13 | { name: '50+', uv: 2.63, pv: 4800, fill: '#d0ed57' }, 14 | { name: 'unknow', uv: 6.67, pv: 4800, fill: '#ffc658' }, 15 | ]; 16 | 17 | const RechartsRadialBarChart = () => ( 18 | 19 | 20 | {/* */} 29 | 37 | 38 | 39 | 40 | ); 41 | 42 | export default RechartsRadialBarChart; 43 | -------------------------------------------------------------------------------- /src/components/charts/RechartsSimpleLineChart.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/21. 3 | */ 4 | import React from 'react'; 5 | import {LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer} from 'recharts'; 6 | 7 | 8 | const data = [ 9 | {name: 'Page A', uv: 4000, pv: 2400, amt: 2400}, 10 | {name: 'Page B', uv: 3000, pv: 1398, amt: 2210}, 11 | {name: 'Page C', uv: 2000, pv: 9800, amt: 2290}, 12 | {name: 'Page D', uv: 2780, pv: 3908, amt: 2000}, 13 | {name: 'Page E', uv: 1890, pv: 4800, amt: 2181}, 14 | {name: 'Page F', uv: 2390, pv: 3800, amt: 2500}, 15 | {name: 'Page G', uv: 3490, pv: 4300, amt: 2100}, 16 | ]; 17 | 18 | const RechartsSimpleLineChart = () => ( 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | export default RechartsSimpleLineChart; -------------------------------------------------------------------------------- /src/components/cssmodule/index.module.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Monoton'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Monoton'), local('Monoton-Regular'), url(../../style/font/y6oxFxU60dYw9khW6q8jGw.woff2) format('woff2'); 6 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215; 7 | } 8 | .header { 9 | font-size: 7em; 10 | width: 100%; 11 | height: 500px; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | color: #fff; 16 | font-family: Monoton; 17 | p { 18 | animation: neon1 1.5s ease-in-out infinite alternate; 19 | &:hover { 20 | color: #FF1177; 21 | animation: none; 22 | } 23 | } 24 | } 25 | @keyframes neon1 { 26 | from { 27 | text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #FF1177, 0 0 70px #FF1177, 0 0 80px #FF1177, 0 0 100px #FF1177, 0 0 150px #FF1177; 28 | } 29 | to { 30 | text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #FF1177, 0 0 35px #FF1177, 0 0 40px #FF1177, 0 0 50px #FF1177, 0 0 75px #FF1177; 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/cssmodule/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 添加注释 4 | * Created by SEELE on 2018/1/12 5 | * 6 | */ 7 | import React, { Component } from 'react'; 8 | import { Col, Card, Row } from 'antd'; 9 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 10 | import styles from './index.module.less'; 11 | 12 | class Cssmodule extends Component { 13 | render() { 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 |
21 |

Hello CssModule

22 |
23 |
24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | export default Cssmodule; 32 | -------------------------------------------------------------------------------- /src/components/dashboard/EchartsProjects.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/5/5. 3 | */ 4 | import React from 'react'; 5 | import ReactEcharts from 'echarts-for-react'; 6 | 7 | let xAxisData = []; 8 | let data = []; 9 | for (let i = 0; i < 50; i++) { 10 | xAxisData.push(i); 11 | data.push(Math.ceil((Math.cos(i / 5) * (i / 5) + i / 6) * 5) + 10); 12 | } 13 | 14 | const option = { 15 | title: { 16 | text: '最近50天每天项目完成情况', 17 | left: 'center', 18 | textStyle: { 19 | color: '#ccc', 20 | fontSize: 10 21 | } 22 | }, 23 | backgroundColor: '#08263a', 24 | xAxis: [{ 25 | show: true, 26 | data: xAxisData, 27 | axisLabel: { 28 | textStyle: { 29 | color: '#ccc' 30 | } 31 | } 32 | }, { 33 | show: false, 34 | data: xAxisData 35 | }], 36 | tooltip: {}, 37 | visualMap: { 38 | show: false, 39 | min: 0, 40 | max: 50, 41 | dimension: 0, 42 | inRange: { 43 | color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055'] 44 | } 45 | }, 46 | yAxis: { 47 | axisLine: { 48 | show: false 49 | }, 50 | axisLabel: { 51 | textStyle: { 52 | color: '#ccc' 53 | } 54 | }, 55 | splitLine: { 56 | show: true, 57 | lineStyle: { 58 | color: '#08263f' 59 | } 60 | }, 61 | axisTick: { 62 | show: false 63 | } 64 | }, 65 | series: [ 66 | { 67 | name: 'Simulate Shadow', 68 | type: 'line', 69 | data: data, 70 | z: 2, 71 | showSymbol: false, 72 | animationDelay: 0, 73 | animationEasing: 'linear', 74 | animationDuration: 1200, 75 | lineStyle: { 76 | normal: { 77 | color: 'transparent' 78 | } 79 | }, 80 | areaStyle: { 81 | normal: { 82 | color: '#08263a', 83 | shadowBlur: 50, 84 | shadowColor: '#000' 85 | } 86 | } 87 | }, { 88 | name: '完成项目数', 89 | type: 'bar', 90 | data: data, 91 | xAxisIndex: 1, 92 | z: 3, 93 | itemStyle: { 94 | normal: { 95 | barBorderRadius: 5 96 | } 97 | } 98 | }], 99 | animationEasing: 'elasticOut', 100 | animationEasingUpdate: 'elasticOut', 101 | animationDelay: function (idx: number) { 102 | return idx * 20; 103 | }, 104 | animationDelayUpdate: function (idx: number) { 105 | return idx * 20; 106 | } 107 | }; 108 | const EchartsProjects = () => ( 109 | 114 | ); 115 | 116 | export default EchartsProjects; -------------------------------------------------------------------------------- /src/components/dashboard/EchartsViews.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/5/5. 3 | */ 4 | import React from 'react'; 5 | import ReactEcharts from 'echarts-for-react'; 6 | import echarts from 'echarts'; 7 | 8 | const option = { 9 | title: { 10 | text: '最近7天用户访问量', 11 | left: '50%', 12 | show: false, 13 | textAlign: 'center' 14 | }, 15 | tooltip: { 16 | trigger: 'axis', 17 | axisPointer: { 18 | lineStyle: { 19 | color: '#ddd' 20 | } 21 | }, 22 | backgroundColor: 'rgba(255,255,255,1)', 23 | padding: [5, 10], 24 | textStyle: { 25 | color: '#7588E4', 26 | }, 27 | extraCssText: 'box-shadow: 0 0 5px rgba(0,0,0,0.3)' 28 | }, 29 | legend: { 30 | right: 20, 31 | orient: 'vertical', 32 | }, 33 | xAxis: { 34 | type: 'category', 35 | data: ['2017-05-01', '2017-05-02', '2017-05-03', '2017-05-04', '2017-05-05', '2017-05-06','2017-05-07'], 36 | boundaryGap: false, 37 | splitLine: { 38 | show: true, 39 | interval: 'auto', 40 | lineStyle: { 41 | color: ['#D4DFF5'] 42 | } 43 | }, 44 | axisTick: { 45 | show: false 46 | }, 47 | axisLine: { 48 | lineStyle: { 49 | color: '#609ee9' 50 | } 51 | }, 52 | axisLabel: { 53 | margin: 10, 54 | textStyle: { 55 | fontSize: 10 56 | } 57 | } 58 | }, 59 | yAxis: { 60 | type: 'value', 61 | splitLine: { 62 | lineStyle: { 63 | color: ['#D4DFF5'] 64 | } 65 | }, 66 | axisTick: { 67 | show: false 68 | }, 69 | axisLine: { 70 | lineStyle: { 71 | color: '#609ee9' 72 | } 73 | }, 74 | axisLabel: { 75 | margin: 0, 76 | textStyle: { 77 | fontSize: 8 78 | } 79 | } 80 | }, 81 | series: [{ 82 | name: '昨日', 83 | type: 'line', 84 | smooth: true, 85 | showSymbol: false, 86 | symbol: 'circle', 87 | symbolSize: 6, 88 | data: ['1200', '1400', '808', '811', '626', '488', '1600'], 89 | areaStyle: { 90 | normal: { 91 | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ 92 | offset: 0, 93 | color: 'rgba(216, 244, 247,1)' 94 | }, { 95 | offset: 1, 96 | color: 'rgba(216, 244, 247,1)' 97 | }], false) 98 | } 99 | }, 100 | itemStyle: { 101 | normal: { 102 | color: '#58c8da' 103 | } 104 | }, 105 | lineStyle: { 106 | normal: { 107 | width: 3 108 | } 109 | } 110 | }] 111 | }; 112 | 113 | const EchartsViews = () => ( 114 | 119 | ); 120 | 121 | export default EchartsViews; -------------------------------------------------------------------------------- /src/components/env/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: index.tsx 3 | * Desc: 环境配置 4 | * File Created: 2020-08-02 23:00:28 5 | * Author: yezi 6 | * ------ 7 | * Copyright 2020 - present, yezi 8 | */ 9 | import React from 'react'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | import { Row, Col, Card, Descriptions } from 'antd'; 12 | 13 | const getEnvs = () => Object.keys(process.env).filter((key) => /^REACT_ADMIN_/i.test(key)); 14 | const Env = () => { 15 | const envs = getEnvs(); 16 | console.log(process.env); 17 | return ( 18 |
19 | 20 | 21 |
22 | 23 | 24 | {envs.map((env) => ( 25 | 26 | {process.env[env]} 27 | 28 | ))} 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default Env; 38 | -------------------------------------------------------------------------------- /src/components/extension/MultipleMenu.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: MultipleMenu.tsx 3 | * Desc: 多级菜单组件 4 | * File Created: 2019-12-18 23:15:35 5 | * Author: chenghao 6 | * ------ 7 | * Copyright 2019 - present, karakal 8 | */ 9 | import React from 'react'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | import { Row, Col, Card } from 'antd'; 12 | 13 | const MultipleMenu = () => { 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 |
多级菜单的功能扩展
21 |
菜单样式可能需要你来调整
22 |
23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default MultipleMenu; 30 | -------------------------------------------------------------------------------- /src/components/extension/QueryParams.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: QueryParams.js 3 | * Desc: query参数demo 4 | * File Created: 2018-11-25 23:18:09 5 | * Author: chenghao 6 | * Copyright 2018 - present, chenghao 7 | */ 8 | import React, { Component } from 'react'; 9 | import { Row, Col, Card } from 'antd'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | 12 | type QueryParamsProps = { 13 | query: any; 14 | }; 15 | 16 | class QueryParams extends Component { 17 | render() { 18 | const { query } = this.props; 19 | return ( 20 |
21 | 22 | 23 |
24 | 25 |
参数1: {query.param1}
26 |
参数2: {query.param2}
27 |
28 | 其他参数:{' '} 29 | {query.others || ( 30 | 31 | 点击查看 32 | 33 | )} 34 |
35 |
36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default QueryParams; 44 | -------------------------------------------------------------------------------- /src/components/extension/Visitor.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: Visitor.tsx 3 | * Desc: 访客 4 | * File Created: 2019-10-25 22:31:37 5 | * Author: chenghao 6 | * ------ 7 | * Copyright 2019 - present, chenghao 8 | */ 9 | import React from 'react'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | import { Row, Col, Card } from 'antd'; 12 | 13 | const Visitor = () => { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 29 | 访客模式的页面,你不需要登录即可访问的页面 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default Visitor; 38 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由组件出口文件 3 | * yezi 2018年6月24日 4 | */ 5 | import Loadable from 'react-loadable'; 6 | import Loading from './widget/Loading'; 7 | import BasicTable from './tables/BasicTables'; 8 | import AdvancedTable from './tables/AdvancedTables'; 9 | import AsynchronousTable from './tables/AsynchronousTable'; 10 | import Echarts from './charts/Echarts'; 11 | import Recharts from './charts/Recharts'; 12 | import Icons from './ui/Icons'; 13 | import Buttons from './ui/Buttons'; 14 | import Spins from './ui/Spins'; 15 | import Modals from './ui/Modals'; 16 | import Notifications from './ui/Notifications'; 17 | import Tabs from './ui/Tabs'; 18 | import Banners from './ui/banners'; 19 | import Drags from './ui/Draggable'; 20 | import Dashboard from './dashboard/Dashboard'; 21 | import Gallery from './ui/Gallery'; 22 | import BasicAnimations from './animation/BasicAnimations'; 23 | import ExampleAnimations from './animation/ExampleAnimations'; 24 | import AuthBasic from './auth/Basic'; 25 | import RouterEnter from './auth/RouterEnter'; 26 | import Cssmodule from './cssmodule'; 27 | import MapUi from './ui/map'; 28 | import QueryParams from './extension/QueryParams'; 29 | import Visitor from './extension/Visitor'; 30 | import MultipleMenu from './extension/MultipleMenu'; 31 | import Sub1 from './smenu/Sub1'; 32 | import Sub2 from './smenu/Sub2'; 33 | import Env from './env'; 34 | 35 | const WysiwygBundle = Loadable({ 36 | // 按需加载富文本配置 37 | loader: () => import('./ui/Wysiwyg'), 38 | loading: Loading, 39 | }); 40 | 41 | export default { 42 | BasicTable, 43 | AdvancedTable, 44 | AsynchronousTable, 45 | Echarts, 46 | Recharts, 47 | Icons, 48 | Buttons, 49 | Spins, 50 | Modals, 51 | Notifications, 52 | Tabs, 53 | Banners, 54 | Drags, 55 | Dashboard, 56 | Gallery, 57 | BasicAnimations, 58 | ExampleAnimations, 59 | AuthBasic, 60 | RouterEnter, 61 | WysiwygBundle, 62 | Cssmodule, 63 | MapUi, 64 | QueryParams, 65 | Visitor, 66 | MultipleMenu, 67 | Sub1, 68 | Sub2, 69 | Env, 70 | } as any; 71 | -------------------------------------------------------------------------------- /src/components/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React, { useEffect } from 'react'; 5 | import { Button, Form, Input } from 'antd'; 6 | import { PwaInstaller } from '../widget'; 7 | import { useAlita } from 'redux-alita'; 8 | import { RouteComponentProps } from 'react-router'; 9 | import { FormProps } from 'antd/lib/form'; 10 | import umbrella from 'umbrella-storage'; 11 | import { GithubOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'; 12 | import { useUpdateEffect } from 'ahooks'; 13 | 14 | const FormItem = Form.Item; 15 | type LoginProps = { 16 | setAlitaState: (param: any) => void; 17 | auth: any; 18 | } & RouteComponentProps & 19 | FormProps; 20 | 21 | const Login = (props: LoginProps) => { 22 | const { history } = props; 23 | const [auth, setAlita] = useAlita({ auth: {} }, { light: true }); 24 | 25 | useEffect(() => { 26 | setAlita('auth', null); 27 | }, [setAlita]); 28 | 29 | useUpdateEffect(() => { 30 | if (auth && auth.uid) { 31 | // 判断是否登陆 32 | umbrella.setLocalStorage('user', auth); 33 | history.push('/'); 34 | } 35 | }, [history, auth]); 36 | 37 | const handleSubmit = (values: any) => { 38 | if (checkUser(values)) { 39 | setAlita({ funcName: values.userName, stateName: 'auth' }); 40 | } 41 | }; 42 | const checkUser = (values: any) => { 43 | const users = [ 44 | ['admin', 'admin'], 45 | ['guest', 'guest'], 46 | ]; 47 | return users.some((user) => user[0] === values.userName && user[1] === values.password); 48 | }; 49 | const gitHub = () => { 50 | window.location.href = 51 | 'https://github.com/login/oauth/authorize?client_id=792cdcd244e98dcd2dee&redirect_uri=http://localhost:3006/&scope=user&state=reactAdmin'; 52 | }; 53 | 54 | return ( 55 |
56 |
57 |
58 | React Admin 59 | 60 |
61 |
62 | 66 | } 68 | placeholder="管理员输入admin, 游客输入guest" 69 | /> 70 | 71 | 72 | } 74 | type="password" 75 | placeholder="管理员输入admin, 游客输入guest" 76 | /> 77 | 78 | 79 | 80 | 忘记密码 81 | 82 | 90 |

91 | 或 现在就去注册! 92 | 93 | 94 | (第三方登录) 95 | 96 |

97 |
98 | 99 |
100 |
101 | ); 102 | }; 103 | 104 | export default Login; 105 | -------------------------------------------------------------------------------- /src/components/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/5/7. 3 | */ 4 | import React from 'react'; 5 | import img from '../../style/imgs/404.png'; 6 | 7 | class NotFound extends React.Component { 8 | state = { 9 | animated: '', 10 | }; 11 | enter = () => { 12 | this.setState({ animated: 'hinge' }); 13 | }; 14 | render() { 15 | return ( 16 |
20 | 404 26 |
27 | ); 28 | } 29 | } 30 | 31 | export default NotFound; 32 | -------------------------------------------------------------------------------- /src/components/smenu/Sub1.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: Sub1.tsx 3 | * Desc: 异步子菜单 4 | * File Created: 2020-01-21 11:31:15 5 | * Author: chenghao at <865470087@qq.com> 6 | * ------ 7 | * Copyright 2020 - present, chenghao 8 | */ 9 | import React from 'react'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | import { Row, Col, Card } from 'antd'; 12 | 13 | const SmenuSub1 = () => { 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 |
异步子菜单1
21 |
22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default SmenuSub1; 29 | -------------------------------------------------------------------------------- /src/components/smenu/Sub2.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: Sub2.tsx 3 | * Desc: 异步子菜单 4 | * File Created: 2020-01-21 11:31:15 5 | * Author: chenghao at <865470087@qq.com> 6 | * ------ 7 | * Copyright 2020 - present, chenghao 8 | */ 9 | import React from 'react'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | import { Row, Col, Card } from 'antd'; 12 | 13 | const SmenuSub2 = () => { 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 |
异步子菜单2
21 |
22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default SmenuSub2; 29 | -------------------------------------------------------------------------------- /src/components/tables/AdvancedTables.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card } from 'antd'; 6 | import FixedTable from './FixedTable'; 7 | import ExpandedTable from './ExpandedTable'; 8 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 9 | 10 | class AdvancedTables extends React.Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | {/* 33 |
34 | 35 | 36 | 37 |
38 | */} 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | export default AdvancedTables; 46 | -------------------------------------------------------------------------------- /src/components/tables/AsynchronousTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React from 'react'; 5 | import { Table, Button, Row, Col, Card } from 'antd'; 6 | import { getBbcNews } from '../../service'; 7 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 8 | 9 | const columns = [ 10 | { 11 | title: '新闻标题', 12 | dataIndex: 'title', 13 | width: 100, 14 | render: (text: any, record: any) => ( 15 | 16 | {text} 17 | 18 | ), 19 | }, 20 | { 21 | title: '作者', 22 | dataIndex: 'author', 23 | width: 80, 24 | }, 25 | { 26 | title: '发布时间', 27 | dataIndex: 'publishedAt', 28 | width: 80, 29 | }, 30 | { 31 | title: '描述', 32 | dataIndex: 'description', 33 | width: 200, 34 | }, 35 | ]; 36 | 37 | class AsynchronousTable extends React.Component { 38 | state = { 39 | selectedRowKeys: [], // Check here to configure the default column 40 | loading: false, 41 | data: [], 42 | }; 43 | componentDidMount() { 44 | this.start(); 45 | } 46 | start = () => { 47 | this.setState({ loading: true }); 48 | getBbcNews().then(({ articles }: { articles: any }) => { 49 | this.setState({ 50 | data: articles, 51 | loading: false, 52 | }); 53 | }); 54 | }; 55 | onSelectChange = (selectedRowKeys: string[]) => { 56 | console.log('selectedRowKeys changed: ', selectedRowKeys); 57 | this.setState({ selectedRowKeys }); 58 | }; 59 | render() { 60 | const { loading, selectedRowKeys } = this.state; 61 | const rowSelection = { 62 | selectedRowKeys, 63 | onChange: this.onSelectChange, 64 | }; 65 | const hasSelected = selectedRowKeys.length > 0; 66 | return ( 67 |
68 | 69 | 70 |
71 |
72 | 73 |
74 | 82 | 83 | {hasSelected 84 | ? `Selected ${selectedRowKeys.length} items` 85 | : ''} 86 | 87 |
88 |
93 | 94 | 95 | 96 | 97 | 98 | ); 99 | } 100 | } 101 | 102 | export default AsynchronousTable; 103 | -------------------------------------------------------------------------------- /src/components/tables/BasicTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/15. 3 | */ 4 | import React from 'react'; 5 | import { Table, Button } from 'antd'; 6 | import { DownOutlined } from '@ant-design/icons'; 7 | 8 | const columns = [ 9 | { 10 | title: 'Name', 11 | dataIndex: 'name', 12 | key: 'name', 13 | render: (text: any) => {text}, 14 | }, 15 | { 16 | title: 'Age', 17 | dataIndex: 'age', 18 | key: 'age', 19 | }, 20 | { 21 | title: 'Address', 22 | dataIndex: 'address', 23 | key: 'address', 24 | }, 25 | { 26 | title: 'Action', 27 | key: 'action', 28 | render: (text: any, record: any) => ( 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | ), 39 | }, 40 | ]; 41 | 42 | const data = [ 43 | { 44 | key: '1', 45 | name: 'John Brown', 46 | age: 32, 47 | address: 'New York No. 1 Lake Park', 48 | }, 49 | { 50 | key: '2', 51 | name: 'Jim Green', 52 | age: 42, 53 | address: 'London No. 1 Lake Park', 54 | }, 55 | { 56 | key: '3', 57 | name: 'Joe Black', 58 | age: 32, 59 | address: 'Sidney No. 1 Lake Park', 60 | }, 61 | ]; 62 | 63 | const BasicTable = () =>
; 64 | 65 | export default BasicTable; 66 | -------------------------------------------------------------------------------- /src/components/tables/BasicTables.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/15. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card } from 'antd'; 6 | import BasicTable from './BasicTable'; 7 | import SelectTable from './SelectTable'; 8 | import SortTable from './SortTable'; 9 | import SearchTable from './SearchTable'; 10 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 11 | 12 | const BasicTables = () => ( 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 |
40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | ); 51 | 52 | export default BasicTables; 53 | -------------------------------------------------------------------------------- /src/components/tables/ExpandedTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React from 'react'; 5 | import { Table, Button } from 'antd'; 6 | 7 | const columns = [ 8 | { title: 'Name', dataIndex: 'name', key: 'name' }, 9 | { title: 'Age', dataIndex: 'age', key: 'age' }, 10 | { title: 'Address', dataIndex: 'address', key: 'address' }, 11 | { title: 'Action', dataIndex: '', key: 'x', render: () => }, 12 | ]; 13 | 14 | const data = [ 15 | { key: 1, name: 'John Brown', age: 32, address: 'New York No. 1 Lake Park', description: 'My name is John Brown, I am 32 years old, living in New York No. 1 Lake Park.' }, 16 | { key: 2, name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park', description: 'My name is Jim Green, I am 42 years old, living in London No. 1 Lake Park.' }, 17 | { key: 3, name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park', description: 'My name is Joe Black, I am 32 years old, living in Sidney No. 1 Lake Park.' }, 18 | ]; 19 | 20 | const ExpandedTable = () => ( 21 |

{record.description}

} 24 | dataSource={data} 25 | /> 26 | ); 27 | 28 | export default ExpandedTable; -------------------------------------------------------------------------------- /src/components/tables/FixedTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React from 'react'; 5 | import { Table } from 'antd'; 6 | import { ColumnProps } from 'antd/lib/table'; 7 | 8 | const columns: ColumnProps[] = [ 9 | { title: 'Full Name', width: 100, dataIndex: 'name', key: 'name', fixed: 'left' }, 10 | { title: 'Age', width: 100, dataIndex: 'age', key: 'age', fixed: 'left' }, 11 | { title: 'Column 1', dataIndex: 'address', key: '1' }, 12 | { title: 'Column 2', dataIndex: 'address', key: '2' }, 13 | { title: 'Column 3', dataIndex: 'address', key: '3' }, 14 | { title: 'Column 4', dataIndex: 'address', key: '4' }, 15 | { title: 'Column 5', dataIndex: 'address', key: '5' }, 16 | { title: 'Column 6', dataIndex: 'address', key: '6' }, 17 | { title: 'Column 7', dataIndex: 'address', key: '7' }, 18 | { title: 'Column 8', dataIndex: 'address', key: '8' }, 19 | { 20 | title: 'Action', 21 | key: 'operation', 22 | fixed: 'right', 23 | width: 100, 24 | render: () => action, 25 | }, 26 | ]; 27 | 28 | const data = [ 29 | { 30 | key: '1', 31 | name: 'John Brown', 32 | age: 32, 33 | address: 'New York Park', 34 | }, 35 | { 36 | key: '2', 37 | name: 'Jim Green', 38 | age: 40, 39 | address: 'London Park', 40 | }, 41 | ]; 42 | 43 | const FixedTable = () =>
; 44 | 45 | export default FixedTable; 46 | -------------------------------------------------------------------------------- /src/components/tables/SearchTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import React from 'react'; 5 | import { Table, Input, Button } from 'antd'; 6 | import { SmileOutlined } from '@ant-design/icons'; 7 | 8 | const data = [ 9 | { 10 | key: '1', 11 | name: 'John Brown', 12 | age: 32, 13 | address: 'New York No. 1 Lake Park', 14 | }, 15 | { 16 | key: '2', 17 | name: 'Joe Black', 18 | age: 42, 19 | address: 'London No. 1 Lake Park', 20 | }, 21 | { 22 | key: '3', 23 | name: 'Jim Green', 24 | age: 32, 25 | address: 'Sidney No. 1 Lake Park', 26 | }, 27 | { 28 | key: '4', 29 | name: 'Jim Red', 30 | age: 32, 31 | address: 'London No. 2 Lake Park', 32 | }, 33 | ]; 34 | 35 | class SearchTable extends React.Component { 36 | state = { 37 | filterDropdownVisible: false, 38 | data, 39 | searchText: '', 40 | filtered: false, 41 | }; 42 | searchInput: any; 43 | onInputChange = (e: any) => { 44 | this.setState({ searchText: e.target.value }); 45 | }; 46 | onSearch = () => { 47 | const { searchText } = this.state; 48 | const reg = new RegExp(searchText, 'gi'); 49 | this.setState({ 50 | filterDropdownVisible: false, 51 | filtered: !!searchText, 52 | data: data 53 | .map((record) => { 54 | const match = record.name.match(reg); 55 | if (!match) { 56 | return null; 57 | } 58 | return { 59 | ...record, 60 | name: ( 61 | 62 | {record.name 63 | .split(reg) 64 | .map((text, i) => 65 | i > 0 66 | ? [{match[0]}, text] 67 | : text 68 | )} 69 | 70 | ), 71 | }; 72 | }) 73 | .filter((record) => !!record), 74 | }); 75 | }; 76 | render() { 77 | const columns = [ 78 | { 79 | title: 'Name', 80 | dataIndex: 'name', 81 | key: 'name', 82 | filterDropdown: ( 83 |
84 | (this.searchInput = ele)} 86 | placeholder="Search name" 87 | value={this.state.searchText} 88 | onChange={this.onInputChange} 89 | onPressEnter={this.onSearch} 90 | /> 91 | 94 |
95 | ), 96 | filterIcon: ( 97 | 98 | ), 99 | filterDropdownVisible: this.state.filterDropdownVisible, 100 | onFilterDropdownVisibleChange: (visible: boolean) => 101 | this.setState({ filterDropdownVisible: visible }, () => 102 | this.searchInput.focus() 103 | ), 104 | }, 105 | { 106 | title: 'Age', 107 | dataIndex: 'age', 108 | key: 'age', 109 | }, 110 | { 111 | title: 'Address', 112 | dataIndex: 'address', 113 | key: 'address', 114 | filters: [ 115 | { 116 | text: 'London', 117 | value: 'London', 118 | }, 119 | { 120 | text: 'New York', 121 | value: 'New York', 122 | }, 123 | ], 124 | onFilter: (value: any, record: any) => record.address.indexOf(value) === 0, 125 | }, 126 | ]; 127 | return ( 128 |
129 |
130 | 145 | 146 | ); 147 | } 148 | } 149 | 150 | export default SearchTable; 151 | -------------------------------------------------------------------------------- /src/components/tables/SelectTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/15. 3 | */ 4 | import React from 'react'; 5 | import { Table } from 'antd'; 6 | // import { TableRowSelection } from 'antd/lib/table'; 7 | 8 | const columns = [ 9 | { 10 | title: 'Name', 11 | dataIndex: 'name', 12 | }, 13 | { 14 | title: 'Age', 15 | dataIndex: 'age', 16 | }, 17 | { 18 | title: 'Address', 19 | dataIndex: 'address', 20 | }, 21 | ]; 22 | 23 | const data: any[] = []; 24 | for (let i = 0; i < 46; i++) { 25 | data.push({ 26 | key: i, 27 | name: `Edward King ${i}`, 28 | age: 32, 29 | address: `London, Park Lane no. ${i}`, 30 | }); 31 | } 32 | 33 | class SelectTable extends React.Component { 34 | state = { 35 | selectedRowKeys: [], // Check here to configure the default column 36 | }; 37 | onSelectChange = (selectedRowKeys: string[] | number[]) => { 38 | console.log('selectedRowKeys changed: ', selectedRowKeys); 39 | this.setState({ selectedRowKeys }); 40 | }; 41 | render() { 42 | const { selectedRowKeys } = this.state; 43 | const rowSelection: any = { 44 | selectedRowKeys, 45 | onChange: this.onSelectChange, 46 | selections: [ 47 | { 48 | key: 'odd', 49 | text: '选择奇数列', 50 | onSelect: (changableRowKeys: string[]) => { 51 | let newSelectedRowKeys = []; 52 | newSelectedRowKeys = changableRowKeys.filter((key, index) => { 53 | if (index % 2 !== 0) { 54 | return false; 55 | } 56 | return true; 57 | }); 58 | this.setState({ selectedRowKeys: newSelectedRowKeys }); 59 | }, 60 | }, 61 | { 62 | key: 'even', 63 | text: '选择偶数列', 64 | onSelect: (changableRowKeys: string[]) => { 65 | let newSelectedRowKeys = []; 66 | newSelectedRowKeys = changableRowKeys.filter((key, index) => { 67 | if (index % 2 !== 0) { 68 | return true; 69 | } 70 | return false; 71 | }); 72 | this.setState({ selectedRowKeys: newSelectedRowKeys }); 73 | }, 74 | }, 75 | ], 76 | // onSelection: this.onSelection, 77 | }; 78 | return
; 79 | } 80 | } 81 | 82 | export default SelectTable; 83 | -------------------------------------------------------------------------------- /src/components/tables/SortTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/15. 3 | */ 4 | import React from 'react'; 5 | import { Table, Button } from 'antd'; 6 | 7 | const data = [ 8 | { 9 | key: '1', 10 | name: 'John Brown', 11 | age: 32, 12 | address: 'New York No. 1 Lake Park', 13 | }, 14 | { 15 | key: '2', 16 | name: 'Jim Green', 17 | age: 42, 18 | address: 'London No. 1 Lake Park', 19 | }, 20 | { 21 | key: '3', 22 | name: 'Joe Black', 23 | age: 32, 24 | address: 'Sidney No. 1 Lake Park', 25 | }, 26 | { 27 | key: '4', 28 | name: 'Jim Red', 29 | age: 32, 30 | address: 'London No. 2 Lake Park', 31 | }, 32 | ]; 33 | 34 | type SortTableState = { 35 | filteredInfo: any; 36 | sortedInfo: any; 37 | }; 38 | class SortTable extends React.Component { 39 | constructor(props: any) { 40 | super(props); 41 | this.state = { 42 | filteredInfo: {}, 43 | sortedInfo: {}, 44 | }; 45 | } 46 | handleChange = (pagination: any, filters: any, sorter: any) => { 47 | console.log('Various parameters', pagination, filters, sorter); 48 | this.setState({ 49 | filteredInfo: filters, 50 | sortedInfo: sorter, 51 | }); 52 | }; 53 | clearFilters = () => { 54 | this.setState({ filteredInfo: null }); 55 | }; 56 | clearAll = () => { 57 | this.setState({ 58 | filteredInfo: null, 59 | sortedInfo: null, 60 | }); 61 | }; 62 | setAgeSort = () => { 63 | this.setState({ 64 | sortedInfo: { 65 | order: 'descend', 66 | columnKey: 'age', 67 | }, 68 | }); 69 | }; 70 | render() { 71 | let { sortedInfo, filteredInfo } = this.state; 72 | const columns = [ 73 | { 74 | title: 'Name', 75 | dataIndex: 'name', 76 | key: 'name', 77 | filters: [{ text: 'Joe', value: 'Joe' }, { text: 'Jim', value: 'Jim' }], 78 | filteredValue: filteredInfo.name || null, 79 | onFilter: (value: any, record: any) => record.name.includes(value), 80 | sorter: (a: any, b: any) => a.name.length - b.name.length, 81 | sortOrder: sortedInfo.columnKey === 'name' && sortedInfo.order, 82 | }, 83 | { 84 | title: 'Age', 85 | dataIndex: 'age', 86 | key: 'age', 87 | sorter: (a: any, b: any) => a.age - b.age, 88 | sortOrder: sortedInfo.columnKey === 'age' && sortedInfo.order, 89 | }, 90 | { 91 | title: 'Address', 92 | dataIndex: 'address', 93 | key: 'address', 94 | filters: [ 95 | { text: 'London', value: 'London' }, 96 | { text: 'New York', value: 'New York' }, 97 | ], 98 | filteredValue: filteredInfo.address || null, 99 | onFilter: (value: any, record: any) => record.address.includes(value), 100 | sorter: (a: any, b: any) => a.address.length - b.address.length, 101 | sortOrder: sortedInfo.columnKey === 'address' && sortedInfo.order, 102 | }, 103 | ]; 104 | return ( 105 |
106 |
107 | 108 | 109 | 110 |
111 |
112 | 113 | ); 114 | } 115 | } 116 | 117 | export default SortTable; 118 | -------------------------------------------------------------------------------- /src/components/ui/Spins.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/23. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card, Spin, Alert, Switch, Button } from 'antd'; 6 | import BreadcrumbCustom from '../widget/BreadcrumbCustom'; 7 | import NProgress from 'nprogress'; 8 | import 'nprogress/nprogress.css'; 9 | 10 | class Spins extends React.Component { 11 | state = { loading: false }; 12 | toggle = (value: boolean) => { 13 | this.setState({ loading: value }); 14 | }; 15 | nprogressStart = () => { 16 | NProgress.start(); 17 | }; 18 | nprogressDone = () => { 19 | NProgress.done(); 20 | }; 21 | render() { 22 | const container = ( 23 | 28 | ); 29 | return ( 30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 |
39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 60 | 61 | 62 |
63 | 64 | 65 | 66 |
67 | 68 | {container} 69 | Loading state: 70 | 71 | 72 |
73 | 74 | 75 |
76 | 77 |

顶部进度条

78 |

79 |

88 | 89 | 90 | 91 | ); 92 | } 93 | } 94 | 95 | export default Spins; 96 | -------------------------------------------------------------------------------- /src/components/ui/banners/AutoPlay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/26. 3 | */ 4 | import React from 'react'; 5 | import BannerAnim, { Element } from 'rc-banner-anim'; 6 | import TweenOne from 'rc-tween-one'; 7 | import 'rc-banner-anim/assets/index.css'; 8 | const BgElement = Element.BgElement; 9 | class AutoPlay extends React.Component { 10 | render(){ 11 | return ( 12 | 13 | 17 | 24 | 25 | Ant Motion Banner 26 | 27 | 30 | The Fast Way Use Animation In React 31 | 32 | 33 | 37 | 44 | 45 | Ant Motion Banner 46 | 47 | 50 | The Fast Way Use Animation In React 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default AutoPlay; -------------------------------------------------------------------------------- /src/components/ui/banners/Basic.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/26. 3 | */ 4 | import React from 'react'; 5 | import BannerAnim, { Element } from 'rc-banner-anim'; 6 | import TweenOne from 'rc-tween-one'; 7 | import 'rc-banner-anim/assets/index.css'; 8 | const BgElement = Element.BgElement; 9 | class Basic extends React.Component { 10 | render(){ 11 | return ( 12 | 13 | 17 | 24 | 25 | Ant Motion Banner 26 | 27 | 31 | The Fast Way Use Animation In React 32 | 33 | 34 | 38 | 45 | 46 | Ant Motion Banner 47 | 48 | 52 | The Fast Way Use Animation In React 53 | 54 | 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default Basic; -------------------------------------------------------------------------------- /src/components/ui/banners/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/26. 3 | */ 4 | import React from 'react'; 5 | import { Row, Col, Card } from 'antd'; 6 | import BreadcrumbCustom from '../../widget/BreadcrumbCustom'; 7 | import Basic from './Basic'; 8 | import AutoPlay from './AutoPlay'; 9 | // import Custom from './Custom'; 10 | 11 | class Banners extends React.Component { 12 | render() { 13 | return ( 14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | {/*引入自定义会导致组件冲突不显示*/} 35 | {/**/} 36 | 37 |
38 | 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | export default Banners; 46 | -------------------------------------------------------------------------------- /src/components/ui/emoji/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/22. 3 | */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import setFont from './iconfont'; 7 | 8 | setFont(); 9 | type EmojiProps = { 10 | type: string; 11 | }; 12 | const Emoji = ({ type }: EmojiProps) => { 13 | const useTag = ``; 14 | return ( 15 | 16 | 17 | 30 | 31 | ); 32 | }; 33 | 34 | Emoji.propTypes = { 35 | type: PropTypes.string.isRequired, 36 | }; 37 | 38 | export default Emoji; 39 | -------------------------------------------------------------------------------- /src/components/ui/map/Tencent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactQMap from 'react-qmap'; 3 | 4 | const getContianer = (dom: any) => { 5 | const middleControl = document.createElement('div'); 6 | middleControl.style.cssText = 7 | 'width: 22px;height: 30px;position: absolute;left: 50%;top: 50%;z-index: 999;margin-left: -23px;margin-top: -23px;'; 8 | middleControl.innerHTML = ``; 9 | dom.appendChild(middleControl); 10 | }; 11 | 12 | export default () => ( 13 | 21 | ); 22 | -------------------------------------------------------------------------------- /src/components/ui/map/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tencent from './Tencent'; 3 | import { Row, Col, Card } from 'antd'; 4 | import BreadcrumbCustom from '../../widget/BreadcrumbCustom'; 5 | 6 | export default () => ( 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/components/widget/AuthWidget.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 叶子 on 2017/7/31. 3 | */ 4 | import { Component } from 'react'; 5 | import { connectAlita } from 'redux-alita'; 6 | 7 | type AuthWidgetProps = { 8 | auth: any; 9 | children: (param: any) => React.ReactElement; 10 | }; 11 | 12 | class AuthWidget extends Component { 13 | render() { 14 | const { auth = {} } = this.props; 15 | return this.props.children(auth.data || {}); 16 | } 17 | } 18 | 19 | export default connectAlita(['auth'])(AuthWidget); 20 | -------------------------------------------------------------------------------- /src/components/widget/BreadcrumbCustom.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/22. 3 | */ 4 | import React, { ReactNode } from 'react'; 5 | import { Breadcrumb } from 'antd'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | interface BreadcrumbCustomProps { 9 | breads?: ReactNode[]; 10 | } 11 | const BreadcrumbCustom = (props: BreadcrumbCustomProps) => { 12 | const { breads } = props; 13 | return ( 14 | 15 | 16 | 首页 17 | 18 | {breads?.map((bread, i) => ( 19 | {bread} 20 | ))} 21 | 22 | ); 23 | }; 24 | 25 | export default BreadcrumbCustom; 26 | -------------------------------------------------------------------------------- /src/components/widget/Copyright.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: Copyright.tsx 3 | * Desc: 版权信息 4 | * File Created: 2020-04-12 22:50:33 5 | * Author: chenghao 6 | * ------ 7 | * Copyright 2020 - present, karakal 8 | */ 9 | import React from 'react'; 10 | 11 | const Copyright = () => { 12 | return ( 13 |
14 | react-admin ©{new Date().getFullYear()} Created by yezihaohao@yezi.haohao@foxmail.com 15 |
16 | ); 17 | }; 18 | 19 | export default Copyright; 20 | -------------------------------------------------------------------------------- /src/components/widget/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
7 | 页面加载中... 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /src/components/widget/PwaInstaller.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: PwaInstaller.js 3 | * Desc: pwa手动触发安装 4 | * File Created: 2018-11-07 21:13:18 5 | * Author: chenghao 6 | * 7 | * Copyright 2018 - present, chenghao 8 | */ 9 | import React, { Component } from 'react'; 10 | 11 | class PwaInstaller extends Component { 12 | state = { 13 | installed: true, 14 | }; 15 | componentDidMount() { 16 | window.addEventListener('beforeinstallprompt', this.beforeInstallPrompt); 17 | } 18 | componentWillUnmount() { 19 | window.removeEventListener('beforeinstallprompt', this.beforeInstallPrompt); 20 | } 21 | deferredPrompt: any; 22 | beforeInstallPrompt = (e: Event) => { 23 | console.log('beforeinstallprompt Event fired'); 24 | // 未安装PWA应用 25 | this.setState({ installed: false }); 26 | 27 | e.preventDefault(); 28 | // Stash the event so it can be triggered later. 29 | this.deferredPrompt = e; 30 | return false; 31 | }; 32 | download = () => { 33 | if (this.deferredPrompt !== undefined) { 34 | // The user has had a postive interaction with our app and Chrome 35 | // has tried to prompt previously, so let's show the prompt. 36 | this.deferredPrompt.prompt(); 37 | // Follow what the user has done with the prompt. 38 | this.deferredPrompt.userChoice.then((choiceResult: any) => { 39 | console.log(choiceResult.outcome); 40 | if (choiceResult.outcome === 'dismissed') { 41 | console.log('User cancelled home screen install'); 42 | } else { 43 | console.log('User added to home screen'); 44 | } 45 | // We no longer need the prompt. Clear it up. 46 | this.deferredPrompt = null; 47 | }); 48 | } 49 | }; 50 | render() { 51 | const { installed } = this.state; 52 | return ( 53 | !installed && ( 54 |
55 |
56 |
57 | ) 58 | ); 59 | } 60 | } 61 | 62 | export default PwaInstaller; 63 | -------------------------------------------------------------------------------- /src/components/widget/ThemePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { SketchPicker } from 'react-color'; 3 | import classNames from 'classnames'; 4 | import { SettingOutlined } from '@ant-design/icons'; 5 | 6 | class ThemePicker extends Component { 7 | state = { 8 | switcherOn: false, 9 | background: localStorage.getItem('@primary-color') || '#313653', 10 | }; 11 | _switcherOn = () => { 12 | this.setState({ 13 | switcherOn: !this.state.switcherOn, 14 | }); 15 | }; 16 | _handleChangeComplete = (color: any) => { 17 | console.log(color); 18 | this.setState({ background: color.hex }); 19 | localStorage.setItem('@primary-color', color.hex); 20 | 21 | (window as any).less.modifyVars({ 22 | '@primary-color': color.hex, 23 | }); 24 | }; 25 | render() { 26 | const { switcherOn, background } = this.state; 27 | return ( 28 |
29 | 30 | 31 | 32 |
33 | 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default ThemePicker; 44 | -------------------------------------------------------------------------------- /src/components/widget/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as PwaInstaller } from './PwaInstaller'; 2 | export { default as AuthWidget } from './AuthWidget'; 3 | export { default as ThemePicker } from './ThemePicker'; 4 | export { default as Copyright } from './Copyright'; 5 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AlitaProvider, setConfig } from 'redux-alita'; 4 | import umbrella from 'umbrella-storage'; 5 | import Page from './Page'; 6 | import * as serviceWorker from './serviceWorker'; 7 | import * as apis from './service'; 8 | // import { AppContainer } from 'react-hot-loader'; 9 | import './style/lib/animate.css'; 10 | import './style/index.less'; 11 | import './style/antd/index.less'; 12 | 13 | setConfig(apis); 14 | umbrella.config('REACT-ADMIN'); 15 | // const render = Component => { // 增加react-hot-loader保持状态刷新操作,如果不需要可去掉并把下面注释的打开 16 | // ReactDOM.render( 17 | // 18 | // 19 | // 20 | // 21 | // 22 | // , 23 | // document.getElementById('root') 24 | // ); 25 | // }; 26 | 27 | // render(Page); 28 | 29 | // Webpack Hot Module Replacement API 30 | // if (module.hot) { 31 | // // 隐藏You cannot change ; it will be ignored 错误提示 32 | // // react-hot-loader 使用在react-router 3.x上引起的提示,react-router 4.x不存在 33 | // // 详情可参照https://github.com/gaearon/react-hot-loader/issues/298 34 | // const orgError = console.error; // eslint-disable-line no-console 35 | // console.error = (...args) => { // eslint-disable-line no-console 36 | // if (args && args.length === 1 && typeof args[0] === 'string' && args[0].indexOf('You cannot change ;') > -1) { 37 | // // React route changed 38 | // } else { 39 | // // Log the error as normally 40 | // orgError.apply(console, args); 41 | // } 42 | // }; 43 | // module.hot.accept('./Page', () => { 44 | // render(Page); 45 | // }) 46 | // } 47 | 48 | ReactDOM.render( 49 | // 50 | 51 | 52 | , 53 | // 54 | document.getElementById('root') 55 | ); 56 | // If you want your app to work offline and load faster, you can change 57 | // unregister() to register() below. Note this comes with some pitfalls. 58 | // Learn more about service workers: http://bit.ly/CRA-PWA 59 | // serviceWorker.unregister(); 60 | serviceWorker.register(); 61 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test'; 8 | readonly PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module '*.bmp' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.gif' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.jpg' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.jpeg' { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module '*.png' { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module '*.webp' { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.svg' { 43 | import * as React from 'react'; 44 | 45 | export const ReactComponent: React.FunctionComponent>; 46 | 47 | const src: string; 48 | export default src; 49 | } 50 | 51 | declare module '*.module.css' { 52 | const classes: { readonly [key: string]: string }; 53 | export default classes; 54 | } 55 | 56 | declare module '*.module.scss' { 57 | const classes: { readonly [key: string]: string }; 58 | export default classes; 59 | } 60 | 61 | declare module '*.module.sass' { 62 | const classes: { readonly [key: string]: string }; 63 | export default classes; 64 | } 65 | 66 | declare module '*.module.less' { 67 | const classes: { readonly [key: string]: string }; 68 | export default classes; 69 | } 70 | 71 | declare module 'draftjs-to-html'; 72 | 73 | declare module 'draftjs-to-markdown'; 74 | 75 | declare module 'react-qmap'; 76 | -------------------------------------------------------------------------------- /src/routes/RouteWrapper.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * File: RouteWrapper.tsx 3 | * Desc: 描述 4 | * File Created: 2020-05-19 11:32:58 5 | * Author: chenghao at 6 | * ------ 7 | * Copyright 2020 - present, karakal 8 | */ 9 | import React, { useMemo } from 'react'; 10 | import DocumentTitle from 'react-document-title'; 11 | import queryString from 'query-string'; 12 | 13 | const RouteWrapper = (props: any) => { 14 | let { Comp, route, ...restProps } = props; 15 | /** useMemo 缓存query,避免每次生成生的query */ 16 | const queryMemo = useMemo(() => { 17 | const queryReg = /\?\S*/g; 18 | const matchQuery = (reg: RegExp) => { 19 | const queryParams = restProps.location.search.match(reg); 20 | return queryParams ? queryParams[0] : '{}'; 21 | }; 22 | return queryString.parse(matchQuery(queryReg)); 23 | }, [restProps.location.search]); 24 | const mergeQueryToProps = () => { 25 | const queryReg = /\?\S*/g; 26 | const removeQueryInRouter = (restProps: any, reg: RegExp) => { 27 | const { params } = restProps.match; 28 | Object.keys(params).forEach((key) => { 29 | params[key] = params[key] && params[key].replace(reg, ''); 30 | }); 31 | restProps.match.params = { ...params }; 32 | }; 33 | 34 | restProps = removeQueryInRouter(restProps, queryReg); 35 | const merge = { 36 | ...restProps, 37 | query: queryMemo, 38 | }; 39 | return merge; 40 | }; 41 | return ( 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default RouteWrapper; 49 | -------------------------------------------------------------------------------- /src/routes/config.ts: -------------------------------------------------------------------------------- 1 | export interface IFMenuBase { 2 | key: string; 3 | title: string; 4 | icon?: string; 5 | component?: string; 6 | query?: string; 7 | requireAuth?: string; 8 | route?: string; 9 | /** 是否登录校验,true不进行校验(访客) */ 10 | login?: boolean; 11 | } 12 | 13 | export interface IFMenu extends IFMenuBase { 14 | subs?: IFMenu[]; 15 | } 16 | 17 | const menus: { 18 | menus: IFMenu[]; 19 | others: IFMenu[] | []; 20 | [index: string]: any; 21 | } = { 22 | menus: [ 23 | // 菜单相关路由 24 | { key: '/app/dashboard/index', title: '首页', icon: 'mobile', component: 'Dashboard' }, 25 | { 26 | key: '/app/ui', 27 | title: 'UI', 28 | icon: 'scan', 29 | subs: [ 30 | { key: '/app/ui/buttons', title: '按钮', component: 'Buttons' }, 31 | { key: '/app/ui/icons', title: '图标', component: 'Icons' }, 32 | { key: '/app/ui/spins', title: '加载中', component: 'Spins' }, 33 | { key: '/app/ui/modals', title: '对话框', component: 'Modals' }, 34 | { key: '/app/ui/notifications', title: '通知提醒框', component: 'Notifications' }, 35 | { key: '/app/ui/tabs', title: '标签页', component: 'Tabs' }, 36 | { key: '/app/ui/banners', title: '轮播图', component: 'Banners' }, 37 | { key: '/app/ui/wysiwyg', title: '富文本', component: 'WysiwygBundle' }, 38 | { key: '/app/ui/drags', title: '拖拽', component: 'Drags' }, 39 | { key: '/app/ui/gallery', title: '画廊', component: 'Gallery' }, 40 | { key: '/app/ui/map', title: '地图', component: 'MapUi' }, 41 | ], 42 | }, 43 | { 44 | key: '/app/animation', 45 | title: '动画', 46 | icon: 'rocket', 47 | subs: [ 48 | { 49 | key: '/app/animation/basicAnimations', 50 | title: '基础动画', 51 | component: 'BasicAnimations', 52 | }, 53 | { 54 | key: '/app/animation/exampleAnimations', 55 | title: '动画案例', 56 | component: 'ExampleAnimations', 57 | }, 58 | ], 59 | }, 60 | { 61 | key: '/app/table', 62 | title: '表格', 63 | icon: 'copy', 64 | subs: [ 65 | { key: '/app/table/basicTable', title: '基础表格', component: 'BasicTable' }, 66 | { key: '/app/table/advancedTable', title: '高级表格', component: 'AdvancedTable' }, 67 | { 68 | key: '/app/table/asynchronousTable', 69 | title: '异步表格', 70 | component: 'AsynchronousTable', 71 | }, 72 | ], 73 | }, 74 | { 75 | key: '/app/chart', 76 | title: '图表', 77 | icon: 'area-chart', 78 | subs: [ 79 | { key: '/app/chart/echarts', title: 'echarts', component: 'Echarts' }, 80 | { key: '/app/chart/recharts', title: 'recharts', component: 'Recharts' }, 81 | ], 82 | }, 83 | { 84 | key: '/subs4', 85 | title: '页面', 86 | icon: 'switcher', 87 | subs: [ 88 | { key: '/login', title: '登录' }, 89 | { key: '/404', title: '404' }, 90 | ], 91 | }, 92 | { 93 | key: '/app/auth', 94 | title: '权限管理', 95 | icon: 'safety', 96 | subs: [ 97 | { key: '/app/auth/basic', title: '基础演示', component: 'AuthBasic' }, 98 | { 99 | key: '/app/auth/routerEnter', 100 | title: '路由拦截', 101 | component: 'RouterEnter', 102 | requireAuth: 'auth/testPage', 103 | }, 104 | ], 105 | }, 106 | { 107 | key: '/app/cssModule', 108 | title: 'cssModule', 109 | icon: 'star', 110 | component: 'Cssmodule', 111 | }, 112 | { 113 | key: '/app/extension', 114 | title: '功能扩展', 115 | icon: 'bars', 116 | subs: [ 117 | { 118 | key: '/app/extension/queryParams', 119 | title: '问号形式参数', 120 | component: 'QueryParams', 121 | query: '?param1=1¶m2=2', 122 | }, 123 | { 124 | key: '/app/extension/visitor', 125 | title: '访客模式', 126 | component: 'Visitor', 127 | login: true, 128 | }, 129 | { 130 | key: '/app/extension/multiple', 131 | title: '多级菜单', 132 | subs: [ 133 | { 134 | key: '/app/extension/multiple/child', 135 | title: '多级菜单子菜单', 136 | subs: [ 137 | { 138 | key: '/app/extension/multiple/child/child', 139 | title: '多级菜单子子菜单', 140 | component: 'MultipleMenu', 141 | }, 142 | ], 143 | }, 144 | ], 145 | }, 146 | { 147 | key: '/app/extension/env', 148 | title: '环境配置', 149 | component: 'Env', 150 | }, 151 | ], 152 | }, 153 | ], 154 | others: [], // 非菜单相关路由 155 | }; 156 | 157 | export default menus; 158 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 叶子 on 2017/8/13. 3 | */ 4 | import React from 'react'; 5 | import { Route, Redirect, Switch } from 'react-router-dom'; 6 | import { useAlita } from 'redux-alita'; 7 | import umbrella from 'umbrella-storage'; 8 | import AllComponents from '../components'; 9 | import routesConfig, { IFMenuBase, IFMenu } from './config'; 10 | import { checkLogin } from '../utils'; 11 | import RouteWrapper from './RouteWrapper'; 12 | 13 | type CRouterProps = { 14 | auth: any; 15 | }; 16 | 17 | const CRouter = (props: CRouterProps) => { 18 | const { auth } = props; 19 | const [smenus] = useAlita({ smenus: null }, { light: true }); 20 | 21 | const getPermits = (): any[] | null => { 22 | return auth ? auth.permissions : null; 23 | }; 24 | const requireAuth = (permit: any, component: React.ReactElement) => { 25 | const permits = getPermits(); 26 | if (!permits || !permits.includes(permit)) return ; 27 | return component; 28 | }; 29 | const requireLogin = (component: React.ReactElement, permit: any) => { 30 | const permits = getPermits(); 31 | if (!checkLogin(permits)) { 32 | // 线上环境判断是否登录 33 | return ; 34 | } 35 | return permit ? requireAuth(permit, component) : component; 36 | }; 37 | const createMenu = (r: IFMenu) => { 38 | const route = (r: IFMenuBase) => { 39 | const Component = r.component && AllComponents[r.component]; 40 | return ( 41 | { 46 | // 重新包装组件 47 | const wrapper = ( 48 | 49 | ); 50 | return r.login ? wrapper : requireLogin(wrapper, r.requireAuth); 51 | }} 52 | /> 53 | ); 54 | }; 55 | 56 | const subRoute = (r: IFMenu): any => 57 | r.subs && r.subs.map((subR: IFMenu) => (subR.subs ? subRoute(subR) : route(subR))); 58 | 59 | return r.component ? route(r) : subRoute(r); 60 | }; 61 | const createRoute = (key: string) => routesConfig[key].map(createMenu); 62 | const getAsyncMenus = () => smenus || umbrella.getLocalStorage('smenus') || []; 63 | return ( 64 | 65 | {Object.keys(routesConfig).map((key) => createRoute(key))} 66 | {getAsyncMenus().map(createMenu)} 67 | } /> 68 | 69 | ); 70 | }; 71 | 72 | export default CRouter; 73 | -------------------------------------------------------------------------------- /src/service/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 叶子 on 2017/7/30. 3 | * 接口地址配置文件 4 | */ 5 | 6 | //easy-mock模拟数据接口地址 7 | const MOCK_API = 'https://react-admin-mock.now.sh/api'; 8 | export const MOCK_AUTH_ADMIN = MOCK_API + '/admin.js'; // 管理员权限接口 9 | export const MOCK_AUTH_VISITOR = MOCK_API + '/visitor.js'; // 访问权限接口 10 | /** 服务端异步菜单接口 */ 11 | export const MOCK_MENU = MOCK_API + '/menu.js'; 12 | 13 | // github授权 14 | export const GIT_OAUTH = 'https://github.com/login/oauth'; 15 | // github用户 16 | export const GIT_USER = 'https://api.github.com/user'; 17 | 18 | // bbc top news 19 | export const NEWS_BBC = 20 | 'https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=429904aa01f54a39a278a406acf50070'; 21 | -------------------------------------------------------------------------------- /src/service/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/16. 3 | */ 4 | import axios from 'axios'; 5 | import { get, post } from './tools'; 6 | import * as config from './config'; 7 | 8 | export const getBbcNews = () => get({ url: config.NEWS_BBC }); 9 | 10 | export const npmDependencies = () => 11 | axios 12 | .get('./npm.json') 13 | .then((res) => res.data) 14 | .catch((err) => console.log(err)); 15 | 16 | export const weibo = () => 17 | axios 18 | .get('./weibo.json') 19 | .then((res) => res.data) 20 | .catch((err) => console.log(err)); 21 | 22 | export const gitOauthLogin = () => 23 | get({ 24 | url: `${config.GIT_OAUTH}/authorize?client_id=792cdcd244e98dcd2dee&redirect_uri=http://localhost:3006/&scope=user&state=reactAdmin`, 25 | }); 26 | export const gitOauthToken = (code: string) => 27 | post({ 28 | url: `https://cors-anywhere.herokuapp.com/${config.GIT_OAUTH}/access_token`, 29 | data: { 30 | client_id: '792cdcd244e98dcd2dee', 31 | client_secret: '81c4ff9df390d482b7c8b214a55cf24bf1f53059', 32 | redirect_uri: 'http://localhost:3006/', 33 | state: 'reactAdmin', 34 | code, 35 | }, 36 | }); 37 | // {headers: {Accept: 'application/json'}} 38 | export const gitOauthInfo = (access_token: string) => 39 | get({ url: `${config.GIT_USER}access_token=${access_token}` }); 40 | 41 | // easy-mock数据交互 42 | // 管理员权限获取 43 | export const admin = () => get({ url: config.MOCK_AUTH_ADMIN }); 44 | // 访问权限获取 45 | export const guest = () => get({ url: config.MOCK_AUTH_VISITOR }); 46 | /** 获取服务端菜单 */ 47 | export const fetchMenu = () => get({ url: config.MOCK_MENU }); 48 | -------------------------------------------------------------------------------- /src/service/tools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 叶子 on 2017/7/30. 3 | * http通用工具函数 4 | */ 5 | import axios from 'axios'; 6 | import { message } from 'antd'; 7 | 8 | interface IFRequestParam { 9 | url: string; 10 | msg?: string; 11 | config?: any; 12 | data?: any; 13 | } 14 | /** 15 | * 公用get请求 16 | * @param url 接口地址 17 | * @param msg 接口异常提示 18 | * @param headers 接口所需header配置 19 | */ 20 | export const get = ({ url, msg = '接口异常', config }: IFRequestParam) => 21 | axios 22 | .get(url, config) 23 | .then((res) => res.data) 24 | .catch((err) => { 25 | console.log(err); 26 | message.warn(msg); 27 | }); 28 | 29 | /** 30 | * 公用post请求 31 | * @param url 接口地址 32 | * @param data 接口参数 33 | * @param msg 接口异常提示 34 | * @param headers 接口所需header配置 35 | */ 36 | export const post = ({ url, data, msg = '接口异常', config }: IFRequestParam) => 37 | axios 38 | .post(url, data, config) 39 | .then((res) => res.data) 40 | .catch((err) => { 41 | console.log(err); 42 | message.warn(msg); 43 | }); 44 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/style/antd/header.less: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 0; 3 | height: 65px; 4 | .ant-menu { 5 | background: transparent; 6 | color: @white; 7 | .ant-menu-item { 8 | &:hover { 9 | color: @white; 10 | } 11 | } 12 | } 13 | } 14 | .header__trigger { 15 | color: @white; 16 | } 17 | -------------------------------------------------------------------------------- /src/style/antd/index.less: -------------------------------------------------------------------------------- 1 | @import './variables.less'; 2 | @import './menu.less'; 3 | @import './utils.less'; 4 | @import './header.less'; 5 | @import './layout.less'; 6 | @import './reset.less'; 7 | -------------------------------------------------------------------------------- /src/style/antd/layout.less: -------------------------------------------------------------------------------- 1 | .ant-layout-content { 2 | min-height: auto; 3 | } 4 | 5 | .ant-layout { 6 | &.ant-layout-has-sider { 7 | &.app_layout-mobile { 8 | flex-direction: column; 9 | .ant-layout-content { 10 | margin: 0; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/style/antd/menu.less: -------------------------------------------------------------------------------- 1 | .ant-menu-root { 2 | &.ant-menu-inline, &.ant-menu-vertical { 3 | background: @primary-color; 4 | border-right: 1px solid @primary-color; 5 | color: @white; 6 | a { 7 | color: @white; 8 | } 9 | .ant-menu-submenu-selected, { 10 | color: @white; 11 | } 12 | .ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open, .ant-menu-submenu-active { 13 | color: @white; 14 | } 15 | .ant-menu-submenu-title { 16 | .ant-menu-submenu-arrow { 17 | &::before, &::after { 18 | background: @white; 19 | } 20 | } 21 | &:hover { 22 | color: @white; 23 | .ant-menu-submenu-arrow { 24 | &::before, &::after { 25 | background: @white; 26 | } 27 | } 28 | } 29 | } 30 | .ant-menu-submenu > .ant-menu { 31 | background-color: @primary-color-light; 32 | } 33 | .ant-menu-item > a:hover { 34 | color: @white; 35 | } 36 | } 37 | } 38 | .ant-menu-horizontal { 39 | > .ant-menu-item-selected { 40 | color: @white; 41 | } 42 | } 43 | 44 | .sider-custom { 45 | .ant-menu-submenu-title { 46 | color: @white; 47 | } 48 | } -------------------------------------------------------------------------------- /src/style/antd/reset.less: -------------------------------------------------------------------------------- 1 | /* 2 | * File: reset.less 3 | * Desc: 样式重写 4 | * File Created: 2020-04-12 23:08:16 5 | * Author: chenghao 6 | * ------ 7 | * Copyright 2020 - present, karakal 8 | */ 9 | -------------------------------------------------------------------------------- /src/style/antd/utils.less: -------------------------------------------------------------------------------- 1 | [class*=btn] { 2 | cursor: pointer 3 | } 4 | 5 | .bg--primary { 6 | background: @primary-color; 7 | } -------------------------------------------------------------------------------- /src/style/antd/variables.less: -------------------------------------------------------------------------------- 1 | @import "../../../node_modules/antd/lib/style/themes/default.less"; 2 | // 基础颜色 3 | @white: #ffffff; 4 | @primary-color: #313653; 5 | @primary-color-light: fade(lighten(@primary-color, 5%), 15%); 6 | // 顶部背景色 7 | @layout-header-background:@primary-color; 8 | // 左边菜单light颜色 9 | @layout-sider-background-light: #f9f9f9; 10 | // 字体颜色 11 | @text-color: #000000; 12 | @table-selected-row-bg: #fbfbfb; 13 | @primary-2: @primary-color-light; 14 | // 基础圆角 15 | @border-radius-base: 2px; 16 | // 输入框后缀背景色 17 | @input-addon-bg: @primary-color; 18 | 19 | -------------------------------------------------------------------------------- /src/style/app.less: -------------------------------------------------------------------------------- 1 | /* 2 | * File: app.less 3 | * Desc: 描述 4 | * File Created: 2020-07-26 18:27:37 5 | * Author: yezi 6 | * ------ 7 | * Copyright 2020 - present, yezi 8 | */ 9 | @prefix: app; 10 | 11 | .@{prefix} { 12 | &_layout { 13 | flex-direction: column; 14 | &_content { 15 | margin: 0 16px; 16 | overflow: initial; 17 | flex: 1 1 0; 18 | } 19 | &_foot { 20 | text-align: center; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/style/banner.less: -------------------------------------------------------------------------------- 1 | .banner-user { 2 | height: 200px; 3 | } 4 | .banner-user-elem { 5 | text-align: center; 6 | color: #fff; 7 | position: relative; 8 | overflow: hidden; 9 | .banner-user-title { 10 | font-size: 32px; 11 | top: 40%; 12 | } 13 | .banner-user-text { 14 | top: 40%; 15 | } 16 | } 17 | .banner-anim-elem { 18 | .bg { 19 | width: 100%; 20 | height: 100%; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | overflow: hidden; 25 | } 26 | } 27 | 28 | .custom-arrow-thumb{ 29 | height: 220px; 30 | .user-arrow { 31 | top: 50%; 32 | margin-top: -40px; 33 | .img-wrapper { 34 | width: 120px; 35 | height: 80px; 36 | float: left; 37 | position: relative; 38 | li { 39 | width: 100%; 40 | height: 100%; 41 | background-size: cover; 42 | background-position: center; 43 | position: absolute; 44 | } 45 | } 46 | .arrow { 47 | width: 20px; 48 | height: 80px; 49 | background: rgba(0, 0, 0, 0.3); 50 | position: relative; 51 | &:before, &:after { 52 | width: 2px; 53 | height: 15px; 54 | background: #fff; 55 | display: block; 56 | content: ' '; 57 | position: absolute; 58 | } 59 | } 60 | &.next { 61 | right: -120px; 62 | .arrow { 63 | float: left; 64 | &:before { 65 | -webkit-transform: rotate(-40deg); 66 | transform: rotate(-40deg); 67 | top: 28px; 68 | left: 10px; 69 | } 70 | &:after { 71 | -webkit-transform: rotate(40deg); 72 | transform: rotate(40deg); 73 | bottom: 27px; 74 | left: 10px; 75 | } 76 | } 77 | } 78 | &.prev { 79 | left: -120px; 80 | .arrow { 81 | float: right; 82 | &:before { 83 | -webkit-transform: rotate(40deg); 84 | transform: rotate(40deg); 85 | top: 28px; 86 | left: 8px; 87 | } 88 | &:after { 89 | -webkit-transform: rotate(-40deg); 90 | transform: rotate(-40deg); 91 | bottom: 27px; 92 | left: 8px; 93 | } 94 | } 95 | } 96 | } 97 | .user-thumb { 98 | overflow: hidden; 99 | background: rgba(255, 255, 255, 0.15); 100 | height: 40px; 101 | > span { 102 | width: 50px; 103 | height: 30px; 104 | margin: 5px; 105 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.15); 106 | -webkit-transition: background .3s; 107 | transition: background .3s; 108 | background: transparent; 109 | &.active { 110 | background: rgba(255, 255, 255, 0.45); 111 | } 112 | i { 113 | display: block; 114 | width: 46px; 115 | height: 26px; 116 | margin: 2px; 117 | background-size: cover; 118 | background-position: center; 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/style/button.less: -------------------------------------------------------------------------------- 1 | .ant-btn+.ant-btn { 2 | margin-left: 10px; 3 | } -------------------------------------------------------------------------------- /src/style/card.less: -------------------------------------------------------------------------------- 1 | .react-draggable, .cursor-move{ 2 | cursor: move; 3 | strong { 4 | background: #ddd; 5 | border: 1px solid #999; 6 | border-radius: 3px; 7 | display: block; 8 | margin-bottom: 10px; 9 | padding: 3px 5px; 10 | text-align: center; 11 | } 12 | } 13 | .no-cursor { 14 | cursor: auto; 15 | } 16 | .card-tool { 17 | position: absolute; 18 | right: 24px; 19 | top: 24px; 20 | } 21 | 22 | .list-group { 23 | .list-group-item { 24 | position: relative; 25 | display: block; 26 | margin-bottom: -1px; 27 | padding: 12px 16px; 28 | background: transparent; 29 | border: 1px solid #ddd; 30 | border-color: rgba(120, 130, 140, 0.065); 31 | border-width: 1px 0; 32 | &:first-child { 33 | border-top-width: 0; 34 | } 35 | &:last-child { 36 | border-bottom-width: 0; 37 | } 38 | } 39 | } 40 | .no-padding { 41 | .ant-card-body { 42 | padding: 0 !important; 43 | } 44 | } -------------------------------------------------------------------------------- /src/style/font/y6oxFxU60dYw9khW6q8jGw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/font/y6oxFxU60dYw9khW6q8jGw.woff2 -------------------------------------------------------------------------------- /src/style/global.less: -------------------------------------------------------------------------------- 1 | small { 2 | opacity: .6; 3 | } 4 | .text-muted{ 5 | opacity: .6; 6 | } 7 | .clear{ 8 | display: block; 9 | overflow: hidden; 10 | } 11 | .center { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | .y-center{ 17 | display: flex; 18 | align-items: center; 19 | } 20 | .block{ 21 | display: block; 22 | } 23 | .inline { 24 | display: inline; 25 | } 26 | .none{ 27 | display: none; 28 | } 29 | .b-white { 30 | border-color: #ffffff; 31 | } 32 | .w-full { 33 | width: 100%; 34 | } 35 | 36 | .w-auto { 37 | width: auto; 38 | } 39 | 40 | .h-auto { 41 | height: auto; 42 | } 43 | 44 | .h-full { 45 | height: 100%; 46 | } 47 | 48 | .h-v { 49 | height: 100vh; 50 | } 51 | 52 | .h-v-5 { 53 | height: 50vh; 54 | } 55 | 56 | 57 | .pull-left { 58 | float: left; 59 | } 60 | 61 | .pull-right { 62 | float: right; 63 | } 64 | 65 | .w-40 { 66 | width: 40px; 67 | height: 40px; 68 | line-height: 40px; 69 | display: inline-block; 70 | text-align: center; 71 | } -------------------------------------------------------------------------------- /src/style/icons.less: -------------------------------------------------------------------------------- 1 | ul.icons-list{ 2 | list-style: none; 3 | overflow: hidden; 4 | li{ 5 | float: left; 6 | width: 10%; 7 | text-align: center; 8 | list-style: none; 9 | cursor: pointer; 10 | height: 100px; 11 | transition: all .3s; 12 | background-color: #fff; 13 | &:hover{ 14 | background-color: #cccccc; 15 | color: #fff; 16 | } 17 | i{ 18 | margin: 16px 0 10px; 19 | } 20 | span{ 21 | display: block; 22 | text-align: center; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/style/img.less: -------------------------------------------------------------------------------- 1 | img { 2 | vertical-align: middle; 3 | } 4 | .img-responsive{ 5 | width: 100%; 6 | height: auto; 7 | } 8 | .img-circle { 9 | border-radius: 50%; 10 | } 11 | -------------------------------------------------------------------------------- /src/style/imgs/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/404.png -------------------------------------------------------------------------------- /src/style/imgs/b1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/b1.jpg -------------------------------------------------------------------------------- /src/style/imgs/beauty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/beauty.jpg -------------------------------------------------------------------------------- /src/style/imgs/installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/installer.png -------------------------------------------------------------------------------- /src/style/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/logo.png -------------------------------------------------------------------------------- /src/style/imgs/mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/mobile.gif -------------------------------------------------------------------------------- /src/style/imgs/spot_location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yezihaohao/react-admin/cfaaa12a98a5fbc5fc77a1a5ccb8a85c6a2839fd/src/style/imgs/spot_location.png -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'global'; 3 | @import 'scroll'; 4 | @import 'table'; 5 | @import 'login'; 6 | @import 'icons'; 7 | @import 'button'; 8 | @import 'modal'; 9 | @import 'menu'; 10 | @import 'banner'; 11 | @import 'card'; 12 | @import 'img'; 13 | @import 'utils-text'; 14 | @import 'utils-color'; 15 | @import 'utils-size'; 16 | @import 'utils-border'; 17 | @import 'utils-spacing'; 18 | @import 'app.less'; 19 | body { 20 | margin: 0; 21 | padding: 0; 22 | font-family: sans-serif; 23 | } 24 | 25 | #root { 26 | height: 100%; 27 | } 28 | .ant-layout { 29 | height: 100%; 30 | .logo { 31 | height: 32px; 32 | background: #ffffff; 33 | border-radius: 6px; 34 | margin: 16px; 35 | } 36 | .ant-layout-sider-collapsed { 37 | .anticon { 38 | font-size: 16px; 39 | // margin-left: 8px; 40 | } 41 | .nav-text { 42 | display: none; 43 | } 44 | .ant-menu-submenu-vertical > .ant-menu-submenu-title:after { 45 | display: none; 46 | } 47 | .ant-menu-dark:not(.ant-menu-inline) .ant-menu-submenu-open { 48 | color: inherit; 49 | } 50 | } 51 | p { 52 | margin: 10px 0 10px 0; 53 | } 54 | } 55 | .gutter-example { 56 | .ant-row { 57 | //margin-left: 0 !important; 58 | //margin-right: 0 !important; 59 | > div { 60 | background: transparent; 61 | border: 0; 62 | } 63 | } 64 | } 65 | .gutter-box { 66 | padding: 5px 0; 67 | } 68 | 69 | :global { 70 | .ant-card-head-title { 71 | font-size: 14px !important; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/style/login.less: -------------------------------------------------------------------------------- 1 | .login{ 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | background: #f3f3f3; 7 | .login-form{ 8 | width: 320px; 9 | height: 340px; 10 | padding: 36px; 11 | box-shadow: 0 0 100px rgba(0,0,0,.08); 12 | background: #fff; 13 | .login-logo{ 14 | text-align: center; 15 | height: 40px; 16 | line-height: 40px; 17 | cursor: pointer; 18 | margin-bottom: 24px; 19 | span { 20 | vertical-align: text-bottom; 21 | font-size: 16px; 22 | text-transform: uppercase; 23 | display: inline-block; 24 | } 25 | } 26 | } 27 | } 28 | .installer { 29 | width: 0; 30 | height: 0; 31 | border-style: solid; 32 | border-width: 0 100px 100px 0; 33 | border-color: transparent #313653 transparent transparent; 34 | position: fixed; 35 | top: 0; 36 | right: 0; 37 | } 38 | .installer__btn { 39 | width: 50px; 40 | height: 50px; 41 | transform: rotate(45deg); 42 | left: 50px; 43 | position: absolute; 44 | background: url('./imgs/installer.png') no-repeat; 45 | background-size: cover; 46 | } -------------------------------------------------------------------------------- /src/style/menu.less: -------------------------------------------------------------------------------- 1 | .ant-menu-dark { 2 | &.ant-menu-inline { 3 | .ant-menu-item-selected { 4 | background-color: #5f5f5f !important; 5 | } 6 | } 7 | } 8 | .custom-trigger { 9 | font-size: 18px; 10 | line-height: 64px; 11 | padding: 0 16px; 12 | cursor: pointer; 13 | transition: color .3s; 14 | } 15 | .ant-layout-sider-collapsed { 16 | overflow-y: initial !important; 17 | } 18 | .avatar { 19 | position: relative; 20 | display: inline-block; 21 | width: 40px; 22 | line-height: 1; 23 | border-radius: 500px; 24 | white-space: nowrap; 25 | font-weight: bold; 26 | i { 27 | position: absolute; 28 | left: 0; 29 | top: 0; 30 | width: 10px; 31 | height: 10px; 32 | margin: 1px; 33 | border-width: 2px; 34 | border-style: solid; 35 | border-radius: 100%; 36 | &.bottom { 37 | left: auto; 38 | top: auto; 39 | bottom: 0; 40 | right: 0; 41 | } 42 | &.on { 43 | background-color: #6cc788; 44 | } 45 | } 46 | img { 47 | border-radius: 500px; 48 | width: 100%; 49 | } 50 | } 51 | .switcher { 52 | z-index: 1050; 53 | position: fixed; 54 | top: 78px; 55 | right: -240px; 56 | width: 240px; 57 | transition: right 0.2s ease; 58 | border: 1px solid rgba(120, 120, 120, 0.1); 59 | background-clip: padding-box; 60 | &.active { 61 | right: -2px; 62 | } 63 | .sw-btn { 64 | position: absolute; 65 | left: -43px; 66 | top: -1px; 67 | padding: 10px 15px; 68 | z-index: 1045; 69 | border: 1px solid rgba(120, 120, 120, 0.1); 70 | border-right-width: 0; 71 | background-clip: padding-box; 72 | } 73 | } -------------------------------------------------------------------------------- /src/style/modal.less: -------------------------------------------------------------------------------- 1 | .vertical-center-modal { 2 | text-align: center; 3 | white-space: nowrap; 4 | &:before { 5 | content: ''; 6 | display: inline-block; 7 | height: 100%; 8 | vertical-align: middle; 9 | width: 0; 10 | } 11 | .ant-modal { 12 | display: inline-block; 13 | vertical-align: middle; 14 | top: 0; 15 | text-align: left; 16 | } 17 | } 18 | /* 19 | // Use flex which not working in IE 20 | .vertical-center-modal { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | .vertical-center-modal .ant-modal { 27 | top: 0; 28 | } 29 | */ -------------------------------------------------------------------------------- /src/style/scroll.less: -------------------------------------------------------------------------------- 1 | //美化webkit内核滚动条 2 | ::-webkit-scrollbar { 3 | width: 8px; 4 | height: 8px; 5 | } 6 | ::-webkit-scrollbar-thumb { 7 | background-color: #777; 8 | } -------------------------------------------------------------------------------- /src/style/table.less: -------------------------------------------------------------------------------- 1 | .table-operations { 2 | margin-bottom: 16px; 3 | > button { 4 | margin-right: 8px; 5 | } 6 | } 7 | 8 | .editable-row-text { 9 | padding: 5px; 10 | } 11 | 12 | .editable-row-operations a { 13 | margin-right: 8px; 14 | } -------------------------------------------------------------------------------- /src/style/theme/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/5/6. 3 | */ 4 | import themeinfo from './theme-info.json'; 5 | import themegrey from './theme-grey.json'; 6 | import themedanger from './theme-danger.json'; 7 | import themewarn from './theme-warn.json'; 8 | 9 | export default { themeinfo, themegrey, themedanger, themewarn }; 10 | -------------------------------------------------------------------------------- /src/style/theme/theme-danger.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "background": "#f44455" 4 | } 5 | } -------------------------------------------------------------------------------- /src/style/theme/theme-grey.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "background": "#001529" 4 | } 5 | } -------------------------------------------------------------------------------- /src/style/theme/theme-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "background": "#6887ff" 4 | } 5 | } -------------------------------------------------------------------------------- /src/style/theme/theme-warn.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "background": "#fcc100" 4 | } 5 | } -------------------------------------------------------------------------------- /src/style/utils-border.less: -------------------------------------------------------------------------------- 1 | .b-a { 2 | border: 1px solid @border-color; 3 | } -------------------------------------------------------------------------------- /src/style/utils-color.less: -------------------------------------------------------------------------------- 1 | .color-variant(@bg, @color) { 2 | color: @color; 3 | background-color: @bg; 4 | } 5 | .dark-white { 6 | color: @dark; 7 | background-color: @white; 8 | } 9 | .min-black { 10 | .color-variant(@min-black, @black-color) 11 | } 12 | .black { 13 | .color-variant(@black, @black-color); 14 | } 15 | 16 | .dark { 17 | .color-variant(@dark, @dark-color); 18 | } 19 | 20 | .grey { 21 | .color-variant(@grey, @grey-color); 22 | } 23 | 24 | .primary { 25 | .color-variant(@primary, @primary-color); 26 | } 27 | 28 | .info { 29 | .color-variant(@info, @info-color); 30 | } 31 | .warn { 32 | .color-variant(@warn, @warn-color); 33 | } 34 | .danger { 35 | .color-variant(@danger, @danger-color) 36 | } -------------------------------------------------------------------------------- /src/style/utils-size.less: -------------------------------------------------------------------------------- 1 | .w-8{ 2 | width: 8px; 3 | height: 8px; 4 | display: inline-block; 5 | } 6 | .w-16{ 7 | width: 16px; 8 | height: 16px; 9 | display: inline-block; 10 | } 11 | .w-20{ 12 | width: 20px; 13 | height: 20px; 14 | display: inline-block; 15 | } 16 | .w-24{ 17 | width: 24px; 18 | height: 24px; 19 | display: inline-block; 20 | text-align: center; 21 | } 22 | .w-32{ 23 | width: 32px; 24 | height: 32px; 25 | line-height: 32px; 26 | display: inline-block; 27 | text-align: center; 28 | } 29 | .w-40{ 30 | width: 40px; 31 | height: 40px; 32 | line-height: 40px; 33 | display: inline-block; 34 | text-align: center; 35 | } 36 | .w-48{ 37 | width: 48px; 38 | height: 48px; 39 | line-height: 48px; 40 | display: inline-block; 41 | text-align: center; 42 | } 43 | .w-56{ 44 | width: 56px; 45 | height: 56px; 46 | line-height: 56px; 47 | display: inline-block; 48 | text-align: center; 49 | } -------------------------------------------------------------------------------- /src/style/utils-spacing.less: -------------------------------------------------------------------------------- 1 | .pb-s { padding-bottom: @spacer * 0.5 !important; } 2 | .pb-m { padding-bottom: @spacer * 1 !important; } 3 | .pb-l { padding-bottom: @spacer * 2 !important; } 4 | .pa-s { padding: @spacer * 0.5 !important; } 5 | .pa-m { padding: @spacer * 1 !important; } 6 | .pa-l { padding: @spacer * 2 !important; } 7 | 8 | .mr-s { margin-right: @spacer * 0.5 !important; } 9 | .mr-m { margin-right: @spacer * 1 !important; } 10 | .mr-l { margin-right: @spacer * 2 !important; } 11 | .mb-s { margin-bottom: @spacer * 0.5 !important; } 12 | .mb-m { margin-bottom: @spacer * 1 !important; } 13 | .mb-l { margin-bottom: @spacer * 2 !important; } 14 | -------------------------------------------------------------------------------- /src/style/utils-text.less: -------------------------------------------------------------------------------- 1 | .text{ 2 | font-size: 1rem; 3 | } 4 | .text-2x{ 5 | font-size: 2rem; 6 | } 7 | .text-3x{ 8 | font-size: 3rem; 9 | } 10 | .text-4x{ 11 | font-size: 4rem; 12 | } 13 | 14 | .text-center { 15 | text-align: center; 16 | } 17 | 18 | .text-left { 19 | text-align: left; 20 | } 21 | 22 | .text-right { 23 | text-align: right; 24 | } 25 | 26 | .text-danger, 27 | .text-danger-hover a:hover { 28 | color: #f44455 !important; 29 | } 30 | .text-dark, 31 | .text-dark-hover a:hover { 32 | color: #2e3e4e !important; 33 | } 34 | .text-info, 35 | .text-info-hover a:hover { 36 | color: #6887ff !important; 37 | } 38 | .text-success, 39 | .text-success-hover a:hover { 40 | color: #6cc788 !important; 41 | } 42 | .text-blue, 43 | .text-blue-hover a:hover { 44 | color: #2196f3 !important; } 45 | -------------------------------------------------------------------------------- /src/style/variables.less: -------------------------------------------------------------------------------- 1 | @full-black: rgba(0, 0, 0, 1); 2 | @dark-black: rgba(0, 0, 0, 0.87); 3 | @light-black: rgba(0, 0, 0, 0.54); 4 | @min-black: rgba(0, 0, 0, 0.065); 5 | 6 | @full-white: rgba(255, 255, 255, 1); 7 | @dark-white: rgba(255, 255, 255, 0.87); 8 | @light-white: rgba(255, 255, 255, 0.54); 9 | @min-white: rgba(255, 255, 255, 0.1); 10 | 11 | @primary: #0cc2aa; 12 | @accent: #a88add; 13 | @warn: #fcc100; 14 | 15 | @info: #6887ff; 16 | @success: #6cc788; 17 | @warning: #f77a99; 18 | @danger: #f44455; 19 | 20 | @light: #f8f8f8; 21 | @grey: #424242; 22 | @dark: #2e3e4e; 23 | @black: #2a2b3c; 24 | @white: #ffffff; 25 | 26 | @primary-color: @dark-white; 27 | @accent-color: @dark-white; 28 | @warn-color: @dark-white; 29 | @success-color: @dark-white; 30 | @info-color: @dark-white; 31 | @warning-color: @dark-white; 32 | @danger-color: @dark-white; 33 | @light-color: @dark-black; 34 | @grey-color: @dark-white; 35 | @dark-color: @dark-white; 36 | @black-color: @dark-white; 37 | 38 | @border-color: rgba(120, 130, 140, 0.13); 39 | 40 | @spacer: 1rem; -------------------------------------------------------------------------------- /src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * File: hooks.ts 3 | * Desc: 自定义hooks 4 | * File Created: 2020-08-24 22:45:40 5 | * Author: yezi 6 | * ------ 7 | * Copyright 2020 - present, yezi 8 | */ 9 | import { useState } from 'react'; 10 | 11 | interface ITurn { 12 | turnOn: () => void; 13 | turnOff: () => void; 14 | setSwitcher: React.Dispatch>; 15 | } 16 | /** 17 | * 布尔开关 18 | * @param init 19 | */ 20 | export function useSwitch(init: boolean = false): [boolean, ITurn] { 21 | const [switcher, setSwitcher] = useState(init); 22 | const turnOn = () => setSwitcher(true); 23 | const turnOff = () => setSwitcher(false); 24 | return [switcher, { turnOn, turnOff, setSwitcher }]; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hao.cheng on 2017/4/28. 3 | */ 4 | import queryString from 'query-string'; 5 | /** 6 | * 获取URL参数 7 | */ 8 | export function parseQuery() { 9 | return queryString.parseUrl(window.location.href).query; 10 | } 11 | 12 | /** 13 | * 校验是否登录 14 | * @param permits 15 | */ 16 | export const checkLogin = (permits: any): boolean => 17 | (process.env.NODE_ENV === 'production' && !!permits) || process.env.NODE_ENV === 'development'; 18 | -------------------------------------------------------------------------------- /theme.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { generateTheme } = require('antd-theme-generator'); 3 | 4 | const options = { 5 | antDir: path.join(__dirname, './node_modules/antd'), 6 | stylesDir: path.join(__dirname, './src/style/antd'), 7 | varFile: path.join(__dirname, './src/style/antd/variables.less'), 8 | mainLessFile: path.join(__dirname, './src/style/antd/index.less'), 9 | indexFileName: 'index.html', 10 | outputFilePath: path.join(__dirname, './public/theme.less'), 11 | } 12 | 13 | generateTheme(options).then(less => { 14 | console.log('Theme generated successfully'); 15 | }).catch(error => { 16 | console.log('Error', error); 17 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------