├── src ├── tests │ └── models │ │ └── example-test.js ├── themes │ ├── vars.less │ ├── mixin.less │ ├── default.less │ └── index.less ├── public │ ├── iconfont.eot │ ├── iconfont.ttf │ ├── iconfont.woff │ ├── antd │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ └── iconfont.woff │ ├── assets │ │ ├── data-1491837999815-H1_44Qtal.jpg │ │ ├── data-1493804606544-r1PBU7D1W.jpg │ │ └── data-1493804610896-SJoBIXPkW.jpg │ ├── iconfont.css │ └── logo.svg ├── utils │ ├── enums.ts │ ├── getDisplayName.ts │ ├── theme.ts │ ├── sortByKey.ts │ ├── strTemplate.ts │ ├── sliceString.ts │ ├── cutStr.ts │ ├── subscribe.ts │ ├── storage.ts │ ├── index.ts │ └── request.ts ├── components │ ├── Page │ │ ├── package.json │ │ ├── Page.less │ │ └── Page.tsx │ ├── Loader │ │ ├── package.json │ │ ├── Loader.tsx │ │ └── Loader.less │ ├── Iconfont │ │ ├── package.json │ │ ├── iconfont.less │ │ └── Iconfont.tsx │ ├── Layout │ │ ├── index.tsx │ │ ├── Sider.tsx │ │ ├── Header.less │ │ ├── Header.tsx │ │ ├── Bread.tsx │ │ ├── Layout.less │ │ └── Menu.tsx │ └── index.ts ├── services │ ├── app.ts │ ├── menus.ts │ ├── posts.ts │ └── login.ts ├── routes │ ├── Error │ │ ├── index.less │ │ └── index.tsx │ ├── home │ │ ├── index.less │ │ └── index.tsx │ ├── login │ │ ├── index.less │ │ └── index.tsx │ ├── app.less │ └── App.tsx ├── config │ ├── apis.js │ └── index.js ├── models │ ├── home.ts │ ├── login.ts │ ├── common.ts │ └── app.ts ├── index.tsx ├── entry.ejs ├── ts-types │ └── index.ts ├── router.tsx └── svg │ ├── emoji │ ├── tired.svg │ ├── surprised.svg │ ├── smirking.svg │ ├── unamused.svg │ ├── wink.svg │ ├── tongue.svg │ ├── zombie.svg │ └── vomiting.svg │ └── cute │ ├── kiss.svg │ ├── proud.svg │ ├── cry.svg │ ├── shy.svg │ ├── sweat.svg │ ├── notice.svg │ ├── leisurely.svg │ ├── congratulations.svg │ └── think.svg ├── .eslintignore ├── assets ├── dashboard.jpg ├── 4.2.1-demo-1.gif ├── 4.2.1-demo-2.gif └── standard.md ├── .gitignore ├── tsd.json ├── .roadhogrc.mock.js ├── theme.config.js ├── .editorconfig ├── mock ├── menu.js ├── common.js └── user.js ├── tsconfig.json ├── LICENSE ├── .roadhogrc.js ├── webpack.config.js ├── tslint.json ├── version.js ├── README.md ├── package.json └── global.d.ts /src/tests/models/example-test.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*-test.js 2 | src/public 3 | -------------------------------------------------------------------------------- /src/themes/vars.less: -------------------------------------------------------------------------------- 1 | @import "~themes/default"; 2 | @import "~themes/mixin"; 3 | -------------------------------------------------------------------------------- /assets/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/assets/dashboard.jpg -------------------------------------------------------------------------------- /assets/4.2.1-demo-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/assets/4.2.1-demo-1.gif -------------------------------------------------------------------------------- /assets/4.2.1-demo-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/assets/4.2.1-demo-2.gif -------------------------------------------------------------------------------- /src/public/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/iconfont.eot -------------------------------------------------------------------------------- /src/public/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/iconfont.ttf -------------------------------------------------------------------------------- /src/public/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/iconfont.woff -------------------------------------------------------------------------------- /src/public/antd/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/antd/iconfont.eot -------------------------------------------------------------------------------- /src/public/antd/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/antd/iconfont.ttf -------------------------------------------------------------------------------- /src/public/antd/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/antd/iconfont.woff -------------------------------------------------------------------------------- /src/utils/enums.ts: -------------------------------------------------------------------------------- 1 | export const EnumRoleType = { 2 | ADMIN: 'admin', 3 | DEFAULT: 'admin', 4 | DEVELOPER: 'developer', 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Page", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Page.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Loader", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Loader.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Iconfont/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Iconfont", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Iconfont.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/public/assets/data-1491837999815-H1_44Qtal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/assets/data-1491837999815-H1_44Qtal.jpg -------------------------------------------------------------------------------- /src/public/assets/data-1493804606544-r1PBU7D1W.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/assets/data-1493804606544-r1PBU7D1W.jpg -------------------------------------------------------------------------------- /src/public/assets/data-1493804610896-SJoBIXPkW.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/dva-ts-react-antd/HEAD/src/public/assets/data-1493804610896-SJoBIXPkW.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | yarn.lock 4 | npm-debug.log 5 | # ide 6 | .idea 7 | 8 | # Mac General 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | -------------------------------------------------------------------------------- /src/services/app.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@utils' 2 | import { apis } from '@config' 3 | 4 | export async function query () { 5 | return request(apis.user) 6 | } 7 | -------------------------------------------------------------------------------- /src/services/menus.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@utils' 2 | import { apis } from '@config' 3 | 4 | export async function query() { 5 | return request(apis.menus) 6 | } 7 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": {} 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Iconfont/iconfont.less: -------------------------------------------------------------------------------- 1 | :global .colorful-icon { 2 | width: 1em; 3 | height: 1em; 4 | vertical-align: -0.15em; 5 | fill: currentColor; 6 | overflow: hidden; 7 | font-size: 16px; 8 | } 9 | -------------------------------------------------------------------------------- /.roadhogrc.mock.js: -------------------------------------------------------------------------------- 1 | const mock = {} 2 | require('fs').readdirSync(require('path').join(__dirname + '/mock')).forEach(function(file) { 3 | Object.assign(mock, require('./mock/' + file)) 4 | }) 5 | module.exports = mock 6 | -------------------------------------------------------------------------------- /src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | import Menu from './Menu' 3 | import Bread from './Bread' 4 | import Sider from './Sider' 5 | import styles from './Layout.less' 6 | 7 | export { Header, Menu, Bread, Sider, styles } 8 | -------------------------------------------------------------------------------- /src/utils/getDisplayName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 添加displayName 3 | * @author ronffy 4 | */ 5 | 6 | export default function getDisplayName(WarpedComponent: any): string { 7 | return (WarpedComponent).displayName || (WarpedComponent).name || 'Component'; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import Iconfont from './Iconfont/Iconfont' 2 | import Loader from './Loader/Loader' 3 | import * as MyLayout from './Layout' 4 | import Page from './Page/Page' 5 | 6 | 7 | export { 8 | MyLayout, 9 | Iconfont, 10 | Loader, 11 | Page, 12 | } 13 | -------------------------------------------------------------------------------- /theme.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const lessToJs = require('less-vars-to-js') 4 | 5 | module.exports = () => { 6 | const themePath = path.join(__dirname, './src/themes/default.less') 7 | return lessToJs(fs.readFileSync(themePath, 'utf8')) 8 | } 9 | -------------------------------------------------------------------------------- /src/services/posts.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@utils' 2 | import { apis } from '@config' 3 | 4 | type QueryParams = { 5 | status: number 6 | } 7 | export async function query({ status }: QueryParams) { 8 | return request(apis.posts, { 9 | data: { 10 | status, 11 | }, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | 2 | export const color = { 3 | green: '#64ea91', 4 | blue: '#8fc9fb', 5 | purple: '#d897eb', 6 | red: '#f69899', 7 | yellow: '#f8c82e', 8 | peach: '#f797d6', 9 | borderBase: '#e5e5e5', 10 | borderSplit: '#f4f4f4', 11 | grass: '#d6fbb5', 12 | sky: '#c1e0fc', 13 | } 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/utils/sortByKey.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * object 排序 3 | * @param {*} obj 4 | */ 5 | export default function sortByKey(obj: any) { 6 | const newkey = Object.keys(obj).sort(); 7 | const newObj = {}; 8 | for (let i = 0; i < newkey.length; i++) { 9 | newObj[newkey[i]] = obj[newkey[i]]; 10 | } 11 | return newObj; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/strTemplate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 字符串模版引擎 3 | * @param tpl 模版字符串 4 | * @param data 注入模版中的数据 5 | * @author ronffy 6 | */ 7 | 8 | export default function strTemplate(tpl: string, data: Object) { 9 | if (!data) { 10 | return tpl; 11 | } 12 | return tpl.replace(/{(.*?)}/g, (_match, key) => data[key.trim()]); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/sliceString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 截取指定长度字符串(正则为处理emoji) 3 | * @author ronffy 4 | */ 5 | 6 | export default function sliceString(str: string, start: number, end: number) { 7 | const arr = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g); 8 | if (!arr) { 9 | return ''; 10 | } 11 | return arr.slice(start, end).join(''); 12 | } -------------------------------------------------------------------------------- /src/routes/Error/index.less: -------------------------------------------------------------------------------- 1 | .error { 2 | color: black; 3 | text-align: center; 4 | position: absolute; 5 | top: 30%; 6 | margin-top: -50px; 7 | left: 50%; 8 | margin-left: -100px; 9 | width: 200px; 10 | 11 | :global .anticon { 12 | font-size: 48px; 13 | margin-bottom: 16px; 14 | } 15 | 16 | h1 { 17 | font-family: cursive; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Page/Page.less: -------------------------------------------------------------------------------- 1 | @import "~themes/vars"; 2 | 3 | .contentInner{ 4 | background: #fff; 5 | padding: 24px; 6 | box-shadow: @shadow-1; 7 | min-height: ~"calc(100vh - 198px)"; 8 | position: relative; 9 | } 10 | 11 | @media (max-width: 767px) { 12 | .contentInner { 13 | padding: 12px; 14 | min-height: ~"calc(100vh - 160px)"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Icon } from 'antd' 3 | import styles from './index.less' 4 | import { Page } from '@components' 5 | 6 | const Error = () => ( 7 | 8 |
9 | 10 |

404 Not Found

11 |
12 |
13 | ) 14 | 15 | export default Error 16 | -------------------------------------------------------------------------------- /src/config/apis.js: -------------------------------------------------------------------------------- 1 | 2 | const APIV1 = '/api/v1' 3 | const APIV2 = '/api/v2' 4 | const apiPrefix = APIV1; 5 | 6 | module.exports = { 7 | APIV1, 8 | APIV2, 9 | apiPrefix, 10 | CORS: [], 11 | login: `${APIV1}/user/login`, 12 | logout: `${APIV1}/user/logout`, 13 | userInfo: `${APIV1}/userInfo`, 14 | posts: `${APIV1}/posts`, 15 | user: `${APIV1}/user`, 16 | home: `${APIV1}/home`, 17 | menus: `${APIV1}/menus`, 18 | } 19 | -------------------------------------------------------------------------------- /src/themes/mixin.less: -------------------------------------------------------------------------------- 1 | @import "~themes/default"; // antd-admin 2 | @dark-half: #494949; 3 | @purple: #d897eb; 4 | @shadow-1: 4px 4px 20px 0 rgba(0,0,0,0.01); 5 | @shadow-2: 4px 4px 40px 0 rgba(0,0,0,0.05); 6 | @transition-ease-in: all 0.3s ease-out; 7 | @transition-ease-out: all 0.3s ease-out; 8 | @ease-in: ease-in; 9 | 10 | .text-overflow { 11 | white-space: nowrap; 12 | text-overflow: ellipsis; 13 | overflow: hidden; 14 | } 15 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | const apis = require('./apis'); 2 | 3 | module.exports = { 4 | apis, 5 | name: '后台管理系统', 6 | prefix: 'antdAdmin', 7 | footerText: '', 8 | logo: '/logo.svg', 9 | iconFontCSS: '/iconfont.css', 10 | iconFontJS: '/iconfont.js', 11 | 12 | // 路由相关 13 | defaultPageInfo: { 14 | router: '/home', 15 | name: '首页', 16 | icon: 'home', 17 | id: 1 18 | }, 19 | loginRouter: '/login', 20 | openPages: ['/login'], 21 | }; -------------------------------------------------------------------------------- /src/routes/home/index.less: -------------------------------------------------------------------------------- 1 | .home{ 2 | position: relative; 3 | :global { 4 | .ant-card { 5 | border-radius: 0; 6 | margin-bottom: 24px; 7 | &:hover{ 8 | box-shadow: 4px 4px 40px rgba(0,0,0,.05); 9 | } 10 | } 11 | .ant-card-body{ 12 | overflow-y: scroll; 13 | overflow-x: hidden; 14 | } 15 | } 16 | 17 | .quote { 18 | &:hover { 19 | box-shadow: 4px 4px 40px rgba(246,152,153,.6); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/cutStr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 截断指定长度字符串,以指定标识结尾 3 | * @author ronffy 4 | */ 5 | import sliceString from './sliceString'; 6 | 7 | export default function cutStr(str: string, max: number, endSign: string = '...'): string { 8 | try { 9 | if (!str || typeof str !== 'string') { 10 | return ''; 11 | } 12 | 13 | if (str.length <= max) { 14 | return str; 15 | } 16 | return sliceString(str, 0, max) + endSign 17 | } catch (error) { 18 | return '' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mock/menu.js: -------------------------------------------------------------------------------- 1 | const { apis } = require('./common') 2 | 3 | const { apiPrefix } = apis 4 | let database = [ 5 | { 6 | id: '1', 7 | icon: 'home', 8 | name: '首页', 9 | route: '/home', 10 | }, 11 | // { 12 | // id: '7', 13 | // bpid: '1', 14 | // mpid: '1', 15 | // name: '用户列表', 16 | // icon: 'shopping-cart', 17 | // route: '/post', 18 | // } 19 | ] 20 | 21 | module.exports = { 22 | 23 | [`GET ${apiPrefix}/menus`] (req, res) { 24 | res.status(200).json(database) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/services/login.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@utils' 2 | import { apis } from '@config' 3 | 4 | /** 5 | * 登录请求 6 | */ 7 | type LoginParams = { 8 | password: string | number 9 | username: string | number 10 | } 11 | export async function login({ username, password }: LoginParams) { 12 | return request(apis.login, { 13 | method: 'post', 14 | data: { 15 | username, 16 | password, 17 | } 18 | }) 19 | } 20 | 21 | /** 22 | * 退出请求 23 | */ 24 | export async function logout() { 25 | return request(apis.logout) 26 | } 27 | -------------------------------------------------------------------------------- /src/models/home.ts: -------------------------------------------------------------------------------- 1 | import { modelExtend } from './common' 2 | import { defaultPageInfo } from '@config'; 3 | import { ReduxSagaEffects, Dispatch, DvaSetupParams, ReduxAction } from '@ts-types'; 4 | 5 | export default modelExtend({ 6 | namespace: 'home', 7 | state: { 8 | 9 | }, 10 | subscriptions: { 11 | setup({ dispatch, history }: DvaSetupParams) { 12 | history.listen(({ pathname }) => { 13 | if (pathname === defaultPageInfo.router || pathname === '/') { 14 | console.log('xxx'); 15 | } 16 | }) 17 | }, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/themes/default.less: -------------------------------------------------------------------------------- 1 | // 本文件是对 ant-design: 2 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less 3 | // 相应变量值的覆盖 4 | // 注意:只需写出要覆盖的变量即可(不需要覆盖的变量不要写) 5 | // http://www.iconfont.cn/collections/detail?cid=790 6 | 7 | @import "../../node_modules/antd/lib/style/themes/default.less"; 8 | @border-radius-base: 3px; 9 | @border-radius-sm: 2px; 10 | @shadow-color: rgba(0,0,0,0.05); 11 | @shadow-1-down: 4px 4px 40px @shadow-color; 12 | @border-color-split: #f4f4f4; 13 | @border-color-base: #e5e5e5; 14 | @font-size-base: 13px; 15 | @text-color: #666; 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | import dva from 'dva' 3 | import createLoading from 'dva-loading' 4 | import { createBrowserHistory } from 'history' 5 | import 'babel-polyfill' 6 | 7 | // 1. Initialize 8 | const app = dva({ 9 | ...createLoading({ 10 | effects: true, 11 | }), 12 | history: createBrowserHistory(), 13 | onError (error: Error) { 14 | message.error(`dva报错: ${error.message}`) 15 | }, 16 | }) 17 | 18 | // 2. Model 19 | app.model(require('./models/app')) 20 | 21 | // 3. Router 22 | app.router(require('./router')) 23 | 24 | // 4. Start 25 | app.start('#root') 26 | -------------------------------------------------------------------------------- /src/utils/subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 订阅方法 3 | * @param {*} listener 4 | * @param {*} listeners 5 | * @author ronffy 6 | */ 7 | 8 | type Listener = (...args: any[]) => any 9 | 10 | export default function subscribe(listener: Listener, listeners: Listener[]) { 11 | if (typeof listener !== 'function') { 12 | throw new Error('Expected listener to be a function.'); 13 | } 14 | 15 | let isSubscribed = true; 16 | listeners.push(listener); 17 | 18 | return function unsubscribe() { 19 | if (!isSubscribed) { 20 | return; 21 | } 22 | isSubscribed = false; 23 | 24 | const index = listeners.indexOf(listener); 25 | listeners.splice(index, 1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/standard.md: -------------------------------------------------------------------------------- 1 | ### 开发环境 2 | 3 | - OS:Windows 4 | - 编辑器:Atom 5 | - cmd:Cmder 6 | 7 | ### Atom 插件 8 | 9 | - [atom-beautify](https://atom.io/packages/atom-beautify) 10 | 11 | #### Less 12 | 13 | - [x] Beautify On Save 14 | 15 | #### Javascript 16 | 17 | - [ ] Disable Beautifying Language 18 | 19 | #### Markdown 20 | 21 | - [x] Beautify On Save 22 | - [x] Default Beautifier:Remark 23 | 24 | #### HTML 25 | 26 | - [x] Beautify On Save 27 | 28 | - [linter](https://atom.io/packages/linter) 29 | - [ ] Lint on Change 30 | - [linter-eslint](https://atom.io/packages/linter-eslint) 31 | - [x] Fix errors on save 32 | 33 | ### Cmder 主题 34 | 35 | - [Panda-Theme-Cmder](https://github.com/HamidFaraji/Panda-Theme-Cmder) 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esNext", 4 | "target": "es2017", 5 | "experimentalDecorators": true, 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "jsx": "react", 9 | "allowJs": true, 10 | "baseUrl": "./src", 11 | "moduleResolution": "node", 12 | "paths": { 13 | "@components": [ 14 | "components" 15 | ], 16 | "@models": [ 17 | "models" 18 | ], 19 | "@services/*": [ 20 | "services/*" 21 | ], 22 | "@utils": [ 23 | "utils" 24 | ], 25 | "@config": [ 26 | "config" 27 | ], 28 | "@ts-types": [ 29 | "ts-types" 30 | ], 31 | "@enums": [ 32 | "utils/enums" 33 | ] 34 | } 35 | }, 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/entry.ejs: -------------------------------------------------------------------------------- 1 | <% htmlWebpackPlugin.options.headScripts = htmlWebpackPlugin.options.headScripts || [] %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | dva-ts-react-antd 后台管理系统 11 | 15 | <% for (item of htmlWebpackPlugin.options.headScripts) { %> 16 | 17 | <% } %> 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/routes/home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'dva' 4 | import { Row, Col } from 'antd' 5 | import { Page } from '@components' 6 | import styles from './index.less' 7 | 8 | function Home ({ home, loading }) { 9 | const { 10 | sales 11 | } = home 12 | 13 | 14 | return ( 15 | 16 | 17 | 18 | hahah 19 | 20 | 21 | aaa 22 | 23 | 24 | ggg 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | Home.propTypes = { 32 | home: PropTypes.object, 33 | loading: PropTypes.object, 34 | } 35 | 36 | export default connect(({ home, loading }) => ({ home, loading }))(Home) 37 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import styles from './Loader.less' 5 | 6 | type Props = { 7 | fullScreen?: boolean 8 | spinning: boolean 9 | } 10 | 11 | // #----------- 上: ts类型定义 ----------- 分割线 ----------- 下: JS代码 ----------- 12 | 13 | const Loader: FC = ({ spinning, fullScreen }) => { 14 | const classes = classNames(styles.loader, { 15 | [styles.hidden]: !spinning, 16 | [styles.fullScreen]: fullScreen, 17 | }); 18 | 19 | return ( 20 |
21 |
22 |
23 |
LOADING
24 |
25 |
26 | ) 27 | } 28 | 29 | 30 | Loader.propTypes = { 31 | spinning: PropTypes.bool, 32 | fullScreen: PropTypes.bool, 33 | } 34 | 35 | export default Loader 36 | -------------------------------------------------------------------------------- /src/components/Iconfont/Iconfont.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import './iconfont.less' 5 | 6 | type Props = { 7 | type: string 8 | colorful?: boolean 9 | className?: string 10 | } 11 | 12 | // #----------- 上: ts类型定义 ----------- 分割线 ----------- 下: JS代码 ----------- 13 | 14 | const Iconfont: FC = ({ type, colorful = false, className }) => { 15 | if (colorful) { 16 | return ( 17 | 20 | ) 21 | } 22 | 23 | return 24 | } 25 | 26 | Iconfont.propTypes = { 27 | type: PropTypes.string.isRequired, 28 | colorful: PropTypes.bool, 29 | className: PropTypes.string, 30 | } 31 | 32 | export default Iconfont 33 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | 3 | const localStorage = window.localStorage; 4 | 5 | /** 6 | * 获取所有存储的数据 7 | */ 8 | function valueOf() { 9 | return localStorage.valueOf() 10 | } 11 | 12 | /** 13 | * 清空localStorage 14 | */ 15 | function clear() { 16 | localStorage.clear() 17 | } 18 | 19 | /** 20 | * 存储数据 21 | * @param {键} key 22 | * @param {值} value 23 | */ 24 | function setItem(key: string, value: any) { 25 | localStorage.setItem(key, value) 26 | } 27 | 28 | /** 29 | * 读取数据 30 | * @param {键} key 31 | */ 32 | function getItem(key: string) { 33 | return localStorage.getItem(key) 34 | } 35 | 36 | /** 37 | * 删除某个变量 38 | * @param {键} key 39 | */ 40 | function removeItem(key: string) { 41 | localStorage.removeItem(key) 42 | } 43 | 44 | /** 45 | * 检查localStorage里是否保存某个变量 46 | * @param {键} key 47 | */ 48 | function hasOwnProperty(key: string) { 49 | return localStorage.hasOwnProperty(key) 50 | } 51 | 52 | const storage = { 53 | valueOf, 54 | clear, 55 | setItem, 56 | getItem, 57 | removeItem, 58 | hasOwnProperty, 59 | }; 60 | 61 | export default storage; 62 | -------------------------------------------------------------------------------- /src/routes/login/index.less: -------------------------------------------------------------------------------- 1 | @import "~themes/vars"; 2 | 3 | .form { 4 | position: absolute; 5 | top: 50%; 6 | left: 50%; 7 | margin: -160px 0 0 -160px; 8 | width: 320px; 9 | height: 320px; 10 | padding: 36px; 11 | box-shadow: 0 0 100px rgba(0,0,0,.08); 12 | 13 | button { 14 | width: 100%; 15 | } 16 | 17 | p { 18 | color: rgb(204, 204, 204); 19 | text-align: center; 20 | margin-top: 16px; 21 | font-size: 12px; 22 | display: flex; 23 | justify-content: space-between; 24 | } 25 | } 26 | 27 | .logo { 28 | text-align: center; 29 | cursor: pointer; 30 | margin-bottom: 24px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | 35 | img { 36 | width: 40px; 37 | margin-right: 8px; 38 | } 39 | 40 | span { 41 | vertical-align: text-bottom; 42 | font-size: 16px; 43 | text-transform: uppercase; 44 | display: inline-block; 45 | font-weight: 700; 46 | color: @primary-color; 47 | } 48 | } 49 | 50 | .ant-spin-container, 51 | .ant-spin-nested-loading { 52 | height: 100%; 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2016 zuiidea (zuiiidea@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/components/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import Loader from '../Loader/Loader' 5 | import styles from './Page.less' 6 | 7 | type Props = { 8 | className?: string 9 | loading?: boolean 10 | inner?: boolean 11 | } 12 | 13 | // #----------- 上: ts类型定义 ----------- 分割线 ----------- 下: JS代码 ----------- 14 | 15 | export default class Page extends Component { 16 | render () { 17 | const { className, children, loading = false, inner = false } = this.props 18 | const loadingStyle = { 19 | height: 'calc(100vh - 184px)', 20 | overflow: 'hidden', 21 | } 22 | return ( 23 |
29 | {loading ? : ''} 30 | {children} 31 |
32 | ) 33 | } 34 | 35 | static propTypes = { 36 | className: PropTypes.string, 37 | children: PropTypes.node, 38 | loading: PropTypes.bool, 39 | inner: PropTypes.bool, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/models/login.ts: -------------------------------------------------------------------------------- 1 | import { routerRedux } from 'dva/router' 2 | import * as loginService from '@services/login' 3 | import { modelExtend } from './common' 4 | import { defaultPageInfo, loginRouter } from '@config'; 5 | import { ReduxSagaEffects, ReduxAction } from '@ts-types'; 6 | 7 | export default modelExtend({ 8 | namespace: 'login', 9 | 10 | state: {}, 11 | 12 | effects: { 13 | * login({ payload }: ReduxAction, { put, call, select }: ReduxSagaEffects) { 14 | const data = yield call(loginService.login, payload) 15 | const { locationQuery } = yield select(_ => _.app) 16 | if (data.success) { 17 | const { from } = locationQuery 18 | yield put({ type: 'app/query' }) 19 | if (from && from !== loginRouter) { 20 | yield put(routerRedux.push(from)) 21 | } else { 22 | yield put(routerRedux.push(defaultPageInfo.router)) 23 | } 24 | } else { 25 | console.warn('login-login 报错'); 26 | } 27 | }, 28 | 29 | * logout({ payload }: ReduxAction, { call, put }: ReduxSagaEffects) { 30 | const data = yield call(loginService.logout) 31 | if (data.success) { 32 | yield put({ type: 'app/query' }) 33 | } else { 34 | console.warn('app-logout 报错'); 35 | } 36 | }, 37 | }, 38 | 39 | }) 40 | -------------------------------------------------------------------------------- /mock/common.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const config = require('../src/config') 3 | const apis = require('../src/config/apis') 4 | 5 | const queryArray = (array, key, keyAlias = 'key') => { 6 | if (!(array instanceof Array)) { 7 | return null 8 | } 9 | let data 10 | 11 | for (const item of array) { 12 | if (item[keyAlias] === key) { 13 | data = item 14 | break 15 | } 16 | } 17 | 18 | if (data) { 19 | return data 20 | } 21 | return null 22 | } 23 | 24 | const NOTFOUND = { 25 | message: 'Not Found', 26 | documentation_url: 'http://localhost:8000/request', 27 | } 28 | 29 | 30 | let postId = 0 31 | const posts = Mock.mock({ 32 | 'data|100': [ 33 | { 34 | id () { 35 | postId += 1 36 | return postId + 10000 37 | }, 38 | 'status|1-2': 1, 39 | title: '@title', 40 | author: '@last', 41 | categories: '@word', 42 | tags: '@word', 43 | 'views|10-200': 1, 44 | 'comments|10-200': 1, 45 | visibility: () => { 46 | return Mock.mock('@pick(["Public",' 47 | + '"Password protected", ' 48 | + '"Private"])') 49 | }, 50 | date: '@dateTime', 51 | image () { 52 | return Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', this.author.substr(0, 1)) 53 | }, 54 | }, 55 | ], 56 | }).data 57 | 58 | module.exports = { 59 | queryArray, 60 | NOTFOUND, 61 | Mock, 62 | posts, 63 | config, 64 | apis, 65 | } 66 | -------------------------------------------------------------------------------- /.roadhogrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { version } = require('./package.json') 3 | 4 | const svgSpriteDirs = [ 5 | path.resolve(__dirname, 'src/svg/'), 6 | require.resolve('antd').replace(/index\.js$/, '') 7 | ] 8 | 9 | export default { 10 | entry: 'src/index.tsx', 11 | svgSpriteLoaderDirs: svgSpriteDirs, 12 | theme: "./theme.config.js", 13 | publicPath: `/${version}/`, 14 | outputPath: `./dist/${version}`, 15 | // 接口代理示例 16 | proxy: { 17 | "/api/v1/weather": { 18 | "target": "https://api.seniverse.com/", 19 | "changeOrigin": true, 20 | "pathRewrite": { "^/api/v1/weather": "/v3/weather" } 21 | }, 22 | // "/api/v2": { 23 | // "target": "http://192.168.0.110", 24 | // "changeOrigin": true, 25 | // "pathRewrite": { "^/api/v2" : "/api/v2" } 26 | // } 27 | }, 28 | env: { 29 | development: { 30 | extraBabelPlugins: [ 31 | "dva-hmr", 32 | "transform-runtime", 33 | [ 34 | "import", { 35 | "libraryName": "antd", 36 | "style": true 37 | } 38 | ] 39 | ] 40 | }, 41 | production: { 42 | extraBabelPlugins: [ 43 | "transform-runtime", 44 | [ 45 | "import", { 46 | "libraryName": "antd", 47 | "style": true 48 | } 49 | ] 50 | ] 51 | } 52 | }, 53 | dllPlugin: { 54 | exclude: ["babel-runtime", "roadhog", "cross-env"], 55 | include: ["dva/router", "dva/saga", "dva/fetch"] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.less: -------------------------------------------------------------------------------- 1 | .loader { 2 | display: block; 3 | background-color: #fff; 4 | width: 100%; 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | left: 0; 9 | z-index: 100000; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | opacity: 1; 14 | text-align: center; 15 | 16 | &.fullScreen { 17 | position: fixed; 18 | } 19 | 20 | .warpper { 21 | width: 100px; 22 | height: 100px; 23 | display: inline-flex; 24 | flex-direction: column; 25 | justify-content: space-around; 26 | } 27 | 28 | .inner { 29 | width: 40px; 30 | height: 40px; 31 | margin: 0 auto; 32 | text-indent: -12345px; 33 | border-top: 1px solid rgba(0, 0, 0, 0.08); 34 | border-right: 1px solid rgba(0, 0, 0, 0.08); 35 | border-bottom: 1px solid rgba(0, 0, 0, 0.08); 36 | border-left: 1px solid rgba(0, 0, 0, 0.7); 37 | border-radius: 50%; 38 | z-index: 100001; 39 | 40 | :local { 41 | animation: spinner 600ms infinite linear; 42 | } 43 | } 44 | 45 | .text { 46 | width: 100px; 47 | height: 20px; 48 | text-align: center; 49 | font-size: 12px; 50 | letter-spacing: 4px; 51 | color: #000; 52 | } 53 | 54 | &.hidden { 55 | z-index: -1; 56 | opacity: 0; 57 | transition: opacity 1s ease 0.5s, z-index 0.1s ease 1.5s; 58 | } 59 | } 60 | @keyframes spinner { 61 | 0% { 62 | transform: rotate(0deg); 63 | } 64 | 65 | 100% { 66 | transform: rotate(360deg); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/models/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 公共 model 3 | * @author ronffy 4 | */ 5 | import _modelExtend from 'dva-model-extend' 6 | import { DvaModel } from '@ts-types'; 7 | 8 | const commonModel = { 9 | reducers: { 10 | updateState (state: any, { payload }: any) { 11 | return { 12 | ...state, 13 | ...payload, 14 | } 15 | }, 16 | error(state: any, { payload }: any) { 17 | return { 18 | ...state, 19 | error: payload, 20 | } 21 | }, 22 | }, 23 | } 24 | 25 | const modelExtend = (model: DvaModel): DvaModel => _modelExtend(commonModel, model); 26 | 27 | const pageModel = { 28 | state: { 29 | list: [], 30 | pagination: { 31 | showTotal: total => `共 ${total} 条`, 32 | current: 1, 33 | total: 0, 34 | }, 35 | }, 36 | 37 | reducers: { 38 | querySuccess (state: any, { payload }: any) { 39 | const { list, pagination } = payload 40 | return { 41 | ...state, 42 | list, 43 | pagination: { 44 | ...state.pagination, 45 | ...pagination, 46 | }, 47 | } 48 | }, 49 | updateState(state: any, { payload }: any) { 50 | return { 51 | ...state, 52 | ...payload, 53 | } 54 | }, 55 | error(state: any, { payload }: any) { 56 | return { 57 | ...state, 58 | error: payload, 59 | } 60 | }, 61 | }, 62 | 63 | } 64 | 65 | const pageModelExtend = (model: DvaModel): DvaModel => _modelExtend(pageModel, model); 66 | 67 | export { 68 | modelExtend, 69 | pageModelExtend, 70 | 71 | commonModel, 72 | pageModel, 73 | } -------------------------------------------------------------------------------- /src/routes/app.less: -------------------------------------------------------------------------------- 1 | @import "~themes/vars"; 2 | 3 | :global { 4 | #nprogress { 5 | pointer-events: none; 6 | 7 | .bar { 8 | background: @primary-color; 9 | position: fixed; 10 | z-index: 1024; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | .peg { 19 | display: block; 20 | position: absolute; 21 | right: 0; 22 | width: 100px; 23 | height: 100%; 24 | box-shadow: 0 0 10px @primary-color,0 0 5px @primary-color; 25 | opacity: 1.0; 26 | transform: rotate(3deg) translate(0px,-4px); 27 | } 28 | 29 | .spinner { 30 | display: block; 31 | position: fixed; 32 | z-index: 1031; 33 | top: 15px; 34 | right: 15px; 35 | } 36 | 37 | .spinner-icon { 38 | width: 18px; 39 | height: 18px; 40 | box-sizing: border-box; 41 | border: solid 2px transparent; 42 | border-top-color: @primary-color; 43 | border-left-color: @primary-color; 44 | border-radius: 50%; 45 | 46 | :local { 47 | animation: nprogress-spinner 400ms linear infinite; 48 | } 49 | } 50 | } 51 | 52 | .nprogress-custom-parent { 53 | overflow: hidden; 54 | position: relative; 55 | 56 | #nprogress { 57 | .bar, 58 | .spinner { 59 | position: absolute; 60 | } 61 | } 62 | } 63 | } 64 | @keyframes nprogress-spinner { 65 | 0% { 66 | transform: rotate(0deg); 67 | } 68 | 69 | 100% { 70 | transform: rotate(360deg); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ts-types/index.ts: -------------------------------------------------------------------------------- 1 | import { History } from 'history'; 2 | 3 | export interface ReduxAction { 4 | type: string, 5 | [propName: string]: any, 6 | } 7 | 8 | export interface Dispatch { 9 | (action: A): A; 10 | } 11 | 12 | export type ReduxSagaEffects = { 13 | call?: (p: (arg: any) => Promise, arg?: any) => Promise 14 | put?: (action: ReduxAction) => void 15 | select?: (state: any) => any 16 | } 17 | 18 | export interface DvaModelReducer { 19 | (preState: object, action: ReduxAction): object 20 | } 21 | 22 | export interface DvaModelReducers { 23 | [reducerName: string]: DvaModelReducer 24 | } 25 | 26 | export interface DvaModelEffectFn { 27 | (action: ReduxAction, sagaEffects: ReduxSagaEffects): any 28 | } 29 | 30 | export interface ReduxSagaTaker { 31 | type: string, 32 | [propsName: string]: any 33 | } 34 | // problem 35 | export interface DvaModelEffectWithTaker extends Array { 36 | [index: number]: ReduxSagaTaker | DvaModelEffectFn, 37 | } 38 | 39 | export type DvaModelEffect = DvaModelEffectFn | DvaModelEffectWithTaker 40 | 41 | export interface DvaModelEffects { 42 | [effectName: string]: DvaModelEffect 43 | } 44 | 45 | export interface DvaModel { 46 | namespace: string, 47 | state?: T, 48 | reducers?: DvaModelReducers, 49 | effects?: DvaModelEffects, 50 | subscriptions?: object 51 | } 52 | 53 | export type DvaApp = { 54 | _models: any 55 | _store: any 56 | _plugin: any 57 | use: (...args: any[]) => any 58 | model: any 59 | start: any 60 | } 61 | 62 | export type DvaSetupParams = { 63 | dispatch: Dispatch 64 | history: History 65 | } 66 | 67 | export { 68 | History 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/Layout/Sider.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Icon, Switch } from 'antd' 4 | import config from '@config' 5 | import styles from './Layout.less' 6 | import Menus from './Menu' 7 | 8 | type MenuItem = { 9 | id: number 10 | icon: string 11 | name: string 12 | router: string 13 | } 14 | 15 | type Props = { 16 | darkTheme: boolean 17 | siderFold: boolean 18 | location: Location 19 | changeTheme: any 20 | changeOpenKeys: any 21 | navOpenKeys: string[] 22 | menu: MenuItem[] 23 | } 24 | 25 | // #----------- 上: ts类型定义 ----------- 分割线 ----------- 下: JS代码 ----------- 26 | 27 | const Sider: FC = ({ 28 | siderFold, darkTheme, location, changeTheme, navOpenKeys, changeOpenKeys, menu, 29 | }) => { 30 | const menusProps = { 31 | menu, 32 | siderFold, 33 | darkTheme, 34 | location, 35 | navOpenKeys, 36 | changeOpenKeys, 37 | } 38 | return ( 39 |
40 |
41 | logo 42 | {siderFold ? '' : {config.name}} 43 |
44 | 45 | {!siderFold ?
46 | Switch Theme 47 | 48 |
: ''} 49 |
50 | ) 51 | } 52 | 53 | Sider.propTypes = { 54 | menu: PropTypes.array, 55 | siderFold: PropTypes.bool, 56 | darkTheme: PropTypes.bool, 57 | changeTheme: PropTypes.func, 58 | navOpenKeys: PropTypes.array, 59 | changeOpenKeys: PropTypes.func, 60 | } 61 | 62 | export default Sider 63 | -------------------------------------------------------------------------------- /src/router.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Switch, Route, Redirect, routerRedux } from 'dva/router' 4 | import dynamic from 'dva/dynamic' 5 | import App from './routes/app' 6 | import { defaultPageInfo } from './config'; 7 | import { DvaApp, History } from '@ts-types'; 8 | 9 | const { ConnectedRouter } = routerRedux 10 | 11 | type Props = { 12 | history: History 13 | app: DvaApp 14 | } 15 | 16 | const Routers = function ({ history, app }: Props) { 17 | const error = dynamic({ 18 | app, 19 | component: () => import('./routes/error'), 20 | }) 21 | const routes = [ 22 | { 23 | path: '/home', 24 | models: () => [import('./models/home')], 25 | component: () => import('./routes/home'), 26 | }, { 27 | path: '/login', 28 | models: () => [import('./models/login')], 29 | component: () => import('./routes/login'), 30 | } 31 | ] 32 | 33 | return ( 34 | 35 | 36 | 37 | ()} /> 38 | { 39 | routes.map(({ path, ...dynamics }, key) => ( 40 | 49 | )) 50 | } 51 | 52 | 53 | 54 | 55 | ) 56 | } 57 | 58 | Routers.propTypes = { 59 | history: PropTypes.object, 60 | app: PropTypes.object, 61 | } 62 | 63 | export default Routers 64 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | import cloneDeep from 'lodash/cloneDeep' 3 | 4 | export { default as request } from './request' 5 | export { default as subscribe } from './subscribe' 6 | export { default as strTemplate } from './strTemplate' 7 | export { default as storage } from './storage' 8 | export { color } from './theme' 9 | 10 | 11 | /** 12 | * 解析浏览器地址栏 url 的 search 的某个 name 的值 13 | * @param {String} 14 | * @return {String} 15 | */ 16 | 17 | export const queryURL = (name: string) => { 18 | let reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i') 19 | let r = window.location.search.substr(1).match(reg) 20 | if (r != null) return decodeURI(r[2]) 21 | return null 22 | } 23 | 24 | /** 25 | * 数组内查询 26 | * @param {array} array 27 | * @param {String} id 28 | * @param {String} keyAlias 29 | * @return {Array} 30 | */ 31 | export const queryArray = (array, key, keyAlias = 'key') => { 32 | if (!(array instanceof Array)) { 33 | return null 34 | } 35 | const item = array.filter(_ => _[keyAlias] === key) 36 | if (item.length) { 37 | return item[0] 38 | } 39 | return null 40 | } 41 | 42 | /** 43 | * 数组格式转树状结构 44 | * @param {array} array 45 | * @param {String} id 46 | * @param {String} pid 47 | * @param {String} children 48 | * @return {Array} 49 | */ 50 | export const arrayToTree = (array, id = 'id', pid = 'pid', children = 'children') => { 51 | let data = cloneDeep(array) 52 | let result = [] 53 | let hash = {} 54 | data.forEach((item, index) => { 55 | hash[data[index][id]] = data[index] 56 | }) 57 | 58 | data.forEach((item) => { 59 | let hashVP = hash[item[pid]] 60 | if (hashVP) { 61 | !hashVP[children] && (hashVP[children] = []) 62 | hashVP[children].push(item) 63 | } else { 64 | result.push(item) 65 | } 66 | }) 67 | return result 68 | } 69 | -------------------------------------------------------------------------------- /src/public/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "antdadmin"; 3 | src: url('iconfont.eot?t=1494298210172'); /* IE9*/ 4 | src: url('iconfont.eot?t=1494298210172#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('iconfont.woff?t=1494298210172') format('woff'), /* chrome, firefox */ 6 | url('iconfont.ttf?t=1494298210172') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1494298210172#antdadmin') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .antdadmin { 11 | font-family:"antdadmin" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-home:before { content: "\e600"; } 19 | 20 | .icon-user:before { content: "\e601"; } 21 | 22 | .icon-timelimit:before { content: "\e602"; } 23 | 24 | .icon-shopcart:before { content: "\e603"; } 25 | 26 | .icon-message:before { content: "\e604"; } 27 | 28 | .icon-remind:before { content: "\e605"; } 29 | 30 | .icon-service:before { content: "\e606"; } 31 | 32 | .icon-shop:before { content: "\e607"; } 33 | 34 | .icon-sweep:before { content: "\e609"; } 35 | 36 | .icon-express:before { content: "\e60a"; } 37 | 38 | .icon-payment:before { content: "\e60b"; } 39 | 40 | .icon-search:before { content: "\e60c"; } 41 | 42 | .icon-feedback:before { content: "\e60d"; } 43 | 44 | .icon-pencil:before { content: "\e60e"; } 45 | 46 | .icon-setting:before { content: "\e60f"; } 47 | 48 | .icon-refund:before { content: "\e610"; } 49 | 50 | .icon-delete:before { content: "\e611"; } 51 | 52 | .icon-star:before { content: "\e612"; } 53 | 54 | .icon-heart:before { content: "\e613"; } 55 | 56 | .icon-share:before { content: "\e615"; } 57 | 58 | .icon-location:before { content: "\e616"; } 59 | 60 | .icon-position:before { content: "\e617"; } 61 | 62 | .icon-console:before { content: "\e618"; } 63 | 64 | .icon-mobile:before { content: "\e61a"; } 65 | 66 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const CopyWebpackPlugin = require('copy-webpack-plugin') 3 | const webpack = require('webpack') 4 | 5 | module.exports = (webpackConfig, env) => { 6 | const production = env === 'production' 7 | // FilenameHash 8 | webpackConfig.output.chunkFilename = '[name].[chunkhash].js' 9 | 10 | if (production) { 11 | if (webpackConfig.module) { 12 | // ClassnameHash 13 | webpackConfig.module.rules.map((item) => { 14 | if (String(item.test) === '/\\.less$/' || String(item.test) === '/\\.css/') { 15 | item.use.filter(iitem => iitem.loader === 'css')[0].options.localIdentName = '[hash:base64:5]' 16 | } 17 | return item 18 | }) 19 | } 20 | webpackConfig.plugins.push( 21 | new webpack.LoaderOptionsPlugin({ 22 | minimize: true, 23 | debug: false, 24 | }) 25 | ) 26 | } 27 | 28 | webpackConfig.plugins = webpackConfig.plugins.concat([ 29 | new CopyWebpackPlugin([ 30 | { 31 | from: 'src/public', 32 | to: production ? '../' : webpackConfig.output.outputPath, 33 | }, 34 | ]), 35 | new HtmlWebpackPlugin({ 36 | template: `${__dirname}/src/entry.ejs`, 37 | filename: production ? '../index.html' : 'index.html', 38 | minify: production ? { 39 | collapseWhitespace: true, 40 | } : null, 41 | hash: true, 42 | headScripts: production ? null : ['/roadhog.dll.js'], 43 | }), 44 | ]) 45 | 46 | webpackConfig.resolve.alias = { 47 | '@components': `${__dirname}/src/components`, 48 | '@models': `${__dirname}/src/models`, 49 | '@services': `${__dirname}/src/services`, 50 | '@utils': `${__dirname}/src/utils`, 51 | '@config': `${__dirname}/src/config`, 52 | '@ts-types': `${__dirname}/src/ts-types`, 53 | '@enums': `${__dirname}/src/utils/enums`, 54 | themes: `${__dirname}/src/themes`, 55 | } 56 | 57 | return webpackConfig 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Layout/Header.less: -------------------------------------------------------------------------------- 1 | @import "~themes/vars"; 2 | 3 | .header { 4 | :global { 5 | .ant-menu-submenu-title { 6 | height: 56px; 7 | } 8 | 9 | .ant-menu-horizontal { 10 | line-height: 56px; 11 | 12 | & > .ant-menu-submenu:hover { 13 | color: #1890ff; 14 | background-color: rgba(24, 144, 255, 0.15); 15 | } 16 | } 17 | 18 | .ant-menu { 19 | border-bottom: none; 20 | height: 56px; 21 | } 22 | 23 | .ant-menu-horizontal > .ant-menu-submenu { 24 | top: 0; 25 | margin-top: 0; 26 | } 27 | 28 | .ant-menu-horizontal > .ant-menu-item, 29 | .ant-menu-horizontal > .ant-menu-submenu { 30 | border-bottom: none; 31 | } 32 | 33 | .ant-menu-horizontal > .ant-menu-item-active, 34 | .ant-menu-horizontal > .ant-menu-item-open, 35 | .ant-menu-horizontal > .ant-menu-item-selected, 36 | .ant-menu-horizontal > .ant-menu-item:hover, 37 | .ant-menu-horizontal > .ant-menu-submenu-active, 38 | .ant-menu-horizontal > .ant-menu-submenu-open, 39 | .ant-menu-horizontal > .ant-menu-submenu-selected, 40 | .ant-menu-horizontal > .ant-menu-submenu:hover { 41 | border-bottom: none; 42 | } 43 | } 44 | box-shadow: @shadow-2; 45 | position: relative; 46 | display: flex; 47 | justify-content: space-between; 48 | height: 56px; 49 | z-index: 9; 50 | display: flex; 51 | align-items: center; 52 | background-color: #fff; 53 | 54 | .rightWarpper { 55 | display: flex; 56 | padding-right: 16px; 57 | } 58 | 59 | } 60 | 61 | .popovermenu { 62 | width: 280px; 63 | margin-left: 6px; 64 | 65 | :global .ant-popover-inner-content { 66 | padding: 0; 67 | 68 | .ant-menu-inline .ant-menu-item, 69 | .ant-menu-vertical .ant-menu-item { 70 | border-right: 0; 71 | } 72 | 73 | .ant-menu-inline .ant-menu-item-selected, 74 | .ant-menu-inline .ant-menu-selected { 75 | border-right: 0; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/routes/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'dva' 4 | import { Button, Row, Form, Input } from 'antd' 5 | import config from '@config' 6 | import styles from './index.less' 7 | 8 | const FormItem = Form.Item 9 | 10 | const Login = ({ 11 | loading, 12 | dispatch, 13 | form: { 14 | getFieldDecorator, 15 | validateFieldsAndScroll, 16 | }, 17 | }) => { 18 | function handleOk () { 19 | validateFieldsAndScroll((errors, values) => { 20 | if (errors) { 21 | return 22 | } 23 | dispatch({ type: 'login/login', payload: values }) 24 | }) 25 | } 26 | 27 | return ( 28 |
29 |
30 | logo 31 | {config.name} 32 |
33 |
34 | 35 | {getFieldDecorator('username', { 36 | rules: [ 37 | { 38 | required: true, 39 | }, 40 | ], 41 | })()} 42 | 43 | 44 | {getFieldDecorator('password', { 45 | rules: [ 46 | { 47 | required: true, 48 | }, 49 | ], 50 | })()} 51 | 52 | 53 | 56 |

57 | Username:guest 58 | Password:guest 59 |

60 |
61 | 62 |
63 |
64 | ) 65 | } 66 | 67 | Login.propTypes = { 68 | form: PropTypes.object, 69 | dispatch: PropTypes.func, 70 | loading: PropTypes.object, 71 | } 72 | 73 | export default connect(({ loading }) => ({ loading }))(Form.create()(Login)) 74 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react" 4 | ], 5 | "rules": { 6 | "align": false, 7 | "ban": false, 8 | "class-name": true, 9 | "jsx-curly-spacing": false, 10 | "jsx-boolean-value": false, 11 | "comment-format": false, 12 | "curly": false, 13 | "eofline": false, 14 | "forin": true, 15 | "indent": false, 16 | "interface-name": [ 17 | true, 18 | "never-prefix" 19 | ], 20 | "jsdoc-format": true, 21 | "jsx-no-lambda": false, 22 | "jsx-no-multiline-js": false, 23 | "label-position": true, 24 | "max-line-length": false, 25 | "no-any": false, 26 | "no-arg": true, 27 | "no-bitwise": true, 28 | "no-console": false, 29 | "no-consecutive-blank-lines": false, 30 | "no-construct": true, 31 | "no-debugger": true, 32 | "no-duplicate-variable": true, 33 | "no-empty": true, 34 | "no-eval": true, 35 | "no-shadowed-variable": false, 36 | "no-string-literal": true, 37 | "no-switch-case-fall-through": true, 38 | "no-trailing-whitespace": false, 39 | "no-unused-expression": false, 40 | "no-use-before-declare": true, 41 | "one-line": false, 42 | "quotemark": [ 43 | true, 44 | "single", 45 | "jsx-double" 46 | ], 47 | "radix": true, 48 | "semicolon": false, 49 | "switch-default": true, 50 | "trailing-comma": false, 51 | "triple-equals": [ 52 | true, 53 | "allow-null-check" 54 | ], 55 | "typedef": [ 56 | true, 57 | "parameter", 58 | "property-declaration" 59 | ], 60 | "typedef-whitespace": [ 61 | true, 62 | { 63 | "call-signature": "nospace", 64 | "index-signature": "nospace", 65 | "parameter": "nospace", 66 | "property-declaration": "nospace", 67 | "variable-declaration": "nospace" 68 | } 69 | ], 70 | "variable-name": [ 71 | true, 72 | "ban-keywords", 73 | "check-format", 74 | "allow-leading-underscore", 75 | "allow-pascal-case" 76 | ], 77 | "whitespace": [ 78 | true, 79 | "check-branch", 80 | "check-decl", 81 | "check-module", 82 | "check-operator", 83 | "check-separator", 84 | "check-type", 85 | "check-typecast" 86 | ] 87 | } 88 | } -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const beautify = require('js-beautify').js_beautify 4 | const config = require('./package.json') 5 | 6 | const dist = path.join(`${__dirname}/dist`) 7 | const maxVersion = 5 8 | 9 | const writeVersion = () => new Promise((resolve, reject) => { 10 | const { version } = config 11 | const numbers = version.split('.') 12 | numbers[2] = Number(numbers[2]) + 1 13 | config.version = numbers.join('.') 14 | 15 | fs.writeFile(path.join(__dirname, 'package.json'), beautify(JSON.stringify(config), { indent_size: 2 }), (err) => { 16 | if (err) { 17 | reject() 18 | } 19 | resolve() 20 | console.log(`version: ${config.version}`) 21 | }) 22 | }) 23 | 24 | const removeFolder = (folderPath) => { 25 | let files = [] 26 | if (fs.existsSync(folderPath)) { 27 | files = fs.readdirSync(folderPath) 28 | files.forEach((file) => { 29 | const curPath = `${folderPath}/${file}` 30 | if (fs.statSync(curPath).isDirectory()) { 31 | removeFolder(curPath) 32 | } else { 33 | fs.unlinkSync(curPath) 34 | } 35 | }) 36 | fs.rmdirSync(folderPath) 37 | } 38 | } 39 | 40 | const start = async () => { 41 | const files = fs.readdirSync(dist) 42 | const promises = files.map(file => new Promise((resolve, reject) => { 43 | fs.stat(`${dist}/${file}`, (err, stats) => { 44 | if (err) { 45 | reject() 46 | } else { 47 | resolve(!stats.isFile() 48 | ? file 49 | : null) 50 | } 51 | }) 52 | })) 53 | 54 | const result = await Promise.all(promises) 55 | 56 | const folders = result.filter((item) => { 57 | if (item) { 58 | return item.split('.').length === 3 59 | } 60 | return false 61 | }).sort((a, b) => { 62 | const an = a.split('.').map(_ => Number(_)) 63 | const bn = b.split('.').map(_ => Number(_)) 64 | if (an[0] === bn[0]) { 65 | if (an[1] === bn[1]) { 66 | return an[2] < bn[2] 67 | } 68 | return an[1] < bn[1] 69 | } 70 | return an[0] < bn[0] 71 | }).filter((item, index) => { 72 | return index > (maxVersion - 1) 73 | }) 74 | 75 | for (const item of folders) { 76 | await removeFolder(`${dist}/${item}`) 77 | } 78 | 79 | await writeVersion() 80 | } 81 | 82 | start() 83 | -------------------------------------------------------------------------------- /src/components/Layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Menu, Icon, Popover, Layout } from 'antd' 4 | import classnames from 'classnames' 5 | import styles from './Header.less' 6 | import Menus from './Menu' 7 | 8 | const { SubMenu } = Menu 9 | 10 | const Header = ({ 11 | user, logout, switchSider, siderFold, isNavbar, menuPopoverVisible, location, switchMenuPopover, navOpenKeys, changeOpenKeys, menu, 12 | }) => { 13 | let handleClickMenu = e => e.key === 'logout' && logout() 14 | const menusProps = { 15 | menu, 16 | siderFold: false, 17 | darkTheme: false, 18 | isNavbar, 19 | handleClickNavMenu: switchMenuPopover, 20 | location, 21 | navOpenKeys, 22 | changeOpenKeys, 23 | } 24 | return ( 25 | 26 | {isNavbar 27 | ? }> 28 |
29 | 30 |
31 |
32 | :
36 | 37 |
} 38 |
39 | 40 | 45 | 46 | {user.username} 47 | } 48 | > 49 | 50 | Sign out 51 | 52 | 53 | 54 |
55 |
56 | ) 57 | } 58 | 59 | Header.propTypes = { 60 | menu: PropTypes.array, 61 | user: PropTypes.object, 62 | logout: PropTypes.func, 63 | switchSider: PropTypes.func, 64 | siderFold: PropTypes.bool, 65 | isNavbar: PropTypes.bool, 66 | menuPopoverVisible: PropTypes.bool, 67 | location: PropTypes.object, 68 | switchMenuPopover: PropTypes.func, 69 | navOpenKeys: PropTypes.array, 70 | changeOpenKeys: PropTypes.func, 71 | } 72 | 73 | export default Header 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于Dva和TypeScript的后台管理系统框架 2 | [![React](https://img.shields.io/badge/react-^16.0.0-brightgreen.svg?style=flat-square)](https://github.com/facebook/react) 3 | [![roadhog](https://img.shields.io/badge/roadhog-^1.2.1-yellowgreen.svg?style=flat-square)](https://github.com/sorrycc/roadhog) 4 | [![dva](https://img.shields.io/badge/dva-^2.0.4-orange.svg?style=flat-square)](https://github.com/dvajs/dva) 5 | [![Ant Design](https://img.shields.io/badge/ant--design-^3.0.0-yellowgreen.svg?style=flat-square)](https://github.com/ant-design/ant-design) 6 | 7 | ## 介绍 8 | 9 | - [dva](https://github.com/dvajs/dva) 基于 [redux](https://github.com/reactjs/redux)、[redux-saga](https://github.com/redux-saga/redux-saga) 和 [react-router](https://github.com/ReactTraining/react-router) 的轻量级前端框架。 10 | - [roadhog](https://github.com/sorrycc/roadhog) 开箱即用的 react 应用开发工具,内置 css-modules、babel、postcss、HMR 等 11 | - [typescript](https://github.com/Microsoft/TypeScript) JS的强类型版本 12 | - UI库是[Ant Design](https://ant.design/docs/react/introduce-cn) 13 | - 用[tslint](https://github.com/palantir/tslint)做代码规范 14 | 15 | ## 安装 16 | 17 | ```bash 18 | yarn 19 | # or 20 | npm install 21 | ``` 22 | 23 | ## 开发 24 | 25 | ```bash 26 | npm run dev 27 | ``` 28 | 29 | ## 构建 30 | 31 | ```bash 32 | npm run build 33 | ``` 34 | 35 | ## 项目目录 36 | 37 | ```bash 38 | ├── /dist/ # 项目输出目录 39 | ├── /mock/ # 数据mock 40 | ├── /src/ # 项目源码目录 41 | │ ├── /public/ # 公共文件,编译时copy至dist目录 42 | │ ├── /components/ # UI组件及UI相关方法 43 | │ │ ├── /Component/ # 单个UI组件目录 44 | │ │ │ ├── index.less # 单个UI组件的样式 45 | │ │ │ └── index.tsx # 单个UI组件 46 | │ │ └── index.tsx # UI组件对外输出口 47 | │ ├── /routes/ # 路由组件 48 | │ │ └── app.tsx # 路由入口 49 | │ ├── /models/ # 数据模型 50 | │ ├── /services/ # 数据接口 51 | │ ├── /themes/ # 项目样式 52 | │ ├── /interfaces/ # TS接口文件目录 53 | │ │ └── index.tsx # 定义全局TS接口,如models的接口等 54 | │ ├── /configs/ # 项目常规配置 55 | │ │ └── Apis.ts # api配置 56 | │ ├── /utils/ # 工具函数 57 | │ │ └── request.js # 异步请求函数 58 | │ ├── route.tsx # 路由配置 59 | │ ├── index.tsx # 入口文件 60 | │ ├── index.less # 全局样式 61 | │ └── index.ejs # 入口html 62 | ├── package.json # 项目信息 63 | ├── theme.config.js # 主题样式配置引入文件 64 | ├── tsconfig.json # TypeScript配置 65 | ├── alias.config.js # 配置webpackConfig.resolve.alias 66 | ├── .roadhogrc.mock.js # 配置mock 67 | ├── globals.d.ts # 配置TS全局的声明文件 68 | ├── tslint.json # TSlint配置 69 | └── webpackrc.js # roadhog配置 70 | ``` 71 | -------------------------------------------------------------------------------- /src/components/Layout/Bread.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Breadcrumb, Icon } from 'antd' 4 | import { Link } from 'react-router-dom' 5 | import pathToRegexp from 'path-to-regexp' 6 | import { queryArray } from '@utils' 7 | import styles from './Layout.less' 8 | 9 | type MenuItem = { 10 | id: number 11 | icon: string 12 | name: string 13 | route: string 14 | bpid?: number 15 | mpid?: number 16 | } 17 | 18 | type Props = { 19 | location: Location 20 | menu: MenuItem[] 21 | } 22 | 23 | // #----------- 上: ts类型定义 ----------- 分割线 ----------- 下: JS代码 ----------- 24 | 25 | const Bread: FC = ({ menu, location }) => { 26 | // 匹配当前路由 27 | let pathArray = [] 28 | let current 29 | for (let index in menu) { 30 | if (menu[index].route && pathToRegexp(menu[index].route).exec(location.pathname)) { 31 | current = menu[index] 32 | break 33 | } 34 | } 35 | 36 | const getPathArray = (item) => { 37 | pathArray.unshift(item) 38 | if (item.bpid || item.mpid) { 39 | getPathArray(queryArray(menu, item.bpid || item.mpid, 'id')) 40 | } 41 | } 42 | 43 | let paramMap = {} 44 | if (!current) { 45 | pathArray.push(menu[0] || { 46 | id: 1, 47 | icon: 'laptop', 48 | name: '首页', 49 | }) 50 | pathArray.push({ 51 | id: 404, 52 | name: 'Not Found', 53 | }) 54 | } else { 55 | getPathArray(current) 56 | 57 | let keys = [] 58 | let values = pathToRegexp(current.route, keys).exec(location.pathname.replace('#', '')) 59 | if (keys.length) { 60 | keys.forEach((currentValue, index) => { 61 | if (typeof currentValue.name !== 'string') { 62 | return 63 | } 64 | paramMap[currentValue.name] = values[index + 1] 65 | }) 66 | } 67 | } 68 | 69 | // 递归查找父级 70 | const breads = pathArray.map((item, key) => { 71 | const content = ( 72 | {item.icon 73 | ? 74 | : ''}{item.name} 75 | ) 76 | return ( 77 | 78 | {((pathArray.length - 1) !== key) 79 | ? 80 | {content} 81 | 82 | : content} 83 | 84 | ) 85 | }) 86 | 87 | return ( 88 |
89 | 90 | {breads} 91 | 92 |
93 | ) 94 | } 95 | 96 | Bread.propTypes = { 97 | menu: PropTypes.array, 98 | } 99 | 100 | export default Bread 101 | -------------------------------------------------------------------------------- /src/themes/index.less: -------------------------------------------------------------------------------- 1 | @import "~themes/vars"; 2 | 3 | body { 4 | height: 100%; 5 | overflow-y: hidden; 6 | background-color: #f8f8f8; 7 | } 8 | 9 | ::-webkit-scrollbar-thumb { 10 | background-color: #e6e6e6; 11 | } 12 | 13 | ::-webkit-scrollbar { 14 | width: 8px; 15 | height: 8px; 16 | } 17 | 18 | :global { 19 | .ant-breadcrumb { 20 | & > span { 21 | &:last-child { 22 | color: #999; 23 | font-weight: normal; 24 | } 25 | } 26 | } 27 | 28 | .ant-breadcrumb-link { 29 | .anticon + span { 30 | margin-left: 4px; 31 | } 32 | } 33 | 34 | .ant-table { 35 | .ant-table-thead > tr > th { 36 | text-align: center; 37 | } 38 | 39 | .ant-table-tbody > tr > td { 40 | text-align: center; 41 | } 42 | 43 | &.ant-table-small { 44 | .ant-table-thead > tr > th { 45 | background: #f7f7f7; 46 | } 47 | 48 | .ant-table-body > table { 49 | padding: 0; 50 | } 51 | } 52 | } 53 | 54 | .ant-table-pagination { 55 | float: none!important; 56 | display: table; 57 | margin: 16px auto !important; 58 | } 59 | 60 | .ant-popover-inner { 61 | border: none; 62 | border-radius: 0; 63 | box-shadow: 0 0 20px rgba(100, 100, 100, 0.2); 64 | } 65 | 66 | .vertical-center-modal { 67 | display: flex; 68 | align-items: center; 69 | justify-content: center; 70 | 71 | .ant-modal { 72 | top: 0; 73 | 74 | .ant-modal-body { 75 | max-height: 80vh; 76 | overflow-y: auto; 77 | } 78 | } 79 | } 80 | 81 | .ant-form-item-control { 82 | vertical-align: middle; 83 | } 84 | 85 | .ant-modal-mask { 86 | background-color: rgba(55, 55, 55, 0.2); 87 | } 88 | 89 | .ant-modal-content { 90 | box-shadow: none; 91 | } 92 | 93 | .ant-select-dropdown-menu-item { 94 | padding: 12px 16px !important; 95 | } 96 | 97 | .margin-right { 98 | margin-right: 16px; 99 | } 100 | 101 | a:focus { 102 | text-decoration: none; 103 | } 104 | } 105 | @media (min-width: 1600px) { 106 | :global { 107 | .ant-col-xl-48 { 108 | width: 20%; 109 | } 110 | 111 | .ant-col-xl-96 { 112 | width: 40%; 113 | } 114 | } 115 | } 116 | @media (max-width: 767px) { 117 | :global { 118 | .ant-pagination-item, 119 | .ant-pagination-next, 120 | .ant-pagination-options, 121 | .ant-pagination-prev { 122 | margin-bottom: 8px; 123 | } 124 | 125 | .ant-card { 126 | .ant-card-head { 127 | padding: 0 12px; 128 | } 129 | 130 | .ant-card-body { 131 | padding: 12px; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/svg/emoji/tired.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "antd-admin", 4 | "version": "4.3.9", 5 | "dependencies": { 6 | "antd": "3.0.3", 7 | "axios": "^0.21.1", 8 | "babel-polyfill": "^6.23.0", 9 | "classnames": "^2.2.5", 10 | "cross-env": "^5.0.5", 11 | "d3-shape": "^1.2.0", 12 | "draftjs-to-markdown": "^0.5.0", 13 | "dva": "2.1.0", 14 | "dva-loading": "^1.0.4", 15 | "echarts": "^3.7.2", 16 | "echarts-for-react": "^2.0.0", 17 | "echarts-gl": "^1.0.0-beta.5", 18 | "echarts-liquidfill": "^1.1.0", 19 | "ejs-loader": "^0.3.0", 20 | "highcharts-exporting": "^0.1.2", 21 | "highcharts-more": "^0.1.2", 22 | "history": "^4.7.2", 23 | "js-beautify": "^1.6.14", 24 | "jsonp": "^0.2.1", 25 | "less-vars-to-js": "^1.1.2", 26 | "lodash": "^4.17.4", 27 | "md5": "^2.2.1", 28 | "mockjs": "^1.0.1-beta3", 29 | "nprogress": "^0.2.0", 30 | "path-to-regexp": "^2.1.0", 31 | "prop-types": "^15.5.10", 32 | "qs": "^6.5.0", 33 | "query-string": "^5.0.0", 34 | "rc-tween-one": "^1.3.1", 35 | "react": "^16.2.0", 36 | "react-countup": "^3.0.2", 37 | "react-dom": "^16.2.0", 38 | "react-draft-wysiwyg": "^1.10.0", 39 | "react-helmet": "^5.1.3", 40 | "react-highcharts": "^15.0.0", 41 | "recharts": "^1.0.0-beta.0" 42 | }, 43 | "devDependencies": { 44 | "@redux-saga/types": "^1.1.0", 45 | "@types/node": "^12.11.7", 46 | "@types/chai": "^4.0.8", 47 | "@types/dva": "^1.1.0", 48 | "@types/history": "^4.7.3", 49 | "@types/mockjs": "^1.0.0", 50 | "@types/react": "^16.0.15", 51 | "babel-eslint": "^8.1.0", 52 | "babel-plugin-dev-expression": "^0.2.1", 53 | "babel-plugin-dva-hmr": "^0.4.0", 54 | "babel-plugin-import": "^1.1.1", 55 | "babel-plugin-transform-runtime": "^6.9.0", 56 | "babel-runtime": "^6.9.2", 57 | "copy-webpack-plugin": "^4.0.1", 58 | "draftjs-to-html": "^0.8.1", 59 | "dva-model-extend": "^0.1.1", 60 | "eslint": "^4.1.1", 61 | "eslint-config-airbnb": "^16.1.0", 62 | "eslint-import-resolver-node": "^0.3.1", 63 | "eslint-loader": "^1.9.0", 64 | "eslint-plugin-import": "^2.6.1", 65 | "eslint-plugin-jsx-a11y": "^6.0.2", 66 | "eslint-plugin-react": "^7.1.0", 67 | "html-webpack-plugin": "^2.29.0", 68 | "redbox-react": "^1.2.10", 69 | "roadhog": "1.3.1", 70 | "ts-node": "^3.3.0", 71 | "tslint": "^5.9.1", 72 | "tslint-react": "^3.5.1", 73 | "typescript": "^2.6.2", 74 | "typescript-eslint-parser": "^13.0.0" 75 | }, 76 | "pre-commit": [ 77 | "lint" 78 | ], 79 | "scripts": { 80 | "dev": "cross-env BROWSER=none HOST=0.0.0.0 roadhog server", 81 | "start": "roadhog buildDll && cross-env BROWSER=none HOST=0.0.0.0 roadhog server", 82 | "lint": "eslint --fix --ext .js src", 83 | "build": "roadhog build", 84 | "build:dll": "roadhog buildDll", 85 | "build:new": "node version && roadhog build" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/svg/emoji/surprised.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/emoji/smirking.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | 2 | import axios, { AxiosRequestConfig, AxiosResponse, AxiosPromise } from 'axios' 3 | import cloneDeep from 'lodash/cloneDeep' 4 | import { message } from 'antd' 5 | import md5 from 'md5' 6 | import qs from 'qs' 7 | import storage from './storage' 8 | import sortByKey from './sortByKey' 9 | 10 | export default function request(url: string, options: AxiosRequestConfig = {}): AxiosPromise { 11 | let { data } = options 12 | 13 | options.url = url 14 | if (data) { 15 | options.params = cloneDeep(data) 16 | } 17 | 18 | return axios(options) 19 | .then(response => { 20 | const { status, statusText } = response 21 | const successed = checkRspStatus(status) 22 | if (successed) { 23 | return Promise.resolve({ 24 | ...response, 25 | success: true, 26 | message: statusText, 27 | statusCode: status, 28 | data: response.data || {}, 29 | }) 30 | } 31 | 32 | // 错误提示 33 | tipError(response) 34 | 35 | const error = { 36 | name: 'http error', 37 | message: 'http response status error', 38 | config: options, 39 | code: `${status}`, 40 | response, 41 | isAxiosError: false, 42 | } 43 | return Promise.reject(error) 44 | }) 45 | .catch(error => { 46 | const { response } = error 47 | 48 | // 错误提示 49 | tipError(response || { 50 | ...error, 51 | status: 600 52 | }) 53 | 54 | let msg 55 | let statusCode 56 | 57 | if (response && response instanceof Object) { 58 | const { statusText } = response 59 | statusCode = response.status 60 | msg = response.data.message || statusText 61 | } else { 62 | statusCode = 600 63 | msg = error.message || 'Network Error' 64 | } 65 | 66 | /* eslint-disable */ 67 | return Promise.resolve({ 68 | ...response, 69 | success: false, 70 | status: statusCode, 71 | message: msg, 72 | }) 73 | }) 74 | } 75 | 76 | export function checkRspStatus(status: number) { 77 | if (status >= 200 && status < 300) { 78 | return true; 79 | } 80 | 81 | return false; 82 | } 83 | 84 | function tipError(response: AxiosResponse) { 85 | const status = response.status; 86 | 87 | switch (status) { 88 | case 401: 89 | storage.clear(); 90 | message.error('登录过期,请重新登录'); 91 | break; 92 | 93 | case 400: 94 | message.error('请求错误,请刷新重试'); 95 | break; 96 | 97 | default: 98 | if (status >= 500) { 99 | message.error('网络错误,请刷新重试'); 100 | } 101 | // 注意:其他错误的错误提示需要在业务内自行处理 102 | break; 103 | } 104 | console.error('http返回结果的 status 码错误,错误信息是:', response); 105 | } 106 | 107 | export const encryptMD5 = (value, apiKey = 'apikey=sunlandzlcx') => { 108 | if (qs.stringify(sortByKey(value))) { 109 | return md5(`${apiKey}&${qs.stringify(sortByKey(value), { encode: false })}`) 110 | } else { 111 | return md5(apiKey); 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /src/svg/emoji/unamused.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/emoji/wink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.less: -------------------------------------------------------------------------------- 1 | @import "~themes/vars"; 2 | 3 | :global { 4 | .ant-layout-header { 5 | padding: 0; 6 | } 7 | 8 | .ant-layout-sider { 9 | transition: all 0.3s; 10 | 11 | .ant-menu-root { 12 | height: ~"calc(100vh - 144px)"; 13 | overflow-x: hidden; 14 | border-right: none; 15 | 16 | &::-webkit-scrollbar-thumb { 17 | background-color: transparent; 18 | } 19 | 20 | &:hover { 21 | &::-webkit-scrollbar-thumb { 22 | background-color: rgba(0, 0, 0, 0.2); 23 | } 24 | } 25 | } 26 | 27 | .ant-menu { 28 | .ant-menu-item, 29 | .ant-menu-submenu-title { 30 | overflow: hidden; 31 | white-space: normal; 32 | } 33 | } 34 | } 35 | 36 | .ant-layout-content { 37 | padding: 24px; 38 | } 39 | 40 | .ant-layout-footer { 41 | text-align: center; 42 | padding: 0 24px; 43 | height: 40px; 44 | font-size: 12px; 45 | line-height: 40px; 46 | } 47 | 48 | .ant-back-top { 49 | right: 50px; 50 | } 51 | 52 | .ant-switch.ant-switch-large { 53 | line-height: 24px; 54 | height: 26px; 55 | 56 | &:after, 57 | &:before { 58 | width: 22px; 59 | height: 22px; 60 | } 61 | 62 | &.ant-switch-checked:after, 63 | &.ant-switch-checked:before { 64 | margin-left: -23px; 65 | } 66 | } 67 | 68 | .flex-vertical-center { 69 | display: flex; 70 | align-items: center; 71 | } 72 | 73 | .ant-form-item { 74 | margin-bottom: 12px; 75 | } 76 | } 77 | 78 | .logo { 79 | height: 96px; 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | 84 | img { 85 | width: 40px; 86 | margin-right: 8px; 87 | } 88 | 89 | span { 90 | vertical-align: text-bottom; 91 | font-size: 16px; 92 | text-transform: uppercase; 93 | display: inline-block; 94 | font-weight: 700; 95 | color: @primary-color; 96 | } 97 | } 98 | 99 | .switchtheme { 100 | width: 100%; 101 | position: absolute; 102 | bottom: 0; 103 | height: 48px; 104 | background-color: #fff; 105 | border-top: solid 1px #f8f8f8; 106 | display: flex; 107 | justify-content: space-between; 108 | align-items: center; 109 | padding: 0 16px; 110 | overflow: hidden; 111 | z-index: 9; 112 | transition: all 0.3s; 113 | 114 | span { 115 | white-space: nowrap; 116 | overflow: hidden; 117 | font-size: 12px; 118 | } 119 | 120 | :global { 121 | .anticon { 122 | min-width: 14px; 123 | margin-right: 4px; 124 | font-size: 14px; 125 | } 126 | } 127 | } 128 | 129 | .bread { 130 | margin-bottom: 24px; 131 | 132 | :global { 133 | .ant-breadcrumb { 134 | display: flex; 135 | align-items: center; 136 | } 137 | } 138 | } 139 | 140 | .dark { 141 | .switchtheme { 142 | background-color: #000d18; 143 | border-color: #001629; 144 | } 145 | } 146 | 147 | .light { 148 | :global { 149 | .ant-layout-sider { 150 | background: #fff; 151 | } 152 | } 153 | } 154 | @media (max-width: 767px) { 155 | .bread { 156 | margin-bottom: 12px; 157 | } 158 | 159 | :global { 160 | .ant-layout-content { 161 | padding: 12px; 162 | } 163 | 164 | .ant-back-top { 165 | right: 20px; 166 | bottom: 20px; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/svg/cute/kiss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Layout/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Menu, Icon } from 'antd' 4 | import { Link } from 'react-router-dom' 5 | import { arrayToTree, queryArray } from '@utils' 6 | import pathToRegexp from 'path-to-regexp' 7 | 8 | const Menus = ({ siderFold, darkTheme, navOpenKeys, changeOpenKeys, menu, location }) => { 9 | // 生成树状 10 | const menuTree = arrayToTree(menu.filter(_ => _.mpid !== '-1'), 'id', 'mpid') 11 | const levelMap = {} 12 | 13 | // 递归生成菜单 14 | const getMenus = (menuTreeN, siderFoldN) => { 15 | return menuTreeN.map((item) => { 16 | if (item.children) { 17 | if (item.mpid) { 18 | levelMap[item.id] = item.mpid 19 | } 20 | return ( 21 | 24 | {item.icon && } 25 | {(!siderFoldN || !menuTree.includes(item)) && item.name} 26 | } 27 | > 28 | {getMenus(item.children, siderFoldN)} 29 | 30 | ) 31 | } 32 | return ( 33 | 34 | 35 | {item.icon && } 36 | {(!siderFoldN || !menuTree.includes(item)) && item.name} 37 | 38 | 39 | ) 40 | }) 41 | } 42 | const menuItems = getMenus(menuTree, siderFold) 43 | 44 | // 保持选中 45 | const getAncestorKeys = (key) => { 46 | let map = {} 47 | const getParent = (index) => { 48 | const result = [String(levelMap[index])] 49 | if (levelMap[result[0]]) { 50 | result.unshift(getParent(result[0])[0]) 51 | } 52 | return result 53 | } 54 | for (let index in levelMap) { 55 | if ({}.hasOwnProperty.call(levelMap, index)) { 56 | map[index] = getParent(index) 57 | } 58 | } 59 | return map[key] || [] 60 | } 61 | 62 | const onOpenChange = (openKeys) => { 63 | const latestOpenKey = openKeys.find(key => !navOpenKeys.includes(key)) 64 | const latestCloseKey = navOpenKeys.find(key => !openKeys.includes(key)) 65 | let nextOpenKeys = [] 66 | if (latestOpenKey) { 67 | nextOpenKeys = getAncestorKeys(latestOpenKey).concat(latestOpenKey) 68 | } 69 | if (latestCloseKey) { 70 | nextOpenKeys = getAncestorKeys(latestCloseKey) 71 | } 72 | changeOpenKeys(nextOpenKeys) 73 | } 74 | 75 | let menuProps = !siderFold ? { 76 | onOpenChange, 77 | openKeys: navOpenKeys, 78 | } : {} 79 | 80 | 81 | // 寻找选中路由 82 | let currentMenu 83 | let defaultSelectedKeys 84 | for (let item of menu) { 85 | if (item.route && pathToRegexp(item.route).exec(location.pathname)) { 86 | currentMenu = item 87 | break 88 | } 89 | } 90 | const getPathArray = (array, current, pid, id) => { 91 | let result = [String(current[id])] 92 | const getPath = (item) => { 93 | if (item && item[pid]) { 94 | result.unshift(String(item[pid])) 95 | getPath(queryArray(array, item[pid], id)) 96 | } 97 | } 98 | getPath(current) 99 | return result 100 | } 101 | if (currentMenu) { 102 | defaultSelectedKeys = getPathArray(menu, currentMenu, 'mpid', 'id') 103 | } 104 | 105 | if (!defaultSelectedKeys) { 106 | defaultSelectedKeys = ['1'] 107 | } 108 | 109 | return ( 110 | 116 | {menuItems} 117 | 118 | ) 119 | } 120 | 121 | Menus.propTypes = { 122 | menu: PropTypes.array, 123 | siderFold: PropTypes.bool, 124 | darkTheme: PropTypes.bool, 125 | navOpenKeys: PropTypes.array, 126 | changeOpenKeys: PropTypes.func, 127 | location: PropTypes.object, 128 | } 129 | 130 | export default Menus 131 | -------------------------------------------------------------------------------- /src/svg/cute/proud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/cute/cry.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/App.tsx: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | /* global document */ 3 | import React from 'react' 4 | import NProgress from 'nprogress' 5 | import PropTypes from 'prop-types' 6 | import pathToRegexp from 'path-to-regexp' 7 | import { connect } from 'dva' 8 | import { Loader, MyLayout } from '@components' 9 | import { BackTop, Layout } from 'antd' 10 | import classnames from 'classnames' 11 | import config from '@config' 12 | import { Helmet } from 'react-helmet' 13 | import { withRouter } from 'dva/router' 14 | import Error from './error' 15 | import '../themes/index.less' 16 | import './app.less' 17 | 18 | const { Content, Footer, Sider } = Layout 19 | const { Header, Bread, styles } = MyLayout 20 | const { prefix, openPages } = config 21 | 22 | let lastHref 23 | 24 | const App = ({ 25 | children, dispatch, app, loading, location, 26 | }) => { 27 | const { 28 | user, siderFold, darkTheme, isNavbar, menuPopoverVisible, navOpenKeys, menu, permissions, 29 | } = app 30 | let { pathname } = location 31 | pathname = pathname.startsWith('/') ? pathname : `/${pathname}` 32 | const { iconFontJS, iconFontCSS, logo } = config 33 | const current = menu.filter(item => pathToRegexp(item.route || '').exec(pathname)) 34 | const hasPermission = current.length ? permissions.visit.includes(current[0].id) : false 35 | const { href } = window.location 36 | 37 | if (lastHref !== href) { 38 | NProgress.start() 39 | if (!loading.global) { 40 | NProgress.done() 41 | lastHref = href 42 | } 43 | } 44 | 45 | const headerProps = { 46 | menu, 47 | user, 48 | location, 49 | siderFold, 50 | isNavbar, 51 | menuPopoverVisible, 52 | navOpenKeys, 53 | switchMenuPopover() { 54 | dispatch({ type: 'app/switchMenuPopver' }) 55 | }, 56 | logout() { 57 | dispatch({ type: 'login/logout' }) 58 | }, 59 | switchSider() { 60 | dispatch({ type: 'app/switchSider' }) 61 | }, 62 | changeOpenKeys(openKeys: string[]) { 63 | dispatch({ type: 'app/handleNavOpenKeys', payload: { navOpenKeys: openKeys } }) 64 | }, 65 | } 66 | 67 | const siderProps = { 68 | menu, 69 | location, 70 | siderFold, 71 | darkTheme, 72 | navOpenKeys, 73 | changeTheme() { 74 | dispatch({ type: 'app/switchTheme' }) 75 | }, 76 | changeOpenKeys(openKeys: string[]) { 77 | window.localStorage.setItem(`${prefix}navOpenKeys`, JSON.stringify(openKeys)) 78 | dispatch({ type: 'app/handleNavOpenKeys', payload: { navOpenKeys: openKeys } }) 79 | }, 80 | } 81 | 82 | const breadProps = { 83 | menu, 84 | location, 85 | } 86 | 87 | if (openPages && openPages.includes(pathname)) { 88 | return ( 89 |
90 | 91 | {children} 92 |
93 | ) 94 | } 95 | 96 | return ( 97 |
98 | 99 | 100 | ANTD ADMIN 101 | 102 | 103 | {iconFontJS &&