├── .editorconfig
├── .gitignore
├── .temp
└── ant-design-pro.less
├── README.adoc
├── README.html
├── config
├── config.js
├── plugin.config.js
└── router.config.js
├── firebase.json
├── functions
├── index.js
├── matchMock.js
└── package.json
├── jest-puppeteer.config.js
├── jest.config.js
├── jsconfig.json
├── mock
├── api.js
├── chart.js
├── geographic.js
├── geographic
│ ├── city.json
│ └── province.json
├── notices.js
├── profile.js
├── route.js
├── rule.js
└── user.js
├── netlify.toml
├── package.json
├── plugins
├── module-gen-plugin
│ ├── index.js
│ └── templates
│ │ ├── index.js.tplt
│ │ ├── model.js.tplt
│ │ ├── module.config.js.tplt
│ │ ├── module.locale.js.tplt
│ │ └── service.js.tplt
└── module-plugin
│ └── index.js
├── public
├── favicon.png
└── icons
│ ├── icon-128x128.png
│ ├── icon-192x192.png
│ └── icon-512x512.png
├── scripts
├── generateMock.js
├── getPrettierFiles.js
├── lint-prettier.js
└── prettier.js
├── src
├── app.js
├── assets
│ ├── EDA-LOGO.svg
│ ├── MY-LOGO.png
│ ├── MY-LOGO.svg
│ ├── bg-1.jpg
│ ├── bg-3.jpg
│ └── logo.svg
├── components
│ ├── Ellipsis
│ │ ├── demo
│ │ │ ├── line.md
│ │ │ └── number.md
│ │ ├── index.d.ts
│ │ ├── index.en-US.md
│ │ ├── index.js
│ │ ├── index.less
│ │ ├── index.test.js
│ │ └── index.zh-CN.md
│ ├── Exception
│ │ ├── 403.js
│ │ ├── 404.js
│ │ ├── demo
│ │ │ ├── 403.md
│ │ │ ├── 404.md
│ │ │ └── 500.md
│ │ ├── index.d.ts
│ │ ├── index.en-US.md
│ │ ├── index.js
│ │ ├── index.less
│ │ ├── index.zh-CN.md
│ │ └── typeConfig.js
│ ├── GlobalFooter
│ │ ├── demo
│ │ │ └── basic.md
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── index.less
│ │ └── index.md
│ ├── GlobalHeader
│ │ ├── RightContent.js
│ │ ├── index.js
│ │ └── index.less
│ ├── HeaderDropdown
│ │ ├── index.js
│ │ └── index.less
│ ├── HeaderSearch
│ │ ├── demo
│ │ │ └── basic.md
│ │ ├── index.d.ts
│ │ ├── index.en-US.md
│ │ ├── index.js
│ │ ├── index.less
│ │ └── index.zh-CN.md
│ ├── NoticeIcon
│ │ ├── NoticeIconTab.d.ts
│ │ ├── NoticeList.js
│ │ ├── NoticeList.less
│ │ ├── demo
│ │ │ ├── basic.md
│ │ │ └── popover.md
│ │ ├── index.d.ts
│ │ ├── index.en-US.md
│ │ ├── index.js
│ │ ├── index.less
│ │ └── index.zh-CN.md
│ ├── NumberInfo
│ │ ├── demo
│ │ │ └── basic.md
│ │ ├── index.d.ts
│ │ ├── index.en-US.md
│ │ ├── index.js
│ │ ├── index.less
│ │ └── index.zh-CN.md
│ ├── PageHeader
│ │ ├── breadcrumb.d.ts
│ │ ├── breadcrumb.js
│ │ ├── demo
│ │ │ ├── image.md
│ │ │ ├── simple.md
│ │ │ ├── standard.md
│ │ │ └── structure.md
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── index.less
│ │ ├── index.md
│ │ └── index.test.js
│ ├── PageHeaderWrapper
│ │ ├── GridContent.js
│ │ ├── GridContent.less
│ │ ├── index.js
│ │ └── index.less
│ ├── PageLoading
│ │ └── index.js
│ ├── SelectLang
│ │ ├── index.js
│ │ └── index.less
│ ├── SettingDrawer
│ │ ├── BlockCheckbox.js
│ │ ├── ThemeColor.js
│ │ ├── ThemeColor.less
│ │ ├── index.js
│ │ └── index.less
│ ├── SiderMenu
│ │ ├── BaseMenu.js
│ │ ├── SiderMenu.js
│ │ ├── SiderMenu.test.js
│ │ ├── SiderMenuUtils.js
│ │ ├── index.js
│ │ └── index.less
│ ├── StandardFormRow
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ └── index.less
│ ├── TMDataList
│ │ ├── index.jsx
│ │ ├── query-condition.ts
│ │ └── style.less
│ ├── TMFormDrawer
│ │ ├── index.js
│ │ ├── index2.tsx
│ │ └── style.less
│ ├── TMStandardFormRow
│ │ ├── index.js
│ │ └── index.less
│ ├── TagSelect
│ │ ├── TagSelectOption.d.ts
│ │ ├── demo
│ │ │ ├── controlled.md
│ │ │ ├── expandable.md
│ │ │ └── simple.md
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── index.less
│ │ └── index.md
│ ├── TopNavHeader
│ │ ├── index.js
│ │ └── index.less
│ ├── _utils
│ │ ├── pathTools.js
│ │ └── pathTools.test.js
│ └── form-item
│ │ ├── TMCombobox
│ │ ├── index.js
│ │ └── style.less
│ │ └── TMCombobox2
│ │ ├── index.js
│ │ └── style.less
├── defaultSettings.js
├── e2e
│ ├── baseLayout.e2e.js
│ ├── home.e2e.js
│ ├── login.e2e.js
│ ├── topMenu.e2e.js
│ └── userLayout.e2e.js
├── environment.dev.js
├── environment.prod.js
├── environment.qa.js
├── global.js
├── global.less
├── layouts
│ ├── BasicLayout.js
│ ├── BasicLayout.less
│ ├── Footer.js
│ ├── Header.js
│ ├── Header.less
│ └── MenuContext.js
├── locales
│ ├── en-US.js
│ ├── en-US
│ │ ├── component.js
│ │ ├── exception.js
│ │ ├── globalHeader.js
│ │ ├── menu.js
│ │ ├── pwa.js
│ │ ├── settingDrawer.js
│ │ └── settings.js
│ ├── zh-CN.js
│ └── zh-CN
│ │ ├── component.js
│ │ ├── exception.js
│ │ ├── globalHeader.js
│ │ ├── menu.js
│ │ ├── pwa.js
│ │ ├── settingDrawer.js
│ │ └── settings.js
├── manifest.json
├── menu.config.js
├── models
│ ├── global.model.js
│ ├── menu.model.js
│ └── setting.model.js
├── pages
│ ├── authority
│ │ ├── auditlog
│ │ │ ├── components
│ │ │ │ └── QueryConditionForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── style.less
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ └── auditlog.model.js
│ │ │ ├── module.config.js
│ │ │ ├── module.locale.en-US.js
│ │ │ ├── module.locale.zh-CN.js
│ │ │ ├── services
│ │ │ │ └── auditlog.service.js
│ │ │ └── style.less
│ │ ├── resource-mgnt
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ └── resource.model.js
│ │ │ ├── module.config.js
│ │ │ ├── module.locale.en-US.js
│ │ │ ├── module.locale.zh-CN.js
│ │ │ ├── services
│ │ │ │ └── resource.service.js
│ │ │ └── style.less
│ │ ├── role-mgnt
│ │ │ ├── components
│ │ │ │ ├── AuthorityTree
│ │ │ │ │ ├── AuthorityTreeNode
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── QueryConditionForm
│ │ │ │ │ └── index.js
│ │ │ │ └── RoleDetailDrawer
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── style.less
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ │ ├── role.detail.model.js
│ │ │ │ └── role.model.js
│ │ │ ├── module.config.js
│ │ │ ├── module.locale.en-US.js
│ │ │ ├── module.locale.zh-CN.js
│ │ │ ├── services
│ │ │ │ └── role.service.js
│ │ │ └── style.less
│ │ └── user-mgnt
│ │ │ ├── components
│ │ │ ├── QueryConditionForm
│ │ │ │ └── index.js
│ │ │ ├── RoleAssignment
│ │ │ │ ├── index.js
│ │ │ │ └── style.less
│ │ │ └── UserDetailDrawer
│ │ │ │ ├── index.js
│ │ │ │ └── style.less
│ │ │ ├── index.js
│ │ │ ├── models
│ │ │ ├── user.detail.model.js
│ │ │ └── user.model.js
│ │ │ ├── module.config.js
│ │ │ ├── module.locale.en-US.js
│ │ │ ├── module.locale.zh-CN.js
│ │ │ ├── services
│ │ │ └── user.service.js
│ │ │ └── style.less
│ ├── document.ejs
│ ├── login
│ │ ├── index.js
│ │ └── login.css
│ ├── test-a
│ │ ├── index.js
│ │ └── module.config.js
│ ├── test-b
│ │ ├── index.js
│ │ └── module.config.js
│ └── test-c
│ │ ├── index.js
│ │ └── module.config.js
├── router.config.js
├── service-worker.js
├── services
│ └── global.service.js
└── utils
│ ├── Yuan.js
│ ├── authority.js
│ ├── date-util.js
│ ├── message-util.js
│ ├── request-util.js
│ ├── request.js
│ ├── rest-accessor.js
│ ├── rest-err-processor.js
│ ├── rest-token-resolver.js
│ ├── utils.js
│ ├── utils.less
│ └── utils.test.js
├── tests
└── run-tests.js
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # umi temp directory
2 | /src/pages/.umi
3 |
4 | # build directory
5 | /dist
6 |
7 | # Dependency directories
8 | node_modules/
9 |
10 | # Logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 |
--------------------------------------------------------------------------------
/config/plugin.config.js:
--------------------------------------------------------------------------------
1 | // Change theme plugin
2 |
3 | import MergeLessPlugin from 'antd-pro-merge-less';
4 | import AntDesignThemePlugin from 'antd-theme-webpack-plugin';
5 | import path from 'path';
6 |
7 | export default config => {
8 | // pro 和 开发环境再添加这个插件
9 | if (process.env.APP_TYPE === 'site' || process.env.NODE_ENV !== 'production') {
10 | // 将所有 less 合并为一个供 themePlugin使用
11 | const outFile = path.join(__dirname, '../.temp/ant-design-pro.less');
12 | const stylesDir = path.join(__dirname, '../src/');
13 |
14 | config.plugin('merge-less').use(MergeLessPlugin, [
15 | {
16 | stylesDir,
17 | outFile,
18 | },
19 | ]);
20 |
21 | config.plugin('ant-design-theme').use(AntDesignThemePlugin, [
22 | {
23 | antDir: path.join(__dirname, '../node_modules/antd'),
24 | stylesDir,
25 | varFile: path.join(__dirname, '../node_modules/antd/lib/style/themes/default.less'),
26 | mainLessFile: outFile, // themeVariables: ['@primary-color'],
27 | indexFileName: 'index.html',
28 | generateOne: true,
29 | lessUrl: 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js',
30 | },
31 | ]);
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/config/router.config.js:
--------------------------------------------------------------------------------
1 | // do not modify this file,
2 | // modify src/router.config.js or module.config.js under each module
3 |
4 | import addtionalRoutes from '../src/router.config';
5 |
6 | export default [
7 | ...addtionalRoutes,
8 | {
9 | "path": "/",
10 | "component": "../layouts/BasicLayout",
11 | "routes": []
12 | },
13 |
14 | ]
15 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "dist",
4 | "rewrites": [
5 | { "source": "/api/**", "function": "api" },
6 | {
7 | "source": "**",
8 | "destination": "/index.html"
9 | }
10 | ],
11 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | // [START functionsimport]
2 | const functions = require('firebase-functions');
3 | const express = require('express');
4 |
5 | const matchMock = require('./matchMock');
6 |
7 | const app = express();
8 |
9 | app.use(matchMock);
10 | exports.api = functions.https.onRequest(app);
11 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "serve": "firebase serve --only functions",
6 | "shell": "firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "npm run mock && firebase deploy --only functions",
9 | "logs": "firebase functions:log",
10 | "mock": "node ../scripts/generateMock.js"
11 | },
12 | "dependencies": {
13 | "@babel/runtime": "^7.0.0",
14 | "body-parser": "^1.18.3",
15 | "express": "^4.16.4",
16 | "firebase-admin": "^6.4.0",
17 | "firebase-functions": "^2.1.0",
18 | "mockjs": "^1.0.1-beta3",
19 | "moment": "^2.22.2",
20 | "path-to-regexp": "^3.0.0"
21 | },
22 | "private": true
23 | }
24 |
--------------------------------------------------------------------------------
/jest-puppeteer.config.js:
--------------------------------------------------------------------------------
1 | // ps https://github.com/GoogleChrome/puppeteer/issues/3120
2 | module.exports = {
3 | launch: {
4 | args: [
5 | '--disable-gpu',
6 | '--disable-dev-shm-usage',
7 | '--no-first-run',
8 | '--no-zygote',
9 | ],
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testURL: 'http://localhost:8000',
3 | preset: 'jest-puppeteer',
4 | };
5 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/mock/geographic.js:
--------------------------------------------------------------------------------
1 | import city from './geographic/city.json';
2 | import province from './geographic/province.json';
3 |
4 | function getProvince(req, res) {
5 | return res.json(province);
6 | }
7 |
8 | function getCity(req, res) {
9 | return res.json(city[req.params.province]);
10 | }
11 |
12 | export default {
13 | 'GET /api/geographic/province': getProvince,
14 | 'GET /api/geographic/city/:province': getCity,
15 | };
16 |
--------------------------------------------------------------------------------
/mock/geographic/province.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "北京市",
4 | "id": "110000"
5 | },
6 | {
7 | "name": "天津市",
8 | "id": "120000"
9 | },
10 | {
11 | "name": "河北省",
12 | "id": "130000"
13 | },
14 | {
15 | "name": "山西省",
16 | "id": "140000"
17 | },
18 | {
19 | "name": "内蒙古自治区",
20 | "id": "150000"
21 | },
22 | {
23 | "name": "辽宁省",
24 | "id": "210000"
25 | },
26 | {
27 | "name": "吉林省",
28 | "id": "220000"
29 | },
30 | {
31 | "name": "黑龙江省",
32 | "id": "230000"
33 | },
34 | {
35 | "name": "上海市",
36 | "id": "310000"
37 | },
38 | {
39 | "name": "江苏省",
40 | "id": "320000"
41 | },
42 | {
43 | "name": "浙江省",
44 | "id": "330000"
45 | },
46 | {
47 | "name": "安徽省",
48 | "id": "340000"
49 | },
50 | {
51 | "name": "福建省",
52 | "id": "350000"
53 | },
54 | {
55 | "name": "江西省",
56 | "id": "360000"
57 | },
58 | {
59 | "name": "山东省",
60 | "id": "370000"
61 | },
62 | {
63 | "name": "河南省",
64 | "id": "410000"
65 | },
66 | {
67 | "name": "湖北省",
68 | "id": "420000"
69 | },
70 | {
71 | "name": "湖南省",
72 | "id": "430000"
73 | },
74 | {
75 | "name": "广东省",
76 | "id": "440000"
77 | },
78 | {
79 | "name": "广西壮族自治区",
80 | "id": "450000"
81 | },
82 | {
83 | "name": "海南省",
84 | "id": "460000"
85 | },
86 | {
87 | "name": "重庆市",
88 | "id": "500000"
89 | },
90 | {
91 | "name": "四川省",
92 | "id": "510000"
93 | },
94 | {
95 | "name": "贵州省",
96 | "id": "520000"
97 | },
98 | {
99 | "name": "云南省",
100 | "id": "530000"
101 | },
102 | {
103 | "name": "西藏自治区",
104 | "id": "540000"
105 | },
106 | {
107 | "name": "陕西省",
108 | "id": "610000"
109 | },
110 | {
111 | "name": "甘肃省",
112 | "id": "620000"
113 | },
114 | {
115 | "name": "青海省",
116 | "id": "630000"
117 | },
118 | {
119 | "name": "宁夏回族自治区",
120 | "id": "640000"
121 | },
122 | {
123 | "name": "新疆维吾尔自治区",
124 | "id": "650000"
125 | },
126 | {
127 | "name": "台湾省",
128 | "id": "710000"
129 | },
130 | {
131 | "name": "香港特别行政区",
132 | "id": "810000"
133 | },
134 | {
135 | "name": "澳门特别行政区",
136 | "id": "820000"
137 | }
138 | ]
139 |
--------------------------------------------------------------------------------
/mock/route.js:
--------------------------------------------------------------------------------
1 | export default {
2 | '/api/auth_routes': {
3 | '/form/advanced-form': { authority: ['admin', 'user'] },
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "/api/*"
3 | to = "https://us-central1-antd-pro.cloudfunctions.net/api/api/:splat"
4 | status = 200
5 | force = true
6 | [redirects.headers]
7 | X-From = "Netlify"
8 | X-Api-Key = "some-api-key-string"
9 |
10 | [[redirects]]
11 | from = "/*"
12 | to = "/index.html"
13 | status = 200
--------------------------------------------------------------------------------
/plugins/module-gen-plugin/templates/index.js.tplt:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PageHeaderWrapper from '@/components/PageHeaderWrapper';
3 | import { formatMessage as fm, FormattedMessage } from 'umi/locale';
4 | import mm from '@/utils/message-util';
5 | import { connect } from 'dva';
6 | import styles from './style.less';
7 |
8 |
9 | @connect(({ loading }) => ({ loading }))
10 | export default class Index extends Component {
11 |
12 | componentDidMount() {
13 |
14 | }
15 |
16 | render = () => {
17 |
18 | return (
19 |
20 | to do
21 |
22 | );
23 |
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/plugins/module-gen-plugin/templates/model.js.tplt:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | namespace: '${modelName}',
4 |
5 |
6 | state: {
7 |
8 | },
9 |
10 |
11 | effects: {
12 |
13 | },
14 |
15 |
16 | reducers: {
17 |
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/plugins/module-gen-plugin/templates/module.config.js.tplt:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'moduleName',
4 |
5 | authority: {
6 | resources: [],
7 | events: [],
8 | },
9 |
10 | routes: []
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/plugins/module-gen-plugin/templates/module.locale.js.tplt:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': 'Change me',
4 |
5 | };
6 |
--------------------------------------------------------------------------------
/plugins/module-gen-plugin/templates/service.js.tplt:
--------------------------------------------------------------------------------
1 | import { get, post } from '@/utils/rest-accessor';
2 |
3 |
4 | export function test(filterParam) {
5 | return get('/a/b', null, filterParam);
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/public/favicon.png
--------------------------------------------------------------------------------
/public/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/public/icons/icon-128x128.png
--------------------------------------------------------------------------------
/public/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/public/icons/icon-192x192.png
--------------------------------------------------------------------------------
/public/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/public/icons/icon-512x512.png
--------------------------------------------------------------------------------
/scripts/generateMock.js:
--------------------------------------------------------------------------------
1 | const generateMock = require('merge-umi-mock-data');
2 | const path = require('path');
3 | generateMock(path.join(__dirname, '../mock'), path.join(__dirname, '../functions/mock/index.js'));
4 |
--------------------------------------------------------------------------------
/scripts/getPrettierFiles.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob');
2 |
3 | const getPrettierFiles = () => {
4 | let files = [];
5 | const jsFiles = glob.sync('src/**/*.js*', { ignore: ['**/node_modules/**', 'build/**'] });
6 | const tsFiles = glob.sync('src/**/*.ts*', { ignore: ['**/node_modules/**', 'build/**'] });
7 | const configFiles = glob.sync('config/**/*.js*', { ignore: ['**/node_modules/**', 'build/**'] });
8 | const scriptFiles = glob.sync('scripts/**/*.js');
9 | const lessFiles = glob.sync('src/**/*.less*', { ignore: ['**/node_modules/**', 'build/**'] });
10 | files = files.concat(jsFiles);
11 | files = files.concat(tsFiles);
12 | files = files.concat(configFiles);
13 | files = files.concat(scriptFiles);
14 | files = files.concat(lessFiles);
15 | if (!files.length) {
16 | return;
17 | }
18 | return files;
19 | };
20 |
21 | module.exports = getPrettierFiles;
22 |
--------------------------------------------------------------------------------
/scripts/lint-prettier.js:
--------------------------------------------------------------------------------
1 | /**
2 | * copy to https://github.com/facebook/react/blob/master/scripts/prettier/index.js
3 | * prettier api doc https://prettier.io/docs/en/api.html
4 | *----------*****--------------
5 | * lint file is prettier
6 | *----------*****--------------
7 | */
8 |
9 | const prettier = require('prettier');
10 | const fs = require('fs');
11 | const chalk = require('chalk');
12 | const prettierConfigPath = require.resolve('../.prettierrc');
13 |
14 | const files = process.argv.slice(2);
15 |
16 | let didError = false;
17 |
18 | files.forEach(file => {
19 | Promise.all([
20 | prettier.resolveConfig(file, {
21 | config: prettierConfigPath,
22 | }),
23 | prettier.getFileInfo(file),
24 | ])
25 | .then(resolves => {
26 | const [options, fileInfo] = resolves;
27 | if (fileInfo.ignored) {
28 | return;
29 | }
30 | const input = fs.readFileSync(file, 'utf8');
31 | const withParserOptions = {
32 | ...options,
33 | parser: fileInfo.inferredParser,
34 | };
35 | const output = prettier.format(input, withParserOptions);
36 | if (output !== input) {
37 | fs.writeFileSync(file, output, 'utf8');
38 | console.log(chalk.green(`${file} is prettier`));
39 | }
40 | })
41 | .catch(e => {
42 | didError = true;
43 | })
44 | .finally(() => {
45 | if (didError) {
46 | process.exit(1);
47 | }
48 | console.log(chalk.hex('#1890FF')('prettier success!'));
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/scripts/prettier.js:
--------------------------------------------------------------------------------
1 | /**
2 | * copy to https://github.com/facebook/react/blob/master/scripts/prettier/index.js
3 | * prettier api doc https://prettier.io/docs/en/api.html
4 | *----------*****--------------
5 | * prettier all js and all ts.
6 | *----------*****--------------
7 | */
8 |
9 | const prettier = require('prettier');
10 | const fs = require('fs');
11 | const getPrettierFiles = require('./getPrettierFiles');
12 | const prettierConfigPath = require.resolve('../.prettierrc');
13 | const chalk = require('chalk');
14 |
15 | let didError = false;
16 |
17 | const files = getPrettierFiles();
18 |
19 | files.forEach(file => {
20 | const options = prettier.resolveConfig.sync(file, {
21 | config: prettierConfigPath,
22 | });
23 | const fileInfo = prettier.getFileInfo.sync(file);
24 | if (fileInfo.ignored) {
25 | return;
26 | }
27 | try {
28 | const input = fs.readFileSync(file, 'utf8');
29 | const withParserOptions = {
30 | ...options,
31 | parser: fileInfo.inferredParser,
32 | };
33 | const output = prettier.format(input, withParserOptions);
34 | if (output !== input) {
35 | fs.writeFileSync(file, output, 'utf8');
36 | console.log(chalk.green(`${file} is prettier`));
37 | }
38 | } catch (e) {
39 | didError = true;
40 | }
41 | });
42 |
43 | if (didError) {
44 | process.exit(1);
45 | }
46 | console.log(chalk.hex('#1890FF')('prettier success!'));
47 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import fetch from 'dva/fetch';
2 |
3 | export const dva = {
4 | config: {
5 | onError(err) {
6 | err.preventDefault();
7 | },
8 | },
9 | };
10 |
11 | let authRoutes = {};
12 |
13 | function ergodicRoutes(routes, authKey, authority) {
14 | routes.forEach(element => {
15 | if (element.path === authKey) {
16 | if (!element.authority) element.authority = []; // eslint-disable-line
17 | Object.assign(element.authority, authority || []);
18 | } else if (element.routes) {
19 | ergodicRoutes(element.routes, authKey, authority);
20 | }
21 | return element;
22 | });
23 | }
24 |
25 | export function patchRoutes(routes) {
26 | Object.keys(authRoutes).map(authKey =>
27 | ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
28 | );
29 | window.g_routes = routes;
30 | }
31 |
32 | export function render(oldRender) {
33 | fetch('/api/auth_routes')
34 | .then(res => res.json())
35 | .then(
36 | ret => {
37 | authRoutes = ret;
38 | oldRender();
39 | },
40 | () => {
41 | oldRender();
42 | }
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/MY-LOGO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/src/assets/MY-LOGO.png
--------------------------------------------------------------------------------
/src/assets/bg-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/src/assets/bg-1.jpg
--------------------------------------------------------------------------------
/src/assets/bg-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watertao/teemo/13b0ebafb853d0f110c6ed7817667229efaa22e0/src/assets/bg-3.jpg
--------------------------------------------------------------------------------
/src/components/Ellipsis/demo/line.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title:
4 | zh-CN: 按照行数省略
5 | en-US: Truncate according to the number of rows
6 | ---
7 |
8 | ## zh-CN
9 |
10 | 通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。
11 |
12 | 并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。
13 |
14 | ## en-US
15 |
16 | `lines` attribute specifies the maximum number of rows where the text will automatically be truncated when exceeded. In this mode, all children will be converted to plain text.
17 |
18 | Also note that, in this mode, the outer container needs to have a specified width (or set its own width).
19 |
20 |
21 | ````jsx
22 | import Ellipsis from 'ant-design-pro/lib/Ellipsis';
23 |
24 | const article =
There were injuries alleged in three cases in 2015 , and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
;
25 |
26 | ReactDOM.render(
27 |
28 | {article}
29 |
30 | , mountNode);
31 | ````
32 |
--------------------------------------------------------------------------------
/src/components/Ellipsis/demo/number.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title:
4 | zh-CN: 按照字符数省略
5 | en-US: Truncate according to the number of character
6 | ---
7 |
8 | ## zh-CN
9 |
10 | 通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。
11 |
12 | ## en-US
13 |
14 | `length` attribute specifies the maximum length where the text will automatically be truncated when exceeded.
15 |
16 | ````jsx
17 | import Ellipsis from 'ant-design-pro/lib/Ellipsis';
18 |
19 | const article = 'There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.';
20 |
21 | ReactDOM.render(
22 |
23 | {article}
24 |
Show Tooltip
25 | {article}
26 |
27 | , mountNode);
28 | ````
29 |
--------------------------------------------------------------------------------
/src/components/Ellipsis/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TooltipProps } from 'antd/lib/tooltip';
3 |
4 | export interface IEllipsisTooltipProps extends TooltipProps {
5 | title?: undefined;
6 | overlayStyle?: undefined;
7 | }
8 |
9 | export interface IEllipsisProps {
10 | tooltip?: boolean | IEllipsisTooltipProps;
11 | length?: number;
12 | lines?: number;
13 | style?: React.CSSProperties;
14 | className?: string;
15 | fullWidthRecognition?: boolean;
16 | }
17 |
18 | export function getStrFullLength(str: string): number;
19 | export function cutStrByFullLength(str: string, maxLength: number): string;
20 |
21 | export default class Ellipsis extends React.Component {}
22 |
--------------------------------------------------------------------------------
/src/components/Ellipsis/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Ellipsis
3 | cols: 1
4 | order: 10
5 | ---
6 |
7 | When the text is too long, the Ellipsis automatically shortens it according to its length or the maximum number of lines.
8 |
9 | ## API
10 |
11 | Property | Description | Type | Default
12 | ----|------|-----|------
13 | tooltip | tooltip for showing the full text content when hovering over | boolean | -
14 | length | maximum number of characters in the text before being truncated | number | -
15 | lines | maximum number of rows in the text before being truncated | number | `1`
16 | fullWidthRecognition | whether consider full-width character length as 2 when calculate string length | boolean | -
17 |
--------------------------------------------------------------------------------
/src/components/Ellipsis/index.less:
--------------------------------------------------------------------------------
1 | .ellipsis {
2 | display: inline-block;
3 | width: 100%;
4 | overflow: hidden;
5 | word-break: break-all;
6 | }
7 |
8 | .lines {
9 | position: relative;
10 | .shadow {
11 | position: absolute;
12 | z-index: -999;
13 | display: block;
14 | color: transparent;
15 | opacity: 0;
16 | }
17 | }
18 |
19 | .lineClamp {
20 | position: relative;
21 | display: -webkit-box;
22 | overflow: hidden;
23 | text-overflow: ellipsis;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Ellipsis/index.test.js:
--------------------------------------------------------------------------------
1 | import { getStrFullLength, cutStrByFullLength } from './index';
2 |
3 | describe('test calculateShowLength', () => {
4 | it('get full length', () => {
5 | expect(getStrFullLength('一二,a,')).toEqual(8);
6 | });
7 | it('cut str by full length', () => {
8 | expect(cutStrByFullLength('一二,a,', 7)).toEqual('一二,a');
9 | });
10 | it('cut str when length small', () => {
11 | expect(cutStrByFullLength('一22三', 5)).toEqual('一22');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/Ellipsis/index.zh-CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Ellipsis
3 | subtitle: 文本自动省略号
4 | cols: 1
5 | order: 10
6 | ---
7 |
8 | 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
9 |
10 | ## API
11 |
12 | 参数 | 说明 | 类型 | 默认值
13 | ----|------|-----|------
14 | tooltip | 移动到文本展示完整内容的提示 | boolean | -
15 | length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -
16 | lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1`
17 | fullWidthRecognition | 是否将全角字符的长度视为2来计算字符串长度 | boolean | -
18 |
--------------------------------------------------------------------------------
/src/components/Exception/403.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'umi/link';
3 | import { formatMessage } from 'umi/locale';
4 | import Exception from '@/components/Exception';
5 |
6 | export default () => (
7 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Exception/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'umi/link';
3 | import { formatMessage } from 'umi/locale';
4 | import Exception from '@/components/Exception';
5 |
6 | export default () => (
7 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Exception/demo/403.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 2
3 | title:
4 | zh-CN: 403
5 | en-US: 403
6 | ---
7 |
8 | ## zh-CN
9 |
10 | 403 页面,配合自定义操作。
11 |
12 | ## en-US
13 |
14 | 403 page with custom operations.
15 |
16 | ````jsx
17 | import Exception from 'ant-design-pro/lib/Exception';
18 | import { Button } from 'antd';
19 |
20 | const actions = (
21 |
22 | Home
23 | Detail
24 |
25 | );
26 | ReactDOM.render(
27 |
28 | , mountNode);
29 | ````
30 |
--------------------------------------------------------------------------------
/src/components/Exception/demo/404.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title:
4 | zh-CN: 404
5 | en-US: 404
6 | ---
7 |
8 | ## zh-CN
9 |
10 | 404 页面。
11 |
12 | ## en-US
13 |
14 | 404 page.
15 |
16 | ````jsx
17 | import Exception from 'ant-design-pro/lib/Exception';
18 |
19 | ReactDOM.render(
20 |
21 | , mountNode);
22 | ````
23 |
--------------------------------------------------------------------------------
/src/components/Exception/demo/500.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title:
4 | zh-CN: 500
5 | en-US: 500
6 | ---
7 |
8 | ## zh-CN
9 |
10 | 500 页面。
11 |
12 | ## en-US
13 |
14 | 500 page.
15 |
16 | ````jsx
17 | import Exception from 'ant-design-pro/lib/Exception';
18 |
19 | ReactDOM.render(
20 |
21 | , mountNode);
22 | ````
23 |
--------------------------------------------------------------------------------
/src/components/Exception/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export interface IExceptionProps {
3 | type?: '403' | '404' | '500';
4 | title?: React.ReactNode;
5 | desc?: React.ReactNode;
6 | img?: string;
7 | actions?: React.ReactNode;
8 | linkElement?: string | React.ComponentType;
9 | style?: React.CSSProperties;
10 | className?: string;
11 | backText?: React.ReactNode;
12 | redirect?: string;
13 | }
14 |
15 | export default class Exception extends React.Component {}
16 |
--------------------------------------------------------------------------------
/src/components/Exception/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Exception
3 | cols: 1
4 | order: 5
5 | ---
6 |
7 | Exceptions page is used to provide feedback on specific abnormal state. Usually, it contains an explanation of the error status, and provides users with suggestions or operations, to prevent users from feeling lost and confused.
8 |
9 | ## API
10 |
11 | Property | Description | Type | Default
12 | ---------|-------------|------|--------
13 | | backText | default return button text | ReactNode | back to home |
14 | type | type of exception, the corresponding default `title`, `desc`, `img` will be given if set, which can be overridden by explicit setting of `title`, `desc`, `img` | Enum {'403', '404', '500'} | -
15 | title | title | ReactNode | -
16 | desc | supplementary description | ReactNode | -
17 | img | the url of background image | string | -
18 | actions | suggested operations, a default 'Home' link will show if not set | ReactNode | -
19 | linkElement | to specify the element of link | string\|ReactElement | 'a'
20 | redirect | redirect path | string | '/'
--------------------------------------------------------------------------------
/src/components/Exception/index.js:
--------------------------------------------------------------------------------
1 | import React, { createElement } from 'react';
2 | import classNames from 'classnames';
3 | import { Button } from 'antd';
4 | import config from './typeConfig';
5 | import styles from './index.less';
6 |
7 | class Exception extends React.PureComponent {
8 | static defaultProps = {
9 | backText: 'back to home',
10 | redirect: '/',
11 | };
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {};
16 | }
17 |
18 | render() {
19 | const {
20 | className,
21 | backText,
22 | linkElement = 'a',
23 | type,
24 | title,
25 | desc,
26 | img,
27 | actions,
28 | redirect,
29 | ...rest
30 | } = this.props;
31 | const pageType = type in config ? type : '404';
32 | const clsString = classNames(styles.exception, className);
33 | return (
34 |
35 |
41 |
42 |
{title || config[pageType].title}
43 |
{desc || config[pageType].desc}
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export default Exception;
51 |
--------------------------------------------------------------------------------
/src/components/Exception/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .exception {
4 | display: flex;
5 | align-items: center;
6 | height: 80%;
7 | min-height: 500px;
8 |
9 | .imgBlock {
10 | flex: 0 0 62.5%;
11 | width: 62.5%;
12 | padding-right: 152px;
13 | zoom: 1;
14 | &::before,
15 | &::after {
16 | content: ' ';
17 | display: table;
18 | }
19 | &::after {
20 | clear: both;
21 | height: 0;
22 | font-size: 0;
23 | visibility: hidden;
24 | }
25 | }
26 |
27 | .imgEle {
28 | float: right;
29 | width: 100%;
30 | max-width: 430px;
31 | height: 360px;
32 | background-repeat: no-repeat;
33 | background-position: 50% 50%;
34 | background-size: contain;
35 | }
36 |
37 | .content {
38 | flex: auto;
39 |
40 | h1 {
41 | margin-bottom: 24px;
42 | color: #434e59;
43 | font-weight: 600;
44 | font-size: 72px;
45 | line-height: 72px;
46 | }
47 |
48 | .desc {
49 | margin-bottom: 16px;
50 | color: @text-color-secondary;
51 | font-size: 20px;
52 | line-height: 28px;
53 | }
54 |
55 | .actions {
56 | button:not(:last-child) {
57 | margin-right: 8px;
58 | }
59 | }
60 | }
61 | }
62 |
63 | @media screen and (max-width: @screen-xl) {
64 | .exception {
65 | .imgBlock {
66 | padding-right: 88px;
67 | }
68 | }
69 | }
70 |
71 | @media screen and (max-width: @screen-sm) {
72 | .exception {
73 | display: block;
74 | text-align: center;
75 | .imgBlock {
76 | margin: 0 auto 24px;
77 | padding-right: 0;
78 | }
79 | }
80 | }
81 |
82 | @media screen and (max-width: @screen-xs) {
83 | .exception {
84 | .imgBlock {
85 | margin-bottom: -24px;
86 | overflow: hidden;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/Exception/index.zh-CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Exception
3 | subtitle: 异常
4 | cols: 1
5 | order: 5
6 | ---
7 |
8 | 异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。
9 |
10 | ## API
11 |
12 | | 参数 | 说明| 类型 | 默认值 |
13 | |-------------|------------------------------------------|-------------|-------|
14 | | backText| 默认的返回按钮文本 | ReactNode| back to home |
15 | | type| 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - |
16 | | title | 标题 | ReactNode| -|
17 | | desc| 补充描述| ReactNode| -|
18 | | img | 背景图片地址 | string| -|
19 | | actions | 建议操作,配置此属性时默认的『返回首页』按钮不生效| ReactNode| -|
20 | | linkElement | 定义链接的元素 | string\|ReactElement | 'a' |
21 | | redirect | 返回按钮的跳转地址 | string | '/'
22 |
--------------------------------------------------------------------------------
/src/components/Exception/typeConfig.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | 403: {
3 | img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
4 | title: '403',
5 | desc: '抱歉,你无权访问该页面',
6 | },
7 | 404: {
8 | img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
9 | title: '404',
10 | desc: '抱歉,你访问的页面不存在',
11 | },
12 | 500: {
13 | img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
14 | title: '500',
15 | desc: '抱歉,服务器出错了',
16 | },
17 | };
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/src/components/GlobalFooter/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title: 演示
4 | iframe: 400
5 | ---
6 |
7 | 基本页脚。
8 |
9 | ````jsx
10 | import GlobalFooter from 'ant-design-pro/lib/GlobalFooter';
11 | import { Icon } from 'antd';
12 |
13 | const links = [{
14 | key: '帮助',
15 | title: '帮助',
16 | href: '',
17 | }, {
18 | key: 'github',
19 | title: ,
20 | href: 'https://github.com/ant-design/ant-design-pro',
21 | blankTarget: true,
22 | }, {
23 | key: '条款',
24 | title: '条款',
25 | href: '',
26 | blankTarget: true,
27 | }];
28 |
29 | const copyright = Copyright 2017 蚂蚁金服体验技术部出品
;
30 |
31 | ReactDOM.render(
32 |
36 | , mountNode);
37 | ````
38 |
--------------------------------------------------------------------------------
/src/components/GlobalFooter/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export interface IGlobalFooterProps {
3 | links?: Array<{
4 | key?: string;
5 | title: React.ReactNode;
6 | href: string;
7 | blankTarget?: boolean;
8 | }>;
9 | copyright?: React.ReactNode;
10 | style?: React.CSSProperties;
11 | }
12 |
13 | export default class GlobalFooter extends React.Component {}
14 |
--------------------------------------------------------------------------------
/src/components/GlobalFooter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import styles from './index.less';
4 |
5 | const GlobalFooter = ({ className, links, copyright }) => {
6 | const clsString = classNames(styles.globalFooter, className);
7 | return (
8 |
25 | );
26 | };
27 |
28 | export default GlobalFooter;
29 |
--------------------------------------------------------------------------------
/src/components/GlobalFooter/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .globalFooter {
4 | margin: 48px 0 24px 0;
5 | padding: 0 16px;
6 | text-align: center;
7 |
8 | .links {
9 | margin-bottom: 8px;
10 |
11 | a {
12 | color: @text-color-secondary;
13 | transition: all 0.3s;
14 |
15 | &:not(:last-child) {
16 | margin-right: 40px;
17 | }
18 |
19 | &:hover {
20 | color: @text-color;
21 | }
22 | }
23 | }
24 |
25 | .copyright {
26 | color: @text-color-secondary;
27 | font-size: @font-size-base;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/GlobalFooter/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: GlobalFooter
4 | zh-CN: GlobalFooter
5 | subtitle: 全局页脚
6 | cols: 1
7 | order: 7
8 | ---
9 |
10 | 页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
11 |
12 | ## API
13 |
14 | 参数 | 说明 | 类型 | 默认值
15 | ----|------|-----|------
16 | links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | -
17 | copyright | 版权信息 | ReactNode | -
18 |
--------------------------------------------------------------------------------
/src/components/GlobalHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Icon } from 'antd';
3 | import Link from 'umi/link';
4 | import Debounce from 'lodash-decorators/debounce';
5 | import styles from './index.less';
6 | import RightContent from './RightContent';
7 |
8 | export default class GlobalHeader extends PureComponent {
9 | componentWillUnmount() {
10 | this.triggerResizeEvent.cancel();
11 | }
12 | /* eslint-disable*/
13 | @Debounce(600)
14 | triggerResizeEvent() {
15 | // eslint-disable-line
16 | const event = document.createEvent('HTMLEvents');
17 | event.initEvent('resize', true, false);
18 | window.dispatchEvent(event);
19 | }
20 | toggle = () => {
21 | const { collapsed, onCollapse } = this.props;
22 | onCollapse(!collapsed);
23 | this.triggerResizeEvent();
24 | };
25 | render() {
26 | const { collapsed, isMobile, logo } = this.props;
27 | return (
28 |
29 | {isMobile && (
30 |
31 |
32 |
33 | )}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/GlobalHeader/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025);
4 |
5 | .header {
6 | position: relative;
7 | height: @layout-header-height;
8 | padding: 0;
9 | background: #fff;
10 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
11 | }
12 |
13 | .logo {
14 | display: inline-block;
15 | height: @layout-header-height;
16 | padding: 0 0 0 24px;
17 | font-size: 20px;
18 | line-height: @layout-header-height;
19 | vertical-align: top;
20 | cursor: pointer;
21 | img {
22 | display: inline-block;
23 | vertical-align: middle;
24 | }
25 | }
26 |
27 | .menu {
28 | :global(.anticon) {
29 | margin-right: 8px;
30 | }
31 | :global(.ant-dropdown-menu-item) {
32 | min-width: 160px;
33 | }
34 | }
35 |
36 | .trigger {
37 | height: @layout-header-height;
38 | padding: ~'calc((@{layout-header-height} - 20px) / 2)' 24px;
39 | font-size: 20px;
40 | cursor: pointer;
41 | transition: all 0.3s, padding 0s;
42 | &:hover {
43 | background: @pro-header-hover-bg;
44 | }
45 | }
46 |
47 | .right {
48 | float: right;
49 | height: 100%;
50 | overflow: hidden;
51 | .action {
52 | display: inline-block;
53 | height: 100%;
54 | padding: 0 12px;
55 | cursor: pointer;
56 | transition: all 0.3s;
57 | > i {
58 | color: @text-color;
59 | vertical-align: middle;
60 | }
61 | &:hover {
62 | background: @pro-header-hover-bg;
63 | }
64 | &:global(.opened) {
65 | background: @pro-header-hover-bg;
66 | }
67 | }
68 | .search {
69 | padding: 0 12px;
70 | &:hover {
71 | background: transparent;
72 | }
73 | }
74 | .account {
75 | .avatar {
76 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
77 | margin-right: 8px;
78 | vertical-align: top;
79 | }
80 | }
81 | }
82 |
83 | .dark {
84 | height: @layout-header-height;
85 | .action {
86 | color: rgba(255, 255, 255, 0.85);
87 | > i {
88 | color: rgba(255, 255, 255, 0.85);
89 | }
90 | &:hover,
91 | &:global(.opened) {
92 | background: @primary-color;
93 | }
94 | :global(.ant-badge) {
95 | color: rgba(255, 255, 255, 0.85);
96 | }
97 | }
98 | }
99 |
100 | @media only screen and (max-width: @screen-md) {
101 | .header {
102 | :global(.ant-divider-vertical) {
103 | vertical-align: unset;
104 | }
105 | .name {
106 | display: none;
107 | }
108 | i.trigger {
109 | padding: 22px 12px;
110 | }
111 | .logo {
112 | position: relative;
113 | padding-right: 12px;
114 | padding-left: 12px;
115 | }
116 | .right {
117 | position: absolute;
118 | top: 0;
119 | right: 12px;
120 | background: #fff;
121 | .account {
122 | .avatar {
123 | margin-right: 0;
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/HeaderDropdown/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Dropdown } from 'antd';
3 | import classNames from 'classnames';
4 | import styles from './index.less';
5 |
6 | export default class HeaderDropdown extends PureComponent {
7 | render() {
8 | const { overlayClassName, ...props } = this.props;
9 | return (
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/HeaderDropdown/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .container > * {
4 | background-color: #fff;
5 | border-radius: 4px;
6 | box-shadow: @shadow-1-down;
7 | }
8 |
9 | @media screen and (max-width: @screen-xs) {
10 | .container {
11 | width: 100% !important;
12 | }
13 | .container > * {
14 | border-radius: 0 !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title: 全局搜索
4 | ---
5 |
6 | 通常放置在导航工具条右侧。(点击搜索图标预览效果)
7 |
8 | ````jsx
9 | import HeaderSearch from 'ant-design-pro/lib/HeaderSearch';
10 |
11 | ReactDOM.render(
12 |
22 | {
26 | console.log('input', value); // eslint-disable-line
27 | }}
28 | onPressEnter={(value) => {
29 | console.log('enter', value); // eslint-disable-line
30 | }}
31 | />
32 |
33 | , mountNode);
34 | ````
35 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export interface IHeaderSearchProps {
3 | placeholder?: string;
4 | dataSource?: string[];
5 | defaultOpen?: boolean;
6 | open?: boolean;
7 | onSearch?: (value: string) => void;
8 | onChange?: (value: string) => void;
9 | onVisibleChange?: (visible: boolean) => void;
10 | onPressEnter?: (value: string) => void;
11 | style?: React.CSSProperties;
12 | className?: string;
13 | }
14 |
15 | export default class HeaderSearch extends React.Component {}
16 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: HeaderSearch
4 | zh-CN: HeaderSearch
5 | subtitle: Top search box
6 | cols: 1
7 | order: 8
8 | ---
9 |
10 | Usually placed as an entry to the global search, placed on the right side of the navigation toolbar.
11 |
12 | ## API
13 |
14 | 参数 | 说明 | 类型 | 默认值
15 | ----|------|-----|------
16 | placeholder | placeholder text | string | -
17 | dataSource | current list of prompts | string[] | -
18 | onSearch | Called when searching items. | function(value) | -
19 | onChange | Called when select an option or input value change, or value of input is changed | function(value) | -
20 | onSelect | Called when a option is selected. param is option's value and option instance. | function(value) | -
21 | onPressEnter | Callback when pressing Enter | function(value) | -
22 | onVisibleChange | Show or hide the callback of the text box | function(value) |-
23 | defaultOpen | The input box is displayed for the first time. | boolean | false
24 | open | The input box is displayed | booelan |false
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .headerSearch {
4 | :global(.anticon-search) {
5 | font-size: 16px;
6 | cursor: pointer;
7 | }
8 | .input {
9 | width: 0;
10 | background: transparent;
11 | border-radius: 0;
12 | transition: width 0.3s, margin-left 0.3s;
13 | :global(.ant-select-selection) {
14 | background: transparent;
15 | }
16 | input {
17 | padding-right: 0;
18 | padding-left: 0;
19 | border: 0;
20 | box-shadow: none !important;
21 | }
22 | &,
23 | &:hover,
24 | &:focus {
25 | border-bottom: 1px solid @border-color-base;
26 | }
27 | &.show {
28 | width: 210px;
29 | margin-left: 8px;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.zh-CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: HeaderSearch
4 | zh-CN: HeaderSearch
5 | subtitle: 顶部搜索框
6 | cols: 1
7 | order: 8
8 | ---
9 |
10 | 通常作为全局搜索的入口,放置在导航工具条右侧。
11 |
12 | ## API
13 |
14 | 参数 | 说明 | 类型 | 默认值
15 | ----|------|-----|------
16 | placeholder | 占位文字 | string | -
17 | dataSource | 当前提示内容列表 | string[] | -
18 | onSearch | 搜索补全项的时候调用 | function(value) | -
19 | onChange | 选中 option,或 input 的 value 变化时,调用此函数 | function(value) | -
20 | onSelect | 被选中时调用,参数为选中项的 value 值 | function(value) | -
21 | onPressEnter | 按下回车时的回调 | function(value) | -
22 | onVisibleChange | 显示或隐藏文本框的回调 | function(value) |-
23 | defaultOpen | 输入框首次显示是否显示 | boolean | false
24 | open | 控制输入框是否显示 | booelan |false
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeIconTab.d.ts:
--------------------------------------------------------------------------------
1 | import { SkeletonProps } from 'antd/lib/skeleton';
2 | import * as React from 'react';
3 |
4 | export interface INoticeIconData {
5 | avatar?: string | React.ReactNode;
6 | title?: React.ReactNode;
7 | description?: React.ReactNode;
8 | datetime?: React.ReactNode;
9 | extra?: React.ReactNode;
10 | style?: React.CSSProperties;
11 | }
12 |
13 | export interface INoticeIconTabProps {
14 | count?: number;
15 | emptyText?: React.ReactNode;
16 | emptyImage?: string;
17 | list?: INoticeIconData[];
18 | loadedAll?: boolean;
19 | loading?: boolean;
20 | name?: string;
21 | showClear?: boolean;
22 | skeletonCount?: number;
23 | skeletonProps?: SkeletonProps;
24 | style?: React.CSSProperties;
25 | title?: string;
26 | }
27 |
28 | export default class NoticeIconTab extends React.Component {}
29 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeList.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .list {
4 | max-height: 400px;
5 | overflow: auto;
6 | &::-webkit-scrollbar {
7 | display: none;
8 | }
9 | .item {
10 | padding-right: 24px;
11 | padding-left: 24px;
12 | overflow: hidden;
13 | cursor: pointer;
14 | transition: all 0.3s;
15 |
16 | .meta {
17 | width: 100%;
18 | }
19 |
20 | .avatar {
21 | margin-top: 4px;
22 | background: #fff;
23 | }
24 | .iconElement {
25 | font-size: 32px;
26 | }
27 |
28 | &.read {
29 | opacity: 0.4;
30 | }
31 | &:last-child {
32 | border-bottom: 0;
33 | }
34 | &:hover {
35 | background: @primary-1;
36 | }
37 | .title {
38 | margin-bottom: 8px;
39 | font-weight: normal;
40 | }
41 | .description {
42 | font-size: 12px;
43 | line-height: @line-height-base;
44 | }
45 | .datetime {
46 | margin-top: 4px;
47 | font-size: 12px;
48 | line-height: @line-height-base;
49 | }
50 | .extra {
51 | float: right;
52 | margin-top: -1.5px;
53 | margin-right: 0;
54 | color: @text-color-secondary;
55 | font-weight: normal;
56 | }
57 | }
58 | .loadMore {
59 | padding: 8px 0;
60 | color: @primary-6;
61 | text-align: center;
62 | cursor: pointer;
63 | &.loadedAll {
64 | color: rgba(0, 0, 0, 0.25);
65 | cursor: unset;
66 | }
67 | }
68 | }
69 |
70 | .notFound {
71 | padding: 73px 0 88px 0;
72 | color: @text-color-secondary;
73 | text-align: center;
74 | img {
75 | display: inline-block;
76 | height: 76px;
77 | margin-bottom: 16px;
78 | }
79 | }
80 |
81 | .clear {
82 | height: 46px;
83 | color: @text-color;
84 | line-height: 46px;
85 | text-align: center;
86 | border-top: 1px solid @border-color-split;
87 | border-radius: 0 0 @border-radius-base @border-radius-base;
88 | cursor: pointer;
89 | transition: all 0.3s;
90 |
91 | &:hover {
92 | color: @heading-color;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title: 通知图标
4 | ---
5 |
6 | 通常用在导航工具栏上。
7 |
8 | ````jsx
9 | import NoticeIcon from 'ant-design-pro/lib/NoticeIcon';
10 |
11 | ReactDOM.render( , mountNode);
12 | ````
13 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import NoticeIconTab, { INoticeIconData } from './NoticeIconTab';
3 |
4 | export interface INoticeIconProps {
5 | count?: number;
6 | bell?: React.ReactNode;
7 | className?: string;
8 | loading?: boolean;
9 | onClear?: (tabName: string) => void;
10 | onItemClick?: (item: INoticeIconData, tabProps: INoticeIconProps) => void;
11 | onLoadMore?: (tabProps: INoticeIconProps) => void;
12 | onTabChange?: (tabTile: string) => void;
13 | style?: React.CSSProperties;
14 | onPopupVisibleChange?: (visible: boolean) => void;
15 | popupVisible?: boolean;
16 | locale?: {
17 | emptyText: string;
18 | clear: string;
19 | loadedAll: string;
20 | loadMore: string;
21 | };
22 | clearClose?: boolean;
23 | }
24 |
25 | export default class NoticeIcon extends React.Component {
26 | public static Tab: typeof NoticeIconTab;
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: NoticeIcon
3 | subtitle: Notification Menu
4 | cols: 1
5 | order: 9
6 | ---
7 |
8 | 用在导航工具栏上,作为整个产品统一的通知中心。
9 |
10 | ## API
11 |
12 | Property | Description | Type | Default
13 | ----|------|-----|------
14 | count | Total number of messages | number | -
15 | bell | Change the bell Icon | ReactNode | ` `
16 | loading | Popup card loading status | boolean | `false`
17 | onClear | Click to clear button the callback | function(tabName) | -
18 | onItemClick | Click on the list item's callback | function(item, tabProps) | -
19 | onLoadMore | Callback of click for loading more | function(tabProps, event) | -
20 | onPopupVisibleChange | Popup Card Showing or Hiding Callbacks | function(visible) | -
21 | onTabChange | Switching callbacks for tabs | function(tabTitle) | -
22 | popupVisible | Popup card display state | boolean | -
23 | locale | Default message text | Object | `{ emptyText: 'No notifications', clear: 'Clear', loadedAll: 'Loaded', loadMore: 'Loading more' }`
24 | clearClose | Close menu after clear | boolean | `false`
25 |
26 | ### NoticeIcon.Tab
27 |
28 | Property | Description | Type | Default
29 | ----|------|-----|------
30 | count | Unread messages count of this tab | number | list.length
31 | emptyText | Message text when list is empty | ReactNode | -
32 | emptyImage | Image when list is empty | string | -
33 | list | List data, format refer to the following table | Array | `[]`
34 | loadedAll | All messages have been loaded | boolean | `true`
35 | loading | Loading status of this tab | boolean | `false`
36 | name | identifier for message Tab | string | -
37 | scrollToLoad | Scroll to load | boolean | `true`
38 | skeletonCount | Number of skeleton when tab is loading | number | `5`
39 | skeletonProps | Props of skeleton | SkeletonProps | `{}`
40 | showClear | Clear button display status | boolean | `true`
41 | title | header for message Tab | string | -
42 |
43 | ### Tab data
44 |
45 | Property | Description | Type | Default
46 | ----|------|-----|------
47 | avatar | avatar img url | string \| ReactNode | -
48 | title | title | ReactNode | -
49 | description | description info | ReactNode | -
50 | datetime | Timestamps | ReactNode | -
51 | extra | Additional information in the upper right corner of the list item | ReactNode | -
52 | clickClose | Close menu after clicking list item | boolean | `false`
53 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .popover {
4 | position: relative;
5 | width: 336px;
6 | }
7 |
8 | .noticeButton {
9 | display: inline-block;
10 | cursor: pointer;
11 | transition: all 0.3s;
12 | }
13 | .icon {
14 | padding: 4px;
15 | vertical-align: middle;
16 | }
17 |
18 | .badge {
19 | font-size: 16px;
20 | }
21 |
22 | .tabs {
23 | :global {
24 | .ant-tabs-nav-scroll {
25 | text-align: center;
26 | }
27 | .ant-tabs-bar {
28 | margin-bottom: 0;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.zh-CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: NoticeIcon
3 | subtitle: 通知菜单
4 | cols: 1
5 | order: 9
6 | ---
7 |
8 | 用在导航工具栏上,作为整个产品统一的通知中心。
9 |
10 | ## API
11 |
12 | 参数 | 说明 | 类型 | 默认值
13 | ----|------|-----|------
14 | count | 图标上的消息总数 | number | -
15 | bell | translate this please -> Change the bell Icon | ReactNode | ` `
16 | loading | 弹出卡片加载状态 | boolean | `false`
17 | onClear | 点击清空按钮的回调 | function(tabName) | -
18 | onItemClick | 点击列表项的回调 | function(item, tabProps) | -
19 | onLoadMore | 加载更多的回调 | function(tabProps, event) | -
20 | onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | -
21 | onTabChange | 切换页签的回调 | function(tabTitle) | -
22 | popupVisible | 控制弹层显隐 | boolean | -
23 | locale | 默认文案 | Object | `{ emptyText: 'No notifications', clear: 'Clear', loadedAll: 'Loaded', loadMore: 'Loading more' }`
24 | clearClose | 点击清空按钮后关闭通知菜单 | boolean | `false`
25 |
26 | ### NoticeIcon.Tab
27 |
28 | 参数 | 说明 | 类型 | 默认值
29 | ----|------|-----|------
30 | count | 当前 Tab 未读消息数量 | number | list.length
31 | emptyText | 针对每个 Tab 定制空数据文案 | ReactNode | -
32 | emptyImage | 针对每个 Tab 定制空数据图片 | string | -
33 | list | 列表数据,格式参照下表 | Array | `[]`
34 | loadedAll | 已加载完所有消息 | boolean | `true`
35 | loading | 当前 Tab 的加载状态 | boolean | `false`
36 | name | 消息分类的标识符 | string | -
37 | scrollToLoad | 允许滚动自加载 | boolean | `true`
38 | skeletonCount | 加载时占位骨架的数量 | number | `5`
39 | skeletonProps | 加载时占位骨架的属性 | SkeletonProps | `{}`
40 | showClear | 是否显示清空按钮 | boolean | `true`
41 | title | 消息分类的页签标题 | string | -
42 |
43 | ### Tab data
44 |
45 | 参数 | 说明 | 类型 | 默认值
46 | ----|------|-----|------
47 | avatar | 头像图片链接 | string \| ReactNode | -
48 | title | 标题 | ReactNode | -
49 | description | 描述信息 | ReactNode | -
50 | datetime | 时间戳 | ReactNode | -
51 | extra | 额外信息,在列表项右上角 | ReactNode | -
52 | clickClose | 点击列表项关闭通知菜单 | boolean | `false`
53 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title:
4 | zh-CN: 演示
5 | en-US: Demo
6 | ---
7 |
8 | ## zh-CN
9 |
10 | 各种数据文案的展现方式。
11 |
12 | ## en-US
13 |
14 | Used for presenting various numerical data.
15 |
16 | ````jsx
17 | import NumberInfo from 'ant-design-pro/lib/NumberInfo';
18 | import numeral from 'numeral';
19 |
20 | ReactDOM.render(
21 |
22 | Visits this week}
24 | total={numeral(12321).format('0,0')}
25 | status="up"
26 | subTotal={17.1}
27 | />
28 |
29 | , mountNode);
30 | ````
31 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export interface INumberInfoProps {
3 | title?: React.ReactNode | string;
4 | subTitle?: React.ReactNode | string;
5 | total?: React.ReactNode | string;
6 | status?: 'up' | 'down';
7 | theme?: string;
8 | gap?: number;
9 | subTotal?: number;
10 | style?: React.CSSProperties;
11 | }
12 |
13 | export default class NumberInfo extends React.Component {}
14 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: NumberInfo
3 | cols: 1
4 | order: 10
5 | ---
6 |
7 | Often used in data cards for highlighting the business data.
8 |
9 | ## API
10 |
11 | Property | Description | Type | Default
12 | ----|------|-----|------
13 | title | title | ReactNode\|string | -
14 | subTitle | subtitle | ReactNode\|string | -
15 | total | total amount | ReactNode\|string | -
16 | subTotal | total amount of additional information | ReactNode\|string | -
17 | status | increase state | 'up \| down' | -
18 | theme | state style | string | 'light'
19 | gap | set the spacing (pixels) between numbers and descriptions | number | 8
20 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Icon } from 'antd';
3 | import classNames from 'classnames';
4 | import styles from './index.less';
5 |
6 | const NumberInfo = ({ theme, title, subTitle, total, subTotal, status, suffix, gap, ...rest }) => (
7 |
13 | {title && (
14 |
15 | {title}
16 |
17 | )}
18 | {subTitle && (
19 |
23 | {subTitle}
24 |
25 | )}
26 |
27 |
28 | {total}
29 | {suffix && {suffix} }
30 |
31 | {(status || subTotal) && (
32 |
33 | {subTotal}
34 | {status && }
35 |
36 | )}
37 |
38 |
39 | );
40 |
41 | export default NumberInfo;
42 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .numberInfo {
4 | .suffix {
5 | margin-left: 4px;
6 | color: @text-color;
7 | font-size: 16px;
8 | font-style: normal;
9 | }
10 | .numberInfoTitle {
11 | margin-bottom: 16px;
12 | color: @text-color;
13 | font-size: @font-size-lg;
14 | transition: all 0.3s;
15 | }
16 | .numberInfoSubTitle {
17 | height: 22px;
18 | overflow: hidden;
19 | color: @text-color-secondary;
20 | font-size: @font-size-base;
21 | line-height: 22px;
22 | white-space: nowrap;
23 | text-overflow: ellipsis;
24 | word-break: break-all;
25 | }
26 | .numberInfoValue {
27 | margin-top: 4px;
28 | overflow: hidden;
29 | font-size: 0;
30 | white-space: nowrap;
31 | text-overflow: ellipsis;
32 | word-break: break-all;
33 | & > span {
34 | display: inline-block;
35 | height: 32px;
36 | margin-right: 32px;
37 | color: @heading-color;
38 | font-size: 24px;
39 | line-height: 32px;
40 | }
41 | .subTotal {
42 | margin-right: 0;
43 | color: @text-color-secondary;
44 | font-size: @font-size-lg;
45 | vertical-align: top;
46 | i {
47 | margin-left: 4px;
48 | font-size: 12px;
49 | transform: scale(0.82);
50 | }
51 | :global {
52 | .anticon-caret-up {
53 | color: @red-6;
54 | }
55 | .anticon-caret-down {
56 | color: @green-6;
57 | }
58 | }
59 | }
60 | }
61 | }
62 | .numberInfolight {
63 | .numberInfoValue {
64 | & > span {
65 | color: @text-color;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/index.zh-CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: NumberInfo
3 | subtitle: 数据文本
4 | cols: 1
5 | order: 10
6 | ---
7 |
8 | 常用在数据卡片中,用于突出展示某个业务数据。
9 |
10 | ## API
11 |
12 | 参数 | 说明 | 类型 | 默认值
13 | ----|------|-----|------
14 | title | 标题 | ReactNode\|string | -
15 | subTitle | 子标题 | ReactNode\|string | -
16 | total | 总量 | ReactNode\|string | -
17 | subTotal | 子总量 | ReactNode\|string | -
18 | status | 增加状态 | 'up \| down' | -
19 | theme | 状态样式 | string | 'light'
20 | gap | 设置数字和描述之间的间距(像素)| number | 8
21 |
--------------------------------------------------------------------------------
/src/components/PageHeader/breadcrumb.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { IPageHeaderProps } from './index';
3 |
4 | export default class BreadcrumbView extends React.Component {}
5 |
6 | export function getBreadcrumb(breadcrumbNameMap: object, url: string): object;
7 |
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/image.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 2
3 | title: With Image
4 | ---
5 |
6 | 带图片的页头。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 |
11 | const content = (
12 |
13 |
段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。
14 |
25 |
26 | );
27 |
28 | const extra = (
29 |
30 |
31 |
32 | );
33 |
34 | const breadcrumbList = [{
35 | title: '一级菜单',
36 | href: '/',
37 | }, {
38 | title: '二级菜单',
39 | href: '/',
40 | }, {
41 | title: '三级菜单',
42 | }];
43 |
44 | ReactDOM.render(
45 |
53 | , mountNode);
54 | ````
55 |
56 |
76 |
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/simple.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 3
3 | title: Simple
4 | ---
5 |
6 | 简单的页头。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 |
11 | const breadcrumbList = [{
12 | title: '一级菜单',
13 | href: '/',
14 | }, {
15 | title: '二级菜单',
16 | href: '/',
17 | }, {
18 | title: '三级菜单',
19 | }];
20 |
21 | ReactDOM.render(
22 |
25 | , mountNode);
26 | ````
27 |
28 |
33 |
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/standard.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title: Standard
4 | ---
5 |
6 | 标准页头。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 | import DescriptionList from 'ant-design-pro/lib/DescriptionList';
11 | import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd';
12 |
13 | const { Description } = DescriptionList;
14 | const ButtonGroup = Button.Group;
15 |
16 | const description = (
17 |
18 | 曲丽丽
19 | XX 服务
20 | 2017-07-07
21 | 12421
22 |
23 | );
24 |
25 | const menu = (
26 |
27 | 选项一
28 | 选项二
29 | 选项三
30 |
31 | );
32 |
33 | const action = (
34 |
35 |
36 | 操作
37 | 操作
38 |
39 |
40 |
41 |
42 | 主操作
43 |
44 | );
45 |
46 | const extra = (
47 |
48 |
49 | 状态
50 | 待审批
51 |
52 |
53 | 订单金额
54 | ¥ 568.08
55 |
56 |
57 | );
58 |
59 | const breadcrumbList = [{
60 | title: '一级菜单',
61 | href: '/',
62 | }, {
63 | title: '二级菜单',
64 | href: '/',
65 | }, {
66 | title: '三级菜单',
67 | }];
68 |
69 | const tabList = [{
70 | key: 'detail',
71 | tab: '详情',
72 | }, {
73 | key: 'rule',
74 | tab: '规则',
75 | }];
76 |
77 | function onTabChange(key) {
78 | console.log(key);
79 | }
80 |
81 | ReactDOM.render(
82 |
83 |
}
86 | action={action}
87 | content={description}
88 | extraContent={extra}
89 | breadcrumbList={breadcrumbList}
90 | tabList={tabList}
91 | tabActiveKey="detail"
92 | onTabChange={onTabChange}
93 | />
94 |
95 | , mountNode);
96 | ````
97 |
98 |
103 |
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/structure.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title: Structure
4 | ---
5 |
6 | 基本结构,具备响应式布局功能,主要断点为 768px 和 576px,拖动窗口改变大小试试看。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 |
11 | const breadcrumbList = [{
12 | title: '面包屑',
13 | }];
14 |
15 | const tabList = [{
16 | key: '1',
17 | tab: '页签一',
18 | }, {
19 | key: '2',
20 | tab: '页签二',
21 | }, {
22 | key: '3',
23 | tab: '页签三',
24 | }];
25 |
26 | ReactDOM.render(
27 | }
31 | logo={logo
}
32 | action={action
}
33 | content={content
}
34 | extraContent={extraContent
}
35 | breadcrumbList={breadcrumbList}
36 | tabList={tabList}
37 | tabActiveKey="1"
38 | />
39 |
40 | , mountNode);
41 | ````
42 |
43 |
69 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export interface IPageHeaderProps {
3 | title?: React.ReactNode | string;
4 | logo?: React.ReactNode | string;
5 | action?: React.ReactNode | string;
6 | content?: React.ReactNode;
7 | extraContent?: React.ReactNode;
8 | routes?: any[];
9 | params?: any;
10 | breadcrumbList?: Array<{ title: React.ReactNode; href?: string }>;
11 | tabList?: Array<{ key: string; tab: React.ReactNode }>;
12 | tabActiveKey?: string;
13 | tabDefaultActiveKey?: string;
14 | onTabChange?: (key: string) => void;
15 | tabBarExtraContent?: React.ReactNode;
16 | linkElement?: React.ReactNode;
17 | style?: React.CSSProperties;
18 | home?: React.ReactNode;
19 | wide?: boolean;
20 | hiddenBreadcrumb?: boolean;
21 | }
22 |
23 | export default class PageHeader extends React.Component {}
24 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Tabs, Skeleton } from 'antd';
3 | import classNames from 'classnames';
4 | import styles from './index.less';
5 | import BreadcrumbView from './breadcrumb';
6 |
7 | const { TabPane } = Tabs;
8 | export default class PageHeader extends PureComponent {
9 | onChange = key => {
10 | const { onTabChange } = this.props;
11 | if (onTabChange) {
12 | onTabChange(key);
13 | }
14 | };
15 |
16 | render() {
17 | const {
18 | title,
19 | logo,
20 | action,
21 | content,
22 | extraContent,
23 | tabList,
24 | className,
25 | tabActiveKey,
26 | tabDefaultActiveKey,
27 | tabBarExtraContent,
28 | loading = false,
29 | wide = false,
30 | hiddenBreadcrumb = false,
31 | } = this.props;
32 |
33 | const clsString = classNames(styles.pageHeader, className);
34 | const activeKeyProps = {};
35 | if (tabDefaultActiveKey !== undefined) {
36 | activeKeyProps.defaultActiveKey = tabDefaultActiveKey;
37 | }
38 | if (tabActiveKey !== undefined) {
39 | activeKeyProps.activeKey = tabActiveKey;
40 | }
41 | return (
42 |
43 |
44 |
51 | {hiddenBreadcrumb ? null : }
52 |
53 | {logo &&
{logo}
}
54 |
55 |
56 | {title &&
{title} }
57 | {action &&
{action}
}
58 |
59 |
60 | {content &&
{content}
}
61 | {extraContent &&
{extraContent}
}
62 |
63 |
64 |
65 | {tabList && tabList.length ? (
66 |
72 | {tabList.map(item => (
73 |
74 | ))}
75 |
76 | ) : null}
77 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .pageHeader {
4 | padding: 16px 32px 0 32px;
5 | background: @component-background;
6 | border-bottom: @border-width-base @border-style-base @border-color-split;
7 | .wide {
8 | max-width: 1200px;
9 | margin: auto;
10 | }
11 | .detail {
12 | display: flex;
13 | }
14 |
15 | .row {
16 | display: flex;
17 | width: 100%;
18 | }
19 |
20 | .breadcrumb {
21 | margin-bottom: 16px;
22 | }
23 |
24 | .tabs {
25 | margin: 0 0 0 -8px;
26 |
27 | :global {
28 | // 1px 可以让选中效果显示完成
29 | .ant-tabs-bar {
30 | margin-bottom: 1px;
31 | border-bottom: none;
32 | }
33 | }
34 | }
35 |
36 | .logo {
37 | flex: 0 1 auto;
38 | margin-right: 16px;
39 | padding-top: 1px;
40 | > img {
41 | display: block;
42 | width: 28px;
43 | height: 28px;
44 | border-radius: @border-radius-base;
45 | }
46 | }
47 |
48 | .title {
49 | color: @heading-color;
50 | font-weight: 500;
51 | font-size: 20px;
52 | }
53 |
54 | .action {
55 | min-width: 266px;
56 | margin-left: 56px;
57 |
58 | :global {
59 | .ant-btn-group:not(:last-child),
60 | .ant-btn:not(:last-child) {
61 | margin-right: 8px;
62 | }
63 |
64 | .ant-btn-group > .ant-btn {
65 | margin-right: 0;
66 | }
67 | }
68 | }
69 |
70 | .title,
71 | .content {
72 | flex: auto;
73 | }
74 |
75 | .action,
76 | .extraContent,
77 | .main {
78 | flex: 0 1 auto;
79 | }
80 |
81 | .main {
82 | width: 100%;
83 | }
84 |
85 | .title,
86 | .action {
87 | margin-bottom: 16px;
88 | }
89 |
90 | .logo,
91 | .content,
92 | .extraContent {
93 | margin-bottom: 16px;
94 | }
95 |
96 | .action,
97 | .extraContent {
98 | text-align: right;
99 | }
100 |
101 | .extraContent {
102 | min-width: 242px;
103 | margin-left: 88px;
104 | }
105 | }
106 |
107 | @media screen and (max-width: @screen-xl) {
108 | .pageHeader {
109 | .extraContent {
110 | margin-left: 44px;
111 | }
112 | }
113 | }
114 |
115 | @media screen and (max-width: @screen-lg) {
116 | .pageHeader {
117 | .extraContent {
118 | margin-left: 20px;
119 | }
120 | }
121 | }
122 |
123 | @media screen and (max-width: @screen-md) {
124 | .pageHeader {
125 | .row {
126 | display: block;
127 | }
128 |
129 | .action,
130 | .extraContent {
131 | margin-left: 0;
132 | text-align: left;
133 | }
134 | }
135 | }
136 |
137 | @media screen and (max-width: @screen-sm) {
138 | .pageHeader {
139 | .detail {
140 | display: block;
141 | }
142 | }
143 | }
144 |
145 | @media screen and (max-width: @screen-xs) {
146 | .pageHeader {
147 | .action {
148 | :global {
149 | .ant-btn-group,
150 | .ant-btn {
151 | display: block;
152 | margin-bottom: 8px;
153 | }
154 | .ant-btn-group > .ant-btn {
155 | display: inline-block;
156 | margin-bottom: 0;
157 | }
158 | }
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: PageHeader
4 | zh-CN: PageHeader
5 | subtitle: 页头
6 | cols: 1
7 | order: 11
8 | ---
9 |
10 | 页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。
11 |
12 | ## API
13 |
14 | | 参数 | 说明 | 类型 | 默认值 |
15 | |----------|------------------------------------------|-------------|-------|
16 | | title | title 区域 | ReactNode | - |
17 | | logo | logo区域 | ReactNode | - |
18 | | action | 操作区,位于 title 行的行尾 | ReactNode | - |
19 | | home | 默认的主页说明文字 | ReactNode | - |
20 | | content | 内容区 | ReactNode | - |
21 | | extraContent | 额外内容区,位于content的右侧 | ReactNode | - |
22 | | breadcrumbList | 面包屑数据,配置了此属性时 `routes` `params` `location` `breadcrumbNameMap` 无效 | array<{title: ReactNode, href?: string}> | - |
23 | | hiddenBreadcrumb |隐藏面包屑 | boolean | false |
24 | | routes | 面包屑相关属性,router 的路由栈信息 | object[] | - |
25 | | params | 面包屑相关属性,路由的参数 | object | - |
26 | | location | 面包屑相关属性,当前的路由信息 | object | - |
27 | | breadcrumbNameMap | 面包屑相关属性,路由的地址-名称映射表 | object | - |
28 | | tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - |
29 | | tabActiveKey | 当前高亮的 tab 项 | string | - |
30 | | tabDefaultActiveKey | 默认高亮的 tab 项 | string | 第一项 |
31 | | wide | 是否定宽 | boolean | false |
32 | | onTabChange | 切换面板的回调 | (key) => void | - |
33 | | itemRender | 自定义节点方法 | (menuItem) => ReactNode | - |
34 | | linkElement | 定义链接的元素,默认为 `a`,可传入 react-router 的 Link | string\|ReactElement | - |
35 |
36 | > 面包屑的配置方式有三种,一是直接配置 `breadcrumbList`,二是结合 `react-router@2` `react-router@3`,配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router),三是结合 `react-router@4`,配置 `location` `breadcrumbNameMap`,优先级依次递减,脚手架中使用最后一种。 对于后两种用法,你也可以将 `routes` `params` 及 `location` `breadcrumbNameMap` 放到 context 中,组件会自动获取。
37 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.test.js:
--------------------------------------------------------------------------------
1 | import { getBreadcrumb } from './breadcrumb';
2 | import { urlToList } from '../_utils/pathTools';
3 |
4 | const routerData = {
5 | '/dashboard/analysis': {
6 | name: '分析页',
7 | },
8 | '/userinfo': {
9 | name: '用户列表',
10 | },
11 | '/userinfo/:id': {
12 | name: '用户信息',
13 | },
14 | '/userinfo/:id/addr': {
15 | name: '收货订单',
16 | },
17 | };
18 | describe('test getBreadcrumb', () => {
19 | it('Simple url', () => {
20 | expect(getBreadcrumb(routerData, '/dashboard/analysis').name).toEqual('分析页');
21 | });
22 | it('Parameters url', () => {
23 | expect(getBreadcrumb(routerData, '/userinfo/2144').name).toEqual('用户信息');
24 | });
25 | it('The middle parameter url', () => {
26 | expect(getBreadcrumb(routerData, '/userinfo/2144/addr').name).toEqual('收货订单');
27 | });
28 | it('Loop through the parameters', () => {
29 | const urlNameList = urlToList('/userinfo/2144/addr').map(
30 | url => getBreadcrumb(routerData, url).name
31 | );
32 | expect(urlNameList).toEqual(['用户列表', '用户信息', '收货订单']);
33 | });
34 |
35 | it('a path', () => {
36 | const urlNameList = urlToList('/userinfo').map(url => getBreadcrumb(routerData, url).name);
37 | expect(urlNameList).toEqual(['用户列表']);
38 | });
39 | it('Secondary path', () => {
40 | const urlNameList = urlToList('/userinfo/2144').map(url => getBreadcrumb(routerData, url).name);
41 | expect(urlNameList).toEqual(['用户列表', '用户信息']);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/components/PageHeaderWrapper/GridContent.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'dva';
3 | import styles from './GridContent.less';
4 |
5 | class GridContent extends PureComponent {
6 | render() {
7 | const { contentWidth, children } = this.props;
8 | let className = `${styles.main}`;
9 | if (contentWidth === 'Fixed') {
10 | className = `${styles.main} ${styles.wide}`;
11 | }
12 | return {children}
;
13 | }
14 | }
15 |
16 | export default connect(({ setting }) => ({
17 | contentWidth: setting.contentWidth,
18 | }))(GridContent);
19 |
--------------------------------------------------------------------------------
/src/components/PageHeaderWrapper/GridContent.less:
--------------------------------------------------------------------------------
1 | .main {
2 | width: 100%;
3 | height: 100%;
4 | min-height: 100%;
5 | transition: 0.3s;
6 | &.wide {
7 | max-width: 1200px;
8 | margin: 0 auto;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/PageHeaderWrapper/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage } from 'umi/locale';
3 | import Link from 'umi/link';
4 | import PageHeader from '@/components/PageHeader';
5 | import { connect } from 'dva';
6 | import GridContent from './GridContent';
7 | import styles from './index.less';
8 | import MenuContext from '@/layouts/MenuContext';
9 |
10 | const PageHeaderWrapper = ({ children, contentWidth, wrapperClassName, top, ...restProps }) => (
11 |
12 | {top}
13 |
14 | {value => {
15 | return (
16 | }
19 | {...value}
20 | key="pageheader"
21 | {...restProps}
22 | linkElement={Link}
23 | itemRender={item => {
24 | if (item.locale) {
25 | return ;
26 | }
27 | return item.title;
28 | }}
29 | />
30 | )
31 | }}
32 |
33 | {children ? (
34 |
35 | {children}
36 |
37 | ) : null}
38 |
39 | );
40 |
41 | export default connect(({ setting }) => ({
42 | contentWidth: setting.contentWidth,
43 | }))(PageHeaderWrapper);
44 |
--------------------------------------------------------------------------------
/src/components/PageHeaderWrapper/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .content {
4 | margin: 24px 24px 0;
5 | }
6 |
7 | @media screen and (max-width: @screen-sm) {
8 | .content {
9 | margin: 24px 0 0;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/PageLoading/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spin } from 'antd';
3 |
4 | // loading components from code split
5 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
6 | export default () => (
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/components/SelectLang/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { formatMessage, setLocale, getLocale } from 'umi/locale';
3 | import { Menu, Icon } from 'antd';
4 | import classNames from 'classnames';
5 | import HeaderDropdown from '../HeaderDropdown';
6 | import styles from './index.less';
7 |
8 | export default class SelectLang extends PureComponent {
9 | changeLang = ({ key }) => {
10 | setLocale(key);
11 | };
12 |
13 | render() {
14 | const { className } = this.props;
15 | const selectedLang = getLocale();
16 | const locales = ['zh-CN', 'en-US' ];
17 | const languageLabels = {
18 | 'zh-CN': '简体中文',
19 | 'en-US': 'English',
20 | };
21 | const languageIcons = {
22 | 'zh-CN': '🇨🇳',
23 | 'en-US': '🇬🇧',
24 | };
25 | const langMenu = (
26 |
27 | {locales.map(locale => (
28 |
29 |
30 | {languageIcons[locale]}
31 | {' '}
32 | {languageLabels[locale]}
33 |
34 | ))}
35 |
36 | );
37 | return (
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/SelectLang/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .menu {
4 | :global(.anticon) {
5 | margin-right: 8px;
6 | }
7 | :global(.ant-dropdown-menu-item) {
8 | min-width: 160px;
9 | }
10 | }
11 |
12 | .dropDown {
13 | line-height: @layout-header-height;
14 | vertical-align: top;
15 | cursor: pointer;
16 | > i {
17 | font-size: 16px !important;
18 | transform: none !important;
19 | svg {
20 | position: relative;
21 | top: -1px;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/SettingDrawer/BlockCheckbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip, Icon } from 'antd';
3 | import style from './index.less';
4 |
5 | const BlockChecbox = ({ value, onChange, list }) => (
6 |
7 | {list.map(item => (
8 |
9 | onChange(item.key)}>
10 |
11 |
17 |
18 |
19 |
20 |
21 | ))}
22 |
23 | );
24 |
25 | export default BlockChecbox;
26 |
--------------------------------------------------------------------------------
/src/components/SettingDrawer/ThemeColor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip, Icon } from 'antd';
3 | import { formatMessage } from 'umi/locale';
4 | import styles from './ThemeColor.less';
5 |
6 | const Tag = ({ color, check, ...rest }) => (
7 |
13 | {check ? : ''}
14 |
15 | );
16 |
17 | const ThemeColor = ({ colors, title, value, onChange }) => {
18 | let colorList = colors;
19 | if (!colors) {
20 | colorList = [
21 | {
22 | key: 'dust',
23 | color: '#F5222D',
24 | },
25 | {
26 | key: 'volcano',
27 | color: '#FA541C',
28 | },
29 | {
30 | key: 'sunset',
31 | color: '#FAAD14',
32 | },
33 | {
34 | key: 'cyan',
35 | color: '#13C2C2',
36 | },
37 | {
38 | key: 'green',
39 | color: '#52C41A',
40 | },
41 | {
42 | key: 'daybreak',
43 | color: '#1890FF',
44 | },
45 | {
46 | key: 'geekblue',
47 | color: '#2F54EB',
48 | },
49 | {
50 | key: 'purple',
51 | color: '#722ED1',
52 | },
53 | ];
54 | }
55 | return (
56 |
57 |
{title}
58 |
59 | {colorList.map(({ key, color }) => (
60 |
61 | onChange && onChange(color)}
66 | />
67 |
68 | ))}
69 |
70 |
71 | );
72 | };
73 |
74 | export default ThemeColor;
75 |
--------------------------------------------------------------------------------
/src/components/SettingDrawer/ThemeColor.less:
--------------------------------------------------------------------------------
1 | .themeColor {
2 | margin-top: 24px;
3 | overflow: hidden;
4 | .title {
5 | margin-bottom: 12px;
6 | color: rgba(0, 0, 0, 0.65);
7 | font-size: 14px;
8 | line-height: 22px;
9 | }
10 | .colorBlock {
11 | float: left;
12 | width: 20px;
13 | height: 20px;
14 | margin-right: 8px;
15 | color: #fff;
16 | font-weight: bold;
17 | text-align: center;
18 | border-radius: 2px;
19 | cursor: pointer;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/SettingDrawer/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .content {
4 | position: relative;
5 | min-height: 100%;
6 | background: #fff;
7 | }
8 |
9 | .blockChecbox {
10 | display: flex;
11 | .item {
12 | position: relative;
13 | margin-right: 16px;
14 | // box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
15 | border-radius: @border-radius-base;
16 | cursor: pointer;
17 | img {
18 | width: 48px;
19 | }
20 | }
21 | .selectIcon {
22 | position: absolute;
23 | top: 0;
24 | right: 0;
25 | width: 100%;
26 | height: 100%;
27 | padding-top: 15px;
28 | padding-left: 24px;
29 | color: @primary-color;
30 | font-weight: bold;
31 | font-size: 14px;
32 | }
33 | }
34 |
35 | .color_block {
36 | display: inline-block;
37 | width: 38px;
38 | height: 22px;
39 | margin: 4px;
40 | margin-right: 12px;
41 | vertical-align: middle;
42 | border-radius: 4px;
43 | cursor: pointer;
44 | }
45 |
46 | .title {
47 | margin-bottom: 12px;
48 | color: @heading-color;
49 | font-size: 14px;
50 | line-height: 22px;
51 | }
52 |
53 | .handle {
54 | position: absolute;
55 | top: 240px;
56 | right: 300px;
57 | z-index: 0;
58 | display: flex;
59 | justify-content: center;
60 | align-items: center;
61 | width: 48px;
62 | height: 48px;
63 | font-size: 16px;
64 | text-align: center;
65 | background: @primary-color;
66 | border-radius: 4px 0 0 4px;
67 | cursor: pointer;
68 | pointer-events: auto;
69 | }
70 |
71 | .productionHint {
72 | margin-top: 16px;
73 | font-size: 12px;
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/SiderMenu.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Suspense } from 'react';
2 | import { Layout } from 'antd';
3 | import classNames from 'classnames';
4 | import Link from 'umi/link';
5 | import styles from './index.less';
6 | import PageLoading from '../PageLoading';
7 | import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
8 | import pconf from 'projectConfig';
9 | import { formatMessage } from 'umi/locale';
10 |
11 |
12 | const BaseMenu = React.lazy(() => import('./BaseMenu'));
13 | const { Sider } = Layout;
14 |
15 | let firstMount = true;
16 |
17 | export default class SiderMenu extends PureComponent {
18 | constructor(props) {
19 | super(props);
20 | this.state = {
21 | openKeys: getDefaultCollapsedSubMenus(props),
22 | };
23 | }
24 |
25 | componentDidMount() {
26 | firstMount = false;
27 | }
28 |
29 | static getDerivedStateFromProps(props, state) {
30 | const { pathname, flatMenuKeysLen } = state;
31 | if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) {
32 | return {
33 | pathname: props.location.pathname,
34 | flatMenuKeysLen: props.flatMenuKeys.length,
35 | openKeys: getDefaultCollapsedSubMenus(props),
36 | };
37 | }
38 | return null;
39 | }
40 |
41 | isMainMenu = key => {
42 | const { menuData } = this.props;
43 | return menuData.some(item => {
44 | if (key) {
45 | return item.key === key || item.path === key;
46 | }
47 | return false;
48 | });
49 | };
50 |
51 | handleOpenChange = openKeys => {
52 | const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
53 | this.setState({
54 | openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
55 | });
56 | };
57 |
58 | render() {
59 | const { logo, collapsed, onCollapse, fixSiderbar, theme, isMobile } = this.props;
60 | const { openKeys } = this.state;
61 | const defaultProps = collapsed ? {} : { openKeys };
62 |
63 | const siderClassName = classNames(styles.sider, {
64 | [styles.fixSiderBar]: fixSiderbar,
65 | [styles.light]: theme === 'light',
66 | });
67 | return (
68 | {
74 | if (firstMount || !isMobile) {
75 | onCollapse(collapse);
76 | }
77 | }}
78 | width={256}
79 | theme={theme}
80 | className={siderClassName}
81 | >
82 |
83 |
84 |
85 |
{ formatMessage({id: 'app.title'}) }
86 |
87 |
88 | }>
89 |
97 |
98 |
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/SiderMenu.test.js:
--------------------------------------------------------------------------------
1 | import { getFlatMenuKeys } from './SiderMenuUtils';
2 |
3 | const menu = [
4 | {
5 | path: '/dashboard',
6 | children: [
7 | {
8 | path: '/dashboard/name',
9 | },
10 | ],
11 | },
12 | {
13 | path: '/userinfo',
14 | children: [
15 | {
16 | path: '/userinfo/:id',
17 | children: [
18 | {
19 | path: '/userinfo/:id/info',
20 | },
21 | ],
22 | },
23 | ],
24 | },
25 | ];
26 |
27 | const flatMenuKeys = getFlatMenuKeys(menu);
28 |
29 | describe('test convert nested menu to flat menu', () => {
30 | it('simple menu', () => {
31 | expect(flatMenuKeys).toEqual([
32 | '/dashboard',
33 | '/dashboard/name',
34 | '/userinfo',
35 | '/userinfo/:id',
36 | '/userinfo/:id/info',
37 | ]);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/SiderMenuUtils.js:
--------------------------------------------------------------------------------
1 | import pathToRegexp from 'path-to-regexp';
2 | import { urlToList } from '../_utils/pathTools';
3 |
4 | /**
5 | * Recursively flatten the data
6 | * [{path:string},{path:string}] => {path,path2}
7 | * @param menus
8 | */
9 | export const getFlatMenuKeys = menuData => {
10 | let keys = [];
11 | menuData.forEach(item => {
12 | keys.push(item.path);
13 | if (item.children) {
14 | keys = keys.concat(getFlatMenuKeys(item.children));
15 | }
16 | });
17 | return keys;
18 | };
19 |
20 | export const getMenuMatches = (flatMenuKeys, path) =>
21 | flatMenuKeys.filter(item => {
22 | if (item) {
23 | return pathToRegexp(item).test(path);
24 | }
25 | return false;
26 | });
27 | /**
28 | * 获得菜单子节点
29 | * @memberof SiderMenu
30 | */
31 | export const getDefaultCollapsedSubMenus = props => {
32 | const {
33 | location: { pathname },
34 | flatMenuKeys,
35 | } = props;
36 | return urlToList(pathname)
37 | .map(item => getMenuMatches(flatMenuKeys, item)[0])
38 | .filter(item => item)
39 | .reduce((acc, curr) => [...acc, curr], ['/']);
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Drawer } from 'antd';
3 | import SiderMenu from './SiderMenu';
4 | import { getFlatMenuKeys } from './SiderMenuUtils';
5 |
6 | const SiderMenuWrapper = React.memo(props => {
7 | const { isMobile, menuData, collapsed, onCollapse } = props;
8 | const flatMenuKeys = getFlatMenuKeys(menuData);
9 | return isMobile ? (
10 | onCollapse(true)}
14 | style={{
15 | padding: 0,
16 | height: '100vh',
17 | }}
18 | >
19 |
20 |
21 | ) : (
22 |
23 | );
24 | });
25 |
26 | export default SiderMenuWrapper;
27 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | @nav-header-height: @layout-header-height;
4 |
5 | .logo {
6 | position: relative;
7 | height: @nav-header-height;
8 | padding-left: (@menu-collapsed-width - 32px) / 2;
9 | overflow: hidden;
10 | line-height: @nav-header-height;
11 | background: #002140;
12 | transition: all 0.3s;
13 | img {
14 | display: inline-block;
15 | height: 32px;
16 | vertical-align: middle;
17 | }
18 | h2 {
19 | display: inline-block;
20 | margin: 0 0 0 12px;
21 | color: white;
22 | font-size: 20px;
23 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
24 | vertical-align: middle;
25 | width: 180px;
26 | white-space: nowrap;
27 | }
28 | }
29 | .sider {
30 | position: relative;
31 | z-index: 10;
32 | min-height: 100vh;
33 | box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
34 | &.fixSiderBar {
35 | position: fixed;
36 | top: 0;
37 | left: 0;
38 | box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
39 | :global {
40 | .ant-menu-root {
41 | height: ~'calc(100vh - @{nav-header-height})';
42 | overflow-y: auto;
43 | }
44 | .ant-menu-inline {
45 | border-right: 0;
46 | .ant-menu-item,
47 | .ant-menu-submenu-title {
48 | width: 100%;
49 | }
50 | }
51 | }
52 | }
53 | &.light {
54 | background-color: white;
55 | box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
56 | .logo {
57 | background: white;
58 | box-shadow: 1px 1px 0 0 @border-color-split;
59 | h1 {
60 | color: @primary-color;
61 | }
62 | }
63 | :global(.ant-menu-light) {
64 | border-right-color: transparent;
65 | }
66 | }
67 | }
68 |
69 | .icon {
70 | width: 14px;
71 | vertical-align: baseline;
72 | }
73 |
74 | :global {
75 | .top-nav-menu li.ant-menu-item {
76 | height: @nav-header-height;
77 | line-height: @nav-header-height;
78 | }
79 | .drawer .drawer-content {
80 | background: #001529;
81 | }
82 | .ant-menu-inline-collapsed {
83 | & > .ant-menu-item .sider-menu-item-img + span,
84 | &
85 | > .ant-menu-item-group
86 | > .ant-menu-item-group-list
87 | > .ant-menu-item
88 | .sider-menu-item-img
89 | + span,
90 | & > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span {
91 | display: inline-block;
92 | max-width: 0;
93 | opacity: 0;
94 | }
95 | }
96 | .ant-menu-item .sider-menu-item-img + span,
97 | .ant-menu-submenu-title .sider-menu-item-img + span {
98 | opacity: 1;
99 | transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
100 | }
101 | .ant-drawer-left {
102 | .ant-drawer-body {
103 | padding: 0;
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/StandardFormRow/index.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface StandardFormRowProps {
4 | title: string;
5 | last?: boolean;
6 | block?: boolean;
7 | grid?: boolean;
8 | style?: React.CSSProperties;
9 | }
10 |
11 | export default class StandardFormRow extends React.Component {}
12 |
--------------------------------------------------------------------------------
/src/components/StandardFormRow/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import styles from './index.less';
4 |
5 | const StandardFormRow = ({ title, children, last, block, grid, ...rest }) => {
6 | const cls = classNames(styles.standardFormRow, {
7 | [styles.standardFormRowBlock]: block,
8 | [styles.standardFormRowLast]: last,
9 | [styles.standardFormRowGrid]: grid,
10 | });
11 |
12 | return (
13 |
14 | {title && (
15 |
16 | {title}
17 |
18 | )}
19 |
{children}
20 |
21 | );
22 | };
23 |
24 | export default StandardFormRow;
25 |
--------------------------------------------------------------------------------
/src/components/StandardFormRow/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .standardFormRow {
4 | display: flex;
5 | margin-bottom: 16px;
6 | padding-bottom: 16px;
7 | border-bottom: 1px dashed @border-color-split;
8 | :global {
9 | .ant-form-item {
10 | margin-right: 24px;
11 | }
12 | .ant-form-item-label label {
13 | margin-right: 0;
14 | color: @text-color;
15 | }
16 | .ant-form-item-label,
17 | .ant-form-item-control {
18 | padding: 0;
19 | line-height: 32px;
20 | }
21 | }
22 | .label {
23 | flex: 0 0 auto;
24 | margin-right: 24px;
25 | color: @heading-color;
26 | font-size: @font-size-base;
27 | text-align: right;
28 | & > span {
29 | display: inline-block;
30 | height: 32px;
31 | line-height: 32px;
32 | &::after {
33 | content: ':';
34 | }
35 | }
36 | }
37 | .content {
38 | flex: 1 1 0;
39 | :global {
40 | .ant-form-item:last-child {
41 | margin-right: 0;
42 | }
43 | }
44 | }
45 | }
46 |
47 | .standardFormRowLast {
48 | margin-bottom: 0;
49 | padding-bottom: 0;
50 | border: none;
51 | }
52 |
53 | .standardFormRowBlock {
54 | :global {
55 | .ant-form-item,
56 | div.ant-form-item-control-wrapper {
57 | display: block;
58 | }
59 | }
60 | }
61 |
62 | .standardFormRowGrid {
63 | :global {
64 | .ant-form-item,
65 | div.ant-form-item-control-wrapper {
66 | display: block;
67 | }
68 | .ant-form-item-label {
69 | float: left;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/TMDataList/query-condition.ts:
--------------------------------------------------------------------------------
1 | import { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import _ from 'lodash';
4 |
5 | interface IQueryConditionProps {
6 | componentRef: Function,
7 | onQueryConditionChange: Function,
8 | loading: boolean,
9 | }
10 |
11 | /**
12 | * 子类需要处理搜索条件变化时的回调。
13 | * 在 props 中会有 onQueryConditionChange 回调函数
14 | *
15 | */
16 | export default abstract class AbstractQueryCondition extends PureComponent {
17 |
18 | static propTypes = {
19 | componentRef: PropTypes.func,
20 | onQueryConditionChange: PropTypes.func,
21 | loading: PropTypes.bool,
22 | };
23 |
24 | protected constructor(props) {
25 | super(props);
26 |
27 | props.componentRef && props.componentRef(this);
28 | }
29 |
30 | protected lastQueryCondition: Object;
31 |
32 | protected fireQueryConditionChangeEvent = () => {
33 | if (_.isEqual(this.lastQueryCondition, this.state)) {
34 | return;
35 | }
36 | this.lastQueryCondition = _.cloneDeep(this.state);
37 | const { onQueryConditionChange } = this.props;
38 | onQueryConditionChange(this.state);
39 | }
40 |
41 | protected onFieldChange = (valueObj) => {
42 | this.setState({...valueObj}, this.fireQueryConditionChangeEvent);
43 | }
44 |
45 | public reset = () => {
46 | this.resetForm(this.fireQueryConditionChangeEvent);
47 |
48 | }
49 |
50 | protected abstract resetForm(callback): Object;
51 |
52 | abstract getFormValues(): Object;
53 |
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/components/TMDataList/style.less:
--------------------------------------------------------------------------------
1 | .query-content {
2 | padding: 24px;
3 | background: #FFF;
4 | border-radius: 2px;
5 | margin-bottom: 16px;
6 | }
7 |
8 | .content {
9 | padding: 24px;
10 | background: #FFF;
11 | border-radius: 2px;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/TMFormDrawer/index2.tsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import {Button, Drawer, Form,} from "antd";
3 | import { FormComponentProps } from "antd/lib/form/Form";
4 | import PropTypes from "prop-types";
5 | import PageLoading from '@/components/PageLoading';
6 |
7 | const styles = require("./style.less");
8 |
9 | interface IFormComponentProps extends FormComponentProps {
10 | componentRef: Function,
11 | title: string,
12 | width: number,
13 | loading: boolean,
14 | onSubmitBtnClick: Function,
15 | }
16 |
17 |
18 | export default abstract class TMFormDrawer extends PureComponent {
19 |
20 | static propTypes = {
21 | componentRef: PropTypes.func,
22 | title: PropTypes.string,
23 | width: PropTypes.number,
24 | /** drawer loading status, usually used when form data is on loading */
25 | loading: PropTypes.bool,
26 | /**
27 | * IMPORTANT: trigger when submit button clicked, this function should return or a boolean value or a promise which resolve a
28 | * boolean value, TMFormDrawer use this for deciding whether close or remain drawer.
29 | * */
30 | onSubmitBtnClick: PropTypes.func,
31 | };
32 |
33 | static defaultProps = {
34 | componentRef: () => {},
35 | title: '',
36 | width: 620,
37 | loading: false,
38 | onSubmitBtnClick: () => true,
39 | };
40 |
41 | state = {
42 | visible: false,
43 | submitLoading: false,
44 | };
45 |
46 | protected constructor(props) {
47 | super(props);
48 | props.componentRef(this);
49 | }
50 |
51 | show() {
52 | this.setState({ visible: true });
53 | }
54 |
55 | abstract renderForm(): React.ReactNode;
56 |
57 | render() {
58 | const { visible, submitLoading } = this.state;
59 | const { width, title, loading } = this.props;
60 | return (
61 |
70 | {
71 | loading ? (
72 |
73 | ) : (
74 |
75 |
78 |
79 | 提交
80 | 关闭
81 |
82 |
83 | )
84 | }
85 |
86 | );
87 | }
88 |
89 | _closeDrawer = () => {
90 | this.setState({ visible: false });
91 | }
92 |
93 | _fireSubmitEvent = () => {
94 | const { onSubmitBtnClick } = this.props;
95 | this.setState({ submitLoading: true }, () => {
96 |
97 | Promise.resolve(onSubmitBtnClick()).then(() => {
98 | this.setState({ submitLoading: false })
99 | }).catch(() => {
100 | this.setState({ submitLoading: false })
101 | });
102 | })
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/TMFormDrawer/style.less:
--------------------------------------------------------------------------------
1 | .form-drawer {
2 | :global {
3 | .ant-drawer-wrapper-body {
4 | height: calc(100% - 53px) !important;
5 | .ant-drawer-body {
6 | height: calc(100% - 55px) !important;
7 | }
8 | }
9 | .ant-form-item {
10 | margin-bottom: 0px;
11 | padding-bottom: 12px;
12 | .ant-form-item-label {
13 | line-height: 17px;
14 | text-align: left;
15 | display: block;
16 | width: 100%;
17 | float: none;
18 | &>label {
19 | font-size: 12px;
20 | color: rgba(0,0,0,0.85);
21 | }
22 | &>label.ant-form-item-required:before {
23 | content: none;
24 | margin-right: 0;
25 | }
26 | &>label.ant-form-item-required:after {
27 | content: '*';
28 | font-size: 12px;
29 | color: red;
30 | }
31 | &>label:after {
32 | content: none;
33 | }
34 | }
35 | .ant-form-item-control-wrapper {
36 | float: none;
37 | display: block;
38 | width: 100%;
39 | }
40 | }
41 | }
42 |
43 | .footer {
44 | position: absolute;
45 | left: 0;
46 | bottom: 0;
47 | width: 100%;
48 | border-top: 1px solid #e9e9e9;
49 | padding: 10px 16px;
50 | background: #fff;
51 | z-index: 2;
52 |
53 | .button {
54 | margin-right: 16px;
55 | }
56 |
57 | }
58 |
59 |
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/TMStandardFormRow/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import styles from './index.less';
4 |
5 | const StandardFormRow = ({ title, children, last, block, grid, labelWidth, ...rest }) => {
6 | const cls = classNames(styles.standardFormRow, {
7 | [styles.standardFormRowBlock]: block,
8 | [styles.standardFormRowLast]: last,
9 | [styles.standardFormRowGrid]: grid,
10 | });
11 |
12 | return (
13 |
14 | {title && (
15 |
16 | {title}
17 |
18 | )}
19 |
{children}
20 |
21 | );
22 | };
23 |
24 | export default StandardFormRow;
25 |
--------------------------------------------------------------------------------
/src/components/TMStandardFormRow/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .standardFormRow {
4 | display: flex;
5 | margin-bottom: 16px;
6 | padding-bottom: 16px;
7 | border-bottom: 1px dashed @border-color-split;
8 | :global {
9 | .ant-form-item {
10 | margin-right: 24px;
11 | }
12 | .ant-form-item-label label {
13 | margin-right: 8px;
14 | color: rgba(0,0,0,0.85);
15 | }
16 | .ant-form-item-label,
17 | .ant-form-item-control {
18 | padding: 0;
19 | line-height: 32px;
20 | }
21 | }
22 | .label {
23 | flex: 0 0 auto;
24 | margin-right: 24px;
25 | color: @heading-color;
26 | font-size: @font-size-base;
27 | text-align: right;
28 | & > span {
29 | display: inline-block;
30 | height: 32px;
31 | line-height: 32px;
32 | &::after {
33 | content: ':';
34 | }
35 | }
36 | }
37 | .content {
38 | flex: 1 1 0;
39 | :global {
40 | .ant-form-item:last-child {
41 | margin-right: 0;
42 | }
43 | }
44 | }
45 | }
46 |
47 | .standardFormRowLast {
48 | margin-bottom: 0;
49 | padding-bottom: 0;
50 | border: none;
51 | }
52 |
53 | .standardFormRowBlock {
54 | :global {
55 | .ant-form-item,
56 | div.ant-form-item-control-wrapper {
57 | display: block;
58 | }
59 | }
60 | }
61 |
62 | .standardFormRowGrid {
63 | :global {
64 | .ant-form-item,
65 | div.ant-form-item-control-wrapper {
66 | display: block;
67 | }
68 | .ant-form-item-label {
69 | float: left;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/TagSelect/TagSelectOption.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export interface ITagSelectOptionProps {
4 | value: string | number;
5 | style?: React.CSSProperties;
6 | }
7 |
8 | export default class TagSelectOption extends React.Component {}
9 |
--------------------------------------------------------------------------------
/src/components/TagSelect/demo/controlled.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 3
3 | title: 受控模式
4 | ---
5 |
6 | 结合 `Tag` 的 `TagSelect` 组件,方便的应用于筛选类目的业务场景中。
7 |
8 | ```jsx
9 | import { Button } from 'antd';
10 | import TagSelect from 'ant-design-pro/lib/TagSelect';
11 |
12 | class Demo extends React.Component {
13 | state = {
14 | value: ['cat1'],
15 | };
16 | handleFormSubmit = value => {
17 | this.setState({
18 | value,
19 | });
20 | };
21 | checkAll = () => {
22 | this.setState({
23 | value: ['cat1', 'cat2', 'cat3', 'cat4', 'cat5', 'cat6'],
24 | });
25 | };
26 | render() {
27 | return (
28 |
29 |
全部
30 |
35 |
36 | 类目一
37 | 类目二
38 | 类目三
39 | 类目四
40 | 类目五
41 | 类目六
42 |
43 |
44 |
45 | );
46 | }
47 | }
48 |
49 | ReactDOM.render( , mountNode);
50 | ```
51 |
--------------------------------------------------------------------------------
/src/components/TagSelect/demo/expandable.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title: 可展开和收起
4 | ---
5 |
6 | 使用 `expandable` 属性,让标签组可以收起,避免过高。
7 |
8 | ````jsx
9 | import TagSelect from 'ant-design-pro/lib/TagSelect';
10 |
11 | function handleFormSubmit(checkedValue) {
12 | console.log(checkedValue);
13 | }
14 |
15 | ReactDOM.render(
16 |
17 | 类目一
18 | 类目二
19 | 类目三
20 | 类目四
21 | 类目五
22 | 类目六
23 | 类目七
24 | 类目八
25 | 类目九
26 | 类目十
27 | 类目十一
28 | 类目十二
29 |
30 | , mountNode);
31 | ````
32 |
--------------------------------------------------------------------------------
/src/components/TagSelect/demo/simple.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title: 基础样例
4 | ---
5 |
6 | 结合 `Tag` 的 `TagSelect` 组件,方便的应用于筛选类目的业务场景中。
7 |
8 | ````jsx
9 | import TagSelect from 'ant-design-pro/lib/TagSelect';
10 |
11 | function handleFormSubmit(checkedValue) {
12 | console.log(checkedValue);
13 | }
14 |
15 | ReactDOM.render(
16 |
17 | 类目一
18 | 类目二
19 | 类目三
20 | 类目四
21 | 类目五
22 | 类目六
23 |
24 | , mountNode);
25 | ````
26 |
--------------------------------------------------------------------------------
/src/components/TagSelect/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import TagSelectOption from './TagSelectOption';
3 |
4 | export interface ITagSelectProps {
5 | onChange?: (value: string[]) => void;
6 | expandable?: boolean;
7 | value?: string[] | number[];
8 | style?: React.CSSProperties;
9 | hideCheckAll?: boolean;
10 | actionsText?: { expandText?: string; collapseText?: string; selectAllText?: string };
11 | }
12 |
13 | export default class TagSelect extends React.Component {
14 | public static Option: typeof TagSelectOption;
15 | private children:
16 | | React.ReactElement
17 | | Array>;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/TagSelect/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .tagSelect {
4 | position: relative;
5 | max-height: 32px;
6 | margin-left: -8px;
7 | overflow: hidden;
8 | line-height: 32px;
9 | transition: all 0.3s;
10 | user-select: none;
11 | :global {
12 | .ant-tag {
13 | margin-right: 24px;
14 | padding: 0 8px;
15 | font-size: @font-size-base;
16 | }
17 | }
18 | &.expanded {
19 | max-height: 200px;
20 | transition: all 0.3s;
21 | }
22 | .trigger {
23 | position: absolute;
24 | top: 0;
25 | right: 0;
26 | i {
27 | font-size: 12px;
28 | }
29 | }
30 | &.hasExpandTag {
31 | padding-right: 50px;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/TagSelect/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: TagSelect
4 | zh-CN: TagSelect
5 | subtitle: 标签选择器
6 | cols: 1
7 | order: 13
8 | ---
9 |
10 | 可进行多选,带折叠收起和展开更多功能,常用于对列表进行筛选。
11 |
12 | ## API
13 |
14 | ### TagSelect
15 |
16 | | 参数 | 说明 | 类型 | 默认值 |
17 | |----------|------------------------------------------|-------------|-------|
18 | | value |选中的项 |string[] \| number[] | |
19 | | defaultValue |默认选中的项 |string[] \| number[] | |
20 | | onChange | 标签选择的回调函数 | Function(checkedTags) | |
21 | | expandable | 是否展示 `展开/收起` 按钮 | Boolean | false |
22 | | hideCheckAll | 隐藏 `全部` 按钮 | Boolean | false |
23 |
24 | ### TagSelectOption
25 |
26 | | 参数 | 说明 | 类型 | 默认值 |
27 | |----------|------------------------------------------|-------------|-------|
28 | | value | TagSelect的值 | string\| number | - |
29 | | children | tag的内容 | string \| ReactNode | - |
30 |
--------------------------------------------------------------------------------
/src/components/TopNavHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Link from 'umi/link';
3 | import RightContent from '../GlobalHeader/RightContent';
4 | import BaseMenu from '../SiderMenu/BaseMenu';
5 | import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils';
6 | import styles from './index.less';
7 | import pconf from 'projectConfig';
8 | import { formatMessage } from 'umi/locale';
9 |
10 | export default class TopNavHeader extends PureComponent {
11 | state = {
12 | maxWidth: undefined,
13 | };
14 |
15 | static getDerivedStateFromProps(props) {
16 | return {
17 | maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40,
18 | };
19 | }
20 |
21 | render() {
22 | const { theme, contentWidth, menuData, logo } = this.props;
23 | const { maxWidth } = this.state;
24 | const flatMenuKeys = getFlatMenuKeys(menuData);
25 | return (
26 |
27 |
{
29 | this.maim = ref;
30 | }}
31 | className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`}
32 | >
33 |
34 |
35 |
36 |
37 |
{formatMessage({id: pconf.title})}
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/TopNavHeader/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .head {
4 | position: relative;
5 | width: 100%;
6 | height: @layout-header-height;
7 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
8 | transition: background 0.3s, width 0.2s;
9 | :global {
10 | .ant-menu-submenu.ant-menu-submenu-horizontal {
11 | height: 100%;
12 | line-height: @layout-header-height;
13 | .ant-menu-submenu-title {
14 | height: 100%;
15 | }
16 | }
17 | }
18 | &.light {
19 | background-color: #fff;
20 | }
21 | .main {
22 | display: flex;
23 | height: @layout-header-height;
24 | padding-left: 24px;
25 | &.wide {
26 | max-width: 1200px;
27 | margin: auto;
28 | padding-left: 0;
29 | }
30 | .left {
31 | display: flex;
32 | flex: 1;
33 | }
34 | .right {
35 | width: 324px;
36 | }
37 | }
38 | }
39 |
40 | .logo {
41 | position: relative;
42 | width: 165px;
43 | height: @layout-header-height;
44 | overflow: hidden;
45 | line-height: @layout-header-height;
46 | transition: all 0.3s;
47 | img {
48 | display: inline-block;
49 | height: 32px;
50 | vertical-align: middle;
51 | }
52 | h1 {
53 | display: inline-block;
54 | margin: 0 0 0 12px;
55 | color: #fff;
56 | font-weight: 400;
57 | font-size: 16px;
58 | vertical-align: top;
59 | }
60 | }
61 |
62 | .light {
63 | h1 {
64 | color: #002140;
65 | }
66 | }
67 |
68 | .menu {
69 | height: @layout-header-height;
70 | line-height: @layout-header-height;
71 | border: none;
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/_utils/pathTools.js:
--------------------------------------------------------------------------------
1 | // /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
2 | // eslint-disable-next-line import/prefer-default-export
3 | export function urlToList(url) {
4 | const urllist = url.split('/').filter(i => i);
5 | return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`);
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/_utils/pathTools.test.js:
--------------------------------------------------------------------------------
1 | import { urlToList } from './pathTools';
2 |
3 | describe('test urlToList', () => {
4 | it('A path', () => {
5 | expect(urlToList('/userinfo')).toEqual(['/userinfo']);
6 | });
7 | it('Secondary path', () => {
8 | expect(urlToList('/userinfo/2144')).toEqual(['/userinfo', '/userinfo/2144']);
9 | });
10 | it('Three paths', () => {
11 | expect(urlToList('/userinfo/2144/addr')).toEqual([
12 | '/userinfo',
13 | '/userinfo/2144',
14 | '/userinfo/2144/addr',
15 | ]);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/form-item/TMCombobox/style.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .keyword-highlight {
4 | color: @primary-color;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/form-item/TMCombobox2/style.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .keyword-highlight {
4 | color: @primary-color;
5 | }
6 |
7 | .pagination {
8 | text-align: right;
9 | height: 40px;
10 | padding-top: 8px;
11 | border-top: 1px solid #f5f5f5;
12 | }
13 |
--------------------------------------------------------------------------------
/src/defaultSettings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | navTheme: 'dark', // theme for nav menu
4 | primaryColor: '#1890FF', // primary color of ant design
5 | layout: 'sidemenu', // nav menu position: sidemenu or topmenu
6 | contentWidth: 'Fluid', // layout of content: Fluid or Fixed, only works when layout is topmenu
7 | fixedHeader: false, // sticky header
8 | autoHideHeader: false, // auto hide header
9 | fixSiderbar: false, // sticky siderbar
10 |
11 | menu: {
12 | disableLocal: false,
13 | },
14 |
15 | pwa: true,
16 |
17 | title: 'app.title', // 浏览器 TAB 标签的 title,登录框title,导航栏 title。 NOTE: title 过长可能会导致样式扭曲,文字消失
18 | logoImg: 'MY-LOGO.svg', // LOGO 图片,相对于 src/assets 的路径, 注意该值修改后需要重启 npm start
19 |
20 | request: {
21 | // default fetch timeout
22 | timeout: 5000,
23 | debounceDelay: 50,
24 | },
25 |
26 | footer: {
27 | copyright: 'Copyright123 © 2019 watertao.github.com',
28 | },
29 |
30 | };
31 |
32 |
--------------------------------------------------------------------------------
/src/e2e/baseLayout.e2e.js:
--------------------------------------------------------------------------------
1 | import RouterConfig from '../../config/router.config';
2 |
3 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
4 |
5 | function formatter(data) {
6 | return data
7 | .reduce((pre, item) => {
8 | pre.push(item.path);
9 | return pre;
10 | }, [])
11 | .filter(item => item);
12 | }
13 |
14 | describe('Homepage', async () => {
15 | const testPage = path => async () => {
16 | await page.goto(`${BASE_URL}${path}`);
17 | await page.waitForSelector('footer', {
18 | timeout: 2000,
19 | });
20 | const haveFooter = await page.evaluate(
21 | () => document.getElementsByTagName('footer').length > 0
22 | );
23 | expect(haveFooter).toBeTruthy();
24 | };
25 |
26 | beforeAll(async () => {
27 | jest.setTimeout(1000000);
28 | await page.setCacheEnabled(false);
29 | });
30 | const routers = formatter(RouterConfig[1].routes);
31 | routers.forEach(route => {
32 | it(`test pages ${route}`, testPage(route));
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/e2e/home.e2e.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
2 |
3 | describe('Homepage', () => {
4 | beforeAll(async () => {
5 | jest.setTimeout(1000000);
6 | });
7 | it('it should have logo text', async () => {
8 | await page.goto(BASE_URL);
9 | await page.waitForSelector('h1', {
10 | timeout: 5000,
11 | });
12 | const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText);
13 | expect(text).toContain('Ant Design Pro');
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/e2e/login.e2e.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
2 |
3 | describe('Login', () => {
4 | beforeAll(async () => {
5 | jest.setTimeout(1000000);
6 | });
7 |
8 | beforeEach(async () => {
9 | await page.goto(`${BASE_URL}/user/login`, { waitUntil: 'networkidle2' });
10 | await page.evaluate(() => window.localStorage.setItem('antd-pro-authority', 'guest'));
11 | });
12 |
13 | it('should login with failure', async () => {
14 | await page.waitForSelector('#userName', {
15 | timeout: 2000,
16 | });
17 | await page.type('#userName', 'mockuser');
18 | await page.type('#password', 'wrong_password');
19 | await page.click('button[type="submit"]');
20 | await page.waitForSelector('.ant-alert-error'); // should display error
21 | });
22 |
23 | it('should login successfully', async () => {
24 | await page.waitForSelector('#userName', {
25 | timeout: 2000,
26 | });
27 | await page.type('#userName', 'admin');
28 | await page.type('#password', 'ant.design');
29 | await page.click('button[type="submit"]');
30 | await page.waitForSelector('.ant-layout-sider h1'); // should display error
31 | const text = await page.evaluate(() => document.body.innerHTML);
32 | expect(text).toContain('Ant Design Pro ');
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/e2e/topMenu.e2e.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
2 |
3 | describe('Homepage', () => {
4 | beforeAll(async () => {
5 | jest.setTimeout(1000000);
6 | });
7 | it('topmenu should have footer', async () => {
8 | const params = '/form/basic-form?navTheme=light&layout=topmenu';
9 | await page.goto(`${BASE_URL}${params}`);
10 | await page.waitForSelector('footer', {
11 | timeout: 2000,
12 | });
13 | const haveFooter = await page.evaluate(
14 | () => document.getElementsByTagName('footer').length > 0
15 | );
16 | expect(haveFooter).toBeTruthy();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/e2e/userLayout.e2e.js:
--------------------------------------------------------------------------------
1 | import RouterConfig from '../../config/router.config';
2 |
3 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
4 |
5 | function formatter(data) {
6 | return data
7 | .reduce((pre, item) => {
8 | pre.push(item.path);
9 | return pre;
10 | }, [])
11 | .filter(item => item);
12 | }
13 |
14 | describe('Homepage', () => {
15 | const testPage = path => async () => {
16 | await page.goto(`${BASE_URL}${path}`);
17 | await page.waitForSelector('footer', {
18 | timeout: 2000,
19 | });
20 | const haveFooter = await page.evaluate(
21 | () => document.getElementsByTagName('footer').length > 0
22 | );
23 | expect(haveFooter).toBeTruthy();
24 | };
25 |
26 | beforeAll(async () => {
27 | jest.setTimeout(1000000);
28 | });
29 | formatter(RouterConfig[0].routes).forEach(route => {
30 | it(`test pages ${route}`, testPage(route));
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/environment.dev.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | // endpoint: 'http://localhost:8000/api',
4 |
5 | endpoint: 'http://localhost:8080',
6 |
7 | };
8 |
--------------------------------------------------------------------------------
/src/environment.prod.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | endpoint: 'https://www.watertao.top/api'
4 |
5 | };
6 |
--------------------------------------------------------------------------------
/src/environment.qa.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | endpoint: 'http://172.190.14.156:8080'
4 |
5 | };
6 |
--------------------------------------------------------------------------------
/src/global.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { notification, Button, message } from 'antd';
3 | import { formatMessage } from 'umi/locale';
4 | import defaultSettings from './defaultSettings';
5 |
6 | const { pwa } = defaultSettings;
7 | // if pwa is true
8 | if (pwa) {
9 | // Notify user if offline now
10 | window.addEventListener('sw.offline', () => {
11 | message.warning(formatMessage({ id: 'app.pwa.offline' }));
12 | });
13 |
14 | // Pop up a prompt on the page asking the user if they want to use the latest version
15 | window.addEventListener('sw.updated', e => {
16 | const reloadSW = async () => {
17 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration
18 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
19 | const worker = e.detail && e.detail.waiting;
20 | if (!worker) {
21 | return Promise.resolve();
22 | }
23 | // Send skip-waiting event to waiting SW with MessageChannel
24 | await new Promise((resolve, reject) => {
25 | const channel = new MessageChannel();
26 | channel.port1.onmessage = event => {
27 | if (event.data.error) {
28 | reject(event.data.error);
29 | } else {
30 | resolve(event.data);
31 | }
32 | };
33 | worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
34 | });
35 | // Refresh current page to use the updated HTML and other assets after SW has skiped waiting
36 | window.location.reload(true);
37 | return true;
38 | };
39 | const key = `open${Date.now()}`;
40 | const btn = (
41 | {
44 | notification.close(key);
45 | reloadSW();
46 | }}
47 | >
48 | {formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
49 |
50 | );
51 | notification.open({
52 | message: formatMessage({ id: 'app.pwa.serviceworker.updated' }),
53 | description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
54 | btn,
55 | key,
56 | onClose: async () => {},
57 | });
58 | });
59 | }
60 |
--------------------------------------------------------------------------------
/src/global.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | html,
4 | body,
5 | #root {
6 | height: 100%;
7 | }
8 |
9 | .colorWeak {
10 | filter: invert(80%);
11 | }
12 |
13 | .ant-layout {
14 | min-height: 100vh;
15 | }
16 |
17 | canvas {
18 | display: block;
19 | }
20 |
21 | body {
22 | text-rendering: optimizeLegibility;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | .globalSpin {
28 | width: 100%;
29 | margin: 40px 0 !important;
30 | }
31 |
32 | ul,
33 | ol {
34 | list-style: none;
35 | }
36 |
37 | @media (max-width: @screen-xs) {
38 | .ant-table {
39 | width: 100%;
40 | overflow-x: auto;
41 | &-thead > tr,
42 | &-tbody > tr {
43 | > th,
44 | > td {
45 | white-space: pre;
46 | > span {
47 | display: block;
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/layouts/BasicLayout.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .content {
4 | margin: 24px;
5 | padding-top: @layout-header-height;
6 | }
7 |
--------------------------------------------------------------------------------
/src/layouts/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Layout, Icon } from 'antd';
3 | import GlobalFooter from '@/components/GlobalFooter';
4 | import pconf from 'projectConfig';
5 |
6 | const { Footer } = Layout;
7 | const FooterView = () => (
8 |
14 | );
15 | export default FooterView;
16 |
--------------------------------------------------------------------------------
/src/layouts/Header.less:
--------------------------------------------------------------------------------
1 | .fixedHeader {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | z-index: 9;
6 | width: 100%;
7 | transition: width 0.2s;
8 | }
9 |
--------------------------------------------------------------------------------
/src/layouts/MenuContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext();
4 |
--------------------------------------------------------------------------------
/src/locales/en-US.js:
--------------------------------------------------------------------------------
1 | import exception from './en-US/exception';
2 | import globalHeader from './en-US/globalHeader';
3 | import menu from './en-US/menu';
4 | import settingDrawer from './en-US/settingDrawer';
5 | import settings from './en-US/settings';
6 | import pwa from './en-US/pwa';
7 | import component from './en-US/component';
8 | import MODULE_LOCALE from '@tmp/moduleLocale_en-US';
9 |
10 | export default {
11 | 'app.title': 'Teemo Console',
12 |
13 | 'login.title': 'Login',
14 | 'login.literal.loginTo': 'Login to ',
15 | 'login.userName': 'User Name',
16 | 'login.password': 'Password',
17 | 'login.forgotPassword': 'Forgot Password?',
18 | 'login.forgotPassword.tip': 'Please contact to administrator',
19 | 'login.loginButton': 'Login',
20 | 'login.form.userName.empty': 'User name could not be empty',
21 | 'login.form.password.empty': 'Password could not be empty',
22 |
23 | 'label.action': 'Action',
24 | 'label.delete': 'Delete',
25 | 'label.viewOrModify': 'View/Modify',
26 |
27 |
28 | 'navBar.lang': 'Languages',
29 | 'layout.user.link.help': 'Help',
30 | 'layout.user.link.privacy': 'Privacy',
31 | 'layout.user.link.terms': 'Terms',
32 | 'app.home.introduce': 'introduce',
33 | 'app.forms.basic.title': 'Basic form',
34 | 'app.forms.basic.description':
35 | 'Form pages are used to collect or verify information to users, and basic forms are common in scenarios where there are fewer data items.',
36 | ...exception,
37 | ...globalHeader,
38 | ...menu,
39 | ...settingDrawer,
40 | ...settings,
41 | ...pwa,
42 | ...component,
43 | ...MODULE_LOCALE,
44 | };
45 |
--------------------------------------------------------------------------------
/src/locales/en-US/component.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'component.tagSelect.expand': 'Expand',
3 | 'component.tagSelect.collapse': 'Collapse',
4 | 'component.tagSelect.all': 'All',
5 |
6 | 'component.TMDataList.total': '{rangeStart}-{rangeEnd} of {total} items',
7 | 'component.TMDataList.btn.refresh': 'Refresh',
8 | 'component.TMDataList.btn.reset': 'Reset Query',
9 |
10 | 'component.TMFormDrawer.button.submit': 'Submit',
11 | 'component.TMFormDrawer.button.close': 'Close',
12 | 'component.TMFormDrawer.tip.submitSuccess': 'Submit Successfully',
13 |
14 | };
15 |
--------------------------------------------------------------------------------
/src/locales/en-US/exception.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.exception.back': 'Back to home',
3 | 'app.exception.description.403': "Sorry, you don't have access to this page",
4 | 'app.exception.description.404': 'Sorry, the page you visited does not exist',
5 | 'app.exception.description.500': 'Sorry, the server is reporting an error',
6 | };
7 |
--------------------------------------------------------------------------------
/src/locales/en-US/globalHeader.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'component.globalHeader.search': 'Search',
3 | 'component.globalHeader.search.example1': 'Search example 1',
4 | 'component.globalHeader.search.example2': 'Search example 2',
5 | 'component.globalHeader.search.example3': 'Search example 3',
6 | 'component.globalHeader.help': 'Help',
7 | 'component.globalHeader.notification': 'Notification',
8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.',
9 | 'component.globalHeader.message': 'Message',
10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.',
11 | 'component.globalHeader.event': 'Event',
12 | 'component.globalHeader.event.empty': 'You have viewed all events.',
13 | 'component.noticeIcon.clear': 'Clear',
14 | 'component.noticeIcon.cleared': 'Cleared',
15 | 'component.noticeIcon.empty': 'No notifications',
16 | 'component.noticeIcon.loaded': 'Loaded',
17 | 'component.noticeIcon.loading-more': 'Loading more',
18 | };
19 |
--------------------------------------------------------------------------------
/src/locales/en-US/menu.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.home': 'Home',
3 |
4 | 'menu.account.settings': 'User Settings',
5 | 'menu.account.logout': 'Logout',
6 | 'menu.account.debug': 'Debug Console',
7 |
8 |
9 | 'menu.dashboard': 'Dashboard',
10 | 'menu.dashboard.analysis': 'Analysis',
11 | 'menu.dashboard.monitor': 'Monitor',
12 | 'menu.dashboard.workplace': 'Workplace',
13 |
14 | 'menu.authority': 'Authority Management',
15 |
16 | };
17 |
--------------------------------------------------------------------------------
/src/locales/en-US/pwa.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.pwa.offline': 'You are offline now',
3 | 'app.pwa.serviceworker.updated': 'New content is available',
4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
5 | 'app.pwa.serviceworker.updated.ok': 'Refresh',
6 | };
7 |
--------------------------------------------------------------------------------
/src/locales/en-US/settingDrawer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': 'Page style setting',
3 | 'app.setting.pagestyle.dark': 'Dark style',
4 | 'app.setting.pagestyle.light': 'Light style',
5 | 'app.setting.content-width': 'Content Width',
6 | 'app.setting.content-width.fixed': 'Fixed',
7 | 'app.setting.content-width.fluid': 'Fluid',
8 | 'app.setting.themecolor': 'Theme Color',
9 | 'app.setting.themecolor.dust': 'Dust Red',
10 | 'app.setting.themecolor.volcano': 'Volcano',
11 | 'app.setting.themecolor.sunset': 'Sunset Orange',
12 | 'app.setting.themecolor.cyan': 'Cyan',
13 | 'app.setting.themecolor.green': 'Polar Green',
14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
15 | 'app.setting.themecolor.geekblue': 'Geek Glue',
16 | 'app.setting.themecolor.purple': 'Golden Purple',
17 | 'app.setting.navigationmode': 'Navigation Mode',
18 | 'app.setting.sidemenu': 'Side Menu Layout',
19 | 'app.setting.topmenu': 'Top Menu Layout',
20 | 'app.setting.fixedheader': 'Fixed Header',
21 | 'app.setting.fixedsidebar': 'Fixed Sidebar',
22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
23 | 'app.setting.hideheader': 'Hidden Header when scrolling',
24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
25 | 'app.setting.othersettings': 'Other Settings',
26 | 'app.setting.weakmode': 'Weak Mode',
27 | 'app.setting.copy': 'Copy Setting',
28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
29 | 'app.setting.production.hint':
30 | 'Setting panel shows in development environment only, please manually modify',
31 | };
32 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.js:
--------------------------------------------------------------------------------
1 | import exception from './zh-CN/exception';
2 | import globalHeader from './zh-CN/globalHeader';
3 | import menu from './zh-CN/menu';
4 | import settingDrawer from './zh-CN/settingDrawer';
5 | import settings from './zh-CN/settings';
6 | import pwa from './zh-CN/pwa';
7 | import component from './zh-CN/component';
8 | import MODULE_LOCALE from '@tmp/moduleLocale_zh-CN';
9 |
10 | export default {
11 | 'app.title': 'Teemo 管理系统',
12 |
13 | 'login.title': '登录',
14 | 'login.literal.loginTo': '登录到',
15 | 'login.userName': '用户名',
16 | 'login.password': '密码',
17 | 'login.forgotPassword': '忘记密码?',
18 | 'login.forgotPassword.tip': '请联系管理员',
19 | 'login.loginButton': '登 录',
20 | 'login.form.userName.empty': '用户名不可为空',
21 | 'login.form.password.empty': '密码不可为空',
22 |
23 | 'label.action': '操作',
24 | 'label.delete': '删除',
25 | 'label.viewOrModify': '查看/编辑',
26 |
27 | 'navBar.lang': '语言',
28 | 'layout.user.link.help': '帮助',
29 | 'layout.user.link.privacy': '隐私',
30 | 'layout.user.link.terms': '条款',
31 | 'app.home.introduce': '介绍',
32 | 'app.forms.basic.title': '基础表单',
33 | 'app.forms.basic.description':
34 | '表单页用于向用户收集或验证信息,基础表单常见于数据项较少的表单场景。',
35 | ...exception,
36 | ...globalHeader,
37 | ...menu,
38 | ...settingDrawer,
39 | ...settings,
40 | ...pwa,
41 | ...component,
42 | ...MODULE_LOCALE,
43 | };
44 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/component.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'component.tagSelect.expand': '展开',
3 | 'component.tagSelect.collapse': '收起',
4 | 'component.tagSelect.all': '全部',
5 |
6 | 'component.TMDataList.total': '{rangeStart}-{rangeEnd} , 共 {total} 条记录',
7 | 'component.TMDataList.btn.refresh': '刷新',
8 | 'component.TMDataList.btn.reset': '重置查询',
9 |
10 | 'component.TMFormDrawer.button.submit': '提交',
11 | 'component.TMFormDrawer.button.close': '关闭',
12 | 'component.TMFormDrawer.tip.submitSuccess': '提交成功',
13 | };
14 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/exception.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.exception.back': '返回首页',
3 | 'app.exception.description.403': '抱歉,你无权访问该页面',
4 | 'app.exception.description.404': '抱歉,你访问的页面不存在',
5 | 'app.exception.description.500': '抱歉,服务器出错了',
6 | };
7 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/globalHeader.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'component.globalHeader.search': '站内搜索',
3 | 'component.globalHeader.search.example1': '搜索提示一',
4 | 'component.globalHeader.search.example2': '搜索提示二',
5 | 'component.globalHeader.search.example3': '搜索提示三',
6 | 'component.globalHeader.help': '使用文档',
7 | 'component.globalHeader.notification': '通知',
8 | 'component.globalHeader.notification.empty': '你已查看所有通知',
9 | 'component.globalHeader.message': '消息',
10 | 'component.globalHeader.message.empty': '您已读完所有消息',
11 | 'component.globalHeader.event': '待办',
12 | 'component.globalHeader.event.empty': '你已完成所有待办',
13 | 'component.noticeIcon.clear': '清空',
14 | 'component.noticeIcon.cleared': '清空了',
15 | 'component.noticeIcon.empty': '暂无数据',
16 | 'component.noticeIcon.loaded': '加载完毕',
17 | 'component.noticeIcon.loading-more': '加载更多',
18 | };
19 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/menu.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.home': '首页',
3 |
4 | 'menu.account.settings': '个人设置',
5 | 'menu.account.logout': '注销登出',
6 | 'menu.account.debug': '调试控制台',
7 |
8 | 'menu.test': '测试',
9 | 'menu.demo': '范例代码',
10 | 'menu.demo.data-display': '展示类范例代码我靠这么长',
11 |
12 | 'menu.authority': '权限管理',
13 |
14 | };
15 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/pwa.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.pwa.offline': '当前处于离线状态',
3 | 'app.pwa.serviceworker.updated': '有新内容',
4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
5 | 'app.pwa.serviceworker.updated.ok': '刷新',
6 | };
7 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/settingDrawer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': '整体风格设置',
3 | 'app.setting.pagestyle.dark': '暗色菜单风格',
4 | 'app.setting.pagestyle.light': '亮色菜单风格',
5 | 'app.setting.content-width': '内容区域宽度',
6 | 'app.setting.content-width.fixed': '定宽',
7 | 'app.setting.content-width.fluid': '流式',
8 | 'app.setting.themecolor': '主题色',
9 | 'app.setting.themecolor.dust': '薄暮',
10 | 'app.setting.themecolor.volcano': '火山',
11 | 'app.setting.themecolor.sunset': '日暮',
12 | 'app.setting.themecolor.cyan': '明青',
13 | 'app.setting.themecolor.green': '极光绿',
14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
15 | 'app.setting.themecolor.geekblue': '极客蓝',
16 | 'app.setting.themecolor.purple': '酱紫',
17 | 'app.setting.navigationmode': '导航模式',
18 | 'app.setting.sidemenu': '侧边菜单布局',
19 | 'app.setting.topmenu': '顶部菜单布局',
20 | 'app.setting.fixedheader': '固定 Header',
21 | 'app.setting.fixedsidebar': '固定侧边菜单',
22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
23 | 'app.setting.hideheader': '下滑时隐藏 Header',
24 | 'app.setting.hideheader.hint': '固定 Header 时可配置',
25 | 'app.setting.othersettings': '其他设置',
26 | 'app.setting.weakmode': '色弱模式',
27 | 'app.setting.copy': '拷贝设置',
28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
29 | 'app.setting.production.hint':
30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
31 | };
32 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/settings.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.settings.menuMap.basic': '基本设置',
3 | 'app.settings.menuMap.security': '安全设置',
4 | 'app.settings.menuMap.binding': '账号绑定',
5 | 'app.settings.menuMap.notification': '新消息通知',
6 | 'app.settings.basic.avatar': '头像',
7 | 'app.settings.basic.change-avatar': '更换头像',
8 | 'app.settings.basic.email': '邮箱',
9 | 'app.settings.basic.email-message': '请输入您的邮箱!',
10 | 'app.settings.basic.nickname': '昵称',
11 | 'app.settings.basic.nickname-message': '请输入您的昵称!',
12 | 'app.settings.basic.profile': '个人简介',
13 | 'app.settings.basic.profile-message': '请输入个人简介!',
14 | 'app.settings.basic.profile-placeholder': '个人简介',
15 | 'app.settings.basic.country': '国家/地区',
16 | 'app.settings.basic.country-message': '请输入您的国家或地区!',
17 | 'app.settings.basic.geographic': '所在省市',
18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!',
19 | 'app.settings.basic.address': '街道地址',
20 | 'app.settings.basic.address-message': '请输入您的街道地址!',
21 | 'app.settings.basic.phone': '联系电话',
22 | 'app.settings.basic.phone-message': '请输入您的联系电话!',
23 | 'app.settings.basic.update': '更新基本信息',
24 | 'app.settings.security.strong': '强',
25 | 'app.settings.security.medium': '中',
26 | 'app.settings.security.weak': '弱',
27 | 'app.settings.security.password': '账户密码',
28 | 'app.settings.security.password-description': '当前密码强度',
29 | 'app.settings.security.phone': '密保手机',
30 | 'app.settings.security.phone-description': '已绑定手机',
31 | 'app.settings.security.question': '密保问题',
32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
33 | 'app.settings.security.email': '备用邮箱',
34 | 'app.settings.security.email-description': '已绑定邮箱',
35 | 'app.settings.security.mfa': 'MFA 设备',
36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
37 | 'app.settings.security.modify': '修改',
38 | 'app.settings.security.set': '设置',
39 | 'app.settings.security.bind': '绑定',
40 | 'app.settings.binding.taobao': '绑定淘宝',
41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
42 | 'app.settings.binding.alipay': '绑定支付宝',
43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
44 | 'app.settings.binding.dingding': '绑定钉钉',
45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
46 | 'app.settings.binding.bind': '绑定',
47 | 'app.settings.notification.password': '账户密码',
48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
49 | 'app.settings.notification.messages': '系统消息',
50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
51 | 'app.settings.notification.todo': '待办任务',
52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知',
53 | 'app.settings.open': '开',
54 | 'app.settings.close': '关',
55 | };
56 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ant Design Pro",
3 | "short_name": "Ant Design Pro",
4 | "display": "standalone",
5 | "start_url": "./?utm_source=homescreen",
6 | "theme_color": "#002140",
7 | "background_color": "#001529",
8 | "icons": [
9 | {
10 | "src": "icons/icon-192x192.png",
11 | "sizes": "192x192"
12 | },
13 | {
14 | "src": "icons/icon-128x128.png",
15 | "sizes": "128x128"
16 | },
17 | {
18 | "src": "icons/icon-512x512.png",
19 | "sizes": "512x512"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/menu.config.js:
--------------------------------------------------------------------------------
1 | // restart server after change
2 |
3 | module.exports = [
4 | // 测试
5 | {
6 | code: 'test', type: 'group', icon: 'profile',
7 | children: [
8 | // 测试A
9 | { code: 'test-a', type: 'module' },
10 | // 测试B
11 | { code: 'test-b', type: 'module', },
12 | // 测试C
13 | { code: 'test-c', type: 'module', },
14 |
15 |
16 | ]
17 | },
18 |
19 |
20 | // authority management
21 | {
22 | code: 'authority', type: 'group', icon: 'safety',
23 | children: [
24 | // resource management
25 | { code: 'authority_resource-mgnt', type: 'module' },
26 | // role management
27 | { code: 'authority_role-mgnt', type: 'module' },
28 | // user management
29 | { code: 'authority_user-mgnt', type: 'module' },
30 | // audit log
31 | { code: 'authority_auditlog', type: 'module' },
32 | ]
33 | },
34 |
35 |
36 | ];
37 |
--------------------------------------------------------------------------------
/src/models/global.model.js:
--------------------------------------------------------------------------------
1 | import { fetchFullResourceMetadata, queryCurrent, login,logout } from '@/services/global.service';
2 | import { storeToken } from '@/utils/rest-token-resolver';
3 | import router from 'umi/router';
4 |
5 | export default {
6 | namespace: 'global',
7 |
8 | state: {
9 | collapsed: false,
10 | notices: [],
11 | loadedAllNotices: false,
12 | currentUser: null,
13 | resourceMetadata: [],
14 | isSessionSynced: false,
15 | },
16 |
17 | effects: {
18 |
19 | /**
20 | * 同步会话,获取当前登录用户的会话信息,保存到 currentUser
21 | */
22 | *fetchCurrent(_, { call, put }) {
23 | try {
24 | const { data } = yield call(queryCurrent);
25 | yield put({
26 | type: 'saveCurrentUser',
27 | payload: data
28 | });
29 | return true;
30 | } catch (ex) {
31 | // 若获取会话失败,则跳转到 login
32 | if (ex.extra.status == 401) {
33 | router.push('/login');
34 | }
35 | return false;
36 | }
37 | },
38 |
39 | *fetchResourceMetadata(_, { call, put }) {
40 | try {
41 | const { data } = yield call(fetchFullResourceMetadata);
42 | yield put({
43 | type: 'saveResourceMetadata',
44 | payload: data
45 | });
46 | } catch (ex) {
47 | console.error(ex);
48 | }
49 | },
50 |
51 | *login(action, { call, put, select }){
52 | try {
53 | const { data } = yield call(login, action.payload);
54 | yield put({
55 | type: 'saveCurrentUser',
56 | payload: data
57 | });
58 | storeToken(data.token);
59 | router.push("/");
60 | } catch (ex) {
61 | console.error(ex);
62 | }
63 | },
64 |
65 | /**
66 | * 登出,销毁服务端会话,并刷新当前页,由于当前页无法再同步到已经销毁的会话,便会转向到 /login
67 | */
68 | *logout(_, { call, put}) {
69 | try {
70 | yield call(logout);
71 | yield put({
72 | type: 'user/saveCurrentUser',
73 | payload: {}
74 | });
75 | window.location.reload(true);
76 | } catch (ex) {
77 | console.error(ex);
78 | }
79 |
80 | },
81 |
82 | },
83 |
84 | reducers: {
85 | changeLayoutCollapsed(state, { payload }) {
86 | return {
87 | ...state,
88 | collapsed: payload,
89 | };
90 | },
91 | saveCurrentUser(state, action) {
92 | return {
93 | ...state,
94 | currentUser: action.payload || {},
95 | };
96 | },
97 | saveResourceMetadata(state, action) {
98 | return {
99 | ...state,
100 | resourceMetadata: action.payload || [],
101 | isSessionSynced: true,
102 | };
103 | },
104 |
105 | },
106 |
107 | subscriptions: {
108 | setup({ history }) {
109 | // Subscribe history(url) change, trigger `load` action if pathname is `/`
110 | return history.listen(({ pathname, search }) => {
111 | if (typeof window.ga !== 'undefined') {
112 | window.ga('send', 'pageview', pathname + search);
113 | }
114 | });
115 | },
116 | },
117 | };
118 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/components/QueryConditionForm/style.less:
--------------------------------------------------------------------------------
1 | .login_name {
2 | color: rgba(0,0,0,0.35);
3 | font-size: 12px;
4 | margin-left: 8px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/models/auditlog.model.js:
--------------------------------------------------------------------------------
1 | import { findAuditlogs, findOperators, findResources } from "../services/auditlog.service";
2 | import { formatDateFromMoment } from '@/utils/date-util';
3 |
4 |
5 | export default {
6 |
7 | namespace: 'auditlog',
8 |
9 |
10 | state: {
11 | list: [],
12 | },
13 |
14 |
15 | effects: {
16 | *fetchList({ payload: { filterParameters } }, { call, put, select }) {
17 |
18 | const nFilterParameters = {
19 | ...filterParameters,
20 | start_time: filterParameters.date_range.length > 0 ? formatDateFromMoment(filterParameters.date_range[0], true) : null,
21 | end_time: filterParameters.date_range.length > 1 ? formatDateFromMoment(filterParameters.date_range[1], true) : null,
22 | };
23 | delete nFilterParameters.date_range;
24 |
25 | try {
26 | const response = yield call(findAuditlogs, nFilterParameters);
27 |
28 | yield put({
29 | type: 'updateList',
30 | payload: response.data
31 | });
32 | return parseInt(response.headers['x-total-count']);
33 | } catch (e) {
34 | console.error(e);
35 | return 0;
36 | }
37 | },
38 |
39 |
40 | *findOperators({ payload: { filterParameters } }, { call, put, select }) {
41 | try {
42 | const response = yield call(findOperators, filterParameters);
43 | return {
44 | total: parseInt(response.headers['x-total-count']),
45 | data: response.data,
46 | };
47 | } catch (e) {
48 | console.error(e);
49 | }
50 | },
51 |
52 | *findOperations({ payload: { filterParameters } }, { call, put, select }) {
53 | try {
54 | const response = yield call(findResources, filterParameters);
55 | return {
56 | total: parseInt(response.headers['x-total-count']),
57 | data: response.data,
58 | };
59 | } catch (e) {
60 | console.error(e);
61 | }
62 | },
63 |
64 | },
65 |
66 |
67 | reducers: {
68 | updateList(state, action) {
69 | return {
70 | ...state,
71 | list: action.payload
72 | }
73 | },
74 |
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'moduleName',
4 |
5 | authority: {
6 | resources: [
7 | 'GET /auth/auditlogs'
8 | ],
9 | events: [],
10 | },
11 |
12 | routes: []
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/module.locale.en-US.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': 'Audit Log',
4 |
5 | 'auditlog.field.logId': 'Serial No',
6 | 'auditlog.field.method': 'Method',
7 | 'auditlog.field.uriPattern': 'URI Pattern',
8 | 'auditlog.field.operation': 'Operation',
9 | 'auditlog.field.operator': 'Operator',
10 | 'auditlog.field.cost': 'Cost',
11 | 'auditlog.field.operateTime': 'Operate Time',
12 |
13 | };
14 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/module.locale.zh-CN.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': '操作日志',
4 |
5 | };
6 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/services/auditlog.service.js:
--------------------------------------------------------------------------------
1 | import { get, post } from '@/utils/rest-accessor';
2 |
3 |
4 | export function findAuditlogs(filterParam) {
5 | return get('/auth/auditlogs', null, filterParam);
6 | }
7 |
8 |
9 | export function findOperators(filterParam) {
10 | return get('/metadata/auth/auditlogs/operators', null, filterParam);
11 | }
12 |
13 | export function findResources(filterParam) {
14 | return get('/metadata/auth/auditlogs/resources', null, filterParam);
15 | }
16 |
--------------------------------------------------------------------------------
/src/pages/authority/auditlog/style.less:
--------------------------------------------------------------------------------
1 | .content {
2 | padding: 24px;
3 | background: #FFF;
4 | border-radius: 2px;
5 | }
6 |
7 | .button-area {
8 | margin-bottom: 16px;
9 | height: 32px;
10 | }
11 |
12 | .right {
13 | text-align: right;
14 | }
15 |
16 | .uri_pattern {
17 | font-style: italic;
18 | }
19 |
20 | .operator {
21 | position: relative;
22 | top: 0px;
23 |
24 | .name {
25 | position: absolute;
26 | top: -16px;
27 | }
28 |
29 | .extra {
30 | position: absolute;
31 | bottom: -20px;
32 | font-size: 12px;
33 | color: rgba(0,0,0,0.45);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PageHeaderWrapper from '@/components/PageHeaderWrapper';
3 | import { formatMessage as fm, FormattedMessage } from 'umi/locale';
4 | import mm from '@/utils/message-util';
5 | import {Button, Icon, Table, Tag} from 'antd';
6 | import TMDataList from '@/components/TMDataList';
7 | import { connect } from 'dva';
8 | import styles from './style.less';
9 |
10 |
11 | @connect(({ resource, loading }) => ({ resource, loading }))
12 | export default class ResourceManagement extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 | this.tmDataListRef = React.createRef();
17 | this._onDataFetchExpect = this._onDataFetchExpect.bind(this);
18 | }
19 |
20 | componentDidMount() {
21 | this.tmDataListRef.current.query();
22 | }
23 |
24 |
25 | render = () => {
26 |
27 | const { resource: { list }, loading } = this.props;
28 |
29 | const columns = [
30 | {
31 | title: '#',
32 | dataIndex: 'seq',
33 | width: 50,
34 | render: (text, record, index) => {
35 | return {index + 1} ;
36 | }
37 | },
38 | {
39 | title: mm('resource.field.method'),
40 | dataIndex: 'verb',
41 | key: 'verb',
42 | width: 170,
43 | render: (text, record, index) => {
44 | switch(record.verb) {
45 | case 'DELETE':
46 | return { record.verb } ;
47 | case 'GET':
48 | return { record.verb } ;
49 | case 'POST':
50 | return { record.verb } ;
51 | case 'PUT':
52 | return { record.verb } ;
53 | default:
54 | return { record.verb }
55 | }
56 | },
57 | sorter: true,
58 | },
59 | {
60 | title: mm('resource.field.pattern'),
61 | dataIndex: 'uri_pattern',
62 | key: 'uri_pattern',
63 | sorter: true,
64 | render: (text) => {
65 | return {text}
66 | }
67 | },
68 | {
69 | title: mm('resource.field.name'),
70 | dataIndex: 'name',
71 | key: 'name'
72 | },
73 | {
74 | title: mm('resource.field.remark'),
75 | dataIndex: 'remark',
76 | key: 'remark'
77 | },
78 | ];
79 |
80 | return (
81 |
84 |
85 |
92 |
93 |
94 | );
95 |
96 | }
97 |
98 |
99 | _onDataFetchExpect = (filterParameters) => {
100 | const { dispatch } = this.props;
101 | return dispatch({
102 | type: 'resource/fetchList',
103 | payload: {
104 | filterParameters,
105 | }
106 | });
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/models/resource.model.js:
--------------------------------------------------------------------------------
1 | import { findResources } from '../services/resource.service';
2 | import { convertPaginationFilterParam, convertSortFilterParam } from '@/utils/request-util';
3 |
4 |
5 | export default {
6 |
7 | namespace: 'resource',
8 |
9 | state: {
10 | list: [],
11 | },
12 |
13 |
14 | effects: {
15 |
16 | *fetchList({ payload: { filterParameters } }, { call, put, select }) {
17 |
18 | try {
19 | const response = yield call(findResources, filterParameters);
20 |
21 | yield put({
22 | type: 'updateList',
23 | payload: response.data
24 | });
25 | return parseInt(response.headers['x-total-count']);
26 | } catch (e) {
27 | console.error(e);
28 | return 0;
29 | }
30 | }
31 |
32 | },
33 |
34 |
35 | reducers: {
36 |
37 | updateList(state, action) {
38 | return {
39 | ...state,
40 | list: action.payload
41 | }
42 | },
43 |
44 | }
45 |
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'moduleName',
4 |
5 | authority: {
6 | resources: [
7 | "GET /auth/resources",
8 | ],
9 | events: [],
10 | },
11 |
12 | routes: []
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/module.locale.en-US.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': 'Resource Management',
4 |
5 | 'resource.field.method': 'HTTP Method',
6 | 'resource.field.pattern': 'URI Pattern',
7 | 'resource.field.name': 'Name',
8 | 'resource.field.remark': 'Remark',
9 |
10 |
11 | };
12 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/module.locale.zh-CN.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': '资源管理',
4 |
5 | 'resource.field.method': 'HTTP Method',
6 | 'resource.field.pattern': 'URI Pattern',
7 | 'resource.field.name': '名称',
8 | 'resource.field.remark': '备注',
9 |
10 | };
11 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/services/resource.service.js:
--------------------------------------------------------------------------------
1 | import { get, post } from '@/utils/rest-accessor';
2 |
3 |
4 | /**
5 | * 查询资源列表
6 | *
7 | * @returns {Promise}
8 | */
9 | export function findResources(filterParam) {
10 | return get('/auth/resources', null, filterParam);
11 | }
12 |
--------------------------------------------------------------------------------
/src/pages/authority/resource-mgnt/style.less:
--------------------------------------------------------------------------------
1 | .content {
2 | padding: 24px;
3 | background: #FFF;
4 | border-radius: 2px;
5 | }
6 |
7 | .button-area {
8 | margin-bottom: 16px;
9 | height: 32px;
10 | }
11 |
12 | .right {
13 | text-align: right;
14 | }
15 |
16 | .uri_pattern {
17 | font-style: italic;
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/components/QueryConditionForm/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AbstractQueryCondition from '@/components/TMDataList/query-condition';
3 | import TMStandardFormRow from '@/components/TMStandardFormRow';
4 | import { Form, Input, Radio } from "antd";
5 | import _ from 'lodash';
6 | import mm from '@/utils/message-util';
7 |
8 | const { Item: FormItem } = Form;
9 |
10 |
11 | export default class QueryConditionForm extends AbstractQueryCondition {
12 |
13 | static propTypes = {
14 | ...AbstractQueryCondition.propTypes
15 | };
16 |
17 | static initQueryCondition = {
18 | name: '',
19 | status: 'a',
20 | };
21 |
22 | constructor(props) {
23 | super(props);
24 | this.nameRef = React.createRef();
25 | this.lastQueryCondition = { ...QueryConditionForm.initQueryCondition };
26 | this.state = {
27 | ...QueryConditionForm.initQueryCondition,
28 | }
29 | }
30 |
31 | resetForm(callback) {
32 | this.setState({ ...QueryConditionForm.initQueryCondition }, callback);
33 | }
34 |
35 | getFormValues() {
36 | return _.cloneDeep(this.state);
37 | }
38 |
39 | render() {
40 | const { loading } = this.props;
41 |
42 | const labelWidth = { labelWidth: 100};
43 | const contentWidth = { width: 300 };
44 |
45 | return (
46 |
73 | );
74 |
75 | };
76 |
77 | _onStatusChange = (e) => {
78 | this.onFieldChange({ status: e.target.value });
79 | }
80 |
81 | _onNameChange = (e) => {
82 | this.setState({ name: e.currentTarget.value }, () => {
83 | this.nameRef.current.focus();
84 | });
85 | }
86 |
87 | _onNameBlur = () => {
88 | console.log('blur')
89 | this.fireQueryConditionChangeEvent();
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/components/RoleDetailDrawer/style.less:
--------------------------------------------------------------------------------
1 | .role-drawer {
2 | :global {
3 | .ant-drawer-wrapper-body {
4 | height: calc(100% - 53px) !important;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/models/role.detail.model.js:
--------------------------------------------------------------------------------
1 | import { selectRole, createRole, updateRole } from "../services/role.service";
2 | import { ROLE_OPERATE_TYPE_ENUM } from '../components/RoleDetailDrawer';
3 |
4 | export default {
5 |
6 | namespace: 'role_detail',
7 |
8 |
9 | state: {
10 |
11 | formData: {},
12 |
13 | operateType: null,
14 | id: null,
15 |
16 | tabKey: '1',
17 | },
18 |
19 |
20 | effects: {
21 |
22 | *initDrawer({ payload }, { call, put }) {
23 | const { operateType, roleId } = payload;
24 |
25 | switch(operateType) {
26 | // create a role
27 | case ROLE_OPERATE_TYPE_ENUM.CREATE:
28 | yield put({ type: 'initCreate' })
29 | break;
30 | // modify a role
31 | case ROLE_OPERATE_TYPE_ENUM.MODIFY:
32 | try {
33 | const response = yield call(selectRole, roleId);
34 | yield put({
35 | type: 'initModify',
36 | payload: response.data,
37 | });
38 | } catch (e) {
39 | console.error(e);
40 | }
41 | break;
42 | default:
43 | console.error('unrecognized operation type');
44 | }
45 | },
46 |
47 | *submitForm({ payload }, { call, put, select }) {
48 | const { operateType, id } = yield select(state => state.role_detail);
49 |
50 | console.log(payload)
51 | try {
52 | switch(operateType) {
53 | // create a role
54 | case ROLE_OPERATE_TYPE_ENUM.CREATE:
55 | yield call(createRole, payload);
56 | return true;
57 | // modify a role
58 | case ROLE_OPERATE_TYPE_ENUM.MODIFY:
59 | yield call(updateRole, id, payload);
60 | return true;
61 | default:
62 | console.error('unrecognized operation type');
63 | return false;
64 | }
65 | } catch (e) {
66 | console.error(e);
67 | return false;
68 | }
69 | },
70 |
71 | },
72 |
73 |
74 | reducers: {
75 |
76 | initCreate(state, action) {
77 | return {
78 | operateType: ROLE_OPERATE_TYPE_ENUM.CREATE,
79 | id: null,
80 | formData: {
81 | code: '',
82 | name: '',
83 | remark: '',
84 | resource_ids: [],
85 | },
86 | tabKey: '1',
87 | }
88 | },
89 |
90 | initModify(state, { payload }) {
91 | return {
92 | operateType: ROLE_OPERATE_TYPE_ENUM.MODIFY,
93 | id: payload.id,
94 | formData: {
95 | code: payload.code,
96 | name: payload.name,
97 | status: payload.status,
98 | remark: payload.remark,
99 | resource_ids: payload.resources.map(r => r.id),
100 | },
101 | tabKey: '1',
102 | }
103 | },
104 |
105 | updateModel(state, { payload }) {
106 | return {
107 | ...state,
108 | ...payload,
109 | };
110 | },
111 |
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/models/role.model.js:
--------------------------------------------------------------------------------
1 | import { findRoles, deleteRole } from "../services/role.service";
2 |
3 | export default {
4 |
5 | namespace: 'role',
6 |
7 |
8 | state: {
9 | list: [],
10 | },
11 |
12 |
13 | effects: {
14 |
15 | *fetchList({ payload }, { call, put }) {
16 | const {filterParameters} = payload;
17 | // convert filterParameters
18 | const nFilterParameters = {...filterParameters, status: filterParameters.status == 'a' ? null : filterParameters.status};
19 | try {
20 | const response = yield call(findRoles, nFilterParameters);
21 | yield put({
22 | type: 'updateList',
23 | payload: response.data
24 | });
25 | return parseInt(response.headers['x-total-count']);
26 | } catch (e) {
27 | console.error(e);
28 | return 0;
29 | }
30 | },
31 |
32 | *deleteRole({ payload: { roleId } }, { call, put }) {
33 | try {
34 | yield call(deleteRole, roleId);
35 | } catch (e) {
36 | console.error(e);
37 | }
38 | },
39 |
40 | },
41 |
42 |
43 | reducers: {
44 |
45 | updateList(state, action) {
46 | return {
47 | ...state,
48 | list: action.payload
49 | }
50 | },
51 |
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'moduleName',
4 |
5 | authority: {
6 | resources: [
7 | 'GET /auth/roles',
8 | 'GET /auth/roles/{roleId}',
9 | 'GET /auth/roles/{roleId}',
10 | 'GET /auth/roles/{roleId}',
11 | ],
12 | events: [
13 | {
14 | code: 'create-role',
15 | name: 'createRoleEventName',
16 | resources: [
17 | 'POST /auth/roles',
18 | ],
19 | },
20 | {
21 | code: 'modify-role',
22 | name: 'modifyRoleEventName',
23 | resources: [
24 | 'PUT /auth/roles/{roleId}',
25 | 'GET /auth/roles',
26 | 'GET /auth/roles/{roleId}',
27 | 'GET /auth/rols',
28 | ],
29 | },
30 | {
31 | code: 'delete-role',
32 | name: 'deleteRoleEventName',
33 | resources: [
34 | 'DELETE /auth/roles/{roleId}',
35 | ],
36 | },
37 | ],
38 | },
39 |
40 | routes: []
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/module.locale.en-US.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': 'Role Management',
4 |
5 | 'createRoleEventName': 'Create Role',
6 | 'modifyRoleEventName': 'Modify Role',
7 | 'deleteRoleEventName': 'Delete Role',
8 |
9 | 'button.createRole': 'Create Role',
10 |
11 | 'label.roleName': 'Role Name',
12 | 'label.roleStatus': 'Status',
13 | 'label.roleCode': 'Role Code',
14 | 'label.roleRemark': 'Remark',
15 |
16 | 'label.createRole': 'Create Role',
17 | 'label.roleDetail': 'Role Detail',
18 | 'label.tab.basic': 'Basic Info',
19 | 'label.tab.authority': 'Authority Assignment',
20 |
21 | 'dict.roleStatus.all': 'All',
22 | 'dict.roleStatus.disabled': 'Disabled',
23 | 'dict.roleStatus.enabled': 'Enabled',
24 | 'dict.roleStatus.unknown': 'Unknown',
25 |
26 | 'tip.queryCondition.roleName': 'input search text',
27 | 'tip.confirm.deleteRole': 'Are you sure to delete?',
28 | 'tip.feedback.deleteRoleSuccess': 'delete successfully',
29 |
30 | 'tip.formValidation.roleName.empty': 'role name should not be empty',
31 | 'tip.formValidation.roleCode.empty': 'role code should not be empty',
32 | };
33 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/module.locale.zh-CN.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'moduleName': '角色管理',
4 |
5 | 'createRoleEventName': '创建角色',
6 | 'modifyRoleEventName': '修改角色',
7 | 'deleteRoleEventName': '删除角色',
8 |
9 | 'button.createRole': '创建角色',
10 |
11 | 'label.roleName': '角色名称',
12 | 'label.roleStatus': '角色状态',
13 | 'label.roleCode': '角色编码',
14 | 'label.roleRemark': '备注',
15 |
16 | 'label.createRole': '创建角色',
17 | 'label.roleDetail': '角色详情',
18 | 'label.tab.basic': '基本信息',
19 | 'label.tab.authority': '权限配置',
20 |
21 | 'dict.roleStatus.all': '全部',
22 | 'dict.roleStatus.disabled': '禁用',
23 | 'dict.roleStatus.enabled': '启用',
24 | 'dict.roleStatus.unknown': '未知',
25 |
26 | 'tip.queryCondition.roleName': '输入查询关键字',
27 | 'tip.confirm.deleteRole': '确定要删除么?',
28 | 'tip.feedback.deleteRoleSuccess': '删除成功',
29 |
30 | 'tip.formValidation.roleName.empty': '角色名称不可为空',
31 | 'tip.formValidation.roleCode.empty': '角色编码不可为空',
32 | };
33 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/services/role.service.js:
--------------------------------------------------------------------------------
1 | import { get, post, del, put } from '@/utils/rest-accessor';
2 |
3 |
4 | export function findRoles(filterParam) {
5 | return get('/auth/roles', null, filterParam);
6 | }
7 |
8 | export function deleteRole(roleId) {
9 | return del('/auth/roles/:roleId', { roleId });
10 | }
11 |
12 | export function selectRole(roleId) {
13 | return get('/auth/roles/:roleId', { roleId });
14 | }
15 |
16 | export function updateRole(roleId, role) {
17 | return put('/auth/roles/:roleId', { roleId }, null, role);
18 | }
19 |
20 | export function createRole(role) {
21 | return post('/auth/roles', null, null, role)
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/authority/role-mgnt/style.less:
--------------------------------------------------------------------------------
1 | .content {
2 | padding: 24px;
3 | background: #FFF;
4 | border-radius: 2px;
5 | }
6 |
7 | .button-area {
8 | margin-bottom: 16px;
9 | height: 32px;
10 | }
11 |
12 | .right {
13 | text-align: right;
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/components/QueryConditionForm/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AbstractQueryCondition from '@/components/TMDataList/query-condition';
3 | import TMStandardFormRow from '@/components/TMStandardFormRow';
4 | import { Form, Input, Radio } from "antd";
5 | import _ from 'lodash';
6 | import mm from '@/utils/message-util';
7 |
8 | const { Item: FormItem } = Form;
9 |
10 |
11 | export default class QueryConditionForm extends AbstractQueryCondition {
12 |
13 | static propTypes = {
14 | ...AbstractQueryCondition.propTypes
15 | };
16 |
17 | static initQueryCondition = {
18 | name: '',
19 | status: 'a',
20 | };
21 |
22 | constructor(props) {
23 | super(props);
24 | this.nameRef = React.createRef();
25 | this.lastQueryCondition = { ...QueryConditionForm.initQueryCondition };
26 | this.state = {
27 | ...QueryConditionForm.initQueryCondition,
28 | }
29 | }
30 |
31 | resetForm(callback) {
32 | this.setState({ ...QueryConditionForm.initQueryCondition }, callback);
33 | }
34 |
35 | getFormValues() {
36 | return _.cloneDeep(this.state);
37 | }
38 |
39 | render() {
40 | const { loading } = this.props;
41 |
42 | const labelWidth = { labelWidth: 100};
43 | const contentWidth = { width: 300 };
44 |
45 | return (
46 |
73 | );
74 |
75 | };
76 |
77 | _onStatusChange = (e) => {
78 | this.onFieldChange({ status: e.target.value });
79 | }
80 |
81 | _onNameChange = (e) => {
82 | this.setState({ name: e.currentTarget.value }, () => {
83 | this.nameRef.current.focus();
84 | });
85 | }
86 |
87 | _onNameBlur = () => {
88 | this.fireQueryConditionChangeEvent();
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/components/RoleAssignment/style.less:
--------------------------------------------------------------------------------
1 | .role-assignment {
2 | height: 100%;
3 |
4 | :global {
5 | .ant-transfer-list-content-item {
6 | height: 50px;
7 | position: relative;
8 |
9 | .ant-checkbox-wrapper {
10 | vertical-align: top;
11 | padding-top: 8px;
12 | }
13 | }
14 | }
15 |
16 | .item {
17 | position: absolute;
18 | left: 0px;
19 | top: 0px;
20 | width: 240px;
21 | height: 50px;
22 | padding: 6px 12px 6px 36px;
23 |
24 | &>div {
25 | text-overflow: ellipsis;
26 | overflow: hidden;
27 | }
28 | .assigned {
29 | color: #1890ff;
30 | position: absolute;
31 | right: 12px;
32 | top: 18px;
33 | font-size: 11px;
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/components/UserDetailDrawer/style.less:
--------------------------------------------------------------------------------
1 | .tab {
2 | height: 100%;
3 | :global {
4 | .ant-tabs-content {
5 | height: calc(100% - 60px) !important;
6 | .ant-tabs-tabpane-active {
7 | height: 100%;
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/models/user.detail.model.js:
--------------------------------------------------------------------------------
1 | import { selectUser, createUser, updateUser, fetchFullRoles } from "../services/user.service";
2 | import { OPERATE_TYPE_ENUM } from '../components/UserDetailDrawer';
3 |
4 | const MODEL_SCOPE_NAME = 'user_detail';
5 |
6 | export default {
7 |
8 | namespace: 'user_detail',
9 |
10 |
11 | state: {
12 |
13 | detail: {},
14 | operateType: null,
15 | id: null,
16 |
17 | tabKey: '1',
18 | fullRoles: [],
19 | },
20 |
21 |
22 | effects: {
23 |
24 | *initDrawer({ payload }, { call, put }) {
25 | const { operateType, itemId } = payload;
26 |
27 | const response = yield call(fetchFullRoles);
28 | yield put({
29 | type: 'updateModel',
30 | payload: {
31 | fullRoles: response.data.map(item => {
32 | return {
33 | key: item.id,
34 | title: item.name,
35 | description: item.remark,
36 | disabled: item.status == '0',
37 | roleData: item,
38 | };
39 | }),
40 | }
41 | });
42 |
43 | switch(operateType) {
44 | case OPERATE_TYPE_ENUM.CREATE:
45 | yield put({ type: 'initCreate' })
46 | break;
47 | case OPERATE_TYPE_ENUM.MODIFY:
48 | try {
49 | const response = yield call(selectUser, itemId);
50 | yield put({
51 | type: 'initModify',
52 | payload: response.data,
53 | });
54 | } catch (e) {
55 | console.error(e);
56 | }
57 | break;
58 | default:
59 | console.error('unrecognized operation type');
60 | }
61 | },
62 |
63 | *submitForm({ payload }, { call, put, select }) {
64 | const { operateType, id } = yield select(state => state[MODEL_SCOPE_NAME]);
65 |
66 | try {
67 | switch(operateType) {
68 | case OPERATE_TYPE_ENUM.CREATE:
69 | yield call(createUser, payload);
70 | return true;
71 | case OPERATE_TYPE_ENUM.MODIFY:
72 | yield call(updateUser, id, payload);
73 | return true;
74 | default:
75 | console.error('unrecognized operation type');
76 | return false;
77 | }
78 | } catch (e) {
79 | console.error(e);
80 | return false;
81 | }
82 | },
83 |
84 | },
85 |
86 | reducers: {
87 |
88 | initCreate(state) {
89 | return {
90 | ...state,
91 | operateType: OPERATE_TYPE_ENUM.CREATE,
92 | id: null,
93 | tabKey: '1',
94 | }
95 | },
96 |
97 | initModify(state, { payload }) {
98 | return {
99 | ...state,
100 | operateType: OPERATE_TYPE_ENUM.MODIFY,
101 | id: payload.id,
102 | detail: payload,
103 | tabKey: '1',
104 | }
105 | },
106 |
107 | updateModel(state, { payload }) {
108 | return {
109 | ...state,
110 | ...payload,
111 | };
112 | },
113 |
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/models/user.model.js:
--------------------------------------------------------------------------------
1 | import {deleteUser, findUsers} from "../services/user.service";
2 |
3 | export default {
4 |
5 | namespace: 'user',
6 |
7 | state: {
8 | list: [],
9 | },
10 |
11 |
12 | effects: {
13 | *fetchList({ payload }, { call, put }) {
14 | const {filterParameters} = payload;
15 | // convert filterParameters
16 | const nFilterParameters = {...filterParameters, status: filterParameters.status == 'a' ? null : filterParameters.status};
17 | try {
18 | const response = yield call(findUsers, nFilterParameters);
19 | yield put({
20 | type: 'updateList',
21 | payload: response.data
22 | });
23 | return parseInt(response.headers['x-total-count']);
24 | } catch (e) {
25 | console.error(e);
26 | return 0;
27 | }
28 | },
29 |
30 | *deleteUser({ payload: { userId } }, { call, put }) {
31 | try {
32 | yield call(deleteUser, userId);
33 | return true;
34 | } catch (e) {
35 | return false;
36 | }
37 | },
38 | },
39 |
40 |
41 | reducers: {
42 | updateList(state, action) {
43 | return {
44 | ...state,
45 | list: action.payload
46 | }
47 | },
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'moduleName',
4 |
5 | authority: {
6 | resources: [
7 | 'GET /auth/users',
8 | 'GET /auth/users/{userId}',
9 | ],
10 | events: [],
11 | },
12 |
13 | routes: []
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/module.locale.en-US.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | // module name
4 | 'moduleName': 'User Management',
5 |
6 | // events name
7 | 'createUserEventName': 'Create User',
8 | 'modifyUserEventName': 'Modify User',
9 | 'deleteUserEventName': 'Delete User',
10 |
11 | // ------------------------------------------------------------
12 |
13 |
14 | 'button.createUser': 'Create User',
15 |
16 | 'label.loginName': 'Login Name',
17 | 'label.userName': 'User Name',
18 | 'label.userPassword': 'Password',
19 | 'label.userType': 'Type',
20 | 'label.userRemark': 'Remark',
21 | 'label.userStatus': 'Status',
22 | 'label.lastLoginTime': 'Last Login Time',
23 | 'label.lastLoginIP': 'Last Login IP',
24 |
25 |
26 | 'label.createUser': 'Create User',
27 | 'label.userDetail': 'User Detail',
28 | 'label.tab.basic': 'Basic Info',
29 | 'label.tab.authority': 'Role Assignment',
30 | 'label.transfer.title.unassigned': 'Unassigned',
31 | 'label.transfer.title.assigned': 'Assigned',
32 |
33 |
34 | 'dict.userStatus.all': 'All',
35 | 'dict.userStatus.disabled': 'Disabled',
36 | 'dict.userStatus.enabled': 'Enabled',
37 | 'dict.userStatus.unknown': 'Unknown',
38 | 'dict.userType.all': 'All',
39 | 'dict.userType.supervisor': 'Supervisor',
40 | 'dict.userType.normal': 'Normal',
41 | 'dict.userType.unknown': 'Unknown',
42 |
43 | 'tip.queryCondition.userName': 'input search text',
44 | 'tip.confirm.deleteUser': 'Are you sure to delete?',
45 | 'tip.feedback.deleteUserSuccess': 'delete successfully',
46 |
47 | 'tip.formValidation.userName.empty': 'user name should not be empty',
48 | 'tip.formValidation.loginName.empty': 'login name should not be empty',
49 | 'tip.formValidation.password.empty': 'password should not be empty',
50 |
51 | 'tip.description.userType': 'A supervisor has full authority to access this system, it cannot be created, modified, ' +
52 | 'removed or assign any roles to.',
53 |
54 | };
55 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/module.locale.zh-CN.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | // module name
4 | 'moduleName': '用户管理',
5 |
6 | // events name
7 | 'createUserEventName': '创建用户',
8 | 'modifyUserEventName': '修改用户',
9 | 'deleteUserEventName': '删除用户',
10 |
11 | // ------------------------------------------------------------
12 |
13 |
14 | 'button.createUser': '创建用户',
15 |
16 | 'label.loginName': '登录名',
17 | 'label.userName': '姓名',
18 | 'label.userPassword': '密码',
19 | 'label.userType': '类型',
20 | 'label.userRemark': '备注',
21 | 'label.userStatus': '状态',
22 | 'label.lastLoginTime': '最近登录时间',
23 | 'label.lastLoginIP': '最近登录IP',
24 |
25 | 'label.createUser': '创建用户',
26 | 'label.userDetail': '用户详情',
27 | 'label.tab.basic': '基本信息',
28 | 'label.tab.authority': '角色分配',
29 | 'label.transfer.title.unassigned': '未分配',
30 | 'label.transfer.title.assigned': '已分配',
31 |
32 | 'dict.userStatus.all': '全部',
33 | 'dict.userStatus.disabled': '禁用',
34 | 'dict.userStatus.enabled': '启用',
35 | 'dict.userStatus.unknown': '未知',
36 | 'dict.userType.all': '全部',
37 | 'dict.userType.supervisor': '超级用户',
38 | 'dict.userType.normal': '普通用户',
39 | 'dict.userType.unknown': '未知',
40 |
41 | 'tip.queryCondition.userName': '输入查询关键字',
42 | 'tip.confirm.deleteUser': '确定要删除么?',
43 | 'tip.feedback.deleteUserSuccess': '删除成功',
44 |
45 | 'tip.formValidation.userName.empty': '用户姓名不可为空',
46 | 'tip.formValidation.loginName.empty': '登录名不可为空',
47 | 'tip.formValidation.password.empty': '密码不可为空',
48 |
49 | 'tip.description.userType': '超级用户对系统具有全部访问权限,它无法被创建,修改,删除或者分配任何角色',
50 |
51 | };
52 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/services/user.service.js:
--------------------------------------------------------------------------------
1 | import { get, post, del, put } from '@/utils/rest-accessor';
2 |
3 |
4 | export function findUsers(filterParam) {
5 | return get('/auth/users', null, filterParam);
6 | }
7 |
8 | export function deleteUser(userId) {
9 | return del('/auth/users/:userId', { userId });
10 | }
11 |
12 | export function selectUser(userId) {
13 | return get('/auth/users/:userId', { userId });
14 | }
15 |
16 | export function updateUser(userId, user) {
17 | return put('/auth/users/:userId', { userId }, null, user);
18 | }
19 |
20 | export function createUser(user) {
21 | return post('/auth/users', null, null, user)
22 | }
23 |
24 | export function fetchFullRoles() {
25 | return get('/auth/roles', null, {
26 | infinite_count: true,
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/pages/authority/user-mgnt/style.less:
--------------------------------------------------------------------------------
1 | .content {
2 | padding: 24px;
3 | background: #FFF;
4 | border-radius: 2px;
5 | }
6 |
7 | .button-area {
8 | margin-bottom: 16px;
9 | height: 32px;
10 | }
11 |
12 | .right {
13 | text-align: right;
14 | }
15 |
16 | .tb-column-title-tip {
17 | font-size: 12px;
18 | margin-left: 4px;
19 | line-height: 17px;
20 | vertical-align: middle;
21 | color: rgba(0,0,0,0.45);
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/document.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | loading
11 |
12 | <% if(context.env === 'production') { %>
13 |
14 |
15 |
16 | <% }%>
17 |
18 |
19 |
20 | Sorry, we need js to run correctly!
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/pages/login/login.css:
--------------------------------------------------------------------------------
1 | .login-container {
2 | height: 100%;
3 | width: 100%;
4 | background: url(../../assets/bg-3.jpg) no-repeat center center fixed;
5 | background-size: cover;
6 | }
7 |
8 | @media screen and (max-height: 482px) {
9 | .login-panel {
10 | max-width: 460px;
11 | height: 386px;
12 | position: absolute;
13 | margin: auto;
14 | left: 0px;
15 | right: 0px;
16 | top: 0px;
17 | background: #FFF;
18 | border-radius: 4px;
19 | border-width: 1px;
20 | border-style: solid;
21 | border-color: rgb(212, 218, 223);
22 | box-shadow: rgba(116, 129, 141, 0.1) 0px 1px 1px 0px;
23 | box-shadow: 0 1px 11px rgba(0,0,0,0.27);
24 | z-index: 10;
25 | }
26 | }
27 | @media screen and (min-height: 483px) {
28 | .login-panel {
29 | max-width: 460px;
30 | height: 386px;
31 | position: absolute;
32 | margin: auto;
33 | left: 0px;
34 | right: 0px;
35 | top: 0px;
36 | bottom: 20%;
37 | background: #FFF;
38 | border-radius: 4px;
39 | border-width: 1px;
40 | border-style: solid;
41 | border-color: rgb(212, 218, 223);
42 | box-shadow: rgba(116, 129, 141, 0.1) 0px 1px 1px 0px;
43 | box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
44 | z-index: 10;
45 | }
46 | }
47 |
48 | .login-panel.with_err {
49 | height: 550px;
50 | }
51 |
52 | .input-prefix-icon {
53 | color: rgba(0, 0, 0, 0.25);
54 | }
55 |
56 | .login-panel .err_msg {
57 | margin-bottom: 24px;
58 | }
59 |
60 | .login-panel .banner {
61 | padding: 32px 24px;
62 | border-bottom: 1px solid rgb(230, 236, 241);
63 | }
64 |
65 | .login-panel .banner .title {
66 | color: rgba(0,0,0,0.85);
67 | font-size: 24px;
68 | }
69 |
70 | .login-panel img.logo {
71 | position: absolute;
72 | right: 24px;
73 | top: 32px;
74 | height: 36px;
75 | }
76 |
77 | .login-panel .login-form {
78 | padding: 32px 24px;
79 | }
80 |
81 | .login-panel .login-form .input-label {
82 | height: 23px;
83 | position: relative;
84 | }
85 |
86 | .login-panel .login-form .input-label span {
87 | position: absolute;
88 | top: 0px;
89 | line-height: 14px;
90 | color: rgb(116, 129, 141);
91 | font-size: 12px;
92 | }
93 |
94 | .login-panel .login-form .forget-password a {
95 | position: absolute;
96 | top: 0px;
97 | line-height: 14px;
98 | font-size: 12px;
99 | right: 0px;
100 | }
101 |
102 | .login-panel .login-type {
103 | border-top: 1px solid rgb(230, 236, 241);
104 | position: relative;
105 | padding: 32px 24px;
106 | }
107 |
108 | .login-panel .login-type .login-type-label {
109 | position: absolute;
110 | top: 0px;
111 | line-height: 14px;
112 | color: rgb(116, 129, 141);
113 | font-size: 12px;
114 | left: 50%;
115 | transform: translate(-50%, -50%);
116 | padding: 0px 8px;
117 | background: rgb(255, 255, 255);
118 | }
119 |
120 | .login-panel .login-type .radio-btn {
121 | width: 50%;
122 | text-align: center;
123 | font-size: 13px;
124 | }
125 |
126 | .footer {
127 | position: absolute;
128 | bottom: 0;
129 | width: 100%;
130 | height: 30px;
131 | z-index: 100;
132 | background: #021428;
133 | color: #FFF;
134 | text-align: center;
135 | line-height: 30px;
136 | font-size: 0.9em;
137 | }
138 |
--------------------------------------------------------------------------------
/src/pages/test-a/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PageHeaderWrapper from '@/components/PageHeaderWrapper';
3 | import TMCombobox from '@/components/form-item/TMCombobox2';
4 | import { formatMessage, FormattedMessage } from 'umi/locale';
5 | import { connect } from 'dva';
6 | import { Select, Pagination } from 'antd';
7 |
8 |
9 | const children = [];
10 | for (let i = 0; i < 10; i++) {
11 | children.push(
12 |
13 | {i}
14 | ,
15 | );
16 | }
17 |
18 |
19 | @connect(({ loading }) => ({ loading }))
20 | export default class ModuleEntry extends Component {
21 |
22 | constructor(props) {
23 | super(props);
24 | console.log('Constructor of test-a')
25 | }
26 |
27 |
28 | componentDidMount() {
29 | const { dispatch } = this.props;
30 | // do some initial data loading here
31 | }
32 |
33 | state = {
34 | value: ['name2', 'name3'],
35 | };
36 |
37 | onChange = (value, option) => {
38 | console.log(`changed ${value}`, option);
39 | this.setState({
40 | value,
41 | });
42 | };
43 |
44 | onSelect = (value, option) => {
45 | console.log(`selected ${value}`, option.props);
46 | };
47 |
48 | onDeselect = (value, option) => {
49 | console.log(`deselected ${value}`, option);
50 | };
51 |
52 |
53 | render = () => {
54 | const { loading } = this.props;
55 | const { value } = this.state;
56 | return (
57 | }
59 | >
60 | {
62 |
63 | return new Promise((resolve) => {
64 | setTimeout(() => {
65 | if (keyword == 'a') {
66 | resolve({
67 | total: 12,
68 | data: [
69 | {id: 1, name: 'aaaaa'},
70 | {id: 2, name: 'bbbbb'},
71 | ]
72 | })
73 | } else {
74 | resolve({
75 | total: 0,
76 | data: []
77 | })
78 | }
79 |
80 | }, 1000);
81 | });
82 |
83 | } }
84 | />
85 |
86 |
87 | );
88 |
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/pages/test-a/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'testa',
4 |
5 | authority: {
6 | resources: [
7 | 'GET /test/a',
8 | 'GET /test/a/{aId}'
9 | ],
10 | events: [
11 | {
12 | code: 'update',
13 | name: 'update',
14 | resources: []
15 | },
16 | ],
17 | },
18 |
19 | routes: [
20 |
21 | ]
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/test-b/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PageHeaderWrapper from '@/components/PageHeaderWrapper';
3 | import { formatMessage, FormattedMessage } from 'umi/locale';
4 | import { connect } from 'dva';
5 |
6 |
7 | @connect(({ loading }) => ({ loading }))
8 | export default class TestA extends Component {
9 |
10 |
11 | componentDidMount() {
12 | const { dispatch } = this.props;
13 | // do some initial data loading here
14 | }
15 |
16 |
17 | render = () => {
18 | const { loading } = this.props;
19 |
20 | return (
21 | }
23 | >
24 |
25 | to do
26 |
27 |
28 | );
29 |
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/test-b/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'testa',
4 |
5 | authority: {
6 | resources: [
7 | 'GET /test/b',
8 | 'GET /test/b/{bId}'
9 | ],
10 | events: [
11 | {
12 | code: 'create',
13 | name: 'create test',
14 | resources: []
15 | },
16 | ],
17 | },
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/test-c/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PageHeaderWrapper from '@/components/PageHeaderWrapper';
3 | import { formatMessage, FormattedMessage } from 'umi/locale';
4 | import { connect } from 'dva';
5 |
6 |
7 | @connect(({ loading }) => ({ loading }))
8 | export default class TestA extends Component {
9 |
10 |
11 | componentDidMount() {
12 | const { dispatch } = this.props;
13 | // do some initial data loading here
14 | }
15 |
16 |
17 | render = () => {
18 | const { loading } = this.props;
19 |
20 | return (
21 |
24 |
25 | to do
26 |
27 |
28 | );
29 |
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/test-c/module.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | name: 'testa',
4 |
5 | authority: {
6 | resources: [
7 | 'GET /test/c',
8 | 'GET /test/c/{cId}'
9 | ],
10 | events: [
11 | {
12 | code: 'create',
13 | name: 'create test',
14 | resources: []
15 | },
16 | ],
17 | },
18 |
19 | routes: []
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/router.config.js:
--------------------------------------------------------------------------------
1 | // NOTE: when modify routes in this file, you have to restart service
2 | // 注意:改动此文件后需要重启服务
3 |
4 | module.exports = [
5 |
6 | // user
7 | {
8 | path: '/login',
9 | component: './login',
10 | },
11 |
12 | ];
13 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* globals workbox */
2 | /* eslint-disable no-restricted-globals */
3 | workbox.core.setCacheNameDetails({
4 | prefix: 'antd-pro',
5 | suffix: 'v1',
6 | });
7 | // Control all opened tabs ASAP
8 | workbox.clientsClaim();
9 |
10 | /**
11 | * Use precaching list generated by workbox in build process.
12 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
13 | */
14 | /* eslint-disable no-underscore-dangle */
15 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
16 |
17 | /**
18 | * Register a navigation route.
19 | * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
20 | */
21 | workbox.routing.registerNavigationRoute('/index.html');
22 |
23 | /**
24 | * Use runtime cache:
25 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
26 | *
27 | * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
28 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
29 | */
30 |
31 | /**
32 | * Handle API requests
33 | */
34 | workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
35 |
36 | /**
37 | * Handle third party requests
38 | */
39 | workbox.routing.registerRoute(
40 | /^https:\/\/gw.alipayobjects.com\//,
41 | workbox.strategies.networkFirst()
42 | );
43 | workbox.routing.registerRoute(
44 | /^https:\/\/cdnjs.cloudflare.com\//,
45 | workbox.strategies.networkFirst()
46 | );
47 | workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
48 |
49 | /**
50 | * Response to client after skipping waiting with MessageChannel
51 | */
52 | addEventListener('message', event => {
53 | const replyPort = event.ports[0];
54 | const message = event.data;
55 | if (replyPort && message && message.type === 'skip-waiting') {
56 | event.waitUntil(
57 | self
58 | .skipWaiting()
59 | .then(
60 | () => replyPort.postMessage({ error: null }),
61 | error => replyPort.postMessage({ error })
62 | )
63 | );
64 | }
65 | });
66 |
--------------------------------------------------------------------------------
/src/services/global.service.js:
--------------------------------------------------------------------------------
1 | import { get, post, del } from '../utils/rest-accessor';
2 |
3 |
4 | /**
5 | * 获取完整的资源列表
6 | *
7 | * @returns {Promise}
8 | */
9 | export function fetchFullResourceMetadata() {
10 | return get('/metadata/resources', null, null);
11 | }
12 |
13 |
14 | /**
15 | * 获取当前会话的用户
16 | *
17 | * @returns {Promise}
18 | */
19 | export async function queryCurrent() {
20 |
21 | const response = await get('/system/session', null, null);
22 | _constructResourceIds(response);
23 | return response;
24 |
25 | }
26 |
27 | /**
28 | * 登录
29 | * @returns {Promise<*>}
30 | */
31 | export async function login(loginData) {
32 | const response = await post('/system/session', null, null, loginData);
33 | _constructResourceIds(response);
34 | return response;
35 | }
36 |
37 | /**
38 | * 注销
39 | * @returns {Promise<*|undefined>}
40 | */
41 | export function logout() {
42 | return del('/system/session')
43 | }
44 |
45 | function _constructResourceIds(response) {
46 | response.data.resource_ids = response.data.resources.map(resource => resource.id);
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/Yuan.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { yuan } from '@/components/Charts';
3 | /**
4 | * 减少使用 dangerouslySetInnerHTML
5 | */
6 | export default class Yuan extends React.PureComponent {
7 | componentDidMount() {
8 | this.rendertoHtml();
9 | }
10 |
11 | componentDidUpdate() {
12 | this.rendertoHtml();
13 | }
14 |
15 | rendertoHtml = () => {
16 | const { children } = this.props;
17 | if (this.main) {
18 | this.main.innerHTML = yuan(children);
19 | }
20 | };
21 |
22 | render() {
23 | return (
24 | {
26 | this.main = ref;
27 | }}
28 | />
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/date-util.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | // 2019-05-06T15:56:46.000+0800
4 | const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
5 |
6 | export function formatDateFromMoment(moment, needURLEncode) {
7 | if (needURLEncode) {
8 | return encodeURIComponent(moment.format(DATE_FORMAT));
9 | } else {
10 | return moment.format(DATE_FORMAT);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/message-util.js:
--------------------------------------------------------------------------------
1 | import { formatMessage as fm, FormattedMessage } from 'umi/locale';
2 | import MODULE_SETTINGS from '@tmp/moduleSettings';
3 | import _ from 'lodash';
4 |
5 | export default function(id) {
6 |
7 | const url = new URL(window.location.href);
8 | const { pathname } = url;
9 |
10 | const moduleKeys = Object.keys(MODULE_SETTINGS);
11 | let currentModuleCode = null;
12 | for (let i = 0; i < moduleKeys.length; i++) {
13 | const modulePathname = MODULE_SETTINGS[moduleKeys[i]].path;
14 | if (_.startsWith(pathname, modulePathname)) {
15 | currentModuleCode = moduleKeys[i];
16 | break;
17 | }
18 | }
19 |
20 | if (currentModuleCode) {
21 | return fm({id: `module.${currentModuleCode}.${id}`, defaultMessage: id});
22 | } else {
23 | return id;
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/request-util.js:
--------------------------------------------------------------------------------
1 |
2 | export function serializeQueryParameter(paramObj) {
3 | let queryParamLiteral = '';
4 | if (paramObj) {
5 | Object.keys(paramObj).forEach(key => {
6 | if (paramObj[key] != null) {
7 | if (queryParamLiteral == '') {
8 | queryParamLiteral = `?${key}=${paramObj[key]}`;
9 | } else {
10 | queryParamLiteral += `&${key}=${paramObj[key]}`;
11 | }
12 | }
13 | });
14 | }
15 | return queryParamLiteral;
16 | }
17 |
18 | export function convertPaginationFilterParam(pagination, originParam) {
19 | const filterParam = {
20 | ...(originParam || {}),
21 | last_cursor: (pagination.current - 1) * pagination.pageSize,
22 | count: pagination.pageSize,
23 | };
24 | return filterParam;
25 | }
26 |
27 | export function convertSortFilterParam(sort, originParam) {
28 | if (sort.field) {
29 | const filterParam = {
30 | ...(originParam || {}),
31 | sort_field: sort.field,
32 | sort_order: sort.order,
33 | };
34 | return filterParam;
35 | } else {
36 | return originParam;
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/src/utils/rest-err-processor.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import { notification, Tag } from 'antd';
3 | import env from 'environment';
4 | import router from 'umi/router';
5 |
6 |
7 | const SESSION_SYNC_REQ = { verb: 'GET', uri: '/system/session' };
8 | const SESSION_CREATE_REQ = { verb: 'POST', uri: '/system/session' };
9 |
10 | /**
11 | * process error during rest access
12 | * @param e
13 | */
14 | export default function(e) {
15 | const { extra, request } = e;
16 | const { url, options: { method } } = request;
17 | const { endpoint } = env;
18 |
19 | if (!_.startsWith(url, endpoint)) {
20 | throw `url not start with ${endpoint}`;
21 | }
22 |
23 | let path = url.substring(endpoint.length);
24 | path = _.startsWith(path, '/') ? path : `/${path}`;
25 |
26 | if (extra.status == 401) {
27 | process_401(e, path);
28 | } else {
29 | const { extra, request } = e;
30 | const { options: { method } } = request;
31 | notifyError(
32 | extra.statusText,
33 | extra.status,
34 | request.url,
35 | extra.data? extra.data.message : extra.statusText,
36 | method,
37 | );
38 | }
39 |
40 | }
41 |
42 | function process_401(e, path) {
43 | const { extra, request } = e;
44 | const { options: { method } } = request;
45 |
46 | // GET /system/session
47 | // no notify , but redirect to /login
48 | if (method == SESSION_SYNC_REQ.verb && _.startsWith(path, SESSION_SYNC_REQ.uri)) {
49 | router.push('/login');
50 | }
51 |
52 | // POST /system/session
53 | // notify, but don't redirect to /login
54 | else if (method == SESSION_CREATE_REQ.verb && _.startsWith(path, SESSION_CREATE_REQ.uri)) {
55 | notifyError(
56 | extra.statusText,
57 | extra.status,
58 | request.url,
59 | extra.data? extra.data.message : extra.statusText,
60 | method,
61 | );
62 | }
63 |
64 | // other requests
65 | // notify, and redirect to /login
66 | else {
67 | notifyError(
68 | extra.statusText,
69 | extra.status,
70 | request.url,
71 | extra.data? extra.data.message : extra.statusText,
72 | request.options.method
73 | );
74 | router.push('/login');
75 | }
76 |
77 | }
78 |
79 | function notifyError(error, status, url, message, method) {
80 | notification.open({
81 | message: {status} {error}
,
82 | description: (
83 |
{message}
84 |
85 |
86 | {method} {url}
87 |
88 |
89 |
)
90 | });
91 | }
92 |
--------------------------------------------------------------------------------
/src/utils/rest-token-resolver.js:
--------------------------------------------------------------------------------
1 | const SESSION_STORAGE_TOKEN_KEY = 'auth_token';
2 | const REQ_HEADER_TOKEN_KEY = 'X-Auth-Token';
3 |
4 | export function storeToken(token) {
5 | sessionStorage.setItem(SESSION_STORAGE_TOKEN_KEY, token);
6 | }
7 |
8 | export function appendTokenToRequest(option) {
9 | let newOptions = {headers: [], ...option};
10 | if (sessionStorage.getItem(SESSION_STORAGE_TOKEN_KEY)) {
11 | newOptions.headers[REQ_HEADER_TOKEN_KEY] = sessionStorage.getItem(SESSION_STORAGE_TOKEN_KEY);
12 | }
13 | return newOptions;
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/utils.less:
--------------------------------------------------------------------------------
1 | .textOverflow() {
2 | overflow: hidden;
3 | white-space: nowrap;
4 | text-overflow: ellipsis;
5 | word-break: break-all;
6 | }
7 |
8 | .textOverflowMulti(@line: 3, @bg: #fff) {
9 | position: relative;
10 | max-height: @line * 1.5em;
11 | margin-right: -1em;
12 | padding-right: 1em;
13 | overflow: hidden;
14 | line-height: 1.5em;
15 | text-align: justify;
16 | &::before {
17 | position: absolute;
18 | right: 14px;
19 | bottom: 0;
20 | padding: 0 1px;
21 | background: @bg;
22 | content: '...';
23 | }
24 | &::after {
25 | position: absolute;
26 | right: 14px;
27 | width: 1em;
28 | height: 1em;
29 | margin-top: 0.2em;
30 | background: white;
31 | content: '';
32 | }
33 | }
34 |
35 | // mixins for clearfix
36 | // ------------------------
37 | .clearfix() {
38 | zoom: 1;
39 | &::before,
40 | &::after {
41 | content: ' ';
42 | display: table;
43 | }
44 | &::after {
45 | clear: both;
46 | height: 0;
47 | font-size: 0;
48 | visibility: hidden;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/utils.test.js:
--------------------------------------------------------------------------------
1 | import { fixedZero, isUrl } from './utils';
2 |
3 | describe('fixedZero tests', () => {
4 | it('should not pad large numbers', () => {
5 | expect(fixedZero(10)).toEqual(10);
6 | expect(fixedZero(11)).toEqual(11);
7 | expect(fixedZero(15)).toEqual(15);
8 | expect(fixedZero(20)).toEqual(20);
9 | expect(fixedZero(100)).toEqual(100);
10 | expect(fixedZero(1000)).toEqual(1000);
11 | });
12 |
13 | it('should pad single digit numbers and return them as string', () => {
14 | expect(fixedZero(0)).toEqual('00');
15 | expect(fixedZero(1)).toEqual('01');
16 | expect(fixedZero(2)).toEqual('02');
17 | expect(fixedZero(3)).toEqual('03');
18 | expect(fixedZero(4)).toEqual('04');
19 | expect(fixedZero(5)).toEqual('05');
20 | expect(fixedZero(6)).toEqual('06');
21 | expect(fixedZero(7)).toEqual('07');
22 | expect(fixedZero(8)).toEqual('08');
23 | expect(fixedZero(9)).toEqual('09');
24 | });
25 | });
26 |
27 | describe('isUrl tests', () => {
28 | it('should return false for invalid and corner case inputs', () => {
29 | expect(isUrl([])).toBeFalsy();
30 | expect(isUrl({})).toBeFalsy();
31 | expect(isUrl(false)).toBeFalsy();
32 | expect(isUrl(true)).toBeFalsy();
33 | expect(isUrl(NaN)).toBeFalsy();
34 | expect(isUrl(null)).toBeFalsy();
35 | expect(isUrl(undefined)).toBeFalsy();
36 | expect(isUrl()).toBeFalsy();
37 | expect(isUrl('')).toBeFalsy();
38 | });
39 |
40 | it('should return false for invalid URLs', () => {
41 | expect(isUrl('foo')).toBeFalsy();
42 | expect(isUrl('bar')).toBeFalsy();
43 | expect(isUrl('bar/test')).toBeFalsy();
44 | expect(isUrl('http:/example.com/')).toBeFalsy();
45 | expect(isUrl('ttp://example.com/')).toBeFalsy();
46 | });
47 |
48 | it('should return true for valid URLs', () => {
49 | expect(isUrl('http://example.com/')).toBeTruthy();
50 | expect(isUrl('https://example.com/')).toBeTruthy();
51 | expect(isUrl('http://example.com/test/123')).toBeTruthy();
52 | expect(isUrl('https://example.com/test/123')).toBeTruthy();
53 | expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
54 | expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
55 | expect(isUrl('http://www.example.com/')).toBeTruthy();
56 | expect(isUrl('https://www.example.com/')).toBeTruthy();
57 | expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
58 | expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
59 | expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
60 | expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/tests/run-tests.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const { spawn } = require('child_process');
3 | const { kill } = require('cross-port-killer');
4 |
5 | const env = Object.create(process.env);
6 | env.BROWSER = 'none';
7 | env.TEST = true;
8 | // flag to prevent multiple test
9 | let once = false;
10 |
11 | const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
12 | env,
13 | });
14 |
15 | startServer.stderr.on('data', data => {
16 | // eslint-disable-next-line
17 | console.log(data.toString());
18 | });
19 |
20 | startServer.on('exit', () => {
21 | kill(process.env.PORT || 8000);
22 | });
23 |
24 | console.log('Starting development server for e2e tests...');
25 | startServer.stdout.on('data', data => {
26 | console.log(data.toString());
27 | // hack code , wait umi
28 | if (
29 | (!once && data.toString().indexOf('Compiled successfully') >= 0) ||
30 | data.toString().indexOf('Theme generated successfully') >= 0
31 | ) {
32 | // eslint-disable-next-line
33 | once = true;
34 | console.log('Development server is started, ready to run tests.');
35 | const testCmd = spawn(
36 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
37 | ['test', '--', '--maxWorkers=1', '--runInBand'],
38 | {
39 | stdio: 'inherit',
40 | }
41 | );
42 | testCmd.on('exit', code => {
43 | startServer.kill();
44 | process.exit(code);
45 | });
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build/dist",
4 | "module": "esnext",
5 | "target": "es2016",
6 | "lib": ["es6", "dom"],
7 | "sourceMap": true,
8 | "baseUrl": ".",
9 | "jsx": "react",
10 | "allowSyntheticDefaultImports": true,
11 | "moduleResolution": "node",
12 | "rootDirs": ["/src", "/test", "/mock","./typings"],
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": true,
15 | "suppressImplicitAnyIndexErrors": true,
16 | "noUnusedLocals": true,
17 | "allowJs": true,
18 | "experimentalDecorators": true,
19 | "paths": {
20 | "@/*": ["./src/*"]
21 | }
22 | },
23 | "include": ["./src"],
24 | "exclude": [
25 | "node_modules",
26 | "build",
27 | "scripts",
28 | "acceptance-tests",
29 | "webpack",
30 | "jest",
31 | "src/setupTests.ts",
32 | "tslint:latest",
33 | "tslint-config-prettier"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"],
3 | "rules": {
4 | "no-var-requires": false,
5 | "no-submodule-imports": false,
6 | "object-literal-sort-keys": false,
7 | "jsx-no-lambda": false,
8 | "no-implicit-dependencies": false,
9 | "no-console": false
10 | }
11 | }
12 |
--------------------------------------------------------------------------------