Commit 2f3e2847 2f3e284747f07cdad531f8585bb001874b30500d by tailor

origin

1 parent 3073e49c
Showing 150 changed files with 9454 additions and 0 deletions
1 # http://editorconfig.org
2 root = true
3
4 [*]
5 indent_style = space
6 indent_size = 2
7 end_of_line = lf
8 charset = utf-8
9 trim_trailing_whitespace = true
10 insert_final_newline = true
11
12 [*.md]
13 trim_trailing_whitespace = false
14
15 [Makefile]
16 indent_style = tab
1 /lambda/
2 /scripts
3 /config
4 .history
...\ No newline at end of file ...\ No newline at end of file
1 module.exports = {
2 extends: [require.resolve('@umijs/fabric/dist/eslint')],
3 globals: {
4 ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
5 page: true,
6 },
7 };
1 ---
2 name: '报告Bug 🐛'
3 about: 报告 Ant Design Pro 的 bug
4 title: '🐛[BUG]'
5 labels: '🐛bug'
6 assignees: ''
7 ---
8
9 ### 🐛 bug 描述
10
11 <!--
12 详细地描述 bug,让大家都能理解
13 -->
14
15 ### 📷 复现步骤
16
17 <!--
18 清晰描述复现步骤,让别人也能看到问题
19 -->
20
21 ### 🏞 期望结果
22
23 <!--
24 描述你原本期望看到的结果
25 -->
26
27 ### 💻 复现代码
28
29 <!--
30 提供可复现的代码,仓库,或线上示例
31 -->
32
33 ### © 版本信息
34
35 - Ant Design Pro 版本: [e.g. 4.0.0]
36 - umi 版本
37 - 浏览器环境
38 - 开发环境 [e.g. mac OS]
39
40 ### 🚑 其他信息
41
42 <!--
43 如截图等其他信息可以贴在这里
44 -->
1 ---
2 name: '功能需求 ✨'
3 about: 对 Ant Design Pro 的需求或建议
4 title: '👑 [需求]'
5 labels: '👑Feature Request'
6 assignees: ''
7 ---
8
9 ### 🥰 需求描述
10
11 <!--
12 详细地描述需求,让大家都能理解
13 -->
14
15 ### 🧐 解决方案
16
17 <!--
18 如果你有解决方案,在这里清晰地阐述
19 -->
20
21 ### 🚑 其他信息
22
23 <!--
24 如截图等其他信息可以贴在这里
25 -->
1 ---
2 name: '疑问或需要帮助 ❓'
3 about: 对 Ant Design Pro 使用的疑问或需要帮助
4 title: '🧐[问题]'
5 labels: '🧐question'
6 assignees: ''
7 ---
8
9 ### 🧐 问题描述
10
11 <!--
12 详细地描述问题,让大家都能理解
13 -->
14
15 ### 💻 示例代码
16
17 <!--
18 如果你有解决方案,在这里清晰地阐述
19 -->
20
21 ### 🚑 其他信息
22
23 <!--
24 如截图等其他信息可以贴在这里
25 -->
1 on:
2 issue_comment:
3 types: [created]
4 name: Automatic Rebase
5 jobs:
6 rebase:
7 name: Rebase
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@master
11 - name: Automatic Rebase
12 uses: cirrus-actions/rebase@master
13 env:
14 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
3 # dependencies
4 **/node_modules
5 # roadhog-api-doc ignore
6 /src/utils/request-temp.js
7 _roadhog-api-doc
8
9 # production
10 /dist
11 /.vscode
12
13 # misc
14 .DS_Store
15 npm-debug.log*
16 yarn-error.log
17
18 /coverage
19 .idea
20 yarn.lock
21 package-lock.json
22 *bak
23 .vscode
24
25 # visual studio code
26 .history
27 *.log
28 functions/*
29 .temp/**
30
31 # umi
32 .umi
33 .umi-production
34
35 # screenshot
36 screenshot
37 .firebase
38 .eslintcache
39
40 build
1 **/*.svg
2 package.json
3 .umi
4 .umi-production
5 /dist
6 .dockerignore
7 .DS_Store
8 .eslintignore
9 *.png
10 *.toml
11 docker
12 .editorconfig
13 Dockerfile*
14 .gitignore
15 .prettierignore
16 LICENSE
17 .eslintcache
18 *.lock
19 yarn-error.log
20 .history
...\ No newline at end of file ...\ No newline at end of file
1 const fabric = require('@umijs/fabric');
2
3 module.exports = {
4 ...fabric.prettier,
5 };
1 const fabric = require('@umijs/fabric');
2
3 module.exports = {
4 ...fabric.stylelint,
5 };
1 import { IConfig, IPlugin } from 'umi-types';
2 import defaultSettings from './defaultSettings'; // https://umijs.org/config/
3
4 import slash from 'slash2';
5 import webpackPlugin from './plugin.config';
6 const { pwa, primaryColor } = defaultSettings; // preview.pro.ant.design only do not use in your production ;
7 // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
8
9 const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
10 const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site';
11 const plugins: IPlugin[] = [
12 [
13 'umi-plugin-react',
14 {
15 antd: true,
16 dva: {
17 hmr: true,
18 },
19 locale: {
20 // default false
21 enable: true,
22 // default zh-CN
23 default: 'zh-CN',
24 // default true, when it is true, will use `navigator.language` overwrite default
25 baseNavigator: true,
26 },
27 // dynamicImport: {
28 // loadingComponent: './components/PageLoading/index',
29 // webpackChunkName: true,
30 // level: 3,
31 // },
32 pwa: pwa
33 ? {
34 workboxPluginMode: 'InjectManifest',
35 workboxOptions: {
36 importWorkboxFrom: 'local',
37 },
38 }
39 : false, // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665
40 // dll features https://webpack.js.org/plugins/dll-plugin/
41 // dll: {
42 // include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
43 // exclude: ['@babel/runtime', 'netlify-lambda'],
44 // },
45 },
46 ],
47 [
48 'umi-plugin-pro-block',
49 {
50 moveMock: false,
51 moveService: false,
52 modifyRequest: true,
53 autoAddMenu: true,
54 },
55 ],
56 ]; // 针对 preview.pro.ant.design 的 GA 统计代码
57
58 if (isAntDesignProPreview) {
59 plugins.push([
60 'umi-plugin-ga',
61 {
62 code: 'UA-72788897-6',
63 },
64 ]);
65 }
66
67 export default {
68 plugins,
69 block: {
70 // 国内用户可以使用码云
71 // defaultGitUrl: 'https://gitee.com/ant-design/pro-blocks',
72 defaultGitUrl: 'https://github.com/ant-design/pro-blocks',
73 },
74 hash: true,
75 targets: {
76 ie: 11,
77 },
78 devtool: isAntDesignProPreview ? 'source-map' : false,
79 // umi routes: https://umijs.org/zh/guide/router.html
80 routes: [
81 {
82 path: '/user',
83 component: '../layouts/UserLayout',
84 routes: [
85 {
86 name: 'login',
87 path: '/user/login',
88 component: './user/login',
89 },
90 ],
91 },
92 {
93 path: '/',
94 component: '../layouts/SecurityLayout',
95 routes: [
96 {
97 path: '/',
98 component: '../layouts/BasicLayout',
99 authority: ['admin', 'user'],
100 routes: [
101 {
102 path: '/',
103 redirect: '/welcome',
104 },
105 {
106 path: '/welcome',
107 name: 'welcome',
108 icon: 'smile',
109 component: './Welcome',
110 },
111 {
112 name: 'ajax',
113 icon: 'smile',
114 path: '/ajax',
115 component: './ajax',
116 },
117 {
118 name: 'reports',
119 icon: 'smile',
120 path: '/reports',
121 component: './reports',
122 routes: [{
123 path: '/reports/blocks',
124 hideInMenu:true,
125 name: 'blocks',
126 component: './reports/blocks',
127 }]
128 },
129 {
130 path: '/employees',
131 name: 'employees',
132 icon: 'smile',
133 component: './employees',
134 },
135 {
136 path: '/machine',
137 name: 'machine',
138 icon: 'smile',
139 component: './Machine',
140 },
141 {
142 path: '/material',
143 name: 'material',
144 icon: 'smile',
145 component: './Material',
146 },
147 {
148 path: '/admin',
149 name: 'admin',
150 icon: 'crown',
151 component: './Admin',
152 authority: ['admin'],
153 },
154 {
155 component: './404',
156 },
157 ],
158 },
159 {
160 component: './404',
161 },
162 ],
163 },
164 {
165 component: './404',
166 },
167 ],
168 // Theme for antd: https://ant.design/docs/react/customize-theme-cn
169 theme: {
170 'primary-color': primaryColor,
171 },
172 define: {
173 ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION:
174 ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION || '', // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
175 },
176 ignoreMomentLocale: true,
177 lessLoaderOptions: {
178 javascriptEnabled: true,
179 },
180 disableRedirectHoist: true,
181 cssLoaderOptions: {
182 modules: true,
183 getLocalIdent: (
184 context: {
185 resourcePath: string;
186 },
187 _: string,
188 localName: string
189 ) => {
190 if (
191 context.resourcePath.includes('node_modules') ||
192 context.resourcePath.includes('ant.design.pro.less') ||
193 context.resourcePath.includes('global.less')
194 ) {
195 return localName;
196 }
197
198 const match = context.resourcePath.match(/src(.*)/);
199
200 if (match && match[1]) {
201 const antdProPath = match[1].replace('.less', '');
202 const arr = slash(antdProPath)
203 .split('/')
204 .map((a: string) => a.replace(/([A-Z])/g, '-$1'))
205 .map((a: string) => a.toLowerCase());
206 return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
207 }
208
209 return localName;
210 },
211 },
212 manifest: {
213 basePath: '/',
214 },
215 chainWebpack: webpackPlugin,
216 /*
217 proxy: {
218 '/server/api/': {
219 target: 'https://preview.pro.ant.design/',
220 changeOrigin: true,
221 pathRewrite: { '^/server': '' },
222 },
223 },
224 */
225 } as IConfig;
1 import { MenuTheme } from 'antd/es/menu/MenuContext';
2
3 export type ContentWidth = 'Fluid' | 'Fixed';
4
5 export interface DefaultSettings {
6 /**
7 * theme for nav menu
8 */
9 navTheme: MenuTheme;
10 /**
11 * primary color of ant design
12 */
13 primaryColor: string;
14 /**
15 * nav menu position: `sidemenu` or `topmenu`
16 */
17 layout: 'sidemenu' | 'topmenu';
18 /**
19 * layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
20 */
21 contentWidth: ContentWidth;
22 /**
23 * sticky header
24 */
25 fixedHeader: boolean;
26 /**
27 * auto hide header
28 */
29 autoHideHeader: boolean;
30 /**
31 * sticky siderbar
32 */
33 fixSiderbar: boolean;
34 menu: { locale: boolean };
35 title: string;
36 pwa: boolean;
37 // Your custom iconfont Symbol script Url
38 // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
39 // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
40 // Usage: https://github.com/ant-design/ant-design-pro/pull/3517
41 iconfontUrl: string;
42 colorWeak: boolean;
43 }
44
45 export default {
46 navTheme: 'dark',
47 primaryColor: '#1890FF',
48 layout: 'sidemenu',
49 contentWidth: 'Fluid',
50 fixedHeader: false,
51 autoHideHeader: false,
52 fixSiderbar: false,
53 colorWeak: false,
54 menu: {
55 locale: true,
56 },
57 title: 'LIMSChain 平台',
58 pwa: false,
59 iconfontUrl: '',
60 } as DefaultSettings;
1 // Change theme plugin
2 // eslint-disable-next-line eslint-comments/abdeils - enable - pair;
3 /* eslint-disable import/no-extraneous-dependencies */
4 import ThemeColorReplacer from 'webpack-theme-color-replacer';
5 import generate from '@ant-design/colors/lib/generate';
6 import path from 'path';
7
8 function getModulePackageName(module: { context: string }) {
9 if (!module.context) return null;
10
11 const nodeModulesPath = path.join(__dirname, '../node_modules/');
12 if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
13 return null;
14 }
15
16 const moduleRelativePath = module.context.substring(nodeModulesPath.length);
17 const [moduleDirName] = moduleRelativePath.split(path.sep);
18 let packageName: string | null = moduleDirName;
19 // handle tree shaking
20 if (packageName && packageName.match('^_')) {
21 // eslint-disable-next-line prefer-destructuring
22 packageName = packageName.match(/^_(@?[^@]+)/)![1];
23 }
24 return packageName;
25 }
26
27 export default (config: any) => {
28 // preview.pro.ant.design only do not use in your production;
29 if (
30 process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
31 process.env.NODE_ENV !== 'production'
32 ) {
33 config.plugin('webpack-theme-color-replacer').use(ThemeColorReplacer, [
34 {
35 fileName: 'css/theme-colors-[contenthash:8].css',
36 matchColors: getAntdSerials('#1890ff'), // 主色系列
37 // 改变样式选择器,解决样式覆盖问题
38 changeSelector(selector: string): string {
39 switch (selector) {
40 case '.ant-calendar-today .ant-calendar-date':
41 return ':not(.ant-calendar-selected-date)' + selector;
42 case '.ant-btn:focus,.ant-btn:hover':
43 return '.ant-btn:focus:not(.ant-btn-primary),.ant-btn:hover:not(.ant-btn-primary)';
44 case '.ant-btn.active,.ant-btn:active':
45 return '.ant-btn.active:not(.ant-btn-primary),.ant-btn:active:not(.ant-btn-primary)';
46 default:
47 return selector;
48 }
49 },
50 // isJsUgly: true,
51 },
52 ]);
53 }
54
55 // optimize chunks
56 config.optimization
57 // share the same chunks across different modules
58 .runtimeChunk(false)
59 .splitChunks({
60 chunks: 'async',
61 name: 'vendors',
62 maxInitialRequests: Infinity,
63 minSize: 0,
64 cacheGroups: {
65 vendors: {
66 test: (module: { context: string }) => {
67 const packageName = getModulePackageName(module) || '';
68 if (packageName) {
69 return [
70 'bizcharts',
71 'gg-editor',
72 'g6',
73 '@antv',
74 'gg-editor-core',
75 'bizcharts-plugin-slider',
76 ].includes(packageName);
77 }
78 return false;
79 },
80 name(module: { context: string }) {
81 const packageName = getModulePackageName(module);
82 if (packageName) {
83 if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
84 return 'viz'; // visualization package
85 }
86 }
87 return 'misc';
88 },
89 },
90 },
91 });
92 };
93
94 const getAntdSerials = (color: string) => {
95 const lightNum = 9;
96 const devide10 = 10;
97 // 淡化(即less的tint)
98 const lightens = new Array(lightNum).fill(undefined).map((_, i: number) => {
99 return ThemeColorReplacer.varyColor.lighten(color, i / devide10);
100 });
101 const colorPalettes = generate(color);
102 const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',');
103 return lightens.concat(colorPalettes).concat(rgb);
104 };
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 '--no-sandbox',
10 ],
11 },
12 };
1 module.exports = {
2 testURL: 'http://localhost:8000',
3 preset: 'jest-puppeteer',
4 globals: {
5 ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
6 localStorage: null,
7 },
8 };
1 {
2 "compilerOptions": {
3 "emitDecoratorMetadata": true,
4 "experimentalDecorators": true,
5 "baseUrl": ".",
6 "paths": {
7 "@/*": ["./src/*"]
8 }
9 }
10 }
1 import { Request, Response } from 'express';
2
3 const getNotices = (req: Request, res: Response) => {
4 res.json([
5 {
6 id: '000000001',
7 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
8 title: '你收到了 14 份新周报',
9 datetime: '2017-08-09',
10 type: 'notification',
11 },
12 {
13 id: '000000002',
14 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
15 title: '你推荐的 曲妮妮 已通过第三轮面试',
16 datetime: '2017-08-08',
17 type: 'notification',
18 },
19 {
20 id: '000000003',
21 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
22 title: '这种模板可以区分多种通知类型',
23 datetime: '2017-08-07',
24 read: true,
25 type: 'notification',
26 },
27 {
28 id: '000000004',
29 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
30 title: '左侧图标用于区分不同的类型',
31 datetime: '2017-08-07',
32 type: 'notification',
33 },
34 {
35 id: '000000005',
36 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
37 title: '内容不要超过两行字,超出时自动截断',
38 datetime: '2017-08-07',
39 type: 'notification',
40 },
41 {
42 id: '000000006',
43 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
44 title: '曲丽丽 评论了你',
45 description: '描述信息描述信息描述信息',
46 datetime: '2017-08-07',
47 type: 'message',
48 clickClose: true,
49 },
50 {
51 id: '000000007',
52 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
53 title: '朱偏右 回复了你',
54 description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
55 datetime: '2017-08-07',
56 type: 'message',
57 clickClose: true,
58 },
59 {
60 id: '000000008',
61 avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
62 title: '标题',
63 description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
64 datetime: '2017-08-07',
65 type: 'message',
66 clickClose: true,
67 },
68 {
69 id: '000000009',
70 title: '任务名称',
71 description: '任务需要在 2017-01-12 20:00 前启动',
72 extra: '未开始',
73 status: 'todo',
74 type: 'event',
75 },
76 {
77 id: '000000010',
78 title: '第三方紧急代码变更',
79 description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
80 extra: '马上到期',
81 status: 'urgent',
82 type: 'event',
83 },
84 {
85 id: '000000011',
86 title: '信息安全考试',
87 description: '指派竹尔于 2017-01-09 前完成更新并发布',
88 extra: '已耗时 8 天',
89 status: 'doing',
90 type: 'event',
91 },
92 {
93 id: '000000012',
94 title: 'ABCD 版本发布',
95 description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
96 extra: '进行中',
97 status: 'processing',
98 type: 'event',
99 },
100 ]);
101 };
102
103 export default {
104 'GET /api/notices': getNotices,
105 };
1 export default {
2 '/api/auth_routes': {
3 '/form/advanced-form': { authority: ['admin', 'user'] },
4 },
5 };
1 import { Request, Response } from 'express';
2
3 function getFakeCaptcha(req: Request, res: Response) {
4 return res.json('captcha-xxx');
5 }
6 // 代码中会兼容本地 service mock 以及部署站点的静态数据
7 export default {
8 // 支持值为 Object 和 Array
9 'GET /api/currentUser': {
10 name: 'Serati Ma',
11 avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
12 userid: '00000001',
13 email: 'antdesign@alipay.com',
14 signature: '海纳百川,有容乃大',
15 title: '交互专家',
16 group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
17 tags: [
18 {
19 key: '0',
20 label: '很有想法的',
21 },
22 {
23 key: '1',
24 label: '专注设计',
25 },
26 {
27 key: '2',
28 label: '辣~',
29 },
30 {
31 key: '3',
32 label: '大长腿',
33 },
34 {
35 key: '4',
36 label: '川妹子',
37 },
38 {
39 key: '5',
40 label: '海纳百川',
41 },
42 ],
43 notifyCount: 12,
44 unreadCount: 11,
45 country: 'China',
46 geographic: {
47 province: {
48 label: '浙江省',
49 key: '330000',
50 },
51 city: {
52 label: '杭州市',
53 key: '330100',
54 },
55 },
56 address: '西湖区工专路 77 号',
57 phone: '0752-268888888',
58 },
59 // GET POST 可省略
60 'GET /api/users': [
61 {
62 key: '1',
63 name: 'John Brown',
64 age: 32,
65 address: 'New York No. 1 Lake Park',
66 },
67 {
68 key: '2',
69 name: 'Jim Green',
70 age: 42,
71 address: 'London No. 1 Lake Park',
72 },
73 {
74 key: '3',
75 name: 'Joe Black',
76 age: 32,
77 address: 'Sidney No. 1 Lake Park',
78 },
79 ],
80 'POST /api/login/account': (req: Request, res: Response) => {
81 const { password, userName, type } = req.body;
82 if (password === 'ant.design' && userName === 'admin') {
83 res.send({
84 status: 'ok',
85 type,
86 currentAuthority: 'admin',
87 });
88 return;
89 }
90 if (password === 'ant.design' && userName === 'user') {
91 res.send({
92 status: 'ok',
93 type,
94 currentAuthority: 'user',
95 });
96 return;
97 }
98 res.send({
99 status: 'error',
100 type,
101 currentAuthority: 'guest',
102 });
103 },
104 'POST /api/register': (req: Request, res: Response) => {
105 res.send({ status: 'ok', currentAuthority: 'user' });
106 },
107 'GET /api/500': (req: Request, res: Response) => {
108 res.status(500).send({
109 timestamp: 1513932555104,
110 status: 500,
111 error: 'error',
112 message: 'error',
113 path: '/base/category/list',
114 });
115 },
116 'GET /api/404': (req: Request, res: Response) => {
117 res.status(404).send({
118 timestamp: 1513932643431,
119 status: 404,
120 error: 'Not Found',
121 message: 'No message available',
122 path: '/base/category/list/2121212',
123 });
124 },
125 'GET /api/403': (req: Request, res: Response) => {
126 res.status(403).send({
127 timestamp: 1513932555104,
128 status: 403,
129 error: 'Unauthorized',
130 message: 'Unauthorized',
131 path: '/base/category/list',
132 });
133 },
134 'GET /api/401': (req: Request, res: Response) => {
135 res.status(401).send({
136 timestamp: 1513932555104,
137 status: 401,
138 error: 'Unauthorized',
139 message: 'Unauthorized',
140 path: '/base/category/list',
141 });
142 },
143
144 'GET /api/login/captcha': getFakeCaptcha,
145 };
1 {
2 "name": "ant-design-pro",
3 "version": "1.0.0",
4 "private": true,
5 "description": "An out-of-box UI solution for enterprise applications",
6 "proxy": "https://randomuser.me/api",
7 "scripts": {
8 "analyze": "cross-env ANALYZE=1 umi build",
9 "build": "umi build",
10 "deploy": "npm run site && npm run gh-pages",
11 "fetch:blocks": "pro fetch-blocks && npm run prettier",
12 "format-imports": "cross-env import-sort --write '**/*.{js,jsx,ts,tsx}'",
13 "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist",
14 "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
15 "lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
16 "lint-staged": "lint-staged",
17 "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
18 "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
19 "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
20 "lint:prettier": "check-prettier lint",
21 "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
22 "prettier": "prettier -c --write \"**/*\"",
23 "start": "umi dev",
24 "start:no-mock": "cross-env MOCK=none umi dev",
25 "start:no-ui": "cross-env UMI_UI=none umi dev",
26 "test": "umi test",
27 "test:all": "node ./tests/run-tests.js",
28 "test:component": "umi test ./src/components",
29 "ui": "umi ui"
30 },
31 "husky": {
32 "hooks": {
33 "pre-commit": "npm run lint-staged"
34 }
35 },
36 "lint-staged": {
37 "**/*.less": "stylelint --syntax less",
38 "**/*.{js,jsx,tsx,ts,less,md,json}": [
39 "prettier --write",
40 "git add"
41 ],
42 "**/*.{js,jsx}": "npm run lint-staged:js",
43 "**/*.{js,ts,tsx}": "npm run lint-staged:js"
44 },
45 "browserslist": [
46 "> 1%",
47 "last 2 versions",
48 "not ie <= 10"
49 ],
50 "dependencies": {
51 "@ant-design/colors": "^3.1.0",
52 "@ant-design/pro-layout": "^4.5.16",
53 "@antv/data-set": "^0.10.2",
54 "antd": "^3.23.6",
55 "classnames": "^2.2.6",
56 "dva": "^2.4.1",
57 "echarts": "^4.5.0",
58 "echarts-for-react": "^2.0.15-beta.1",
59 "lodash": "^4.17.11",
60 "moment": "^2.24.0",
61 "omit.js": "^1.0.2",
62 "path-to-regexp": "2.4.0",
63 "qs": "^6.9.0",
64 "react": "^16.8.6",
65 "react-copy-to-clipboard": "^5.0.1",
66 "react-dom": "^16.8.6",
67 "react-helmet": "^5.2.1",
68 "redux": "^4.0.1",
69 "slash2": "^2.0.0",
70 "umi": "^2.9.6",
71 "umi-plugin-pro-block": "^1.3.4",
72 "umi-plugin-react": "^1.10.1",
73 "umi-request": "^1.2.7",
74 "webpack-theme-color-replacer": "^1.2.15"
75 },
76 "devDependencies": {
77 "@ant-design/pro-cli": "^1.0.13",
78 "@types/classnames": "^2.2.7",
79 "@types/express": "^4.17.0",
80 "@types/history": "^4.7.2",
81 "@types/jest": "^24.0.13",
82 "@types/lodash": "^4.14.144",
83 "@types/qs": "^6.5.3",
84 "@types/react": "^16.8.19",
85 "@types/react-dom": "^16.8.4",
86 "@types/react-helmet": "^5.0.13",
87 "@umijs/fabric": "^1.2.0",
88 "chalk": "^3.0.0",
89 "check-prettier": "^1.0.3",
90 "cross-env": "^6.0.0",
91 "cross-port-killer": "^1.1.1",
92 "enzyme": "^3.9.0",
93 "eslint": "5.16.0",
94 "express": "^4.17.1",
95 "gh-pages": "^2.0.1",
96 "husky": "^3.0.0",
97 "import-sort-cli": "^6.0.0",
98 "import-sort-parser-babylon": "^6.0.0",
99 "import-sort-parser-typescript": "^6.0.0",
100 "import-sort-style-module": "^6.0.0",
101 "jest-puppeteer": "^4.2.0",
102 "lint-staged": "^9.0.0",
103 "mockjs": "^1.0.1-beta3",
104 "node-fetch": "^2.6.0",
105 "prettier": "^1.17.1",
106 "pro-download": "1.0.1",
107 "stylelint": "^12.0.0",
108 "umi-plugin-ga": "^1.1.3",
109 "umi-plugin-pro": "^1.0.2",
110 "umi-types": "^0.5.0"
111 },
112 "optionalDependencies": {
113 "puppeteer": "^1.17.0"
114 },
115 "engines": {
116 "node": ">=10.0.0"
117 },
118 "checkFiles": [
119 "src/**/*.js*",
120 "src/**/*.ts*",
121 "src/**/*.less",
122 "config/**/*.js*",
123 "scripts/**/*.js"
124 ]
125 }
1 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 <svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
4 <title>Group 28 Copy 5</title>
5 <desc>Created with Sketch.</desc>
6 <defs>
7 <linearGradient x1="62.1023273%" y1="0%" x2="108.19718%" y2="37.8635764%" id="linearGradient-1">
8 <stop stop-color="#4285EB" offset="0%"></stop>
9 <stop stop-color="#2EC7FF" offset="100%"></stop>
10 </linearGradient>
11 <linearGradient x1="69.644116%" y1="0%" x2="54.0428975%" y2="108.456714%" id="linearGradient-2">
12 <stop stop-color="#29CDFF" offset="0%"></stop>
13 <stop stop-color="#148EFF" offset="37.8600687%"></stop>
14 <stop stop-color="#0A60FF" offset="100%"></stop>
15 </linearGradient>
16 <linearGradient x1="69.6908165%" y1="-12.9743587%" x2="16.7228981%" y2="117.391248%" id="linearGradient-3">
17 <stop stop-color="#FA816E" offset="0%"></stop>
18 <stop stop-color="#F74A5C" offset="41.472606%"></stop>
19 <stop stop-color="#F51D2C" offset="100%"></stop>
20 </linearGradient>
21 <linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-4">
22 <stop stop-color="#FA8E7D" offset="0%"></stop>
23 <stop stop-color="#F74A5C" offset="51.2635191%"></stop>
24 <stop stop-color="#F51D2C" offset="100%"></stop>
25 </linearGradient>
26 </defs>
27 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
28 <g id="logo" transform="translate(-20.000000, -20.000000)">
29 <g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)">
30 <g id="Group-27-Copy-3">
31 <g id="Group-25" fill-rule="nonzero">
32 <g id="2">
33 <path 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" id="Shape" fill="url(#linearGradient-1)"></path>
34 <path 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" id="Shape" fill="url(#linearGradient-2)"></path>
35 </g>
36 <path 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" id="Shape" fill="url(#linearGradient-3)"></path>
37 </g>
38 <ellipse id="Combined-Shape" fill="url(#linearGradient-4)" cx="100.519339" cy="100.436681" rx="23.6001926" ry="23.580786"></ellipse>
39 </g>
40 </g>
41 </g>
42 </g>
43 </svg>
...\ No newline at end of file ...\ No newline at end of file
1 import React from 'react';
2 import { Result } from 'antd';
3 import check, { IAuthorityType } from './CheckPermissions';
4
5 import AuthorizedRoute from './AuthorizedRoute';
6 import Secured from './Secured';
7
8 interface AuthorizedProps {
9 authority: IAuthorityType;
10 noMatch?: React.ReactNode;
11 }
12
13 type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
14 Secured: typeof Secured;
15 check: typeof check;
16 AuthorizedRoute: typeof AuthorizedRoute;
17 };
18
19 const Authorized: React.FunctionComponent<AuthorizedProps> = ({
20 children,
21 authority,
22 noMatch = (
23 <Result
24 status="403"
25 title="403"
26 subTitle="Sorry, you are not authorized to access this page."
27 />
28 ),
29 }) => {
30 const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
31 const dom = check(authority, childrenRender, noMatch);
32 return <>{dom}</>;
33 };
34
35 export default Authorized as IAuthorizedType;
1 import { Redirect, Route } from 'umi';
2
3 import React from 'react';
4 import Authorized from './Authorized';
5 import { IAuthorityType } from './CheckPermissions';
6
7 interface AuthorizedRoutePops {
8 currentAuthority: string;
9 component: React.ComponentClass<any, any>;
10 render: (props: any) => React.ReactNode;
11 redirectPath: string;
12 authority: IAuthorityType;
13 }
14
15 const AuthorizedRoute: React.SFC<AuthorizedRoutePops> = ({
16 component: Component,
17 render,
18 authority,
19 redirectPath,
20 ...rest
21 }) => (
22 <Authorized
23 authority={authority}
24 noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
25 >
26 <Route
27 {...rest}
28 render={(props: any) => (Component ? <Component {...props} /> : render(props))}
29 />
30 </Authorized>
31 );
32
33 export default AuthorizedRoute;
1 import React from 'react';
2 import { CURRENT } from './renderAuthorize';
3 // eslint-disable-next-line import/no-cycle
4 import PromiseRender from './PromiseRender';
5
6 export type IAuthorityType =
7 | undefined
8 | string
9 | string[]
10 | Promise<boolean>
11 | ((currentAuthority: string | string[]) => IAuthorityType);
12
13 /**
14 * 通用权限检查方法
15 * Common check permissions method
16 * @param { 权限判定 | Permission judgment } authority
17 * @param { 你的权限 | Your permission description } currentAuthority
18 * @param { 通过的组件 | Passing components } target
19 * @param { 未通过的组件 | no pass components } Exception
20 */
21 const checkPermissions = <T, K>(
22 authority: IAuthorityType,
23 currentAuthority: string | string[],
24 target: T,
25 Exception: K,
26 ): T | K | React.ReactNode => {
27 // 没有判定权限.默认查看所有
28 // Retirement authority, return target;
29 if (!authority) {
30 return target;
31 }
32 // 数组处理
33 if (Array.isArray(authority)) {
34 if (Array.isArray(currentAuthority)) {
35 if (currentAuthority.some(item => authority.includes(item))) {
36 return target;
37 }
38 } else if (authority.includes(currentAuthority)) {
39 return target;
40 }
41 return Exception;
42 }
43 // string 处理
44 if (typeof authority === 'string') {
45 if (Array.isArray(currentAuthority)) {
46 if (currentAuthority.some(item => authority === item)) {
47 return target;
48 }
49 } else if (authority === currentAuthority) {
50 return target;
51 }
52 return Exception;
53 }
54 // Promise 处理
55 if (authority instanceof Promise) {
56 return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
57 }
58 // Function 处理
59 if (typeof authority === 'function') {
60 try {
61 const bool = authority(currentAuthority);
62 // 函数执行后返回值是 Promise
63 if (bool instanceof Promise) {
64 return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
65 }
66 if (bool) {
67 return target;
68 }
69 return Exception;
70 } catch (error) {
71 throw error;
72 }
73 }
74 throw new Error('unsupported parameters');
75 };
76
77 export { checkPermissions };
78
79 function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
80 return checkPermissions<T, K>(authority, CURRENT, target, Exception);
81 }
82
83 export default check;
1 import React from 'react';
2 import { Spin } from 'antd';
3 import isEqual from 'lodash/isEqual';
4 import { isComponentClass } from './Secured';
5 // eslint-disable-next-line import/no-cycle
6
7 interface PromiseRenderProps<T, K> {
8 ok: T;
9 error: K;
10 promise: Promise<boolean>;
11 }
12
13 interface PromiseRenderState {
14 component: React.ComponentClass | React.FunctionComponent;
15 }
16
17 export default class PromiseRender<T, K> extends React.Component<
18 PromiseRenderProps<T, K>,
19 PromiseRenderState
20 > {
21 state: PromiseRenderState = {
22 component: () => null,
23 };
24
25 componentDidMount() {
26 this.setRenderComponent(this.props);
27 }
28
29 shouldComponentUpdate = (nextProps: PromiseRenderProps<T, K>, nextState: PromiseRenderState) => {
30 const { component } = this.state;
31 if (!isEqual(nextProps, this.props)) {
32 this.setRenderComponent(nextProps);
33 }
34 if (nextState.component !== component) return true;
35 return false;
36 };
37
38 // set render Component : ok or error
39 setRenderComponent(props: PromiseRenderProps<T, K>) {
40 const ok = this.checkIsInstantiation(props.ok);
41 const error = this.checkIsInstantiation(props.error);
42 props.promise
43 .then(() => {
44 this.setState({
45 component: ok,
46 });
47 return true;
48 })
49 .catch(() => {
50 this.setState({
51 component: error,
52 });
53 });
54 }
55
56 // Determine whether the incoming component has been instantiated
57 // AuthorizedRoute is already instantiated
58 // Authorized render is already instantiated, children is no instantiated
59 // Secured is not instantiated
60 checkIsInstantiation = (
61 target: React.ReactNode | React.ComponentClass,
62 ): React.FunctionComponent => {
63 if (isComponentClass(target)) {
64 const Target = target as React.ComponentClass;
65 return (props: any) => <Target {...props} />;
66 }
67 if (React.isValidElement(target)) {
68 return (props: any) => React.cloneElement(target, props);
69 }
70 return () => target as React.ReactNode & null;
71 };
72
73 render() {
74 const { component: Component } = this.state;
75 const { ok, error, promise, ...rest } = this.props;
76
77 return Component ? (
78 <Component {...rest} />
79 ) : (
80 <div
81 style={{
82 width: '100%',
83 height: '100%',
84 margin: 'auto',
85 paddingTop: 50,
86 textAlign: 'center',
87 }}
88 >
89 <Spin size="large" />
90 </div>
91 );
92 }
93 }
1 import React from 'react';
2 import CheckPermissions from './CheckPermissions';
3
4 /**
5 * 默认不能访问任何页面
6 * default is "NULL"
7 */
8 const Exception403 = () => 403;
9
10 export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
11 if (!component) return false;
12 const proto = Object.getPrototypeOf(component);
13 if (proto === React.Component || proto === Function.prototype) return true;
14 return isComponentClass(proto);
15 };
16
17 // Determine whether the incoming component has been instantiated
18 // AuthorizedRoute is already instantiated
19 // Authorized render is already instantiated, children is no instantiated
20 // Secured is not instantiated
21 const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
22 if (isComponentClass(target)) {
23 const Target = target as React.ComponentClass;
24 return (props: any) => <Target {...props} />;
25 }
26 if (React.isValidElement(target)) {
27 return (props: any) => React.cloneElement(target, props);
28 }
29 return () => target;
30 };
31
32 /**
33 * 用于判断是否拥有权限访问此 view 权限
34 * authority 支持传入 string, () => boolean | Promise
35 * e.g. 'user' 只有 user 用户能访问
36 * e.g. 'user,admin' user 和 admin 都能访问
37 * e.g. ()=>boolean 返回true能访问,返回false不能访问
38 * e.g. Promise then 能访问 catch不能访问
39 * e.g. authority support incoming string, () => boolean | Promise
40 * e.g. 'user' only user user can access
41 * e.g. 'user, admin' user and admin can access
42 * e.g. () => boolean true to be able to visit, return false can not be accessed
43 * e.g. Promise then can not access the visit to catch
44 * @param {string | function | Promise} authority
45 * @param {ReactNode} error 非必需参数
46 */
47 const authorize = (authority: string, error?: React.ReactNode) => {
48 /**
49 * conversion into a class
50 * 防止传入字符串时找不到staticContext造成报错
51 * String parameters can cause staticContext not found error
52 */
53 let classError: boolean | React.FunctionComponent = false;
54 if (error) {
55 classError = (() => error) as React.FunctionComponent;
56 }
57 if (!authority) {
58 throw new Error('authority is required');
59 }
60 return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
61 const component = CheckPermissions(authority, target, classError || Exception403);
62 return checkIsInstantiation(component);
63 };
64 };
65
66 export default authorize;
1 import Authorized from './Authorized';
2 import Secured from './Secured';
3 import check from './CheckPermissions';
4 import renderAuthorize from './renderAuthorize';
5
6 Authorized.Secured = Secured;
7 Authorized.check = check;
8
9 const RenderAuthorize = renderAuthorize(Authorized);
10
11 export default RenderAuthorize;
1 /* eslint-disable eslint-comments/disable-enable-pair */
2 /* eslint-disable import/no-mutable-exports */
3 let CURRENT: string | string[] = 'NULL';
4
5 type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
6 /**
7 * use authority or getAuthority
8 * @param {string|()=>String} currentAuthority
9 */
10 const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
11 currentAuthority: CurrentAuthorityType,
12 ): T => {
13 if (currentAuthority) {
14 if (typeof currentAuthority === 'function') {
15 CURRENT = currentAuthority();
16 }
17 if (
18 Object.prototype.toString.call(currentAuthority) === '[object String]' ||
19 Array.isArray(currentAuthority)
20 ) {
21 CURRENT = currentAuthority as string[];
22 }
23 } else {
24 CURRENT = 'NULL';
25 }
26 return Authorized;
27 };
28
29 export { CURRENT };
30 export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);
1 .copy-block {
2 position: fixed;
3 right: 80px;
4 bottom: 40px;
5 z-index: 99;
6 display: flex;
7 flex-direction: column;
8 align-items: center;
9 justify-content: center;
10 width: 40px;
11 height: 40px;
12 font-size: 20px;
13 background: #fff;
14 border-radius: 40px;
15 box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
16 0 1px 10px 0 rgba(0, 0, 0, 0.12);
17 cursor: pointer;
18 }
19
20 .copy-block-view {
21 position: relative;
22 .copy-block-code {
23 display: inline-block;
24 margin: 0 0.2em;
25 padding: 0.2em 0.4em 0.1em;
26 font-size: 85%;
27 border-radius: 3px;
28 }
29 }
1 import { Icon, Popover, Typography } from 'antd';
2 import React, { useRef } from 'react';
3
4 import { FormattedMessage } from 'umi-plugin-react/locale';
5 import { connect } from 'dva';
6 import { isAntDesignPro } from '@/utils/utils';
7 import styles from './index.less';
8
9 const firstUpperCase = (pathString: string): string =>
10 pathString
11 .replace('.', '')
12 .split(/\/|-/)
13 .map((s): string => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
14 .filter((s): boolean => !!s)
15 .join('');
16
17 // when click block copy, send block url to ga
18 const onBlockCopy = (label: string) => {
19 if (!isAntDesignPro()) {
20 return;
21 }
22
23 const ga = window && window.ga;
24 if (ga) {
25 ga('send', 'event', {
26 eventCategory: 'block',
27 eventAction: 'copy',
28 eventLabel: label,
29 });
30 }
31 };
32
33 const BlockCodeView: React.SFC<{
34 url: string;
35 }> = ({ url }) => {
36 const blockUrl = `npx umi block add ${firstUpperCase(url)} --path=${url}`;
37 return (
38 <div className={styles['copy-block-view']}>
39 <Typography.Paragraph
40 copyable={{
41 text: blockUrl,
42 onCopy: () => onBlockCopy(url),
43 }}
44 style={{
45 display: 'flex',
46 }}
47 >
48 <pre>
49 <code className={styles['copy-block-code']}>{blockUrl}</code>
50 </pre>
51 </Typography.Paragraph>
52 </div>
53 );
54 };
55
56 interface RoutingType {
57 location: {
58 pathname: string;
59 };
60 }
61
62 export default connect(({ routing }: { routing: RoutingType }) => ({
63 location: routing.location,
64 }))(({ location }: RoutingType) => {
65 const url = location.pathname;
66 const divDom = useRef<HTMLDivElement>(null);
67 return (
68 <Popover
69 title={<FormattedMessage id="app.preview.down.block" defaultMessage="下载此页面到本地项目" />}
70 placement="topLeft"
71 content={<BlockCodeView url={url} />}
72 trigger="click"
73 getPopupContainer={dom => (divDom.current ? divDom.current : dom)}
74 >
75 <div className={styles['copy-block']} ref={divDom}>
76 <Icon type="download" />
77 </div>
78 </Popover>
79 );
80 });
1 import React, { Component } from 'react';
2 // import ReactEcharts from '../../../src/index';
3 import ReactEcharts from "echarts-for-react";
4
5 require('echarts/map/js/china.js');
6
7 export default class CountryMap extends Component {
8 constructor(props: any) {
9 super(props);
10 this.state = this.getInitialState();
11 }
12 timeTicket: any = null;
13 state: any;
14 getInitialState = () => ({option: this.getOption()});
15
16 componentDidMount() {
17 if (this.timeTicket) {
18 clearInterval(this.timeTicket);
19 }
20 this.timeTicket = setInterval(() => {
21 const option = this.state.option;
22 const r = new Date().getSeconds();
23 option.title.text = '国抽任务' + r;
24 option.series[0].name = '省抽任务' + r;
25 option.legend.data[0] = '其他任务' + r;
26 this.setState({ option: option });
27 }, 1000);
28 };
29 componentWillUnmount() {
30 if (this.timeTicket) {
31 clearInterval(this.timeTicket);
32 }
33 };
34 randomData() {
35 return Math.round(Math.random()*1000);
36 };
37 getOption = () => {
38 return {
39 title: {
40 text: '检测任务',
41 subtext: '全国检测任务分布图',
42 left: 'center'
43 },
44 tooltip: {
45 trigger: 'item'
46 },
47 legend: {
48 orient: 'vertical',
49 left: 'left',
50 data:['国抽任务','省抽任务','其他任务']
51 },
52 visualMap: {
53 min: 0,
54 max: 2500,
55 left: 'left',
56 top: 'bottom',
57 text: ['高','低'], // 文本,默认为数值文本
58 calculable: true
59 },
60 toolbox: {
61 show: true,
62 orient: 'vertical',
63 left: 'right',
64 top: 'center',
65 feature: {
66 dataView: {readOnly: false},
67 restore: {},
68 saveAsImage: {}
69 }
70 },
71 series: [
72 {
73 name: '国抽任务',
74 type: 'map',
75 mapType: 'china',
76 roam: false,
77 label: {
78 normal: {
79 show: true
80 },
81 emphasis: {
82 show: true
83 }
84 },
85 data:[
86 {name: '青岛', value: this.randomData()},
87 {name: '莱西', value: this.randomData()},
88 {name: '日照', value: this.randomData()},
89 {name: '烟台', value: this.randomData()},
90 {name: '即墨', value: this.randomData()},
91 {name: '莱州', value: this.randomData()},
92 {name: '蓬莱', value: this.randomData()},
93 {name: '寿光', value: this.randomData()},
94 {name: '潍坊', value: this.randomData()},
95 {name: '枣庄', value: this.randomData()},
96 {name: '淄博', value: this.randomData()},
97 {name: '济南', value: this.randomData()},
98 {name: '临沂', value: this.randomData()},
99 {name: '泰安', value: this.randomData()},
100 {name: '聊城', value: this.randomData()},
101 {name: '德州', value: this.randomData()},
102 {name: '济宁', value: this.randomData()},
103 {name: '莱芜', value: this.randomData()},
104 {name: '菏泽', value: this.randomData()},
105 {name: '北京',value: this.randomData() },
106 {name: '天津',value: this.randomData() },
107 {name: '上海',value: this.randomData() },
108 {name: '重庆',value: this.randomData() },
109 {name: '河北',value: this.randomData() },
110 {name: '河南',value: this.randomData() },
111 {name: '云南',value: this.randomData() },
112 {name: '辽宁',value: this.randomData() },
113 {name: '黑龙江',value: this.randomData() },
114 {name: '湖南',value: this.randomData() },
115 {name: '安徽',value: this.randomData() },
116 {name: '山东',value: this.randomData() },
117 {name: '新疆',value: this.randomData() },
118 {name: '江苏',value: this.randomData() },
119 {name: '浙江',value: this.randomData() },
120 {name: '江西',value: this.randomData() },
121 {name: '湖北',value: this.randomData() },
122 {name: '广西',value: this.randomData() },
123 {name: '甘肃',value: this.randomData() },
124 {name: '山西',value: this.randomData() },
125 {name: '内蒙古',value: this.randomData() },
126 {name: '陕西',value: this.randomData() },
127 {name: '吉林',value: this.randomData() },
128 {name: '福建',value: this.randomData() },
129 {name: '贵州',value: this.randomData() },
130 {name: '广东',value: this.randomData() },
131 {name: '青海',value: this.randomData() },
132 {name: '西藏',value: this.randomData() },
133 {name: '四川',value: this.randomData() },
134 {name: '宁夏',value: this.randomData() },
135 {name: '海南',value: this.randomData() },
136 {name: '台湾',value: this.randomData() },
137 {name: '香港',value: this.randomData() },
138 {name: '澳门',value: this.randomData() }
139 ]
140 },
141 {
142 name: '省抽任务',
143 type: 'map',
144 mapType: 'china',
145 label: {
146 normal: {
147 show: true
148 },
149 emphasis: {
150 show: true
151 }
152 },
153 data:[
154 {name: '北京',value: this.randomData() },
155 {name: '天津',value: this.randomData() },
156 {name: '上海',value: this.randomData() },
157 {name: '重庆',value: this.randomData() },
158 {name: '河北',value: this.randomData() },
159 {name: '安徽',value: this.randomData() },
160 {name: '新疆',value: this.randomData() },
161 {name: '浙江',value: this.randomData() },
162 {name: '江西',value: this.randomData() },
163 {name: '山西',value: this.randomData() },
164 {name: '内蒙古',value: this.randomData() },
165 {name: '吉林',value: this.randomData() },
166 {name: '福建',value: this.randomData() },
167 {name: '广东',value: this.randomData() },
168 {name: '西藏',value: this.randomData() },
169 {name: '四川',value: this.randomData() },
170 {name: '宁夏',value: this.randomData() },
171 {name: '香港',value: this.randomData() },
172 {name: '澳门',value: this.randomData() }
173 ]
174 },
175 {
176 name: '其他任务',
177 type: 'map',
178 mapType: 'china',
179 label: {
180 normal: {
181 show: true
182 },
183 emphasis: {
184 show: true
185 }
186 },
187 data:[
188 {name: '北京',value: this.randomData() },
189 {name: '天津',value: this.randomData() },
190 {name: '上海',value: this.randomData() },
191 {name: '广东',value: this.randomData() },
192 {name: '台湾',value: this.randomData() },
193 {name: '香港',value: this.randomData() },
194 {name: '澳门',value: this.randomData() }
195 ]
196 }
197 ]
198 };
199 };
200 render() {
201 return (
202 <div className='examples'>
203 <div className='parent'>
204 <label> 全国检测任务分布信息 <strong>区域分布</strong>: </label>
205 <ReactEcharts
206 option={this.state.option}
207 style={{height: '500px', width: '100%'}}
208 className='react_for_echarts' />
209 </div>
210 </div>
211 );
212 };
213 }
...\ No newline at end of file ...\ No newline at end of file
1 import React from 'react';
2
3 import ReactEcharts from "echarts-for-react";
4 // const echarts = require('echarts/lib/echarts');
5 require('echarts/map/js/china.js');
6
7 export default class ProvinceMap extends React.Component {
8 constructor(props: any) {
9 super(props);
10 this.state = this.getInitialState();
11 }
12 timeTicket: any = null;
13 state: any;
14 getInitialState = () => ({option: this.getOption()});
15 componentDidMount() {
16 if (this.timeTicket) {
17 clearInterval(this.timeTicket);
18 }
19 this.timeTicket = setInterval(() => {
20 const option = this.state.option;
21 const r = new Date().getSeconds();
22 option.title.text = 'iphone销量' + r;
23 option.series[0].name = 'iphone销量' + r;
24 // option.legend.data[0] = 'iphone销量' + r;
25 this.setState({ option: option });
26 }, 1000);
27 };
28 componentWillUnmount() {
29 if (this.timeTicket) {
30 clearInterval(this.timeTicket);
31 }
32 };
33 randomData() {
34 return Math.round(Math.random()*1000);
35 };
36 data: any[] = [
37 {name: '青岛', value: 18},
38 {name: '莱西', value: 21},
39 {name: '日照', value: 21},
40 {name: '烟台', value: 28},
41 {name: '即墨', value: 30},
42 {name: '莱州', value: 32},
43 {name: '蓬莱', value: 37},
44 {name: '寿光', value: 40},
45 {name: '潍坊', value: 65},
46 {name: '枣庄', value: 84},
47 {name: '淄博', value: 85},
48 {name: '济南', value: 92},
49 {name: '临沂', value: 103},
50 {name: '泰安', value: 112},
51 {name: '聊城', value: 116},
52 {name: '德州', value: 120},
53 {name: '济宁', value: 120},
54 {name: '莱芜', value: 148},
55 {name: '菏泽', value: 194}
56 ];
57
58 geoCoordMap: any = {
59 '青岛':[120.33,36.07],
60 '莱西':[120.53,36.86],
61 '日照':[119.46,35.42],
62 '威海':[122.1,37.5],
63 '烟台':[121.39,37.52],
64 '即墨':[120.45,36.38],
65 '莱州':[119.942327,37.177017],
66 '蓬莱':[120.75,37.8],
67 '寿光':[118.73,36.86],
68 '潍坊':[119.1,36.62],
69 '枣庄':[117.57,34.86],
70 '淄博':[118.05,36.78],
71 '济南':[117,36.65],
72 '临沂':[118.35,35.05],
73 '泰安':[117.13,36.18],
74 '聊城':[115.97,36.45],
75 '德州':[116.29,37.45],
76 '济宁':[116.59,35.38],
77 '莱芜':[117.67,36.19],
78 '菏泽':[115.480656,35.23375]
79 };
80
81 convertData = (data: any) => {
82 var res = [];
83 for (var i = 0; i < data.length; i++) {
84 var geoCoord = this.geoCoordMap[data[i].name];
85 if (geoCoord) {
86 res.push({
87 name: data[i].name,
88 value: geoCoord.concat(data[i].value)
89 });
90 }
91 }
92 return res;
93 };
94 // renderItem = (params: any, api: any) => {
95 // var coords = [
96 // [116.7,39.53],
97 // [103.73,36.03],
98 // [112.91,27.87],
99 // [120.65,28.01],
100 // [119.57,39.95]
101 // ];
102 // var points = [];
103 // for (var i = 0; i < coords.length; i++) {
104 // points.push(api.coord(coords[i]));
105 // }
106 // var color = api.visual('color');
107
108 // return {
109 // type: 'polygon',
110 // shape: {
111 // points: echarts.graphic.clipPointsByRect(points, {
112 // x: params.coordSys.x,
113 // y: params.coordSys.y,
114 // width: params.coordSys.width,
115 // height: params.coordSys.height
116 // })
117 // },
118 // style: api.style({
119 // fill: color,
120 // stroke: echarts.color.lift(color)
121 // })
122 // };
123 // }
124 getOption = () => {
125 return {
126 backgroundColor: 'transparent',
127 title: {
128 text: '全国主要城市空气质量',
129 subtext: 'data from PM25.in',
130 sublink: 'http://www.pm25.in',
131 left: 'center',
132 textStyle: {
133 color: '#fff'
134 }
135 },
136 tooltip : {
137 trigger: 'item'
138 },
139 bmap: {
140 center: [104.114129, 37.550339],
141 zoom: 5,
142 roam: true,
143 mapStyle: {
144 styleJson: [
145 {
146 "featureType": "water",
147 "elementType": "all",
148 "stylers": {
149 "color": "#044161"
150 }
151 },
152 {
153 "featureType": "land",
154 "elementType": "all",
155 "stylers": {
156 "color": "#004981"
157 }
158 },
159 {
160 "featureType": "boundary",
161 "elementType": "geometry",
162 "stylers": {
163 "color": "#064f85"
164 }
165 },
166 {
167 "featureType": "railway",
168 "elementType": "all",
169 "stylers": {
170 "visibility": "off"
171 }
172 },
173 {
174 "featureType": "highway",
175 "elementType": "geometry",
176 "stylers": {
177 "color": "#004981"
178 }
179 },
180 {
181 "featureType": "highway",
182 "elementType": "geometry.fill",
183 "stylers": {
184 "color": "#005b96",
185 "lightness": 1
186 }
187 },
188 {
189 "featureType": "highway",
190 "elementType": "labels",
191 "stylers": {
192 "visibility": "off"
193 }
194 },
195 {
196 "featureType": "arterial",
197 "elementType": "geometry",
198 "stylers": {
199 "color": "#004981"
200 }
201 },
202 {
203 "featureType": "arterial",
204 "elementType": "geometry.fill",
205 "stylers": {
206 "color": "#00508b"
207 }
208 },
209 {
210 "featureType": "poi",
211 "elementType": "all",
212 "stylers": {
213 "visibility": "off"
214 }
215 },
216 {
217 "featureType": "green",
218 "elementType": "all",
219 "stylers": {
220 "color": "#056197",
221 "visibility": "off"
222 }
223 },
224 {
225 "featureType": "subway",
226 "elementType": "all",
227 "stylers": {
228 "visibility": "off"
229 }
230 },
231 {
232 "featureType": "manmade",
233 "elementType": "all",
234 "stylers": {
235 "visibility": "off"
236 }
237 },
238 {
239 "featureType": "local",
240 "elementType": "all",
241 "stylers": {
242 "visibility": "off"
243 }
244 },
245 {
246 "featureType": "arterial",
247 "elementType": "labels",
248 "stylers": {
249 "visibility": "off"
250 }
251 },
252 {
253 "featureType": "boundary",
254 "elementType": "geometry.fill",
255 "stylers": {
256 "color": "#029fd4"
257 }
258 },
259 {
260 "featureType": "building",
261 "elementType": "all",
262 "stylers": {
263 "color": "#1a5787"
264 }
265 },
266 {
267 "featureType": "label",
268 "elementType": "all",
269 "stylers": {
270 "visibility": "off"
271 }
272 }
273 ]
274 }
275 },
276 series : [
277 {
278 name: 'pm2.5',
279 type: 'scatter',
280 coordinateSystem: 'bmap',
281 data: this.convertData(this.data),
282 symbolSize: function (val: any) {
283 return val[2] / 10;
284 },
285 label: {
286 normal: {
287 formatter: '{b}',
288 position: 'right',
289 show: false
290 },
291 emphasis: {
292 show: true
293 }
294 },
295 itemStyle: {
296 normal: {
297 color: '#ddb926'
298 }
299 }
300 },
301 {
302 name: 'Top 5',
303 type: 'effectScatter',
304 coordinateSystem: 'bmap',
305 data: this.convertData(this.data.sort(function (a, b) {
306 return b.value - a.value;
307 }).slice(0, 6)),
308 symbolSize: function (val: any) {
309 return val[2] / 10;
310 },
311 showEffectOn: 'emphasis',
312 rippleEffect: {
313 brushType: 'stroke'
314 },
315 hoverAnimation: true,
316 label: {
317 normal: {
318 formatter: '{b}',
319 position: 'right',
320 show: true
321 }
322 },
323 itemStyle: {
324 normal: {
325 color: '#f4e925',
326 shadowBlur: 10,
327 shadowColor: '#333'
328 }
329 },
330 zlevel: 1
331 }
332 ]
333 };
334 }
335
336 onChartReadyCallback = (e: any) => {
337 console.log(e);
338 }
339 onEvent1 = (e: any) => {
340 console.log(e);
341 }
342 onEvent2 = (e: any) => {
343 console.log(e);
344 }
345
346 render() {
347
348 return (
349 <ReactEcharts
350 style={{height: '500px', width: '100%'}}
351 className='react_for_echarts'
352 option={this.state.option}
353 notMerge={true}
354 lazyUpdate={true}
355 theme={"dark"}
356 onChartReady={this.onChartReadyCallback}
357 onEvents={{click: this.onEvent1, focus: this.onEvent2}}
358 opts={{}} />
359 );
360 }
361 }
...\ No newline at end of file ...\ No newline at end of file
1 import { Avatar, Icon, Menu, Spin } from 'antd';
2 import { ClickParam } from 'antd/es/menu';
3 import { FormattedMessage } from 'umi-plugin-react/locale';
4 import React from 'react';
5 import { connect } from 'dva';
6 import router from 'umi/router';
7
8 import { ConnectProps, ConnectState } from '@/models/connect';
9 import { CurrentUser } from '@/models/user';
10 import HeaderDropdown from '../HeaderDropdown';
11 import styles from './index.less';
12
13 export interface GlobalHeaderRightProps extends ConnectProps {
14 currentUser?: CurrentUser;
15 menu?: boolean;
16 }
17
18 class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
19 onMenuClick = (event: ClickParam) => {
20 const { key } = event;
21
22 if (key === 'logout') {
23 const { dispatch } = this.props;
24 if (dispatch) {
25 dispatch({
26 type: 'login/logout',
27 });
28 }
29
30 return;
31 }
32 router.push(`/account/${key}`);
33 };
34
35 render(): React.ReactNode {
36 const { currentUser = { avatar: '', name: '' }, menu } = this.props;
37
38 const menuHeaderDropdown = (
39 <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
40 {menu && (
41 <Menu.Item key="center">
42 <Icon type="user" />
43 <FormattedMessage id="menu.account.center" defaultMessage="account center" />
44 </Menu.Item>
45 )}
46 {menu && (
47 <Menu.Item key="settings">
48 <Icon type="setting" />
49 <FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
50 </Menu.Item>
51 )}
52 {menu && <Menu.Divider />}
53
54 <Menu.Item key="logout">
55 <Icon type="logout" />
56 <FormattedMessage id="menu.account.logout" defaultMessage="logout" />
57 </Menu.Item>
58 </Menu>
59 );
60
61 return currentUser && currentUser.name ? (
62 <HeaderDropdown overlay={menuHeaderDropdown}>
63 <span className={`${styles.action} ${styles.account}`}>
64 <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
65 <span className={styles.name}>{currentUser.name}</span>
66 </span>
67 </HeaderDropdown>
68 ) : (
69 <Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
70 );
71 }
72 }
73 export default connect(({ user }: ConnectState) => ({
74 currentUser: user.currentUser,
75 }))(AvatarDropdown);
1 import React, { Component } from 'react';
2 import { Tag, message } from 'antd';
3 import { connect } from 'dva';
4 import { formatMessage } from 'umi-plugin-react/locale';
5 import groupBy from 'lodash/groupBy';
6 import moment from 'moment';
7
8 import { NoticeItem } from '@/models/global';
9 import NoticeIcon from '../NoticeIcon';
10 import { CurrentUser } from '@/models/user';
11 import { ConnectProps, ConnectState } from '@/models/connect';
12 import styles from './index.less';
13
14 export interface GlobalHeaderRightProps extends ConnectProps {
15 notices?: NoticeItem[];
16 currentUser?: CurrentUser;
17 fetchingNotices?: boolean;
18 onNoticeVisibleChange?: (visible: boolean) => void;
19 onNoticeClear?: (tabName?: string) => void;
20 }
21
22 class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
23 componentDidMount() {
24 const { dispatch } = this.props;
25 if (dispatch) {
26 dispatch({
27 type: 'global/fetchNotices',
28 });
29 }
30 }
31
32 changeReadState = (clickedItem: NoticeItem): void => {
33 const { id } = clickedItem;
34 const { dispatch } = this.props;
35 if (dispatch) {
36 dispatch({
37 type: 'global/changeNoticeReadState',
38 payload: id,
39 });
40 }
41 };
42
43 handleNoticeClear = (title: string, key: string) => {
44 const { dispatch } = this.props;
45 message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`);
46 if (dispatch) {
47 dispatch({
48 type: 'global/clearNotices',
49 payload: key,
50 });
51 }
52 };
53
54 getNoticeData = (): { [key: string]: NoticeItem[] } => {
55 const { notices = [] } = this.props;
56 if (notices.length === 0) {
57 return {};
58 }
59 const newNotices = notices.map(notice => {
60 const newNotice = { ...notice };
61 if (newNotice.datetime) {
62 newNotice.datetime = moment(notice.datetime as string).fromNow();
63 }
64 if (newNotice.id) {
65 newNotice.key = newNotice.id;
66 }
67 if (newNotice.extra && newNotice.status) {
68 const color = {
69 todo: '',
70 processing: 'blue',
71 urgent: 'red',
72 doing: 'gold',
73 }[newNotice.status];
74 newNotice.extra = (
75 <Tag color={color} style={{ marginRight: 0 }}>
76 {newNotice.extra}
77 </Tag>
78 );
79 }
80 return newNotice;
81 });
82 return groupBy(newNotices, 'type');
83 };
84
85 getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
86 const unreadMsg: { [key: string]: number } = {};
87 Object.keys(noticeData).forEach(key => {
88 const value = noticeData[key];
89 if (!unreadMsg[key]) {
90 unreadMsg[key] = 0;
91 }
92 if (Array.isArray(value)) {
93 unreadMsg[key] = value.filter(item => !item.read).length;
94 }
95 });
96 return unreadMsg;
97 };
98
99 render() {
100 const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
101 const noticeData = this.getNoticeData();
102 const unreadMsg = this.getUnreadData(noticeData);
103
104 return (
105 <NoticeIcon
106 className={styles.action}
107 count={currentUser && currentUser.unreadCount}
108 onItemClick={item => {
109 this.changeReadState(item as NoticeItem);
110 }}
111 loading={fetchingNotices}
112 clearText={formatMessage({ id: 'component.noticeIcon.clear' })}
113 viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })}
114 onClear={this.handleNoticeClear}
115 onPopupVisibleChange={onNoticeVisibleChange}
116 onViewMore={() => message.info('Click on view more')}
117 clearClose
118 >
119 <NoticeIcon.Tab
120 tabKey="notification"
121 count={unreadMsg.notification}
122 list={noticeData.notification}
123 title={formatMessage({ id: 'component.globalHeader.notification' })}
124 emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
125 showViewMore
126 />
127 <NoticeIcon.Tab
128 tabKey="message"
129 count={unreadMsg.message}
130 list={noticeData.message}
131 title={formatMessage({ id: 'component.globalHeader.message' })}
132 emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })}
133 showViewMore
134 />
135 <NoticeIcon.Tab
136 tabKey="event"
137 title={formatMessage({ id: 'component.globalHeader.event' })}
138 emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
139 count={unreadMsg.event}
140 list={noticeData.event}
141 showViewMore
142 />
143 </NoticeIcon>
144 );
145 }
146 }
147
148 export default connect(({ user, global, loading }: ConnectState) => ({
149 currentUser: user.currentUser,
150 collapsed: global.collapsed,
151 fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
152 fetchingNotices: loading.effects['global/fetchNotices'],
153 notices: global.notices,
154 }))(GlobalHeaderRight);
1 import { Icon, Tooltip } from 'antd';
2 import React from 'react';
3 import { connect } from 'dva';
4 import { formatMessage } from 'umi-plugin-react/locale';
5 import { ConnectProps, ConnectState } from '@/models/connect';
6
7 import Avatar from './AvatarDropdown';
8 import HeaderSearch from '../HeaderSearch';
9 import SelectLang from '../SelectLang';
10 import styles from './index.less';
11
12 export type SiderTheme = 'light' | 'dark';
13 export interface GlobalHeaderRightProps extends ConnectProps {
14 theme?: SiderTheme;
15 layout: 'sidemenu' | 'topmenu';
16 }
17
18 const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = props => {
19 const { theme, layout } = props;
20 let className = styles.right;
21
22 if (theme === 'dark' && layout === 'topmenu') {
23 className = `${styles.right} ${styles.dark}`;
24 }
25
26 return (
27 <div className={className}>
28 <HeaderSearch
29 className={`${styles.action} ${styles.search}`}
30 placeholder={formatMessage({
31 id: 'component.globalHeader.search',
32 })}
33 defaultValue="umi ui"
34 dataSource={[
35 formatMessage({
36 id: 'component.globalHeader.search.example1',
37 }),
38 formatMessage({
39 id: 'component.globalHeader.search.example2',
40 }),
41 formatMessage({
42 id: 'component.globalHeader.search.example3',
43 }),
44 ]}
45 onSearch={value => {
46 console.log('input', value);
47 }}
48 onPressEnter={value => {
49 console.log('enter', value);
50 }}
51 />
52 <Tooltip
53 title={formatMessage({
54 id: 'component.globalHeader.help',
55 })}
56 >
57 <a
58 target="_blank"
59 href="https://pro.ant.design/docs/getting-started"
60 rel="noopener noreferrer"
61 className={styles.action}
62 >
63 <Icon type="question-circle-o" />
64 </a>
65 </Tooltip>
66 <Avatar />
67 <SelectLang className={styles.action} />
68 </div>
69 );
70 };
71
72 export default connect(({ settings }: ConnectState) => ({
73 theme: settings.navTheme,
74 layout: settings.layout,
75 }))(GlobalHeaderRight);
1 @import '~antd/es/style/themes/default.less';
2
3 @pro-header-hover-bg: rgba(0, 0, 0, 0.025);
4
5 .menu {
6 :global(.anticon) {
7 margin-right: 8px;
8 }
9 :global(.ant-dropdown-menu-item) {
10 min-width: 160px;
11 }
12 }
13
14 .right {
15 float: right;
16 height: 100%;
17 margin-left: auto;
18 overflow: hidden;
19 .action {
20 display: inline-block;
21 height: 100%;
22 padding: 0 12px;
23 cursor: pointer;
24 transition: all 0.3s;
25 > i {
26 color: @text-color;
27 vertical-align: middle;
28 }
29 &:hover {
30 background: @pro-header-hover-bg;
31 }
32 &:global(.opened) {
33 background: @pro-header-hover-bg;
34 }
35 }
36 .search {
37 padding: 0 12px;
38 &:hover {
39 background: transparent;
40 }
41 }
42 .account {
43 .avatar {
44 margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
45 margin-right: 8px;
46 color: @primary-color;
47 vertical-align: top;
48 background: rgba(255, 255, 255, 0.85);
49 }
50 }
51 }
52
53 .dark {
54 height: @layout-header-height;
55 .action {
56 color: rgba(255, 255, 255, 0.85);
57 > i {
58 color: rgba(255, 255, 255, 0.85);
59 }
60 &:hover,
61 &:global(.opened) {
62 background: @primary-color;
63 }
64 }
65 }
66
67 :global(.ant-pro-global-header) {
68 .dark {
69 .action {
70 color: @text-color;
71 > i {
72 color: @text-color;
73 }
74 &:hover {
75 color: rgba(255, 255, 255, 0.85);
76 > i {
77 color: rgba(255, 255, 255, 0.85);
78 }
79 }
80 }
81 }
82 }
83
84 @media only screen and (max-width: @screen-md) {
85 :global(.ant-divider-vertical) {
86 vertical-align: unset;
87 }
88 .name {
89 display: none;
90 }
91 .right {
92 position: absolute;
93 top: 0;
94 right: 12px;
95 .account {
96 .avatar {
97 margin-right: 0;
98 }
99 }
100 }
101 }
1 @import '~antd/es/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 }
1 import { DropDownProps } from 'antd/es/dropdown';
2 import { Dropdown } from 'antd';
3 import React from 'react';
4 import classNames from 'classnames';
5 import styles from './index.less';
6
7 declare type OverlayFunc = () => React.ReactNode;
8
9 export interface HeaderDropdownProps extends DropDownProps {
10 overlayClassName?: string;
11 overlay: React.ReactNode | OverlayFunc;
12 placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
13 }
14
15 const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
16 <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
17 );
18
19 export default HeaderDropdown;
1 @import '~antd/es/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 }
1 import { AutoComplete, Icon, Input } from 'antd';
2 import { AutoCompleteProps, DataSourceItemType } from 'antd/es/auto-complete';
3 import React, { Component } from 'react';
4
5 import classNames from 'classnames';
6 import debounce from 'lodash/debounce';
7 import styles from './index.less';
8
9 export interface HeaderSearchProps {
10 onPressEnter: (value: string) => void;
11 onSearch: (value: string) => void;
12 onChange: (value: string) => void;
13 onVisibleChange: (b: boolean) => void;
14 className: string;
15 placeholder: string;
16 defaultActiveFirstOption: boolean;
17 dataSource: DataSourceItemType[];
18 defaultOpen: boolean;
19 open?: boolean;
20 defaultValue?: string;
21 }
22
23 interface HeaderSearchState {
24 value?: string;
25 searchMode: boolean;
26 }
27
28 export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
29 static defaultProps = {
30 defaultActiveFirstOption: false,
31 onPressEnter: () => {},
32 onSearch: () => {},
33 onChange: () => {},
34 className: '',
35 placeholder: '',
36 dataSource: [],
37 defaultOpen: false,
38 onVisibleChange: () => {},
39 };
40
41 static getDerivedStateFromProps(props: HeaderSearchProps) {
42 if ('open' in props) {
43 return {
44 searchMode: props.open,
45 };
46 }
47 return null;
48 }
49
50 private inputRef: Input | null = null;
51
52 constructor(props: HeaderSearchProps) {
53 super(props);
54 this.state = {
55 searchMode: props.defaultOpen,
56 value: props.defaultValue,
57 };
58 this.debouncePressEnter = debounce(this.debouncePressEnter, 500, {
59 leading: true,
60 trailing: false,
61 });
62 }
63
64 onKeyDown = (e: React.KeyboardEvent) => {
65 if (e.key === 'Enter') {
66 this.debouncePressEnter();
67 }
68 };
69
70 onChange: AutoCompleteProps['onChange'] = value => {
71 if (typeof value === 'string') {
72 const { onSearch, onChange } = this.props;
73 this.setState({ value });
74 if (onSearch) {
75 onSearch(value);
76 }
77 if (onChange) {
78 onChange(value);
79 }
80 }
81 };
82
83 enterSearchMode = () => {
84 const { onVisibleChange } = this.props;
85 onVisibleChange(true);
86 this.setState({ searchMode: true }, () => {
87 const { searchMode } = this.state;
88 if (searchMode && this.inputRef) {
89 this.inputRef.focus();
90 }
91 });
92 };
93
94 leaveSearchMode = () => {
95 this.setState({
96 searchMode: false,
97 });
98 };
99
100 debouncePressEnter = () => {
101 const { onPressEnter } = this.props;
102 const { value } = this.state;
103 onPressEnter(value || '');
104 };
105
106 render() {
107 const { className, defaultValue, placeholder, open, ...restProps } = this.props;
108 const { searchMode, value } = this.state;
109 delete restProps.defaultOpen; // for rc-select not affected
110 const inputClass = classNames(styles.input, {
111 [styles.show]: searchMode,
112 });
113
114 return (
115 <span
116 className={classNames(className, styles.headerSearch)}
117 onClick={this.enterSearchMode}
118 onTransitionEnd={({ propertyName }) => {
119 if (propertyName === 'width' && !searchMode) {
120 const { onVisibleChange } = this.props;
121 onVisibleChange(searchMode);
122 }
123 }}
124 >
125 <Icon type="search" key="Icon" />
126 <AutoComplete
127 key="AutoComplete"
128 {...restProps}
129 className={inputClass}
130 value={value}
131 onChange={this.onChange}
132 >
133 <Input
134 ref={node => {
135 this.inputRef = node;
136 }}
137 defaultValue={defaultValue}
138 aria-label={placeholder}
139 placeholder={placeholder}
140 onKeyDown={this.onKeyDown}
141 onBlur={this.leaveSearchMode}
142 />
143 </AutoComplete>
144 </span>
145 );
146 }
147 }
1 @import '~antd/es/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;
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 .bottomBar {
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 transition: all 0.3s;
89 div {
90 display: inline-block;
91 width: 50%;
92 cursor: pointer;
93 transition: all 0.3s;
94 user-select: none;
95 &:hover {
96 color: @heading-color;
97 }
98 &:only-child {
99 width: 100%;
100 }
101 &:not(:only-child):last-child {
102 border-left: 1px solid @border-color-split;
103 }
104 }
105 }
1 import { Avatar, List } from 'antd';
2
3 import React from 'react';
4 import classNames from 'classnames';
5 import { NoticeIconData } from './index';
6 import styles from './NoticeList.less';
7
8 export interface NoticeIconTabProps {
9 count?: number;
10 name?: string;
11 showClear?: boolean;
12 showViewMore?: boolean;
13 style?: React.CSSProperties;
14 title: string;
15 tabKey: string;
16 data?: NoticeIconData[];
17 onClick?: (item: NoticeIconData) => void;
18 onClear?: () => void;
19 emptyText?: string;
20 clearText?: string;
21 viewMoreText?: string;
22 list: NoticeIconData[];
23 onViewMore?: (e: any) => void;
24 }
25 const NoticeList: React.SFC<NoticeIconTabProps> = ({
26 data = [],
27 onClick,
28 onClear,
29 title,
30 onViewMore,
31 emptyText,
32 showClear = true,
33 clearText,
34 viewMoreText,
35 showViewMore = false,
36 }) => {
37 if (data.length === 0) {
38 return (
39 <div className={styles.notFound}>
40 <img
41 src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
42 alt="not found"
43 />
44 <div>{emptyText}</div>
45 </div>
46 );
47 }
48 return (
49 <div>
50 <List<NoticeIconData>
51 className={styles.list}
52 dataSource={data}
53 renderItem={(item, i) => {
54 const itemCls = classNames(styles.item, {
55 [styles.read]: item.read,
56 });
57 // eslint-disable-next-line no-nested-ternary
58 const leftIcon = item.avatar ? (
59 typeof item.avatar === 'string' ? (
60 <Avatar className={styles.avatar} src={item.avatar} />
61 ) : (
62 <span className={styles.iconElement}>{item.avatar}</span>
63 )
64 ) : null;
65
66 return (
67 <List.Item
68 className={itemCls}
69 key={item.key || i}
70 onClick={() => onClick && onClick(item)}
71 >
72 <List.Item.Meta
73 className={styles.meta}
74 avatar={leftIcon}
75 title={
76 <div className={styles.title}>
77 {item.title}
78 <div className={styles.extra}>{item.extra}</div>
79 </div>
80 }
81 description={
82 <div>
83 <div className={styles.description}>{item.description}</div>
84 <div className={styles.datetime}>{item.datetime}</div>
85 </div>
86 }
87 />
88 </List.Item>
89 );
90 }}
91 />
92 <div className={styles.bottomBar}>
93 {showClear ? (
94 <div onClick={onClear}>
95 {clearText} {title}
96 </div>
97 ) : null}
98 {showViewMore ? (
99 <div
100 onClick={e => {
101 if (onViewMore) {
102 onViewMore(e);
103 }
104 }}
105 >
106 {viewMoreText}
107 </div>
108 ) : null}
109 </div>
110 </div>
111 );
112 };
113
114 export default NoticeList;
1 @import '~antd/es/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 }
1 import { Badge, Icon, Spin, Tabs } from 'antd';
2 import React, { Component } from 'react';
3 import classNames from 'classnames';
4 import NoticeList, { NoticeIconTabProps } from './NoticeList';
5
6 import HeaderDropdown from '../HeaderDropdown';
7 import styles from './index.less';
8
9 const { TabPane } = Tabs;
10
11 export interface NoticeIconData {
12 avatar?: string | React.ReactNode;
13 title?: React.ReactNode;
14 description?: React.ReactNode;
15 datetime?: React.ReactNode;
16 extra?: React.ReactNode;
17 style?: React.CSSProperties;
18 key?: string | number;
19 read?: boolean;
20 }
21
22 export interface NoticeIconProps {
23 count?: number;
24 bell?: React.ReactNode;
25 className?: string;
26 loading?: boolean;
27 onClear?: (tabName: string, tabKey: string) => void;
28 onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void;
29 onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
30 onTabChange?: (tabTile: string) => void;
31 style?: React.CSSProperties;
32 onPopupVisibleChange?: (visible: boolean) => void;
33 popupVisible?: boolean;
34 clearText?: string;
35 viewMoreText?: string;
36 clearClose?: boolean;
37 children: React.ReactElement<NoticeIconTabProps>[];
38 }
39
40 export default class NoticeIcon extends Component<NoticeIconProps> {
41 public static Tab: typeof NoticeList = NoticeList;
42
43 static defaultProps = {
44 onItemClick: (): void => {},
45 onPopupVisibleChange: (): void => {},
46 onTabChange: (): void => {},
47 onClear: (): void => {},
48 onViewMore: (): void => {},
49 loading: false,
50 clearClose: false,
51 emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
52 };
53
54 state = {
55 visible: false,
56 };
57
58 onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => {
59 const { onItemClick } = this.props;
60 if (onItemClick) {
61 onItemClick(item, tabProps);
62 }
63 };
64
65 onClear = (name: string, key: string): void => {
66 const { onClear } = this.props;
67 if (onClear) {
68 onClear(name, key);
69 }
70 };
71
72 onTabChange = (tabType: string): void => {
73 const { onTabChange } = this.props;
74 if (onTabChange) {
75 onTabChange(tabType);
76 }
77 };
78
79 onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => {
80 const { onViewMore } = this.props;
81 if (onViewMore) {
82 onViewMore(tabProps, event);
83 }
84 };
85
86 getNotificationBox(): React.ReactNode {
87 const { children, loading, clearText, viewMoreText } = this.props;
88 if (!children) {
89 return null;
90 }
91 const panes = React.Children.map(
92 children,
93 (child: React.ReactElement<NoticeIconTabProps>): React.ReactNode => {
94 if (!child) {
95 return null;
96 }
97 const { list, title, count, tabKey, showClear, showViewMore } = child.props;
98 const len = list && list.length ? list.length : 0;
99 const msgCount = count || count === 0 ? count : len;
100 const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
101 return (
102 <TabPane tab={tabTitle} key={title}>
103 <NoticeList
104 clearText={clearText}
105 viewMoreText={viewMoreText}
106 data={list}
107 onClear={(): void => this.onClear(title, tabKey)}
108 onClick={(item): void => this.onItemClick(item, child.props)}
109 onViewMore={(event): void => this.onViewMore(child.props, event)}
110 showClear={showClear}
111 showViewMore={showViewMore}
112 title={title}
113 {...child.props}
114 />
115 </TabPane>
116 );
117 },
118 );
119 return (
120 <>
121 <Spin spinning={loading} delay={300}>
122 <Tabs className={styles.tabs} onChange={this.onTabChange}>
123 {panes}
124 </Tabs>
125 </Spin>
126 </>
127 );
128 }
129
130 handleVisibleChange = (visible: boolean): void => {
131 const { onPopupVisibleChange } = this.props;
132 this.setState({ visible });
133 if (onPopupVisibleChange) {
134 onPopupVisibleChange(visible);
135 }
136 };
137
138 render(): React.ReactNode {
139 const { className, count, popupVisible, bell } = this.props;
140 const { visible } = this.state;
141 const noticeButtonClass = classNames(className, styles.noticeButton);
142 const notificationBox = this.getNotificationBox();
143 const NoticeBellIcon = bell || <Icon type="bell" className={styles.icon} />;
144 const trigger = (
145 <span className={classNames(noticeButtonClass, { opened: visible })}>
146 <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
147 {NoticeBellIcon}
148 </Badge>
149 </span>
150 );
151 if (!notificationBox) {
152 return trigger;
153 }
154 const popoverProps: {
155 visible?: boolean;
156 } = {};
157 if ('popupVisible' in this.props) {
158 popoverProps.visible = popupVisible;
159 }
160
161 return (
162 <HeaderDropdown
163 placement="bottomRight"
164 overlay={notificationBox}
165 overlayClassName={styles.popover}
166 trigger={['click']}
167 visible={visible}
168 onVisibleChange={this.handleVisibleChange}
169 {...popoverProps}
170 >
171 {trigger}
172 </HeaderDropdown>
173 );
174 }
175 }
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 const PageLoading: React.FC = () => (
7 <div style={{ paddingTop: 100, textAlign: 'center' }}>
8 <Spin size="large" />
9 </div>
10 );
11 export default PageLoading;
1 @import '~antd/es/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 }
1 import { Icon, Menu } from 'antd';
2 import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale';
3
4 import { ClickParam } from 'antd/es/menu';
5 import React from 'react';
6 import classNames from 'classnames';
7 import HeaderDropdown from '../HeaderDropdown';
8 import styles from './index.less';
9
10 interface SelectLangProps {
11 className?: string;
12 }
13 const SelectLang: React.FC<SelectLangProps> = props => {
14 const { className } = props;
15 const selectedLang = getLocale();
16 const changeLang = ({ key }: ClickParam): void => setLocale(key);
17 const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
18 const languageLabels = {
19 'zh-CN': '简体中文',
20 'zh-TW': '繁体中文',
21 'en-US': 'English',
22 'pt-BR': 'Português',
23 };
24 const languageIcons = {
25 'zh-CN': '🇨🇳',
26 'zh-TW': '🇭🇰',
27 'en-US': '🇺🇸',
28 'pt-BR': '🇧🇷',
29 };
30 const langMenu = (
31 <Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
32 {locales.map(locale => (
33 <Menu.Item key={locale}>
34 <span role="img" aria-label={languageLabels[locale]}>
35 {languageIcons[locale]}
36 </span>{' '}
37 {languageLabels[locale]}
38 </Menu.Item>
39 ))}
40 </Menu>
41 );
42 return (
43 <HeaderDropdown overlay={langMenu} placement="bottomRight">
44 <span className={classNames(styles.dropDown, className)}>
45 <Icon type="global" title={formatMessage({ id: 'navBar.lang' })} />
46 </span>
47 </HeaderDropdown>
48 );
49 };
50
51 export default SelectLang;
1 // eslint-disable-next-line eslint-comments/disable-enable-pair
2 /* eslint-disable import/no-extraneous-dependencies */
3 import client from 'webpack-theme-color-replacer/client';
4 import generate from '@ant-design/colors/lib/generate';
5
6 export default {
7 getAntdSerials(color: string): string[] {
8 const lightCount = 9;
9 const divide = 10;
10 // 淡化(即less的tint)
11 let lightens = new Array(lightCount).fill(0);
12 lightens = lightens.map((_, i) => client.varyColor.lighten(color, i / divide));
13 const colorPalettes = generate(color);
14 const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',');
15 return lightens.concat(colorPalettes).concat(rgb);
16 },
17 changeColor(color?: string): Promise<void> {
18 if (!color) {
19 return Promise.resolve();
20 }
21 const options = {
22 // new colors array, one-to-one corresponde with `matchColors`
23 newColors: this.getAntdSerials(color),
24 changeUrl(cssUrl: string): string {
25 // while router is not `hash` mode, it needs absolute path
26 return `/${cssUrl}`;
27 },
28 };
29 return client.changer.changeColor(options, Promise);
30 },
31 };
1 @import '~antd/es/style/themes/default.less';
2
3 .standardTable {
4 :global {
5 .ant-table-pagination {
6 margin-top: 24px;
7 }
8 }
9
10 .tableAlert {
11 margin-bottom: 16px;
12 }
13 }
1 import { Alert, Table } from 'antd';
2 import { ColumnProps, TableRowSelection, TableProps } from 'antd/es/table';
3 import React, { Component, Fragment } from 'react';
4
5 import { TableListItem } from '../../data.d';
6 import styles from './index.less';
7
8 type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
9
10 export interface StandardTableProps<T> extends Omit<TableProps<T>, 'columns'> {
11 columns: StandardTableColumnProps[];
12 data: {
13 list: TableListItem[];
14 pagination: StandardTableProps<TableListItem>['pagination'];
15 };
16 selectedRows: TableListItem[];
17 onSelectRow: (rows: any) => void;
18 }
19
20 export interface StandardTableColumnProps extends ColumnProps<TableListItem> {
21 needTotal?: boolean;
22 total?: number;
23 }
24
25 function initTotalList(columns: StandardTableColumnProps[]) {
26 if (!columns) {
27 return [];
28 }
29 const totalList: StandardTableColumnProps[] = [];
30 columns.forEach(column => {
31 if (column.needTotal) {
32 totalList.push({ ...column, total: 0 });
33 }
34 });
35 return totalList;
36 }
37
38 interface StandardTableState {
39 selectedRowKeys: string[];
40 needTotalList: StandardTableColumnProps[];
41 }
42
43 class StandardTable extends Component<StandardTableProps<TableListItem>, StandardTableState> {
44 static getDerivedStateFromProps(nextProps: StandardTableProps<TableListItem>) {
45 // clean state
46 if (nextProps.selectedRows.length === 0) {
47 const needTotalList = initTotalList(nextProps.columns);
48 return {
49 selectedRowKeys: [],
50 needTotalList,
51 };
52 }
53 return null;
54 }
55
56 constructor(props: StandardTableProps<TableListItem>) {
57 super(props);
58 const { columns } = props;
59 const needTotalList = initTotalList(columns);
60
61 this.state = {
62 selectedRowKeys: [],
63 needTotalList,
64 };
65 }
66
67 handleRowSelectChange: TableRowSelection<TableListItem>['onChange'] = (
68 selectedRowKeys,
69 selectedRows: TableListItem[],
70 ) => {
71 const currySelectedRowKeys = selectedRowKeys as string[];
72 let { needTotalList } = this.state;
73 needTotalList = needTotalList.map(item => ({
74 ...item,
75 total: selectedRows.reduce((sum, val) => sum + parseFloat(val[item.dataIndex || 0]), 0),
76 }));
77 const { onSelectRow } = this.props;
78 if (onSelectRow) {
79 onSelectRow(selectedRows);
80 }
81
82 this.setState({ selectedRowKeys: currySelectedRowKeys, needTotalList });
83 };
84
85 handleTableChange: TableProps<TableListItem>['onChange'] = (
86 pagination,
87 filters,
88 sorter,
89 ...rest
90 ) => {
91 const { onChange } = this.props;
92 if (onChange) {
93 onChange(pagination, filters, sorter, ...rest);
94 }
95 };
96
97 cleanSelectedKeys = () => {
98 if (this.handleRowSelectChange) {
99 this.handleRowSelectChange([], []);
100 }
101 };
102
103 render() {
104 const { selectedRowKeys, needTotalList } = this.state;
105 const { data, rowKey, ...rest } = this.props;
106 const { list = [], pagination = false } = data || {};
107
108 const paginationProps = pagination
109 ? {
110 showSizeChanger: true,
111 showQuickJumper: true,
112 ...pagination,
113 }
114 : false;
115
116 const rowSelection: TableRowSelection<TableListItem> = {
117 selectedRowKeys,
118 onChange: this.handleRowSelectChange,
119 getCheckboxProps: (record: TableListItem) => ({
120 disabled: record.disabled,
121 }),
122 };
123
124 return (
125 <div className={styles.standardTable}>
126 <div className={styles.tableAlert}>
127 <Alert
128 message={
129 <Fragment>
130 已选择 <a style={{ fontWeight: 600 }}>{selectedRowKeys.length}</a> 项&nbsp;&nbsp;
131 {needTotalList.map((item, index) => (
132 <span style={{ marginLeft: 8 }} key={item.dataIndex}>
133 {item.title}
134 总计&nbsp;
135 <span style={{ fontWeight: 600 }}>
136 {item.render
137 ? item.render(item.total, item as TableListItem, index)
138 : item.total}
139 </span>
140 </span>
141 ))}
142 <a onClick={this.cleanSelectedKeys} style={{ marginLeft: 24 }}>
143 清空
144 </a>
145 </Fragment>
146 }
147 type="info"
148 showIcon
149 />
150 </div>
151 <Table
152 rowKey={rowKey || 'key'}
153 rowSelection={rowSelection}
154 dataSource={list}
155 pagination={paginationProps}
156 onChange={this.handleTableChange}
157 {...rest}
158 />
159 </div>
160 );
161 }
162 }
163
164 export default StandardTable;
1 const { uniq } = require('lodash');
2 const RouterConfig = require('../../config/config').default.routes;
3
4 const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
5
6 function formatter(routes, parentPath = '') {
7 const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
8 let result = [];
9 routes.forEach(item => {
10 if (item.path) {
11 result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
12 }
13 if (item.routes) {
14 result = result.concat(
15 formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
16 );
17 }
18 });
19 return uniq(result.filter(item => !!item));
20 }
21
22 describe('Ant Design Pro E2E test', () => {
23 const testPage = path => async () => {
24 await page.goto(`${BASE_URL}${path}`);
25 await page.waitForSelector('footer', {
26 timeout: 2000,
27 });
28 const haveFooter = await page.evaluate(
29 () => document.getElementsByTagName('footer').length > 0,
30 );
31 expect(haveFooter).toBeTruthy();
32 };
33
34 const routers = formatter(RouterConfig);
35 console.log('routers', routers);
36 routers.forEach(route => {
37 it(`test pages ${route}`, testPage(route));
38 });
39 });
1 const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
2
3 describe('Homepage', () => {
4 it('topmenu should have footer', async () => {
5 const params = '/form/basic-form?navTheme=light&layout=topmenu';
6 await page.goto(`${BASE_URL}${params}`);
7 await page.waitForSelector('footer', {
8 timeout: 2000,
9 });
10 const haveFooter = await page.evaluate(
11 () => document.getElementsByTagName('footer').length > 0,
12 );
13 expect(haveFooter).toBeTruthy();
14 });
15 });
1 @import '~antd/es/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 ul,
28 ol {
29 list-style: none;
30 }
31
32 @media (max-width: @screen-xs) {
33 .ant-table {
34 width: 100%;
35 overflow-x: auto;
36 &-thead > tr,
37 &-tbody > tr {
38 > th,
39 > td {
40 white-space: pre;
41 > span {
42 display: block;
43 }
44 }
45 }
46 }
47 }
48
49 .ant-layout-header {
50 height: 32px;
51 line-height: 32px;
52 }
53 .ant-pro-global-header {
54 height: 32px;
55 }
56 .ant-pro-global-header-trigger {
57 height: 32px;
58 padding: calc((32px - 26px) / 2) 24px;
59 }
60 .antd-pro-components-global-header-index-right .antd-pro-components-global-header-index-account .antd-pro-components-global-header-index-avatar {
61 margin: calc((32px - 24px) / 2) 0;
62 }
63
64 .ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
65 padding-top: 32px;
66 }
67 .ant-page-header.has-breadcrumb {
68 padding-top: 5px;
69 }
70 .ant-page-header {
71 padding: 0 24px;
72 }
73 .ant-breadcrumb + .ant-page-header-heading {
74 margin-top: 0;
75 }
76 .antd-pro-components-select-lang-index-dropDown {
77 line-height: 32px;
78 }
79
80 .ant-page-header-heading-title {
81 font-size: 16px;
82 }
83 .ant-layout-footer {
84 padding: 24px 5px;
85 background: #fff;
86 }
87
88 .ant-pro-global-footer {
89 margin: 10px 0;
90 }
91
92 .ant-pro-global-footer-links {
93 margin-bottom: 5px;
94 }
...\ No newline at end of file ...\ No newline at end of file
1 import { Button, message, notification } from 'antd';
2
3 import React from 'react';
4 import { formatMessage } from 'umi-plugin-react/locale';
5 import defaultSettings from '../config/defaultSettings';
6
7 const { pwa } = defaultSettings;
8 // if pwa is true
9 if (pwa) {
10 // Notify user if offline now
11 window.addEventListener('sw.offline', () => {
12 message.warning(formatMessage({ id: 'app.pwa.offline' }));
13 });
14
15 // Pop up a prompt on the page asking the user if they want to use the latest version
16 window.addEventListener('sw.updated', (event: Event) => {
17 const e = event as CustomEvent;
18 const reloadSW = async () => {
19 // Check if there is sw whose state is waiting in ServiceWorkerRegistration
20 // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
21 const worker = e.detail && e.detail.waiting;
22 if (!worker) {
23 return true;
24 }
25 // Send skip-waiting event to waiting SW with MessageChannel
26 await new Promise((resolve, reject) => {
27 const channel = new MessageChannel();
28 channel.port1.onmessage = msgEvent => {
29 if (msgEvent.data.error) {
30 reject(msgEvent.data.error);
31 } else {
32 resolve(msgEvent.data);
33 }
34 };
35 worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
36 });
37 // Refresh current page to use the updated HTML and other assets after SW has skiped waiting
38 window.location.reload(true);
39 return true;
40 };
41 const key = `open${Date.now()}`;
42 const btn = (
43 <Button
44 type="primary"
45 onClick={() => {
46 notification.close(key);
47 reloadSW();
48 }}
49 >
50 {formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
51 </Button>
52 );
53 notification.open({
54 message: formatMessage({ id: 'app.pwa.serviceworker.updated' }),
55 description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
56 btn,
57 key,
58 onClose: async () => {},
59 });
60 });
61 } else if ('serviceWorker' in navigator) {
62 // unregister service worker
63 const { serviceWorker } = navigator;
64 if (serviceWorker.getRegistrations) {
65 serviceWorker.getRegistrations().then(sws => {
66 sws.forEach(sw => {
67 sw.unregister();
68 });
69 });
70 }
71 serviceWorker.getRegistration().then(sw => {
72 if (sw) sw.unregister();
73 });
74
75 // remove all caches
76 if (window.caches && window.caches.keys) {
77 caches.keys().then(keys => {
78 keys.forEach(key => {
79 caches.delete(key);
80 });
81 });
82 }
83 }
1 /**
2 * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
3 * You can view component api by:
4 * https://github.com/ant-design/ant-design-pro-layout
5 */
6
7 import ProLayout, {
8 MenuDataItem,
9 BasicLayoutProps as ProLayoutProps,
10 Settings,
11 DefaultFooter,
12 } from '@ant-design/pro-layout';
13 import React, { useEffect } from 'react';
14 import Link from 'umi/link';
15 import { Dispatch } from 'redux';
16 import { connect } from 'dva';
17 import { Icon, Result, Button } from 'antd';
18 import { formatMessage } from 'umi-plugin-react/locale';
19
20 import Authorized from '@/utils/Authorized';
21 import RightContent from '@/components/GlobalHeader/RightContent';
22 import { ConnectState } from '@/models/connect';
23 import { isAntDesignPro, getAuthorityFromRouter } from '@/utils/utils';
24 import logo from '../assets/logo.svg';
25
26 const noMatch = (
27 <Result
28 status="403"
29 title="403"
30 subTitle="Sorry, you are not authorized to access this page."
31 extra={
32 <Button type="primary">
33 <Link to="/user/login">Go Login</Link>
34 </Button>
35 }
36 />
37 );
38
39 export interface BasicLayoutProps extends ProLayoutProps {
40 breadcrumbNameMap: {
41 [path: string]: MenuDataItem;
42 };
43 route: ProLayoutProps['route'] & {
44 authority: string[];
45 };
46 settings: Settings;
47 dispatch: Dispatch;
48 }
49 export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
50 breadcrumbNameMap: {
51 [path: string]: MenuDataItem;
52 };
53 };
54
55 /**
56 * use Authorized check all menu item
57 */
58 const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
59 menuList.map(item => {
60 const localItem = {
61 ...item,
62 children: item.children ? menuDataRender(item.children) : [],
63 };
64 return Authorized.check(item.authority, localItem, null) as MenuDataItem;
65 });
66
67 const defaultFooterDom = (
68 <DefaultFooter
69 styles="margin: 0 0 24px 0; padding: 0 5px"
70 copyright="2017 - 2019 利姆斯(北京)区块链技术有限公司 京ICP备18046899号-1 京网信备1101091997877663001X号"
71 links={[
72 {
73 key: 'LIMSChain',
74 title: 'LIMSChain 基于区块链的检验检测监管平台 v1.0',
75 href: 'http://limschain.com',
76 blankTarget: true,
77 }
78 ]}
79 />
80 );
81
82 const footerRender: BasicLayoutProps['footerRender'] = () => {
83 if (!isAntDesignPro()) {
84 return defaultFooterDom;
85 }
86 return (
87 <>
88 {defaultFooterDom}
89 </>
90 );
91 };
92
93 const BasicLayout: React.FC<BasicLayoutProps> = props => {
94 const { dispatch, children, settings, location = { pathname: '/' } } = props;
95 /**
96 * constructor
97 */
98
99 useEffect(() => {
100 if (dispatch) {
101 dispatch({
102 type: 'user/fetchCurrent',
103 });
104 dispatch({
105 type: 'settings/getSetting',
106 });
107 }
108 }, []);
109 /**
110 * init variables
111 */
112 const handleMenuCollapse = (payload: boolean): void => {
113 if (dispatch) {
114 dispatch({
115 type: 'global/changeLayoutCollapsed',
116 payload,
117 });
118 }
119 };
120 // get children authority
121 const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
122 authority: undefined,
123 };
124
125 return (
126 <ProLayout
127 logo={logo}
128 menuHeaderRender={(logoDom, titleDom) => (
129 <Link to="/">
130 {logoDom}
131 {titleDom}
132 </Link>
133 )}
134 onCollapse={handleMenuCollapse}
135 menuItemRender={(menuItemProps, defaultDom) => {
136 if (menuItemProps.isUrl || menuItemProps.children) {
137 return defaultDom;
138 }
139 return <Link to={menuItemProps.path}>{defaultDom}</Link>;
140 }}
141 breadcrumbRender={(routers = []) => [
142 {
143 path: '/',
144 breadcrumbName: formatMessage({
145 id: 'menu.home',
146 defaultMessage: 'Home',
147 }),
148 },
149 ...routers,
150 ]}
151 itemRender={(route, params, routes, paths) => {
152 const first = routes.indexOf(route) === 0;
153 return first ? (
154 <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
155 ) : (
156 <span>{route.breadcrumbName}</span>
157 );
158 }}
159 footerRender={footerRender}
160 menuDataRender={menuDataRender}
161 formatMessage={formatMessage}
162 rightContentRender={rightProps => <RightContent {...rightProps} />}
163 {...props}
164 {...settings}
165 >
166 <Authorized authority={authorized!.authority} noMatch={noMatch}>
167 {children}
168 </Authorized>
169 </ProLayout>
170 );
171 };
172
173 export default connect(({ global, settings }: ConnectState) => ({
174 collapsed: global.collapsed,
175 settings,
176 }))(BasicLayout);
1 import React from 'react';
2
3 const Layout: React.FC = ({ children }) => <div>{children}</div>;
4
5 export default Layout;
1 import React from 'react';
2 import { connect } from 'dva';
3 import { Redirect } from 'umi';
4 import { stringify } from 'querystring';
5 import { ConnectState, ConnectProps } from '@/models/connect';
6 import { CurrentUser } from '@/models/user';
7 import PageLoading from '@/components/PageLoading';
8
9 interface SecurityLayoutProps extends ConnectProps {
10 loading: boolean;
11 currentUser: CurrentUser;
12 }
13
14 interface SecurityLayoutState {
15 isReady: boolean;
16 }
17
18 class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
19 state: SecurityLayoutState = {
20 isReady: false,
21 };
22
23 componentDidMount() {
24 this.setState({
25 isReady: true,
26 });
27 const { dispatch } = this.props;
28 if (dispatch) {
29 dispatch({
30 type: 'user/fetchCurrent',
31 });
32 }
33 }
34
35 render() {
36 const { isReady } = this.state;
37 const { children, loading, currentUser } = this.props;
38 // You can replace it to your authentication rule (such as check token exists)
39 // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
40 const isLogin = currentUser && currentUser.userid;
41 const queryString = stringify({
42 redirect: window.location.href,
43 });
44
45 if ((!isLogin && loading) || !isReady) {
46 return <PageLoading />;
47 }
48 if (!isLogin) {
49 return <Redirect to={`/user/login?${queryString}`}></Redirect>;
50 }
51 return children;
52 }
53 }
54
55 export default connect(({ user, loading }: ConnectState) => ({
56 currentUser: user.currentUser,
57 loading: loading.models.user,
58 }))(SecurityLayout);
1 @import '~antd/es/style/themes/default.less';
2
3 .container {
4 display: flex;
5 flex-direction: column;
6 height: 100vh;
7 overflow: auto;
8 background: @layout-body-background;
9 }
10
11 .lang {
12 width: 100%;
13 height: 40px;
14 line-height: 44px;
15 text-align: right;
16 :global(.ant-dropdown-trigger) {
17 margin-right: 24px;
18 }
19 }
20
21 .content {
22 flex: 1;
23 padding: 32px 0;
24 }
25
26 @media (min-width: @screen-md-min) {
27 .container {
28 background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
29 background-repeat: no-repeat;
30 background-position: center 110px;
31 background-size: 100%;
32 }
33
34 .content {
35 padding: 32px 0 24px;
36 }
37 }
38
39 .top {
40 text-align: center;
41 }
42
43 .header {
44 height: 44px;
45 line-height: 44px;
46 a {
47 text-decoration: none;
48 }
49 }
50
51 .logo {
52 height: 44px;
53 margin-right: 16px;
54 vertical-align: top;
55 }
56
57 .title {
58 position: relative;
59 top: 2px;
60 color: @heading-color;
61 font-weight: 600;
62 font-size: 33px;
63 font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
64 }
65
66 .desc {
67 margin-top: 12px;
68 margin-bottom: 40px;
69 color: @text-color-secondary;
70 font-size: @font-size-base;
71 }
1 import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-design/pro-layout';
2 import { Helmet } from 'react-helmet';
3 import Link from 'umi/link';
4 import React from 'react';
5 import { connect } from 'dva';
6 import { formatMessage } from 'umi-plugin-react/locale';
7
8 import SelectLang from '@/components/SelectLang';
9 import { ConnectProps, ConnectState } from '@/models/connect';
10 import logo from '../assets/logo.svg';
11 import styles from './UserLayout.less';
12
13 export interface UserLayoutProps extends ConnectProps {
14 breadcrumbNameMap: { [path: string]: MenuDataItem };
15 }
16
17 const UserLayout: React.SFC<UserLayoutProps> = props => {
18 const {
19 route = {
20 routes: [],
21 },
22 } = props;
23 const { routes = [] } = route;
24 const {
25 children,
26 location = {
27 pathname: '',
28 },
29 } = props;
30 const { breadcrumb } = getMenuData(routes);
31 const title = getPageTitle({
32 pathname: location.pathname,
33 breadcrumb,
34 formatMessage,
35 ...props,
36 });
37 return (
38 <>
39 <Helmet>
40 <title>{title}</title>
41 <meta name="description" content={title} />
42 </Helmet>
43
44 <div className={styles.container}>
45 <div className={styles.lang}>
46 <SelectLang />
47 </div>
48 <div className={styles.content}>
49 <div className={styles.top}>
50 <div className={styles.header}>
51 <Link to="/">
52 <img alt="logo" className={styles.logo} src={logo} />
53 <span className={styles.title}>Ant Design</span>
54 </Link>
55 </div>
56 <div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
57 </div>
58 {children}
59 </div>
60 <DefaultFooter />
61 </div>
62 </>
63 );
64 };
65
66 export default connect(({ settings }: ConnectState) => ({
67 ...settings,
68 }))(UserLayout);
1 import component from './en-US/component';
2 import globalHeader from './en-US/globalHeader';
3 import menu from './en-US/menu';
4 import pwa from './en-US/pwa';
5 import settingDrawer from './en-US/settingDrawer';
6 import settings from './en-US/settings';
7
8 export default {
9 'navBar.lang': 'Languages',
10 'layout.user.link.help': 'Help',
11 'layout.user.link.privacy': 'Privacy',
12 'layout.user.link.terms': 'Terms',
13 'app.preview.down.block': 'Download this page to your local project',
14 'app.welcome.link.fetch-blocks': 'Get all block',
15 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',
16 ...globalHeader,
17 ...menu,
18 ...settingDrawer,
19 ...settings,
20 ...pwa,
21 ...component,
22 };
1 export default {
2 'component.tagSelect.expand': 'Expand',
3 'component.tagSelect.collapse': 'Collapse',
4 'component.tagSelect.all': 'All',
5 };
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.view-more': 'View more',
17 };
1 export default {
2 'menu.welcome': 'Welcome',
3 'menu.more-blocks': 'More Blocks',
4 'menu.home': 'Home',
5 'menu.admin': 'admin',
6 'menu.login': 'Login',
7 'menu.register': 'Register',
8 'menu.register.result': 'Register Result',
9 'menu.dashboard': 'Dashboard',
10 'menu.dashboard.analysis': 'Analysis',
11 'menu.dashboard.monitor': 'Monitor',
12 'menu.dashboard.workplace': 'Workplace',
13 'menu.exception.403': '403',
14 'menu.exception.404': '404',
15 'menu.exception.500': '500',
16 'menu.form': 'Form',
17 'menu.form.basic-form': 'Basic Form',
18 'menu.form.step-form': 'Step Form',
19 'menu.form.step-form.info': 'Step Form(write transfer information)',
20 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
21 'menu.form.step-form.result': 'Step Form(finished)',
22 'menu.form.advanced-form': 'Advanced Form',
23 'menu.list': 'List',
24 'menu.list.table-list': 'Search Table',
25 'menu.list.basic-list': 'Basic List',
26 'menu.list.card-list': 'Card List',
27 'menu.list.search-list': 'Search List',
28 'menu.list.search-list.articles': 'Search List(articles)',
29 'menu.list.search-list.projects': 'Search List(projects)',
30 'menu.list.search-list.applications': 'Search List(applications)',
31 'menu.profile': 'Profile',
32 'menu.profile.basic': 'Basic Profile',
33 'menu.profile.advanced': 'Advanced Profile',
34 'menu.result': 'Result',
35 'menu.result.success': 'Success',
36 'menu.result.fail': 'Fail',
37 'menu.exception': 'Exception',
38 'menu.exception.not-permission': '403',
39 'menu.exception.not-find': '404',
40 'menu.exception.server-error': '500',
41 'menu.exception.trigger': 'Trigger',
42 'menu.account': 'Account',
43 'menu.account.center': 'Account Center',
44 'menu.account.settings': 'Account Settings',
45 'menu.account.trigger': 'Trigger Error',
46 'menu.account.logout': 'Logout',
47 'menu.editor': 'Graphic Editor',
48 'menu.editor.flow': 'Flow Editor',
49 'menu.editor.mind': 'Mind Editor',
50 'menu.editor.koni': 'Koni Editor',
51 };
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 };
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 };
1 export default {
2 'app.settings.menuMap.basic': 'Basic Settings',
3 'app.settings.menuMap.security': 'Security Settings',
4 'app.settings.menuMap.binding': 'Account Binding',
5 'app.settings.menuMap.notification': 'New Message Notification',
6 'app.settings.basic.avatar': 'Avatar',
7 'app.settings.basic.change-avatar': 'Change avatar',
8 'app.settings.basic.email': 'Email',
9 'app.settings.basic.email-message': 'Please input your email!',
10 'app.settings.basic.nickname': 'Nickname',
11 'app.settings.basic.nickname-message': 'Please input your Nickname!',
12 'app.settings.basic.profile': 'Personal profile',
13 'app.settings.basic.profile-message': 'Please input your personal profile!',
14 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
15 'app.settings.basic.country': 'Country/Region',
16 'app.settings.basic.country-message': 'Please input your country!',
17 'app.settings.basic.geographic': 'Province or city',
18 'app.settings.basic.geographic-message': 'Please input your geographic info!',
19 'app.settings.basic.address': 'Street Address',
20 'app.settings.basic.address-message': 'Please input your address!',
21 'app.settings.basic.phone': 'Phone Number',
22 'app.settings.basic.phone-message': 'Please input your phone!',
23 'app.settings.basic.update': 'Update Information',
24 'app.settings.security.strong': 'Strong',
25 'app.settings.security.medium': 'Medium',
26 'app.settings.security.weak': 'Weak',
27 'app.settings.security.password': 'Account Password',
28 'app.settings.security.password-description': 'Current password strength',
29 'app.settings.security.phone': 'Security Phone',
30 'app.settings.security.phone-description': 'Bound phone',
31 'app.settings.security.question': 'Security Question',
32 'app.settings.security.question-description':
33 'The security question is not set, and the security policy can effectively protect the account security',
34 'app.settings.security.email': 'Backup Email',
35 'app.settings.security.email-description': 'Bound Email',
36 'app.settings.security.mfa': 'MFA Device',
37 'app.settings.security.mfa-description':
38 'Unbound MFA device, after binding, can be confirmed twice',
39 'app.settings.security.modify': 'Modify',
40 'app.settings.security.set': 'Set',
41 'app.settings.security.bind': 'Bind',
42 'app.settings.binding.taobao': 'Binding Taobao',
43 'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
44 'app.settings.binding.alipay': 'Binding Alipay',
45 'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
46 'app.settings.binding.dingding': 'Binding DingTalk',
47 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
48 'app.settings.binding.bind': 'Bind',
49 'app.settings.notification.password': 'Account Password',
50 'app.settings.notification.password-description':
51 'Messages from other users will be notified in the form of a station letter',
52 'app.settings.notification.messages': 'System Messages',
53 'app.settings.notification.messages-description':
54 'System messages will be notified in the form of a station letter',
55 'app.settings.notification.todo': 'To-do Notification',
56 'app.settings.notification.todo-description':
57 'The to-do list will be notified in the form of a letter from the station',
58 'app.settings.open': 'Open',
59 'app.settings.close': 'Close',
60 };
1 import component from './pt-BR/component';
2 import globalHeader from './pt-BR/globalHeader';
3 import menu from './pt-BR/menu';
4 import pwa from './pt-BR/pwa';
5 import settingDrawer from './pt-BR/settingDrawer';
6 import settings from './pt-BR/settings';
7
8 export default {
9 'navBar.lang': 'Idiomas',
10 'layout.user.link.help': 'ajuda',
11 'layout.user.link.privacy': 'política de privacidade',
12 'layout.user.link.terms': 'termos de serviços',
13 'app.preview.down.block': 'Download this page to your local project',
14 ...globalHeader,
15 ...menu,
16 ...settingDrawer,
17 ...settings,
18 ...pwa,
19 ...component,
20 };
1 export default {
2 'component.tagSelect.expand': 'Expandir',
3 'component.tagSelect.collapse': 'Diminuir',
4 'component.tagSelect.all': 'Todas',
5 };
1 export default {
2 'component.globalHeader.search': 'Busca',
3 'component.globalHeader.search.example1': 'Exemplo de busca 1',
4 'component.globalHeader.search.example2': 'Exemplo de busca 2',
5 'component.globalHeader.search.example3': 'Exemplo de busca 3',
6 'component.globalHeader.help': 'Ajuda',
7 'component.globalHeader.notification': 'Notificação',
8 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.',
9 'component.globalHeader.message': 'Mensagem',
10 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.',
11 'component.globalHeader.event': 'Evento',
12 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.',
13 'component.noticeIcon.clear': 'Limpar',
14 'component.noticeIcon.cleared': 'Limpo',
15 'component.noticeIcon.empty': 'Sem notificações',
16 'component.noticeIcon.loaded': 'Carregado',
17 'component.noticeIcon.view-more': 'Veja mais',
18 };
1 export default {
2 'menu.welcome': 'Welcome',
3 'menu.more-blocks': 'More Blocks',
4 'menu.home': 'Início',
5 'menu.login': 'Login',
6 'menu.admin': 'admin',
7 'menu.register': 'Registro',
8 'menu.register.result': 'Resultado de registro',
9 'menu.dashboard': 'Dashboard',
10 'menu.dashboard.analysis': 'Análise',
11 'menu.dashboard.monitor': 'Monitor',
12 'menu.dashboard.workplace': 'Ambiente de Trabalho',
13 'menu.exception.403': '403',
14 'menu.exception.404': '404',
15 'menu.exception.500': '500',
16 'menu.form': 'Formulário',
17 'menu.form.basic-form': 'Formulário Básico',
18 'menu.form.step-form': 'Formulário Assistido',
19 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)',
20 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)',
21 'menu.form.step-form.result': 'Formulário Assistido(finalizado)',
22 'menu.form.advanced-form': 'Formulário Avançado',
23 'menu.list': 'Lista',
24 'menu.list.table-list': 'Tabela de Busca',
25 'menu.list.basic-list': 'Lista Básica',
26 'menu.list.card-list': 'Lista de Card',
27 'menu.list.search-list': 'Lista de Busca',
28 'menu.list.search-list.articles': 'Lista de Busca(artigos)',
29 'menu.list.search-list.projects': 'Lista de Busca(projetos)',
30 'menu.list.search-list.applications': 'Lista de Busca(aplicações)',
31 'menu.profile': 'Perfil',
32 'menu.profile.basic': 'Perfil Básico',
33 'menu.profile.advanced': 'Perfil Avançado',
34 'menu.result': 'Resultado',
35 'menu.result.success': 'Sucesso',
36 'menu.result.fail': 'Falha',
37 'menu.exception': 'Exceção',
38 'menu.exception.not-permission': '403',
39 'menu.exception.not-find': '404',
40 'menu.exception.server-error': '500',
41 'menu.exception.trigger': 'Disparar',
42 'menu.account': 'Conta',
43 'menu.account.center': 'Central da Conta',
44 'menu.account.settings': 'Configurar Conta',
45 'menu.account.trigger': 'Disparar Erro',
46 'menu.account.logout': 'Sair',
47 'menu.editor': 'Graphic Editor',
48 'menu.editor.flow': 'Flow Editor',
49 'menu.editor.mind': 'Mind Editor',
50 'menu.editor.koni': 'Koni Editor',
51 };
1 export default {
2 'app.pwa.offline': 'Você está offline agora',
3 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível',
4 'app.pwa.serviceworker.updated.hint':
5 'Por favor, pressione o botão "Atualizar" para recarregar a página atual',
6 'app.pwa.serviceworker.updated.ok': 'Atualizar',
7 };
1 export default {
2 'app.setting.pagestyle': 'Configuração de estilo da página',
3 'app.setting.pagestyle.dark': 'Dark style',
4 'app.setting.pagestyle.light': 'Light style',
5 'app.setting.content-width': 'Largura do conteúdo',
6 'app.setting.content-width.fixed': 'Fixo',
7 'app.setting.content-width.fluid': 'Fluido',
8 'app.setting.themecolor': 'Cor do Tema',
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': 'Modo de Navegação',
18 'app.setting.sidemenu': 'Layout do Menu Lateral',
19 'app.setting.topmenu': 'Layout do Menu Superior',
20 'app.setting.fixedheader': 'Cabeçalho fixo',
21 'app.setting.fixedsidebar': 'Barra lateral fixa',
22 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral',
23 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar',
24 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado',
25 'app.setting.othersettings': 'Outras configurações',
26 'app.setting.weakmode': 'Weak Mode',
27 'app.setting.copy': 'Copiar Configuração',
28 'app.setting.copyinfo':
29 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js',
30 'app.setting.production.hint':
31 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o',
32 };
1 export default {
2 'app.settings.menuMap.basic': 'Configurações Básicas',
3 'app.settings.menuMap.security': 'Configurações de Segurança',
4 'app.settings.menuMap.binding': 'Vinculação de Conta',
5 'app.settings.menuMap.notification': 'Mensagens de Notificação',
6 'app.settings.basic.avatar': 'Avatar',
7 'app.settings.basic.change-avatar': 'Alterar avatar',
8 'app.settings.basic.email': 'Email',
9 'app.settings.basic.email-message': 'Por favor insira seu email!',
10 'app.settings.basic.nickname': 'Nome de usuário',
11 'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!',
12 'app.settings.basic.profile': 'Perfil pessoal',
13 'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!',
14 'app.settings.basic.profile-placeholder': 'Breve introdução sua',
15 'app.settings.basic.country': 'País/Região',
16 'app.settings.basic.country-message': 'Por favor insira país!',
17 'app.settings.basic.geographic': 'Província, estado ou cidade',
18 'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!',
19 'app.settings.basic.address': 'Endereço',
20 'app.settings.basic.address-message': 'Por favor insira seu endereço!',
21 'app.settings.basic.phone': 'Número de telefone',
22 'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!',
23 'app.settings.basic.update': 'Atualizar Informações',
24 'app.settings.security.strong': 'Forte',
25 'app.settings.security.medium': 'Média',
26 'app.settings.security.weak': 'Fraca',
27 'app.settings.security.password': 'Senha da Conta',
28 'app.settings.security.password-description': 'Força da senha',
29 'app.settings.security.phone': 'Telefone de Seguraça',
30 'app.settings.security.phone-description': 'Telefone vinculado',
31 'app.settings.security.question': 'Pergunta de Segurança',
32 'app.settings.security.question-description':
33 'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta',
34 'app.settings.security.email': 'Email de Backup',
35 'app.settings.security.email-description': 'Email vinculado',
36 'app.settings.security.mfa': 'Dispositivo MFA',
37 'app.settings.security.mfa-description':
38 'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes',
39 'app.settings.security.modify': 'Modificar',
40 'app.settings.security.set': 'Atribuir',
41 'app.settings.security.bind': 'Vincular',
42 'app.settings.binding.taobao': 'Vincular Taobao',
43 'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao',
44 'app.settings.binding.alipay': 'Vincular Alipay',
45 'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay',
46 'app.settings.binding.dingding': 'Vincular DingTalk',
47 'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk',
48 'app.settings.binding.bind': 'Vincular',
49 'app.settings.notification.password': 'Senha da Conta',
50 'app.settings.notification.password-description':
51 'Mensagens de outros usuários serão notificadas na forma de uma estação de letra',
52 'app.settings.notification.messages': 'Mensagens de Sistema',
53 'app.settings.notification.messages-description':
54 'Mensagens de sistema serão notificadas na forma de uma estação de letra',
55 'app.settings.notification.todo': 'Notificação de To-do',
56 'app.settings.notification.todo-description':
57 'A lista de to-do será notificada na forma de uma estação de letra',
58 'app.settings.open': 'Aberto',
59 'app.settings.close': 'Fechado',
60 };
1 import component from './zh-CN/component';
2 import globalHeader from './zh-CN/globalHeader';
3 import menu from './zh-CN/menu';
4 import pwa from './zh-CN/pwa';
5 import settingDrawer from './zh-CN/settingDrawer';
6 import settings from './zh-CN/settings';
7
8 export default {
9 'navBar.lang': '语言',
10 'layout.user.link.help': '帮助',
11 'layout.user.link.privacy': '隐私',
12 'layout.user.link.terms': '条款',
13 'app.preview.down.block': '下载此页面到本地项目',
14 'app.welcome.link.fetch-blocks': '获取全部区块',
15 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
16 ...globalHeader,
17 ...menu,
18 ...settingDrawer,
19 ...settings,
20 ...pwa,
21 ...component,
22 };
1 export default {
2 'component.tagSelect.expand': '展开',
3 'component.tagSelect.collapse': '收起',
4 'component.tagSelect.all': '全部',
5 };
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.view-more': '查看更多',
17 };
1 export default {
2 'menu.welcome': '全景',
3 'menu.reports': '报告追溯',
4 'menu.operation': '过程追溯',
5 'menu.employees': '人员',
6 'menu.machine': '设备',
7 'menu.material': '材料',
8 'menu.items': '能力',
9 'menu.more-blocks': '更多区块',
10 'menu.home': '首页',
11 'menu.admin': '管理页',
12 'menu.login': '登录',
13 'menu.register': '注册',
14 'menu.register.result': '注册结果',
15 'menu.dashboard': 'Dashboard',
16 'menu.dashboard.analysis': '分析页',
17 'menu.dashboard.monitor': '监控页',
18 'menu.dashboard.workplace': '工作台',
19 'menu.exception.403': '403',
20 'menu.exception.404': '404',
21 'menu.exception.500': '500',
22 'menu.form': '表单页',
23 'menu.form.basic-form': '基础表单',
24 'menu.form.step-form': '分步表单',
25 'menu.form.step-form.info': '分步表单(填写转账信息)',
26 'menu.form.step-form.confirm': '分步表单(确认转账信息)',
27 'menu.form.step-form.result': '分步表单(完成)',
28 'menu.form.advanced-form': '高级表单',
29 'menu.list': '列表页',
30 'menu.list.table-list': '查询表格',
31 'menu.list.basic-list': '标准列表',
32 'menu.list.card-list': '卡片列表',
33 'menu.list.search-list': '搜索列表',
34 'menu.list.search-list.articles': '搜索列表(文章)',
35 'menu.list.search-list.projects': '搜索列表(项目)',
36 'menu.list.search-list.applications': '搜索列表(应用)',
37 'menu.profile': '详情页',
38 'menu.profile.basic': '基础详情页',
39 'menu.profile.advanced': '高级详情页',
40 'menu.result': '结果页',
41 'menu.result.success': '成功页',
42 'menu.result.fail': '失败页',
43 'menu.exception': '异常页',
44 'menu.exception.not-permission': '403',
45 'menu.exception.not-find': '404',
46 'menu.exception.server-error': '500',
47 'menu.exception.trigger': '触发错误',
48 'menu.account': '个人页',
49 'menu.account.center': '个人中心',
50 'menu.account.settings': '个人设置',
51 'menu.account.trigger': '触发报错',
52 'menu.account.logout': '退出登录',
53 'menu.editor': '图形编辑器',
54 'menu.editor.flow': '流程编辑器',
55 'menu.editor.mind': '脑图编辑器',
56 'menu.editor.koni': '拓扑编辑器',
57 };
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 };
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 };
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 };
1 import component from './zh-TW/component';
2 import globalHeader from './zh-TW/globalHeader';
3 import menu from './zh-TW/menu';
4 import pwa from './zh-TW/pwa';
5 import settingDrawer from './zh-TW/settingDrawer';
6 import settings from './zh-TW/settings';
7
8 export default {
9 'navBar.lang': '語言',
10 'layout.user.link.help': '幫助',
11 'layout.user.link.privacy': '隱私',
12 'layout.user.link.terms': '條款',
13 'app.preview.down.block': '下載此頁面到本地項目',
14 ...globalHeader,
15 ...menu,
16 ...settingDrawer,
17 ...settings,
18 ...pwa,
19 ...component,
20 };
1 export default {
2 'component.tagSelect.expand': '展開',
3 'component.tagSelect.collapse': '收起',
4 'component.tagSelect.all': '全部',
5 };
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.view-more': '查看更多',
17 };
1 export default {
2 'menu.welcome': '歡迎',
3 'menu.more-blocks': '更多區塊',
4
5 'menu.home': '首頁',
6 'menu.login': '登錄',
7 'menu.admin': '权限',
8 'menu.exception.403': '403',
9 'menu.exception.404': '404',
10 'menu.exception.500': '500',
11 'menu.register': '註冊',
12 'menu.register.result': '註冊結果',
13 'menu.dashboard': 'Dashboard',
14 'menu.dashboard.analysis': '分析頁',
15 'menu.dashboard.monitor': '監控頁',
16 'menu.dashboard.workplace': '工作臺',
17 'menu.form': '表單頁',
18 'menu.form.basic-form': '基礎表單',
19 'menu.form.step-form': '分步表單',
20 'menu.form.step-form.info': '分步表單(填寫轉賬信息)',
21 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)',
22 'menu.form.step-form.result': '分步表單(完成)',
23 'menu.form.advanced-form': '高級表單',
24 'menu.list': '列表頁',
25 'menu.list.table-list': '查詢表格',
26 'menu.list.basic-list': '標淮列表',
27 'menu.list.card-list': '卡片列表',
28 'menu.list.search-list': '搜索列表',
29 'menu.list.search-list.articles': '搜索列表(文章)',
30 'menu.list.search-list.projects': '搜索列表(項目)',
31 'menu.list.search-list.applications': '搜索列表(應用)',
32 'menu.profile': '詳情頁',
33 'menu.profile.basic': '基礎詳情頁',
34 'menu.profile.advanced': '高級詳情頁',
35 'menu.result': '結果頁',
36 'menu.result.success': '成功頁',
37 'menu.result.fail': '失敗頁',
38 'menu.account': '個人頁',
39 'menu.account.center': '個人中心',
40 'menu.account.settings': '個人設置',
41 'menu.account.trigger': '觸發報錯',
42 'menu.account.logout': '退出登錄',
43 'menu.exception': '异常页',
44 'menu.exception.not-permission': '403',
45 'menu.exception.not-find': '404',
46 'menu.exception.server-error': '500',
47 'menu.exception.trigger': '触发错误',
48 'menu.editor': '圖形編輯器',
49 'menu.editor.flow': '流程編輯器',
50 'menu.editor.mind': '腦圖編輯器',
51 'menu.editor.koni': '拓撲編輯器',
52 };
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 };
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 };
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 };
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 }
1 import { AnyAction } from 'redux';
2 import { MenuDataItem } from '@ant-design/pro-layout';
3 import { RouterTypes } from 'umi';
4 import { GlobalModelState } from './global';
5 import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
6 import { UserModelState } from './user';
7 import { LoginModelType } from './login';
8
9 export { GlobalModelState, SettingModelState, UserModelState };
10
11 export interface Loading {
12 global: boolean;
13 effects: { [key: string]: boolean | undefined };
14 models: {
15 global?: boolean;
16 menu?: boolean;
17 setting?: boolean;
18 user?: boolean;
19 login?: boolean;
20 };
21 }
22
23 export interface ConnectState {
24 global: GlobalModelState;
25 loading: Loading;
26 settings: SettingModelState;
27 user: UserModelState;
28 login: LoginModelType;
29 }
30
31 export interface Route extends MenuDataItem {
32 routes?: Route[];
33 }
34
35 /**
36 * @type T: Params matched in dynamic routing
37 */
38 export interface ConnectProps<T = {}> extends Partial<RouterTypes<Route, T>> {
39 dispatch?<K = any>(action: AnyAction): K;
40 }
1 import { Reducer } from 'redux';
2 import { Subscription, Effect } from 'dva';
3
4 import { NoticeIconData } from '@/components/NoticeIcon';
5 import { queryNotices } from '@/services/user';
6 import { ConnectState } from './connect.d';
7
8 export interface NoticeItem extends NoticeIconData {
9 id: string;
10 type: string;
11 status: string;
12 }
13
14 export interface GlobalModelState {
15 collapsed: boolean;
16 notices: NoticeItem[];
17 }
18
19 export interface GlobalModelType {
20 namespace: 'global';
21 state: GlobalModelState;
22 effects: {
23 fetchNotices: Effect;
24 clearNotices: Effect;
25 changeNoticeReadState: Effect;
26 };
27 reducers: {
28 changeLayoutCollapsed: Reducer<GlobalModelState>;
29 saveNotices: Reducer<GlobalModelState>;
30 saveClearedNotices: Reducer<GlobalModelState>;
31 };
32 subscriptions: { setup: Subscription };
33 }
34
35 const GlobalModel: GlobalModelType = {
36 namespace: 'global',
37
38 state: {
39 collapsed: false,
40 notices: [],
41 },
42
43 effects: {
44 *fetchNotices(_, { call, put, select }) {
45 const data = yield call(queryNotices);
46 yield put({
47 type: 'saveNotices',
48 payload: data,
49 });
50 const unreadCount: number = yield select(
51 (state: ConnectState) => state.global.notices.filter(item => !item.read).length,
52 );
53 yield put({
54 type: 'user/changeNotifyCount',
55 payload: {
56 totalCount: data.length,
57 unreadCount,
58 },
59 });
60 },
61 *clearNotices({ payload }, { put, select }) {
62 yield put({
63 type: 'saveClearedNotices',
64 payload,
65 });
66 const count: number = yield select((state: ConnectState) => state.global.notices.length);
67 const unreadCount: number = yield select(
68 (state: ConnectState) => state.global.notices.filter(item => !item.read).length,
69 );
70 yield put({
71 type: 'user/changeNotifyCount',
72 payload: {
73 totalCount: count,
74 unreadCount,
75 },
76 });
77 },
78 *changeNoticeReadState({ payload }, { put, select }) {
79 const notices: NoticeItem[] = yield select((state: ConnectState) =>
80 state.global.notices.map(item => {
81 const notice = { ...item };
82 if (notice.id === payload) {
83 notice.read = true;
84 }
85 return notice;
86 }),
87 );
88
89 yield put({
90 type: 'saveNotices',
91 payload: notices,
92 });
93
94 yield put({
95 type: 'user/changeNotifyCount',
96 payload: {
97 totalCount: notices.length,
98 unreadCount: notices.filter(item => !item.read).length,
99 },
100 });
101 },
102 },
103
104 reducers: {
105 changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
106 return {
107 ...state,
108 collapsed: payload,
109 };
110 },
111 saveNotices(state, { payload }): GlobalModelState {
112 return {
113 collapsed: false,
114 ...state,
115 notices: payload,
116 };
117 },
118 saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
119 return {
120 collapsed: false,
121 ...state,
122 notices: state.notices.filter((item): boolean => item.type !== payload),
123 };
124 },
125 },
126
127 subscriptions: {
128 setup({ history }): void {
129 // Subscribe history(url) change, trigger `load` action if pathname is `/`
130 history.listen(({ pathname, search }): void => {
131 if (typeof window.ga !== 'undefined') {
132 window.ga('send', 'pageview', pathname + search);
133 }
134 });
135 },
136 },
137 };
138
139 export default GlobalModel;
1 import { Reducer } from 'redux';
2 import { routerRedux } from 'dva/router';
3 import { Effect } from 'dva';
4 import { stringify } from 'querystring';
5
6 import { fakeAccountLogin, getFakeCaptcha } from '@/services/login';
7 import { setAuthority } from '@/utils/authority';
8 import { getPageQuery } from '@/utils/utils';
9
10 export interface StateType {
11 status?: 'ok' | 'error';
12 type?: string;
13 currentAuthority?: 'user' | 'guest' | 'admin';
14 }
15
16 export interface LoginModelType {
17 namespace: string;
18 state: StateType;
19 effects: {
20 login: Effect;
21 getCaptcha: Effect;
22 logout: Effect;
23 };
24 reducers: {
25 changeLoginStatus: Reducer<StateType>;
26 };
27 }
28
29 const Model: LoginModelType = {
30 namespace: 'login',
31
32 state: {
33 status: undefined,
34 },
35
36 effects: {
37 *login({ payload }, { call, put }) {
38 const response = yield call(fakeAccountLogin, payload);
39 yield put({
40 type: 'changeLoginStatus',
41 payload: response,
42 });
43 // Login successfully
44 if (response.status === 'ok') {
45 const urlParams = new URL(window.location.href);
46 const params = getPageQuery();
47 let { redirect } = params as { redirect: string };
48 if (redirect) {
49 const redirectUrlParams = new URL(redirect);
50 if (redirectUrlParams.origin === urlParams.origin) {
51 redirect = redirect.substr(urlParams.origin.length);
52 if (redirect.match(/^\/.*#/)) {
53 redirect = redirect.substr(redirect.indexOf('#') + 1);
54 }
55 } else {
56 window.location.href = '/';
57 return;
58 }
59 }
60 yield put(routerRedux.replace(redirect || '/'));
61 }
62 },
63
64 *getCaptcha({ payload }, { call }) {
65 yield call(getFakeCaptcha, payload);
66 },
67 *logout(_, { put }) {
68 const { redirect } = getPageQuery();
69 // redirect
70 if (window.location.pathname !== '/user/login' && !redirect) {
71 yield put(
72 routerRedux.replace({
73 pathname: '/user/login',
74 search: stringify({
75 redirect: window.location.href,
76 }),
77 }),
78 );
79 }
80 },
81 },
82
83 reducers: {
84 changeLoginStatus(state, { payload }) {
85 setAuthority(payload.currentAuthority);
86 return {
87 ...state,
88 status: payload.status,
89 type: payload.type,
90 };
91 },
92 },
93 };
94
95 export default Model;
1 import { Reducer } from 'redux';
2 import { message } from 'antd';
3 import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
4 import themeColorClient from '../components/SettingDrawer/themeColorClient';
5
6 export interface SettingModelType {
7 namespace: 'settings';
8 state: DefaultSettings;
9 reducers: {
10 getSetting: Reducer<DefaultSettings>;
11 changeSetting: Reducer<DefaultSettings>;
12 };
13 }
14
15 const updateTheme = (newPrimaryColor?: string) => {
16 if (newPrimaryColor) {
17 const timeOut = 0;
18 const hideMessage = message.loading('正在切换主题!', timeOut);
19 themeColorClient.changeColor(newPrimaryColor).finally(() => hideMessage());
20 }
21 };
22
23 const updateColorWeak: (colorWeak: boolean) => void = colorWeak => {
24 const root = document.getElementById('root');
25 if (root) {
26 root.className = colorWeak ? 'colorWeak' : '';
27 }
28 };
29
30 const SettingModel: SettingModelType = {
31 namespace: 'settings',
32 state: defaultSettings,
33 reducers: {
34 getSetting(state = defaultSettings) {
35 const setting: Partial<DefaultSettings> = {};
36 const urlParams = new URL(window.location.href);
37 Object.keys(state).forEach(key => {
38 if (urlParams.searchParams.has(key)) {
39 const value = urlParams.searchParams.get(key);
40 setting[key] = value === '1' ? true : value;
41 }
42 });
43 const { primaryColor, colorWeak } = setting;
44
45 if (primaryColor && state.primaryColor !== primaryColor) {
46 updateTheme(primaryColor);
47 }
48 updateColorWeak(!!colorWeak);
49 return {
50 ...state,
51 ...setting,
52 };
53 },
54 changeSetting(state = defaultSettings, { payload }) {
55 const urlParams = new URL(window.location.href);
56 Object.keys(defaultSettings).forEach(key => {
57 if (urlParams.searchParams.has(key)) {
58 urlParams.searchParams.delete(key);
59 }
60 });
61 Object.keys(payload).forEach(key => {
62 if (key === 'collapse') {
63 return;
64 }
65 let value = payload[key];
66 if (value === true) {
67 value = 1;
68 }
69 if (defaultSettings[key] !== value) {
70 urlParams.searchParams.set(key, value);
71 }
72 });
73 const { primaryColor, colorWeak, contentWidth } = payload;
74 if (primaryColor && state.primaryColor !== primaryColor) {
75 updateTheme(primaryColor);
76 }
77 if (state.contentWidth !== contentWidth && window.dispatchEvent) {
78 window.dispatchEvent(new Event('resize'));
79 }
80 updateColorWeak(!!colorWeak);
81 window.history.replaceState(null, 'setting', urlParams.href);
82 return {
83 ...state,
84 ...payload,
85 };
86 },
87 },
88 };
89 export default SettingModel;
1 import { Effect } from 'dva';
2 import { Reducer } from 'redux';
3
4 import { queryCurrent, query as queryUsers } from '@/services/user';
5
6 export interface CurrentUser {
7 avatar?: string;
8 name?: string;
9 title?: string;
10 group?: string;
11 signature?: string;
12 tags?: {
13 key: string;
14 label: string;
15 }[];
16 userid?: string;
17 unreadCount?: number;
18 }
19
20 export interface UserModelState {
21 currentUser?: CurrentUser;
22 }
23
24 export interface UserModelType {
25 namespace: 'user';
26 state: UserModelState;
27 effects: {
28 fetch: Effect;
29 fetchCurrent: Effect;
30 };
31 reducers: {
32 saveCurrentUser: Reducer<UserModelState>;
33 changeNotifyCount: Reducer<UserModelState>;
34 };
35 }
36
37 const UserModel: UserModelType = {
38 namespace: 'user',
39
40 state: {
41 currentUser: {},
42 },
43
44 effects: {
45 *fetch(_, { call, put }) {
46 const response = yield call(queryUsers);
47 yield put({
48 type: 'save',
49 payload: response,
50 });
51 },
52 *fetchCurrent(_, { call, put }) {
53 const response = yield call(queryCurrent);
54 yield put({
55 type: 'saveCurrentUser',
56 payload: response,
57 });
58 },
59 },
60
61 reducers: {
62 saveCurrentUser(state, action) {
63 return {
64 ...state,
65 currentUser: action.payload || {},
66 };
67 },
68 changeNotifyCount(
69 state = {
70 currentUser: {},
71 },
72 action,
73 ) {
74 return {
75 ...state,
76 currentUser: {
77 ...state.currentUser,
78 notifyCount: action.payload.totalCount,
79 unreadCount: action.payload.unreadCount,
80 },
81 };
82 },
83 },
84 };
85
86 export default UserModel;
1 import { Button, Result } from 'antd';
2 import React from 'react';
3 import router from 'umi/router';
4
5 // 这里应该使用 antd 的 404 result 组件,
6 // 但是还没发布,先来个简单的。
7
8 const NoFoundPage: React.FC<{}> = () => (
9 <Result
10 status="404"
11 title="404"
12 subTitle="Sorry, the page you visited does not exist."
13 extra={
14 <Button type="primary" onClick={() => router.push('/')}>
15 Back Home
16 </Button>
17 }
18 ></Result>
19 );
20
21 export default NoFoundPage;
1 import React from 'react';
2 import { Card, Typography, Alert, Icon } from 'antd';
3 import { PageHeaderWrapper } from '@ant-design/pro-layout';
4
5 export default (): React.ReactNode => (
6 <PageHeaderWrapper content=" 这个页面只有 admin 权限才能查看">
7 <Card>
8 <Alert
9 message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
10 type="success"
11 showIcon
12 banner
13 style={{
14 margin: -12,
15 marginBottom: 48,
16 }}
17 />
18 <Typography.Title level={2} style={{ textAlign: 'center' }}>
19 <Icon type="smile" theme="twoTone" /> Ant Design Pro{' '}
20 <Icon type="heart" theme="twoTone" twoToneColor="#eb2f96" /> You
21 </Typography.Title>
22 </Card>
23 <p style={{ textAlign: 'center', marginTop: 24 }}>
24 Want to add more pages? Please refer to{' '}
25 <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
26 use block
27 </a>
28
29 </p>
30 </PageHeaderWrapper>
31 );
1 import React from 'react';
2 import Redirect from 'umi/redirect';
3 import { connect } from 'dva';
4 import pathToRegexp from 'path-to-regexp';
5 import Authorized from '@/utils/Authorized';
6 import { ConnectProps, ConnectState, Route, UserModelState } from '@/models/connect';
7
8 interface AuthComponentProps extends ConnectProps {
9 user: UserModelState;
10 }
11
12 const getRouteAuthority = (path: string, routeData: Route[]) => {
13 let authorities: string[] | string | undefined;
14 routeData.forEach(route => {
15 if (route.authority) {
16 authorities = route.authority;
17 }
18 // match prefix
19 if (pathToRegexp(`${route.path}(.*)`).test(path)) {
20 // exact match
21 if (route.path === path) {
22 authorities = route.authority || authorities;
23 }
24 // get children authority recursively
25 if (route.routes) {
26 authorities = getRouteAuthority(path, route.routes) || authorities;
27 }
28 }
29 });
30 return authorities;
31 };
32
33 const AuthComponent: React.FC<AuthComponentProps> = ({
34 children,
35 route = {
36 routes: [],
37 },
38 location = {
39 pathname: '',
40 },
41 user,
42 }) => {
43 const { currentUser } = user;
44 const { routes = [] } = route;
45 const isLogin = currentUser && currentUser.name;
46 return (
47 <Authorized
48 authority={getRouteAuthority(location.pathname, routes) || ''}
49 noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
50 >
51 {children}
52 </Authorized>
53 );
54 };
55
56 export default connect(({ user }: ConnectState) => ({
57 user,
58 }))(AuthComponent);
1 import React from 'react';
2 import { Card, Typography, Alert } from 'antd';
3 import { PageHeaderWrapper } from '@ant-design/pro-layout';
4 import { FormattedMessage } from 'umi-plugin-react/locale';
5
6 const CodePreview: React.FC<{}> = ({ children }) => (
7 <pre
8 style={{
9 background: '#f2f4f5',
10 padding: '12px 20px',
11 margin: '12px 0',
12 }}
13 >
14 <code>
15 <Typography.Text copyable>{children}</Typography.Text>
16 </code>
17 </pre>
18 );
19
20 export default (): React.ReactNode => (
21 <PageHeaderWrapper>
22 <Card>
23 <Alert
24 message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
25 type="success"
26 showIcon
27 banner
28 style={{
29 margin: -12,
30 marginBottom: 24,
31 }}
32 />
33 <Typography.Text strong>
34 <a target="_blank" rel="noopener noreferrer" href="https://pro.ant.design/docs/block">
35 <FormattedMessage
36 id="app.welcome.link.block-list"
37 defaultMessage="基于 block 开发,快速构建标准页面"
38 />
39 </a>
40 </Typography.Text>
41 <CodePreview>npx umi block list</CodePreview>
42 <Typography.Text
43 strong
44 style={{
45 marginBottom: 12,
46 }}
47 >
48 <a
49 target="_blank"
50 rel="noopener noreferrer"
51 href="https://pro.ant.design/docs/available-script#npm-run-fetchblocks"
52 >
53 <FormattedMessage id="app.welcome.link.fetch-blocks" defaultMessage="获取全部区块" />
54 </a>
55 </Typography.Text>
56 <CodePreview> npm run fetch:blocks</CodePreview>
57 </Card>
58 <p
59 style={{
60 textAlign: 'center',
61 marginTop: 24,
62 }}
63 >
64 Want to add more pages? Please refer to{' '}
65 <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
66 use block
67 </a>
68
69 </p>
70 </PageHeaderWrapper>
71 );
1 import React from 'react';
2 import { Card, Typography, Alert } from 'antd';
3 import { PageHeaderWrapper } from '@ant-design/pro-layout';
4 import { FormattedMessage } from 'umi-plugin-react/locale';
5
6 const CodePreview: React.FC<{}> = ({ children }) => (
7 <pre
8 style={{
9 background: '#f2f4f5',
10 padding: '12px 20px',
11 margin: '12px 0',
12 }}
13 >
14 <code>
15 <Typography.Text copyable>{children}</Typography.Text>
16 </code>
17 </pre>
18 );
19
20 export default (): React.ReactNode => (
21 <PageHeaderWrapper>
22 <Card>
23 <Alert
24 message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
25 type="success"
26 showIcon
27 banner
28 style={{
29 margin: -12,
30 marginBottom: 24,
31 }}
32 />
33 <Typography.Text strong>
34 <a target="_blank" rel="noopener noreferrer" href="https://pro.ant.design/docs/block">
35 <FormattedMessage
36 id="app.welcome.link.block-list"
37 defaultMessage="基于 block 开发,快速构建标准页面"
38 />
39 </a>
40 </Typography.Text>
41 <CodePreview>npx umi block list</CodePreview>
42 <Typography.Text
43 strong
44 style={{
45 marginBottom: 12,
46 }}
47 >
48 <a
49 target="_blank"
50 rel="noopener noreferrer"
51 href="https://pro.ant.design/docs/available-script#npm-run-fetchblocks"
52 >
53 <FormattedMessage id="app.welcome.link.fetch-blocks" defaultMessage="获取全部区块" />
54 </a>
55 </Typography.Text>
56 <CodePreview> npm run fetch:blocks</CodePreview>
57 </Card>
58 <p style={{ textAlign: 'center', marginTop: 24 }}>
59 Want to add more pages? Please refer to{' '}
60 <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
61 use block
62 </a>
63
64 </p>
65 </PageHeaderWrapper>
66 );
1 import React from 'react';
2 import { Card, Alert } from 'antd';
3 import { PageHeaderWrapper } from '@ant-design/pro-layout';
4 // import EchartsMap from '@/components/EchartsMap';
5 import CountryMap from '@/components/EchartsMap/CountryMap';
6
7 export default (): React.ReactNode => (
8 <PageHeaderWrapper>
9 <Card>
10 <Alert
11 message="可点击进入某个地域或者检验机构,查看详细内容。"
12 type="success"
13 showIcon
14 banner
15 style={{
16 margin: -12,
17 marginBottom: 24,
18 }}
19 />
20 <CountryMap></CountryMap>
21 </Card>
22 </PageHeaderWrapper>
23 );
1 import React from "react";
2 import styles from "./index.less";
3 import { Table } from "antd";
4 // import reqwest from "reqwest";
5 import request from '@/utils/request';
6
7 const columns = [
8 {
9 title: "Name",
10 dataIndex: "name",
11 sorter: true,
12 render: (name: any) => `${name.first} ${name.last}`,
13 width: "20%"
14 },
15 {
16 title: "Gender",
17 dataIndex: "gender",
18 filters: [
19 { text: "Male", value: "male" },
20 { text: "Female", value: "female" }
21 ],
22 width: "20%"
23 },
24 {
25 title: "Email",
26 dataIndex: "email"
27 }
28 ];
29
30 class App extends React.Component {
31 state: any = {
32 data: [],
33 pagination: {},
34 loading: false
35 };
36
37 componentDidMount() {
38 this.fetch();
39 }
40
41 handleTableChange = (pagination: any, filters: any, sorter: any) => {
42 const pager: any = { ...this.state.pagination };
43 pager.current = pagination.current;
44 this.setState({
45 pagination: pager
46 });
47 this.fetch({
48 results: pagination.pageSize,
49 page: pagination.current,
50 sortField: sorter.field,
51 sortOrder: sorter.order,
52 ...filters
53 });
54 };
55
56 fetch = (params = {}) => {
57 console.log("params:", params);
58 this.setState({ loading: true });
59 request('https://randomuser.me/api', {
60 method: 'get',
61 data: {
62 results: 10,
63 ...params,
64 type: "json"
65 },
66 }).then((data: any) => {
67 const pagination = { ...this.state.pagination };
68 // Read total count from server
69 // pagination.total = data.totalCount;
70 console.log(data)
71 pagination.total = 200;
72 this.setState({
73 loading: false,
74 data: (data && data.results) || [],
75 pagination
76 });
77 });
78 };
79
80 render() {
81 return (
82 <Table
83 columns={columns}
84 rowKey={(record: any) => record.login.uuid}
85 dataSource={this.state.data}
86 pagination={this.state.pagination}
87 loading={this.state.loading}
88 onChange={this.handleTableChange}
89 />
90 );
91 }
92 }
93
94 export default () => (
95 <div className={styles.container}>
96 <div id="components-table-demo-ajax">
97 <App />
98 </div>
99 </div>
100 );
File mode changed
1 import React from 'react';
2 import { PageHeaderWrapper } from '@ant-design/pro-layout';
3 import TableAjax from './TableAjax';
4 export default (): React.ReactNode => (
5 <PageHeaderWrapper>
6 <TableAjax />
7 </PageHeaderWrapper>
8 );
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6 <meta
7 name="viewport"
8 content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
9 />
10 <title>LIMSChain 检验检测监管平台</title>
11 <link rel="icon" href="/favicon.png" type="image/x-icon" />
12 </head>
13 <body>
14 <noscript>Out-of-the-box mid-stage front/design solution!</noscript>
15 <div id="root">
16 <style>
17 .page-loading-warp {
18 padding: 120px;
19 display: flex;
20 justify-content: center;
21 align-items: center;
22 }
23 .ant-spin {
24 -webkit-box-sizing: border-box;
25 box-sizing: border-box;
26 margin: 0;
27 padding: 0;
28 color: rgba(0, 0, 0, 0.65);
29 font-size: 14px;
30 font-variant: tabular-nums;
31 line-height: 1.5;
32 list-style: none;
33 -webkit-font-feature-settings: 'tnum';
34 font-feature-settings: 'tnum';
35 position: absolute;
36 display: none;
37 color: #1890ff;
38 text-align: center;
39 vertical-align: middle;
40 opacity: 0;
41 -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
42 transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
43 transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
44 transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
45 -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
46 }
47
48 .ant-spin-spinning {
49 position: static;
50 display: inline-block;
51 opacity: 1;
52 }
53
54 .ant-spin-dot {
55 position: relative;
56 display: inline-block;
57 font-size: 20px;
58 width: 20px;
59 height: 20px;
60 }
61
62 .ant-spin-dot-item {
63 position: absolute;
64 display: block;
65 width: 9px;
66 height: 9px;
67 background-color: #1890ff;
68 border-radius: 100%;
69 -webkit-transform: scale(0.75);
70 -ms-transform: scale(0.75);
71 transform: scale(0.75);
72 -webkit-transform-origin: 50% 50%;
73 -ms-transform-origin: 50% 50%;
74 transform-origin: 50% 50%;
75 opacity: 0.3;
76 -webkit-animation: antSpinMove 1s infinite linear alternate;
77 animation: antSpinMove 1s infinite linear alternate;
78 }
79
80 .ant-spin-dot-item:nth-child(1) {
81 top: 0;
82 left: 0;
83 }
84
85 .ant-spin-dot-item:nth-child(2) {
86 top: 0;
87 right: 0;
88 -webkit-animation-delay: 0.4s;
89 animation-delay: 0.4s;
90 }
91
92 .ant-spin-dot-item:nth-child(3) {
93 right: 0;
94 bottom: 0;
95 -webkit-animation-delay: 0.8s;
96 animation-delay: 0.8s;
97 }
98
99 .ant-spin-dot-item:nth-child(4) {
100 bottom: 0;
101 left: 0;
102 -webkit-animation-delay: 1.2s;
103 animation-delay: 1.2s;
104 }
105
106 .ant-spin-dot-spin {
107 -webkit-transform: rotate(45deg);
108 -ms-transform: rotate(45deg);
109 transform: rotate(45deg);
110 -webkit-animation: antRotate 1.2s infinite linear;
111 animation: antRotate 1.2s infinite linear;
112 }
113
114 .ant-spin-lg .ant-spin-dot {
115 font-size: 32px;
116 width: 32px;
117 height: 32px;
118 }
119
120 .ant-spin-lg .ant-spin-dot i {
121 width: 14px;
122 height: 14px;
123 }
124
125 @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
126 .ant-spin-blur {
127 background: #fff;
128 opacity: 0.5;
129 }
130 }
131
132 @-webkit-keyframes antSpinMove {
133 to {
134 opacity: 1;
135 }
136 }
137
138 @keyframes antSpinMove {
139 to {
140 opacity: 1;
141 }
142 }
143
144 @-webkit-keyframes antRotate {
145 to {
146 -webkit-transform: rotate(405deg);
147 transform: rotate(405deg);
148 }
149 }
150
151 @keyframes antRotate {
152 to {
153 -webkit-transform: rotate(405deg);
154 transform: rotate(405deg);
155 }
156 }
157 </style>
158 <div class="page-loading-warp">
159 <div class="ant-spin ant-spin-lg ant-spin-spinning">
160 <span class="ant-spin-dot ant-spin-dot-spin"
161 ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
162 ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
163 ></span>
164 </div>
165 </div>
166 </div>
167 </body>
168 </html>
1 import { Request, Response } from 'express';
2 import { parse } from 'url';
3 import { TableListItem, TableListParams } from './data.d';
4
5 // mock tableListDataSource
6 let tableListDataSource: TableListItem[] = [];
7
8 for (let i = 0; i < 8; i += 1) {
9 tableListDataSource.push({
10 key: i,
11 disabled: i % 6 === 0,
12 href: 'https://ant.design',
13 avatar: [
14 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
15 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
16 ][i % 2],
17 name: `TradeCode ${i}`,
18 title: `一个任务名称 ${i}`,
19 owner: '曲丽丽',
20 desc: '这是一段描述',
21 callNo: Math.floor(Math.random() * 1000),
22 status: Math.floor(Math.random() * 10) % 4,
23 updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
24 createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
25 progress: Math.ceil(Math.random() * 100),
26 });
27 }
28
29 function getRule(req: Request, res: Response, u: string) {
30 let url = u;
31 if (!url || Object.prototype.toString.call(url) !== '[object String]') {
32 // eslint-disable-next-line prefer-destructuring
33 url = req.url;
34 }
35
36 const params = (parse(url, true).query as unknown) as TableListParams;
37
38 let dataSource = tableListDataSource;
39
40 if (params.sorter) {
41 const s = params.sorter.split('_');
42 dataSource = dataSource.sort((prev, next) => {
43 if (s[1] === 'descend') {
44 return next[s[0]] - prev[s[0]];
45 }
46 return prev[s[0]] - next[s[0]];
47 });
48 }
49
50 if (params.status) {
51 const status = params.status.split(',');
52 let filterDataSource: TableListItem[] = [];
53 status.forEach((s: string) => {
54 filterDataSource = filterDataSource.concat(
55 dataSource.filter(item => {
56 if (parseInt(`${item.status}`, 10) === parseInt(s.split('')[0], 10)) {
57 return true;
58 }
59 return false;
60 }),
61 );
62 });
63 dataSource = filterDataSource;
64 }
65
66 if (params.name) {
67 dataSource = dataSource.filter(data => data.name.indexOf(params.name) > -1);
68 }
69
70 let pageSize = 10;
71 if (params.pageSize) {
72 pageSize = parseInt(`${params.pageSize}`, 0);
73 }
74
75 const result = {
76 list: dataSource,
77 pagination: {
78 total: dataSource.length,
79 pageSize,
80 current: parseInt(`${params.currentPage}`, 10) || 1,
81 },
82 };
83
84 return res.json(result);
85 }
86
87 function postRule(req: Request, res: Response, u: string, b: Request) {
88 let url = u;
89 if (!url || Object.prototype.toString.call(url) !== '[object String]') {
90 // eslint-disable-next-line prefer-destructuring
91 url = req.url;
92 }
93
94 const body = (b && b.body) || req.body;
95 const { method, name, desc, key } = body;
96
97 switch (method) {
98 /* eslint no-case-declarations:0 */
99 case 'delete':
100 tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1);
101 break;
102 case 'post':
103 const i = Math.ceil(Math.random() * 10000);
104 tableListDataSource.unshift({
105 key: i,
106 href: 'https://ant.design',
107 avatar: [
108 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
109 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
110 ][i % 2],
111 name: `TradeCode ${i}`,
112 title: `一个任务名称 ${i}`,
113 owner: '曲丽丽',
114 desc,
115 callNo: Math.floor(Math.random() * 1000),
116 status: Math.floor(Math.random() * 10) % 2,
117 updatedAt: new Date(),
118 createdAt: new Date(),
119 progress: Math.ceil(Math.random() * 100),
120 });
121 break;
122 case 'update':
123 tableListDataSource = tableListDataSource.map(item => {
124 if (item.key === key) {
125 return { ...item, desc, name };
126 }
127 return item;
128 });
129 break;
130 default:
131 break;
132 }
133
134 const result = {
135 list: tableListDataSource,
136 pagination: {
137 total: tableListDataSource.length,
138 },
139 };
140
141 return res.json(result);
142 }
143
144 export default {
145 'GET /api/rule': getRule,
146 'POST /api/rule': postRule,
147 };
1 export interface TableListItem {
2 key: number;
3 disabled?: boolean;
4 href: string;
5 avatar: string;
6 name: string;
7 title: string;
8 owner: string;
9 desc: string;
10 callNo: number;
11 status: number;
12 updatedAt: Date;
13 createdAt: Date;
14 progress: number;
15 }
16
17 export interface TableListPagination {
18 total: number;
19 pageSize: number;
20 current: number;
21 }
22
23 export interface TableListData {
24 list: TableListItem[];
25 pagination: Partial<TableListPagination>;
26 }
27
28 export interface TableListParams {
29 sorter: string;
30 status: string;
31 name: string;
32 pageSize: number;
33 currentPage: number;
34 }
1 import {
2 Badge,
3 Button,
4 Card,
5 Col,
6 DatePicker,
7 Divider,
8 Dropdown,
9 Form,
10 Icon,
11 Input,
12 InputNumber,
13 Menu,
14 Row,
15 Select,
16 message,
17 } from 'antd';
18 import React, { Component, Fragment } from 'react';
19
20 import { Dispatch, Action } from 'redux';
21 import { FormComponentProps } from 'antd/es/form';
22 import { PageHeaderWrapper } from '@ant-design/pro-layout';
23 import { SorterResult } from 'antd/es/table';
24 import { connect } from 'dva';
25 import moment from 'moment';
26 import { StateType } from './model';
27 import StandardTable, { StandardTableColumnProps } from '../../components/StandardTable';
28 import { TableListItem, TableListPagination, TableListParams } from './data.d';
29
30 import styles from './style.less';
31
32 const FormItem = Form.Item;
33 const { Option } = Select;
34 const getValue = (obj: { [x: string]: string[] }) =>
35 Object.keys(obj)
36 .map(key => obj[key])
37 .join(',');
38
39 type IStatusMapType = 'default' | 'processing' | 'success' | 'error';
40 const statusMap = ['default', 'processing', 'success', 'error'];
41 const status = ['关闭', '运行中', '已上线', '异常'];
42
43 interface TableListProps extends FormComponentProps {
44 dispatch: Dispatch<
45 Action<
46 | 'employees/add'
47 | 'employees/fetch'
48 | 'employees/remove'
49 | 'employees/update'
50 >
51 >;
52 loading: boolean;
53 employees: StateType;
54 }
55
56 interface TableListState {
57 modalVisible: boolean;
58 updateModalVisible: boolean;
59 expandForm: boolean;
60 selectedRows: TableListItem[];
61 formValues: { [key: string]: string };
62 stepFormValues: Partial<TableListItem>;
63 }
64
65 /* eslint react/no-multi-comp:0 */
66 @connect(
67 ({
68 employees,
69 loading,
70 }: {
71 employees: StateType;
72 loading: {
73 models: {
74 [key: string]: boolean;
75 };
76 };
77 }) => ({
78 employees,
79 loading: loading.models.employees,
80 }),
81 )
82 class TableList extends Component<TableListProps, TableListState> {
83 state: TableListState = {
84 modalVisible: false,
85 updateModalVisible: false,
86 expandForm: false,
87 selectedRows: [],
88 formValues: {},
89 stepFormValues: {},
90 };
91
92 columns: StandardTableColumnProps[] = [
93 {
94 title: '规则名称',
95 dataIndex: 'name',
96 },
97 {
98 title: '描述',
99 dataIndex: 'desc',
100 },
101 {
102 title: '服务调用次数',
103 dataIndex: 'callNo',
104 sorter: true,
105 align: 'right',
106 render: (val: string) => `${val} 万`,
107 // mark to display a total number
108 needTotal: true,
109 },
110 {
111 title: '状态',
112 dataIndex: 'status',
113 filters: [
114 {
115 text: status[0],
116 value: '0',
117 },
118 {
119 text: status[1],
120 value: '1',
121 },
122 {
123 text: status[2],
124 value: '2',
125 },
126 {
127 text: status[3],
128 value: '3',
129 },
130 ],
131 render(val: IStatusMapType) {
132 return <Badge status={statusMap[val]} text={status[val]} />;
133 },
134 },
135 {
136 title: '上次调度时间',
137 dataIndex: 'updatedAt',
138 sorter: true,
139 render: (val: string) => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
140 },
141 {
142 title: '操作',
143 render: (text, record) => (
144 <Fragment>
145 <a href="">报告溯源</a>
146 </Fragment>
147 ),
148 }
149 ];
150
151 componentDidMount() {
152 const { dispatch } = this.props;
153 dispatch({
154 type: 'employees/fetch',
155 });
156 }
157
158 handleStandardTableChange = (
159 pagination: Partial<TableListPagination>,
160 filtersArg: Record<keyof TableListItem, string[]>,
161 sorter: SorterResult<TableListItem>,
162 ) => {
163 const { dispatch } = this.props;
164 const { formValues } = this.state;
165
166 const filters = Object.keys(filtersArg).reduce((obj, key) => {
167 const newObj = { ...obj };
168 newObj[key] = getValue(filtersArg[key]);
169 return newObj;
170 }, {});
171
172 const params: Partial<TableListParams> = {
173 currentPage: pagination.current,
174 pageSize: pagination.pageSize,
175 ...formValues,
176 ...filters,
177 };
178 if (sorter.field) {
179 params.sorter = `${sorter.field}_${sorter.order}`;
180 }
181
182 dispatch({
183 type: 'employees/fetch',
184 payload: params,
185 });
186 };
187
188 handleFormReset = () => {
189 const { form, dispatch } = this.props;
190 form.resetFields();
191 this.setState({
192 formValues: {},
193 });
194 dispatch({
195 type: 'employees/fetch',
196 payload: {},
197 });
198 };
199
200 toggleForm = () => {
201 const { expandForm } = this.state;
202 this.setState({
203 expandForm: !expandForm,
204 });
205 };
206
207 handleMenuClick = (e: { key: string }) => {
208 const { dispatch } = this.props;
209 const { selectedRows } = this.state;
210
211 if (!selectedRows) return;
212 switch (e.key) {
213 case 'remove':
214 dispatch({
215 type: 'employees/remove',
216 payload: {
217 key: selectedRows.map(row => row.key),
218 },
219 callback: () => {
220 this.setState({
221 selectedRows: [],
222 });
223 },
224 });
225 break;
226 default:
227 break;
228 }
229 };
230
231 handleSelectRows = (rows: TableListItem[]) => {
232 this.setState({
233 selectedRows: rows,
234 });
235 };
236
237 handleSearch = (e: React.FormEvent) => {
238 e.preventDefault();
239
240 const { dispatch, form } = this.props;
241
242 form.validateFields((err, fieldsValue) => {
243 if (err) return;
244
245 const values = {
246 ...fieldsValue,
247 updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(),
248 };
249
250 this.setState({
251 formValues: values,
252 });
253
254 dispatch({
255 type: 'employees/fetch',
256 payload: values,
257 });
258 });
259 };
260
261 renderSimpleForm() {
262 const { form } = this.props;
263 const { getFieldDecorator } = form;
264 return (
265 <Form onSubmit={this.handleSearch} layout="inline">
266 <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
267 <Col md={8} sm={24}>
268 <FormItem label="规则名称">
269 {getFieldDecorator('name')(<Input placeholder="请输入" />)}
270 </FormItem>
271 </Col>
272 <Col md={8} sm={24}>
273 <FormItem label="使用状态">
274 {getFieldDecorator('status')(
275 <Select placeholder="请选择" style={{ width: '100%' }}>
276 <Option value="0">关闭</Option>
277 <Option value="1">运行中</Option>
278 </Select>,
279 )}
280 </FormItem>
281 </Col>
282 <Col md={8} sm={24}>
283 <span className={styles.submitButtons}>
284 <Button type="primary" htmlType="submit">
285 查询
286 </Button>
287 <Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
288 重置
289 </Button>
290 <a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
291 展开 <Icon type="down" />
292 </a>
293 </span>
294 </Col>
295 </Row>
296 </Form>
297 );
298 }
299
300 renderAdvancedForm() {
301 const {
302 form: { getFieldDecorator },
303 } = this.props;
304 return (
305 <Form onSubmit={this.handleSearch} layout="inline">
306 <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
307 <Col md={8} sm={24}>
308 <FormItem label="规则名称">
309 {getFieldDecorator('name')(<Input placeholder="请输入" />)}
310 </FormItem>
311 </Col>
312 <Col md={8} sm={24}>
313 <FormItem label="使用状态">
314 {getFieldDecorator('status')(
315 <Select placeholder="请选择" style={{ width: '100%' }}>
316 <Option value="0">关闭</Option>
317 <Option value="1">运行中</Option>
318 </Select>,
319 )}
320 </FormItem>
321 </Col>
322 <Col md={8} sm={24}>
323 <FormItem label="调用次数">
324 {getFieldDecorator('number')(<InputNumber style={{ width: '100%' }} />)}
325 </FormItem>
326 </Col>
327 </Row>
328 <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
329 <Col md={8} sm={24}>
330 <FormItem label="更新日期">
331 {getFieldDecorator('date')(
332 <DatePicker style={{ width: '100%' }} placeholder="请输入更新日期" />,
333 )}
334 </FormItem>
335 </Col>
336 <Col md={8} sm={24}>
337 <FormItem label="使用状态">
338 {getFieldDecorator('status3')(
339 <Select placeholder="请选择" style={{ width: '100%' }}>
340 <Option value="0">关闭</Option>
341 <Option value="1">运行中</Option>
342 </Select>,
343 )}
344 </FormItem>
345 </Col>
346 <Col md={8} sm={24}>
347 <FormItem label="使用状态">
348 {getFieldDecorator('status4')(
349 <Select placeholder="请选择" style={{ width: '100%' }}>
350 <Option value="0">关闭</Option>
351 <Option value="1">运行中</Option>
352 </Select>,
353 )}
354 </FormItem>
355 </Col>
356 </Row>
357 <div style={{ overflow: 'hidden' }}>
358 <div style={{ float: 'right', marginBottom: 24 }}>
359 <Button type="primary" htmlType="submit">
360 查询
361 </Button>
362 <Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
363 重置
364 </Button>
365 <a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
366 收起 <Icon type="up" />
367 </a>
368 </div>
369 </div>
370 </Form>
371 );
372 }
373
374 renderForm() {
375 const { expandForm } = this.state;
376 return expandForm ? this.renderAdvancedForm() : this.renderSimpleForm();
377 }
378
379 render() {
380 const {
381 employees: { data },
382 loading,
383 } = this.props;
384
385 const { selectedRows, stepFormValues } = this.state;
386 const menu = (
387 <Menu onClick={this.handleMenuClick} selectedKeys={[]}>
388 <Menu.Item key="remove">删除</Menu.Item>
389 <Menu.Item key="approval">批量审批</Menu.Item>
390 </Menu>
391 );
392
393 return (
394 <PageHeaderWrapper>
395 <Card bordered={false}>
396 <div className={styles.tableList}>
397 <div className={styles.tableListForm}>{this.renderForm()}</div>
398 <div className={styles.tableListOperator}>
399 {selectedRows.length > 0 && (
400 <span>
401 <Button>批量操作</Button>
402 <Dropdown overlay={menu}>
403 <Button>
404 更多操作 <Icon type="down" />
405 </Button>
406 </Dropdown>
407 </span>
408 )}
409 </div>
410 <StandardTable
411 selectedRows={selectedRows}
412 loading={loading}
413 data={data}
414 columns={this.columns}
415 onSelectRow={this.handleSelectRows}
416 onChange={this.handleStandardTableChange}
417 />
418 </div>
419 </Card>
420 </PageHeaderWrapper>
421 );
422 }
423 }
424
425 export default Form.create<TableListProps>()(TableList);
1 import { AnyAction, Reducer } from 'redux';
2 import { EffectsCommandMap } from 'dva';
3 import { addRule, queryRule, removeRule, updateRule } from './service';
4
5 import { TableListData } from './data.d';
6
7 export interface StateType {
8 data: TableListData;
9 }
10
11 export type Effect = (
12 action: AnyAction,
13 effects: EffectsCommandMap & { select: <T>(func: (state: StateType) => T) => T },
14 ) => void;
15
16 export interface ModelType {
17 namespace: string;
18 state: StateType;
19 effects: {
20 fetch: Effect;
21 add: Effect;
22 remove: Effect;
23 update: Effect;
24 };
25 reducers: {
26 save: Reducer<StateType>;
27 };
28 }
29
30 const Model: ModelType = {
31 namespace: 'employees',
32
33 state: {
34 data: {
35 list: [],
36 pagination: {},
37 },
38 },
39
40 effects: {
41 *fetch({ payload }, { call, put }) {
42 const response = yield call(queryRule, payload);
43 yield put({
44 type: 'save',
45 payload: response,
46 });
47 },
48 *add({ payload, callback }, { call, put }) {
49 const response = yield call(addRule, payload);
50 yield put({
51 type: 'save',
52 payload: response,
53 });
54 if (callback) callback();
55 },
56 *remove({ payload, callback }, { call, put }) {
57 const response = yield call(removeRule, payload);
58 yield put({
59 type: 'save',
60 payload: response,
61 });
62 if (callback) callback();
63 },
64 *update({ payload, callback }, { call, put }) {
65 const response = yield call(updateRule, payload);
66 yield put({
67 type: 'save',
68 payload: response,
69 });
70 if (callback) callback();
71 },
72 },
73
74 reducers: {
75 save(state, action) {
76 return {
77 ...state,
78 data: action.payload,
79 };
80 },
81 },
82 };
83
84 export default Model;
1 import request from '@/utils/request';
2 import { TableListParams } from './data.d';
3
4 export async function queryRule(params: TableListParams) {
5 return request('/api/rule', {
6 params,
7 });
8 }
9
10 export async function removeRule(params: TableListParams) {
11 return request('/api/rule', {
12 method: 'POST',
13 data: {
14 ...params,
15 method: 'delete',
16 },
17 });
18 }
19
20 export async function addRule(params: TableListParams) {
21 return request('/api/rule', {
22 method: 'POST',
23 data: {
24 ...params,
25 method: 'post',
26 },
27 });
28 }
29
30 export async function updateRule(params: TableListParams) {
31 return request('/api/rule', {
32 method: 'POST',
33 data: {
34 ...params,
35 method: 'update',
36 },
37 });
38 }
1 @import '~antd/es/style/themes/default.less';
2 @import '../../utils/utils.less';
3
4 .tableList {
5 .tableListOperator {
6 margin-bottom: 16px;
7 button {
8 margin-right: 8px;
9 }
10 }
11 }
12
13 .tableListForm {
14 :global {
15 .ant-form-item {
16 display: flex;
17 margin-right: 0;
18 margin-bottom: 24px;
19 > .ant-form-item-label {
20 width: auto;
21 padding-right: 8px;
22 line-height: 32px;
23 }
24 .ant-form-item-control {
25 line-height: 32px;
26 }
27 }
28 .ant-form-item-control-wrapper {
29 flex: 1;
30 }
31 }
32 .submitButtons {
33 display: block;
34 margin-bottom: 24px;
35 white-space: nowrap;
36 }
37 }
38
39 @media screen and (max-width: @screen-lg) {
40 .tableListForm :global(.ant-form-item) {
41 margin-right: 24px;
42 }
43 }
44
45 @media screen and (max-width: @screen-md) {
46 .tableListForm :global(.ant-form-item) {
47 margin-right: 8px;
48 }
49 }
1 import { Request, Response } from 'express';
2 import { parse } from 'url';
3 import { TableListItem, TableListParams } from './data.d';
4
5 // mock tableListDataSource
6 let tableListDataSource: TableListItem[] = [];
7
8 for (let i = 0; i < 8; i += 1) {
9 tableListDataSource.push({
10 key: i,
11 disabled: i % 6 === 0,
12 href: 'https://ant.design',
13 avatar: [
14 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
15 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
16 ][i % 2],
17 name: `TradeCode ${i}`,
18 title: `一个任务名称 ${i}`,
19 owner: '曲丽丽',
20 desc: '这是一段描述',
21 callNo: Math.floor(Math.random() * 1000),
22 status: Math.floor(Math.random() * 10) % 4,
23 updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
24 createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
25 progress: Math.ceil(Math.random() * 100),
26 });
27 }
28
29 function getRule(req: Request, res: Response, u: string) {
30 let url = u;
31 if (!url || Object.prototype.toString.call(url) !== '[object String]') {
32 // eslint-disable-next-line prefer-destructuring
33 url = req.url;
34 }
35
36 const params = (parse(url, true).query as unknown) as TableListParams;
37
38 let dataSource = tableListDataSource;
39
40 if (params.sorter) {
41 const s = params.sorter.split('_');
42 dataSource = dataSource.sort((prev, next) => {
43 if (s[1] === 'descend') {
44 return next[s[0]] - prev[s[0]];
45 }
46 return prev[s[0]] - next[s[0]];
47 });
48 }
49
50 if (params.status) {
51 const status = params.status.split(',');
52 let filterDataSource: TableListItem[] = [];
53 status.forEach((s: string) => {
54 filterDataSource = filterDataSource.concat(
55 dataSource.filter(item => {
56 if (parseInt(`${item.status}`, 10) === parseInt(s.split('')[0], 10)) {
57 return true;
58 }
59 return false;
60 }),
61 );
62 });
63 dataSource = filterDataSource;
64 }
65
66 if (params.name) {
67 dataSource = dataSource.filter(data => data.name.indexOf(params.name) > -1);
68 }
69
70 let pageSize = 10;
71 if (params.pageSize) {
72 pageSize = parseInt(`${params.pageSize}`, 0);
73 }
74
75 const result = {
76 list: dataSource,
77 pagination: {
78 total: dataSource.length,
79 pageSize,
80 current: parseInt(`${params.currentPage}`, 10) || 1,
81 },
82 };
83
84 return res.json(result);
85 }
86
87 function postRule(req: Request, res: Response, u: string, b: Request) {
88 let url = u;
89 if (!url || Object.prototype.toString.call(url) !== '[object String]') {
90 // eslint-disable-next-line prefer-destructuring
91 url = req.url;
92 }
93
94 const body = (b && b.body) || req.body;
95 const { method, name, desc, key } = body;
96
97 switch (method) {
98 /* eslint no-case-declarations:0 */
99 case 'delete':
100 tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1);
101 break;
102 case 'post':
103 const i = Math.ceil(Math.random() * 10000);
104 tableListDataSource.unshift({
105 key: i,
106 href: 'https://ant.design',
107 avatar: [
108 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
109 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
110 ][i % 2],
111 name: `TradeCode ${i}`,
112 title: `一个任务名称 ${i}`,
113 owner: '曲丽丽',
114 desc,
115 callNo: Math.floor(Math.random() * 1000),
116 status: Math.floor(Math.random() * 10) % 2,
117 updatedAt: new Date(),
118 createdAt: new Date(),
119 progress: Math.ceil(Math.random() * 100),
120 });
121 break;
122 case 'update':
123 tableListDataSource = tableListDataSource.map(item => {
124 if (item.key === key) {
125 return { ...item, desc, name };
126 }
127 return item;
128 });
129 break;
130 default:
131 break;
132 }
133
134 const result = {
135 list: tableListDataSource,
136 pagination: {
137 total: tableListDataSource.length,
138 },
139 };
140
141 return res.json(result);
142 }
143
144 export default {
145 'GET /api/rule': getRule,
146 'POST /api/rule': postRule,
147 };
1 ul li {
2 list-style: none;
3 }
4
5 .package-status {
6 padding: 18px 0 0 0
7 }
8
9 .package-status .status-box {
10 position: relative;
11 height: 100%;
12 overflow: hidden;
13 text-align: center;
14 }
15
16 .package-status .status-list {
17 overflow-y: auto;
18 height: 200%;
19 margin: 0;
20 padding: 0;
21 margin-top: -5px;
22 padding-left: 30%;
23 list-style: none;
24 }
25
26 .package-status .status-list>li {
27 height: auto;
28 width: 95%;
29 border-left: 1px dashed rgba(29, 29, 29, 0.5);
30 /* border-left: 2px solid #0278D8; */
31 text-align: left;
32 }
33
34 .package-status .status-list>li:not(:first-child):before {
35 content: '';
36 background-image: url(../../../../assets/images/logos/right/list.png);
37 background-repeat: no-repeat;
38 width: 15px;
39 height: 15px;
40 border-radius: 50%;
41 display: inline-block;
42 margin-right: 10px;
43 background-position: -5px -5px;
44 margin-left: -9px;
45 }
46
47 .package-status .status-list>li:first-child:before {
48 content: '';
49 background-image: url(../../../../assets/images/logos/right/right.png);
50 background-repeat: no-repeat;
51 width: 15px;
52 height: 15px;
53 border-radius: 50%;
54 display: inline-block;
55 margin-right: 10px;
56 background-position: -5px -7px;
57 margin-left: -9px;
58 }
59
60 .status-list>li:first-child {
61 padding-top: 5px;
62 }
63
64 .status-list>li:not(:first-child) {
65 padding-top: 20px;
66 }
67
68 .status-list>li:not(:first-child) span {
69 display: none;
70 }
71
72 .status-list>li:first-child span {
73 background-color: #30363e;
74 display: inline-block;
75 margin-left: 10px;
76 font-size: 12px;
77 width: 95px;
78 height: 25px;
79 line-height: 25px;
80 text-align: center;
81 color: #fff;
82 }
83 .status-list>li:not(:first-child) .cert {
84 display: none;
85 }
86
87 .status-list>li:first-child .cert {
88 display: block;
89 }
90
91 .status-content-before {
92 height: 23px;
93 line-height: 17px;
94 text-align: left;
95 margin-left: 25px;
96 margin-top: -20px;
97 margin-bottom: 15px;
98 font-size: 16px;
99 font-weight: bold;
100 color: rgba(29, 29, 29, 1);
101 }
102
103 .status-time-before {
104 text-align: left;
105 margin-left: 25px;
106 font-size: 12px;
107 color: #1D1D1D;
108 margin-bottom: 5px;
109 }
110
111 .status-a {
112 display: block;
113 text-align: left;
114 margin-left: 25px;
115 font-size: 12px;
116 color: #0278D8;
117 cursor: pointer;
118 }
119
120 button {
121 width: 20%;
122 height: 31px;
123 line-height: 31px;
124 margin-top: 15px;
125 margin-bottom: 15px;
126 outline: none;
127 border-radius: 6px;
128 color: #0278D8;
129 border: 1px solid #0278D8;
130 background-color: #e9e9e9;
131 }
...\ No newline at end of file ...\ No newline at end of file
1 import React from 'react';
2 import { Card, Alert } from 'antd';
3 import { PageHeaderWrapper } from '@ant-design/pro-layout';
4 import request from '@/utils/request';
5
6 export default class ReportBlock extends React.Component {
7 more = '加载更多';
8 state: any;
9 constructor(props: any) {
10 super(props);
11 this.state.block = {
12 blockList: [],
13 totalItems: 0,
14 totalPages: 1,
15 page: 1,
16 per_page: 6
17 };
18 }
19
20 componentDidMount() {
21 let id: string = '';
22 this.blockhistory(
23 { page: this.state.page, per_page: this.state.per_page, report_id: id }
24 ).then((resp: any) => {
25 this.state.blockList = resp.data;
26 this.state.totalItems = resp.total_items;
27 this.state.totalPages = resp.total_pages;
28 if (this.state.totalPages === 1) {
29 this.more = '加载完成';
30 }
31 if (this.state.blockList.length === 0) {
32 this.more = '没有历史';
33 }
34 });
35 }
36
37 componentWillUnmount() {
38 }
39
40 blockhistory(params: any) {
41 return request('/api/rule', {
42 method: 'GET',
43 data: {
44 ...params,
45 method: 'get',
46 },
47 });
48 }
49
50 getMore = () => {
51 let id: string = '1';
52 if (this.state.page + 1 < this.state.totalPages) {
53 this.blockhistory(
54 { page: this.state.page + 1, per_page: this.state.per_page, report_id: id }
55 ).then((r: any) => {
56 let history = r.data;
57 this.setState({
58 page: r.page,
59 totalItems: r.total_items,
60 totalPages: r.total_pages,
61 blockList: [this.state.blockList, ...history]
62 });
63 });
64 } else {
65 this.more = '加载完成';
66 }
67 }
68
69 listItems = () => {
70 return this.state.blockList.map((block: any) =>
71 <li>
72 <div className="status-content-before">
73 {block.created_at}&nbsp;&nbsp;{block.content}
74 <span>报告已存证</span>
75 </div>
76 <div className="status-time-before">md5值: {block.content_md5}</div>
77 <div className="status-time-before">交易id: {block.transaction_id? block.transaction_id : '--'}</div>
78 <a className="status-a cert" href="http://testnet.ebookchain.org/assets/{{block.object_id}}"
79 target="_blank">报告证书
80 </a>
81 <a className="status-a" href="http://testnet.ebookchain.org/transactions/{{block.transaction_id}}"
82 target="_blank">交易详情
83 </a>
84 </li>
85 );
86 }
87
88 render() {
89 return (
90 <PageHeaderWrapper>
91 <Card bordered={false}>
92 <div className="package-status">
93 <div className="status-box">
94 <h2 style={{paddingLeft: '30%', textAlign: 'left'}}>报告追溯详情</h2>
95 <ul className="status-list">
96 {this.listItems()}
97 </ul>
98 <button type="button" onClick={this.getMore}>{this.more}</button>
99 </div>
100 </div>
101 </Card>
102 </PageHeaderWrapper>
103 );
104 };
105 }
1 export interface TableListItem {
2 key: number;
3 disabled?: boolean;
4 href: string;
5 avatar: string;
6 name: string;
7 title: string;
8 owner: string;
9 desc: string;
10 callNo: number;
11 status: number;
12 updatedAt: Date;
13 createdAt: Date;
14 progress: number;
15 }
16
17 export interface TableListPagination {
18 total: number;
19 pageSize: number;
20 current: number;
21 }
22
23 export interface TableListData {
24 list: TableListItem[];
25 pagination: Partial<TableListPagination>;
26 }
27
28 export interface TableListParams {
29 sorter: string;
30 status: string;
31 name: string;
32 pageSize: number;
33 currentPage: number;
34 }
1 import { Form, Input, Modal } from 'antd';
2
3 import { FormComponentProps } from 'antd/es/form';
4 import React from 'react';
5
6 const FormItem = Form.Item;
7
8 interface CreateFormProps extends FormComponentProps {
9 modalVisible: boolean;
10 handleAdd: (fieldsValue: { desc: string }) => void;
11 handleModalVisible: () => void;
12 }
13 const CreateForm: React.FC<CreateFormProps> = props => {
14 const { modalVisible, form, handleAdd, handleModalVisible } = props;
15 const okHandle = () => {
16 form.validateFields((err, fieldsValue) => {
17 if (err) return;
18 form.resetFields();
19 handleAdd(fieldsValue);
20 });
21 };
22 return (
23 <Modal
24 destroyOnClose
25 title="新建规则"
26 visible={modalVisible}
27 onOk={okHandle}
28 onCancel={() => handleModalVisible()}
29 >
30 <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="描述">
31 {form.getFieldDecorator('desc', {
32 rules: [{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }],
33 })(<Input placeholder="请输入" />)}
34 </FormItem>
35 </Modal>
36 );
37 };
38
39 export default Form.create<CreateFormProps>()(CreateForm);
1 import { Button, DatePicker, Form, Input, Modal, Radio, Select, Steps } from 'antd';
2 import React, { Component } from 'react';
3
4 import { FormComponentProps } from 'antd/es/form';
5 import { TableListItem } from '../data.d';
6
7 export interface FormValueType extends Partial<TableListItem> {
8 target?: string;
9 template?: string;
10 type?: string;
11 time?: string;
12 frequency?: string;
13 }
14
15 export interface UpdateFormProps extends FormComponentProps {
16 handleUpdateModalVisible: (flag?: boolean, formVals?: FormValueType) => void;
17 handleUpdate: (values: FormValueType) => void;
18 updateModalVisible: boolean;
19 values: Partial<TableListItem>;
20 }
21 const FormItem = Form.Item;
22 const { Step } = Steps;
23 const { TextArea } = Input;
24 const { Option } = Select;
25 const RadioGroup = Radio.Group;
26
27 export interface UpdateFormState {
28 formVals: FormValueType;
29 currentStep: number;
30 }
31
32 class UpdateForm extends Component<UpdateFormProps, UpdateFormState> {
33 static defaultProps = {
34 handleUpdate: () => {},
35 handleUpdateModalVisible: () => {},
36 values: {},
37 };
38
39 formLayout = {
40 labelCol: { span: 7 },
41 wrapperCol: { span: 13 },
42 };
43
44 constructor(props: UpdateFormProps) {
45 super(props);
46
47 this.state = {
48 formVals: {
49 name: props.values.name,
50 desc: props.values.desc,
51 key: props.values.key,
52 target: '0',
53 template: '0',
54 type: '1',
55 time: '',
56 frequency: 'month',
57 },
58 currentStep: 0,
59 };
60 }
61
62 handleNext = (currentStep: number) => {
63 const { form, handleUpdate } = this.props;
64 const { formVals: oldValue } = this.state;
65 form.validateFields((err, fieldsValue) => {
66 if (err) return;
67 const formVals = { ...oldValue, ...fieldsValue };
68 this.setState(
69 {
70 formVals,
71 },
72 () => {
73 if (currentStep < 2) {
74 this.forward();
75 } else {
76 handleUpdate(formVals);
77 }
78 },
79 );
80 });
81 };
82
83 backward = () => {
84 const { currentStep } = this.state;
85 this.setState({
86 currentStep: currentStep - 1,
87 });
88 };
89
90 forward = () => {
91 const { currentStep } = this.state;
92 this.setState({
93 currentStep: currentStep + 1,
94 });
95 };
96
97 renderContent = (currentStep: number, formVals: FormValueType) => {
98 const { form } = this.props;
99 if (currentStep === 1) {
100 return [
101 <FormItem key="target" {...this.formLayout} label="监控对象">
102 {form.getFieldDecorator('target', {
103 initialValue: formVals.target,
104 })(
105 <Select style={{ width: '100%' }}>
106 <Option value="0">表一</Option>
107 <Option value="1">表二</Option>
108 </Select>,
109 )}
110 </FormItem>,
111 <FormItem key="template" {...this.formLayout} label="规则模板">
112 {form.getFieldDecorator('template', {
113 initialValue: formVals.template,
114 })(
115 <Select style={{ width: '100%' }}>
116 <Option value="0">规则模板一</Option>
117 <Option value="1">规则模板二</Option>
118 </Select>,
119 )}
120 </FormItem>,
121 <FormItem key="type" {...this.formLayout} label="规则类型">
122 {form.getFieldDecorator('type', {
123 initialValue: formVals.type,
124 })(
125 <RadioGroup>
126 <Radio value="0">强</Radio>
127 <Radio value="1">弱</Radio>
128 </RadioGroup>,
129 )}
130 </FormItem>,
131 ];
132 }
133 if (currentStep === 2) {
134 return [
135 <FormItem key="time" {...this.formLayout} label="开始时间">
136 {form.getFieldDecorator('time', {
137 rules: [{ required: true, message: '请选择开始时间!' }],
138 })(
139 <DatePicker
140 style={{ width: '100%' }}
141 showTime
142 format="YYYY-MM-DD HH:mm:ss"
143 placeholder="选择开始时间"
144 />,
145 )}
146 </FormItem>,
147 <FormItem key="frequency" {...this.formLayout} label="调度周期">
148 {form.getFieldDecorator('frequency', {
149 initialValue: formVals.frequency,
150 })(
151 <Select style={{ width: '100%' }}>
152 <Option value="month">月</Option>
153 <Option value="week">周</Option>
154 </Select>,
155 )}
156 </FormItem>,
157 ];
158 }
159 return [
160 <FormItem key="name" {...this.formLayout} label="规则名称">
161 {form.getFieldDecorator('name', {
162 rules: [{ required: true, message: '请输入规则名称!' }],
163 initialValue: formVals.name,
164 })(<Input placeholder="请输入" />)}
165 </FormItem>,
166 <FormItem key="desc" {...this.formLayout} label="规则描述">
167 {form.getFieldDecorator('desc', {
168 rules: [{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }],
169 initialValue: formVals.desc,
170 })(<TextArea rows={4} placeholder="请输入至少五个字符" />)}
171 </FormItem>,
172 ];
173 };
174
175 renderFooter = (currentStep: number) => {
176 const { handleUpdateModalVisible, values } = this.props;
177 if (currentStep === 1) {
178 return [
179 <Button key="back" style={{ float: 'left' }} onClick={this.backward}>
180 上一步
181 </Button>,
182 <Button key="cancel" onClick={() => handleUpdateModalVisible(false, values)}>
183 取消
184 </Button>,
185 <Button key="forward" type="primary" onClick={() => this.handleNext(currentStep)}>
186 下一步
187 </Button>,
188 ];
189 }
190 if (currentStep === 2) {
191 return [
192 <Button key="back" style={{ float: 'left' }} onClick={this.backward}>
193 上一步
194 </Button>,
195 <Button key="cancel" onClick={() => handleUpdateModalVisible(false, values)}>
196 取消
197 </Button>,
198 <Button key="submit" type="primary" onClick={() => this.handleNext(currentStep)}>
199 完成
200 </Button>,
201 ];
202 }
203 return [
204 <Button key="cancel" onClick={() => handleUpdateModalVisible(false, values)}>
205 取消
206 </Button>,
207 <Button key="forward" type="primary" onClick={() => this.handleNext(currentStep)}>
208 下一步
209 </Button>,
210 ];
211 };
212
213 render() {
214 const { updateModalVisible, handleUpdateModalVisible, values } = this.props;
215 const { currentStep, formVals } = this.state;
216
217 return (
218 <Modal
219 width={640}
220 bodyStyle={{ padding: '32px 40px 48px' }}
221 destroyOnClose
222 title="规则配置"
223 visible={updateModalVisible}
224 footer={this.renderFooter(currentStep)}
225 onCancel={() => handleUpdateModalVisible(false, values)}
226 afterClose={() => handleUpdateModalVisible()}
227 >
228 <Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
229 <Step title="基本信息" />
230 <Step title="配置规则属性" />
231 <Step title="设定调度周期" />
232 </Steps>
233 {this.renderContent(currentStep, formVals)}
234 </Modal>
235 );
236 }
237 }
238
239 export default Form.create<UpdateFormProps>()(UpdateForm);
1 import {
2 Badge,
3 Button,
4 Card,
5 Col,
6 DatePicker,
7 Divider,
8 Dropdown,
9 Form,
10 Icon,
11 Input,
12 InputNumber,
13 Menu,
14 Row,
15 Select,
16 message,
17 } from 'antd';
18 import React, { Component, Fragment } from 'react';
19
20 import { Dispatch, Action } from 'redux';
21 import { FormComponentProps } from 'antd/es/form';
22 import { PageHeaderWrapper } from '@ant-design/pro-layout';
23 import { SorterResult } from 'antd/es/table';
24 import { connect } from 'dva';
25 import moment from 'moment';
26 import { StateType } from './model';
27 import CreateForm from './forms/CreateForm';
28 import StandardTable, { StandardTableColumnProps } from '../../components/StandardTable';
29 import UpdateForm, { FormValueType } from './forms/UpdateForm';
30 import { TableListItem, TableListPagination, TableListParams } from './data.d';
31
32 import styles from './style.less';
33 import { Link } from 'umi';
34
35 const FormItem = Form.Item;
36 const { Option } = Select;
37 const getValue = (obj: { [x: string]: string[] }) =>
38 Object.keys(obj)
39 .map(key => obj[key])
40 .join(',');
41
42 type IStatusMapType = 'default' | 'processing' | 'success' | 'error';
43 const statusMap = ['default', 'processing', 'success', 'error'];
44 const status = ['关闭', '运行中', '已上线', '异常'];
45
46 interface TableListProps extends FormComponentProps {
47 dispatch: Dispatch<
48 Action<
49 | 'reports/add'
50 | 'reports/fetch'
51 | 'reports/remove'
52 | 'reports/update'
53 >
54 >;
55 loading: boolean;
56 reports: StateType;
57 }
58
59 interface TableListState {
60 modalVisible: boolean;
61 updateModalVisible: boolean;
62 expandForm: boolean;
63 selectedRows: TableListItem[];
64 formValues: { [key: string]: string };
65 stepFormValues: Partial<TableListItem>;
66 }
67
68 /* eslint react/no-multi-comp:0 */
69 @connect(
70 ({
71 reports,
72 loading,
73 }: {
74 reports: StateType;
75 loading: {
76 models: {
77 [key: string]: boolean;
78 };
79 };
80 }) => ({
81 reports,
82 loading: loading.models.reports,
83 }),
84 )
85 class TableList extends Component<TableListProps, TableListState> {
86 state: TableListState = {
87 modalVisible: false,
88 updateModalVisible: false,
89 expandForm: false,
90 selectedRows: [],
91 formValues: {},
92 stepFormValues: {},
93 };
94
95 columns: StandardTableColumnProps[] = [
96 {
97 title: '规则名称',
98 dataIndex: 'name',
99 },
100 {
101 title: '描述',
102 dataIndex: 'desc',
103 },
104 {
105 title: '服务调用次数',
106 dataIndex: 'callNo',
107 sorter: true,
108 align: 'right',
109 render: (val: string) => `${val} 万`,
110 // mark to display a total number
111 needTotal: true,
112 },
113 {
114 title: '状态',
115 dataIndex: 'status',
116 filters: [
117 {
118 text: status[0],
119 value: '0',
120 },
121 {
122 text: status[1],
123 value: '1',
124 },
125 {
126 text: status[2],
127 value: '2',
128 },
129 {
130 text: status[3],
131 value: '3',
132 },
133 ],
134 render(val: IStatusMapType) {
135 return <Badge status={statusMap[val]} text={status[val]} />;
136 },
137 },
138 {
139 title: '上次调度时间',
140 dataIndex: 'updatedAt',
141 sorter: true,
142 render: (val: string) => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
143 },
144 {
145 title: '操作',
146 render: (text, record) => (
147 <Fragment>
148 <Link to="/reports/123/block">报告溯源</Link>
149 </Fragment>
150 ),
151 },
152 ];
153
154 componentDidMount() {
155 const { dispatch } = this.props;
156 dispatch({
157 type: 'reports/fetch',
158 });
159 }
160
161 handleStandardTableChange = (
162 pagination: Partial<TableListPagination>,
163 filtersArg: Record<keyof TableListItem, string[]>,
164 sorter: SorterResult<TableListItem>,
165 ) => {
166 const { dispatch } = this.props;
167 const { formValues } = this.state;
168
169 const filters = Object.keys(filtersArg).reduce((obj, key) => {
170 const newObj = { ...obj };
171 newObj[key] = getValue(filtersArg[key]);
172 return newObj;
173 }, {});
174
175 const params: Partial<TableListParams> = {
176 currentPage: pagination.current,
177 pageSize: pagination.pageSize,
178 ...formValues,
179 ...filters,
180 };
181 if (sorter.field) {
182 params.sorter = `${sorter.field}_${sorter.order}`;
183 }
184
185 dispatch({
186 type: 'reports/fetch',
187 payload: params,
188 });
189 };
190
191 handleFormReset = () => {
192 const { form, dispatch } = this.props;
193 form.resetFields();
194 this.setState({
195 formValues: {},
196 });
197 dispatch({
198 type: 'reports/fetch',
199 payload: {},
200 });
201 };
202
203 toggleForm = () => {
204 const { expandForm } = this.state;
205 this.setState({
206 expandForm: !expandForm,
207 });
208 };
209
210 handleMenuClick = (e: { key: string }) => {
211 const { dispatch } = this.props;
212 const { selectedRows } = this.state;
213
214 if (!selectedRows) return;
215 switch (e.key) {
216 case 'remove':
217 dispatch({
218 type: 'reports/remove',
219 payload: {
220 key: selectedRows.map(row => row.key),
221 },
222 callback: () => {
223 this.setState({
224 selectedRows: [],
225 });
226 },
227 });
228 break;
229 default:
230 break;
231 }
232 };
233
234 handleSelectRows = (rows: TableListItem[]) => {
235 this.setState({
236 selectedRows: rows,
237 });
238 };
239
240 handleSearch = (e: React.FormEvent) => {
241 e.preventDefault();
242
243 const { dispatch, form } = this.props;
244
245 form.validateFields((err, fieldsValue) => {
246 if (err) return;
247
248 const values = {
249 ...fieldsValue,
250 updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(),
251 };
252
253 this.setState({
254 formValues: values,
255 });
256
257 dispatch({
258 type: 'reports/fetch',
259 payload: values,
260 });
261 });
262 };
263
264 handleModalVisible = (flag?: boolean) => {
265 this.setState({
266 modalVisible: !!flag,
267 });
268 };
269
270 handleUpdateModalVisible = (flag?: boolean, record?: FormValueType) => {
271 this.setState({
272 updateModalVisible: !!flag,
273 stepFormValues: record || {},
274 });
275 };
276
277 handleAdd = (fields: { desc: any }) => {
278 const { dispatch } = this.props;
279 dispatch({
280 type: 'reports/add',
281 payload: {
282 desc: fields.desc,
283 },
284 });
285
286 message.success('添加成功');
287 this.handleModalVisible();
288 };
289
290 handleUpdate = (fields: FormValueType) => {
291 const { dispatch } = this.props;
292 dispatch({
293 type: 'reports/update',
294 payload: {
295 name: fields.name,
296 desc: fields.desc,
297 key: fields.key,
298 },
299 });
300
301 message.success('配置成功');
302 this.handleUpdateModalVisible();
303 };
304
305 renderSimpleForm() {
306 const { form } = this.props;
307 const { getFieldDecorator } = form;
308 return (
309 <Form onSubmit={this.handleSearch} layout="inline">
310 <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
311 <Col md={8} sm={24}>
312 <FormItem label="规则名称">
313 {getFieldDecorator('name')(<Input placeholder="请输入" />)}
314 </FormItem>
315 </Col>
316 <Col md={8} sm={24}>
317 <FormItem label="使用状态">
318 {getFieldDecorator('status')(
319 <Select placeholder="请选择" style={{ width: '100%' }}>
320 <Option value="0">关闭</Option>
321 <Option value="1">运行中</Option>
322 </Select>,
323 )}
324 </FormItem>
325 </Col>
326 <Col md={8} sm={24}>
327 <span className={styles.submitButtons}>
328 <Button type="primary" htmlType="submit">
329 查询
330 </Button>
331 <Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
332 重置
333 </Button>
334 <a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
335 展开 <Icon type="down" />
336 </a>
337 </span>
338 </Col>
339 </Row>
340 </Form>
341 );
342 }
343
344 renderAdvancedForm() {
345 const {
346 form: { getFieldDecorator },
347 } = this.props;
348 return (
349 <Form onSubmit={this.handleSearch} layout="inline">
350 <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
351 <Col md={8} sm={24}>
352 <FormItem label="规则名称">
353 {getFieldDecorator('name')(<Input placeholder="请输入" />)}
354 </FormItem>
355 </Col>
356 <Col md={8} sm={24}>
357 <FormItem label="使用状态">
358 {getFieldDecorator('status')(
359 <Select placeholder="请选择" style={{ width: '100%' }}>
360 <Option value="0">关闭</Option>
361 <Option value="1">运行中</Option>
362 </Select>,
363 )}
364 </FormItem>
365 </Col>
366 <Col md={8} sm={24}>
367 <FormItem label="调用次数">
368 {getFieldDecorator('number')(<InputNumber style={{ width: '100%' }} />)}
369 </FormItem>
370 </Col>
371 </Row>
372 <Row gutter={{ md: 8, lg: 24, xl: 48 }}>
373 <Col md={8} sm={24}>
374 <FormItem label="更新日期">
375 {getFieldDecorator('date')(
376 <DatePicker style={{ width: '100%' }} placeholder="请输入更新日期" />,
377 )}
378 </FormItem>
379 </Col>
380 <Col md={8} sm={24}>
381 <FormItem label="使用状态">
382 {getFieldDecorator('status3')(
383 <Select placeholder="请选择" style={{ width: '100%' }}>
384 <Option value="0">关闭</Option>
385 <Option value="1">运行中</Option>
386 </Select>,
387 )}
388 </FormItem>
389 </Col>
390 <Col md={8} sm={24}>
391 <FormItem label="使用状态">
392 {getFieldDecorator('status4')(
393 <Select placeholder="请选择" style={{ width: '100%' }}>
394 <Option value="0">关闭</Option>
395 <Option value="1">运行中</Option>
396 </Select>,
397 )}
398 </FormItem>
399 </Col>
400 </Row>
401 <div style={{ overflow: 'hidden' }}>
402 <div style={{ float: 'right', marginBottom: 24 }}>
403 <Button type="primary" htmlType="submit">
404 查询
405 </Button>
406 <Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
407 重置
408 </Button>
409 <a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
410 收起 <Icon type="up" />
411 </a>
412 </div>
413 </div>
414 </Form>
415 );
416 }
417
418 renderForm() {
419 const { expandForm } = this.state;
420 return expandForm ? this.renderAdvancedForm() : this.renderSimpleForm();
421 }
422
423 render() {
424 const {
425 reports: { data },
426 loading,
427 } = this.props;
428
429 const { selectedRows, modalVisible, updateModalVisible, stepFormValues } = this.state;
430 const menu = (
431 <Menu onClick={this.handleMenuClick} selectedKeys={[]}>
432 <Menu.Item key="remove">删除</Menu.Item>
433 <Menu.Item key="approval">批量审批</Menu.Item>
434 </Menu>
435 );
436
437 const parentMethods = {
438 handleAdd: this.handleAdd,
439 handleModalVisible: this.handleModalVisible,
440 };
441 const updateMethods = {
442 handleUpdateModalVisible: this.handleUpdateModalVisible,
443 handleUpdate: this.handleUpdate,
444 };
445 return (
446 <PageHeaderWrapper>
447 <Card bordered={false}>
448 <div className={styles.tableList}>
449 <div className={styles.tableListForm}>{this.renderForm()}</div>
450 <div className={styles.tableListOperator}>
451 <Button icon="plus" type="primary" onClick={() => this.handleModalVisible(true)}>
452 新建
453 </Button>
454 {selectedRows.length > 0 && (
455 <span>
456 <Button>批量操作</Button>
457 <Dropdown overlay={menu}>
458 <Button>
459 更多操作 <Icon type="down" />
460 </Button>
461 </Dropdown>
462 </span>
463 )}
464 </div>
465 <StandardTable
466 selectedRows={selectedRows}
467 loading={loading}
468 data={data}
469 columns={this.columns}
470 onSelectRow={this.handleSelectRows}
471 onChange={this.handleStandardTableChange}
472 />
473 </div>
474 </Card>
475 <CreateForm {...parentMethods} modalVisible={modalVisible} />
476 {stepFormValues && Object.keys(stepFormValues).length ? (
477 <UpdateForm
478 {...updateMethods}
479 updateModalVisible={updateModalVisible}
480 values={stepFormValues}
481 />
482 ) : null}
483 </PageHeaderWrapper>
484 );
485 }
486 }
487
488 export default Form.create<TableListProps>()(TableList);
1 import { AnyAction, Reducer } from 'redux';
2 import { EffectsCommandMap } from 'dva';
3 import { addRule, queryRule, removeRule, updateRule } from './service';
4
5 import { TableListData } from './data.d';
6
7 export interface StateType {
8 data: TableListData;
9 }
10
11 export type Effect = (
12 action: AnyAction,
13 effects: EffectsCommandMap & { select: <T>(func: (state: StateType) => T) => T },
14 ) => void;
15
16 export interface ModelType {
17 namespace: string;
18 state: StateType;
19 effects: {
20 fetch: Effect;
21 add: Effect;
22 remove: Effect;
23 update: Effect;
24 };
25 reducers: {
26 save: Reducer<StateType>;
27 };
28 }
29
30 const Model: ModelType = {
31 namespace: 'reports',
32
33 state: {
34 data: {
35 list: [],
36 pagination: {},
37 },
38 },
39
40 effects: {
41 *fetch({ payload }, { call, put }) {
42 const response = yield call(queryRule, payload);
43 yield put({
44 type: 'save',
45 payload: response,
46 });
47 },
48 *add({ payload, callback }, { call, put }) {
49 const response = yield call(addRule, payload);
50 yield put({
51 type: 'save',
52 payload: response,
53 });
54 if (callback) callback();
55 },
56 *remove({ payload, callback }, { call, put }) {
57 const response = yield call(removeRule, payload);
58 yield put({
59 type: 'save',
60 payload: response,
61 });
62 if (callback) callback();
63 },
64 *update({ payload, callback }, { call, put }) {
65 const response = yield call(updateRule, payload);
66 yield put({
67 type: 'save',
68 payload: response,
69 });
70 if (callback) callback();
71 },
72 },
73
74 reducers: {
75 save(state, action) {
76 return {
77 ...state,
78 data: action.payload,
79 };
80 },
81 },
82 };
83
84 export default Model;
1 import request from '@/utils/request';
2 import { TableListParams } from './data.d';
3
4 export async function queryRule(params: TableListParams) {
5 return request('/api/rule', {
6 params,
7 });
8 }
9
10 export async function removeRule(params: TableListParams) {
11 return request('/api/rule', {
12 method: 'POST',
13 data: {
14 ...params,
15 method: 'delete',
16 },
17 });
18 }
19
20 export async function addRule(params: TableListParams) {
21 return request('/api/rule', {
22 method: 'POST',
23 data: {
24 ...params,
25 method: 'post',
26 },
27 });
28 }
29
30 export async function updateRule(params: TableListParams) {
31 return request('/api/rule', {
32 method: 'POST',
33 data: {
34 ...params,
35 method: 'update',
36 },
37 });
38 }
1 @import '~antd/es/style/themes/default.less';
2 @import '../../utils/utils.less';
3
4 .tableList {
5 .tableListOperator {
6 margin-bottom: 16px;
7 button {
8 margin-right: 8px;
9 }
10 }
11 }
12
13 .tableListForm {
14 :global {
15 .ant-form-item {
16 display: flex;
17 margin-right: 0;
18 margin-bottom: 24px;
19 > .ant-form-item-label {
20 width: auto;
21 padding-right: 8px;
22 line-height: 32px;
23 }
24 .ant-form-item-control {
25 line-height: 32px;
26 }
27 }
28 .ant-form-item-control-wrapper {
29 flex: 1;
30 }
31 }
32 .submitButtons {
33 display: block;
34 margin-bottom: 24px;
35 white-space: nowrap;
36 }
37 }
38
39 @media screen and (max-width: @screen-lg) {
40 .tableListForm :global(.ant-form-item) {
41 margin-right: 24px;
42 }
43 }
44
45 @media screen and (max-width: @screen-md) {
46 .tableListForm :global(.ant-form-item) {
47 margin-right: 8px;
48 }
49 }
1 import { createContext } from 'react';
2
3 export interface LoginContextProps {
4 tabUtil?: {
5 addTab: (id: string) => void;
6 removeTab: (id: string) => void;
7 };
8 updateActive?: (activeItem: { [key: string]: string } | string) => void;
9 }
10
11 const LoginContext: React.Context<LoginContextProps> = createContext({});
12
13 export default LoginContext;
1 import { Button, Col, Form, Input, Row } from 'antd';
2 import React, { Component } from 'react';
3 import { FormComponentProps } from 'antd/es/form';
4 import { GetFieldDecoratorOptions } from 'antd/es/form/Form';
5
6 import omit from 'omit.js';
7 import ItemMap from './map';
8 import LoginContext, { LoginContextProps } from './LoginContext';
9 import styles from './index.less';
10
11 type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
12
13 export type WrappedLoginItemProps = Omit<LoginItemProps, 'form' | 'type' | 'updateActive'>;
14 export type LoginItemKeyType = keyof typeof ItemMap;
15 export interface LoginItemType {
16 UserName: React.FC<WrappedLoginItemProps>;
17 Password: React.FC<WrappedLoginItemProps>;
18 Mobile: React.FC<WrappedLoginItemProps>;
19 Captcha: React.FC<WrappedLoginItemProps>;
20 }
21
22 export interface LoginItemProps extends GetFieldDecoratorOptions {
23 name?: string;
24 style?: React.CSSProperties;
25 onGetCaptcha?: (event?: MouseEvent) => void | Promise<boolean> | false;
26 placeholder?: string;
27 buttonText?: React.ReactNode;
28 onPressEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
29 countDown?: number;
30 getCaptchaButtonText?: string;
31 getCaptchaSecondText?: string;
32 updateActive?: LoginContextProps['updateActive'];
33 type?: string;
34 defaultValue?: string;
35 form?: FormComponentProps['form'];
36 customProps?: { [key: string]: unknown };
37 onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
38 tabUtil?: LoginContextProps['tabUtil'];
39 }
40
41 interface LoginItemState {
42 count: number;
43 }
44
45 const FormItem = Form.Item;
46
47 class WrapFormItem extends Component<LoginItemProps, LoginItemState> {
48 static defaultProps = {
49 getCaptchaButtonText: 'captcha',
50 getCaptchaSecondText: 'second',
51 };
52
53 interval: number | undefined = undefined;
54
55 constructor(props: LoginItemProps) {
56 super(props);
57 this.state = {
58 count: 0,
59 };
60 }
61
62 componentDidMount() {
63 const { updateActive, name = '' } = this.props;
64 if (updateActive) {
65 updateActive(name);
66 }
67 }
68
69 componentWillUnmount() {
70 clearInterval(this.interval);
71 }
72
73 onGetCaptcha = () => {
74 const { onGetCaptcha } = this.props;
75 const result = onGetCaptcha ? onGetCaptcha() : null;
76 if (result === false) {
77 return;
78 }
79 if (result instanceof Promise) {
80 result.then(this.runGetCaptchaCountDown);
81 } else {
82 this.runGetCaptchaCountDown();
83 }
84 };
85
86 getFormItemOptions = ({ onChange, defaultValue, customProps = {}, rules }: LoginItemProps) => {
87 const options: {
88 rules?: LoginItemProps['rules'];
89 onChange?: LoginItemProps['onChange'];
90 initialValue?: LoginItemProps['defaultValue'];
91 } = {
92 rules: rules || (customProps.rules as LoginItemProps['rules']),
93 };
94 if (onChange) {
95 options.onChange = onChange;
96 }
97 if (defaultValue) {
98 options.initialValue = defaultValue;
99 }
100 return options;
101 };
102
103 runGetCaptchaCountDown = () => {
104 const { countDown } = this.props;
105 let count = countDown || 59;
106 this.setState({ count });
107 this.interval = window.setInterval(() => {
108 count -= 1;
109 this.setState({ count });
110 if (count === 0) {
111 clearInterval(this.interval);
112 }
113 }, 1000);
114 };
115
116 render() {
117 const { count } = this.state;
118
119 // 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
120 const {
121 onChange,
122 customProps,
123 defaultValue,
124 rules,
125 name,
126 getCaptchaButtonText,
127 getCaptchaSecondText,
128 updateActive,
129 type,
130 form,
131 tabUtil,
132 ...restProps
133 } = this.props;
134 if (!name) {
135 return null;
136 }
137 if (!form) {
138 return null;
139 }
140 const { getFieldDecorator } = form;
141 // get getFieldDecorator props
142 const options = this.getFormItemOptions(this.props);
143 const otherProps = restProps || {};
144
145 if (type === 'Captcha') {
146 const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
147
148 return (
149 <FormItem>
150 <Row gutter={8}>
151 <Col span={16}>
152 {getFieldDecorator(name, options)(<Input {...customProps} {...inputProps} />)}
153 </Col>
154 <Col span={8}>
155 <Button
156 disabled={!!count}
157 className={styles.getCaptcha}
158 size="large"
159 onClick={this.onGetCaptcha}
160 >
161 {count ? `${count} ${getCaptchaSecondText}` : getCaptchaButtonText}
162 </Button>
163 </Col>
164 </Row>
165 </FormItem>
166 );
167 }
168 return (
169 <FormItem>
170 {getFieldDecorator(name, options)(<Input {...customProps} {...otherProps} />)}
171 </FormItem>
172 );
173 }
174 }
175
176 const LoginItem: Partial<LoginItemType> = {};
177
178 Object.keys(ItemMap).forEach(key => {
179 const item = ItemMap[key];
180 LoginItem[key] = (props: LoginItemProps) => (
181 <LoginContext.Consumer>
182 {context => (
183 <WrapFormItem
184 customProps={item.props}
185 rules={item.rules}
186 {...props}
187 type={key}
188 {...context}
189 updateActive={context.updateActive}
190 />
191 )}
192 </LoginContext.Consumer>
193 );
194 });
195
196 export default LoginItem as LoginItemType;
1 import { Button, Form } from 'antd';
2
3 import { ButtonProps } from 'antd/es/button';
4 import React from 'react';
5 import classNames from 'classnames';
6 import styles from './index.less';
7
8 const FormItem = Form.Item;
9
10 interface LoginSubmitProps extends ButtonProps {
11 className?: string;
12 }
13
14 const LoginSubmit: React.FC<LoginSubmitProps> = ({ className, ...rest }) => {
15 const clsString = classNames(styles.submit, className);
16 return (
17 <FormItem>
18 <Button size="large" className={clsString} type="primary" htmlType="submit" {...rest} />
19 </FormItem>
20 );
21 };
22
23 export default LoginSubmit;
1 import React, { Component } from 'react';
2
3 import { TabPaneProps } from 'antd/es/tabs';
4 import { Tabs } from 'antd';
5 import LoginContext, { LoginContextProps } from './LoginContext';
6
7 const { TabPane } = Tabs;
8
9 const generateId = (() => {
10 let i = 0;
11 return (prefix = '') => {
12 i += 1;
13 return `${prefix}${i}`;
14 };
15 })();
16
17 interface LoginTabProps extends TabPaneProps {
18 tabUtil: LoginContextProps['tabUtil'];
19 }
20
21 class LoginTab extends Component<LoginTabProps> {
22 uniqueId: string = '';
23
24 constructor(props: LoginTabProps) {
25 super(props);
26 this.uniqueId = generateId('login-tab-');
27 }
28
29 componentDidMount() {
30 const { tabUtil } = this.props;
31 if (tabUtil) {
32 tabUtil.addTab(this.uniqueId);
33 }
34 }
35
36 render() {
37 const { children } = this.props;
38 return <TabPane {...this.props}>{children}</TabPane>;
39 }
40 }
41
42 const WrapContext: React.FC<TabPaneProps> & {
43 typeName: string;
44 } = props => (
45 <LoginContext.Consumer>
46 {value => <LoginTab tabUtil={value.tabUtil} {...props} />}
47 </LoginContext.Consumer>
48 );
49
50 // 标志位 用来判断是不是自定义组件
51 WrapContext.typeName = 'LoginTab';
52
53 export default WrapContext;
1 @import '~antd/es/style/themes/default.less';
2
3 .login {
4 :global {
5 .ant-tabs .ant-tabs-bar {
6 margin-bottom: 24px;
7 text-align: center;
8 border-bottom: 0;
9 }
10
11 .ant-form-item {
12 margin: 0 2px 24px;
13 }
14 }
15
16 .getCaptcha {
17 display: block;
18 width: 100%;
19 }
20
21 .icon {
22 margin-left: 16px;
23 color: rgba(0, 0, 0, 0.2);
24 font-size: 24px;
25 vertical-align: middle;
26 cursor: pointer;
27 transition: color 0.3s;
28
29 &:hover {
30 color: @primary-color;
31 }
32 }
33
34 .other {
35 margin-top: 24px;
36 line-height: 22px;
37 text-align: left;
38
39 .register {
40 float: right;
41 }
42 }
43
44 .prefixIcon {
45 color: @disabled-color;
46 font-size: @font-size-base;
47 }
48
49 .submit {
50 width: 100%;
51 margin-top: 24px;
52 }
53 }
1 import { Form, Tabs } from 'antd';
2 import React, { Component } from 'react';
3 import { FormComponentProps } from 'antd/es/form';
4 import classNames from 'classnames';
5 import LoginContext, { LoginContextProps } from './LoginContext';
6 import LoginItem, { LoginItemProps, LoginItemType } from './LoginItem';
7
8 import LoginSubmit from './LoginSubmit';
9 import LoginTab from './LoginTab';
10 import styles from './index.less';
11 import { LoginParamsType } from '@/services/login';
12
13 export interface LoginProps {
14 defaultActiveKey?: string;
15 onTabChange?: (key: string) => void;
16 style?: React.CSSProperties;
17 onSubmit?: (error: unknown, values: LoginParamsType) => void;
18 className?: string;
19 form: FormComponentProps['form'];
20 onCreate?: (form?: FormComponentProps['form']) => void;
21 children: React.ReactElement<typeof LoginTab>[];
22 }
23
24 interface LoginState {
25 tabs?: string[];
26 type?: string;
27 active?: { [key: string]: unknown[] };
28 }
29
30 class Login extends Component<LoginProps, LoginState> {
31 public static Tab = LoginTab;
32
33 public static Submit = LoginSubmit;
34
35 public static UserName: React.FunctionComponent<LoginItemProps>;
36
37 public static Password: React.FunctionComponent<LoginItemProps>;
38
39 public static Mobile: React.FunctionComponent<LoginItemProps>;
40
41 public static Captcha: React.FunctionComponent<LoginItemProps>;
42
43 static defaultProps = {
44 className: '',
45 defaultActiveKey: '',
46 onTabChange: () => {},
47 onSubmit: () => {},
48 };
49
50 constructor(props: LoginProps) {
51 super(props);
52 this.state = {
53 type: props.defaultActiveKey,
54 tabs: [],
55 active: {},
56 };
57 }
58
59 componentDidMount() {
60 const { form, onCreate } = this.props;
61 if (onCreate) {
62 onCreate(form);
63 }
64 }
65
66 onSwitch = (type: string) => {
67 this.setState(
68 {
69 type,
70 },
71 () => {
72 const { onTabChange } = this.props;
73 if (onTabChange) {
74 onTabChange(type);
75 }
76 },
77 );
78 };
79
80 getContext: () => LoginContextProps = () => {
81 const { form } = this.props;
82 const { tabs = [] } = this.state;
83 return {
84 tabUtil: {
85 addTab: id => {
86 this.setState({
87 tabs: [...tabs, id],
88 });
89 },
90 removeTab: id => {
91 this.setState({
92 tabs: tabs.filter(currentId => currentId !== id),
93 });
94 },
95 },
96 form: { ...form },
97 updateActive: activeItem => {
98 const { type = '', active = {} } = this.state;
99 if (active[type]) {
100 active[type].push(activeItem);
101 } else {
102 active[type] = [activeItem];
103 }
104 this.setState({
105 active,
106 });
107 },
108 };
109 };
110
111 handleSubmit = (e: React.FormEvent) => {
112 e.preventDefault();
113 const { active = {}, type = '' } = this.state;
114 const { form, onSubmit } = this.props;
115 const activeFields = active[type] || [];
116 if (form) {
117 form.validateFields(activeFields as string[], { force: true }, (err, values) => {
118 if (onSubmit) {
119 onSubmit(err, values);
120 }
121 });
122 }
123 };
124
125 render() {
126 const { className, children } = this.props;
127 const { type, tabs = [] } = this.state;
128 const TabChildren: React.ReactComponentElement<typeof LoginTab>[] = [];
129 const otherChildren: React.ReactElement<unknown>[] = [];
130 React.Children.forEach(
131 children,
132 (child: React.ReactComponentElement<typeof LoginTab> | React.ReactElement<unknown>) => {
133 if (!child) {
134 return;
135 }
136 if ((child.type as { typeName: string }).typeName === 'LoginTab') {
137 TabChildren.push(child as React.ReactComponentElement<typeof LoginTab>);
138 } else {
139 otherChildren.push(child);
140 }
141 },
142 );
143 return (
144 <LoginContext.Provider value={this.getContext()}>
145 <div className={classNames(className, styles.login)}>
146 <Form onSubmit={this.handleSubmit}>
147 {tabs.length ? (
148 <React.Fragment>
149 <Tabs
150 animated={false}
151 className={styles.tabs}
152 activeKey={type}
153 onChange={this.onSwitch}
154 >
155 {TabChildren}
156 </Tabs>
157 {otherChildren}
158 </React.Fragment>
159 ) : (
160 children
161 )}
162 </Form>
163 </div>
164 </LoginContext.Provider>
165 );
166 }
167 }
168
169 (Object.keys(LoginItem) as (keyof LoginItemType)[]).forEach(item => {
170 Login[item] = LoginItem[item];
171 });
172
173 export default Form.create<LoginProps>()(Login);
1 import { Icon } from 'antd';
2 import React from 'react';
3 import styles from './index.less';
4
5 export default {
6 UserName: {
7 props: {
8 size: 'large',
9 id: 'userName',
10 prefix: <Icon type="user" className={styles.prefixIcon} />,
11 placeholder: 'admin',
12 },
13 rules: [
14 {
15 required: true,
16 message: 'Please enter username!',
17 },
18 ],
19 },
20 Password: {
21 props: {
22 size: 'large',
23 prefix: <Icon type="lock" className={styles.prefixIcon} />,
24 type: 'password',
25 id: 'password',
26 placeholder: '888888',
27 },
28 rules: [
29 {
30 required: true,
31 message: 'Please enter password!',
32 },
33 ],
34 },
35 Mobile: {
36 props: {
37 size: 'large',
38 prefix: <Icon type="mobile" className={styles.prefixIcon} />,
39 placeholder: 'mobile number',
40 },
41 rules: [
42 {
43 required: true,
44 message: 'Please enter mobile number!',
45 },
46 {
47 pattern: /^1\d{10}$/,
48 message: 'Wrong mobile number format!',
49 },
50 ],
51 },
52 Captcha: {
53 props: {
54 size: 'large',
55 prefix: <Icon type="mail" className={styles.prefixIcon} />,
56 placeholder: 'captcha',
57 },
58 rules: [
59 {
60 required: true,
61 message: 'Please enter Captcha!',
62 },
63 ],
64 },
65 };
1 import { Alert, Checkbox, Icon } from 'antd';
2 import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
3 import React, { Component } from 'react';
4
5 import { CheckboxChangeEvent } from 'antd/es/checkbox';
6 import { Dispatch, AnyAction } from 'redux';
7 import { FormComponentProps } from 'antd/es/form';
8 import Link from 'umi/link';
9 import { connect } from 'dva';
10 import { StateType } from '@/models/login';
11 import LoginComponents from './components/Login';
12 import styles from './style.less';
13 import { LoginParamsType } from '@/services/login';
14 import { ConnectState } from '@/models/connect';
15
16 const { Tab, UserName, Password, Mobile, Captcha, Submit } = LoginComponents;
17
18 interface LoginProps {
19 dispatch: Dispatch<AnyAction>;
20 userLogin: StateType;
21 submitting: boolean;
22 }
23 interface LoginState {
24 type: string;
25 autoLogin: boolean;
26 }
27
28 @connect(({ login, loading }: ConnectState) => ({
29 userLogin: login,
30 submitting: loading.effects['login/login'],
31 }))
32 class Login extends Component<LoginProps, LoginState> {
33 loginForm: FormComponentProps['form'] | undefined | null = undefined;
34
35 state: LoginState = {
36 type: 'account',
37 autoLogin: true,
38 };
39
40 changeAutoLogin = (e: CheckboxChangeEvent) => {
41 this.setState({
42 autoLogin: e.target.checked,
43 });
44 };
45
46 handleSubmit = (err: unknown, values: LoginParamsType) => {
47 const { type } = this.state;
48 if (!err) {
49 const { dispatch } = this.props;
50 dispatch({
51 type: 'login/login',
52 payload: {
53 ...values,
54 type,
55 },
56 });
57 }
58 };
59
60 onTabChange = (type: string) => {
61 this.setState({ type });
62 };
63
64 onGetCaptcha = () =>
65 new Promise<boolean>((resolve, reject) => {
66 if (!this.loginForm) {
67 return;
68 }
69 this.loginForm.validateFields(
70 ['mobile'],
71 {},
72 async (err: unknown, values: LoginParamsType) => {
73 if (err) {
74 reject(err);
75 } else {
76 const { dispatch } = this.props;
77 try {
78 const success = await ((dispatch({
79 type: 'login/getCaptcha',
80 payload: values.mobile,
81 }) as unknown) as Promise<unknown>);
82 resolve(!!success);
83 } catch (error) {
84 reject(error);
85 }
86 }
87 },
88 );
89 });
90
91 renderMessage = (content: string) => (
92 <Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon />
93 );
94
95 render() {
96 const { userLogin, submitting } = this.props;
97 const { status, type: loginType } = userLogin;
98 const { type, autoLogin } = this.state;
99 return (
100 <div className={styles.main}>
101 <LoginComponents
102 defaultActiveKey={type}
103 onTabChange={this.onTabChange}
104 onSubmit={this.handleSubmit}
105 onCreate={(form?: FormComponentProps['form']) => {
106 this.loginForm = form;
107 }}
108 >
109 <Tab key="account" tab={formatMessage({ id: 'user-login.login.tab-login-credentials' })}>
110 {status === 'error' &&
111 loginType === 'account' &&
112 !submitting &&
113 this.renderMessage(
114 formatMessage({ id: 'user-login.login.message-invalid-credentials' }),
115 )}
116 <UserName
117 name="userName"
118 placeholder={`${formatMessage({ id: 'user-login.login.userName' })}: admin or user`}
119 rules={[
120 {
121 required: true,
122 message: formatMessage({ id: 'user-login.userName.required' }),
123 },
124 ]}
125 />
126 <Password
127 name="password"
128 placeholder={`${formatMessage({ id: 'user-login.login.password' })}: ant.design`}
129 rules={[
130 {
131 required: true,
132 message: formatMessage({ id: 'user-login.password.required' }),
133 },
134 ]}
135 onPressEnter={e => {
136 e.preventDefault();
137 if (this.loginForm) {
138 this.loginForm.validateFields(this.handleSubmit);
139 }
140 }}
141 />
142 </Tab>
143 <Tab key="mobile" tab={formatMessage({ id: 'user-login.login.tab-login-mobile' })}>
144 {status === 'error' &&
145 loginType === 'mobile' &&
146 !submitting &&
147 this.renderMessage(
148 formatMessage({ id: 'user-login.login.message-invalid-verification-code' }),
149 )}
150 <Mobile
151 name="mobile"
152 placeholder={formatMessage({ id: 'user-login.phone-number.placeholder' })}
153 rules={[
154 {
155 required: true,
156 message: formatMessage({ id: 'user-login.phone-number.required' }),
157 },
158 {
159 pattern: /^1\d{10}$/,
160 message: formatMessage({ id: 'user-login.phone-number.wrong-format' }),
161 },
162 ]}
163 />
164 <Captcha
165 name="captcha"
166 placeholder={formatMessage({ id: 'user-login.verification-code.placeholder' })}
167 countDown={120}
168 onGetCaptcha={this.onGetCaptcha}
169 getCaptchaButtonText={formatMessage({ id: 'user-login.form.get-captcha' })}
170 getCaptchaSecondText={formatMessage({ id: 'user-login.captcha.second' })}
171 rules={[
172 {
173 required: true,
174 message: formatMessage({ id: 'user-login.verification-code.required' }),
175 },
176 ]}
177 />
178 </Tab>
179 <div>
180 <Checkbox checked={autoLogin} onChange={this.changeAutoLogin}>
181 <FormattedMessage id="user-login.login.remember-me" />
182 </Checkbox>
183 <a style={{ float: 'right' }} href="">
184 <FormattedMessage id="user-login.login.forgot-password" />
185 </a>
186 </div>
187 <Submit loading={submitting}>
188 <FormattedMessage id="user-login.login.login" />
189 </Submit>
190 <div className={styles.other}>
191 <FormattedMessage id="user-login.login.sign-in-with" />
192 <Icon type="alipay-circle" className={styles.icon} theme="outlined" />
193 <Icon type="taobao-circle" className={styles.icon} theme="outlined" />
194 <Icon type="weibo-circle" className={styles.icon} theme="outlined" />
195 <Link className={styles.register} to="/user/register">
196 <FormattedMessage id="user-login.login.signup" />
197 </Link>
198 </div>
199 </LoginComponents>
200 </div>
201 );
202 }
203 }
204
205 export default Login;
1 export default {
2 'user-login.login.userName': 'userName',
3 'user-login.login.password': 'password',
4 'user-login.login.message-invalid-credentials':
5 'Invalid username or password(admin/ant.design)',
6 'user-login.login.message-invalid-verification-code': 'Invalid verification code',
7 'user-login.login.tab-login-credentials': 'Credentials',
8 'user-login.login.tab-login-mobile': 'Mobile number',
9 'user-login.login.remember-me': 'Remember me',
10 'user-login.login.forgot-password': 'Forgot your password?',
11 'user-login.login.sign-in-with': 'Sign in with',
12 'user-login.login.signup': 'Sign up',
13 'user-login.login.login': 'Login',
14 'user-login.register.register': 'Register',
15 'user-login.register.get-verification-code': 'Get code',
16 'user-login.register.sign-in': 'Already have an account?',
17 'user-login.register-result.msg': 'Account:registered at {email}',
18 'user-login.register-result.activation-email':
19 'The activation email has been sent to your email address and is valid for 24 hours. Please log in to the email in time and click on the link in the email to activate the account.',
20 'user-login.register-result.back-home': 'Back to home',
21 'user-login.register-result.view-mailbox': 'View mailbox',
22 'user-login.email.required': 'Please enter your email!',
23 'user-login.email.wrong-format': 'The email address is in the wrong format!',
24 'user-login.userName.required': 'Please enter your userName!',
25 'user-login.password.required': 'Please enter your password!',
26 'user-login.password.twice': 'The passwords entered twice do not match!',
27 'user-login.strength.msg':
28 "Please enter at least 6 characters and don't use passwords that are easy to guess.",
29 'user-login.strength.strong': 'Strength: strong',
30 'user-login.strength.medium': 'Strength: medium',
31 'user-login.strength.short': 'Strength: too short',
32 'user-login.confirm-password.required': 'Please confirm your password!',
33 'user-login.phone-number.required': 'Please enter your phone number!',
34 'user-login.phone-number.wrong-format': 'Malformed phone number!',
35 'user-login.verification-code.required': 'Please enter the verification code!',
36 'user-login.title.required': 'Please enter a title',
37 'user-login.date.required': 'Please select the start and end date',
38 'user-login.goal.required': 'Please enter a description of the goal',
39 'user-login.standard.required': 'Please enter a metric',
40 'user-login.form.get-captcha': 'Get Captcha',
41 'user-login.captcha.second': 'sec',
42 'user-login.form.optional': ' (optional) ',
43 'user-login.form.submit': 'Submit',
44 'user-login.form.save': 'Save',
45 'user-login.email.placeholder': 'Email',
46 'user-login.password.placeholder': 'Password',
47 'user-login.confirm-password.placeholder': 'Confirm password',
48 'user-login.phone-number.placeholder': 'Phone number',
49 'user-login.verification-code.placeholder': 'Verification code',
50 'user-login.title.label': 'Title',
51 'user-login.title.placeholder': 'Give the target a name',
52 'user-login.date.label': 'Start and end date',
53 'user-login.placeholder.start': 'Start date',
54 'user-login.placeholder.end': 'End date',
55 'user-login.goal.label': 'Goal description',
56 'user-login.goal.placeholder': 'Please enter your work goals',
57 'user-login.standard.label': 'Metrics',
58 'user-login.standard.placeholder': 'Please enter a metric',
59 'user-login.client.label': 'Client',
60 'user-login.label.tooltip': 'Target service object',
61 'user-login.client.placeholder':
62 'Please describe your customer service, internal customers directly @ Name / job number',
63 'user-login.invites.label': 'Inviting critics',
64 'user-login.invites.placeholder':
65 'Please direct @ Name / job number, you can invite up to 5 people',
66 'user-login.weight.label': 'Weight',
67 'user-login.weight.placeholder': 'Please enter weight',
68 'user-login.public.label': 'Target disclosure',
69 'user-login.label.help': 'Customers and invitees are shared by default',
70 'user-login.radio.public': 'Public',
71 'user-login.radio.partially-public': 'Partially public',
72 'user-login.radio.private': 'Private',
73 'user-login.publicUsers.placeholder': 'Open to',
74 'user-login.option.A': 'Colleague A',
75 'user-login.option.B': 'Colleague B',
76 'user-login.option.C': 'Colleague C',
77 'user-login.navBar.lang': 'Languages',
78 };
1 export default {
2 'user-login.login.userName': '用户名',
3 'user-login.login.password': '密码',
4 'user-login.login.message-invalid-credentials': '账户或密码错误(admin/ant.design)',
5 'user-login.login.message-invalid-verification-code': '验证码错误',
6 'user-login.login.tab-login-credentials': '账户密码登录',
7 'user-login.login.tab-login-mobile': '手机号登录',
8 'user-login.login.remember-me': '自动登录',
9 'user-login.login.forgot-password': '忘记密码',
10 'user-login.login.sign-in-with': '其他登录方式',
11 'user-login.login.signup': '注册账户',
12 'user-login.login.login': '登录',
13 'user-login.register.register': '注册',
14 'user-login.register.get-verification-code': '获取验证码',
15 'user-login.register.sign-in': '使用已有账户登录',
16 'user-login.register-result.msg': '你的账户:{email} 注册成功',
17 'user-login.register-result.activation-email':
18 '激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。',
19 'user-login.register-result.back-home': '返回首页',
20 'user-login.register-result.view-mailbox': '查看邮箱',
21 'user-login.email.required': '请输入邮箱地址!',
22 'user-login.email.wrong-format': '邮箱地址格式错误!',
23 'user-login.userName.required': '请输入用户名!',
24 'user-login.password.required': '请输入密码!',
25 'user-login.password.twice': '两次输入的密码不匹配!',
26 'user-login.strength.msg': '请至少输入 6 个字符。请不要使用容易被猜到的密码。',
27 'user-login.strength.strong': '强度:强',
28 'user-login.strength.medium': '强度:中',
29 'user-login.strength.short': '强度:太短',
30 'user-login.confirm-password.required': '请确认密码!',
31 'user-login.phone-number.required': '请输入手机号!',
32 'user-login.phone-number.wrong-format': '手机号格式错误!',
33 'user-login.verification-code.required': '请输入验证码!',
34 'user-login.title.required': '请输入标题',
35 'user-login.date.required': '请选择起止日期',
36 'user-login.goal.required': '请输入目标描述',
37 'user-login.standard.required': '请输入衡量标准',
38 'user-login.form.get-captcha': '获取验证码',
39 'user-login.captcha.second': '秒',
40 'user-login.form.optional': '(选填)',
41 'user-login.form.submit': '提交',
42 'user-login.form.save': '保存',
43 'user-login.email.placeholder': '邮箱',
44 'user-login.password.placeholder': '至少6位密码,区分大小写',
45 'user-login.confirm-password.placeholder': '确认密码',
46 'user-login.phone-number.placeholder': '手机号',
47 'user-login.verification-code.placeholder': '验证码',
48 'user-login.title.label': '标题',
49 'user-login.title.placeholder': '给目标起个名字',
50 'user-login.date.label': '起止日期',
51 'user-login.placeholder.start': '开始日期',
52 'user-login.placeholder.end': '结束日期',
53 'user-login.goal.label': '目标描述',
54 'user-login.goal.placeholder': '请输入你的阶段性工作目标',
55 'user-login.standard.label': '衡量标准',
56 'user-login.standard.placeholder': '请输入衡量标准',
57 'user-login.client.label': '客户',
58 'user-login.label.tooltip': '目标的服务对象',
59 'user-login.client.placeholder': '请描述你服务的客户,内部客户直接 @姓名/工号',
60 'user-login.invites.label': '邀评人',
61 'user-login.invites.placeholder': '请直接 @姓名/工号,最多可邀请 5 人',
62 'user-login.weight.label': '权重',
63 'user-login.weight.placeholder': '请输入',
64 'user-login.public.label': '目标公开',
65 'user-login.label.help': '客户、邀评人默认被分享',
66 'user-login.radio.public': '公开',
67 'user-login.radio.partially-public': '部分公开',
68 'user-login.radio.private': '不公开',
69 'user-login.publicUsers.placeholder': '公开给',
70 'user-login.option.A': '同事甲',
71 'user-login.option.B': '同事乙',
72 'user-login.option.C': '同事丙',
73 'user-login.navBar.lang': '语言',
74 };
1 export default {
2 'user-login.login.userName': '賬戶',
3 'user-login.login.password': '密碼',
4 'user-login.login.message-invalid-credentials': '賬戶或密碼錯誤(admin/ant.design)',
5 'user-login.login.message-invalid-verification-code': '驗證碼錯誤',
6 'user-login.login.tab-login-credentials': '賬戶密碼登錄',
7 'user-login.login.tab-login-mobile': '手機號登錄',
8 'user-login.login.remember-me': '自動登錄',
9 'user-login.login.forgot-password': '忘記密碼',
10 'user-login.login.sign-in-with': '其他登錄方式',
11 'user-login.login.signup': '註冊賬戶',
12 'user-login.login.login': '登錄',
13 'user-login.register.register': '註冊',
14 'user-login.register.get-verification-code': '獲取驗證碼',
15 'user-login.register.sign-in': '使用已有賬戶登錄',
16 'user-login.register-result.msg': '妳的賬戶:{email} 註冊成功',
17 'user-login.register-result.activation-email':
18 '激活郵件已發送到妳的郵箱中,郵件有效期為24小時。請及時登錄郵箱,點擊郵件中的鏈接激活帳戶。',
19 'user-login.register-result.back-home': '返回首頁',
20 'user-login.register-result.view-mailbox': '查看郵箱',
21 'user-login.email.required': '請輸入郵箱地址!',
22 'user-login.email.wrong-format': '郵箱地址格式錯誤!',
23 'user-login.userName.required': '請輸入賬戶!',
24 'user-login.password.required': '請輸入密碼!',
25 'user-login.password.twice': '兩次輸入的密碼不匹配!',
26 'user-login.strength.msg': '請至少輸入 6 個字符。請不要使用容易被猜到的密碼。',
27 'user-login.strength.strong': '強度:強',
28 'user-login.strength.medium': '強度:中',
29 'user-login.strength.short': '強度:太短',
30 'user-login.confirm-password.required': '請確認密碼!',
31 'user-login.phone-number.required': '請輸入手機號!',
32 'user-login.phone-number.wrong-format': '手機號格式錯誤!',
33 'user-login.verification-code.required': '請輸入驗證碼!',
34 'user-login.title.required': '請輸入標題',
35 'user-login.date.required': '請選擇起止日期',
36 'user-login.goal.required': '請輸入目標描述',
37 'user-login.standard.required': '請輸入衡量標淮',
38 'user-login.form.get-captcha': '獲取驗證碼',
39 'user-login.captcha.second': '秒',
40 'user-login.form.optional': '(選填)',
41 'user-login.form.submit': '提交',
42 'user-login.form.save': '保存',
43 'user-login.email.placeholder': '郵箱',
44 'user-login.password.placeholder': '至少6位密碼,區分大小寫',
45 'user-login.confirm-password.placeholder': '確認密碼',
46 'user-login.phone-number.placeholder': '手機號',
47 'user-login.verification-code.placeholder': '驗證碼',
48 'user-login.title.label': '標題',
49 'user-login.title.placeholder': '給目標起個名字',
50 'user-login.date.label': '起止日期',
51 'user-login.placeholder.start': '開始日期',
52 'user-login.placeholder.end': '結束日期',
53 'user-login.goal.label': '目標描述',
54 'user-login.goal.placeholder': '請輸入妳的階段性工作目標',
55 'user-login.standard.label': '衡量標淮',
56 'user-login.standard.placeholder': '請輸入衡量標淮',
57 'user-login.client.label': '客戶',
58 'user-login.label.tooltip': '目標的服務對象',
59 'user-login.client.placeholder': '請描述妳服務的客戶,內部客戶直接 @姓名/工號',
60 'user-login.invites.label': '邀評人',
61 'user-login.invites.placeholder': '請直接 @姓名/工號,最多可邀請 5 人',
62 'user-login.weight.label': '權重',
63 'user-login.weight.placeholder': '請輸入',
64 'user-login.public.label': '目標公開',
65 'user-login.label.help': '客戶、邀評人默認被分享',
66 'user-login.radio.public': '公開',
67 'user-login.radio.partially-public': '部分公開',
68 'user-login.radio.private': '不公開',
69 'user-login.publicUsers.placeholder': '公開給',
70 'user-login.option.A': '同事甲',
71 'user-login.option.B': '同事乙',
72 'user-login.option.C': '同事丙',
73 'user-login.navBar.lang': '語言',
74 };
1 @import '~antd/es/style/themes/default.less';
2
3 .main {
4 width: 368px;
5 margin: 0 auto;
6 @media screen and (max-width: @screen-sm) {
7 width: 95%;
8 }
9
10 .icon {
11 margin-left: 16px;
12 color: rgba(0, 0, 0, 0.2);
13 font-size: 24px;
14 vertical-align: middle;
15 cursor: pointer;
16 transition: color 0.3s;
17
18 &:hover {
19 color: @primary-color;
20 }
21 }
22
23 .other {
24 margin-top: 24px;
25 line-height: 22px;
26 text-align: left;
27
28 .register {
29 float: right;
30 }
31 }
32
33 :global {
34 .antd-pro-login-submit {
35 width: 100%;
36 margin-top: 24px;
37 }
38 }
39 }
1 /* eslint-disable eslint-comments/disable-enable-pair */
2 /* eslint-disable no-restricted-globals */
3 /* eslint-disable no-underscore-dangle */
4 /* globals workbox */
5 workbox.core.setCacheNameDetails({
6 prefix: 'antd-pro',
7 suffix: 'v1',
8 });
9 // Control all opened tabs ASAP
10 workbox.clientsClaim();
11
12 /**
13 * Use precaching list generated by workbox in build process.
14 * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
15 */
16 workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
17
18 /**
19 * Register a navigation route.
20 * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
21 */
22 workbox.routing.registerNavigationRoute('/index.html');
23
24 /**
25 * Use runtime cache:
26 * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
27 *
28 * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
29 * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
30 */
31
32 /**
33 * Handle API requests
34 */
35 workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
36
37 /**
38 * Handle third party requests
39 */
40 workbox.routing.registerRoute(
41 /^https:\/\/gw.alipayobjects.com\//,
42 workbox.strategies.networkFirst(),
43 );
44 workbox.routing.registerRoute(
45 /^https:\/\/cdnjs.cloudflare.com\//,
46 workbox.strategies.networkFirst(),
47 );
48 workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
49
50 /**
51 * Response to client after skipping waiting with MessageChannel
52 */
53 addEventListener('message', event => {
54 const replyPort = event.ports[0];
55 const message = event.data;
56 if (replyPort && message && message.type === 'skip-waiting') {
57 event.waitUntil(
58 self.skipWaiting().then(
59 () => replyPort.postMessage({ error: null }),
60 error => replyPort.postMessage({ error }),
61 ),
62 );
63 }
64 });
1 import request from '@/utils/request';
2
3 export interface LoginParamsType {
4 userName: string;
5 password: string;
6 mobile: string;
7 captcha: string;
8 }
9
10 export async function fakeAccountLogin(params: LoginParamsType) {
11 return request('/api/login/account', {
12 method: 'POST',
13 data: params,
14 });
15 }
16
17 export async function getFakeCaptcha(mobile: string) {
18 return request(`/api/login/captcha?mobile=${mobile}`);
19 }
1 import request from '@/utils/request';
2
3 export async function query(): Promise<any> {
4 return request('/api/users');
5 }
6
7 export async function queryCurrent(): Promise<any> {
8 return request('/api/currentUser');
9 }
10
11 export async function queryNotices(): Promise<any> {
12 return request('/api/notices');
13 }
1 declare module 'slash2';
2 declare module 'antd-theme-webpack-plugin';
3
4 declare module '*.css';
5 declare module '*.less';
6 declare module '*.scss';
7 declare module '*.sass';
8 declare module '*.svg';
9 declare module '*.png';
10 declare module '*.jpg';
11 declare module '*.jpeg';
12 declare module '*.gif';
13 declare module '*.bmp';
14 declare module '*.tiff';
15 declare module 'omit.js';
16 declare module 'react-copy-to-clipboard';
17 declare module 'react-fittext';
18 declare module '@antv/data-set';
19 declare module 'nzh/cn';
20 declare module 'webpack-theme-color-replacer';
21 declare module 'webpack-theme-color-replacer/client';
22
23 // google analytics interface
24 interface GAFieldsObject {
25 eventCategory: string;
26 eventAction: string;
27 eventLabel?: string;
28 eventValue?: number;
29 nonInteraction?: boolean;
30 }
31 interface Window {
32 ga: (
33 command: 'send',
34 hitType: 'event' | 'pageview',
35 fieldsObject: GAFieldsObject | string,
36 ) => void;
37 }
38
39 declare let ga: Function;
40
41 // preview.pro.ant.design only do not use in your production ;
42 // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
43 declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
1 import RenderAuthorize from '@/components/Authorized';
2 import { getAuthority } from './authority';
3 /* eslint-disable eslint-comments/disable-enable-pair */
4 /* eslint-disable import/no-mutable-exports */
5 let Authorized = RenderAuthorize(getAuthority());
6
7 // Reload the rights component
8 const reloadAuthorized = (): void => {
9 Authorized = RenderAuthorize(getAuthority());
10 };
11
12 export { reloadAuthorized };
13 export default Authorized;
1 import { getAuthority } from './authority';
2
3 describe('getAuthority should be strong', () => {
4 it('string', () => {
5 expect(getAuthority('admin')).toEqual(['admin']);
6 });
7 it('array with double quotes', () => {
8 expect(getAuthority('"admin"')).toEqual(['admin']);
9 });
10 it('array with single item', () => {
11 expect(getAuthority('["admin"]')).toEqual(['admin']);
12 });
13 it('array with multiple items', () => {
14 expect(getAuthority('["admin", "guest"]')).toEqual(['admin', 'guest']);
15 });
16 });
1 import { reloadAuthorized } from './Authorized';
2
3 // use localStorage to store the authority info, which might be sent from server in actual project.
4 export function getAuthority(str?: string): string | string[] {
5 const authorityString =
6 typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str;
7 // authorityString could be admin, "admin", ["admin"]
8 let authority;
9 try {
10 if (authorityString) {
11 authority = JSON.parse(authorityString);
12 }
13 } catch (e) {
14 authority = authorityString;
15 }
16 if (typeof authority === 'string') {
17 return [authority];
18 }
19 // preview.pro.ant.design only do not use in your production.
20 // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
21 if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
22 return ['admin'];
23 }
24 return authority;
25 }
26
27 export function setAuthority(authority: string | string[]): void {
28 const proAuthority = typeof authority === 'string' ? [authority] : authority;
29 localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
30 // auto reload
31 reloadAuthorized();
32 }
1 /**
2 * request 网络请求工具
3 * 更详细的 api 文档: https://github.com/umijs/umi-request
4 */
5 import { extend } from 'umi-request';
6 import { notification } from 'antd';
7
8 const codeMessage = {
9 200: '服务器成功返回请求的数据。',
10 201: '新建或修改数据成功。',
11 202: '一个请求已经进入后台排队(异步任务)。',
12 204: '删除数据成功。',
13 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
14 401: '用户没有权限(令牌、用户名、密码错误)。',
15 403: '用户得到授权,但是访问是被禁止的。',
16 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
17 406: '请求的格式不可得。',
18 410: '请求的资源被永久删除,且不会再得到的。',
19 422: '当创建一个对象时,发生一个验证错误。',
20 500: '服务器发生错误,请检查服务器。',
21 502: '网关错误。',
22 503: '服务不可用,服务器暂时过载或维护。',
23 504: '网关超时。',
24 };
25
26 /**
27 * 异常处理程序
28 */
29 const errorHandler = (error: { response: Response }): Response => {
30 const { response } = error;
31 if (response && response.status) {
32 const errorText = codeMessage[response.status] || response.statusText;
33 const { status, url } = response;
34
35 notification.error({
36 message: `请求错误 ${status}: ${url}`,
37 description: errorText,
38 });
39 } else if (!response) {
40 notification.error({
41 description: '您的网络发生异常,无法连接服务器',
42 message: '网络异常',
43 });
44 }
45 return response;
46 };
47
48 /**
49 * 配置request请求时的默认参数
50 */
51 const request = extend({
52 errorHandler, // 默认错误处理
53 credentials: 'include', // 默认请求是否带上cookie
54 });
55
56 export default request;
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 display: table;
42 content: ' ';
43 }
44 &::after {
45 clear: both;
46 height: 0;
47 font-size: 0;
48 visibility: hidden;
49 }
50 }
1 import { isUrl } from './utils';
2
3 describe('isUrl tests', (): void => {
4 it('should return false for invalid and corner case inputs', (): void => {
5 expect(isUrl([] as any)).toBeFalsy();
6 expect(isUrl({} as any)).toBeFalsy();
7 expect(isUrl(false as any)).toBeFalsy();
8 expect(isUrl(true as any)).toBeFalsy();
9 expect(isUrl(NaN as any)).toBeFalsy();
10 expect(isUrl(null as any)).toBeFalsy();
11 expect(isUrl(undefined as any)).toBeFalsy();
12 expect(isUrl('')).toBeFalsy();
13 });
14
15 it('should return false for invalid URLs', (): void => {
16 expect(isUrl('foo')).toBeFalsy();
17 expect(isUrl('bar')).toBeFalsy();
18 expect(isUrl('bar/test')).toBeFalsy();
19 expect(isUrl('http:/example.com/')).toBeFalsy();
20 expect(isUrl('ttp://example.com/')).toBeFalsy();
21 });
22
23 it('should return true for valid URLs', (): void => {
24 expect(isUrl('http://example.com/')).toBeTruthy();
25 expect(isUrl('https://example.com/')).toBeTruthy();
26 expect(isUrl('http://example.com/test/123')).toBeTruthy();
27 expect(isUrl('https://example.com/test/123')).toBeTruthy();
28 expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
29 expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
30 expect(isUrl('http://www.example.com/')).toBeTruthy();
31 expect(isUrl('https://www.example.com/')).toBeTruthy();
32 expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
33 expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
34 expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
35 expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
36 });
37 });
1 import { parse } from 'querystring';
2 import pathRegexp from 'path-to-regexp';
3
4 /* eslint no-useless-escape:0 import/prefer-default-export:0 */
5 const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
6
7 export const isUrl = (path: string): boolean => reg.test(path);
8
9 export const isAntDesignPro = (): boolean => {
10 if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
11 return true;
12 }
13 return window.location.hostname === 'preview.pro.ant.design';
14 };
15
16 // 给官方演示站点用,用于关闭真实开发环境不需要使用的特性
17 export const isAntDesignProOrDev = (): boolean => {
18 const { NODE_ENV } = process.env;
19 if (NODE_ENV === 'development') {
20 return true;
21 }
22 return isAntDesignPro();
23 };
24
25 export const getPageQuery = () => parse(window.location.href.split('?')[1]);
26
27 /**
28 * props.route.routes
29 * @param router [{}]
30 * @param pathname string
31 */
32 export const getAuthorityFromRouter = <T extends { path: string }>(
33 router: T[] = [],
34 pathname: string,
35 ): T | undefined => {
36 const authority = router.find(({ path }) => path && pathRegexp(path).exec(pathname));
37 if (authority) return authority;
38 return undefined;
39 };
1 /* eslint-disable eslint-comments/disable-enable-pair */
2 /* eslint-disable @typescript-eslint/no-var-requires */
3 /* eslint-disable eslint-comments/no-unlimited-disable */
4 const { spawn } = require('child_process');
5 const { kill } = require('cross-port-killer');
6
7 const env = Object.create(process.env);
8 env.BROWSER = 'none';
9 env.TEST = true;
10 // flag to prevent multiple test
11 let once = false;
12
13 const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
14 env,
15 });
16
17 startServer.stderr.on('data', data => {
18 // eslint-disable-next-line
19 console.log(data.toString());
20 });
21
22 startServer.on('exit', () => {
23 kill(process.env.PORT || 8000);
24 });
25
26 console.log('Starting development server for e2e tests...');
27 startServer.stdout.on('data', data => {
28 console.log(data.toString());
29 // hack code , wait umi
30 if (
31 (!once && data.toString().indexOf('Compiled successfully') >= 0) ||
32 data.toString().indexOf('Theme generated successfully') >= 0
33 ) {
34 // eslint-disable-next-line
35 once = true;
36 console.log('Development server is started, ready to run tests.');
37 const testCmd = spawn(
38 /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
39 ['test', '--', '--maxWorkers=1', '--runInBand'],
40 {
41 stdio: 'inherit',
42 },
43 );
44 testCmd.on('exit', code => {
45 startServer.kill();
46 process.exit(code);
47 });
48 }
49 });
1 {
2 "compilerOptions": {
3 "outDir": "build/dist",
4 "module": "esnext",
5 "target": "esnext",
6 "lib": ["esnext", "dom"],
7 "sourceMap": true,
8 "baseUrl": ".",
9 "jsx": "react",
10 "allowSyntheticDefaultImports": true,
11 "moduleResolution": "node",
12 "forceConsistentCasingInFileNames": true,
13 "noImplicitReturns": true,
14 "suppressImplicitAnyIndexErrors": true,
15 "noUnusedLocals": true,
16 "allowJs": true,
17 "skipLibCheck": true,
18 "experimentalDecorators": true,
19 "strict": true,
20 "paths": {
21 "@/*": ["./src/*"]
22 }
23 },
24 "exclude": [
25 "node_modules",
26 "build",
27 "dist",
28 "scripts",
29 "acceptance-tests",
30 "webpack",
31 "jest",
32 "src/setupTests.ts",
33 "tslint:latest",
34 "tslint-config-prettier"
35 ]
36 }