├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── build ├── build.js ├── webpack.base.config.js ├── webpack.dev.config.js └── webpack.pro.config.js ├── dev.mock.js ├── mock └── .gitkeep ├── package.json ├── public └── index.html ├── src ├── assets │ └── yay.jpg ├── common │ ├── menu.ts │ └── router.ts ├── components │ ├── Exception │ │ ├── index.less │ │ ├── index.tsx │ │ └── typeconfig.ts │ ├── GlobalHeader │ │ ├── index.less │ │ └── index.tsx │ ├── PageHeader │ │ └── index.tsx │ └── SideMenu │ │ ├── index.less │ │ └── index.tsx ├── hoc │ └── table.tsx ├── hooks │ └── useTable.ts ├── index.less ├── index.ts ├── layouts │ ├── BasicLayout.tsx │ ├── UserLayout.less │ └── UserLayout.tsx ├── models │ ├── connect.ts │ ├── global.ts │ ├── goods.ts │ ├── login.ts │ └── order.ts ├── router.tsx ├── routes │ ├── Dashborad │ │ ├── index.tsx │ │ └── styles.css │ ├── Exception │ │ ├── 403.tsx │ │ └── 404.tsx │ ├── GoodsList │ │ └── GoodsList.tsx │ ├── IndexPage.css │ ├── TableList │ │ ├── MyTable.tsx │ │ └── TableList.tsx │ └── User │ │ ├── Login.less │ │ └── Login.tsx ├── services │ └── api.ts ├── type.d.ts └── utils │ ├── request.ts │ └── utils.ts ├── static └── logo.svg ├── theme.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime", 5 | "@babel/plugin-syntax-dynamic-import", 6 | ["@babel/plugin-proposal-decorators", { 7 | "legacy": true 8 | }], 9 | ["@babel/plugin-proposal-class-properties", {"loose": true}], 10 | ["import", { 11 | "libraryName": "antd", 12 | "libraryDirectory": "es", 13 | "style": true // `style: true` 会加载 less 文件 14 | }] 15 | ] 16 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": [], 10 | "settings": { 11 | "react": { 12 | "pragma": "React", 13 | "version": "detect" 14 | } 15 | }, 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "experimentalObjectRestSpread": true, 19 | "jsx": true 20 | }, 21 | "sourceType": "module" 22 | }, 23 | "plugins": [ 24 | "@typescript-eslint", 25 | "react", 26 | "react-hooks" 27 | ], 28 | "rules": { 29 | "react-hooks/rules-of-hooks": "error", 30 | "react-hooks/exhaustive-deps": "error", 31 | "indent": [ 32 | "error", 33 | 2 34 | ], 35 | "linebreak-style": [ 36 | "error", 37 | "unix" 38 | ], 39 | "quotes": [ 40 | "error", 41 | "single" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # production 7 | /dist 8 | 9 | package-lock.json 10 | 11 | .idea 12 | 13 | dist.rar 14 | 15 | # misc 16 | .DS_Store 17 | npm-debug.log* 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # antd-admin 2 | 基于antd的中后台管理系统模板 3 | 4 | ``` 5 | git clone https://github.com/Chechengyi/antd-admin.git 6 | ``` 7 | ```bash 8 | npm install 9 | ``` 10 | ```bash 11 | npm start 12 | ``` 13 | 14 | ## 简介 15 | 参照`ant design pro`,基于 `ant design` UI组件库,以及 `dva` 的中后台管理系统模板。网络请求使用`axios`封装,项目使用 `webpack4` 构建, 16 | 配置随心所欲。模板内容少,只提供了一个两个表格搜索列表的列子。后续开发无包袱,不用为了删文件却不知道哪使用了这个文件而头痛。 17 | 项目增加了 `typescript`支持。 18 | 19 | ## 权限菜单建议 20 | 做中后台管理系统绕不开的一点就是权限菜单控制。这里说一下我的做法。 21 | 22 | 给资源表中的菜单权限加上一个菜单标识,对应到本项目中 `menu.ts` 文件夹下的菜单那里。既要做权限控制,肯定要向后端询问一个查询用户权限信息的接口。我会将这组权限分为 `菜单权限` 和 `按钮权限` 用 `redux` 管理。然后在 `SideMenu` 就是渲染左侧菜单那里更改一下渲染逻辑。如果渲染的菜单项具有权限标识的话就看看当前用户的菜单权限中是否具有该菜单权限,如果当前菜单没有权限标识就直接渲染。 23 | 24 | 当然这样做只控制了左侧菜单的渲染,当用户直接输入地址进入一个他本该访问不了的页面时,还是能访问。所以为了更好的做到权限控制,还需要从路由处下手。内容页路由逻辑是在 `layouts` 文件夹下的 `BasicLayout.tsx` 中的。同样在这个文件夹中获取用户的菜单权限,然后在渲染的时候使用 `Route` 的 `render` 属性去渲染, 这里我是单独写了一个权限认证组件。 25 | 26 | 27 | 28 | ```jsx 29 | { 30 | getRoutes(match.path, routerData).map(item => ( 31 | { 37 | if ( !this.pathAuto[item.path] ) { 38 | return 39 | } 40 | return ( 41 | 42 | 43 | 44 | ) 45 | }} 46 | /> 47 | )) 48 | } 49 | 50 | 51 | const PermissionPage: React.FC = props=> { 52 | return ( 53 | 54 | {props.menuMap[props.permission]? props.children : props.reNull? null: } 55 | 56 | ) 57 | }; 58 | ``` 59 | 60 | 61 | 62 | 其实权限控制这一块做起来很简单。。。 实现方法也绝不止这一种,这里只是说说的我的做法,如果有人使用了我的这个模板,然后刚好权限控制这块有疑问的话,可以问我。 -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | 4 | process.env.NODE_ENV = 'production'; 5 | // "build": "webpack --config=webpack.pro.config --mode production" 6 | const webpack = require('webpack'); 7 | const rm = require('rimraf'); 8 | const webpackConfig = require('./webpack.pro.config'); 9 | 10 | // 每次打包之前删除掉之前的打包输出文件 11 | rm('../dist', function (err) { 12 | if (err) throw err; 13 | webpack(webpackConfig, function(err, stats){ 14 | process.stdout.write(stats.toString({ 15 | colors: true, 16 | modules: false, 17 | children: true, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 18 | chunks: false, 19 | chunkModules: false 20 | }) + '\n\n'); 21 | 22 | if (stats.hasErrors()) { 23 | console.log('打包失败'); 24 | process.exit(1) 25 | } 26 | }) 27 | }); 28 | 29 | /* eslint-disable no-new */ 30 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable */ 3 | 4 | const path = require('path'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const themeColor = require('../theme'); 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | 10 | module.exports = { 11 | entry: { 12 | index: './src/index.ts', 13 | vendor: [ 14 | 'react', 15 | 'react-dom' 16 | ] 17 | }, 18 | // output: { 19 | // path: path.resolve(__dirname, 'dist'), 20 | // filename: '[name].js', 21 | // publicPath: '/' 22 | // }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /(\.tsx|\.ts)$/, 27 | use: ['babel-loader', 'ts-loader'] 28 | }, 29 | { 30 | test: /\.js|\.jsx?/, // 支持 js 和 jsx 31 | include: [ 32 | path.resolve(__dirname, '../src'), // src 目录下的才需要经过 babel-loader 处理 33 | ], 34 | loader: 'babel-loader', 35 | }, 36 | { // 正常网页中的样式处理 37 | test: /\.css$/, 38 | use: [ 39 | 'style-loader', 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | importLoaders: 1, 44 | modules: true 45 | }, 46 | }], 47 | exclude: /node_modules/, 48 | }, 49 | // 正常网页中的less文件样式处理 50 | { 51 | test: /\.less$/, 52 | use: ['style-loader', 53 | { 54 | loader: 'css-loader', 55 | options: { 56 | importLoaders: 1, 57 | modules: true, 58 | }, 59 | }, 60 | { 61 | loader: 'less-loader', 62 | options: { 63 | sourceMap: true, 64 | javascriptEnabled: true, 65 | }, 66 | } 67 | ], 68 | exclude: /node_modules/, 69 | }, 70 | { 71 | test: /\.less$/, 72 | use: ['style-loader', 73 | { 74 | loader: 'css-loader', 75 | options: { 76 | importLoaders: 1, 77 | }, 78 | }, 79 | { 80 | loader: 'less-loader', 81 | options: { 82 | javascriptEnabled: true, 83 | modifyVars: themeColor 84 | }, 85 | }], 86 | include: /node_modules/, 87 | } 88 | ], 89 | }, 90 | 91 | // 代码模块路径解析的配置 92 | resolve: { 93 | modules: [ 94 | "node_modules", 95 | path.resolve(__dirname, '../src') 96 | ], 97 | extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".tsx", ".ts", ".d.ts"], 98 | alias: { 99 | "@models": path.resolve(__dirname, '../src/models'), 100 | "@components": path.resolve(__dirname, '../src/components'), 101 | "@services": path.resolve(__dirname, '../src/services'), 102 | "@hoc": path.resolve(__dirname, '../src/hoc'), 103 | "@hooks": path.resolve(__dirname, '../src/hooks'), 104 | "@utils": path.resolve(__dirname, '../src/utils'), 105 | } 106 | }, 107 | 108 | plugins: [ 109 | new HtmlWebpackPlugin({ 110 | filename: 'index.html', 111 | template: './public/index.html' 112 | }), 113 | new ExtractTextPlugin('index.css'), 114 | new CopyWebpackPlugin([ 115 | { 116 | from: path.resolve(__dirname, '../static'), 117 | to: 'static', 118 | ignore: ['.*'] 119 | } 120 | ]) 121 | ], 122 | optimization: { 123 | splitChunks: { 124 | cacheGroups: { 125 | vendor: { 126 | name: 'vendor', 127 | chunks: "initial", 128 | minChunks: 2 129 | } 130 | } 131 | } 132 | }, 133 | }; 134 | 135 | /* eslint-disable no-new */ 136 | -------------------------------------------------------------------------------- /build/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable */ 4 | 5 | const path = require('path'); 6 | const merge = require('webpack-merge'); 7 | const commenConfig = require('./webpack.base.config'); 8 | const webpack = require('webpack'); 9 | const mockUrlObj = require('../dev.mock'); 10 | const bodyParser = require('body-parser'); 11 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); 12 | 13 | const port = 8080; 14 | 15 | 16 | const devWebpackConfig = merge(commenConfig, { 17 | mode: "development", 18 | output: { 19 | path: path.resolve(__dirname, '../dist'), 20 | filename: '[name].js', 21 | publicPath: '/' 22 | }, 23 | devtool: 'inline-source-map', 24 | plugins: [ 25 | ...commenConfig.plugins, 26 | //开启HMR(热替换功能,替换更新部分,不重载页面!) 相当于在命令行加 --hot 27 | new webpack.HotModuleReplacementPlugin() 28 | ], 29 | devServer: { 30 | host: 'localhost', 31 | port: port, 32 | open: true, 33 | proxy: {}, // 配置代理 34 | contentBase: path.resolve(__dirname, 'dist'), 35 | historyApiFallback: true, 36 | publicPath: '/', 37 | before(app) { 38 | // 返回模拟请求数据 39 | Object.keys(mockUrlObj).forEach((key) => { 40 | const [type, url] = key.split(' '); 41 | app.use(url, bodyParser.json()); 42 | if (type === 'GET') { 43 | app.get(url, mockUrlObj[key]); 44 | } else if (type === 'POST') { 45 | app.post(url, mockUrlObj[key]); 46 | } 47 | }); 48 | }, 49 | disableHostCheck: true, // 解决内网穿透之后 invalid Header 问题 50 | quiet: true, // 使用webpack-server 清除输出的打包信息 51 | // hot: true, 52 | // hotOnly:true 53 | } 54 | }); 55 | 56 | module.exports = new Promise( (resolve, reject)=> { 57 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 58 | compilationSuccessInfo: { 59 | messages: [`项目运行在${port}端口`] 60 | // clearConsole: true 61 | } 62 | })); 63 | resolve(devWebpackConfig) 64 | }); 65 | 66 | /* eslint-disable no-new */ 67 | -------------------------------------------------------------------------------- /build/webpack.pro.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable */ 4 | 5 | const path = require('path'); 6 | const merge = require('webpack-merge'); 7 | const commonConfig = require('./webpack.base.config') 8 | 9 | module.exports = merge(commonConfig, { 10 | output: { 11 | path: path.resolve(__dirname, '../dist'), 12 | filename: '[name].[contenthash].js', 13 | publicPath: '/' 14 | }, 15 | }); 16 | 17 | /* eslint-disable no-new */ 18 | -------------------------------------------------------------------------------- /dev.mock.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 'POST /api/login': (req, res) => { 4 | const { username, password } = req.body 5 | if (username === 'admin' && password === '000000') { 6 | res.send({ 7 | code: 'OK' 8 | }) 9 | } else { 10 | res.send({ 11 | code: 'ERR' 12 | }) 13 | } 14 | }, 15 | 'GET /api/getOrder': (req, res) => { 16 | const { page, num } = req.query 17 | let start = (page - 1) * num 18 | let end = page * num 19 | let arr = [] 20 | for (start; start < end; start++) { 21 | let obj = { 22 | id: start + 1, 23 | name: '订单' + (start + 1), 24 | } 25 | arr.push(obj) 26 | } 27 | res.send(arr) 28 | }, 29 | 'GET /api/getGoods': (req, res) => { 30 | let { page, num, goodsName } = req.query 31 | let start = (page - 1) * num 32 | let end = page * num 33 | let arr = [] 34 | goodsName = goodsName ? goodsName.slice(0, 7) : '商品' 35 | for (start; start < end; start++) { 36 | let obj = { 37 | id: start + 1, 38 | name: goodsName + (start + 1), 39 | img: 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2560458309,2660522945&fm=27&gp=0.jpg', 40 | desc: '这是好吃的好看的好玩的,苹果' 41 | } 42 | arr.push(obj) 43 | } 44 | res.send(arr) 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chechengyi/antd-admin/5404e649ec389131dbc21615b92db69559e0c8f0/mock/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "webpack-dev-server --config=build/webpack.dev.config --mode development", 5 | "start": "npm run dev", 6 | "build": "node build/build.js" 7 | }, 8 | "dependencies": { 9 | "@ant-design/icons": "^4.2.1", 10 | "@typescript-eslint/eslint-plugin": "^2.8.0", 11 | "antd": "^4.x.x", 12 | "axios": "^0.19.0", 13 | "dva": "^2.4.1", 14 | "eslint": "^6.7.0", 15 | "immutable": "^4.0.0-rc.12", 16 | "react": "^16.12.0", 17 | "react-document-title": "^2.0.3", 18 | "react-dom": "^16.12.0" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.7.4", 22 | "@babel/plugin-proposal-class-properties": "^7.7.4", 23 | "@babel/plugin-proposal-decorators": "^7.7.4", 24 | "@babel/plugin-syntax-dynamic-import": "^7.7.4", 25 | "@babel/plugin-transform-runtime": "^7.7.4", 26 | "@babel/preset-env": "^7.7.4", 27 | "@babel/preset-react": "^7.7.4", 28 | "@types/node": "^12.12.12", 29 | "@typescript-eslint/parser": "^2.8.0", 30 | "babel-loader": "^8.0.6", 31 | "babel-plugin-import": "^1.12.2", 32 | "copy-webpack-plugin": "^5.0.5", 33 | "css-loader": "^2.1.1", 34 | "eslint-plugin-react": "^7.16.0", 35 | "eslint-plugin-react-hooks": "^2.3.0", 36 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 37 | "friendly-errors-webpack-plugin": "^1.7.0", 38 | "html-webpack-plugin": "^3.2.0", 39 | "less": "^3.10.3", 40 | "less-loader": "^4.1.0", 41 | "mini-css-extract-plugin": "^0.6.0", 42 | "style-loader": "^0.23.1", 43 | "ts-loader": "^6.2.1", 44 | "typescript": "^3.7.2", 45 | "webpack": "^4.41.2", 46 | "webpack-cli": "^3.3.10", 47 | "webpack-dev-server": "^3.9.0", 48 | "webpack-merge": "^4.2.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dva Demo 7 | 8 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chechengyi/antd-admin/5404e649ec389131dbc21615b92db69559e0c8f0/src/assets/yay.jpg -------------------------------------------------------------------------------- /src/common/menu.ts: -------------------------------------------------------------------------------- 1 | export interface MenuDataItem { 2 | name: string; // 菜单名字 3 | icon?: string; 4 | path: string; 5 | children?: Array; 6 | } 7 | 8 | const menuData: MenuDataItem[] = [ 9 | { 10 | name: '首页', 11 | icon: 'PieChartOutlined', 12 | path: 'cont/dashborad' 13 | }, 14 | { 15 | name: '列表一', 16 | icon: 'OrderedListOutlined', 17 | path: 'cont/list', 18 | children: [ 19 | { 20 | name: '查询表格', 21 | path: 'one', 22 | icon: 'BlockOutlined' 23 | }, 24 | { 25 | name: '商品列表', 26 | path: 'two' 27 | }, 28 | { 29 | name: '二级菜单', 30 | path: 'menu', 31 | children: [ 32 | { 33 | name: '三级菜单_a', 34 | path: 'one' 35 | }, 36 | { 37 | name: '三级菜单_b', 38 | path: 'two' 39 | } 40 | ] 41 | } 42 | ] 43 | }, 44 | { 45 | name: '列表二', 46 | icon: 'UnorderedListOutlined', 47 | path: 'cont/two', 48 | children: [{ 49 | name: '选项1', 50 | path: 'one' 51 | }, { 52 | name: '选项2', 53 | path: 'two' 54 | }] 55 | } 56 | ]; 57 | 58 | function formatter(data, parentPath = '') { 59 | const list = [] 60 | data.forEach(item => { 61 | if (item.children) { 62 | list.push({ 63 | ...item, 64 | path: `${parentPath}${item.path}`, 65 | children: formatter(item.children, `${parentPath}${item.path}/`) 66 | }) 67 | } else { 68 | list.push({ 69 | ...item, 70 | path: `${parentPath}${item.path}`, 71 | }); 72 | } 73 | }) 74 | 75 | return list 76 | } 77 | 78 | export const getMenuData = () => formatter(menuData) 79 | -------------------------------------------------------------------------------- /src/common/router.ts: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | import dynamic from 'dva/dynamic' 3 | import { getMenuData } from './menu' 4 | 5 | // wrapper of dynamic 6 | const dynamicWrapper = (app, models, component) => (dynamic as any)({ 7 | app, 8 | models: () => models.map(m => import(`../models/${m}`)), 9 | component 10 | }); 11 | 12 | function getFlatMenuData(menus) { 13 | let keys = {}; 14 | menus.forEach((item) => { 15 | if (item.children) { 16 | keys[item.path] = item.name; 17 | keys = { ...keys, ...getFlatMenuData(item.children) }; 18 | } else { 19 | keys[item.path] = item.name; 20 | } 21 | }); 22 | return keys; 23 | } 24 | 25 | export const getRouterData = app => { 26 | const routerData = { 27 | '/user': { 28 | component: dynamicWrapper(app, [], ()=>import('../layouts/UserLayout')) 29 | }, 30 | '/user/login': { 31 | component: dynamicWrapper(app, ['login'], ()=>import('../routes/User/Login')) 32 | }, 33 | '/cont': { 34 | component: dynamicWrapper(app, ['login'], ()=>import('../layouts/BasicLayout')) 35 | }, 36 | '/cont/dashborad': { 37 | component: dynamicWrapper(app, [], ()=>import('../routes/Dashborad')) 38 | }, 39 | '/cont/list/one': { 40 | component: dynamicWrapper(app, ['order'], ()=>import('../routes/TableList/TableList')) 41 | }, 42 | '/cont/list/two': { 43 | component: dynamicWrapper(app, ['goods'], ()=>import('../routes/GoodsList/GoodsList')) 44 | } 45 | }; 46 | const routerDataWithName = {}; 47 | const menuData = getFlatMenuData(getMenuData()); 48 | Object.keys(routerData).forEach(item=> { 49 | routerDataWithName[item] = { 50 | ...routerData[item], 51 | name: routerData[item].name || menuData[item.replace(/^\//, '')], 52 | } 53 | }); 54 | return routerDataWithName 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/Exception/index.less: -------------------------------------------------------------------------------- 1 | .warp { 2 | min-height: 400px; 3 | margin-top: 20px; 4 | text-align: center; 5 | padding-top: 50px; 6 | font-size: 40px; 7 | font-weight: 600; 8 | } 9 | .exceptionImg { 10 | width: 250px 11 | } -------------------------------------------------------------------------------- /src/components/Exception/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.less' 3 | import { typeConfig } from './typeconfig' 4 | 5 | export default ({type}) => { 6 | type = typeConfig[type]; 7 | return ( 8 |
9 |

{type.desc}

10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Exception/typeconfig.ts: -------------------------------------------------------------------------------- 1 | type TypeConfig = { 2 | [name: string]: { 3 | img: string; 4 | desc: string; 5 | title: string 6 | } 7 | } 8 | 9 | export const typeConfig: TypeConfig = { 10 | 404: { 11 | img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg', 12 | desc: '404,抱歉,你访问的页面不存在', 13 | title: '404' 14 | }, 15 | 403: { 16 | img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg', 17 | desc: '403, 您没有权限访问此页面', 18 | title: '403' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | .headerWarp { 2 | background-color: #fff; 3 | padding: 0; 4 | box-shadow: 0 1px 5px #e2e2e2; 5 | z-index:5; 6 | position: relative; 7 | .locationInfo { 8 | position: absolute; 9 | left: 20px; 10 | top: 50%; 11 | transform: translateY(-50%); 12 | display: flex; 13 | flex-direction: row; 14 | align-items: center; 15 | .collapsedBtnBox { 16 | margin-right: 20px; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | } 21 | } 22 | } 23 | .logo { 24 | margin-left: 20px; 25 | font-size: 70px; 26 | color: red; 27 | } 28 | 29 | i.trigger { 30 | font-size: 20px; 31 | line-height: 64px; 32 | cursor: pointer; 33 | transition: all .3s, padding 0s; 34 | padding: 0 24px; 35 | &:hover { 36 | background: #f8f8f8; 37 | } 38 | } 39 | .rightMenu { 40 | float: right; 41 | margin-right: 20px; 42 | .menuItem { 43 | padding: 0 20px; 44 | transition: all ease 0.5s; 45 | cursor: pointer; 46 | &:hover { 47 | background-color: #f8f8f8 48 | } 49 | } 50 | } 51 | .username { 52 | margin-left: 5px; 53 | } -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | Layout, 4 | Avatar, 5 | Dropdown, 6 | Menu, 7 | Breadcrumb 8 | } from 'antd' 9 | import { LoginOutlined, UserOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons' 10 | import styles from './index.less' 11 | import { connect } from 'dva' 12 | import { getMenuData } from '../../common/menu' 13 | 14 | const MenuItem = Menu.Item; 15 | const { Header } = Layout; 16 | 17 | interface IGlobalHeader { 18 | collapsed: boolean; 19 | dispatch?: Function; 20 | location: any; 21 | } 22 | 23 | @connect() 24 | class GlobalHeader extends React.Component { 25 | 26 | changeCollapsed = () => { 27 | const { collapsed } = this.props; 28 | this.props.dispatch({ 29 | type: 'global/changeLayoutCollapsed', 30 | payload: !collapsed 31 | }) 32 | }; 33 | 34 | logout = () => { 35 | this.props.dispatch({ 36 | type: 'login/logout' 37 | }) 38 | }; 39 | 40 | getBreadcrumbList = () => { 41 | var arr = [] 42 | const menu = getMenuData() 43 | const pathname = this.props.location.pathname 44 | function getBreadcrumb(menu) { 45 | menu.forEach(item => { 46 | if (pathname.indexOf(item.path) > -1) { 47 | arr.push({ 48 | name: item.name, 49 | path: item.path 50 | }) 51 | if (item.children) { 52 | getBreadcrumb(item.children) 53 | } 54 | } 55 | }) 56 | } 57 | getBreadcrumb(menu); 58 | return arr 59 | }; 60 | 61 | render() { 62 | const menu = ( 63 | 64 | 1 65 | 2 66 | 67 | 68 |
退出登录
69 |
70 |
71 | ); 72 | return ( 73 |
74 |
75 |
76 | {this.props.collapsed? 77 | : 78 | 79 | } 80 |
81 | 82 | {/* 首页 */} 83 | { 84 | this.getBreadcrumbList().map(item => ( 85 | {item.name} 86 | )) 87 | } 88 | 89 |
90 |
    91 | 92 |
  • 93 | } /> 94 | Chechengyi 95 |
  • 96 |
    97 |
98 |
99 | ) 100 | } 101 | } 102 | 103 | export default GlobalHeader 104 | -------------------------------------------------------------------------------- /src/components/PageHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | Card, 4 | Breadcrumb 5 | } from 'antd' 6 | import { getMenuData } from '@common/menu' 7 | 8 | export default class PageHeader extends React.Component { 9 | 10 | componentDidMount(){ 11 | this.getBreadcrumbList() 12 | } 13 | 14 | getBreadcrumbList =()=> { 15 | var arr = [] 16 | const menu = getMenuData() 17 | const pathname = this.props.location.pathname 18 | function getBreadcrumb(menu){ 19 | menu.forEach( item=> { 20 | if ( pathname.indexOf(item.path) > -1 ) { 21 | arr.push({ 22 | name: item.name, 23 | path: item.path 24 | }) 25 | if ( item.children ) { 26 | getBreadcrumb(item.children) 27 | } 28 | } 29 | }) 30 | } 31 | getBreadcrumb(menu) 32 | return arr 33 | } 34 | 35 | render(){ 36 | return ( 37 | 38 | 39 | 首页 40 | { 41 | this.getBreadcrumbList().map( item=>( 42 | {item.name} 43 | )) 44 | } 45 | 46 | 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /src/components/SideMenu/index.less: -------------------------------------------------------------------------------- 1 | .logo { 2 | position: relative; 3 | height: 63px; 4 | padding-left: 10px; 5 | overflow: hidden; 6 | line-height: 63px; 7 | background: #001529; 8 | transition: all 0.3s; 9 | img { 10 | display: inline-block; 11 | height: 32px; 12 | vertical-align: middle; 13 | } 14 | h1 { 15 | display: inline-block; 16 | margin: 0 0 0 12px; 17 | color: #ccc; 18 | font-weight: 600; 19 | font-size: 20px; 20 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; 21 | vertical-align: middle; 22 | } 23 | } 24 | .menu { 25 | box-shadow: 2px 0px 8px #ccc; 26 | z-index: 10; 27 | background-color: #001529; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/SideMenu/index.tsx: -------------------------------------------------------------------------------- 1 | /* 侧边菜单 */ 2 | 3 | import React from 'react' 4 | import { Menu, Layout } from 'antd' 5 | // import Icon from '@ant-design/icons' 6 | import styles from './index.less' 7 | import { RouteComponentProps } from 'react-router-dom' 8 | import { IGlobalModalState } from '../../models/connect' 9 | import { MenuDataItem } from '../../common/menu' 10 | 11 | const SubMenu = Menu.SubMenu; 12 | const { Sider } = Layout; 13 | 14 | interface ISideMenuProps { 15 | menuData: MenuDataItem[]; 16 | location: RouteComponentProps['location']; 17 | history: RouteComponentProps['history']; 18 | collapsed: IGlobalModalState['collapsed']; 19 | } 20 | 21 | interface ISideMenuState { 22 | openKeys: string[]; 23 | selectKeys: string[]; 24 | } 25 | 26 | type State = Readonly 27 | 28 | class SideMenu extends React.Component { 29 | 30 | rootSubmenuKeys: string[] = []; 31 | 32 | state: State = { 33 | openKeys: [], 34 | selectKeys: [] 35 | }; 36 | 37 | componentDidMount(): void { 38 | const menuKeys = this.getMenuKeys(this.props.location.pathname); 39 | this.setState({ 40 | openKeys: menuKeys 41 | }); 42 | this.setState({ 43 | // 如果没有selectKeys 就跳到菜单列表的第一个页面 44 | selectKeys: menuKeys.length === 0 ? [this.props.menuData[0].path] : menuKeys 45 | }); 46 | } 47 | 48 | componentWillReceiveProps(nextProps: Readonly, nextContext: any): void { 49 | if (nextProps.location.pathname !== this.props.location.pathname) { 50 | const menuKeys = this.getMenuKeys(nextProps.location.pathname); 51 | this.setState({ 52 | openKeys: menuKeys 53 | }); 54 | this.setState({ 55 | // 如果没有selectKeys 就跳到菜单列表的第一个页面 56 | selectKeys: menuKeys.length === 0 ? [this.props.menuData[0].path] : menuKeys 57 | }); 58 | } 59 | } 60 | 61 | menuItemClick = (path: string) => { 62 | path = '/' + path; 63 | if (path === this.props.location.pathname) return; 64 | this.props.history.push(path); 65 | }; 66 | 67 | getMenuKeys = (path: string): Array => { 68 | const flatMenuKeys = this.getFlatMenuKeys(this.props.menuData); 69 | path = path.replace(/^\//, ''); 70 | let arr: Array = path.split('/'); 71 | let back_arr: Array; 72 | back_arr = arr.reduce((ret, item) => { 73 | if (ret.length === 0) { 74 | return [item] 75 | } else { 76 | return [...ret, ret[ret.length - 1] + '/' + item]; 77 | } 78 | }, []); 79 | back_arr.shift(); 80 | return back_arr.filter(item => { 81 | return flatMenuKeys.indexOf(item) > -1; 82 | }) 83 | }; 84 | 85 | getFlatMenuKeys(menus) { 86 | let keys = []; 87 | menus.forEach((item) => { 88 | if (item.children) { 89 | keys.push(item.path); 90 | keys = keys.concat(this.getFlatMenuKeys(item.children)); 91 | } else { 92 | keys.push(item.path); 93 | } 94 | }); 95 | return keys; 96 | } 97 | 98 | getMenuItems = (item: MenuDataItem): React.ReactElement => { 99 | if (!item.name) return null 100 | let Icon 101 | if (item.icon) { 102 | Icon = require('@ant-design/icons')[item.icon] 103 | } 104 | if (item.children) { 105 | return ( 106 | 110 | {Icon && } 111 | {item.name} 112 | 113 | } 114 | > 115 | { 116 | this.getNavMenuItems(item.children, true) 117 | } 118 | 119 | ) 120 | } else { 121 | return ( 122 | this.menuItemClick(item.path)}> 123 | {Icon && } {item.name} 124 | 125 | ) 126 | } 127 | } 128 | 129 | getNavMenuItems = (menuData: MenuDataItem[], isSubMenu = false): Array => { 130 | if (!menuData) { 131 | return [] 132 | } 133 | if (!isSubMenu) { 134 | this.rootSubmenuKeys = []; 135 | } 136 | return menuData.map(item => { 137 | !isSubMenu && item.children && this.rootSubmenuKeys.push(item.path); 138 | return this.getMenuItems(item) 139 | }) 140 | }; 141 | 142 | handSelect = (e) => { 143 | this.setState({ 144 | selectKeys: e.selectedKeys 145 | }) 146 | }; 147 | 148 | handMenuChange = openKeys => { 149 | const latestOpenKey = openKeys.find(key => this.state.openKeys.indexOf(key) === -1); 150 | if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) { 151 | this.setState({ openKeys }); 152 | } else { 153 | this.setState({ 154 | openKeys: latestOpenKey ? [latestOpenKey] : [], 155 | }); 156 | } 157 | }; 158 | 159 | render() { 160 | return ( 161 | 167 |
168 |

Chechengyi

169 |
170 | 179 | {this.getNavMenuItems(this.props.menuData)} 180 | 181 |
182 | ) 183 | } 184 | } 185 | 186 | export default SideMenu 187 | -------------------------------------------------------------------------------- /src/hoc/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getRequest } from '../utils/utils' 3 | import { Store } from 'antd/lib/form/interface'; 4 | 5 | /* 6 | 将表格或列表中搜索和分页的功能提取出来, 7 | 搜索字段和分页的参数都被提到了本高阶组件当中, 8 | type 代表获取数据后需要 dispatch 的model的type 9 | */ 10 | 11 | interface TableState { 12 | formValues: Store; 13 | page: number; 14 | num: number; 15 | } 16 | 17 | interface TableParams { 18 | type?: string; 19 | page?: number; 20 | num?: number; 21 | } 22 | 23 | export interface TableProps { 24 | dispatch: Function; 25 | } 26 | 27 | export interface HocTableComponentProps { 28 | resetData: (e: React.MouseEvent) => void; 29 | searchData: (e) => void; 30 | handlePageChange: (page: number, num: number) => void; 31 | page: number 32 | } 33 | 34 | interface IParams { 35 | [propsName: string]: string | number 36 | } 37 | 38 | type State = Readonly; 39 | export default

