├── 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 |
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 |

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 |

31 |
{config.name}
32 |
33 |
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 |
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 | [](https://github.com/facebook/react)
3 | [](https://github.com/sorrycc/roadhog)
4 | [](https://github.com/dvajs/dva)
5 | [](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 |
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 && }
104 | {iconFontCSS && }
105 |
106 |
107 |
108 | {!isNavbar &&
113 | {siderProps.menu.length === 0 ? null : }
114 | }
115 |
119 | document.getElementById('mainContainer')} />
120 |
121 |
122 |
123 | {hasPermission ? children : }
124 |
125 |
128 |
129 |
130 |
131 | )
132 | }
133 |
134 | App.propTypes = {
135 | children: PropTypes.element.isRequired,
136 | location: PropTypes.object,
137 | dispatch: PropTypes.func,
138 | app: PropTypes.object,
139 | loading: PropTypes.object,
140 | }
141 |
142 | export default withRouter(connect(({ app, loading }) => ({ app, loading }))(App))
143 |
--------------------------------------------------------------------------------
/src/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/svg/cute/shy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/models/app.ts:
--------------------------------------------------------------------------------
1 | /* global window */
2 | /* global document */
3 | import { routerRedux } from 'dva/router'
4 | import config from '@config'
5 | import { storage } from '@utils'
6 | import { EnumRoleType } from '@enums'
7 | import * as appService from '@services/app'
8 | import * as menusService from '@services/menus'
9 | import queryString from 'query-string'
10 | import { modelExtend } from './common';
11 | import { ReduxSagaEffects, DvaSetupParams, ReduxAction } from '../ts-types';
12 |
13 | const { prefix, defaultPageInfo, loginRouter } = config;
14 |
15 | type MenuItem = {
16 | id: number
17 | icon: string
18 | name: string
19 | router: string
20 | }
21 | type State = {
22 | user: Object
23 | permissions: {
24 | visit: any[]
25 | }
26 | menu: MenuItem[]
27 | menuPopoverVisible: boolean
28 | siderFold: boolean
29 | darkTheme: boolean
30 | isNavbar: boolean
31 | navOpenKeys: string[]
32 | locationPathname: string
33 | locationQuery: Object
34 | }
35 |
36 | const namespace = 'app';
37 |
38 | export default modelExtend({
39 | namespace,
40 | state: {
41 | user: {},
42 | permissions: {
43 | visit: [],
44 | },
45 | menu: [
46 | {
47 | id: defaultPageInfo.id,
48 | icon: defaultPageInfo.icon,
49 | name: defaultPageInfo.name,
50 | router: defaultPageInfo.router,
51 | },
52 | ],
53 | menuPopoverVisible: false,
54 | siderFold: storage.getItem(`${prefix}siderFold`) === 'true',
55 | darkTheme: storage.getItem(`${prefix}darkTheme`) === 'true',
56 | isNavbar: document.body.clientWidth < 769,
57 | navOpenKeys: JSON.parse(storage.getItem(`${prefix}navOpenKeys`)) || [],
58 | locationPathname: '',
59 | locationQuery: {},
60 | },
61 | subscriptions: {
62 |
63 | setupHistory({ dispatch, history }: DvaSetupParams) {
64 | history.listen((location) => {
65 | dispatch({
66 | type: 'updateState',
67 | payload: {
68 | locationPathname: location.pathname,
69 | locationQuery: queryString.parse(location.search),
70 | },
71 | })
72 | })
73 | },
74 |
75 | setup({ dispatch }: DvaSetupParams) {
76 | dispatch({ type: 'query' })
77 | let tid
78 | window.onresize = () => {
79 | clearTimeout(tid)
80 | tid = setTimeout(() => {
81 | dispatch({ type: 'changeNavbar' })
82 | }, 300)
83 | }
84 | },
85 |
86 | },
87 | effects: {
88 |
89 | * query(_action: never, { call, put, select }: ReduxSagaEffects) {
90 | const { success, data: { user } } = yield call(appService.query)
91 | const { locationPathname } = yield select(_ => _.app)
92 |
93 | if (success && user) {
94 | const { data: menuList } = yield call(menusService.query)
95 | const { permissions } = user
96 | let menu = menuList
97 | if (permissions.role === EnumRoleType.ADMIN || permissions.role === EnumRoleType.DEVELOPER) {
98 | permissions.visit = menuList.map(item => item.id)
99 | } else {
100 | menu = menuList.filter((item) => {
101 | const cases = [
102 | permissions.visit.includes(item.id),
103 | item.mpid ? permissions.visit.includes(item.mpid) || item.mpid === '-1' : true,
104 | item.bpid ? permissions.visit.includes(item.bpid) : true,
105 | ]
106 | return cases.every(_ => _)
107 | })
108 | }
109 | yield put({
110 | type: 'updateState',
111 | payload: {
112 | user,
113 | permissions,
114 | menu,
115 | },
116 | })
117 | if (window.location.pathname === loginRouter) {
118 | yield put(routerRedux.push({
119 | pathname: defaultPageInfo.router,
120 | }))
121 | }
122 | } else if (config.openPages && config.openPages.indexOf(locationPathname) < 0) {
123 | yield put(routerRedux.push({
124 | pathname: loginRouter,
125 | search: queryString.stringify({
126 | from: locationPathname,
127 | }),
128 | }))
129 | }
130 | },
131 |
132 | * changeNavbar({ payload }: ReduxAction, { put, select }: ReduxSagaEffects) {
133 | const { app } = yield (select(_ => _))
134 | const isNavbar = document.body.clientWidth < 769
135 | if (isNavbar !== app.isNavbar) {
136 | yield put({ type: 'handleNavbar', payload: isNavbar })
137 | }
138 | },
139 |
140 | },
141 | reducers: {
142 | updateState(state: State, { payload }: ReduxAction) {
143 | return {
144 | ...state,
145 | ...payload,
146 | }
147 | },
148 |
149 | switchSider(state: State) {
150 | storage.setItem(`${prefix}siderFold`, !state.siderFold)
151 | return {
152 | ...state,
153 | siderFold: !state.siderFold,
154 | }
155 | },
156 |
157 | switchTheme(state: State) {
158 | storage.setItem(`${prefix}darkTheme`, !state.darkTheme)
159 | return {
160 | ...state,
161 | darkTheme: !state.darkTheme,
162 | }
163 | },
164 |
165 | switchMenuPopver(state: State) {
166 | return {
167 | ...state,
168 | menuPopoverVisible: !state.menuPopoverVisible,
169 | }
170 | },
171 |
172 | handleNavbar(state: State, { payload }: ReduxAction) {
173 | return {
174 | ...state,
175 | isNavbar: payload,
176 | }
177 | },
178 |
179 | handleNavOpenKeys(state: State, { payload: navOpenKeys }: ReduxAction) {
180 | return {
181 | ...state,
182 | ...navOpenKeys,
183 | }
184 | },
185 | },
186 | })
187 |
--------------------------------------------------------------------------------
/src/svg/emoji/tongue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svg/cute/sweat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svg/emoji/zombie.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mock/user.js:
--------------------------------------------------------------------------------
1 | const qs = require('qs')
2 | const Mock = require('mockjs')
3 | const { apis } = require('./common')
4 |
5 | const { apiPrefix } = apis
6 |
7 | let usersListData = Mock.mock({
8 | 'data|80-100': [
9 | {
10 | id: '@id',
11 | name: '@name',
12 | nickName: '@last',
13 | phone: /^1[34578]\d{9}$/,
14 | 'age|11-99': 1,
15 | address: '@county(true)',
16 | isMale: '@boolean',
17 | email: '@email',
18 | createTime: '@datetime',
19 | avatar() {
20 | return Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', this.nickName.substr(0, 1))
21 | },
22 | },
23 | ],
24 | })
25 |
26 |
27 | let database = usersListData.data
28 |
29 | const EnumRoleType = {
30 | ADMIN: 'admin',
31 | DEFAULT: 'guest',
32 | DEVELOPER: 'developer',
33 | }
34 |
35 | const userPermission = {
36 | DEFAULT: {
37 | visit: ['1'],
38 | role: EnumRoleType.DEFAULT,
39 | },
40 | ADMIN: {
41 | role: EnumRoleType.ADMIN,
42 | },
43 | DEVELOPER: {
44 | role: EnumRoleType.DEVELOPER,
45 | },
46 | }
47 |
48 | const adminUsers = [
49 | {
50 | id: 0,
51 | username: 'admin',
52 | password: 'admin',
53 | permissions: userPermission.ADMIN,
54 | }, {
55 | id: 1,
56 | username: 'guest',
57 | password: 'guest',
58 | permissions: userPermission.DEFAULT,
59 | }, {
60 | id: 2,
61 | username: '吴彦祖',
62 | password: '123456',
63 | permissions: userPermission.DEVELOPER,
64 | },
65 | ]
66 |
67 | const queryArray = (array, key, keyAlias = 'key') => {
68 | if (!(array instanceof Array)) {
69 | return null
70 | }
71 | let data
72 |
73 | for (let item of array) {
74 | if (item[keyAlias] === key) {
75 | data = item
76 | break
77 | }
78 | }
79 |
80 | if (data) {
81 | return data
82 | }
83 | return null
84 | }
85 |
86 | const NOTFOUND = {
87 | message: 'Not Found',
88 | documentation_url: 'http://localhost:8000/request',
89 | }
90 |
91 | module.exports = {
92 |
93 | [`POST ${apiPrefix}/user/login`](req, res) {
94 | const { username, password } = req.body
95 | const user = adminUsers.filter(item => item.username === username)
96 |
97 | if (user.length > 0 && user[0].password === password) {
98 | const now = new Date()
99 | now.setDate(now.getDate() + 1)
100 | res.cookie('token', JSON.stringify({ id: user[0].id, deadline: now.getTime() }), {
101 | maxAge: 900000,
102 | httpOnly: true,
103 | })
104 | res.json({ success: true, message: 'Ok' })
105 | } else {
106 | res.status(400).end()
107 | }
108 | },
109 |
110 | [`GET ${apiPrefix}/user/logout`](req, res) {
111 | res.clearCookie('token')
112 | res.status(200).end()
113 | },
114 |
115 | [`GET ${apiPrefix}/user`](req, res) {
116 | const cookie = req.headers.cookie || ''
117 | const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
118 | const response = {}
119 | const user = {}
120 | if (!cookies.token) {
121 | res.status(200).send({ message: 'Not Login' })
122 | return
123 | }
124 | const token = JSON.parse(cookies.token)
125 | if (token) {
126 | response.success = token.deadline > new Date().getTime()
127 | }
128 | if (response.success) {
129 | const userItem = adminUsers.filter(_ => _.id === token.id)
130 | if (userItem.length > 0) {
131 | user.permissions = userItem[0].permissions
132 | user.username = userItem[0].username
133 | user.id = userItem[0].id
134 | }
135 | }
136 | response.user = user
137 | res.json(response)
138 | },
139 |
140 | [`GET ${apiPrefix}/users`](req, res) {
141 | const { query } = req
142 | let { pageSize, page, ...other } = query
143 | pageSize = pageSize || 10
144 | page = page || 1
145 |
146 | let newData = database
147 | for (let key in other) {
148 | if ({}.hasOwnProperty.call(other, key)) {
149 | newData = newData.filter((item) => {
150 | if ({}.hasOwnProperty.call(item, key)) {
151 | if (key === 'address') {
152 | return other[key].every(iitem => item[key].indexOf(iitem) > -1)
153 | } else if (key === 'createTime') {
154 | const start = new Date(other[key][0]).getTime()
155 | const end = new Date(other[key][1]).getTime()
156 | const now = new Date(item[key]).getTime()
157 |
158 | if (start && end) {
159 | return now >= start && now <= end
160 | }
161 | return true
162 | }
163 | return String(item[key]).trim().indexOf(decodeURI(other[key]).trim()) > -1
164 | }
165 | return true
166 | })
167 | }
168 | }
169 |
170 | res.status(200).json({
171 | data: newData.slice((page - 1) * pageSize, page * pageSize),
172 | total: newData.length,
173 | })
174 | },
175 |
176 | [`DELETE ${apiPrefix}/users`](req, res) {
177 | const { ids } = req.body
178 | database = database.filter(item => !ids.some(_ => _ === item.id))
179 | res.status(204).end()
180 | },
181 |
182 |
183 | [`POST ${apiPrefix}/user`](req, res) {
184 | const newData = req.body
185 | newData.createTime = Mock.mock('@now')
186 | newData.avatar = newData.avatar || Mock.Random.image('100x100', Mock.Random.color(), '#757575', 'png', newData.nickName.substr(0, 1))
187 | newData.id = Mock.mock('@id')
188 |
189 | database.unshift(newData)
190 |
191 | res.status(200).end()
192 | },
193 |
194 | [`GET ${apiPrefix}/user/:id`](req, res) {
195 | const { id } = req.params
196 | const data = queryArray(database, id, 'id')
197 | if (data) {
198 | res.status(200).json(data)
199 | } else {
200 | res.status(404).json(NOTFOUND)
201 | }
202 | },
203 |
204 | [`DELETE ${apiPrefix}/user/:id`](req, res) {
205 | const { id } = req.params
206 | const data = queryArray(database, id, 'id')
207 | if (data) {
208 | database = database.filter(item => item.id !== id)
209 | res.status(204).end()
210 | } else {
211 | res.status(404).json(NOTFOUND)
212 | }
213 | },
214 |
215 | [`PATCH ${apiPrefix}/user/:id`](req, res) {
216 | const { id } = req.params
217 | const editItem = req.body
218 | let isExist = false
219 |
220 | database = database.map((item) => {
221 | if (item.id === id) {
222 | isExist = true
223 | return Object.assign({}, item, editItem)
224 | }
225 | return item
226 | })
227 |
228 | if (isExist) {
229 | res.status(201).end()
230 | } else {
231 | res.status(404).json(NOTFOUND)
232 | }
233 | },
234 | }
--------------------------------------------------------------------------------
/src/svg/emoji/vomiting.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.css" {
2 | const content: {
3 | [propName: string]: any
4 | };
5 | export default content;
6 | }
7 |
8 | declare module "*.scss" {
9 | const content: any;
10 | export default content;
11 | }
12 |
13 | declare module "*.less" {
14 | const content: any;
15 | export default content;
16 | }
17 |
18 | declare module "*.json" {
19 | const content: object;
20 | export default content;
21 | }
22 |
23 | interface System {
24 | import(module: string): Promise
25 | }
26 |
27 | declare const System: System
28 |
29 | declare module 'react-async-component';
30 |
31 | interface Window {
32 | __state__: any;
33 | }
34 |
35 | interface CommonElement {
36 | styleName?: string;
37 | [propName: string]: any;
38 | }
39 |
40 | declare namespace JSX {
41 | interface IntrinsicElements {
42 | // 给div元素增加styleName属性,为了兼容 react-css-modules 库
43 | div: CommonElement;
44 | [elemName: string]: any;
45 | }
46 | }
47 |
48 | declare namespace Mocha {
49 | export interface IContextDefinition { }
50 | export interface ITestDefinition { }
51 | export interface ISuiteCallbackContext { }
52 | export interface ITestCallbackContext { }
53 |
54 | export interface ITest {
55 | (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor | void;
56 | }
57 | export interface ISuite {
58 | (target: TFunction): TFunction | void;
59 | }
60 | }
61 |
62 | declare namespace MochaTypeScript {
63 | export interface Suite {
64 | prototype: {
65 | before?: (done?: MochaDone) => void;
66 | after?: (done?: MochaDone) => void;
67 | };
68 | before?: (done?: MochaDone) => void;
69 | after?: (done?: MochaDone) => void;
70 | new(): any;
71 | }
72 | export interface SuiteTrait {
73 | (this: Mocha.ISuiteCallbackContext, ctx: Mocha.ISuiteCallbackContext, ctor: Function): void;
74 | }
75 | export interface TestTrait {
76 | (this: Mocha.ITestCallbackContext, ctx: Mocha.ITestCallbackContext, instance: Object, method: Function): void;
77 | }
78 |
79 | export interface IContextDefinition {
80 | /**
81 | * This is either a single trait overload `(trait: MochaTypeScript.SuiteTrait): ClassDecorator`
82 | * or a class decorator overload `(target: Function): void`.
83 | * Can't figure out proper typing.
84 | */
85 | (args: any): any;
86 | (): ClassDecorator;
87 | (name: string): ClassDecorator;
88 | (name: string, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
89 | (trait: MochaTypeScript.SuiteTrait, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
90 |
91 | only(arg: any): any;
92 | only(): ClassDecorator;
93 | only(name: string): ClassDecorator;
94 | only(name: string, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
95 | only(...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
96 |
97 | skip(arg: any): any;
98 | skip(): ClassDecorator;
99 | skip(name: string): ClassDecorator;
100 | skip(name: string, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
101 | skip(...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
102 | }
103 | export interface ITestDefinition {
104 | (target: Object, propertyKey: string | symbol): void;
105 | (name: string): PropertyDecorator;
106 | (name: string, ...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
107 | (...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
108 |
109 | only(target: Object, propertyKey: string | symbol): void;
110 | only(name: string): PropertyDecorator;
111 | only(name: string, ...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
112 | only(...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
113 |
114 | skip(target: Object, propertyKey: string | symbol): void;
115 | skip(name: string): PropertyDecorator;
116 | skip(name: string, ...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
117 | skip(...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
118 | }
119 | }
120 |
121 | declare module "mocha-typescript" {
122 | export const suite: Mocha.IContextDefinition & MochaTypeScript.IContextDefinition;
123 | export const test: Mocha.ITestDefinition & MochaTypeScript.ITestDefinition;
124 |
125 | export const describe: Mocha.IContextDefinition & MochaTypeScript.IContextDefinition;
126 | export const it: Mocha.ITestDefinition & MochaTypeScript.ITestDefinition;
127 |
128 | export function slow(time: number): PropertyDecorator & ClassDecorator & MochaTypeScript.SuiteTrait & MochaTypeScript.TestTrait;
129 | export function timeout(time: number): PropertyDecorator & ClassDecorator & MochaTypeScript.SuiteTrait & MochaTypeScript.TestTrait;
130 | export function retries(count: number): PropertyDecorator & ClassDecorator & MochaTypeScript.SuiteTrait & MochaTypeScript.TestTrait;
131 |
132 | export function pending(target: Object | TFunction, propertyKey?: string | symbol): void;
133 | export function only(target: Object, propertyKey?: string | symbol): void;
134 | export function skip(target: Object | TFunction, propertyKey?: string | symbol): void;
135 |
136 | export function context(target: Object, propertyKey: string | symbol): void;
137 |
138 | export const skipOnError: MochaTypeScript.SuiteTrait;
139 | }
140 |
141 | declare namespace Mocha {
142 | export interface IContextDefinition {
143 | /**
144 | * This is either a single trait overload `(trait: MochaTypeScript.SuiteTrait): ClassDecorator`
145 | * or a class decorator overload `(target: Function): void`.
146 | * Can't figure out proper typing.
147 | */
148 | (args: any): any;
149 | (): ClassDecorator;
150 | (name: string): ClassDecorator;
151 | (name: string, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
152 | (trait: MochaTypeScript.SuiteTrait, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
153 |
154 | only(arg: any): any;
155 | only(): ClassDecorator;
156 | only(name: string): ClassDecorator;
157 | only(name: string, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
158 | only(...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
159 |
160 | skip(arg: any): any;
161 | skip(): ClassDecorator;
162 | skip(name: string): ClassDecorator;
163 | skip(name: string, ...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
164 | skip(...traits: MochaTypeScript.SuiteTrait[]): ClassDecorator;
165 | }
166 | export interface ITestDefinition {
167 | (target: Object, propertyKey: string | symbol): void;
168 | (name: string): PropertyDecorator;
169 | (name: string, ...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
170 | (...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
171 |
172 | only(target: Object, propertyKey: string | symbol): void;
173 | only(name: string): PropertyDecorator;
174 | only(name: string, ...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
175 | only(...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
176 |
177 | skip(target: Object, propertyKey: string | symbol): void;
178 | skip(name: string): PropertyDecorator;
179 | skip(name: string, ...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
180 | skip(...traits: MochaTypeScript.TestTrait[]): PropertyDecorator;
181 | }
182 | }
183 |
184 | declare var suite: Mocha.IContextDefinition;
185 | declare var test: Mocha.ITestDefinition;
186 |
187 | declare var describe: Mocha.IContextDefinition;
188 | declare var it: Mocha.ITestDefinition;
189 |
190 | declare var skipOnError: MochaTypeScript.SuiteTrait;
191 |
192 | declare function slow(time: number): PropertyDecorator & ClassDecorator & MochaTypeScript.SuiteTrait & MochaTypeScript.TestTrait;
193 | declare function timeout(time: number): PropertyDecorator & ClassDecorator & MochaTypeScript.SuiteTrait & MochaTypeScript.TestTrait;
194 | declare function retries(count: number): PropertyDecorator & ClassDecorator & MochaTypeScript.SuiteTrait & MochaTypeScript.TestTrait;
195 |
196 | declare function pending(target: Object | TFunction, propertyKey?: string | symbol): void;
197 | declare function only(target: Object, propertyKey?: string | symbol): void;
198 | declare function skip(target: Object | TFunction, propertyKey?: string | symbol): void;
--------------------------------------------------------------------------------
/src/svg/cute/notice.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svg/cute/leisurely.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svg/cute/congratulations.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svg/cute/think.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------