├── .editorconfig
├── .gitignore
├── README.md
├── build.sh
├── config
├── config.ts
├── default-settings.ts
├── proxy.ts
└── routes.ts
├── docker
└── nginx.conf
├── jsconfig.json
├── mock
├── dashboard.ts
├── permission.ts
├── user.ts
└── utils.ts
├── now.json
├── package.json
├── renovate.json
├── src
├── app.tsx
├── assets
│ └── logo.svg
├── authority.ts
├── common
│ ├── constant
│ │ └── index.ts
│ └── types
│ │ ├── api
│ │ ├── base.d.ts
│ │ ├── permission.d.ts
│ │ └── user.d.ts
│ │ ├── global.d.ts
│ │ └── login.ts
├── components
│ ├── footer
│ │ └── index.tsx
│ ├── header-dropdown
│ │ ├── index.less
│ │ └── index.tsx
│ ├── header-search
│ │ ├── index.less
│ │ └── index.tsx
│ ├── page-loading
│ │ └── index.tsx
│ └── right-content
│ │ ├── avatar-dropdown.tsx
│ │ ├── index.less
│ │ └── index.tsx
├── config
│ └── index.ts
├── global.less
├── layouts
│ ├── user-layout.less
│ └── user-layout.tsx
├── locales
│ ├── en-US.ts
│ ├── en-US
│ │ └── menu.ts
│ ├── zh-CN.ts
│ └── zh-CN
│ │ └── menu.ts
├── models
│ └── login.ts
├── pages
│ ├── 404.tsx
│ ├── dashboard
│ │ ├── components
│ │ │ ├── introduce-row
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ ├── proportion-sales
│ │ │ │ └── index.tsx
│ │ │ ├── sales-card
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ ├── top-search
│ │ │ │ └── index.tsx
│ │ │ └── trend
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── exception
│ │ ├── 403.tsx
│ │ ├── 404.tsx
│ │ └── 500.tsx
│ ├── libraries
│ │ ├── amap
│ │ │ ├── components
│ │ │ │ ├── cluster
│ │ │ │ │ ├── index.less
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── marker
│ │ │ │ │ ├── index.less
│ │ │ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ └── styles.less
│ │ └── watermark
│ │ │ └── index.tsx
│ ├── login
│ │ ├── components
│ │ │ ├── captcha.tsx
│ │ │ ├── context.tsx
│ │ │ ├── index.less
│ │ │ ├── index.tsx
│ │ │ ├── item.tsx
│ │ │ ├── map.tsx
│ │ │ ├── submit.tsx
│ │ │ └── tab.tsx
│ │ ├── index.tsx
│ │ └── style.less
│ ├── nested
│ │ ├── menu1
│ │ │ ├── menu1-1.tsx
│ │ │ ├── menu1-2
│ │ │ │ ├── menu1-2-1.tsx
│ │ │ │ └── menu1-2-2.tsx
│ │ │ └── menu1-3.tsx
│ │ └── menu2
│ │ │ └── index.tsx
│ ├── permission
│ │ ├── action
│ │ │ ├── components
│ │ │ │ └── operation.tsx
│ │ │ └── index.tsx
│ │ ├── button
│ │ │ └── index.tsx
│ │ ├── constant.ts
│ │ ├── page
│ │ │ └── index.tsx
│ │ └── policy
│ │ │ └── index.tsx
│ ├── register
│ │ ├── index.tsx
│ │ └── style.less
│ └── system
│ │ ├── role
│ │ └── index.tsx
│ │ └── user
│ │ └── index.tsx
├── services
│ ├── dashboard.ts
│ ├── login.ts
│ ├── permission.ts
│ └── user.ts
├── typings.d.ts
└── utils
│ ├── cookie.ts
│ ├── date.ts
│ └── index.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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
41 | *.less.d.ts
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Admin Template
8 |
9 |
10 | 开箱即用的中台前端/设计解决方案。
11 |
12 |
13 | ## ✨ 特性
14 |
15 | - 🛡 **TypeScript**: 应用程序级 JavaScript 的语言
16 | - 💎 **优雅美观**:基于 Ant Design 体系精心设计
17 | - 🚀 **最新技术栈**:使用 React/umi/antd 等前端前沿技术开发
18 | - 🌐 **国际化**:内建业界通用的国际化方案
19 | - 🔢 **Mock 数据**:实用的本地数据调试方案
20 | - ⚙️ **最佳实践**:良好的工程实践助您持续产出高质量代码
21 | - 🔐 **优秀的权限设计**:目前能找到的最好的权限实现方案
22 |
23 | ## 🎉 推荐
24 |
25 | - 微前端版本 [micro-frontends-template](https://github.com/pansyjs/micro-frontends-template) 正在同步开发中...
26 | - 好用的水印组件 [watermark](https://github.com/pansyjs/watermark)
27 |
28 |
29 | ## 📜 目录
30 |
31 | ```
32 | ├── config # umi 相关配置
33 | ├── docker # docker 相关配置
34 | ├── mock # 本地模拟数据
35 | ├── public # 静态资源
36 | ├── src # 源代码
37 | │ ├── assets # 本地静态资源
38 | │ ├── common # 类型定义、常量
39 | │ ├── components # 全局公用组件
40 | │ ├── config # 全局配置
41 | │ ├── layouts # 布局文件
42 | │ ├── locales # 国际化资源
43 | │ ├── models # 路由
44 | │ ├── pages # 业务页面入口和常用模板
45 | │ ├── services # 所有请求
46 | │ ├── utils # 全局公用方法
47 | │ ├── app.tsx # 运行时配置文件
48 | │ ├── authority.ts # 权限定义文件
49 | │ ├── global.less # 全局样式
50 | │ └── typings.d.ts # 补充类型定义
51 | ├── package.json # package.json
52 | └── tsconfig.json # tsconfig.json
53 | ```
54 |
55 | ## 🔐 关于权限
56 |
57 | 基于 [umi-plugin-authority](https://github.com/alitajs/umi-plugins/tree/master/packages/umi-plugin-authority) 提供权限功能,暴露 `useAuthority` hooks 和 `Authority` 组件实现权限控制的能力
58 |
59 | 使用示例如下
60 |
61 | ```tsx
62 | import React from 'react';
63 | import { useAuthority, Authority } from 'umi';
64 |
65 | const PageA = props => {
66 | const { foo } = props;
67 | const { combinationVerify } = useAuthority();
68 |
69 | // 使用 hooks 提供的能力
70 | if (combinationVerify('module1/action1')) {
71 | // 存在 module1/action1 权限,则...
72 | }
73 |
74 | return (
75 |
76 | {/** 指定需要验证的权限 */}
77 |
Can not read foo content. }
80 | >
81 | Foo content.
82 |
83 | {/** 直接指定权限 */}
84 | Can not update foo.}
87 | >
88 | Update foo.
89 |
90 | {/** 复杂的校验 */}
91 | Can not update foo.}
94 | >
95 | Update foo.
96 |
97 | {/** children 为function */}
98 | Can not delete foo.}
101 | >
102 | {(isMatch) => 权限校验结果: {isMatch}}
103 |
104 |
105 | );
106 | };
107 | ```
108 |
109 | ## ⌨️ 本地开发
110 |
111 | ```sh
112 | # 克隆项目到本地
113 | git clone git@github.com:ts-react/react-admin-template.git
114 |
115 | # 切换到项目目录
116 | cd ./react-admin-template
117 |
118 | # 安装依赖
119 | yarn
120 |
121 | # 启动服务
122 | npm run start
123 | ```
124 |
125 | ## 🖥 支持环境
126 |
127 | 现代浏览器及 IE11。
128 |
129 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari | [
](http://godban.github.io/browsers-support-badges/)Opera |
130 | | --- | --- | --- | --- | --- |
131 | | IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
132 |
133 | ## 👥 社区互助
134 |
135 | | Github Issue | 钉钉群 | 微信群 |
136 | | ------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
137 | | [issues](https://github.com/ts-react/react-admin-template/issues) |
|
|
138 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | yarn
3 | yarn build
4 |
--------------------------------------------------------------------------------
/config/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'umi';
2 | import { routes } from './routes';
3 | import defaultSettings from './default-settings';
4 | import proxy from './proxy';
5 |
6 | const { REACT_APP_ENV } = process.env;
7 |
8 | export default defineConfig({
9 | hash: true,
10 | routes,
11 | targets: {
12 | ie: 11,
13 | },
14 | antd: {
15 | config: {}
16 | },
17 | layout: {
18 | name: defaultSettings.title,
19 | locale: true,
20 | siderWidth: 240
21 | },
22 | locale: {
23 | default: 'zh-CN',
24 | antd: true,
25 | baseNavigator: true,
26 | libraryName: '@alitajs/antd-plus'
27 | },
28 | webpack5: {
29 | lazyCompilation: {},
30 | },
31 | consoleVersion: {
32 | projectName: 'admin-template'
33 | },
34 | dynamicImport: {
35 | loading: '@/components/page-loading/index',
36 | },
37 | theme: {
38 | 'primary-color': defaultSettings.primaryColor,
39 | },
40 | cssModulesTypescriptLoader: {},
41 | ignoreMomentLocale: true,
42 | // proxy: proxy[REACT_APP_ENV || 'dev'],
43 | })
44 |
--------------------------------------------------------------------------------
/config/default-settings.ts:
--------------------------------------------------------------------------------
1 | import { Settings } from '@ant-design/pro-layout';
2 |
3 | const settings: Settings = {
4 | navTheme: 'dark',
5 | // 拂晓蓝
6 | primaryColor: '#409EFF',
7 | layout: 'side',
8 | contentWidth: 'Fluid',
9 | fixedHeader: false,
10 | fixSiderbar: true,
11 | colorWeak: false,
12 | menu: {
13 | locale: true,
14 | },
15 | title: 'React Admin',
16 | iconfontUrl: '',
17 | };
18 |
19 | export default settings
20 |
--------------------------------------------------------------------------------
/config/proxy.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | dev: {
3 | '/api/': {
4 | target: 'https://preview.pro.ant.design',
5 | changeOrigin: true,
6 | pathRewrite: { '^': '' },
7 | },
8 | },
9 | test: {
10 | '/api/': {
11 | target: 'https://preview.pro.ant.design',
12 | changeOrigin: true,
13 | pathRewrite: { '^': '' },
14 | },
15 | },
16 | pre: {
17 | '/api/': {
18 | target: 'your pre url',
19 | changeOrigin: true,
20 | pathRewrite: { '^': '' },
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/config/routes.ts:
--------------------------------------------------------------------------------
1 | export const routes = [
2 | {
3 | path: '/login',
4 | component: 'login',
5 | layout: false
6 | },
7 | {
8 | path: '/register',
9 | component: 'register',
10 | layout: false
11 | },
12 | {
13 | path: '/dashboard',
14 | component: 'dashboard',
15 | menu: {
16 | name: '首页',
17 | icon: 'dashboard',
18 | },
19 | },
20 | {
21 | path: '/',
22 | redirect: '/dashboard',
23 | },
24 | {
25 | path: '/nested',
26 | menu: {
27 | name: '路由嵌套',
28 | icon: 'bars',
29 | },
30 | routes: [
31 | {
32 | path: '/nested/menu1',
33 | menu: {
34 | name: '菜单1'
35 | },
36 | routes: [
37 | {
38 | path: '/nested/menu1/menu1-1',
39 | menu: {
40 | name: '菜单1-1'
41 | },
42 | component: '@/pages/nested/menu1/menu1-1'
43 | },
44 | {
45 | path: '/nested/menu1/menu1-2',
46 | menu: {
47 | name: '菜单1-2'
48 | },
49 | routes: [
50 | {
51 | path: '/nested/menu1/menu1-2',
52 | redirect: '/nested/menu1/menu1-2/menu1-2-1',
53 | },
54 | {
55 | path: '/nested/menu1/menu1-2/menu1-2-1',
56 | menu: {
57 | name: '菜单1-2-1'
58 | },
59 | component: '@/pages/nested/menu1/menu1-2/menu1-2-1'
60 | },
61 | {
62 | path: '/nested/menu1/menu1-2/menu1-2-2',
63 | menu: {
64 | name: '菜单1-2-2'
65 | },
66 | component: '@/pages/nested/menu1/menu1-2/menu1-2-2'
67 | }
68 | ]
69 | },
70 | {
71 | path: '/nested/menu1/menu1-3',
72 | menu: {
73 | name: '菜单1-3'
74 | },
75 | component: '@/pages/nested/menu1/menu1-3'
76 | },
77 | {
78 | path: '/nested/menu1',
79 | redirect: '/nested/menu1/menu1-1',
80 | }
81 | ]
82 | },
83 | {
84 | path: '/nested/menu2',
85 | menu: {
86 | name: '菜单2'
87 | },
88 | component: '@/pages/nested/menu2'
89 | },
90 | {
91 | path: '/nested',
92 | redirect: '/nested/menu1',
93 | }
94 | ]
95 | },
96 | {
97 | path: '/exception',
98 | menu: {
99 | name: '异常页',
100 | icon: 'warning',
101 | },
102 | routes: [
103 | {
104 | path: '/exception/403',
105 | menu: {
106 | name: '403'
107 | },
108 | component: '@/pages/exception/403'
109 | },
110 | {
111 | path: '/exception/404',
112 | menu: {
113 | name: '404'
114 | },
115 | component: '@/pages/exception/404'
116 | },
117 | {
118 | path: '/exception/500',
119 | menu: {
120 | name: '500'
121 | },
122 | component: '@/pages/exception/500'
123 | },
124 | ]
125 | },
126 | {
127 | path: '/libraries',
128 | menu: {
129 | name: '组件',
130 | icon: 'appstore',
131 | },
132 | routes: [
133 | {
134 | path: '/libraries/amap',
135 | menu: {
136 | name: '高德地图'
137 | },
138 | component: '@/pages/libraries/amap'
139 | },
140 | {
141 | path: '/libraries/watermark',
142 | menu: {
143 | name: '水印组件'
144 | },
145 | component: '@/pages/libraries/watermark'
146 | }
147 | ]
148 | },
149 | {
150 | path: '/permission',
151 | menu: {
152 | name: '权限管理',
153 | icon: 'lock',
154 | },
155 | routes: [
156 | {
157 | path: '/permission',
158 | redirect: '/permission/action',
159 | },
160 | {
161 | path: '/permission/page',
162 | menu: {
163 | name: '页面权限测试'
164 | },
165 | component: '@/pages/permission/page'
166 | },
167 | {
168 | path: '/permission/button',
169 | menu: {
170 | name: '按钮权限测试'
171 | },
172 | component: '@/pages/permission/button'
173 | },
174 | {
175 | path: '/permission/action',
176 | menu: {
177 | name: '操作管理'
178 | },
179 | authority: 'permission:actionView',
180 | component: '@/pages/permission/action'
181 | },
182 | {
183 | path: '/permission/policy',
184 | menu: {
185 | name: '策略管理'
186 | },
187 | authority: 'permission:policyView',
188 | component: '@/pages/permission/policy'
189 | },
190 | ]
191 | },
192 | {
193 | path: '/system',
194 | menu: {
195 | name: '系统管理',
196 | icon: 'desktop',
197 | },
198 | routes: [
199 | {
200 | path: '/system',
201 | redirect: '/system/user',
202 | },
203 | {
204 | path: '/system/user',
205 | menu: {
206 | name: '用户管理'
207 | },
208 | component: '@/pages/system/user'
209 | },
210 | {
211 | path: '/system/role',
212 | menu: {
213 | name: '角色管理'
214 | },
215 | component: '@/pages/system/role'
216 | }
217 | ]
218 | },
219 | {
220 | component: './404',
221 | }
222 | ]
223 |
--------------------------------------------------------------------------------
/docker/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | # gzip config
4 | gzip on;
5 | gzip_min_length 1k;
6 | gzip_comp_level 9;
7 | gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
8 | gzip_vary on;
9 | gzip_disable "MSIE [1-6]\.";
10 |
11 | root /usr/share/nginx/html;
12 | include /etc/nginx/mime.types;
13 | location / {
14 | try_files $uri $uri/ /index.html;
15 | }
16 | location /api {
17 | proxy_pass https://proapi.azurewebsites.net;
18 | proxy_set_header X-Forwarded-Proto $scheme;
19 | proxy_set_header X-Real-IP $remote_addr;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/mock/dashboard.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import { Request, Response } from 'express';
3 | import { packResult } from './utils';
4 |
5 | // mock data
6 | const visitData: { date: string; value: number }[] = [];
7 | const beginDay = new Date().getTime();
8 |
9 | const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
10 | for (let i = 0; i < fakeY.length; i += 1) {
11 | visitData.push({
12 | date: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
13 | value: fakeY[i],
14 | });
15 | }
16 |
17 | const salesData: { date: string; value: number }[] = [];
18 | for (let i = 0; i < 12; i += 1) {
19 | salesData.push({
20 | date: `${i + 1}月`,
21 | value: Math.floor(Math.random() * 1000) + 200,
22 | });
23 | }
24 |
25 | const fetchChartData = (_: Request, res: Response) => {
26 | return res.json(packResult({ data: { visitData, salesData } }));
27 | }
28 |
29 | export default {
30 | 'GET /api/dashboard/chartData': fetchChartData,
31 | };
32 |
--------------------------------------------------------------------------------
/mock/permission.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import { mock } from 'better-mock';
3 | import { Request, Response } from 'express';
4 | import { packResult } from './utils';
5 |
6 | let actions: API.PermissionActionData[] = [];
7 |
8 | for (let i = 0; i < 11; i++) {
9 | actions.push(
10 | mock({
11 | id: '@guid',
12 | module: 'permission',
13 | code: '@string(8)',
14 | name: '@ctitle(4)',
15 | remark: '@cparagraph(1)',
16 | createdAt: moment().valueOf(),
17 | updatedAt: moment().valueOf(),
18 | }),
19 | );
20 | }
21 |
22 | const fetchActionList = (req: Request, res: Response) => {
23 | const { page = 1, size = 20 } = req.query;
24 |
25 | const pageList = actions.filter((item, index) => {
26 | return index < +size * +page && index >= +size * (+page - 1);
27 | });
28 |
29 | return res.json(
30 | packResult({
31 | data: {
32 | list: pageList,
33 | total: actions.length
34 | }
35 | })
36 | );
37 | }
38 |
39 | export default {
40 | 'GET /api/permission/action/list': fetchActionList,
41 | };
42 |
--------------------------------------------------------------------------------
/mock/user.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 | import { packResult } from './utils';
3 |
4 | const permissionCodes = [
5 | { group: 'dashboard', actions: ['view'] },
6 | {
7 | group: 'permission',
8 | actions: [
9 | 'view',
10 | 'policyAdd',
11 | 'policyDelete',
12 | 'policyModify',
13 | 'policyView',
14 | 'actionAdd',
15 | 'actionDelete',
16 | 'actionModify',
17 | 'actionView',
18 | ]
19 | }
20 | ]
21 |
22 | function fetchCaptcha(req: Request, res: Response) {
23 | res.send(packResult());
24 | }
25 |
26 | function fetchCurrentUser(req: Request, res: Response) {
27 | const authorization = req.headers?.authorization;
28 | const token = authorization?.split(' ')?.[1];
29 |
30 | if (token !== 'admin' && token !== 'user') {
31 | res.status(401).send(packResult({ data: {}, code: 401 }))
32 | }
33 |
34 | const data = {
35 | name: 'Serati Ma',
36 | avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
37 | userid: '00000001',
38 | email: 'antdesign@alipay.com',
39 | signature: '海纳百川,有容乃大',
40 | title: '交互专家',
41 | group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
42 | // 所有的权限
43 | permissionCodes,
44 | // 赋予的权限
45 | access: [
46 | { group: 'module1', actions: '*' },
47 | { group: 'module2', actions: ['action1'] }
48 | ],
49 | tags: [
50 | {
51 | key: '0',
52 | label: '很有想法的',
53 | },
54 | {
55 | key: '1',
56 | label: '专注设计',
57 | },
58 | {
59 | key: '2',
60 | label: '辣~',
61 | },
62 | {
63 | key: '3',
64 | label: '大长腿',
65 | },
66 | {
67 | key: '4',
68 | label: '川妹子',
69 | },
70 | {
71 | key: '5',
72 | label: '海纳百川',
73 | },
74 | ],
75 | notifyCount: 12,
76 | unreadCount: 11,
77 | country: 'China',
78 | geographic: {
79 | province: {
80 | label: '浙江省',
81 | key: '330000',
82 | },
83 | city: {
84 | label: '杭州市',
85 | key: '330100',
86 | },
87 | },
88 | address: '西湖区工专路 77 号',
89 | phone: '0752-268888888',
90 | }
91 |
92 | res.send(packResult({ data }));
93 | };
94 |
95 | function fetchLogin(req: Request, res: Response) {
96 | const { password, username, type } = req.body;
97 | if (password === '123456' && username === 'admin') {
98 | res.send(packResult(
99 | { data: { type, token: 'admin', }
100 | }));
101 | return;
102 | }
103 | if (password === '123456' && username === 'user') {
104 | res.send(packResult({
105 | data: { type, token: 'user', }
106 | }));
107 | return;
108 | }
109 | if (type === 'mobile') {
110 | res.send(packResult({
111 | data: { type, token: 'admin'} }));
112 | return;
113 | }
114 |
115 | res.send(packResult({
116 | data: {
117 | type
118 | },
119 | code: 10010,
120 | message: '用户名或密码不正确'
121 | }));
122 | }
123 |
124 | function fetchLogout(req: Request, res: Response) {
125 | res.send(packResult());
126 | }
127 |
128 | export default {
129 | 'GET /api/user/captcha': fetchCaptcha,
130 |
131 | 'POST /api/user/login': fetchLogin,
132 |
133 | 'GET /api/user/current': fetchCurrentUser,
134 |
135 | 'POST /api/user/logout': fetchLogout
136 | }
137 |
--------------------------------------------------------------------------------
/mock/utils.ts:
--------------------------------------------------------------------------------
1 | interface ResultParams {
2 | data?: any;
3 | code?: number;
4 | message?: string;
5 | [key: string]: any
6 | }
7 |
8 | /**
9 | * 包裹请求数据
10 | * @param data 需要返回的数据
11 | * @param code 返回的状态码
12 | * @param message 返回的状态码描述
13 | */
14 | export function packResult(params?: ResultParams) {
15 | const {
16 | data = undefined,
17 | code = 200,
18 | message = 'success',
19 | ...reset
20 | } = params || {};
21 |
22 | return {
23 | data,
24 | code,
25 | message,
26 | ...reset
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-admin",
3 | "version": 2,
4 | "builds": [
5 | {
6 | "src": "build.sh",
7 | "use": "@now/static-build",
8 | "config": {
9 | "distDir": "dist"
10 | }
11 | }
12 | ],
13 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-admin",
3 | "version": "2.0.0",
4 | "description": "react development template",
5 | "main": "index.js",
6 | "repository": "git@github.com:pansyjs/react-admin.git",
7 | "author": "kang ",
8 | "license": "MIT",
9 | "scripts": {
10 | "analyze": "cross-env ANALYZE=1 umi build",
11 | "start": "npm run start:dev",
12 | "build": "umi build",
13 | "test": "walrus test",
14 | "prettier": "prettier -c --write \"src/**/*\"",
15 | "start:dev": "cross-env REACT_APP_ENV=dev umi dev",
16 | "start:no-mock": "cross-env MOCK=none umi dev",
17 | "lint:es": "walrus eslint",
18 | "lint:style": "walrus stylelint --fix 'src/**/*.less' --syntax less",
19 | "lint:commit": "walrus commitlint --env HUSKY_GIT_PARAMS"
20 | },
21 | "dependencies": {
22 | "@alitajs/antd-plus": "^2.5.0",
23 | "@ant-design/icons": "^4.7.0",
24 | "@ant-design/pro-card": "^1.18.30",
25 | "@ant-design/pro-layout": "^6.32.12",
26 | "@ant-design/pro-table": "^2.63.4",
27 | "@formily/antd": "^2.0.10",
28 | "@formily/antd-components": "^1.3.17",
29 | "@formily/core": "^2.0.10",
30 | "@formily/react": "^2.0.10",
31 | "@pansy/policy": "^0.5.0",
32 | "@pansy/react-amap": "2.12.3",
33 | "@pansy/react-charts": "^1.0.0",
34 | "@pansy/react-hooks": "^0.9.2",
35 | "@pansy/react-watermark": "^3.1.8",
36 | "antd": "^4.18.6",
37 | "classnames": "^2.3.1",
38 | "moment": "^2.29.1",
39 | "numeral": "^2.0.6",
40 | "react": "^17.0.2",
41 | "react-dom": "^17.0.2",
42 | "react-helmet-async": "^1.2.2",
43 | "styled-components": "^5.3.3",
44 | "umi": "^3.5.20",
45 | "umi-request": "^1.4.0",
46 | "use-merge-value": "^1.0.2"
47 | },
48 | "devDependencies": {
49 | "@alitajs/umi-plugin-antd-plus": "0.2.0",
50 | "@alitajs/umi-plugin-console-version": "0.4.0",
51 | "@alitajs/umi-preset-react": "0.3.0",
52 | "@types/classnames": "2.3.1",
53 | "@types/numeral": "2.0.2",
54 | "@types/react": "17.0.39",
55 | "@types/react-dom": "17.0.11",
56 | "@types/styled-components": "5.1.22",
57 | "@walrus/cli": "1.3.4",
58 | "@walrus/plugin-release": "1.14.3",
59 | "@walrus/preset-lint": "1.1.8",
60 | "better-mock": "0.3.2",
61 | "cross-env": "7.0.3",
62 | "husky": "5.2.0",
63 | "typescript": "4.5.5"
64 | },
65 | "husky": {
66 | "hooks": {
67 | "pre-commit": "yarn prettier",
68 | "commit-msg": "yarn lint:commit"
69 | }
70 | },
71 | "browserslist": [
72 | "> 1%",
73 | "last 2 versions",
74 | "not ie < 10"
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { notification } from 'antd';
3 | import { ResponseError } from 'umi-request';
4 | import { ConfigProviderProps } from 'antd/es/config-provider';
5 | import { BasicLayoutProps, Settings as LayoutSettings } from '@ant-design/pro-layout';
6 | import { history, RequestConfig } from 'umi';
7 | import { fetchCurrent } from '@/services/user';
8 | import RightContent from '@/components/right-content';
9 | import Footer from '@/components/footer';
10 | import { NO_LOGIN_WHITELIST } from '@/config';
11 | import { getCookie, removeCookie } from '@/utils/cookie';
12 | import logo from '@/assets/logo.svg';
13 | import defaultSettings from '../config/default-settings';
14 |
15 | export async function getInitialState(): Promise<{
16 | settings?: LayoutSettings;
17 | currentUser?: API.CurrentUser;
18 | fetchUserInfo: () => Promise;
19 | }> {
20 | const fetchUserInfo = async () => {
21 | try {
22 | const { data } = await fetchCurrent();
23 | return data;
24 | } catch (error) {
25 | history.push('/login');
26 | }
27 | return undefined;
28 | };
29 | // 如果是登录页面,不执行
30 | if (NO_LOGIN_WHITELIST.indexOf(history.location.pathname) === -1) {
31 | const currentUser = await fetchUserInfo();
32 | return {
33 | fetchUserInfo,
34 | currentUser,
35 | settings: defaultSettings,
36 | };
37 | }
38 | return {
39 | fetchUserInfo,
40 | settings: defaultSettings,
41 | };
42 | }
43 |
44 | export const layout = ({
45 | initialState,
46 | }: {
47 | initialState: { settings?: LayoutSettings; currentUser?: API.CurrentUser };
48 | }): BasicLayoutProps => {
49 | return {
50 | rightContentRender: () => ,
51 | disableContentMargin: false,
52 | footerRender: () => ,
53 | onPageChange: () => {
54 | const { currentUser } = initialState;
55 | const { location } = history;
56 |
57 | // 如果没有登录,重定向到 login
58 | if (!currentUser?.userid && NO_LOGIN_WHITELIST.indexOf(location.pathname) === -1) {
59 | history.push('/login');
60 | }
61 | },
62 | menuHeaderRender: undefined,
63 | ...initialState?.settings,
64 | logo: logo
65 | };
66 | };
67 |
68 | const codeMessage = {
69 | 200: '服务器成功返回请求的数据。',
70 | 201: '新建或修改数据成功。',
71 | 202: '一个请求已经进入后台排队(异步任务)。',
72 | 204: '删除数据成功。',
73 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
74 | 401: '用户没有权限(令牌、用户名、密码错误)。',
75 | 403: '用户得到授权,但是访问是被禁止的。',
76 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
77 | 405: '请求方法不被允许。',
78 | 406: '请求的格式不可得。',
79 | 410: '请求的资源被永久删除,且不会再得到的。',
80 | 422: '当创建一个对象时,发生一个验证错误。',
81 | 500: '服务器发生错误,请检查服务器。',
82 | 502: '网关错误。',
83 | 503: '服务不可用,服务器暂时过载或维护。',
84 | 504: '网关超时。',
85 | };
86 |
87 | /**
88 | * 异常处理程序
89 | */
90 | const errorHandler = (error: ResponseError) => {
91 | const { response } = error;
92 | if (response && response.status) {
93 | const errorText = codeMessage[response.status] || response.statusText;
94 | const { status, url } = response;
95 |
96 | notification.error({
97 | message: `请求错误 ${status}: ${url}`,
98 | description: errorText,
99 | });
100 | }
101 |
102 | const httpCode = error?.response?.status;
103 |
104 | // 登录过期
105 | if (httpCode === 401) {
106 | removeCookie();
107 | history.replace('/login');
108 | return;
109 | }
110 |
111 | if (!response) {
112 | notification.error({
113 | description: '您的网络发生异常,无法连接服务器',
114 | message: '网络异常',
115 | });
116 | }
117 | throw error;
118 | };
119 |
120 | export const request: RequestConfig = {
121 | errorHandler,
122 | errorConfig: {
123 | adaptor: (resData) => {
124 | return {
125 | ...resData,
126 | success: resData.code === 200,
127 | errorMessage: resData.message,
128 | }
129 | }
130 | },
131 | requestInterceptors: [
132 | (url, options) => {
133 | const token = getCookie();
134 | // token 不存在,则跳转到登录页面
135 | if (!token) {
136 | removeCookie();
137 | history.replace('/login');
138 | }
139 | return {
140 | url: `${url}`,
141 | options: {
142 | ...options,
143 | headers: {
144 | ...options.headers,
145 | authorization: `Bearer ${token}`
146 | }
147 | },
148 | };
149 | }
150 | ]
151 | };
152 |
153 | // antd 配置
154 | // 具体请查看 https://ant.design/components/config-provider-cn/#API
155 | export const antd: ConfigProviderProps = {
156 | autoInsertSpaceInButton: false
157 | }
158 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/authority.ts:
--------------------------------------------------------------------------------
1 | import { AuthorityConfigFun } from 'umi';
2 | import { Action, PolicyData, Statement } from '@pansy/policy';
3 |
4 | const separator = ':';
5 |
6 | /**
7 | * 提供Policy相关配置,可与后端约定相关数据结构进行修改
8 | * 具体请查看
9 | * https://github.com/alitajs/umi-plugins/tree/master/packages/umi-plugin-authority
10 | * https://github.com/pansyjs/utils/tree/master/packages/policy
11 | * @param initialState
12 | */
13 | const authorityConfig: AuthorityConfigFun = (initialState: { currentUser?: API.CurrentUser } = {}) => {
14 | const { currentUser } = initialState;
15 |
16 | const access = currentUser?.access || [];
17 | const permissionCodes = currentUser?.permissionCodes || [];
18 |
19 | const allActions: Action[] = [];
20 | const policies: PolicyData[] = [];
21 |
22 | permissionCodes.forEach(({ actions = [], group }) => {
23 | if (group && typeof group === 'string') {
24 | allActions.push(...actions.map(value => ({ module: group, action: value })));
25 | }
26 | });
27 |
28 | access.forEach(({ actions = [], group }) => {
29 | let statementAction: Statement['action'] = [];
30 | if (group && typeof group === 'string') {
31 | if (actions === '*') {
32 | statementAction = [`${group}${separator}*`]
33 | } else {
34 | statementAction = actions.map(item => `${group}${separator}${item}`);
35 | }
36 | }
37 | policies.push({
38 | version: 1,
39 | statement: [
40 | {
41 | effect: 'allow',
42 | action: statementAction
43 | }
44 | ]
45 | });
46 | });
47 |
48 | return {
49 | actions: allActions,
50 | policies,
51 | separator
52 | };
53 | }
54 |
55 | export default authorityConfig;
56 |
--------------------------------------------------------------------------------
/src/common/constant/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pansyjs/react-admin/a12b3579845690808d76fea5ea4ee7f02069b2f7/src/common/constant/index.ts
--------------------------------------------------------------------------------
/src/common/types/api/base.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace API {
2 | export interface PermissionCode {
3 | group: string;
4 | actions: T;
5 | }
6 |
7 | export interface ResponseResult {
8 | data: T,
9 | code: number;
10 | message: string;
11 | }
12 |
13 | export interface ResponsePaginationResult {
14 | data: {
15 | list: T[];
16 | total: number;
17 | },
18 | code: number;
19 | message: string;
20 | }
21 |
22 | export interface CurrentUser {
23 | /**
24 | * 用户头像
25 | */
26 | avatar?: string;
27 | /**
28 | * 用户名
29 | */
30 | name?: string;
31 | title?: string;
32 | group?: string;
33 | signature?: string;
34 | tags?: {
35 | key: string;
36 | label: string;
37 | }[];
38 | userid?: string;
39 | unreadCount?: number;
40 | /**
41 | * 所有的权限
42 | */
43 | permissionCodes?: PermissionCode[];
44 | /**
45 | * 赋予的权限
46 | */
47 | access?: PermissionCode<'*' | string[]>[];
48 | }
49 |
50 | export interface LoginStateType {
51 | status?: 'ok' | 'error';
52 | type?: string;
53 | }
54 |
55 | export interface NoticeIconData {
56 | id: string;
57 | key: string;
58 | avatar: string;
59 | title: string;
60 | datetime: string;
61 | type: string;
62 | read?: boolean;
63 | description: string;
64 | clickClose?: boolean;
65 | extra: any;
66 | status: string;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/common/types/api/permission.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace API {
2 | export interface PermissionActionData {
3 | /**
4 | * 唯一表示
5 | */
6 | id: string;
7 | /**
8 | * 所属模块
9 | */
10 | module: string;
11 | /**
12 | * 操作标识
13 | */
14 | code: string;
15 | /**
16 | * 显示名称
17 | */
18 | name: string;
19 | /**
20 | * 类型
21 | */
22 | type: 0 | 1;
23 | /**
24 | * 备注
25 | */
26 | remark?: string;
27 | /**
28 | * 创建时间
29 | */
30 | createdAt?: number;
31 | /**
32 | * 修改时间
33 | */
34 | updatedAt?: number;
35 | }
36 |
37 | export interface PermissionData {
38 | id: string;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/common/types/api/user.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace API {
2 | export interface UserInfo {
3 | id: string;
4 | /**
5 | * 用户名
6 | */
7 | username: string;
8 | /**
9 | * 用户昵称
10 | */
11 | nickname: string;
12 | /**
13 | * 手机号
14 | */
15 | mobile: string;
16 | /**
17 | * 用户邮箱
18 | */
19 | email?: string;
20 | /**
21 | * 备注
22 | */
23 | remark?: string;
24 | /**
25 | * 创建时间
26 | */
27 | createTime: number;
28 | /**
29 | * 修改时间
30 | */
31 | updateTime: number;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/common/types/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace APP {
4 | /**
5 | * 约定式组件
6 | */
7 | export interface RouteFC extends React.FC {
8 | menu?: {
9 | name: string;
10 | icon?: React.ReactNode;
11 | }
12 | layout?: boolean | {
13 |
14 | };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/common/types/login.ts:
--------------------------------------------------------------------------------
1 | export interface LoginParamsType {
2 | username: string;
3 | password: string;
4 | mobile: string;
5 | captcha: string;
6 | type: string;
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/components/footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DefaultFooter } from '@ant-design/pro-layout';
3 |
4 | export default () => (
5 |
6 | );
7 |
--------------------------------------------------------------------------------
/src/components/header-dropdown/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .container > * {
4 | background-color: @popover-bg;
5 | border-radius: 4px;
6 | box-shadow: @shadow-1-down;
7 | }
8 |
9 | @media screen and (max-width: @screen-xs) {
10 | .container {
11 | width: 100% !important;
12 | }
13 | .container > * {
14 | border-radius: 0 !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/header-dropdown/index.tsx:
--------------------------------------------------------------------------------
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 Omit {
10 | overlayClassName?: string;
11 | overlay: React.ReactNode | OverlayFunc | any;
12 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
13 | }
14 |
15 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => (
16 |
17 | );
18 |
19 | export default HeaderDropdown;
20 |
--------------------------------------------------------------------------------
/src/components/header-search/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .headerSearch {
4 | .input {
5 | width: 0;
6 | min-width: 0;
7 | overflow: hidden;
8 | background: transparent;
9 | border-radius: 0;
10 | transition: width 0.3s, margin-left 0.3s;
11 | :global(.ant-select-selection) {
12 | background: transparent;
13 | }
14 | input {
15 | box-shadow: none !important;
16 | }
17 |
18 | &.show {
19 | width: 210px;
20 | margin-left: 8px;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/header-search/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { SearchOutlined } from '@ant-design/icons';
3 | import { AutoComplete, Input } from 'antd';
4 | import useMergeValue from 'use-merge-value';
5 | import { AutoCompleteProps } from 'antd/es/auto-complete';
6 |
7 | import classNames from 'classnames';
8 | import styles from './index.less';
9 |
10 | export interface HeaderSearchProps {
11 | onSearch?: (value?: string) => void;
12 | onChange?: (value?: string) => void;
13 | onVisibleChange?: (b: boolean) => void;
14 | className?: string;
15 | placeholder?: string;
16 | options: AutoCompleteProps['options'];
17 | defaultOpen?: boolean;
18 | open?: boolean;
19 | defaultValue?: string;
20 | value?: string;
21 | }
22 |
23 | const HeaderSearch: React.FC = (props) => {
24 | const {
25 | className,
26 | defaultValue,
27 | onVisibleChange,
28 | placeholder,
29 | open,
30 | defaultOpen,
31 | ...restProps
32 | } = props;
33 |
34 | const inputRef = useRef(null);
35 |
36 | const [value, setValue] = useMergeValue(defaultValue, {
37 | value: props.value,
38 | onChange: props.onChange,
39 | });
40 |
41 | const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, {
42 | value: props.open,
43 | onChange: onVisibleChange,
44 | });
45 |
46 | const inputClass = classNames(styles.input, {
47 | [styles.show]: searchMode,
48 | });
49 |
50 | return (
51 | {
54 | setSearchMode(true);
55 | if (searchMode && inputRef.current) {
56 | inputRef.current.focus();
57 | }
58 | }}
59 | onTransitionEnd={({ propertyName }) => {
60 | if (propertyName === 'width' && !searchMode) {
61 | if (onVisibleChange) {
62 | onVisibleChange(searchMode);
63 | }
64 | }
65 | }}
66 | >
67 |
73 |
80 | {
87 | if (e.key === 'Enter') {
88 | if (restProps.onSearch) {
89 | restProps.onSearch(value);
90 | }
91 | }
92 | }}
93 | onBlur={() => {
94 | setSearchMode(false);
95 | }}
96 | />
97 |
98 |
99 | );
100 | };
101 |
102 | export default HeaderSearch;
103 |
--------------------------------------------------------------------------------
/src/components/page-loading/index.tsx:
--------------------------------------------------------------------------------
1 | import { PageLoading } from '@ant-design/pro-layout';
2 |
3 | export default PageLoading;
4 |
--------------------------------------------------------------------------------
/src/components/right-content/avatar-dropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
3 | import { Avatar, Menu, Spin } from 'antd';
4 | import { MenuProps } from 'antd/es/menu';
5 | import { history, useModel } from 'umi';
6 | import { getPageQuery } from '@/utils';
7 | import { fetchLogout } from '@/services/login';
8 | import { stringify } from 'querystring';
9 | import HeaderDropdown from '../header-dropdown';
10 | import styles from './index.less';
11 |
12 | export interface GlobalHeaderRightProps {
13 | menu?: boolean;
14 | }
15 |
16 | /**
17 | * 退出登录,并且将当前的 url 保存
18 | */
19 | const loginOut = async () => {
20 | await fetchLogout();
21 | const { redirect } = getPageQuery();
22 | // Note: There may be security issues, please note
23 | if (window.location.pathname !== '/login' && !redirect) {
24 | history.replace({
25 | pathname: '/login',
26 | search: stringify({
27 | redirect: window.location.href,
28 | }),
29 | });
30 | }
31 | };
32 |
33 | const AvatarDropdown: React.FC = ({ menu }) => {
34 | const { initialState, setInitialState } = useModel('@@initialState');
35 |
36 | const onMenuClick: MenuProps['onClick'] = useCallback(
37 | (event) => {
38 | const { key } = event;
39 | if (key === 'logout') {
40 | // @ts-ignore
41 | setInitialState({ ...initialState, currentUser: undefined });
42 | loginOut();
43 | return;
44 | }
45 | history.push(`/account/${key}`);
46 | },
47 | [],
48 | );
49 |
50 | const loading = (
51 |
52 |
59 |
60 | );
61 |
62 | if (!initialState) {
63 | return loading;
64 | }
65 |
66 | const { currentUser } = initialState;
67 |
68 | if (!currentUser || !currentUser.name) {
69 | return loading;
70 | }
71 |
72 | const menuHeaderDropdown = (
73 |
93 | );
94 | return (
95 |
96 |
97 |
98 | {currentUser.name}
99 |
100 |
101 | );
102 | };
103 |
104 | export default AvatarDropdown;
105 |
--------------------------------------------------------------------------------
/src/components/right-content/index.less:
--------------------------------------------------------------------------------
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 | display: flex;
16 | float: right;
17 | height: 48px;
18 | margin-left: auto;
19 | overflow: hidden;
20 | .action {
21 | display: flex;
22 | align-items: center;
23 | height: 48px;
24 | padding: 0 12px;
25 | cursor: pointer;
26 | transition: all 0.3s;
27 | &:hover {
28 | background: @pro-header-hover-bg;
29 | }
30 | &:global(.opened) {
31 | background: @pro-header-hover-bg;
32 | }
33 | }
34 | .search {
35 | padding: 0 12px;
36 | &:hover {
37 | background: transparent;
38 | }
39 | }
40 | .account {
41 | .avatar {
42 | margin-right: 8px;
43 | color: @primary-color;
44 | vertical-align: top;
45 | background: rgba(255, 255, 255, 0.85);
46 | }
47 | }
48 | }
49 |
50 | .dark {
51 | .action {
52 | &:hover {
53 | background: #252a3d;
54 | }
55 | &:global(.opened) {
56 | background: #252a3d;
57 | }
58 | }
59 | }
60 |
61 | @media only screen and (max-width: @screen-md) {
62 | :global(.ant-divider-vertical) {
63 | vertical-align: unset;
64 | }
65 | .name {
66 | display: none;
67 | }
68 | .right {
69 | position: absolute;
70 | top: 0;
71 | right: 12px;
72 | .account {
73 | .avatar {
74 | margin-right: 0;
75 | }
76 | }
77 | .search {
78 | display: none;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/right-content/index.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip, Tag, Space } from 'antd';
2 | import { QuestionCircleOutlined } from '@ant-design/icons';
3 | import React from 'react';
4 | import { useModel, SelectLang } from 'umi';
5 | import Avatar from './avatar-dropdown';
6 | import HeaderSearch from '../header-search';
7 | import styles from './index.less';
8 |
9 | export type SiderTheme = 'light' | 'dark';
10 |
11 | const ENVTagColor = {
12 | dev: 'orange',
13 | test: 'green',
14 | pre: '#87d068',
15 | };
16 |
17 | const GlobalHeaderRight: React.FC<{}> = () => {
18 | const { initialState } = useModel('@@initialState');
19 |
20 | if (!initialState || !initialState.settings) {
21 | return null;
22 | }
23 |
24 | const { navTheme, layout } = initialState.settings;
25 | let className = styles.right;
26 |
27 | if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
28 | className = `${styles.right} ${styles.dark}`;
29 | }
30 | return (
31 |
32 | umi ui, value: 'umi ui' },
38 | {
39 | label: Ant Design,
40 | value: 'Ant Design',
41 | },
42 | {
43 | label: Pro Table,
44 | value: 'Pro Table',
45 | },
46 | {
47 | label: Pro Layout,
48 | value: 'Pro Layout',
49 | },
50 | ]}
51 | // onSearch={value => {
52 | // //console.log('input', value);
53 | // }}
54 | />
55 |
56 | {
59 | window.location.href = 'https://react-admin-site.vercel.app/guide/getting-started';
60 | }}
61 | >
62 |
63 |
64 |
65 |
66 | {/* {REACT_APP_ENV && (
67 |
68 | {REACT_APP_ENV}
69 |
70 | )} */}
71 |
72 |
73 | );
74 | };
75 | export default GlobalHeaderRight;
76 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 免登陆白名单
3 | */
4 | export const NO_LOGIN_WHITELIST = [
5 | '/login',
6 | '/register'
7 | ];
8 |
9 | // cookie key config
10 | export const TOKEN_KEY = 'ADMIN-TOKEN';
11 |
--------------------------------------------------------------------------------
/src/global.less:
--------------------------------------------------------------------------------
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 | .ant-layout-header {
18 | z-index: 999 !important;
19 | }
20 |
21 | canvas {
22 | display: block;
23 | }
24 |
25 | body {
26 | text-rendering: optimizeLegibility;
27 | -webkit-font-smoothing: antialiased;
28 | -moz-osx-font-smoothing: grayscale;
29 | }
30 |
31 | ul,
32 | ol {
33 | list-style: none;
34 | }
35 |
36 | .ant-pro-global-footer {
37 | margin: 24px 0;
38 | }
39 |
40 | .ant-pro-basicLayout-content {
41 | margin-bottom: 0;
42 | }
43 |
44 | @media (max-width: @screen-xs) {
45 | .ant-table {
46 | width: 100%;
47 | overflow-x: auto;
48 | &-thead > tr,
49 | &-tbody > tr {
50 | > th,
51 | > td {
52 | white-space: pre;
53 | > span {
54 | display: block;
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | // 兼容IE11
62 | @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
63 | body .ant-design-pro > .ant-layout {
64 | min-height: 100vh;
65 | }
66 | }
67 |
68 | // 隐藏高德 Logo
69 | .amap-logo,
70 | .amap-copyright {
71 | display: none !important;
72 | }
73 |
--------------------------------------------------------------------------------
/src/layouts/user-layout.less:
--------------------------------------------------------------------------------
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 | }
72 |
--------------------------------------------------------------------------------
/src/layouts/user-layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, useIntl, SelectLang } from 'umi';
3 | import {
4 | MenuDataItem,
5 | getMenuData,
6 | getPageTitle,
7 | BasicLayoutProps as ProLayoutProps,
8 | } from '@ant-design/pro-layout';
9 | import Footer from '@/components/footer';
10 | import logo from '@/assets/logo.svg';
11 | import styles from './user-layout.less';
12 |
13 | export interface UserLayoutProps {
14 | breadcrumbNameMap?: {
15 | [path: string]: MenuDataItem;
16 | };
17 | location?: ProLayoutProps['location'];
18 | route?: ProLayoutProps['route'];
19 | }
20 |
21 | const UserLayout: React.FC = (props) => {
22 | const {
23 | children,
24 | location = {
25 | pathname: '',
26 | },
27 | route = {
28 | routes: [],
29 | }
30 | } = props;
31 | const { formatMessage } = useIntl();
32 |
33 | const { routes = [] } = route;
34 | const { breadcrumb } = getMenuData(routes);
35 | const title = getPageTitle({
36 | pathname: location.pathname,
37 | formatMessage,
38 | breadcrumb,
39 | ...props,
40 | });
41 |
42 | return (
43 |
44 |
45 |
{title}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |

58 |
Admin Template
59 |
60 |
61 |
62 | 一个示例丰富、开箱即用的开发模板
63 |
64 |
65 | {children}
66 |
67 |
68 |
69 |
70 | )
71 | }
72 |
73 | export default UserLayout;
74 |
--------------------------------------------------------------------------------
/src/locales/en-US.ts:
--------------------------------------------------------------------------------
1 | import menu from './en-US/menu';
2 |
3 | export default {
4 | 'navBar.lang': '语言',
5 | ...menu
6 | }
7 |
--------------------------------------------------------------------------------
/src/locales/en-US/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.login': 'Login'
3 | }
4 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.ts:
--------------------------------------------------------------------------------
1 | import menu from './zh-CN/menu';
2 |
3 | export default {
4 | 'navBar.lang': '语言',
5 | ...menu,
6 | }
7 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.login': '登录'
3 | }
4 |
--------------------------------------------------------------------------------
/src/models/login.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 |
3 | export default function useAuthModel() {
4 | const signin = useCallback((account, password) => {
5 | // signin implementation
6 | // setUser(user from signin API)
7 | }, [])
8 | const signout = useCallback(() => {
9 | // signout implementation
10 | // setUser(null)
11 | }, [])
12 | return {
13 | signin,
14 | signout
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Result } from 'antd';
2 | import React from 'react';
3 | import { useHistory } from 'umi';
4 |
5 | export default () => {
6 | const history = useHistory();
7 |
8 | return (
9 | history.push('/')}>
15 | Back Home
16 |
17 | }
18 | />
19 | )
20 | };
21 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/introduce-row/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .content {
4 | position: relative;
5 | width: 100%;
6 | height: 80px;
7 | }
8 |
9 | .contentFixed {
10 | position: absolute;
11 | bottom: 0;
12 | left: 0;
13 | width: 100%;
14 | }
15 |
16 | .trendText {
17 | margin-left: 8px;
18 | color: @heading-color;
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/introduce-row/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from '@ant-design/pro-card';
3 | import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
4 | import { Tooltip, Statistic } from 'antd';
5 | import { TinyArea, TinyColumn, Progress } from '@pansy/react-charts';
6 | import Trend from '../trend';
7 | import styles from './index.less';
8 |
9 | interface IntroduceRowProps {
10 | loading?: boolean;
11 | }
12 |
13 | const IntroduceRow: React.FC = ({
14 | loading
15 | }) => {
16 | const progressConfig = {
17 | height: 46,
18 | percent: 0.7,
19 | barWidthRatio: 0.2,
20 | };
21 |
22 | return (
23 |
24 |
30 |
31 |
32 | }
33 | colSpan={6}
34 | loading={loading}
35 | >
36 |
37 |
38 |
39 |
40 | 周同比
41 | 12%
42 |
43 |
44 | 日同比
45 | 11%
46 |
47 |
48 |
49 |
50 |
56 |
57 |
58 | }
59 | colSpan={6}
60 | loading={loading}
61 | >
62 |
63 |
64 |
65 | Math.random() * 20)}
70 | />
71 |
72 |
73 |
74 |
80 |
81 |
82 | }
83 | colSpan={6}
84 | loading={loading}
85 | >
86 |
87 |
88 |
89 | Math.random() * 100)}
92 | />
93 |
94 |
95 |
96 |
102 |
103 |
104 | }
105 | colSpan={6}
106 | loading={loading}
107 | >
108 |
114 |
115 |
116 | )
117 | }
118 |
119 | export default IntroduceRow;
120 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/proportion-sales/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from '@ant-design/pro-card';
3 | import { Pie } from '@pansy/react-charts';
4 | import { Radio, Typography } from 'antd';
5 | import { PieConfig } from '@pansy/react-charts/es/pie'
6 | import { RadioChangeEvent } from 'antd/es/radio';
7 |
8 | export type SalesType = 'all' | 'online' | 'stores';
9 |
10 | interface ProportionSalesProps {
11 | salesType?: SalesType;
12 | onChangeSalesType?: (value: SalesType) => void;
13 | }
14 |
15 | const { Text } = Typography;
16 | const salesTypeData = [
17 | { type: '家用电器', value: 4544 },
18 | { type: '食用酒水', value: 3321 },
19 | { type: '个护健康', value: 3113 },
20 | { type: '服饰箱包', value: 2341 },
21 | { type: '母婴产品', value: 1231 },
22 | { type: '其他', value: 1231 },
23 | ];
24 |
25 | const ProportionSales: React.FC = ({
26 | salesType,
27 | onChangeSalesType
28 | }) => {
29 | const handleChangeSalesType = (e: RadioChangeEvent) => {
30 | onChangeSalesType?.(e.target.value);
31 | }
32 |
33 | const config: PieConfig = {
34 | appendPadding: 10,
35 | data: salesTypeData,
36 | angleField: 'value',
37 | colorField: 'type',
38 | radius: 0.8,
39 | innerRadius: 0.6,
40 | label: {
41 | type: 'outer',
42 | content: '{name} {percentage}',
43 | },
44 | height: 382,
45 | interactions: [{ type: 'pie-legend-active' }],
46 | };
47 |
48 | return (
49 |
54 |
55 | 全部
56 |
57 |
58 | 线上
59 |
60 |
61 | 门店
62 |
63 |
64 | }
65 | >
66 | 销售额
67 |
68 |
69 | )
70 | }
71 |
72 | export default ProportionSales;
73 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/sales-card/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .salesExtra {
4 | display: inline-block;
5 | margin-right: 24px;
6 | }
7 |
8 | .salesRank {
9 | padding: 0 32px 32px 72px;
10 | }
11 |
12 |
13 | .rankingList {
14 | margin: 25px 0 0;
15 | padding: 0;
16 | list-style: none;
17 |
18 | li {
19 | display: flex;
20 | align-items: center;
21 | margin-top: 16px;
22 | zoom: 1;
23 |
24 | &::before,
25 | &::after {
26 | display: table;
27 | content: ' ';
28 | }
29 | &::after {
30 | clear: both;
31 | height: 0;
32 | font-size: 0;
33 | visibility: hidden;
34 | }
35 |
36 | span {
37 | color: @text-color;
38 | font-size: 14px;
39 | line-height: 22px;
40 | }
41 |
42 | .rankingItemNumber {
43 | display: inline-block;
44 | width: 20px;
45 | height: 20px;
46 | margin-top: 1.5px;
47 | margin-right: 16px;
48 | font-weight: 600;
49 | font-size: 12px;
50 | line-height: 20px;
51 | text-align: center;
52 | background-color: @tag-default-bg;
53 | border-radius: 20px;
54 |
55 | &.active {
56 | color: #fff;
57 | background-color: #314659;
58 | }
59 | }
60 |
61 | .rankingItemTitle {
62 | flex: 1;
63 | margin-right: 8px;
64 | overflow: hidden;
65 | white-space: nowrap;
66 | text-overflow: ellipsis;
67 | }
68 | }
69 | }
70 |
71 | @media screen and (max-width: @screen-lg) {
72 | .salesExtra {
73 | display: none;
74 | }
75 |
76 | .rankingList {
77 | li {
78 | span:first-child {
79 | margin-right: 8px;
80 | }
81 | }
82 | }
83 | }
84 |
85 | @media screen and (max-width: @screen-md) {
86 | .rankingTitle {
87 | margin-top: 16px;
88 | }
89 |
90 | .salesCard .salesBar {
91 | padding: 16px;
92 | }
93 | }
94 |
95 | @media screen and (max-width: @screen-sm) {
96 | .salesExtraWrap {
97 | display: none;
98 | }
99 |
100 | .salesCard {
101 | :global {
102 | .ant-tabs-content {
103 | padding-top: 30px;
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/sales-card/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col } from 'antd';
3 | import numeral from 'numeral';
4 | import Card from '@ant-design/pro-card';
5 | import { DaysRange } from '@alitajs/antd-plus';
6 | import { Column } from '@pansy/react-charts';
7 | import styles from './index.less';
8 |
9 | interface SalesCardProps {
10 | data?: { time: string; value: number }[];
11 | }
12 |
13 | interface RankData {
14 | title: string;
15 | total: number;
16 | }
17 |
18 | interface RankProps {
19 | title?: string;
20 | list?: RankData[];
21 | }
22 |
23 | const rankingListData: RankData[] = [];
24 | for (let i = 0; i < 7; i += 1) {
25 | rankingListData.push({
26 | title: `工专路 ${i} 号店`,
27 | total: 323234,
28 | });
29 | }
30 |
31 | const Rank: React.FC = ({
32 | title,
33 | list = []
34 | }) => {
35 | return (
36 |
37 |
38 | {title}
39 |
40 |
41 | {list.map((item, i) => (
42 | -
43 |
44 | {i + 1}
45 |
46 |
47 | {item.title}
48 |
49 | {numeral(item.total).format('0,0')}
50 |
51 | ))}
52 |
53 |
54 | )
55 | }
56 |
57 | const SalesCard: React.FC = ({
58 | data = []
59 | }) => {
60 | return (
61 |
71 | )
72 | }}
73 | >
74 |
75 |
76 |
77 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | )
114 | }
115 |
116 | export default SalesCard;
117 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/top-search/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col, Statistic, Table } from 'antd';
3 | import Card from '@ant-design/pro-card';
4 | import { TinyArea } from '@pansy/react-charts';
5 | import Trend from '../trend';
6 |
7 | interface SearchData {
8 | index: number,
9 | keyword: string;
10 | count: number;
11 | range: number;
12 | status: number;
13 | }
14 |
15 | const searchData: SearchData[] = [];
16 | for (let i = 0; i < 50; i += 1) {
17 | searchData.push({
18 | index: i + 1,
19 | keyword: `搜索关键词-${i}`,
20 | count: Math.floor(Math.random() * 1000),
21 | range: Math.floor(Math.random() * 100),
22 | status: Math.floor((Math.random() * 10) % 2),
23 | });
24 | }
25 |
26 | const TopSearch: React.FC = () => {
27 | const config = {
28 | data: new Array(50).fill(0).map(() => Math.random() * 100),
29 | smooth: true,
30 | lineStyle: {
31 | lineDash: [2, 2],
32 | stroke: 'l(0) 0:#ffffff 0.5:#7ec2f3 1:#1890ff',
33 | },
34 | areaStyle: { fill: 'l(0) 0:#ffffff 0.5:#7ec2f3 1:#1890ff' },
35 | };
36 |
37 | const columns = [
38 | {
39 | title: '排名',
40 | dataIndex: 'index',
41 | key: 'index',
42 | },
43 | {
44 | title: '搜索关键词',
45 | dataIndex: 'keyword',
46 | key: 'keyword',
47 | render: (text: React.ReactNode) => {text},
48 | },
49 | {
50 | title: '用户数',
51 | dataIndex: 'count',
52 | key: 'count',
53 | sorter: (a: { count: number }, b: { count: number }) => a.count - b.count,
54 | },
55 | {
56 | title: '周涨幅',
57 | dataIndex: 'range',
58 | key: 'range',
59 | sorter: (a: { range: number }, b: { range: number }) => a.range - b.range,
60 | render: (text: React.ReactNode, record: { status: number }) => (
61 |
62 | {text}%
63 |
64 | ),
65 | },
66 | ];
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | rowKey={(record) => record.index}
87 | size="small"
88 | columns={columns}
89 | dataSource={searchData}
90 | pagination={{
91 | style: { marginBottom: 0 },
92 | pageSize: 5,
93 | }}
94 | />
95 |
96 | )
97 | }
98 |
99 | export default TopSearch;
100 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/trend/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .trendItem {
4 | display: inline-block;
5 | font-size: @font-size-base;
6 | line-height: 22px;
7 |
8 | .up,
9 | .down {
10 | position: relative;
11 | top: 1px;
12 | margin-left: 4px;
13 | span {
14 | font-size: 12px;
15 | transform: scale(0.83);
16 | }
17 | }
18 | .up {
19 | color: @red-6;
20 | }
21 | .down {
22 | top: -1px;
23 | color: @green-6;
24 | }
25 |
26 | &.trendItemGrey .up,
27 | &.trendItemGrey .down {
28 | color: @text-color;
29 | }
30 |
31 | &.reverseColor .up {
32 | color: @green-6;
33 | }
34 | &.reverseColor .down {
35 | color: @red-6;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/trend/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import CaretUpOutlined from '@ant-design/icons/CaretUpOutlined';
4 | import CaretDownOutlined from '@ant-design/icons/CaretDownOutlined';
5 | import styles from './index.less';
6 |
7 | export interface TrendProps {
8 | className?: string;
9 | style?: React.CSSProperties;
10 | flag: 'up' | 'down';
11 | colorful?: boolean;
12 | reverseColor?: boolean;
13 | }
14 |
15 | const Trend: React.FC = ({
16 | className,
17 | style,
18 | flag,
19 | colorful,
20 | reverseColor,
21 | children
22 | }) => {
23 | const cls = classNames(
24 | styles.trendItem,
25 | {
26 | [styles.trendItemGrey]: !colorful,
27 | [styles.reverseColor]: reverseColor && colorful,
28 | },
29 | className,
30 | );
31 |
32 | return (
33 |
34 | {children}
35 | {flag && (
36 |
37 | {flag === 'up' ? : }
38 |
39 | )}
40 |
41 | )
42 | }
43 |
44 | Trend.defaultProps = {
45 | colorful: true,
46 | reverseColor: false
47 | }
48 |
49 | export default Trend;
50 |
--------------------------------------------------------------------------------
/src/pages/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { Space, Row, Col } from 'antd';
3 | import { useRequest } from 'umi';
4 | import { GridContent, PageLoading } from '@ant-design/pro-layout';
5 | import { fetchChartData } from '@/services/dashboard';
6 |
7 | const IntroduceRow = React.lazy(() => import('./components/introduce-row'));
8 | const SalesCard = React.lazy(() => import('./components/sales-card'));
9 | const TopSearch = React.lazy(() => import('./components/top-search'));
10 | const ProportionSales = React.lazy(() => import('./components/proportion-sales'));
11 |
12 | const Dashboard: React.FC = () => {
13 | const { data, loading } = useRequest(
14 | () => { return fetchChartData() }
15 | )
16 |
17 | return (
18 |
19 |
20 | }>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export default Dashboard;
46 |
--------------------------------------------------------------------------------
/src/pages/exception/403.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'umi';
2 | import { Result, Button } from 'antd';
3 | import React from 'react';
4 |
5 | export default () => (
6 |
15 |
16 |
17 | }
18 | />
19 | );
20 |
--------------------------------------------------------------------------------
/src/pages/exception/404.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'umi';
2 | import { Result, Button } from 'antd';
3 | import React from 'react';
4 |
5 | export default () => (
6 |
15 |
16 |
17 | }
18 | />
19 | );
20 |
--------------------------------------------------------------------------------
/src/pages/exception/500.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'umi';
2 | import { Result, Button } from 'antd';
3 | import React from 'react';
4 |
5 | export default () => (
6 |
15 |
16 |
17 | }
18 | />
19 | );
20 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/components/cluster/index.less:
--------------------------------------------------------------------------------
1 | .markers {
2 | position: relative;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | color: #fff;
7 | height: 48px;
8 | width: 48px;
9 | border-radius: 50%;
10 | border: solid 4px #fff;
11 | cursor: pointer;
12 | background: #217ad9;
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/components/cluster/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './index.less';
3 |
4 | export interface ClusterProps {
5 | count: number;
6 | }
7 |
8 | export const Cluster: React.FC = ({ count }) => {
9 | return (
10 |
11 | {count}
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/components/index.tsx:
--------------------------------------------------------------------------------
1 | export { Marker } from './marker';
2 | export { Cluster } from './cluster';
3 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/components/marker/index.less:
--------------------------------------------------------------------------------
1 | .marker {
2 | position: relative;
3 | text-align: center;
4 | cursor: pointer;
5 | width: 32px;
6 | height: 32px;
7 | opacity: 1;
8 | border-radius: 50%;
9 | font-size: 14px;
10 | color: #fff;
11 | border: 4px solid #fff;
12 | background: #1dccbb;
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/components/marker/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ShopOutlined from '@ant-design/icons/ShopOutlined';
3 |
4 | import styles from './index.less';
5 |
6 | // 聚合标记点
7 | export const Marker: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { PageContainer } from '@ant-design/pro-layout';
3 | import { Map, ControlBar, MarkerCluster } from '@pansy/react-amap';
4 | import { Marker, Cluster } from './components';
5 | import './styles.less';
6 |
7 | const randomMarker = (len: number) => (
8 | Array(len).fill(true).map(() => ({
9 | lnglat: [ 100 + Math.random() * 30, 30 + Math.random() * 20,]
10 | })) as AMap.MarkerCluster.DataOptions[]
11 | );
12 |
13 | export default () => {
14 | const [markers] = useState(randomMarker(100));
15 |
16 | return (
17 |
18 |
19 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/libraries/amap/styles.less:
--------------------------------------------------------------------------------
1 | .map-markers {
2 | position: relative;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | color: #fff;
7 | height: 48px;
8 | width: 48px;
9 | border-radius: 50%;
10 | border: solid 4px #fff;
11 | cursor: pointer;
12 | background: #217ad9;
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/libraries/watermark/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import moment from 'moment';
3 | import { useInterval } from '@pansy/react-hooks'
4 | import Watermark from '@pansy/react-watermark';
5 |
6 | export default () => {
7 | const [watermarkTexts, setWatermarkTexts] = useState(
8 | ['王某某 6909', moment().format('YYYY-MM-DD HH:mm:ss')]
9 | );
10 |
11 | useInterval(() => {
12 | setWatermarkTexts((texts) => {
13 | texts[1] = moment().format('YYYY-MM-DD HH:mm:ss');
14 |
15 | return texts;
16 | })
17 | }, 3 * 1000);
18 |
19 | return (
20 |
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/login/components/captcha.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Input } from 'antd';
3 | import { InputProps } from 'antd/es/input';
4 | import { SendCode } from '@alitajs/antd-plus';
5 | import isFunction from 'lodash/isFunction';
6 | import { LoginItemProps } from './item';
7 |
8 | interface CaptchaProps extends InputProps {
9 | onCaptcha?: LoginItemProps['onCaptcha']
10 | }
11 |
12 | const Captcha: React.FC = ({
13 | onCaptcha,
14 | ...restProps
15 | }) => {
16 | const [start, setStart] = useState(false);
17 | const [timing, setTiming] = useState(false);
18 |
19 | const handleClick = (e: React.MouseEvent) => {
20 | e.stopPropagation();
21 | e.preventDefault();
22 | if (onCaptcha && isFunction(onCaptcha)) {
23 | onCaptcha()
24 | ?.then((result) => {
25 | setStart(result);
26 | })
27 | setTiming(true);
28 | }
29 | }
30 |
31 | const handleEnd = () => {
32 | setStart(false);
33 | }
34 |
35 | return (
36 |
46 | }
47 | />
48 | )
49 | }
50 |
51 | export default Captcha;
52 |
--------------------------------------------------------------------------------
/src/pages/login/components/context.tsx:
--------------------------------------------------------------------------------
1 | import { Context, 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: Context = createContext({});
12 |
13 | export default LoginContext;
14 |
--------------------------------------------------------------------------------
/src/pages/login/components/index.less:
--------------------------------------------------------------------------------
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-input-prefix {
12 | margin-right: 6px;
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 | .submit {
45 | width: 100%;
46 | margin-top: 24px;
47 | }
48 | }
49 |
50 | .prefixIcon {
51 | color: @disabled-color;
52 | font-size: 16px;
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/login/components/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FC,
3 | ReactElement,
4 | CSSProperties,
5 | FunctionComponent,
6 | ReactComponentElement,
7 | Children,
8 | useState
9 | } from 'react';
10 | import useMergeValue from 'use-merge-value';
11 | import classNames from 'classnames';
12 | import { Tabs, Form } from 'antd';
13 | import { FormInstance } from 'antd/es/form';
14 | import { LoginParamsType } from '@/common/types/login';
15 | import LoginContext from './context';
16 | import LoginItem, { LoginItemProps } from './item';
17 | import LoginSubmit from './submit';
18 | import LoginTab from './tab';
19 | import styles from './index.less';
20 |
21 | export interface LoginProps {
22 | className?: string;
23 | style?: CSSProperties;
24 | activeKey?: string;
25 | onTabChange?: (key: string) => void;
26 | onSubmit?: (values: LoginParamsType) => void;
27 | form?: FormInstance;
28 | children: ReactElement[];
29 | }
30 |
31 | interface LoginType extends FC {
32 | Tab: typeof LoginTab;
33 | Submit: typeof LoginSubmit;
34 | Username: FunctionComponent;
35 | Password: FunctionComponent;
36 | Mobile: FunctionComponent;
37 | Captcha: FunctionComponent;
38 | }
39 |
40 | const Login: LoginType = (props) => {
41 | const { className } = props;
42 | const [tabs, setTabs] = useState([]);
43 | const [active, setActive] = useState({});
44 | const [type, setType] = useMergeValue('', {
45 | value: props.activeKey,
46 | onChange: props.onTabChange,
47 | });
48 | const TabChildren: ReactComponentElement[] = [];
49 | const otherChildren: ReactElement[] = [];
50 | Children.forEach(
51 | props.children,
52 | (child: ReactComponentElement | ReactElement) => {
53 | if (!child) {
54 | return;
55 | }
56 | if ((child.type as { typeName: string }).typeName === 'LoginTab') {
57 | TabChildren.push(child as ReactComponentElement);
58 | } else {
59 | otherChildren.push(child);
60 | }
61 | },
62 | );
63 | return (
64 | {
68 | setTabs([...tabs, id]);
69 | },
70 | removeTab: (id) => {
71 | setTabs(tabs.filter((currentId) => currentId !== id));
72 | },
73 | },
74 | updateActive: (activeItem) => {
75 | if (!active) return;
76 | if (active[type]) {
77 | active[type].push(activeItem);
78 | } else {
79 | active[type] = [activeItem];
80 | }
81 | setActive(active);
82 | },
83 | }}
84 | >
85 |
86 |
113 |
114 |
115 | );
116 | };
117 |
118 | Login.Tab = LoginTab;
119 | Login.Submit = LoginSubmit;
120 |
121 | Login.Username = LoginItem.Username;
122 | Login.Password = LoginItem.Password;
123 | Login.Mobile = LoginItem.Mobile;
124 | Login.Captcha = LoginItem.Captcha;
125 |
126 | export default Login;
127 |
--------------------------------------------------------------------------------
/src/pages/login/components/item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input, Form } from 'antd';
3 | import { FormItemProps } from 'antd/es/form/FormItem';
4 | import ItemMap from './map';
5 | import LoginContext, { LoginContextProps } from './context';
6 | import Captcha from './captcha';
7 |
8 | export type WrappedLoginItemProps = LoginItemProps;
9 | export type LoginItemKeyType = keyof typeof ItemMap;
10 | export interface LoginItemType {
11 | Username: React.FC;
12 | Password: React.FC;
13 | Mobile: React.FC;
14 | Captcha: React.FC;
15 | }
16 |
17 | export interface LoginItemProps extends Partial {
18 | type?: string;
19 | placeholder?: string;
20 | defaultValue?: string;
21 | customProps?: { [key: string]: unknown };
22 | tabUtil?: LoginContextProps['tabUtil'];
23 | updateActive?: LoginContextProps['updateActive'];
24 | onChange?: (e: React.ChangeEvent) => void;
25 | /**
26 | * 获取验证码回调
27 | */
28 | onCaptcha?: () => Promise;
29 | }
30 |
31 | /**
32 | * 获取表单项的参数
33 | * @param params
34 | */
35 | const getFormItemOptions = ({
36 | onChange,
37 | defaultValue,
38 | customProps = {},
39 | rules,
40 | }: LoginItemProps) => {
41 | const options: {
42 | rules?: LoginItemProps['rules'];
43 | onChange?: LoginItemProps['onChange'];
44 | initialValue?: LoginItemProps['defaultValue'];
45 | } = {
46 | rules: rules || (customProps.rules as LoginItemProps['rules']),
47 | };
48 | if (onChange) {
49 | options.onChange = onChange;
50 | }
51 | if (defaultValue) {
52 | options.initialValue = defaultValue;
53 | }
54 | return options;
55 | };
56 |
57 | const LoginItem: React.FC = (props) => {
58 | const {
59 | onChange,
60 | customProps,
61 | defaultValue,
62 | rules,
63 | name,
64 | updateActive,
65 | type,
66 | tabUtil,
67 | ...restProps
68 | } = props;
69 |
70 | if (!name) {
71 | return null;
72 | }
73 | // get getFieldDecorator props
74 | const options = getFormItemOptions(props);
75 | const otherProps = restProps || {};
76 |
77 | if (type === 'Captcha') {
78 | return (
79 |
80 |
81 |
82 | );
83 | }
84 | return (
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | const LoginItems: Partial = {};
92 |
93 | Object.keys(ItemMap).forEach((key) => {
94 | const item = ItemMap[key];
95 | LoginItems[key] = (props: LoginItemProps) => (
96 |
97 | {(context) => (
98 |
106 | )}
107 |
108 | );
109 | });
110 |
111 | export default LoginItems as LoginItemType;
112 |
--------------------------------------------------------------------------------
/src/pages/login/components/map.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import UserOutlined from '@ant-design/icons/UserOutlined';
3 | import LockOutlined from '@ant-design/icons/LockOutlined';
4 | import MailOutlined from '@ant-design/icons/MailOutlined';
5 | import MobileOutlined from '@ant-design/icons/MobileOutlined';
6 | import styles from './index.less';
7 |
8 | export default {
9 | Username: {
10 | props: {
11 | id: 'username',
12 | size: 'large',
13 | prefix: ,
14 | placeholder: 'admin',
15 | },
16 | rules: [
17 | {
18 | required: true,
19 | message: 'Please enter username!',
20 | },
21 | ],
22 | },
23 | Password: {
24 | props: {
25 | id: 'password',
26 | size: 'large',
27 | type: 'password',
28 | prefix: ,
29 | placeholder: '123456',
30 | },
31 | rules: [
32 | {
33 | required: true,
34 | message: 'Please enter password!',
35 | },
36 | ],
37 | },
38 | Mobile: {
39 | props: {
40 | id: 'mobile',
41 | size: 'large',
42 | prefix: ,
43 | placeholder: 'mobile number',
44 | },
45 | rules: [
46 | {
47 | required: true,
48 | message: 'Please enter mobile number!',
49 | },
50 | {
51 | pattern: /^1\d{10}$/,
52 | message: 'Wrong mobile number format!',
53 | },
54 | ],
55 | },
56 | Captcha: {
57 | props: {
58 | id: 'captcha',
59 | size: 'large',
60 | prefix: ,
61 | placeholder: 'captcha',
62 | },
63 | rules: [
64 | {
65 | required: true,
66 | message: 'Please enter Captcha!',
67 | },
68 | ],
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/src/pages/login/components/submit.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { Button, Form } from 'antd';
3 | import { ButtonProps } from 'antd/es/button';
4 | import classNames from 'classnames';
5 | import styles from './index.less';
6 |
7 | const LoginSubmit: FC = ({ className, ...rest }) => {
8 | const clsString = classNames(styles.submit, className);
9 |
10 | return (
11 |
12 |
19 |
20 | );
21 | }
22 |
23 | export default LoginSubmit;
24 |
--------------------------------------------------------------------------------
/src/pages/login/components/tab.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect } from 'react';
2 | import { Tabs } from 'antd';
3 | import LoginContext, { LoginContextProps } from './context';
4 |
5 | const { TabPane } = Tabs;
6 |
7 | const generateId = (() => {
8 | let i = 0;
9 | return (prefix = '') => {
10 | i += 1;
11 | return `${prefix}${i}`;
12 | };
13 | })();
14 |
15 | type TabPaneProps = Parameters[0];
16 |
17 | interface LoginTabProps extends TabPaneProps {
18 | tabUtil: LoginContextProps['tabUtil'];
19 | active?: boolean;
20 | }
21 |
22 | const LoginTab: FC = (props) => {
23 | useEffect(() => {
24 | const uniqueId = generateId('login-tab-');
25 | const { tabUtil } = props;
26 | if (tabUtil) {
27 | tabUtil.addTab(uniqueId);
28 | }
29 | }, []);
30 | const { children } = props;
31 | return {props.active && children};
32 | };
33 |
34 | const WrapContext: FC & {
35 | typeName: string;
36 | } = (props) => (
37 |
38 | {(value) => }
39 |
40 | );
41 |
42 | WrapContext.typeName = 'LoginTab';
43 |
44 | export default WrapContext;
45 |
--------------------------------------------------------------------------------
/src/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import AlipayCircleOutlined from '@ant-design/icons/AlipayCircleOutlined';
3 | import TaobaoCircleOutlined from '@ant-design/icons/TaobaoCircleOutlined';
4 | import WeiboCircleOutlined from '@ant-design/icons/WeiboCircleOutlined';
5 | import { Alert, Checkbox, message, Form } from 'antd';
6 | import { Link, history, History, useModel, useRequest } from 'umi';
7 | import UserLayout from '@/layouts/user-layout';
8 | import { LoginParamsType } from '@/common/types/login';
9 | import LoginForm from './components';
10 | import { fetchLogin, fetchCaptcha } from '@/services/login';
11 | import { setCookie } from '@/utils/cookie';
12 | import styles from './style.less';
13 |
14 | const {
15 | Tab,
16 | Username,
17 | Password,
18 | Mobile,
19 | Captcha,
20 | Submit
21 | } = LoginForm;
22 |
23 | const LoginMessage: React.FC<{ content: string;}> = ({ content }) => (
24 |
32 | );
33 |
34 | /**
35 | * 此方法会跳转到 redirect 参数所在的位置
36 | */
37 | const replaceGoto = () => {
38 | setTimeout(() => {
39 | const { query } = history.location;
40 | const { redirect } = query as { redirect: string };
41 | if (!redirect) {
42 | history.replace('/');
43 | return;
44 | }
45 | (history as History).replace(redirect);
46 | }, 10);
47 | };
48 |
49 | const Login: React.FC = () => {
50 | const [userLoginState, setUserLoginState] = useState({});
51 | const { initialState, setInitialState } = useModel('@@initialState');
52 | const [autoLogin, setAutoLogin] = useState(true);
53 | const [form] = Form.useForm();
54 | const [type, setType] = useState('account');
55 |
56 | const loginRequest = useRequest(
57 | (values: LoginParamsType) => {
58 | return fetchLogin({ ...values, type })
59 | },
60 | {
61 | manual: true,
62 | onSuccess: (data) => {
63 | if (data.token) {
64 | loginSuccess(data.token);
65 | return;
66 | }
67 | setUserLoginState(data);
68 | }
69 | }
70 | );
71 |
72 | const loginSuccess = async (token: string) => {
73 | if (initialState && token) {
74 | message.success('登录成功!');
75 | setCookie(token);
76 | const currentUser = await initialState?.fetchUserInfo();
77 | setInitialState({
78 | ...initialState,
79 | currentUser,
80 | });
81 | replaceGoto();
82 | }
83 | }
84 |
85 | const handleCaptcha = async () => {
86 | const { mobile } = await form.validateFields(['mobile']);
87 |
88 | if (mobile) {
89 | const result = await fetchCaptcha(mobile);
90 | if (result?.code === 200) {
91 | return true;
92 | }
93 | }
94 |
95 | return false;
96 | }
97 |
98 | const { status, type: loginType } = userLoginState;
99 |
100 | const handleSubmit = (values: LoginParamsType) => {
101 | loginRequest.run(values);
102 | };
103 |
104 | return (
105 |
106 |
107 |
108 |
109 | {status === 'error' && loginType === 'account' && !loginRequest.loading && (
110 |
111 | )}
112 |
113 |
123 |
133 |
134 |
135 | {status === 'error' && loginType === 'mobile' && !loginRequest.loading && (
136 |
137 | )}
138 |
152 |
163 |
164 |
165 |
setAutoLogin(e.target.checked)}>
166 | 自动登录
167 |
168 |
173 | 忘记密码
174 |
175 |
176 | 登录
177 |
178 | 其他登录方式
179 |
180 |
181 |
182 |
183 | 注册账户
184 |
185 |
186 |
187 |
188 |
189 | );
190 | };
191 |
192 | export default Login;
193 |
--------------------------------------------------------------------------------
/src/pages/login/style.less:
--------------------------------------------------------------------------------
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 | }
40 |
--------------------------------------------------------------------------------
/src/pages/nested/menu1/menu1-1.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 |
5 | const Nested: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default Nested;
14 |
--------------------------------------------------------------------------------
/src/pages/nested/menu1/menu1-2/menu1-2-1.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 |
5 | const Nested: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default Nested;
14 |
--------------------------------------------------------------------------------
/src/pages/nested/menu1/menu1-2/menu1-2-2.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 |
5 | const Nested: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default Nested;
14 |
--------------------------------------------------------------------------------
/src/pages/nested/menu1/menu1-3.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 |
5 | const Nested: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default Nested;
14 |
--------------------------------------------------------------------------------
/src/pages/nested/menu2/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 |
5 | const Nested: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default Nested;
14 |
--------------------------------------------------------------------------------
/src/pages/permission/action/components/operation.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Select,
4 | Input,
5 | } from '@formily/antd';
6 | import { FormProvider, Field } from '@formily/react'
7 | import { createForm } from '@formily/core'
8 | import { useRequest } from 'umi';
9 | import { Button, Drawer } from 'antd';
10 | import { UseModalResult } from '@pansy/react-hooks';
11 | import { createAction } from '@/services/permission';
12 | import { modules } from '../../constant';
13 |
14 | const TextArea = Input.TextArea;
15 |
16 | const Operation: React.FC = ({
17 | visible,
18 | close
19 | }) => {
20 | const form = createForm()
21 |
22 | const request = useRequest(
23 | (data: API.PermissionActionData) => {
24 | return createAction(data);
25 | },
26 | {
27 | manual: true,
28 | onSuccess: () => {
29 | console.log('创建成功!');
30 | }
31 | }
32 | );
33 |
34 | const handleSubmit = () => {
35 | form.submit((values) => {
36 | request.run(values);
37 | });
38 | }
39 |
40 | const handleCancel = () => {
41 | form.reset();
42 | close?.();
43 | }
44 |
45 | return (
46 |
58 |
61 |
64 |
65 | }
66 | >
67 |
70 |
82 |
83 |
98 |
99 |
112 |
113 |
123 |
124 |
125 | )
126 | }
127 |
128 | export default Operation;
129 |
--------------------------------------------------------------------------------
/src/pages/permission/action/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 | import Table, { ProColumns } from '@ant-design/pro-table';
5 | import { useModal } from '@pansy/react-hooks';
6 | import { fetchActionList } from '@/services/permission';
7 | import { handleTableRequest } from '@/utils';
8 | import Operation from './components/operation';
9 |
10 | const columns: ProColumns[] = [
11 | {
12 | title: '所属模块',
13 | dataIndex: 'module'
14 | },
15 | {
16 | title: '操作标识',
17 | dataIndex: 'code'
18 | },
19 | {
20 | title: '名称',
21 | dataIndex: 'name'
22 | },
23 | {
24 | title: '创建时间',
25 | dataIndex: 'createdAt',
26 | valueType: 'dateTime'
27 | },
28 | {
29 | title: '创建时间',
30 | dataIndex: 'updatedAt',
31 | valueType: 'dateTime'
32 | },
33 | {
34 | title: '操作',
35 | width: 180,
36 | key: 'option',
37 | valueType: 'option',
38 | render: () => [
39 | 修改
40 | ]
41 | },
42 | ]
43 |
44 | export default () => {
45 | const modal = useModal();
46 |
47 | const handleClick = () => {
48 | modal.open();
49 | }
50 |
51 | return (
52 |
53 |
54 | columns={columns}
55 | request={(params, sorter, filter) => {
56 | // @ts-ignore
57 | return handleTableRequest(params, sorter, filter, fetchActionList);
58 | }}
59 | rowKey="id"
60 | pagination={{
61 | showQuickJumper: true,
62 | pageSize: 10
63 | }}
64 | search={false}
65 | headerTitle="操作列表"
66 | toolBarRender={() => [
67 | ,
68 | ]}
69 | />
70 |
71 |
72 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/src/pages/permission/button/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Authority, useAuthority } from 'umi';
3 | import { Radio, Divider, Alert, Tag, Space } from 'antd';
4 | import { RadioChangeEvent } from 'antd/es/radio/interface';
5 | import { PageContainer } from '@ant-design/pro-layout';
6 |
7 | export default () => {
8 | const [role, setRole] = useState('admin');
9 |
10 | const { singleVerify } = useAuthority();
11 |
12 | const showList = singleVerify('dashboard:list')
13 |
14 | useEffect(
15 | () => {
16 | if (showList) {
17 | // 请求数据
18 |
19 | }
20 | },
21 | [showList]
22 | )
23 |
24 | const handleChange = (e: RadioChangeEvent) => {
25 | setRole(e.target.value);
26 | }
27 |
28 | return (
29 |
30 |
31 | 权限切换:
32 |
33 |
34 | user
35 |
36 |
37 | admin
38 |
39 |
40 |
41 |
42 |
43 |
44 | Only admin can see this } />
45 |
46 |
47 | Only user can see this } />
48 |
49 |
50 | Both admin and user can see this } />
51 |
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/pages/permission/constant.ts:
--------------------------------------------------------------------------------
1 | export const modules = [
2 | { value: 'dashboard', label: '首页' },
3 | { value: 'user', label: '用户管理' },
4 | { value: 'permission', label: '权限管理' },
5 | { value: 'role', label: '角色管理' },
6 | ];
7 |
--------------------------------------------------------------------------------
/src/pages/permission/page/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Radio } from 'antd';
3 | import { RadioChangeEvent } from 'antd/es/radio/interface';
4 | import { PageContainer } from '@ant-design/pro-layout';
5 |
6 | export default () => {
7 | const [role, setRole] = useState('admin');
8 |
9 | const handleChange = (e: RadioChangeEvent) => {
10 | setRole(e.target.value);
11 | }
12 |
13 | return (
14 |
15 |
16 | 权限切换
17 |
18 |
19 | user
20 |
21 |
22 | admin
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/pages/permission/policy/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PageContainer } from '@ant-design/pro-layout';
3 |
4 | export default () => {
5 | return (
6 |
7 | Policy list page
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/pages/register/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useRequest, history } from 'umi';
3 | import { Form, Button, Input, Popover, Progress, Select, message } from 'antd';
4 | import { Store } from 'antd/es/form/interface';
5 | import LockOutlined from '@ant-design/icons/LockOutlined';
6 | import UserLayout from '@/layouts/user-layout';
7 | import { fakeRegister, fetchCaptcha } from '@/services/login';
8 | import LoginForm from '../login/components';
9 | import styles from './style.less';
10 |
11 | const FormItem = Form.Item;
12 | const { Option } = Select;
13 | const { Group } = Input;
14 | const { Captcha } = LoginForm;
15 |
16 | const passwordStatusMap = {
17 | ok: (
18 |
19 | 强度:强
20 |
21 | ),
22 | pass: (
23 |
24 | 强度:中
25 |
26 | ),
27 | poor: (
28 |
29 | 强度:太短
30 |
31 | ),
32 | };
33 |
34 | const passwordProgressMap: {
35 | ok: 'success';
36 | pass: 'normal';
37 | poor: 'exception';
38 | } = {
39 | ok: 'success',
40 | pass: 'normal',
41 | poor: 'exception',
42 | };
43 |
44 | export default () => {
45 | const [visible, setVisible] = useState(false);
46 | const [prefix, setPrefix] = useState('86');
47 | const [popover, setPopover] = useState(false);
48 | const confirmDirty = false;
49 | const [form] = Form.useForm();
50 |
51 | const getPasswordStatus = () => {
52 | const value = form.getFieldValue('password');
53 | if (value && value.length > 9) {
54 | return 'ok';
55 | }
56 | if (value && value.length > 5) {
57 | return 'pass';
58 | }
59 | return 'poor';
60 | };
61 |
62 | const handleCaptcha = async () => {
63 | const { mobile } = await form.validateFields(['mobile']);
64 |
65 | if (mobile) {
66 | const result = await fetchCaptcha(mobile);
67 | if (result?.code === 200) {
68 | return true;
69 | }
70 | }
71 |
72 | return false;
73 | }
74 |
75 | const { loading: submitting, run: register } = useRequest<{ data: any }>(fakeRegister, {
76 | manual: true,
77 | onSuccess: (data, params) => {
78 | if (data.status === 'ok') {
79 | message.success('注册成功!');
80 | history.push({
81 | pathname: '/user/register-result',
82 | state: {
83 | account: params.email,
84 | },
85 | });
86 | }
87 | },
88 | });
89 | const onFinish = (values: Store) => {
90 | register(values);
91 | };
92 |
93 | const checkConfirm = (_: any, value: string) => {
94 | const promise = Promise;
95 | if (value && value !== form.getFieldValue('password')) {
96 | return promise.reject('两次输入的密码不匹配!');
97 | }
98 | return promise.resolve();
99 | };
100 |
101 | const checkPassword = (_: any, value: string) => {
102 | const promise = Promise;
103 | // 没有值的情况
104 | if (!value) {
105 | setVisible(!!value);
106 | return promise.reject('请输入密码!');
107 | }
108 | // 有值的情况
109 | if (!visible) {
110 | setVisible(!!value);
111 | }
112 | setPopover(!popover);
113 | if (value.length < 6) {
114 | return promise.reject('');
115 | }
116 | if (value && confirmDirty) {
117 | form.validateFields(['confirm']);
118 | }
119 | return promise.resolve();
120 | };
121 |
122 | const changePrefix = (value: string) => {
123 | setPrefix(value);
124 | };
125 |
126 | const renderPasswordProgress = () => {
127 | const value = form.getFieldValue('password');
128 | const passwordStatus = getPasswordStatus();
129 | return value && value.length ? (
130 |
131 |
139 | ) : null;
140 | };
141 |
142 | return (
143 |
144 |
256 |
257 | );
258 | };
259 |
--------------------------------------------------------------------------------
/src/pages/register/style.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .main {
4 | width: 368px;
5 | margin: 0 auto;
6 |
7 | .password {
8 | margin-bottom: 24px;
9 | :global {
10 | .ant-form-item-explain {
11 | display: none;
12 | }
13 | }
14 | }
15 |
16 | .submit {
17 | width: 50%;
18 | }
19 |
20 | .login {
21 | float: right;
22 | line-height: @btn-height-lg;
23 | }
24 | }
25 |
26 | .prefixIcon {
27 | color: @disabled-color;
28 | font-size: 16px;
29 | }
30 |
31 | .success,
32 | .warning,
33 | .error {
34 | transition: color 0.3s;
35 | }
36 |
37 | .success {
38 | color: @success-color;
39 | }
40 |
41 | .warning {
42 | color: @warning-color;
43 | }
44 |
45 | .error {
46 | color: @error-color;
47 | }
48 |
49 | .progress-pass > .progress {
50 | :global {
51 | .ant-progress-bg {
52 | background-color: @warning-color;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/pages/system/role/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PageContainer } from '@ant-design/pro-layout';
3 |
4 | export default () => {
5 | return (
6 |
7 | Role page
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/pages/system/user/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'antd';
3 | import { PageContainer } from '@ant-design/pro-layout';
4 | import Table, { ProColumns } from '@ant-design/pro-table';
5 |
6 | const columns: ProColumns[] = [
7 | {
8 | title: '用户名',
9 | dataIndex: 'username'
10 | },
11 | {
12 | title: '用户昵称',
13 | dataIndex: 'nickname'
14 | },
15 | {
16 | title: '邮箱',
17 | dataIndex: 'eamil'
18 | },
19 | {
20 | title: '手机号',
21 | dataIndex: 'mobile'
22 | },
23 | {
24 | title: '创建时间',
25 | dataIndex: 'createTime'
26 | },
27 | {
28 | title: '创建时间',
29 | dataIndex: 'updateTime'
30 | },
31 | {
32 | title: '操作',
33 | width: 180,
34 | key: 'option',
35 | valueType: 'option',
36 | render: () => [
37 | 链路,
38 | 报警,
39 | 监控,
40 | ],
41 | },
42 | ]
43 |
44 | export default () => {
45 | return (
46 |
47 |
48 | columns={columns}
49 | rowKey="id"
50 | pagination={{
51 | showQuickJumper: true,
52 | }}
53 | search={false}
54 | dateFormatter="string"
55 | headerTitle="用户列表"
56 | toolBarRender={() => [
57 | ,
58 | ]}
59 | />
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/src/services/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { request } from 'umi';
2 |
3 | export async function fetchChartData() {
4 | return request(`/api/dashboard/chartData`);
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/login.ts:
--------------------------------------------------------------------------------
1 | import { request } from 'umi';
2 | import { LoginParamsType } from '@/common/types/login';
3 |
4 | /**
5 | * 用户登录
6 | * @param data
7 | */
8 | export async function fetchLogin(data: LoginParamsType) {
9 | return request('/api/user/login', {
10 | method: 'POST',
11 | data
12 | });
13 | }
14 |
15 | /**
16 | * 退出登录
17 | * @param data
18 | */
19 | export async function fetchLogout() {
20 | return request('/api/user/logout', {
21 | method: 'POST',
22 | });
23 | }
24 |
25 | /**
26 | * 用户注册
27 | * @param data
28 | */
29 | export async function fakeRegister(data: any) {
30 | return request('/api/user/register', {
31 | method: 'POST',
32 | data
33 | });
34 | }
35 |
36 | /**
37 | * 获取手机验证码
38 | * @param mobile
39 | */
40 | export async function fetchCaptcha(mobile: string) {
41 | return request(`/api/user/captcha?mobile=${mobile}`);
42 | }
43 |
--------------------------------------------------------------------------------
/src/services/permission.ts:
--------------------------------------------------------------------------------
1 | import { request } from 'umi';
2 |
3 | /**
4 | * 获取操作列表
5 | */
6 | export async function fetchActionList(params: any) {
7 | return request>('/api/permission/action/list', { params });
8 | }
9 |
10 | /**
11 | * 创建操作
12 | */
13 | export async function createAction(data: API.PermissionActionData) {
14 | return request>('/api/permission/action', {
15 | method: 'POST',
16 | data
17 | });
18 | }
19 |
20 | /**
21 | * 创建操作
22 | */
23 | export async function updateAction(data: API.PermissionActionData) {
24 | return request>(`/api/permission/action/${data.id}`, {
25 | method: 'PUT',
26 | data
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/services/user.ts:
--------------------------------------------------------------------------------
1 | import { request } from 'umi';
2 |
3 | /**
4 | * 获取当前登录用户信息
5 | */
6 | export async function fetchCurrent() {
7 | return request>('/api/user/current');
8 | }
9 |
10 | /**
11 | * 获取用户列表
12 | */
13 | export async function fetchList(params: any) {
14 | return request>('/api/user/list', { params });
15 | }
16 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 | declare module '*.scss';
4 | declare module '*.sass';
5 | declare module '*.svg';
6 | declare module '*.png';
7 | declare module '*.jpg';
8 | declare module '*.jpeg';
9 | declare module '*.gif';
10 | declare module '*.bmp';
11 | declare module '*.tiff';
12 |
13 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
14 |
--------------------------------------------------------------------------------
/src/utils/cookie.ts:
--------------------------------------------------------------------------------
1 | import { TOKEN_KEY } from '@/config';
2 |
3 | const cookieKey = TOKEN_KEY;
4 |
5 | export function getCookie() {
6 | return localStorage.getItem(cookieKey);
7 | }
8 |
9 | export function setCookie(value: string) {
10 | return localStorage.setItem(cookieKey, value);
11 | }
12 |
13 | export function removeCookie() {
14 | return localStorage.removeItem(cookieKey);
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/date.ts:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | /**
4 | * 格式化时间
5 | * @param date
6 | * @param format
7 | */
8 | export function formatDate(
9 | date: moment.Moment | Date | string | number,
10 | format = 'YYYY-MM-DD HH:mm:ss'
11 | ): string {
12 | if (!date) return '';
13 | return moment(date).format(format);
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'querystring';
2 | import { RequestData } from '@ant-design/pro-table';
3 | import { SortOrder } from 'antd/lib/table/interface';
4 | import { ParamsType } from '@ant-design/pro-provider';
5 |
6 | export const getPageQuery = () => {
7 | const { href } = window.location;
8 | const qsIndex = href.indexOf('?');
9 | const sharpIndex = href.indexOf('#');
10 |
11 | if (qsIndex !== -1) {
12 | if (qsIndex > sharpIndex) {
13 | return parse(href.split('?')[1]);
14 | }
15 |
16 | return parse(href.slice(qsIndex + 1, sharpIndex));
17 | }
18 |
19 | return {};
20 | };
21 |
22 | /**
23 | * 处理ProTable查询逻辑,以及与接口标准不一致的问题
24 | * @param params
25 | * @param sort
26 | * @param filter
27 | * @param fetchList
28 | */
29 | export function handleTableRequest(
30 | params: U & {
31 | pageSize?: number;
32 | current?: number;
33 | },
34 | sort: {
35 | [key: string]: SortOrder;
36 | } = {},
37 | filter: {
38 | [key: string]: React.ReactText[];
39 | } = {},
40 | fetchList: (params: any) => Promise>
41 | ): Promise> {
42 | return new Promise>((resolve, reject) => {
43 | const { current, pageSize, ...rest } = params || {};
44 |
45 | try {
46 | fetchList({ page: current, size: pageSize, ...rest, ...filter })
47 | .then((resData) => {
48 | const { list = [], total } = resData?.data || {};
49 | resolve({
50 | data: list,
51 | total
52 | })
53 | })
54 | .catch(() => {
55 | resolve({
56 | data: [],
57 | total: 0
58 | })
59 | })
60 | } catch (e) {
61 | resolve({
62 | data: [],
63 | total: 0
64 | });
65 | }
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "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 | "@@/*": ["./src/.umi/*"]
23 | }
24 | },
25 | "exclude": [
26 | "node_modules",
27 | "build",
28 | "dist",
29 | "scripts",
30 | "src/.umi/*",
31 | "webpack",
32 | "jest"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------