(setting: TableParams) => 40 | (WrapComponent: React.ComponentType

): React.ComponentType> => { 41 | return class TableHoc extends React.Component

{ 42 | state: State; 43 | constructor(props) { 44 | super(props); 45 | const params: IParams = getRequest(); 46 | this.state = { 47 | formValues: {}, 48 | page: setting.page || (params.page ? Number(params.page) : 1), 49 | num: setting.num || 10 50 | } 51 | } 52 | 53 | warpCom; 54 | 55 | componentDidMount() { 56 | this.getData(); 57 | } 58 | 59 | static displayName = WrapComponent.displayName || WrapComponent.name || 'Component'; 60 | 61 | resetData = () => { 62 | this.warpCom.form && this.warpCom.form.resetFields(); 63 | this.setState({ 64 | page: 1, 65 | formValues: {} 66 | }, this.getData) 67 | }; 68 | 69 | getData = () => { 70 | let elseSearchParams; 71 | if (this.warpCom.getSearchParams) { 72 | elseSearchParams = this.warpCom.getSearchParams() 73 | } else { 74 | elseSearchParams = {} 75 | } 76 | const { page, num, formValues } = this.state; 77 | this.props.dispatch({ 78 | type: setting.type, 79 | payload: { 80 | page, 81 | num, 82 | ...formValues, 83 | ...elseSearchParams 84 | } 85 | }) 86 | .then(() => { 87 | // 改变地址栏参数,加上page页码 88 | window.history.replaceState(null, null, `?page=${page}`) 89 | }) 90 | }; 91 | 92 | searchData = (formValues: Store) => { 93 | this.setState({ 94 | page: 1, 95 | formValues 96 | }, this.getData) 97 | }; 98 | 99 | handlePageChange = (page, num) => { 100 | this.setState({ 101 | page, 102 | num 103 | }, this.getData) 104 | }; 105 | 106 | render() { 107 | return ( 108 | this.warpCom = com} 110 | {...this.props} 111 | {...this.state} 112 | resetData={this.resetData} 113 | searchData={this.searchData} 114 | handlePageChange={this.handlePageChange} 115 | /> 116 | ) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/hooks/useTable.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormInstance } from 'antd/lib/form' 3 | import { Dispatch } from '../models/connect' 4 | import { getRequest } from '../utils/utils' 5 | 6 | type useTableProps = { 7 | type: string; 8 | elseValue?: Object; 9 | page?: number; 10 | size?: number; 11 | } 12 | 13 | export type UseTableInstance = { 14 | resetData: () => void; 15 | searchData: (e: Object) => void; 16 | handlePageChange: (page: number, size: number) => void; 17 | size: number; 18 | page: number; 19 | } 20 | 21 | const useTable = 22 | ( 23 | props: useTableProps, 24 | dispatch: Dispatch, 25 | form?: FormInstance 26 | ): [UseTableInstance] => { 27 | 28 | const [page, setPage] = React.useState(() => { 29 | let params = getRequest() 30 | return params.page ? Number(params.page) : props.page || 1 31 | }) 32 | 33 | const [size, setSize] = React.useState(props.size || 10) 34 | const [formValues, setFormValues] = React.useState({}); //搜索参数 35 | 36 | React.useEffect(() => { 37 | getData() 38 | }, [page, size, formValues, props.elseValue]) 39 | 40 | const getData = React.useCallback(() => { 41 | dispatch({ 42 | type: props.type, 43 | payload: { 44 | page, 45 | num: size, 46 | ...props.elseValue, 47 | ...formValues 48 | } 49 | }) 50 | .then(() => { 51 | // 改变地址栏参数,加上page页码 52 | let params: any = getRequest(); 53 | let url = `?page=${page}`; 54 | // 保留原有的参数 55 | Object.keys(params).forEach(item => { 56 | if (item !== 'page') { 57 | url += '&' + item + '=' + params[item]; 58 | } 59 | }); 60 | window.history.replaceState(null, null, url); 61 | }) 62 | }, [page, size, formValues, props.elseValue]) 63 | 64 | const resetData = React.useCallback(() => { 65 | if (form) { 66 | form.resetFields() 67 | } 68 | setPage(1); 69 | setFormValues({}); 70 | }, [form]) 71 | 72 | const searchData = React.useCallback((data) => { 73 | setPage(1); 74 | setFormValues(data); 75 | }, []); 76 | 77 | const handlePageChange = React.useCallback((page: number, size: number) => { 78 | setPage(page); 79 | setSize(size); 80 | }, []); 81 | 82 | return [{ 83 | page, 84 | size, 85 | searchData, 86 | resetData, 87 | handlePageChange 88 | }] 89 | } 90 | 91 | export default useTable -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | html, body, :global(#root) { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | text-rendering: optimizeLegibility; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | ul, li { 11 | list-style: none; 12 | } 13 | .globalSpin { 14 | width: 100%; 15 | margin: 40px 0 !important; 16 | } 17 | .onePxBor { 18 | position: relative; 19 | } 20 | .onePxBor:after { 21 | content: ''; 22 | position: absolute; 23 | left: 0; 24 | right: 0; 25 | bottom: 0; 26 | height: 1px; 27 | background-color: #d9d9d9; 28 | transform-origin: 0 0; 29 | } 30 | .onePxTop { 31 | position: relative; 32 | } 33 | .onePxTop:after { 34 | content: ''; 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | height: 1px; 40 | background-color: #f1f1f1; 41 | transform-origin: 0 0; 42 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import dva from 'dva'; 2 | import './index.less'; 3 | import { createBrowserHistory, createHashHistory } from 'history' 4 | // import global from './models/global' 5 | import routerConfig from './router' 6 | 7 | // 1. Initialize 8 | const app = dva({ 9 | history: createBrowserHistory({basename: '/'}) 10 | }); 11 | 12 | // 2. Plugins 13 | // app.use({}); 14 | 15 | // 3. Model 16 | app.model(require('./models/global').default); 17 | 18 | // 4. Router 19 | app.router(require('./router').default); 20 | 21 | // 5. Start 22 | app.start('#root'); 23 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | Layout 4 | } from 'antd'; 5 | import GlobalHeader from '../components/GlobalHeader' 6 | import { connect } from 'dva' 7 | import { getRoutes } from '../utils/utils' 8 | import { getMenuData } from '../common/menu' 9 | import { Route, Redirect, Switch } from 'dva/router' 10 | import SideMenu from '../components/SideMenu' 11 | import NotFound from '../routes/Exception/404' 12 | import DocumentTitle from 'react-document-title' 13 | import { ConnectState, IGlobalModalState } from '../models/connect' 14 | import { RouteComponentProps } from 'react-router-dom' 15 | 16 | const { 17 | Content, Footer 18 | } = Layout; 19 | 20 | /* 获取菜单重定向地址 */ 21 | const redirectData = []; 22 | const getRedirect = (item) => { 23 | if (item && item.children) { 24 | if (item.children[0] && item.children[0].path) { 25 | redirectData.push({ 26 | from: `/${item.path}`, 27 | to: `/${item.children[0].path}`, 28 | }); 29 | item.children.forEach((children) => { 30 | getRedirect(children); 31 | }); 32 | } 33 | } 34 | }; 35 | getMenuData().forEach(getRedirect); 36 | 37 | interface IBasicLayoutProps extends RouteComponentProps, IGlobalModalState{ 38 | dispatch: Promise; 39 | routerData: []; 40 | } 41 | 42 | @connect( (state: ConnectState)=>({ 43 | collapsed: state.global.collapsed 44 | })) 45 | 46 | class BasicLayout extends React.Component { 47 | state = { 48 | collapsed: false, 49 | }; 50 | 51 | getPageTitle() { 52 | const { routerData, location } = this.props; 53 | const { pathname } = location; 54 | let title = 'antd'; 55 | if (routerData[pathname] && routerData[pathname].name) { 56 | title = `${routerData[pathname].name} - antd`; 57 | } 58 | return title; 59 | } 60 | 61 | render() { 62 | const { collapsed, routerData, match } = this.props; 63 | return ( 64 | 65 | 66 | 72 | 73 | 74 | 75 |
76 | 77 | {/* */} 78 | { 79 | redirectData.map(item => 80 | 81 | ) 82 | } 83 | ( 84 | 85 | )} /> 86 | { 87 | getRoutes(match.path, routerData).map(item => ( 88 | 89 | )) 90 | } 91 | 92 | 93 |
94 |
95 |
96 | Ant Design 中后台管理系统 ©2019 Created by Chechengyi 97 |
98 |
99 |
100 |
101 | ); 102 | } 103 | } 104 | 105 | export default BasicLayout 106 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | .container { 2 | background: #f0f2f5; 3 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 4 | width: 100%; 5 | min-height: 100%; 6 | background-repeat: no-repeat; 7 | background-position: center; 8 | background-size: 100%; 9 | padding: 110px 0 144px 0; 10 | position: relative; 11 | } 12 | .title { 13 | text-align: center; 14 | margin-bottom: 20px; 15 | } -------------------------------------------------------------------------------- /src/layouts/UserLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import DocumentTitle from 'react-document-title' 3 | import styles from './UserLayout.less' 4 | import { Route, Redirect } from 'dva/router' 5 | import { getRoutes } from '../utils/utils' 6 | import { RouteComponentProps } from 'react-router-dom' 7 | 8 | interface IUserLayout extends RouteComponentProps{ 9 | routerData: []; 10 | } 11 | 12 | class UserLayout extends React.Component { 13 | 14 | getPageTitle() { 15 | const { routerData, location } = this.props; 16 | const { pathname } = location; 17 | let title = 'antd'; 18 | if (routerData[pathname] && routerData[pathname].name) { 19 | title = `${routerData[pathname].name} - antd`; 20 | } 21 | return title; 22 | } 23 | 24 | render(){ 25 | const { routerData, match } = this.props; 26 | return ( 27 | 28 |
29 |

管理员系统

30 | } /> 31 | { 32 | getRoutes(match.path, routerData).map( item=> ( 33 | 34 | )) 35 | } 36 |
37 |
38 | ) 39 | } 40 | } 41 | 42 | export default UserLayout 43 | -------------------------------------------------------------------------------- /src/models/connect.ts: -------------------------------------------------------------------------------- 1 | import { IGlobalModalState } from './global' 2 | import { LoginModalState } from './login' 3 | import { IOrderModalState } from './order' 4 | import { IGoodsModalState } from './goods' 5 | 6 | export { 7 | IGlobalModalState, 8 | LoginModalState, 9 | IOrderModalState, 10 | IGoodsModalState 11 | } 12 | 13 | 14 | export interface ConnectState { 15 | global: IGlobalModalState; 16 | login: LoginModalState; 17 | order: IOrderModalState; 18 | goods: IGoodsModalState; 19 | } 20 | 21 | export type Dispatch =

