@@ -0,0 +1,16 @@ | |||
# http://editorconfig.org | |||
root = true | |||
[*] | |||
indent_style = space | |||
indent_size = 2 | |||
end_of_line = lf | |||
charset = utf-8 | |||
trim_trailing_whitespace = true | |||
insert_final_newline = true | |||
[*.md] | |||
trim_trailing_whitespace = false | |||
[Makefile] | |||
indent_style = tab |
@@ -0,0 +1,8 @@ | |||
/lambda/ | |||
/scripts | |||
/config | |||
.history | |||
public | |||
dist | |||
.umi | |||
mock |
@@ -0,0 +1,8 @@ | |||
module.exports = { | |||
extends: [require.resolve('@umijs/fabric/dist/eslint')], | |||
globals: { | |||
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, | |||
page: true, | |||
REACT_APP_ENV: true, | |||
}, | |||
}; |
@@ -0,0 +1,40 @@ | |||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||
# dependencies | |||
**/node_modules | |||
# roadhog-api-doc ignore | |||
/src/utils/request-temp.js | |||
_roadhog-api-doc | |||
# production | |||
/dist | |||
/.vscode | |||
# misc | |||
.DS_Store | |||
npm-debug.log* | |||
yarn-error.log | |||
/coverage | |||
.idea | |||
yarn.lock | |||
package-lock.json | |||
*bak | |||
.vscode | |||
# visual studio code | |||
.history | |||
*.log | |||
functions/* | |||
.temp/** | |||
# umi | |||
.umi | |||
.umi-production | |||
# screenshot | |||
screenshot | |||
.firebase | |||
.eslintcache | |||
build |
@@ -0,0 +1,23 @@ | |||
**/*.svg | |||
package.json | |||
.umi | |||
.umi-production | |||
/dist | |||
.dockerignore | |||
.DS_Store | |||
.eslintignore | |||
*.png | |||
*.toml | |||
docker | |||
.editorconfig | |||
Dockerfile* | |||
.gitignore | |||
.prettierignore | |||
LICENSE | |||
.eslintcache | |||
*.lock | |||
yarn-error.log | |||
.history | |||
CNAME | |||
/build | |||
/public |
@@ -0,0 +1,5 @@ | |||
const fabric = require('@umijs/fabric'); | |||
module.exports = { | |||
...fabric.prettier, | |||
}; |
@@ -0,0 +1,5 @@ | |||
const fabric = require('@umijs/fabric'); | |||
module.exports = { | |||
...fabric.stylelint, | |||
}; |
@@ -0,0 +1,57 @@ | |||
# Ant Design Pro | |||
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use. | |||
## Environment Prepare | |||
Install `node_modules`: | |||
```bash | |||
npm install | |||
``` | |||
or | |||
```bash | |||
yarn | |||
``` | |||
## Provided Scripts | |||
Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test. | |||
Scripts provided in `package.json`. It's safe to modify or add additional script: | |||
### Start project | |||
```bash | |||
npm start | |||
``` | |||
### Build project | |||
```bash | |||
npm run build | |||
``` | |||
### Check code style | |||
```bash | |||
npm run lint | |||
``` | |||
You can also use script to auto fix some lint error: | |||
```bash | |||
npm run lint:fix | |||
``` | |||
### Test code | |||
```bash | |||
npm test | |||
``` | |||
## More | |||
You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro). |
@@ -0,0 +1,15 @@ | |||
// https://umijs.org/config/ | |||
import { defineConfig } from 'umi'; | |||
export default defineConfig({ | |||
plugins: [ | |||
// https://github.com/zthxxx/react-dev-inspector | |||
'react-dev-inspector/plugins/umi/react-inspector', | |||
], | |||
// https://github.com/zthxxx/react-dev-inspector#inspector-loader-props | |||
inspectorConfig: { | |||
exclude: [], | |||
babelPlugins: [], | |||
babelOptions: {}, | |||
}, | |||
}); |
@@ -0,0 +1,336 @@ | |||
// https://umijs.org/config/ | |||
import { defineConfig } from 'umi'; | |||
import defaultSettings from './defaultSettings'; | |||
import proxy from './proxy'; | |||
const { REACT_APP_ENV } = process.env; | |||
export default defineConfig({ | |||
hash: true, | |||
antd: {}, | |||
dva: { | |||
hmr: true, | |||
}, | |||
history: { | |||
type: 'browser', | |||
}, | |||
locale: { | |||
// default zh-CN | |||
default: 'zh-CN', | |||
antd: true, | |||
// default true, when it is true, will use `navigator.language` overwrite default | |||
baseNavigator: true, | |||
}, | |||
dynamicImport: { | |||
loading: '@/components/PageLoading/index', | |||
}, | |||
targets: { | |||
ie: 11, | |||
}, | |||
// umi routes: https://umijs.org/docs/routing | |||
routes: [ | |||
{ | |||
path: '/', | |||
component: '../layouts/BlankLayout', | |||
routes: [ | |||
{ | |||
path: '/user', | |||
component: '../layouts/UserLayout', | |||
routes: [ | |||
{ | |||
path: '/user/login', | |||
name: 'login', | |||
component: './User/login', | |||
}, | |||
{ | |||
path: '/user', | |||
redirect: '/user/login', | |||
}, | |||
{ | |||
name: 'register-result', | |||
icon: 'smile', | |||
path: '/user/register-result', | |||
component: './user/register-result', | |||
}, | |||
{ | |||
name: 'register', | |||
icon: 'smile', | |||
path: '/user/register', | |||
component: './user/register', | |||
}, | |||
{ | |||
component: '404', | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/', | |||
component: '../layouts/BasicLayout', | |||
Routes: ['src/pages/Authorized'], | |||
authority: ['admin', 'user'], | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/dashboard/analysis', | |||
}, | |||
{ | |||
path: '/dashboard', | |||
name: 'dashboard', | |||
icon: 'dashboard', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/dashboard/analysis', | |||
}, | |||
{ | |||
name: 'analysis', | |||
icon: 'smile', | |||
path: '/dashboard/analysis', | |||
component: './dashboard/analysis', | |||
}, | |||
{ | |||
name: 'monitor', | |||
icon: 'smile', | |||
path: '/dashboard/monitor', | |||
component: './dashboard/monitor', | |||
}, | |||
{ | |||
name: 'workplace', | |||
icon: 'smile', | |||
path: '/dashboard/workplace', | |||
component: './dashboard/workplace', | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/form', | |||
icon: 'form', | |||
name: 'form', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/form/basic-form', | |||
}, | |||
{ | |||
name: 'basic-form', | |||
icon: 'smile', | |||
path: '/form/basic-form', | |||
component: './form/basic-form', | |||
}, | |||
{ | |||
name: 'step-form', | |||
icon: 'smile', | |||
path: '/form/step-form', | |||
component: './form/step-form', | |||
}, | |||
{ | |||
name: 'advanced-form', | |||
icon: 'smile', | |||
path: '/form/advanced-form', | |||
component: './form/advanced-form', | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/list', | |||
icon: 'table', | |||
name: 'list', | |||
routes: [ | |||
{ | |||
path: '/list/search', | |||
name: 'search-list', | |||
component: './list/search', | |||
routes: [ | |||
{ | |||
path: '/list/search', | |||
redirect: '/list/search/articles', | |||
}, | |||
{ | |||
name: 'articles', | |||
icon: 'smile', | |||
path: '/list/search/articles', | |||
component: './list/search/articles', | |||
}, | |||
{ | |||
name: 'projects', | |||
icon: 'smile', | |||
path: '/list/search/projects', | |||
component: './list/search/projects', | |||
}, | |||
{ | |||
name: 'applications', | |||
icon: 'smile', | |||
path: '/list/search/applications', | |||
component: './list/search/applications', | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/', | |||
redirect: '/list/table-list', | |||
}, | |||
{ | |||
name: 'table-list', | |||
icon: 'smile', | |||
path: '/list/table-list', | |||
component: './list/table-list', | |||
}, | |||
{ | |||
name: 'basic-list', | |||
icon: 'smile', | |||
path: '/list/basic-list', | |||
component: './list/basic-list', | |||
}, | |||
{ | |||
name: 'card-list', | |||
icon: 'smile', | |||
path: '/list/card-list', | |||
component: './list/card-list', | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/profile', | |||
name: 'profile', | |||
icon: 'profile', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/profile/basic', | |||
}, | |||
{ | |||
name: 'basic', | |||
icon: 'smile', | |||
path: '/profile/basic', | |||
component: './profile/basic', | |||
}, | |||
{ | |||
name: 'advanced', | |||
icon: 'smile', | |||
path: '/profile/advanced', | |||
component: './profile/advanced', | |||
}, | |||
], | |||
}, | |||
{ | |||
name: 'result', | |||
icon: 'CheckCircleOutlined', | |||
path: '/result', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/result/success', | |||
}, | |||
{ | |||
name: 'success', | |||
icon: 'smile', | |||
path: '/result/success', | |||
component: './result/success', | |||
}, | |||
{ | |||
name: 'fail', | |||
icon: 'smile', | |||
path: '/result/fail', | |||
component: './result/fail', | |||
}, | |||
], | |||
}, | |||
{ | |||
name: 'exception', | |||
icon: 'warning', | |||
path: '/exception', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/exception/403', | |||
}, | |||
{ | |||
name: '403', | |||
icon: 'smile', | |||
path: '/exception/403', | |||
component: './exception/403', | |||
}, | |||
{ | |||
name: '404', | |||
icon: 'smile', | |||
path: '/exception/404', | |||
component: './exception/404', | |||
}, | |||
{ | |||
name: '500', | |||
icon: 'smile', | |||
path: '/exception/500', | |||
component: './exception/500', | |||
}, | |||
], | |||
}, | |||
{ | |||
name: 'account', | |||
icon: 'user', | |||
path: '/account', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/account/center', | |||
}, | |||
{ | |||
name: 'center', | |||
icon: 'smile', | |||
path: '/account/center', | |||
component: './account/center', | |||
}, | |||
{ | |||
name: 'settings', | |||
icon: 'smile', | |||
path: '/account/settings', | |||
component: './account/settings', | |||
}, | |||
], | |||
}, | |||
{ | |||
name: 'editor', | |||
icon: 'highlight', | |||
path: '/editor', | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/editor/flow', | |||
}, | |||
{ | |||
name: 'flow', | |||
icon: 'smile', | |||
path: '/editor/flow', | |||
component: './editor/flow', | |||
}, | |||
{ | |||
name: 'mind', | |||
icon: 'smile', | |||
path: '/editor/mind', | |||
component: './editor/mind', | |||
}, | |||
{ | |||
name: 'koni', | |||
icon: 'smile', | |||
path: '/editor/koni', | |||
component: './editor/koni', | |||
}, | |||
], | |||
}, | |||
{ | |||
component: '404', | |||
}, | |||
], | |||
}, | |||
], | |||
}, | |||
], | |||
// Theme for antd: https://ant.design/docs/react/customize-theme-cn | |||
theme: { | |||
'primary-color': defaultSettings.primaryColor, | |||
}, | |||
title: false, | |||
ignoreMomentLocale: true, | |||
proxy: proxy[REACT_APP_ENV || 'dev'], | |||
manifest: { | |||
basePath: '/', | |||
}, | |||
esbuild: {}, | |||
}); |
@@ -0,0 +1,23 @@ | |||
import { Settings as ProSettings } from '@ant-design/pro-layout'; | |||
type DefaultSettings = Partial<ProSettings> & { | |||
pwa: boolean; | |||
}; | |||
const proSettings: DefaultSettings = { | |||
navTheme: 'dark', | |||
// 拂晓蓝 | |||
primaryColor: '#1890ff', | |||
layout: 'side', | |||
contentWidth: 'Fluid', | |||
fixedHeader: false, | |||
fixSiderbar: true, | |||
colorWeak: false, | |||
title: 'Ant Design Pro', | |||
pwa: false, | |||
iconfontUrl: '', | |||
}; | |||
export type { DefaultSettings }; | |||
export default proSettings; |
@@ -0,0 +1,30 @@ | |||
/** | |||
* 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 | |||
* The agent cannot take effect in the production environment | |||
* so there is no configuration of the production environment | |||
* For details, please see | |||
* https://pro.ant.design/docs/deploy | |||
*/ | |||
export default { | |||
dev: { | |||
'/api/': { | |||
target: 'https://preview.pro.ant.design', | |||
changeOrigin: true, | |||
pathRewrite: { '^': '' }, | |||
}, | |||
}, | |||
test: { | |||
'/api/': { | |||
target: 'https://preview.pro.ant.design', | |||
changeOrigin: true, | |||
pathRewrite: { '^': '' }, | |||
}, | |||
}, | |||
pre: { | |||
'/api/': { | |||
target: 'your pre url', | |||
changeOrigin: true, | |||
pathRewrite: { '^': '' }, | |||
}, | |||
}, | |||
}; |
@@ -0,0 +1,73 @@ | |||
export default [ | |||
{ | |||
path: '/', | |||
component: '../layouts/BlankLayout', | |||
routes: [ | |||
{ | |||
path: '/user', | |||
component: '../layouts/UserLayout', | |||
routes: [ | |||
{ | |||
name: 'login', | |||
path: '/user/login', | |||
component: './User/login', | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/', | |||
component: '../layouts/SecurityLayout', | |||
routes: [ | |||
{ | |||
path: '/', | |||
component: '../layouts/BasicLayout', | |||
authority: ['admin', 'user'], | |||
routes: [ | |||
{ | |||
path: '/', | |||
redirect: '/welcome', | |||
}, | |||
{ | |||
path: '/welcome', | |||
name: 'welcome', | |||
icon: 'smile', | |||
component: './Welcome', | |||
}, | |||
{ | |||
path: '/admin', | |||
name: 'admin', | |||
icon: 'crown', | |||
component: './Admin', | |||
authority: ['admin'], | |||
routes: [ | |||
{ | |||
path: '/admin/sub-page', | |||
name: 'sub-page', | |||
icon: 'smile', | |||
component: './Welcome', | |||
authority: ['admin'], | |||
}, | |||
], | |||
}, | |||
{ | |||
name: 'list.table-list', | |||
icon: 'table', | |||
path: '/list', | |||
component: './TableList', | |||
}, | |||
{ | |||
component: './404', | |||
}, | |||
], | |||
}, | |||
{ | |||
component: './404', | |||
}, | |||
], | |||
}, | |||
], | |||
}, | |||
{ | |||
component: './404', | |||
}, | |||
]; |
@@ -0,0 +1,9 @@ | |||
module.exports = { | |||
testURL: 'http://localhost:8000', | |||
testEnvironment: './tests/PuppeteerEnvironment', | |||
verbose: false, | |||
globals: { | |||
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, | |||
localStorage: null, | |||
}, | |||
}; |
@@ -0,0 +1,10 @@ | |||
{ | |||
"compilerOptions": { | |||
"emitDecoratorMetadata": true, | |||
"experimentalDecorators": true, | |||
"baseUrl": ".", | |||
"paths": { | |||
"@/*": ["./src/*"] | |||
} | |||
} | |||
} |
@@ -0,0 +1,171 @@ | |||
// eslint-disable-next-line import/no-extraneous-dependencies | |||
import { Request, Response } from 'express'; | |||
import { parse } from 'url'; | |||
import { TableListItem, TableListParams } from '@/pages/TableList/data'; | |||
// mock tableListDataSource | |||
const genList = (current: number, pageSize: number) => { | |||
const tableListDataSource: TableListItem[] = []; | |||
for (let i = 0; i < pageSize; i += 1) { | |||
const index = (current - 1) * 10 + i; | |||
tableListDataSource.push({ | |||
key: index, | |||
disabled: i % 6 === 0, | |||
href: 'https://ant.design', | |||
avatar: [ | |||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', | |||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', | |||
][i % 2], | |||
name: `TradeCode ${index}`, | |||
owner: '曲丽丽', | |||
desc: '这是一段描述', | |||
callNo: Math.floor(Math.random() * 1000), | |||
status: Math.floor(Math.random() * 10) % 4, | |||
updatedAt: new Date(), | |||
createdAt: new Date(), | |||
progress: Math.ceil(Math.random() * 100), | |||
}); | |||
} | |||
tableListDataSource.reverse(); | |||
return tableListDataSource; | |||
}; | |||
let tableListDataSource = genList(1, 100); | |||
function getRule(req: Request, res: Response, u: string) { | |||
let realUrl = u; | |||
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { | |||
realUrl = req.url; | |||
} | |||
const { current = 1, pageSize = 10 } = req.query; | |||
const params = (parse(realUrl, true).query as unknown) as TableListParams; | |||
let dataSource = [...tableListDataSource].slice( | |||
((current as number) - 1) * (pageSize as number), | |||
(current as number) * (pageSize as number), | |||
); | |||
const sorter = JSON.parse(params.sorter as any); | |||
if (sorter) { | |||
dataSource = dataSource.sort((prev, next) => { | |||
let sortNumber = 0; | |||
Object.keys(sorter).forEach((key) => { | |||
if (sorter[key] === 'descend') { | |||
if (prev[key] - next[key] > 0) { | |||
sortNumber += -1; | |||
} else { | |||
sortNumber += 1; | |||
} | |||
return; | |||
} | |||
if (prev[key] - next[key] > 0) { | |||
sortNumber += 1; | |||
} else { | |||
sortNumber += -1; | |||
} | |||
}); | |||
return sortNumber; | |||
}); | |||
} | |||
if (params.filter) { | |||
const filter = JSON.parse(params.filter as any) as { | |||
[key: string]: string[]; | |||
}; | |||
if (Object.keys(filter).length > 0) { | |||
dataSource = dataSource.filter((item) => { | |||
return Object.keys(filter).some((key) => { | |||
if (!filter[key]) { | |||
return true; | |||
} | |||
if (filter[key].includes(`${item[key]}`)) { | |||
return true; | |||
} | |||
return false; | |||
}); | |||
}); | |||
} | |||
} | |||
if (params.name) { | |||
dataSource = dataSource.filter((data) => data.name.includes(params.name || '')); | |||
} | |||
const result = { | |||
data: dataSource, | |||
total: tableListDataSource.length, | |||
success: true, | |||
pageSize, | |||
current: parseInt(`${params.currentPage}`, 10) || 1, | |||
}; | |||
return res.json(result); | |||
} | |||
function postRule(req: Request, res: Response, u: string, b: Request) { | |||
let realUrl = u; | |||
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { | |||
realUrl = req.url; | |||
} | |||
const body = (b && b.body) || req.body; | |||
const { method, name, desc, key } = body; | |||
switch (method) { | |||
/* eslint no-case-declarations:0 */ | |||
case 'delete': | |||
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1); | |||
break; | |||
case 'post': | |||
(() => { | |||
const i = Math.ceil(Math.random() * 10000); | |||
const newRule = { | |||
key: tableListDataSource.length, | |||
href: 'https://ant.design', | |||
avatar: [ | |||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', | |||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', | |||
][i % 2], | |||
name, | |||
owner: '曲丽丽', | |||
desc, | |||
callNo: Math.floor(Math.random() * 1000), | |||
status: Math.floor(Math.random() * 10) % 2, | |||
updatedAt: new Date(), | |||
createdAt: new Date(), | |||
progress: Math.ceil(Math.random() * 100), | |||
}; | |||
tableListDataSource.unshift(newRule); | |||
return res.json(newRule); | |||
})(); | |||
return; | |||
case 'update': | |||
(() => { | |||
let newRule = {}; | |||
tableListDataSource = tableListDataSource.map((item) => { | |||
if (item.key === key) { | |||
newRule = { ...item, desc, name }; | |||
return { ...item, desc, name }; | |||
} | |||
return item; | |||
}); | |||
return res.json(newRule); | |||
})(); | |||
return; | |||
default: | |||
break; | |||
} | |||
const result = { | |||
list: tableListDataSource, | |||
pagination: { | |||
total: tableListDataSource.length, | |||
}, | |||
}; | |||
res.json(result); | |||
} | |||
export default { | |||
'GET /api/rule': getRule, | |||
'POST /api/rule': postRule, | |||
}; |
@@ -0,0 +1,105 @@ | |||
import { Request, Response } from 'express'; | |||
const getNotices = (req: Request, res: Response) => { | |||
res.json([ | |||
{ | |||
id: '000000001', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', | |||
title: '你收到了 14 份新周报', | |||
datetime: '2017-08-09', | |||
type: 'notification', | |||
}, | |||
{ | |||
id: '000000002', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', | |||
title: '你推荐的 曲妮妮 已通过第三轮面试', | |||
datetime: '2017-08-08', | |||
type: 'notification', | |||
}, | |||
{ | |||
id: '000000003', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', | |||
title: '这种模板可以区分多种通知类型', | |||
datetime: '2017-08-07', | |||
read: true, | |||
type: 'notification', | |||
}, | |||
{ | |||
id: '000000004', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', | |||
title: '左侧图标用于区分不同的类型', | |||
datetime: '2017-08-07', | |||
type: 'notification', | |||
}, | |||
{ | |||
id: '000000005', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', | |||
title: '内容不要超过两行字,超出时自动截断', | |||
datetime: '2017-08-07', | |||
type: 'notification', | |||
}, | |||
{ | |||
id: '000000006', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', | |||
title: '曲丽丽 评论了你', | |||
description: '描述信息描述信息描述信息', | |||
datetime: '2017-08-07', | |||
type: 'message', | |||
clickClose: true, | |||
}, | |||
{ | |||
id: '000000007', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', | |||
title: '朱偏右 回复了你', | |||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', | |||
datetime: '2017-08-07', | |||
type: 'message', | |||
clickClose: true, | |||
}, | |||
{ | |||
id: '000000008', | |||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', | |||
title: '标题', | |||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', | |||
datetime: '2017-08-07', | |||
type: 'message', | |||
clickClose: true, | |||
}, | |||
{ | |||
id: '000000009', | |||
title: '任务名称', | |||
description: '任务需要在 2017-01-12 20:00 前启动', | |||
extra: '未开始', | |||
status: 'todo', | |||
type: 'event', | |||
}, | |||
{ | |||
id: '000000010', | |||
title: '第三方紧急代码变更', | |||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', | |||
extra: '马上到期', | |||
status: 'urgent', | |||
type: 'event', | |||
}, | |||
{ | |||
id: '000000011', | |||
title: '信息安全考试', | |||
description: '指派竹尔于 2017-01-09 前完成更新并发布', | |||
extra: '已耗时 8 天', | |||
status: 'doing', | |||
type: 'event', | |||
}, | |||
{ | |||
id: '000000012', | |||
title: 'ABCD 版本发布', | |||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', | |||
extra: '进行中', | |||
status: 'processing', | |||
type: 'event', | |||
}, | |||
]); | |||
}; | |||
export default { | |||
'GET /api/notices': getNotices, | |||
}; |
@@ -0,0 +1,5 @@ | |||
export default { | |||
'/api/auth_routes': { | |||
'/form/advanced-form': { authority: ['admin', 'user'] }, | |||
}, | |||
}; |
@@ -0,0 +1,165 @@ | |||
import { Request, Response } from 'express'; | |||
const waitTime = (time: number = 100) => { | |||
return new Promise((resolve) => { | |||
setTimeout(() => { | |||
resolve(true); | |||
}, time); | |||
}); | |||
}; | |||
async function getFakeCaptcha(req: Request, res: Response) { | |||
await waitTime(2000); | |||
return res.json('captcha-xxx'); | |||
} | |||
// 代码中会兼容本地 service mock 以及部署站点的静态数据 | |||
export default { | |||
// 支持值为 Object 和 Array | |||
'GET /api/currentUser': { | |||
name: 'Serati Ma', | |||
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png', | |||
userid: '00000001', | |||
email: 'antdesign@alipay.com', | |||
signature: '海纳百川,有容乃大', | |||
title: '交互专家', | |||
group: '蚂蚁集团-某某某事业群-某某平台部-某某技术部-UED', | |||
tags: [ | |||
{ | |||
key: '0', | |||
label: '很有想法的', | |||
}, | |||
{ | |||
key: '1', | |||
label: '专注设计', | |||
}, | |||
{ | |||
key: '2', | |||
label: '辣~', | |||
}, | |||
{ | |||
key: '3', | |||
label: '大长腿', | |||
}, | |||
{ | |||
key: '4', | |||
label: '川妹子', | |||
}, | |||
{ | |||
key: '5', | |||
label: '海纳百川', | |||
}, | |||
], | |||
notifyCount: 12, | |||
unreadCount: 11, | |||
country: 'China', | |||
geographic: { | |||
province: { | |||
label: '浙江省', | |||
key: '330000', | |||
}, | |||
city: { | |||
label: '杭州市', | |||
key: '330100', | |||
}, | |||
}, | |||
address: '西湖区工专路 77 号', | |||
phone: '0752-268888888', | |||
}, | |||
// GET POST 可省略 | |||
'GET /api/users': [ | |||
{ | |||
key: '1', | |||
name: 'John Brown', | |||
age: 32, | |||
address: 'New York No. 1 Lake Park', | |||
}, | |||
{ | |||
key: '2', | |||
name: 'Jim Green', | |||
age: 42, | |||
address: 'London No. 1 Lake Park', | |||
}, | |||
{ | |||
key: '3', | |||
name: 'Joe Black', | |||
age: 32, | |||
address: 'Sidney No. 1 Lake Park', | |||
}, | |||
], | |||
'POST /api/login/account': async (req: Request, res: Response) => { | |||
const { password, userName, type } = req.body; | |||
await waitTime(2000); | |||
if (password === 'ant.design' && userName === 'admin') { | |||
res.send({ | |||
status: 'ok', | |||
type, | |||
currentAuthority: 'admin', | |||
}); | |||
return; | |||
} | |||
if (password === 'ant.design' && userName === 'user') { | |||
res.send({ | |||
status: 'ok', | |||
type, | |||
currentAuthority: 'user', | |||
}); | |||
return; | |||
} | |||
if (type === 'mobile') { | |||
res.send({ | |||
status: 'ok', | |||
type, | |||
currentAuthority: 'admin', | |||
}); | |||
return; | |||
} | |||
res.send({ | |||
status: 'error', | |||
type, | |||
currentAuthority: 'guest', | |||
}); | |||
}, | |||
'POST /api/register': (req: Request, res: Response) => { | |||
res.send({ status: 'ok', currentAuthority: 'user' }); | |||
}, | |||
'GET /api/500': (req: Request, res: Response) => { | |||
res.status(500).send({ | |||
timestamp: 1513932555104, | |||
status: 500, | |||
error: 'error', | |||
message: 'error', | |||
path: '/base/category/list', | |||
}); | |||
}, | |||
'GET /api/404': (req: Request, res: Response) => { | |||
res.status(404).send({ | |||
timestamp: 1513932643431, | |||
status: 404, | |||
error: 'Not Found', | |||
message: 'No message available', | |||
path: '/base/category/list/2121212', | |||
}); | |||
}, | |||
'GET /api/403': (req: Request, res: Response) => { | |||
res.status(403).send({ | |||
timestamp: 1513932555104, | |||
status: 403, | |||
error: 'Unauthorized', | |||
message: 'Unauthorized', | |||
path: '/base/category/list', | |||
}); | |||
}, | |||
'GET /api/401': (req: Request, res: Response) => { | |||
res.status(401).send({ | |||
timestamp: 1513932555104, | |||
status: 401, | |||
error: 'Unauthorized', | |||
message: 'Unauthorized', | |||
path: '/base/category/list', | |||
}); | |||
}, | |||
'GET /api/login/captcha': getFakeCaptcha, | |||
}; |
@@ -0,0 +1,120 @@ | |||
{ | |||
"name": "ant-design-pro", | |||
"version": "4.5.0", | |||
"private": true, | |||
"description": "An out-of-box UI solution for enterprise applications", | |||
"scripts": { | |||
"analyze": "cross-env ANALYZE=1 umi build", | |||
"build": "umi build", | |||
"deploy": "npm run site && npm run gh-pages", | |||
"dev": "npm run start:dev", | |||
"fetch:blocks": "pro fetch-blocks && npm run prettier", | |||
"gh-pages": "gh-pages -d dist", | |||
"i18n-remove": "pro i18n-remove --locale=zh-CN --write", | |||
"postinstall": "umi g tmp", | |||
"lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier", | |||
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", | |||
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style", | |||
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", | |||
"lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto", | |||
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", | |||
"precommit": "lint-staged", | |||
"prettier": "prettier -c --write \"src/**/*\"", | |||
"start": "cross-env UMI_ENV=dev umi dev", | |||
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev", | |||
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev", | |||
"start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev", | |||
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev", | |||
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev", | |||
"pretest": "node ./tests/beforeTest", | |||
"test": "umi test", | |||
"test:all": "node ./tests/run-tests.js", | |||
"test:component": "umi test ./src/components", | |||
"tsc": "tsc --noEmit" | |||
}, | |||
"lint-staged": { | |||
"**/*.less": "stylelint --syntax less", | |||
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", | |||
"**/*.{js,jsx,tsx,ts,less,md,json}": ["prettier --write"] | |||
}, | |||
"browserslist": ["> 1%", "last 2 versions", "not ie <= 10"], | |||
"dependencies": { | |||
"@ant-design/icons": "^4.0.0", | |||
"@ant-design/pro-descriptions": "^1.2.0", | |||
"@ant-design/pro-form": "^1.3.0", | |||
"@ant-design/pro-layout": "^6.9.0", | |||
"@ant-design/pro-table": "^2.17.0", | |||
"@antv/data-set": "^0.11.0", | |||
"@antv/l7": "^2.1.9", | |||
"@antv/l7-maps": "^2.1.9", | |||
"@antv/l7-react": "^2.1.9", | |||
"@types/lodash.debounce": "^4.0.6", | |||
"@types/lodash.isequal": "^4.5.5", | |||
"@umijs/route-utils": "^1.0.33", | |||
"antd": "^4.12.0", | |||
"bizcharts": "^3.5.3-beta.0", | |||
"bizcharts-plugin-slider": "^2.1.1-beta.1", | |||
"classnames": "^2.2.6", | |||
"dva": "^2.4.0", | |||
"gg-editor": "^2.0.2", | |||
"lodash": "^4.17.11", | |||
"lodash-decorators": "^6.0.0", | |||
"lodash.debounce": "^4.0.8", | |||
"lodash.isequal": "^4.5.0", | |||
"mockjs": "^1.0.1-beta3", | |||
"moment": "^2.25.3", | |||
"numeral": "^2.0.6", | |||
"nzh": "^1.0.3", | |||
"omit.js": "^2.0.2", | |||
"prop-types": "^15.5.10", | |||
"react": "^16.14.0", | |||
"react-dev-inspector": "^1.1.1", | |||
"react-dom": "^17.0.0", | |||
"react-fittext": "^1.0.0", | |||
"react-helmet-async": "^1.0.4", | |||
"react-router": "^4.3.1", | |||
"umi": "^3.2.14", | |||
"umi-request": "^1.0.8" | |||
}, | |||
"devDependencies": { | |||
"@ant-design/pro-cli": "^1.0.28", | |||
"@types/classnames": "^2.2.7", | |||
"@types/express": "^4.17.0", | |||
"@types/history": "^4.7.2", | |||
"@types/jest": "^26.0.0", | |||
"@types/lodash": "^4.14.144", | |||
"@types/react": "^17.0.0", | |||
"@types/react-dom": "^17.0.0", | |||
"@types/react-helmet": "^6.1.0", | |||
"@umijs/fabric": "^2.5.1", | |||
"@umijs/plugin-blocks": "^2.0.5", | |||
"@umijs/plugin-esbuild": "^1.0.1", | |||
"@umijs/preset-ant-design-pro": "^1.2.0", | |||
"@umijs/preset-react": "^1.4.8", | |||
"@umijs/yorkie": "^2.0.3", | |||
"carlo": "^0.9.46", | |||
"chalk": "^4.0.0", | |||
"cross-env": "^7.0.0", | |||
"cross-port-killer": "^1.1.1", | |||
"detect-installer": "^1.0.1", | |||
"enzyme": "^3.11.0", | |||
"eslint": "^7.1.0", | |||
"express": "^4.17.1", | |||
"gh-pages": "^3.0.0", | |||
"jsdom-global": "^3.0.2", | |||
"lint-staged": "^10.0.0", | |||
"mockjs": "^1.0.1-beta3", | |||
"prettier": "^2.0.1", | |||
"puppeteer-core": "^7.0.1", | |||
"stylelint": "^13.0.0", | |||
"typescript": "^4.0.3" | |||
}, | |||
"engines": { "node": ">=10.0.0" }, | |||
"checkFiles": [ | |||
"src/**/*.js*", | |||
"src/**/*.ts*", | |||
"src/**/*.less", | |||
"config/**/*.js*", | |||
"scripts/**/*.js" | |||
] | |||
} |
@@ -0,0 +1 @@ | |||
preview.pro.ant.design |
@@ -0,0 +1,5 @@ | |||
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg"> | |||
<g> | |||
<path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/> | |||
</g> | |||
</svg> |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg> |
@@ -0,0 +1,35 @@ | |||
import React from 'react'; | |||
import { Result } from 'antd'; | |||
import check from './CheckPermissions'; | |||
import type { IAuthorityType } from './CheckPermissions'; | |||
import type AuthorizedRoute from './AuthorizedRoute'; | |||
import type Secured from './Secured'; | |||
type AuthorizedProps = { | |||
authority: IAuthorityType; | |||
noMatch?: React.ReactNode; | |||
}; | |||
type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & { | |||
Secured: typeof Secured; | |||
check: typeof check; | |||
AuthorizedRoute: typeof AuthorizedRoute; | |||
}; | |||
const Authorized: React.FunctionComponent<AuthorizedProps> = ({ | |||
children, | |||
authority, | |||
noMatch = ( | |||
<Result | |||
status="403" | |||
title="403" | |||
subTitle="Sorry, you are not authorized to access this page." | |||
/> | |||
), | |||
}) => { | |||
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; | |||
const dom = check(authority, childrenRender, noMatch); | |||
return <>{dom}</>; | |||
}; | |||
export default Authorized as IAuthorizedType; |
@@ -0,0 +1,33 @@ | |||
import { Redirect, Route } from 'umi'; | |||
import React from 'react'; | |||
import Authorized from './Authorized'; | |||
import type { IAuthorityType } from './CheckPermissions'; | |||
type AuthorizedRouteProps = { | |||
currentAuthority: string; | |||
component: React.ComponentClass<any, any>; | |||
render: (props: any) => React.ReactNode; | |||
redirectPath: string; | |||
authority: IAuthorityType; | |||
}; | |||
const AuthorizedRoute: React.SFC<AuthorizedRouteProps> = ({ | |||
component: Component, | |||
render, | |||
authority, | |||
redirectPath, | |||
...rest | |||
}) => ( | |||
<Authorized | |||
authority={authority} | |||
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />} | |||
> | |||
<Route | |||
{...rest} | |||
render={(props: any) => (Component ? <Component {...props} /> : render(props))} | |||
/> | |||
</Authorized> | |||
); | |||
export default AuthorizedRoute; |
@@ -0,0 +1,79 @@ | |||
import React from 'react'; | |||
import { CURRENT } from './renderAuthorize'; | |||
// eslint-disable-next-line import/no-cycle | |||
import PromiseRender from './PromiseRender'; | |||
export type IAuthorityType = | |||
| undefined | |||
| string | |||
| string[] | |||
| Promise<boolean> | |||
| ((currentAuthority: string | string[]) => IAuthorityType); | |||
/** | |||
* 通用权限检查方法 Common check permissions method | |||
* | |||
* @param { 权限判定 | Permission judgment } authority | |||
* @param { 你的权限 | Your permission description } currentAuthority | |||
* @param { 通过的组件 | Passing components } target | |||
* @param { 未通过的组件 | no pass components } Exception | |||
*/ | |||
const checkPermissions = <T, K>( | |||
authority: IAuthorityType, | |||
currentAuthority: string | string[], | |||
target: T, | |||
Exception: K, | |||
): T | K | React.ReactNode => { | |||
// 没有判定权限.默认查看所有 | |||
// Retirement authority, return target; | |||
if (!authority) { | |||
return target; | |||
} | |||
// 数组处理 | |||
if (Array.isArray(authority)) { | |||
if (Array.isArray(currentAuthority)) { | |||
if (currentAuthority.some((item) => authority.includes(item))) { | |||
return target; | |||
} | |||
} else if (authority.includes(currentAuthority)) { | |||
return target; | |||
} | |||
return Exception; | |||
} | |||
// string 处理 | |||
if (typeof authority === 'string') { | |||
if (Array.isArray(currentAuthority)) { | |||
if (currentAuthority.some((item) => authority === item)) { | |||
return target; | |||
} | |||
} else if (authority === currentAuthority) { | |||
return target; | |||
} | |||
return Exception; | |||
} | |||
// Promise 处理 | |||
if (authority instanceof Promise) { | |||
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />; | |||
} | |||
// Function 处理 | |||
if (typeof authority === 'function') { | |||
const bool = authority(currentAuthority); | |||
// 函数执行后返回值是 Promise | |||
if (bool instanceof Promise) { | |||
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />; | |||
} | |||
if (bool) { | |||
return target; | |||
} | |||
return Exception; | |||
} | |||
throw new Error('unsupported parameters'); | |||
}; | |||
export { checkPermissions }; | |||
function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { | |||
return checkPermissions<T, K>(authority, CURRENT, target, Exception); | |||
} | |||
export default check; |
@@ -0,0 +1,96 @@ | |||
import React from 'react'; | |||
import { Spin } from 'antd'; | |||
import isEqual from 'lodash/isEqual'; | |||
import { isComponentClass } from './Secured'; | |||
// eslint-disable-next-line import/no-cycle | |||
type PromiseRenderProps<T, K> = { | |||
ok: T; | |||
error: K; | |||
promise: Promise<boolean>; | |||
}; | |||
type PromiseRenderState = { | |||
component: React.ComponentClass | React.FunctionComponent; | |||
}; | |||
export default class PromiseRender<T, K> extends React.Component< | |||
PromiseRenderProps<T, K>, | |||
PromiseRenderState | |||
> { | |||
state: PromiseRenderState = { | |||
component: () => null, | |||
}; | |||
componentDidMount(): void { | |||
this.setRenderComponent(this.props); | |||
} | |||
shouldComponentUpdate = ( | |||
nextProps: PromiseRenderProps<T, K>, | |||
nextState: PromiseRenderState, | |||
): boolean => { | |||
const { component } = this.state; | |||
if (!isEqual(nextProps, this.props)) { | |||
this.setRenderComponent(nextProps); | |||
} | |||
if (nextState.component !== component) return true; | |||
return false; | |||
}; | |||
// set render Component : ok or error | |||
setRenderComponent(props: PromiseRenderProps<T, K>): void { | |||
const ok = this.checkIsInstantiation(props.ok); | |||
const error = this.checkIsInstantiation(props.error); | |||
props.promise | |||
.then(() => { | |||
this.setState({ | |||
component: ok, | |||
}); | |||
return true; | |||
}) | |||
.catch(() => { | |||
this.setState({ | |||
component: error, | |||
}); | |||
}); | |||
} | |||
// Determine whether the incoming component has been instantiated | |||
// AuthorizedRoute is already instantiated | |||
// Authorized render is already instantiated, children is no instantiated | |||
// Secured is not instantiated | |||
checkIsInstantiation = ( | |||
target: React.ReactNode | React.ComponentClass, | |||
): React.FunctionComponent => { | |||
if (isComponentClass(target)) { | |||
const Target = target as React.ComponentClass; | |||
return (props: any) => <Target {...props} />; | |||
} | |||
if (React.isValidElement(target)) { | |||
return (props: any) => React.cloneElement(target, props); | |||
} | |||
return () => target as React.ReactNode & null; | |||
}; | |||
render() { | |||
const { component: Component } = this.state; | |||
const { ok, error, promise, ...rest } = this.props; | |||
return Component ? ( | |||
<Component {...rest} /> | |||
) : ( | |||
<div | |||
style={{ | |||
width: '100%', | |||
height: '100%', | |||
margin: 'auto', | |||
paddingTop: 50, | |||
textAlign: 'center', | |||
}} | |||
> | |||
<Spin size="large" /> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
import React from 'react'; | |||
import CheckPermissions from './CheckPermissions'; | |||
/** 默认不能访问任何页面 default is "NULL" */ | |||
const Exception403 = () => 403; | |||
export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { | |||
if (!component) return false; | |||
const proto = Object.getPrototypeOf(component); | |||
if (proto === React.Component || proto === Function.prototype) return true; | |||
return isComponentClass(proto); | |||
}; | |||
// Determine whether the incoming component has been instantiated | |||
// AuthorizedRoute is already instantiated | |||
// Authorized render is already instantiated, children is no instantiated | |||
// Secured is not instantiated | |||
const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { | |||
if (isComponentClass(target)) { | |||
const Target = target as React.ComponentClass; | |||
return (props: any) => <Target {...props} />; | |||
} | |||
if (React.isValidElement(target)) { | |||
return (props: any) => React.cloneElement(target, props); | |||
} | |||
return () => target; | |||
}; | |||
/** | |||
* 用于判断是否拥有权限访问此 view 权限 authority 支持传入 string, () => boolean | Promise e.g. 'user' 只有 user 用户能访问 | |||
* e.g. 'user,admin' user 和 admin 都能访问 e.g. ()=>boolean 返回true能访问,返回false不能访问 e.g. Promise then 能访问 | |||
* catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user | |||
* user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able | |||
* to visit, return false can not be accessed e.g. Promise then can not access the visit to catch | |||
* | |||
* @param {string | function | Promise} authority | |||
* @param {ReactNode} error 非必需参数 | |||
*/ | |||
const authorize = (authority: string, error?: React.ReactNode) => { | |||
/** | |||
* Conversion into a class 防止传入字符串时找不到staticContext造成报错 String parameters can cause staticContext | |||
* not found error | |||
*/ | |||
let classError: boolean | React.FunctionComponent = false; | |||
if (error) { | |||
classError = (() => error) as React.FunctionComponent; | |||
} | |||
if (!authority) { | |||
throw new Error('authority is required'); | |||
} | |||
return function decideAuthority(target: React.ComponentClass | React.ReactNode) { | |||
const component = CheckPermissions(authority, target, classError || Exception403); | |||
return checkIsInstantiation(component); | |||
}; | |||
}; | |||
export default authorize; |
@@ -0,0 +1,11 @@ | |||
import Authorized from './Authorized'; | |||
import Secured from './Secured'; | |||
import check from './CheckPermissions'; | |||
import renderAuthorize from './renderAuthorize'; | |||
Authorized.Secured = Secured; | |||
Authorized.check = check; | |||
const RenderAuthorize = renderAuthorize(Authorized); | |||
export default RenderAuthorize; |
@@ -0,0 +1,31 @@ | |||
/* eslint-disable eslint-comments/disable-enable-pair */ | |||
/* eslint-disable import/no-mutable-exports */ | |||
let CURRENT: string | string[] = 'NULL'; | |||
type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); | |||
/** | |||
* Use authority or getAuthority | |||
* | |||
* @param {string|()=>String} currentAuthority | |||
*/ | |||
const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( | |||
currentAuthority: CurrentAuthorityType, | |||
): T => { | |||
if (currentAuthority) { | |||
if (typeof currentAuthority === 'function') { | |||
CURRENT = currentAuthority(); | |||
} | |||
if ( | |||
Object.prototype.toString.call(currentAuthority) === '[object String]' || | |||
Array.isArray(currentAuthority) | |||
) { | |||
CURRENT = currentAuthority as string[]; | |||
} | |||
} else { | |||
CURRENT = 'NULL'; | |||
} | |||
return Authorized; | |||
}; | |||
export { CURRENT }; | |||
export default <T>(Authorized: T) => renderAuthorize<T>(Authorized); |
@@ -0,0 +1,93 @@ | |||
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; | |||
import { Avatar, Menu, Spin } from 'antd'; | |||
import React from 'react'; | |||
import type { ConnectProps } from 'umi'; | |||
import { history, connect } from 'umi'; | |||
import type { ConnectState } from '@/models/connect'; | |||
import type { CurrentUser } from '@/models/user'; | |||
import HeaderDropdown from '../HeaderDropdown'; | |||
import styles from './index.less'; | |||
export type GlobalHeaderRightProps = { | |||
currentUser?: CurrentUser; | |||
menu?: boolean; | |||
} & Partial<ConnectProps>; | |||
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> { | |||
onMenuClick = (event: { | |||
key: React.Key; | |||
keyPath: React.Key[]; | |||
item: React.ReactInstance; | |||
domEvent: React.MouseEvent<HTMLElement>; | |||
}) => { | |||
const { key } = event; | |||
if (key === 'logout') { | |||
const { dispatch } = this.props; | |||
if (dispatch) { | |||
dispatch({ | |||
type: 'login/logout', | |||
}); | |||
} | |||
return; | |||
} | |||
history.push(`/account/${key}`); | |||
}; | |||
render(): React.ReactNode { | |||
const { | |||
currentUser = { | |||
avatar: '', | |||
name: '', | |||
}, | |||
menu, | |||
} = this.props; | |||
const menuHeaderDropdown = ( | |||
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}> | |||
{menu && ( | |||
<Menu.Item key="center"> | |||
<UserOutlined /> | |||
个人中心 | |||
</Menu.Item> | |||
)} | |||
{menu && ( | |||
<Menu.Item key="settings"> | |||
<SettingOutlined /> | |||
个人设置 | |||
</Menu.Item> | |||
)} | |||
{menu && <Menu.Divider />} | |||
<Menu.Item key="logout"> | |||
<LogoutOutlined /> | |||
退出登录 | |||
</Menu.Item> | |||
</Menu> | |||
); | |||
return currentUser && currentUser.name ? ( | |||
<HeaderDropdown overlay={menuHeaderDropdown}> | |||
<span className={`${styles.action} ${styles.account}`}> | |||
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" /> | |||
<span className={`${styles.name} anticon`}>{currentUser.name}</span> | |||
</span> | |||
</HeaderDropdown> | |||
) : ( | |||
<span className={`${styles.action} ${styles.account}`}> | |||
<Spin | |||
size="small" | |||
style={{ | |||
marginLeft: 8, | |||
marginRight: 8, | |||
}} | |||
/> | |||
</span> | |||
); | |||
} | |||
} | |||
export default connect(({ user }: ConnectState) => ({ | |||
currentUser: user.currentUser, | |||
}))(AvatarDropdown); |
@@ -0,0 +1,168 @@ | |||
import React, { Component } from 'react'; | |||
import type { ConnectProps } from 'umi'; | |||
import { connect } from 'umi'; | |||
import { Tag, message } from 'antd'; | |||
import groupBy from 'lodash/groupBy'; | |||
import moment from 'moment'; | |||
import type { NoticeItem } from '@/models/global'; | |||
import type { CurrentUser } from '@/models/user'; | |||
import type { ConnectState } from '@/models/connect'; | |||
import NoticeIcon from '../NoticeIcon'; | |||
import styles from './index.less'; | |||
export type GlobalHeaderRightProps = { | |||
notices?: NoticeItem[]; | |||
currentUser?: CurrentUser; | |||
fetchingNotices?: boolean; | |||
onNoticeVisibleChange?: (visible: boolean) => void; | |||
onNoticeClear?: (tabName?: string) => void; | |||
} & Partial<ConnectProps>; | |||
class GlobalHeaderRight extends Component<GlobalHeaderRightProps> { | |||
componentDidMount() { | |||
const { dispatch } = this.props; | |||
if (dispatch) { | |||
dispatch({ | |||
type: 'global/fetchNotices', | |||
}); | |||
} | |||
} | |||
changeReadState = (clickedItem: NoticeItem): void => { | |||
const { id } = clickedItem; | |||
const { dispatch } = this.props; | |||
if (dispatch) { | |||
dispatch({ | |||
type: 'global/changeNoticeReadState', | |||
payload: id, | |||
}); | |||
} | |||
}; | |||
handleNoticeClear = (title: string, key: string) => { | |||
const { dispatch } = this.props; | |||
message.success(`${'清空了'} ${title}`); | |||
if (dispatch) { | |||
dispatch({ | |||
type: 'global/clearNotices', | |||
payload: key, | |||
}); | |||
} | |||
}; | |||
getNoticeData = (): Record<string, NoticeItem[]> => { | |||
const { notices = [] } = this.props; | |||
if (!notices || notices.length === 0 || !Array.isArray(notices)) { | |||
return {}; | |||
} | |||
const newNotices = notices.map((notice) => { | |||
const newNotice = { ...notice }; | |||
if (newNotice.datetime) { | |||
newNotice.datetime = moment(notice.datetime as string).fromNow(); | |||
} | |||
if (newNotice.id) { | |||
newNotice.key = newNotice.id; | |||
} | |||
if (newNotice.extra && newNotice.status) { | |||
const color = { | |||
todo: '', | |||
processing: 'blue', | |||
urgent: 'red', | |||
doing: 'gold', | |||
}[newNotice.status]; | |||
newNotice.extra = ( | |||
<Tag | |||
color={color} | |||
style={{ | |||
marginRight: 0, | |||
}} | |||
> | |||
{newNotice.extra} | |||
</Tag> | |||
); | |||
} | |||
return newNotice; | |||
}); | |||
return groupBy(newNotices, 'type'); | |||
}; | |||
getUnreadData = (noticeData: Record<string, NoticeItem[]>) => { | |||
const unreadMsg: Record<string, number> = {}; | |||
Object.keys(noticeData).forEach((key) => { | |||
const value = noticeData[key]; | |||
if (!unreadMsg[key]) { | |||
unreadMsg[key] = 0; | |||
} | |||
if (Array.isArray(value)) { | |||
unreadMsg[key] = value.filter((item) => !item.read).length; | |||
} | |||
}); | |||
return unreadMsg; | |||
}; | |||
render() { | |||
const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; | |||
const noticeData = this.getNoticeData(); | |||
const unreadMsg = this.getUnreadData(noticeData); | |||
return ( | |||
<NoticeIcon | |||
className={styles.action} | |||
count={currentUser && currentUser.unreadCount} | |||
onItemClick={(item) => { | |||
this.changeReadState(item as NoticeItem); | |||
}} | |||
loading={fetchingNotices} | |||
clearText="清空" | |||
viewMoreText="查看更多" | |||
onClear={this.handleNoticeClear} | |||
onPopupVisibleChange={onNoticeVisibleChange} | |||
onViewMore={() => message.info('Click on view more')} | |||
clearClose | |||
> | |||
<NoticeIcon.Tab | |||
tabKey="notification" | |||
count={unreadMsg.notification} | |||
list={noticeData.notification} | |||
title="通知" | |||
emptyText="你已查看所有通知" | |||
showViewMore | |||
/> | |||
<NoticeIcon.Tab | |||
tabKey="message" | |||
count={unreadMsg.message} | |||
list={noticeData.message} | |||
title="消息" | |||
emptyText="您已读完所有消息" | |||
showViewMore | |||
/> | |||
<NoticeIcon.Tab | |||
tabKey="event" | |||
title="待办" | |||
emptyText="你已完成所有待办" | |||
count={unreadMsg.event} | |||
list={noticeData.event} | |||
showViewMore | |||
/> | |||
</NoticeIcon> | |||
); | |||
} | |||
} | |||
export default connect(({ user, global, loading }: ConnectState) => ({ | |||
currentUser: user.currentUser, | |||
collapsed: global.collapsed, | |||
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], | |||
fetchingNotices: loading.effects['global/fetchNotices'], | |||
notices: global.notices, | |||
}))(GlobalHeaderRight); |
@@ -0,0 +1,86 @@ | |||
import { Tooltip, Tag } from 'antd'; | |||
import type { Settings as ProSettings } from '@ant-design/pro-layout'; | |||
import { QuestionCircleOutlined } from '@ant-design/icons'; | |||
import React from 'react'; | |||
import type { ConnectProps } from 'umi'; | |||
import { connect, SelectLang } from 'umi'; | |||
import type { ConnectState } from '@/models/connect'; | |||
import Avatar from './AvatarDropdown'; | |||
import HeaderSearch from '../HeaderSearch'; | |||
import styles from './index.less'; | |||
import NoticeIconView from './NoticeIconView'; | |||
export type GlobalHeaderRightProps = { | |||
theme?: ProSettings['navTheme'] | 'realDark'; | |||
} & Partial<ConnectProps> & | |||
Partial<ProSettings>; | |||
const ENVTagColor = { | |||
dev: 'orange', | |||
test: 'green', | |||
pre: '#87d068', | |||
}; | |||
const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = (props) => { | |||
const { theme, layout } = props; | |||
let className = styles.right; | |||
if (theme === 'dark' && layout === 'top') { | |||
className = `${styles.right} ${styles.dark}`; | |||
} | |||
return ( | |||
<div className={className}> | |||
<HeaderSearch | |||
className={`${styles.action} ${styles.search}`} | |||
placeholder="站内搜索" | |||
defaultValue="umi ui" | |||
options={[ | |||
{ | |||
label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, | |||
value: 'umi ui', | |||
}, | |||
{ | |||
label: <a href="next.ant.design">Ant Design</a>, | |||
value: 'Ant Design', | |||
}, | |||
{ | |||
label: <a href="https://protable.ant.design/">Pro Table</a>, | |||
value: 'Pro Table', | |||
}, | |||
{ | |||
label: <a href="https://prolayout.ant.design/">Pro Layout</a>, | |||
value: 'Pro Layout', | |||
}, | |||
]} // onSearch={value => { | |||
// //console.log('input', value); | |||
// }} | |||
/> | |||
<Tooltip title="使用文档"> | |||
<a | |||
style={{ | |||
color: 'inherit', | |||
}} | |||
target="_blank" | |||
href="https://pro.ant.design/docs/getting-started" | |||
rel="noopener noreferrer" | |||
className={styles.action} | |||
> | |||
<QuestionCircleOutlined /> | |||
</a> | |||
</Tooltip> | |||
<NoticeIconView /> | |||
<Avatar menu /> | |||
{REACT_APP_ENV && ( | |||
<span> | |||
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag> | |||
</span> | |||
)} | |||
<SelectLang className={styles.action} /> | |||
</div> | |||
); | |||
}; | |||
export default connect(({ settings }: ConnectState) => ({ | |||
theme: settings.navTheme, | |||