void>(action: { 22 | type: string; 23 | payload?: P; 24 | callback?: C; 25 | [key: string]: any; 26 | }) => any; 27 | -------------------------------------------------------------------------------- /src/models/global.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from 'dva' 2 | import { Reducer } from 'redux' 3 | 4 | export interface IGlobalModalState { 5 | collapsed: boolean; 6 | } 7 | 8 | export interface IGlobalModalType { 9 | namespace: 'global'; 10 | state: IGlobalModalState; 11 | effects: { 12 | }; 13 | reducers: { 14 | changeLayoutCollapsed: Reducer 15 | } 16 | } 17 | 18 | const globalModal: IGlobalModalType = { 19 | namespace: 'global', 20 | state: { 21 | collapsed: false 22 | }, 23 | effects: { 24 | 25 | }, 26 | reducers: { 27 | changeLayoutCollapsed(state, {payload}) { 28 | return { 29 | ...state, 30 | collapsed: payload 31 | } 32 | } 33 | } 34 | }; 35 | 36 | export default globalModal; 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/models/goods.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from 'dva' 2 | import { Reducer } from 'redux' 3 | import { getGoods } from '../services/api' 4 | 5 | export interface IGoodsModalState { 6 | list: []; 7 | loading: boolean; 8 | } 9 | 10 | export interface IGoodsModalType { 11 | namespace: 'goods' 12 | state: IGoodsModalState; 13 | effects: { 14 | getData: Effect 15 | }; 16 | reducers: { 17 | saveData: Reducer; 18 | changeLoading: Reducer 19 | } 20 | } 21 | 22 | const goodsModal: IGoodsModalType = { 23 | namespace: 'goods', 24 | state: { 25 | list: [], 26 | loading: false 27 | }, 28 | effects: { 29 | *getData({ payload }, { call, put }) { 30 | yield put({ 31 | type: 'changeLoading', 32 | payload: true 33 | }); 34 | const res = yield call(getGoods, payload) 35 | yield put({ 36 | type: 'saveData', 37 | payload: res 38 | }); 39 | console.log('res', res) 40 | yield put({ 41 | type: 'changeLoading', 42 | payload: false 43 | }) 44 | } 45 | }, 46 | reducers: { 47 | saveData(state, { payload }) { 48 | return { 49 | ...state, 50 | list: payload 51 | } 52 | }, 53 | changeLoading(state, { payload }) { 54 | return { 55 | ...state, 56 | loading: payload 57 | } 58 | } 59 | } 60 | }; 61 | 62 | export default goodsModal; 63 | 64 | // export default { 65 | // namespace: 'goods', 66 | // state:{ 67 | // list: [], 68 | // loading: false 69 | // }, 70 | // effects: { 71 | // *getData({payload}, {call, put}){ 72 | // yield put({ 73 | // type: 'changeLoading', 74 | // payload: true 75 | // }) 76 | // const res = yield call(getGoods, payload) 77 | // yield put({ 78 | // type: 'saveData', 79 | // payload: res 80 | // }) 81 | // yield put({ 82 | // type: 'changeLoading', 83 | // payload: false 84 | // }) 85 | // } 86 | // }, 87 | // reducers: { 88 | // saveData(state, {payload}){ 89 | // return { 90 | // ...state, 91 | // list: payload 92 | // } 93 | // }, 94 | // changeLoading(state, {payload}){ 95 | // return { 96 | // ...state, 97 | // loading: payload 98 | // } 99 | // } 100 | // } 101 | // } 102 | -------------------------------------------------------------------------------- /src/models/login.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from 'dva' 2 | import { Reducer } from 'redux' 3 | import { login } from '@services/api' 4 | import { routerRedux } from 'dva/router' 5 | 6 | type LOGIN_STATUS = 'LOGIN' | 'NO_LOGIN' | 'ERROR'; 7 | 8 | export interface LoginModalState { 9 | loginStatus: LOGIN_STATUS; 10 | loading: boolean 11 | } 12 | 13 | export interface LoginModalType { 14 | namespace: 'login', 15 | state: LoginModalState, 16 | effects: { 17 | login: Effect; 18 | logout: Effect; 19 | }, 20 | reducers: { 21 | saveLoginStatus: Reducer, 22 | changeLoading: Reducer, 23 | 24 | } 25 | } 26 | 27 | const loginModal: LoginModalType = { 28 | namespace: 'login', 29 | state: { 30 | loginStatus: 'NO_LOGIN', 31 | loading: false 32 | }, 33 | effects: { 34 | *login({ payload }, { call, put }) { 35 | yield put({ 36 | type: 'changeLoading', 37 | payload: true 38 | }); 39 | const res = yield call(login, payload); 40 | if (res.code === 'OK') { 41 | yield put({ 42 | type: 'saveLoginStatus', 43 | payload: 'LOGIN' 44 | }); 45 | yield put(routerRedux.replace('/cont')) 46 | } else { 47 | yield put({ 48 | type: 'saveLoginStatus', 49 | payload: 'ERROR' 50 | }); 51 | } 52 | yield put({ 53 | type: 'changeLoading', 54 | payload: false 55 | }); 56 | }, 57 | *logout(_, { call, put }) { 58 | yield put({ 59 | type: 'saveLoginStatus', 60 | payload: 'NO_LOGIN' 61 | }); 62 | yield put(routerRedux.replace('/user/login')); 63 | } 64 | }, 65 | reducers: { 66 | saveLoginStatus(state, { payload }) { 67 | return { 68 | ...state, 69 | loginStatus: payload 70 | } 71 | }, 72 | changeLoading(state, { payload }) { 73 | return { 74 | ...state, 75 | loading: payload 76 | } 77 | } 78 | } 79 | }; 80 | 81 | export default loginModal; 82 | 83 | // import { 84 | // login 85 | // } from '../services/api' 86 | // import { routerRedux } from 'dva/router' 87 | 88 | // export default { 89 | // namespace: 'login', 90 | // state: { 91 | // loginStatus: 'NO_LOGIN', // NO_LOGIN 没有登录 LOGIN 登录成功 ERROR 登录错误 一般是用户名或密码错误 92 | // loading: false 93 | // }, 94 | // effects:{ 95 | // *login({payload}, {call, put}){ 96 | // yield put({ 97 | // type: 'changeLoading', 98 | // payload: true 99 | // }) 100 | // const res = yield call(login, payload) 101 | // if ( res.code === 'OK' ) { 102 | // yield put({ 103 | // type: 'saveLoginStatus', 104 | // payload: 'LOGIN' 105 | // }) 106 | // yield put(routerRedux.replace('/cont')) 107 | // } else { 108 | // yield put({ 109 | // type: 'saveLoginStatus', 110 | // payload: 'ERROR' 111 | // }) 112 | // } 113 | // yield put({ 114 | // type: 'changeLoading', 115 | // payload: false 116 | // }) 117 | // } 118 | // , 119 | // *logout({payload}, {call, put}){ 120 | // yield put({ 121 | // type: 'saveLoginStatus', 122 | // payload: 'NO_LOGIN' 123 | // }) 124 | // yield put(routerRedux.replace('/user/login')) 125 | // } 126 | // }, 127 | // reducers: { 128 | // saveLoginStatus(state, {payload}){ 129 | // return { 130 | // ...state, 131 | // loginStatus:payload 132 | // } 133 | // }, 134 | // changeLoading(state, {payload}){ 135 | // return { 136 | // ...state, 137 | // loading: payload 138 | // } 139 | // } 140 | // } 141 | // } 142 | -------------------------------------------------------------------------------- /src/models/order.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from 'dva' 2 | import { Reducer } from 'redux' 3 | import {getOrder} from "../services/api"; 4 | 5 | export interface IOrderModalState { 6 | list: []; 7 | loading: boolean 8 | } 9 | 10 | export interface IOrderModalType { 11 | namespace: 'order'; 12 | state: IOrderModalState; 13 | effects: { 14 | getData: Effect; 15 | }; 16 | reducers: { 17 | saveData: Reducer; 18 | changeLoading: Reducer 19 | } 20 | } 21 | 22 | const orderModal: IOrderModalType = { 23 | namespace: 'order', 24 | state: { 25 | list: [], 26 | loading: false 27 | }, 28 | effects: { 29 | *getData({payload}, {call, put}){ 30 | yield put({ 31 | type: 'changeLoading', 32 | payload:true 33 | }); 34 | const res = yield call(getOrder, payload); 35 | yield put({ 36 | type: 'saveData', 37 | payload: res 38 | }); 39 | yield put({ 40 | type: 'changeLoading', 41 | payload:false 42 | }) 43 | } 44 | }, 45 | reducers: { 46 | saveData(state, {payload}) { 47 | return { 48 | ...state, 49 | list: payload 50 | } 51 | }, 52 | changeLoading(state, {payload}) { 53 | return { 54 | ...state, 55 | loading: payload 56 | } 57 | } 58 | } 59 | }; 60 | 61 | export default orderModal; 62 | -------------------------------------------------------------------------------- /src/router.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Router, Route, Switch, Redirect } from 'dva/router'; 3 | import { Spin } from 'antd'; 4 | import dynamic from 'dva/dynamic'; 5 | import styles from './index.less'; 6 | import { getRouterData } from './common/router' 7 | 8 | (dynamic as any).setDefaultLoadingComponent(() => { 9 | return ; 10 | }); 11 | 12 | 13 | function RouterConfig({ history, app }) { 14 | const routerData = getRouterData(app); 15 | const UserLayout = routerData['/user'].component; 16 | const BasicLayout = routerData['/cont'].component; 17 | return ( 18 | 19 | 20 | 21 | } /> 22 | } /> 23 | 24 | 25 | ); 26 | } 27 | 28 | export default RouterConfig; 29 | -------------------------------------------------------------------------------- /src/routes/Dashborad/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './styles.css' 3 | 4 | const Dashboard = props=> { 5 | 6 | return ( 7 |

8 |
9 | logo 10 |

Welcome

11 |
12 |
13 | ) 14 | }; 15 | 16 | export default Dashboard 17 | -------------------------------------------------------------------------------- /src/routes/Dashborad/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .AppLogo { 6 | animation: AppLogoSpin infinite 20s linear; 7 | height: 200px; 8 | } 9 | 10 | .AppHeader { 11 | /*background-color: #222;*/ 12 | height: 400px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .AppTitle { 18 | font-size: 1.5em; 19 | } 20 | 21 | .AppIntro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes AppLogoSpin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/Exception/403.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Exception from '../../components/Exception' 3 | 4 | export default () => { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/Exception/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Exception from '../../components/Exception' 3 | 4 | export default ()=> { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/GoodsList/GoodsList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'dva' 3 | import { 4 | Card, 5 | Row, 6 | Col, 7 | Spin, 8 | Input, 9 | Form, 10 | Button 11 | } from 'antd' 12 | import { RouteComponentProps } from 'react-router-dom' 13 | import { ConnectState, IGoodsModalState } from '@models/connect' 14 | import useTable from '@hooks/useTable' 15 | 16 | interface GoodsListProps extends RouteComponentProps, IGoodsModalState { 17 | dispatch: (e) => void; 18 | routerData: []; 19 | } 20 | 21 | const FormItem = Form.Item 22 | 23 | const GoodsList: React.FC = (props) => { 24 | const [formInstance] = Form.useForm() 25 | const [tableInstanve] = useTable({ 26 | type: 'goods/getData' 27 | }, props.dispatch, formInstance) 28 | 29 | 30 | return ( 31 |
32 | 33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | { 45 | props.list.map((item: any) => ( 46 | 47 | 48 | 49 |

商品名称:{item.name}

50 |

{item.desc}

51 |
52 | 53 | )) 54 | } 55 |
56 |
57 |
58 |
59 | ) 60 | } 61 | 62 | 63 | export default connect((state: ConnectState) => ({ 64 | list: state.goods.list, 65 | loading: state.goods.loading 66 | }))(GoodsList) 67 | 68 | // class GoodsList extends React.Component { 69 | 70 | // render() { 71 | // return ( 72 | //
73 | // 74 | //
75 | // 76 | // { 77 | // this.props.list.map((item: any) => ( 78 | // 79 | // 80 | // 81 | //

商品名称:{item.name}

82 | //

{item.desc}

83 | //
84 | // 85 | // )) 86 | // } 87 | //
88 | //
89 | //
90 | //
91 | // ) 92 | // } 93 | // } 94 | // export default connect((state: ConnectState) => ({ 95 | // list: state.goods.list, 96 | // loading: state.goods.loading 97 | // }))( 98 | // TableHoc({ 99 | // type: 'goods/getData' 100 | // })(GoodsList) 101 | // ) 102 | -------------------------------------------------------------------------------- /src/routes/IndexPage.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 3em; 5 | text-align: center; 6 | } 7 | 8 | .title { 9 | font-size: 2.5rem; 10 | font-weight: normal; 11 | letter-spacing: -1px; 12 | } 13 | 14 | .welcome { 15 | height: 328px; 16 | background: url(../assets/yay.jpg) no-repeat center 0; 17 | background-size: 388px 328px; 18 | } 19 | 20 | .list { 21 | font-size: 1.2em; 22 | margin-top: 1.8em; 23 | list-style: none; 24 | line-height: 1.5em; 25 | } 26 | 27 | .list code { 28 | background: #f7f7f7; 29 | } 30 | -------------------------------------------------------------------------------- /src/routes/TableList/MyTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'dva' 3 | import { 4 | Table 5 | } from 'antd' 6 | import { IOrderModalState, ConnectState } from '../../models/connect' 7 | import { HocTableComponentProps } from '../../hoc/table' 8 | 9 | 10 | type MyTableProps = Partial & Partial 11 | 12 | @connect( (state: ConnectState)=>({ 13 | list: state.order.list, 14 | loading: state.order.loading, 15 | })) 16 | export default class MyTable extends React.Component { 17 | render(){ 18 | const columns = [ 19 | { 20 | title:'订单编号', 21 | dataIndex: 'id' 22 | }, 23 | { 24 | title: '订单名称', 25 | dataIndex: 'name' 26 | } 27 | ] 28 | return ( 29 |
30 | (record as any).id} 35 | pagination={ 36 | { 37 | total: 200, 38 | defaultCurrent:1, 39 | current: this.props.page, 40 | onChange: this.props.handlePageChange 41 | } 42 | } 43 | /> 44 | 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/routes/TableList/TableList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | Card, 4 | Form, 5 | Input, 6 | Button 7 | } from 'antd' 8 | import TableHoc, { HocTableComponentProps } from '@hoc/table' 9 | import { connect } from 'dva' 10 | import MyTable from './MyTable' 11 | import { ConnectState } from '@models/connect' 12 | import { RouteComponentProps } from 'react-router-dom' 13 | import { FormInstance } from 'antd/lib/form' 14 | 15 | const FormItem = Form.Item; 16 | 17 | interface TableListProps extends RouteComponentProps, HocTableComponentProps { 18 | dispatch: (e) => void; 19 | routerData: []; 20 | } 21 | 22 | class TableList extends React.Component { 23 | 24 | getSearchParams = (): object => { 25 | return {} 26 | }; 27 | 28 | form: FormInstance 29 | 30 | handleSearch = values => { 31 | this.props.searchData(values) 32 | }; 33 | 34 | renderForm = () => { 35 | return ( 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | ) 44 | }; 45 | 46 | render() { 47 | return ( 48 | 49 |
50 | {this.renderForm()} 51 |
52 |
53 | 57 |
58 |
59 | ) 60 | } 61 | } 62 | 63 | export default connect((state: ConnectState) => ({ 64 | list: state.order.list 65 | }))( 66 | TableHoc({ 67 | type: 'order/getData' 68 | })(TableList) 69 | ) 70 | -------------------------------------------------------------------------------- /src/routes/User/Login.less: -------------------------------------------------------------------------------- 1 | 2 | @import "~antd/lib/style/themes/default.less"; 3 | 4 | .main { 5 | width: 368px; 6 | padding-top: 24px; 7 | margin: 0 auto; 8 | } 9 | .prefixIcon { 10 | font-size: @font-size-base; 11 | color: @disabled-color; 12 | } 13 | .submit { 14 | width: 100%; 15 | margin-top: 24px; 16 | } -------------------------------------------------------------------------------- /src/routes/User/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styles from './Login.less' 3 | import { 4 | Form, 5 | Input, 6 | Button, 7 | Alert 8 | } from 'antd' 9 | import { UserOutlined, LockFilled } from '@ant-design/icons' 10 | import { connect } from 'dva' 11 | import { RouteComponentProps } from 'react-router-dom' 12 | import { LoginModalState, ConnectState } from '@models/connect' 13 | 14 | 15 | interface LoginProps extends RouteComponentProps, LoginModalState { 16 | dispatch: (e) => Promise; 17 | } 18 | 19 | const formItemLayout = { 20 | labelCol: { span: 4 }, 21 | wrapperCol: { span: 18 }, 22 | }; 23 | 24 | const validateMessages = { 25 | required: '${label} 不能为空', 26 | }; 27 | 28 | const Login: React.FC = (props) => { 29 | 30 | const [form] = Form.useForm() 31 | 32 | const onSubmit = () => { 33 | form.validateFields() 34 | .then(values => { 35 | console.log(values) 36 | props.dispatch({ 37 | type: 'login/login', 38 | payload: { 39 | username: values.username, 40 | password: values.password 41 | } 42 | }) 43 | .then(res => { 44 | console.log(res) 45 | }) 46 | }) 47 | } 48 | 49 | const renderMessage = (message: string) => { 50 | return ( 51 | 56 | ); 57 | }; 58 | 59 | return ( 60 |
61 |
66 | {props.loginStatus === 'ERROR' && 67 | {renderMessage('用户名或密码错误')} 68 | } 69 | 70 | } /> 71 | 72 | 73 | } /> 74 | 75 | 76 | 77 | 78 | 79 |
80 | ) 81 | } 82 | 83 | export default connect((state: ConnectState) => ({ 84 | loading: state.login.loading, 85 | loginStatus: state.login.loginStatus 86 | }))(Login) 87 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import { request } from '../utils/request' 2 | 3 | export async function login(params={}){ 4 | return request('/api/login', { 5 | method:'POST', 6 | data: params 7 | }) 8 | } 9 | 10 | export async function getOrder(params){ 11 | return request('/api/getOrder', { 12 | data: params 13 | }) 14 | } 15 | 16 | export async function getGoods(params){ 17 | return request('/api/getGoods', { 18 | data: params 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/type.d.ts: -------------------------------------------------------------------------------- 1 | interface StyleModule { 2 | [key: string]: any; 3 | } 4 | 5 | declare module "*.css" { 6 | const style: StyleModule; 7 | export default style; 8 | } 9 | 10 | declare module "*.less" { 11 | const style: StyleModule; 12 | export default style; 13 | } 14 | 15 | declare module "*.svg" 16 | declare module "*.png" 17 | declare module "*.jpg" 18 | declare module "*.jpeg" 19 | declare module "*.gif" 20 | declare module "*.bmp" 21 | declare module "*.tiff" 22 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import {notification} from 'antd' 2 | import axios, { AxiosRequestConfig, Method } from 'axios'; 3 | 4 | const host = ''; 5 | 6 | const statusType = { 7 | 200: '服务器成功返回请求的数据', 8 | 201: '新建或修改数据成功。', 9 | 202: '一个请求已经进入后台排队(异步任务)', 10 | 204: '删除数据成功。', 11 | 400: '发出的请求有错误,服务器没有进行新建或修改数据,的操作。', 12 | 401: '用户没有权限(令牌、用户名、密码错误)。', 13 | 403: '用户得到授权,但是访问是被禁止的。', 14 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作', 15 | 406: '请求的格式不可得。', 16 | 410: '请求的资源被永久删除,且不会再得到的。', 17 | 422: '当创建一个对象时,发生一个验证错误。', 18 | 500: '服务器发生错误,请检查服务器', 19 | 502: '网关错误', 20 | 503: '服务不可用,服务器暂时过载或维护', 21 | 504: '网关超时', 22 | }; 23 | 24 | // 响应头拦截 25 | axios.interceptors.response.use( res=> { 26 | /* 可以在这中间做一系列拦截操作 */ 27 | return res.data 28 | }, err=> { 29 | if ( err.response ) { 30 | // 请求已经发出, 但是服务器响应的状态码不在2xx范围内 31 | notification.error({ 32 | message: `请求错误 ${err.response.status}: ${err.response.config.url}`, 33 | description: statusType[err.response.status] 34 | }) 35 | 36 | } else { 37 | // 服务器没有响应, 超时 38 | notification.error({ 39 | message: '请求在规定的时间内没有响应', 40 | description: '请求超时了' 41 | }) 42 | } 43 | return err 44 | }); 45 | 46 | export function request(url, options, isToast=true){ 47 | const headers = { 48 | 'Content-Type': 'application/json; charset=utf-8', 49 | }; 50 | 51 | const method: Method = options.method ||'GET'; 52 | const setting = { 53 | headers, 54 | url: host + url, 55 | method: method, 56 | [method==='GET'?'params':'data'] : options.data, 57 | timeout: 10000, 58 | isToast 59 | }; 60 | return axios(setting as AxiosRequestConfig); 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 工具函数 3 | */ 4 | 5 | interface GetRequestBack { 6 | [propsName: string]: string | number 7 | } 8 | 9 | export function getRequest(): GetRequestBack { 10 | let url = window.location.search; //获取url中"?"符后的字串 11 | let theRequest = new Object(); 12 | if (url.indexOf("?") != -1) { 13 | let str = url.substr(1); 14 | let strs = str.split("&"); 15 | for (let i = 0; i < strs.length; i++) { 16 | theRequest[strs[i].split("=")[0] as string] = unescape(strs[i].split("=")[1]) as string | number; 17 | } 18 | } 19 | return theRequest as GetRequestBack; 20 | } 21 | 22 | export function getRoutes(path, routerData) { 23 | let routes = Object.keys(routerData).filter(routePath => 24 | routePath.indexOf(path) === 0 && routePath !== path); 25 | routes = routes.map(item => item.replace(path, '')); 26 | let renderArr = []; 27 | renderArr.push(routes[0]); 28 | for (let i = 1; i < routes.length; i += 1) { 29 | let isAdd = false; 30 | isAdd = renderArr.every(item => getRelation(item, routes[i]) === 3); 31 | renderArr = renderArr.filter(item => getRelation(item, routes[i]) !== 1); 32 | if (isAdd) { 33 | renderArr.push(routes[i]); 34 | } 35 | } 36 | const renderRoutes = renderArr.map((item) => { 37 | const exact = !routes.some(route => route !== item && getRelation(route, item) === 1); 38 | return { 39 | key: `${path}${item}`, 40 | path: `${path}${item}`, 41 | component: routerData[`${path}${item}`].component, 42 | exact, 43 | }; 44 | }); 45 | return renderRoutes; 46 | } 47 | 48 | function getRelation(str1, str2) { 49 | if (str1 === str2) { 50 | console.warn('Two path are equal!'); // eslint-disable-line 51 | } 52 | const arr1 = str1.split('/'); 53 | const arr2 = str2.split('/'); 54 | if (arr2.every((item, index) => item === arr1[index])) { 55 | return 1; 56 | } else if (arr1.every((item, index) => item === arr2[index])) { 57 | return 2; 58 | } 59 | return 3; 60 | } 61 | -------------------------------------------------------------------------------- /static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /theme.js: -------------------------------------------------------------------------------- 1 | /* 主题颜色配置 2 | * 详细配置参照官网: https://ant.design/docs/react/customize-theme-cn 3 | * */ 4 | 5 | module.exports = { 6 | // 'primary-color': '#1DA57A' 7 | 'primary-color': '#1890ff' 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es6", "dom"], 5 | "allowJs": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "downlevelIteration": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": false, 11 | "experimentalDecorators": true, 12 | // "noEmit": true, 13 | "noEmit": false, 14 | "jsx": "react", 15 | "allowSyntheticDefaultImports": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "src/*":["src/*"], 19 | "@components/*":["src/components/*"], 20 | "@common/*":["src/common/*"], 21 | "@services/*":["src/services/*"], 22 | "@models/*": ["src/models/*"], 23 | "@utils/*": ["src/utils/*"], 24 | "@hoc/*": ["src/hoc/*"], 25 | "@hooks/*": ["src/hooks/*"] 26 | } 27 | }, 28 | "files": [ 29 | "./src/models/connect.ts" 30 | ], 31 | "include": [ 32 | "src" 33 | ], 34 | } 35 | --------------------------------------------------------------------------------