├── .editorconfig
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc.js
├── README.md
├── craco.config.js
├── deploy
├── deploy.test.sh
├── machine
│ ├── jenkins.sh
│ └── nginx.conf
├── nginx.conf
├── nginx.sub-app.conf
└── rancher
│ ├── Dockerfile
│ ├── deployment.yaml
│ └── nginx.conf
├── docs
├── .nojekyll
├── AJAX.md
├── GEN.md
├── LAYOUT.md
├── MENU.md
├── MOCK.md
├── MODAL_PAGE.md
├── MODEL.md
├── NGINX.md
├── OTHER.md
├── PAGE.md
├── PERMISSION.md
├── PROXY.md
├── QIANKUN.md
├── README.md
├── ROUTE.md
├── START.md
├── STYLE.md
├── _coverpage.md
├── _sidebar.md
├── build
│ ├── asset-manifest.json
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── robots.txt
│ └── static
│ │ ├── css
│ │ ├── 3.6178f109.chunk.css
│ │ ├── 3.6178f109.chunk.css.map
│ │ ├── 4.185ef8b6.chunk.css
│ │ ├── 4.185ef8b6.chunk.css.map
│ │ ├── 5.185ef8b6.chunk.css
│ │ ├── 5.185ef8b6.chunk.css.map
│ │ ├── 6.9a1388c0.chunk.css
│ │ └── 6.9a1388c0.chunk.css.map
│ │ ├── js
│ │ ├── 0.c00a972e.chunk.js
│ │ ├── 0.c00a972e.chunk.js.map
│ │ ├── 10.515a043b.chunk.js
│ │ ├── 10.515a043b.chunk.js.map
│ │ ├── 11.df3576a8.chunk.js
│ │ ├── 11.df3576a8.chunk.js.map
│ │ ├── 12.99cc42a7.chunk.js
│ │ ├── 12.99cc42a7.chunk.js.map
│ │ ├── 13.bff1e6dc.chunk.js
│ │ ├── 13.bff1e6dc.chunk.js.map
│ │ ├── 14.e62adf89.chunk.js
│ │ ├── 14.e62adf89.chunk.js.map
│ │ ├── 15.510c57f0.chunk.js
│ │ ├── 15.510c57f0.chunk.js.map
│ │ ├── 3.a462f62e.chunk.js
│ │ ├── 3.a462f62e.chunk.js.LICENSE.txt
│ │ ├── 3.a462f62e.chunk.js.map
│ │ ├── 4.dafdb96a.chunk.js
│ │ ├── 4.dafdb96a.chunk.js.map
│ │ ├── 5.0bf26507.chunk.js
│ │ ├── 5.0bf26507.chunk.js.map
│ │ ├── 6.fa51b8dd.chunk.js
│ │ ├── 6.fa51b8dd.chunk.js.map
│ │ ├── 7.019a448a.chunk.js
│ │ ├── 7.019a448a.chunk.js.map
│ │ ├── 8.91f49de0.chunk.js
│ │ ├── 8.91f49de0.chunk.js.map
│ │ ├── 9.5fa78ebf.chunk.js
│ │ ├── 9.5fa78ebf.chunk.js.map
│ │ ├── main.e9dfe8d6.chunk.js
│ │ ├── main.e9dfe8d6.chunk.js.map
│ │ ├── runtime-main.bd4dd1cc.js
│ │ └── runtime-main.bd4dd1cc.js.map
│ │ └── media
│ │ ├── defaultErrorImage.d83d2d78.png
│ │ └── login-bg.3ab1656e.jpg
├── img.png
├── imgs
│ ├── 404.jpg
│ ├── layout-setting.jpg
│ ├── login.jpg
│ ├── logo.ico
│ ├── logo.png
│ ├── menu.jpg
│ ├── role.jpg
│ └── users.jpg
├── index.html
└── lib
│ ├── css
│ └── dark-mode@0.6.1.css
│ ├── docsify.js
│ ├── docsify.min.js
│ ├── js
│ ├── codefund.js
│ ├── dark-mode@0.6.1.js
│ ├── prism-bash.min.js
│ ├── prism-markdown.min.js
│ └── prism-nginx.min.js
│ ├── plugins
│ ├── disqus.js
│ ├── disqus.min.js
│ ├── emoji.js
│ ├── emoji.min.js
│ ├── external-script.js
│ ├── external-script.min.js
│ ├── front-matter.js
│ ├── front-matter.min.js
│ ├── ga.js
│ ├── ga.min.js
│ ├── gitalk.js
│ ├── gitalk.min.js
│ ├── matomo.js
│ ├── matomo.min.js
│ ├── search.js
│ ├── search.min.js
│ ├── zoom-image.js
│ └── zoom-image.min.js
│ └── themes
│ ├── buble.css
│ ├── dark.css
│ ├── dolphin.css
│ ├── pure.css
│ └── vue.css
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.jsx
├── App.less
├── api
│ ├── index.js
│ └── system.js
├── commons
│ ├── ajax.js
│ ├── config-hoc.js
│ ├── handle-error.js
│ ├── handle-success.js
│ └── index.js
├── components
│ ├── error
│ │ └── Error404.jsx
│ ├── footer
│ │ ├── index.jsx
│ │ └── style.less
│ ├── generator
│ │ ├── index.jsx
│ │ └── style.less
│ ├── header
│ │ ├── PasswordModal.js
│ │ ├── index.jsx
│ │ └── style.less
│ ├── icon
│ │ └── index.jsx
│ ├── iframe
│ │ └── index.jsx
│ ├── index.js
│ ├── layout
│ │ ├── index.jsx
│ │ └── layout-setting
│ │ │ ├── SettingPage.jsx
│ │ │ └── index.jsx
│ ├── logo
│ │ ├── index.jsx
│ │ ├── logo.png
│ │ └── style.less
│ ├── permission
│ │ └── index.jsx
│ ├── proxy
│ │ └── index.jsx
│ └── sub-app
│ │ ├── SubError.jsx
│ │ └── index.jsx
├── config
│ ├── config.development.js
│ ├── config.production.js
│ └── index.js
├── index.js
├── mock
│ ├── index.js
│ ├── mock-menus.js
│ ├── mock-roles.js
│ ├── mock-users.js
│ ├── mockdata
│ │ ├── organizations.js
│ │ ├── roles.js
│ │ └── user.js
│ ├── util.js
│ └── web-sql
│ │ ├── index.js
│ │ └── init-sql.js
├── models
│ ├── demo.js
│ ├── index.js
│ └── models.js
├── options
│ ├── index.js
│ └── system.js
├── pages
│ ├── home
│ │ ├── index.jsx
│ │ └── style.less
│ ├── login
│ │ ├── index-full.jsx
│ │ ├── index-portal.jsx
│ │ ├── index.jsx
│ │ ├── login-bg.jpg
│ │ └── style.less
│ ├── menus
│ │ ├── ActionEdit.jsx
│ │ ├── MenuEdit.jsx
│ │ ├── MenuTableSelect.jsx
│ │ ├── SystemSelect.jsx
│ │ ├── index.jsx
│ │ └── style.less
│ ├── page-configs.js
│ ├── role
│ │ ├── EditModal.jsx
│ │ ├── RoleSelectTable.jsx
│ │ └── index.jsx
│ └── user
│ │ ├── EditModal.jsx
│ │ └── index.jsx
├── qiankun.js
├── router
│ ├── AppRouter.jsx
│ └── routes.js
├── setupProxy.js
├── setupProxyConfig.json
├── setupTests.js
└── theme.less
├── webpack.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 4
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 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /deploy/rancher/build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | # log
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # ide
28 | .idea
29 |
30 | # ra-lib
31 | /src/ra-lib
32 | /src/pages/local
33 | /server
34 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | //此处的规则供参考,其中多半其实都是默认值,可以根据个人习惯改写
2 | module.exports = {
3 | printWidth: 120, // 单行长度
4 | tabWidth: 4, // 缩进长度
5 | useTabs: false, // 使用空格代替tab缩进
6 | semi: true, // 句末使用分号
7 | singleQuote: true, // 使用单引号
8 | quoteProps: 'as-needed', // 仅在必需时为对象的key添加引号
9 | jsxSingleQuote: false, // jsx中使用单引号
10 | trailingComma: 'all', // 多行时尽可能打印尾随逗号
11 | bracketSpacing: false, // 在对象前后添加空格-eg: { foo: bar }
12 | jsxBracketSameLine: false, // 多属性html标签的‘>’折行放置
13 | arrowParens: 'always', // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
14 | requirePragma: false, // 无需顶部注释即可格式化
15 | insertPragma: false, // 在已被prettier格式化的文件顶部加上标注
16 | proseWrap: 'preserve', // 不知道怎么翻译
17 | htmlWhitespaceSensitivity: 'ignore', // 对HTML全局空白不敏感
18 | vueIndentScriptAndStyle: false, // 不对vue中的script及style标签缩进
19 | endOfLine: 'lf', // 结束行形式
20 | embeddedLanguageFormatting: 'auto', // 对引用代码进行格式化
21 | };
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Admin
2 |
3 | 基于[React17.x](https://reactjs.org)、[Ant Design4.x](https://ant.design/)的管理系统架构。
4 |
5 | - [在线预览](https://sxfad.github.io/react-admin/build)
6 | - [查看文档](https://sxfad.github.io/react-admin/#/)
7 | - [组件库ra-lib文档](https://sxfad.github.io/ra-lib/)
8 |
9 | ## 安装依赖
10 |
11 | - node v12.14.0
12 | - yarn 1.22.10
13 |
14 | ```bash
15 | yarn
16 | ```
17 |
18 | 注:如果由于网络等原因安装不成功,可以尝试 `tyarn` 或 `cnpm` 或 `npm` 或 `yarn --registry https://registry.npm.taobao.org`
19 |
20 | 设置环境变量,windows平台可以使用 [cross-env](https://github.com/kentcdodds/cross-env#)
21 |
22 | ## 开发启动
23 |
24 | 如果您是第一次使用,想快速预览效果,可以是用mock方式启动:`REACT_APP_MOCK=true yarn start`
25 |
26 | ```
27 | # 正常启动开发模式
28 | yarn start
29 |
30 | # 自定义端口
31 | PORT=3001 yarn start
32 |
33 | # HTTPS 方式启动
34 | HTTPS=true yarn start
35 |
36 | # 开启本地mock
37 | REACT_APP_MOCK=true yarn start
38 | ```
39 |
40 | ## 开发代理 & 测试代理
41 |
42 | 修改`src/setupProxyConfig.json`,页面右上角头部有下拉,可以快速切换代理。
43 |
44 | ```json
45 | [
46 | {
47 | "name": "张三",
48 | "disabled": false,
49 | "baseUrl": "/zhangsan",
50 | "target": "http://127.0.0.1:8080"
51 | },
52 | {
53 | "name": "测试环境",
54 | "baseUrl": "/api",
55 | "target": "http://127.0.0.1:8080"
56 | }
57 | ]
58 | ```
59 |
60 | ## 生产构建
61 |
62 | ```
63 | # 正常构建
64 | yarn build
65 |
66 | # 构建到指定目录
67 | BUILD_PATH=./dist yarn build
68 |
69 | # 指定配置环境
70 | REACT_APP_CONFIG_ENV=test yarn build
71 |
72 | # 打包大小分析
73 | yarn build:analyzer
74 |
75 | # 打包时间分析
76 | yarn build:time
77 | ```
78 |
79 | ## 样式
80 |
81 | - 支持less及css
82 | - src下less进行css module处理,css不进行css module处理
83 | - css module 应用样式写法
84 | ```jsx
85 | import styles from './style.module.less';
86 |
87 |
88 |
89 |
90 | ```
91 | - 项目中添加了[classnames](https://github.com/JedWatson/classnames)扩展,可以直接按照[classnames](https://github.com/JedWatson/classnames)方式在className中编写样式;
92 | ```jsx
93 | import styles from './style.module.less';
94 |
95 | const isActive = true;
96 |
97 |
98 |
99 |
100 | ```
101 | - 主题变量修改 theme.less [antd 样式变量](https://ant.design/docs/react/customize-theme-cn)
102 |
103 | ## 代码规范
104 |
105 | 代码规范使用 [prettier](https://prettier.io/) [参考知乎这片文章](https://zhuanlan.zhihu.com/p/81764012)
106 |
107 | 团队多人开发,无论使用webstorm还是vscode,都使用prettier(配置文件:.prettierrc.js)进行代码格式化。
108 |
109 | IDE格式化快捷键可以配置成prettier
110 |
111 | git commit 时会根据prettier进行代码格式化,确保提交到仓库的代码都符合规范
112 |
113 |
--------------------------------------------------------------------------------
/deploy/deploy.test.sh:
--------------------------------------------------------------------------------
1 | # 本地发布测试环境脚本
2 |
3 | # 构建前端
4 | yarn build
5 |
6 | echo "开始上传..."
7 | # 删除原文件
8 | sshpass -p 123456 ssh app@xxx.xxx.xxx.xxx "rm -rf /home/app/nginx/html/*"
9 | # 上传新文件
10 | sshpass -p 123456 scp -r ./build/* app@xxx.xxx.xxx.xxx:/home/app/nginx/html
11 | echo "上传成功!"
12 |
13 | # 拷贝ng配置
14 | # sshpass -p 123456 scp ./deploy/nginx.test.conf app@xxx.xxx.xxx.xxx:/home/app/nginx/conf
15 | # 重启ng,使配置生效
16 | # sshpass -p 123456 ssh app@xxx.xxx.xxx.xxx "nginx -s reload"
17 |
--------------------------------------------------------------------------------
/deploy/machine/jenkins.sh:
--------------------------------------------------------------------------------
1 | # 安装依赖
2 | yarn
3 |
4 | # 前端构建
5 | yarn build
6 |
7 | # 虚拟机部署
8 | # 直接copy,如果scp出现权限问题,需要将 ~/.ssh/id_rsa.pub内容添加到目标机的~/.ssh/authorized_keys文件中
9 | # nginx 配置修改后,要在目标机器上执行 nginx -s reload
10 | TARGET="172.16.xxx.xxx"
11 | scp -r build/* app@$TARGET:/home/app/nginx/html
12 | ssh app@$TARGET "chmod 775 -R /home/app/nginx"
13 |
14 | scp deploy/machine/nginx.conf app@$TARGET:/home/app/nginx/conf
15 | ssh app@$TARGET "nginx -s reload" # 重启ng,一般是ng配置改变了之后需要
16 |
--------------------------------------------------------------------------------
/deploy/machine/nginx.conf:
--------------------------------------------------------------------------------
1 | # 后端服务地址
2 | upstream api_service {
3 | server 172.16.40.72:8080;
4 | keepalive 2000;
5 | }
6 |
7 | server {
8 | listen 80;
9 | # server_name xxx.com; # 注意修改为相应的server_name
10 |
11 | # http body 大小限制,可以控制文件上传大小
12 | client_max_body_size 10M;
13 |
14 | # gzip config
15 | gzip on;
16 | gzip_min_length 1k;
17 | gzip_comp_level 9;
18 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
19 | gzip_vary on;
20 | gzip_disable "MSIE [1-6]\.";
21 | add_header Access-Control-Allow-Origin *; # 如果是微前端子项目,要设置允许跨域
22 |
23 | root /home/app/nginx/html; # 前端静态文件目录
24 | location / {
25 | index index.html;
26 | try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404
27 | }
28 |
29 | # 静态文件缓存,启用Cache-Control: max-age、Expires
30 | location ~ ^/static/(css|js|media)/ {
31 | expires 10y;
32 | access_log off;
33 | }
34 |
35 | # 代理ajax请求
36 | location ^~/api {
37 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
38 | proxy_pass http://api_service/;
39 | proxy_set_header Host $http_host;
40 | proxy_set_header Connection close;
41 | proxy_set_header X-Real-IP $remote_addr;
42 | proxy_set_header X-Forwarded-Server $host;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/deploy/nginx.conf:
--------------------------------------------------------------------------------
1 | # 负载均衡 后端服务地址 可写多个server
2 | upstream api_service {
3 | server 172.16.40.72:8080;
4 | keepalive 2000;
5 | }
6 |
7 | server {
8 | listen 80;
9 | # server_name xxx.com; # 注意修改为相应的server_name
10 |
11 | # http body 大小限制,可以控制文件上传大小
12 | client_max_body_size 10M;
13 |
14 | # gzip config
15 | gzip on;
16 | gzip_min_length 1k;
17 | gzip_comp_level 9;
18 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
19 | gzip_vary on;
20 | gzip_disable "MSIE [1-6]\.";
21 |
22 | add_header Access-Control-Allow-Origin *; # 如果是微前端子项目,要设置允许跨域
23 |
24 | root /usr/share/nginx/html; # 前端静态文件目录
25 | location / {
26 | index index.html;
27 | try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404
28 | }
29 |
30 | # 静态文件缓存,启用Cache-Control: max-age、Expires
31 | location ~ ^/static/(css|js|media)/ {
32 | expires 1y; # 可以同时设置 Expires 和 Cache-control mas-age ,规范规定,最大一年
33 | access_log off;
34 | }
35 |
36 | # 代理ajax请求
37 | location ^~/api {
38 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
39 | proxy_pass http://api_service/;
40 | proxy_set_header Host $http_host;
41 | proxy_set_header Connection close;
42 | proxy_set_header X-Real-IP $remote_addr;
43 | proxy_set_header X-Forwarded-Server $host;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/deploy/nginx.sub-app.conf:
--------------------------------------------------------------------------------
1 | # 后端服务地址
2 | upstream audit_api_service {
3 | server 172.16.40.72:8080;
4 | keepalive 2000;
5 | }
6 |
7 | server {
8 | listen 5101;
9 | # server_name xxx.com; # 注意修改为相应的server_name
10 |
11 | # http body 大小限制,可以控制文件上传大小
12 | client_max_body_size 10M;
13 |
14 | # gzip config
15 | gzip on;
16 | gzip_min_length 1k;
17 | gzip_comp_level 9;
18 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
19 | gzip_vary on;
20 | gzip_disable "MSIE [1-6]\.";
21 |
22 | # 如果是微前端子项目,要设置允许跨域
23 | add_header Access-Control-Allow-Origin *;
24 | add_header Access-Control-Allow-Methods *;
25 | add_header Access-Control-Allow-Headers *;
26 |
27 | # 删除接口服务器header
28 | proxy_hide_header Access-Control-Allow-Origin;
29 | proxy_hide_header Access-Control-Allow-Methods;
30 | proxy_hide_header Access-Control-Allow-Headers;
31 |
32 | root /usr/share/nginx/html/audit-front; # 前端静态文件目录
33 | location / {
34 | index index.html;
35 | try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404
36 | }
37 |
38 | # 静态文件缓存,启用Cache-Control: max-age、Expires
39 | location ~ ^/static/(css|js|media)/ {
40 | expires 10y;
41 | access_log off;
42 | }
43 |
44 | # 代理ajax请求
45 | location ^~/api {
46 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
47 | proxy_pass http://audit_api_service/;
48 | proxy_set_header Host $http_host;
49 | proxy_set_header Connection close;
50 | proxy_set_header X-Real-IP $remote_addr;
51 | proxy_set_header X-Forwarded-Server $host;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/deploy/rancher/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM bh-harbor.suixingpay.com/zhaikun/nginx:alpine
2 |
3 | RUN rm -rf /etc/nginx/conf.d/* \
4 | && rm -rf /usr/share/nginx/html/*
5 |
6 | ADD build/ /usr/share/nginx/html
7 | ADD nginx.conf /etc/nginx/conf.d/
8 |
9 | RUN chmod 775 -R /usr/share/nginx/html \
10 | && export LC_ALL=en_US.UTF-8 \
11 | && echo "设置时区" \
12 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
13 |
14 | EXPOSE 80
15 |
--------------------------------------------------------------------------------
/deploy/rancher/deployment.yaml:
--------------------------------------------------------------------------------
1 | # 配合jenkins使用 http://172.16.175.93:8080/jenkins/job/front-center/configure
2 | # 注意:rancher 要手动创建命名空间
3 | apiVersion: apps/v1
4 | kind: Deployment
5 | metadata:
6 | name: JOB_BASE_NAME
7 | namespace: NAMESPACE_NAME
8 | labels:
9 | app: JOB_BASE_NAME
10 | spec:
11 | replicas: 1
12 | selector:
13 | matchLabels:
14 | app: JOB_BASE_NAME
15 | template:
16 | metadata:
17 | labels:
18 | app: JOB_BASE_NAME
19 | spec:
20 | imagePullSecrets:
21 | - name: bh-harbor
22 | containers:
23 | - image: bh-harbor.suixingpay.com/zhaikun/JOB_BASE_NAME:BUILD_ID
24 | imagePullPolicy: IfNotPresent
25 | name: JOB_BASE_NAME
26 | ports:
27 | - containerPort: 80
28 | name: web
29 | ---
30 | apiVersion: v1
31 | kind: Service
32 | metadata:
33 | name: JOB_BASE_NAME
34 | namespace: NAMESPACE_NAME
35 | spec:
36 | selector:
37 | app: JOB_BASE_NAME
38 | type: NodePort
39 | ports:
40 | - port: 80
41 | protocol: TCP
42 | targetPort: 80
43 |
--------------------------------------------------------------------------------
/deploy/rancher/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | # server_name xxx.com; # 注意修改为相应的server_name
4 |
5 | # http body 大小限制,可以控制文件上传大小
6 | client_max_body_size 10M;
7 |
8 | # gzip config
9 | gzip on;
10 | gzip_min_length 1k;
11 | gzip_comp_level 9;
12 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
13 | gzip_vary on;
14 | gzip_disable "MSIE [1-6]\.";
15 | add_header Access-Control-Allow-Origin *; # 如果是微前端子项目,要设置允许跨域
16 |
17 | root /usr/share/nginx/html; # 前端静态文件目录
18 | location / {
19 | index index.html;
20 | try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404
21 | }
22 |
23 | # 静态文件缓存,启用Cache-Control: max-age、Expires
24 | location ~ ^/static/(css|js|media)/ {
25 | expires 10y;
26 | access_log off;
27 | }
28 |
29 |
30 | # 门户代理
31 | location ^~/portal {
32 | rewrite ^/portal/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
33 | proxy_pass http://172.16.143.44:32328/;
34 | proxy_set_header Host $http_host;
35 | proxy_set_header Connection close;
36 | proxy_set_header X-Real-IP $remote_addr;
37 | proxy_set_header X-Forwarded-Server $host;
38 | }
39 |
40 | # 代理ajax请求 张三
41 | location ^~/zhangsan {
42 | rewrite ^/zhangsan/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
43 | proxy_pass http://127.0.0.1:8080;
44 | proxy_set_header Host $http_host;
45 | proxy_set_header Connection close;
46 | proxy_set_header X-Real-IP $remote_addr;
47 | proxy_set_header X-Forwarded-Server $host;
48 | }
49 |
50 | # 代理ajax请求 测试环境
51 | location ^~/api {
52 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
53 | proxy_pass http://127.0.0.1:8080;
54 | proxy_set_header Host $http_host;
55 | proxy_set_header Connection close;
56 | proxy_set_header X-Real-IP $remote_addr;
57 | proxy_set_header X-Forwarded-Server $host;
58 | }
59 | }
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/AJAX.md:
--------------------------------------------------------------------------------
1 | # Ajax 请求
2 | 系统的ajax请求基于[axios](https://github.com/axios/axios)进行了二次封装,参见[@ra-lib/ajax](https://sxfad.github.io/ra-lib/ajax)。
3 |
4 | ## 方法
5 | 基于restful规范,提供了5个方法:
6 |
7 | - get 获取服务端数据,参数拼接在url上,以 query string 方式发送给后端
8 | - post 新增数据,参数以body形式发送给后端
9 | - put 修改数据,参数以body形式发送给后端
10 | - del 删除数据,参数拼接在url上,以 query string 方式发送给后端
11 | - patch 修改部分数据,参数以body形式发送给后端
12 |
13 | ## 调用方式
14 | 可以通过三种方式,让React组件拿到ajax对象
15 |
16 | - config装饰器ajax属性
17 | ```js
18 | import React, {Component} from 'react';
19 | import config from 'src/commons/config-hoc';
20 |
21 | @config({
22 | // ajax: true, // 默认为true
23 | ...
24 | })
25 | export default class SomePage extend Component {
26 | componentDidMount() {
27 | this.props.ajax
28 | .get(...)
29 | .then(...)
30 | }
31 | ...
32 | }
33 | ```
34 | 注: hooks `props.ajax.useGet`
35 | - ajax装饰器
36 | ```js
37 | import React, {Component} from 'react';
38 | import {ajaxHoc} from 'src/commpons/ajax';
39 |
40 | @ajaxHoc()
41 | export default class SomePage extend Component {
42 | componentDidMount() {
43 | this.props.ajax
44 | .get(...)
45 | .then(...)
46 | }
47 | ...
48 | }
49 | ```
50 |
51 | - 直接引入ajax对象
52 | ```js
53 | import React, {Component} from 'react';
54 | import ajax from 'src/commpons/ajax';
55 |
56 | export default class SomePage extend Component {
57 | componentDidMount() {
58 | ajax.post(...).then(...);
59 |
60 | // 组件卸载或者其他什么情况,需要打算ajax请求,可以用如下方式
61 | const ajaxFunc = ajax.get(...);
62 | ajaxFunc.then(...).finally(...);
63 | ajaxFunc.cancel();
64 | }
65 | ...
66 | }
67 | ```
68 | 注:config、ajaxHoc方式做了封装,页面被卸载之后会**自动打断**未完成的请求
69 |
70 | ## 参数
71 | 所有的ajax方法参数统一,都能够接受三个参数:
72 |
73 | 参数|说明
74 | ---|---
75 | url|请求地址
76 | params|请求传递给后端的参数
77 | options|请求配置,即axios的配置,
78 |
79 | options配置
80 |
81 | 参数|说明
82 | ---|---
83 | axios配置|可以接受axios参数
84 | successTip|扩展的参数,成功提示
85 | errorTip|扩展的参数,失败提示
86 | noEmpty|扩展的参数,过滤掉 ''、null、undefined的参数,不提交给后端
87 | originResponse|扩展参数,.then中可以拿到完整的response,而不只是response.data
88 |
89 | 注:全局默认参数可以在src/commons/ajax.js中进行配置,默认baseURL='/api'、timeout=1000 * 60。
90 |
91 | ## 请求结果提示
92 | 系统对ajax失败做了自动提示,开发人员可通过src/commons/handle-error.js进行配置;
93 |
94 | 成功提示默认不显示,如果需要成功提示,可以配置successTip参数,或者.then()中自行处理;
95 |
96 | 成功提示在src/commons/handle-success.js中配置;
97 | ```js
98 | this.props.ajax.del('/user/1', null, {successTip: '删除成功!', errorTip: '删除失败!', noEmpty: true});
99 | ```
100 |
101 | ## loading处理
102 | 系统扩展了promise,提供了finally方法,用于无论成功还是失败,都要进行的处理。一般用于关闭loading
103 | ```js
104 | this.setState({loading: true});
105 | this.props.ajax
106 | .get('/url')
107 | .then(...)
108 | .finally(() => this.setState({loading: false}));
109 | ```
110 |
111 |
--------------------------------------------------------------------------------
/docs/GEN.md:
--------------------------------------------------------------------------------
1 | # 代码生成工具
2 | 为了方便开发,系统配套提供了基于配置(页面)+模版的代码生成工具,可以快速生成增删改查相关代码。
3 |
4 | ## 特性
5 | 1. 基于文本配置文件,简单直接;
6 | 1. 可以基于swagger文档、数据库、配置文件,多种方式生成代码;
7 | 1. 基于模版,生成代码质量高,而且很容易扩展我们所需要的模版;
8 |
9 | ## 使用说明
10 | 基于默认配置文件 /script/gen/config.conf 进行代码生成
11 | ```bash
12 | $ yarn gen
13 | ```
14 |
15 | 指定配置文件
16 | ```bash
17 | $ yarn gen ./my-confog.conf
18 | ```
19 |
20 | 忽略文件覆盖提示
21 | ```bash
22 | $ yarn gen -y
23 | ```
24 |
25 | 基于数据库表,快速生成
26 | ```bash
27 | $ yarn gen -t users
28 | ```
29 |
30 | ## 配置文件编写说明
31 | 配置文件通过自定义脚本读取,需要遵循一些简单的编写规则
32 |
33 | 1. 脚本读取文件文本内容,配置文件可以是任何类型;
34 | 1. 脚本通过空格拆分各个配置项,空格个数不限制;
35 | 1. \#\#\#\#\#\# (六个#号)所包裹的内容是用来区分配置模块的,不要随意改动, 配置块的顺序可以随意调整;
36 | 1. 配置获取优先级为:接口 > 数据库表 > 当前配置文件;
37 | 1. 接口配置中的url行注释掉,将不基于接口进行生成;
38 | 1. 数据库配置中,tableName行注释掉,将不基于数据库表生成;
39 | 1. 三种注释方式 # ; //,可以基于需求,使用任意一种方式进行代码注释;
40 |
41 | ### form表单元素关键字说明:
42 | 1. q: 作为查询条件(query) ;
43 | 1. f: 作为表单(form);
44 | 1. r: 是否必填(required);
45 | 1. 5: 单独数字,最大可输入长度;
46 | 1. form表单可用类型:
47 | 1. input hidden number textarea password mobile email
48 | 1. select select-tree checkbox checkbox-group radio radio-group switch
49 | 1. date time date-time date-range cascader icon-picker
50 |
51 | ### 数据库链接各式
52 | ```
53 | mysql://user:password@host:port/database?querystring
54 | ```
55 | ### ajax请求地址说明
56 | 1. 请求关键字:查询、修改、添加、删除、详情、批量删除;
57 | 1. 各式:请求类型 method url dataPath(获取接口数据的key 比如: data.list);
58 | 1. 基于接口生成页面时:
59 | 1. 具体的地址在《基础配置》中填写;
60 | 1. 通过「查询」接口获取查询条件、表头信息;
61 | 1. 通过「修改」接口获取表单信息;
62 | 1.没有填写的请求将基于「查询」或者「目录」RestFul风格生成;
63 |
64 | ### 页面类型配置
65 | 1. 列表页、弹框编辑、页面编辑 系统默认模板和文件名,可以不指定模板和文件名;
66 | 1. 自定义页面:
67 | 1. 需要同时指定模板和目标文件名比如:自定义页面 ./src/template.js->customer.jsx;
68 | 1. 模板路径相对项目根目录开始写起;
69 |
70 | ## 代码生成页面
71 | 系统提供了代码生成页面,分为「快速生成」和「单独生成」,进入系统后,点击「代码生成」,可以打开代码生成页面。
72 |
--------------------------------------------------------------------------------
/docs/LAYOUT.md:
--------------------------------------------------------------------------------
1 | # 导航布局
2 | 为了满足不同系统的需求,提供了四种导航布局:
3 | - 头部菜单
4 | - 左侧菜单
5 | - 头部+左侧菜单
6 | - tab页方式
7 |
8 | ## 更改方式
9 | - 用户可以通过 页面头部右上角设置 页面进行选择(如果您为用户提供了此页面);
10 | - 开发人员可以通过修改`src/config`指定布局方式;
11 |
12 | ## 不需要导航
13 | 有些页面可能不需要显示导航,可以通过如下任意一种方式进行设置:
14 |
15 | - 页面配置高级组件
16 | ```js
17 | @config({
18 | header: false,
19 | side: false,
20 | pageHeader: false,
21 | tab: false,
22 | })
23 | ```
24 |
25 | 注:
26 |
27 | 1. tab基于页面地址,每当使用`this.props.history.push('/some/path')`,就会选中或者新打开一个tab页(`/path` 与 `/path?name=Tom`属于不同url地址,会对应两个tab页);
28 | 1. 没有菜单对应的页面,需要单独设置title,否则无法显示tab标签页;
29 |
30 |
--------------------------------------------------------------------------------
/docs/MENU.md:
--------------------------------------------------------------------------------
1 | # 菜单配置
2 |
3 | 在`/src/menus.js`文件中配置菜单数据,前端硬编码或异步加载菜单数据。
4 |
5 | ## 菜单特性
6 |
7 | 为了简化开发,让开发着专注于业务逻辑,系统对菜单进行了一些封装。
8 |
9 | - 菜单支持头部、左侧、头部+左侧**三种布局方式**,详见[导航布局](LAYOUT.md);
10 | - 系统会基于路由path**自动选中**对应的菜单;
11 | - 无菜单对应的二级页面也可以**选中相应父级**菜单,详见[系统路由](ROUTE.md);
12 | - 左侧菜单会**自动滚动**到可视范围内;
13 | - 左侧菜单支持**展开收起**;
14 | - 页面标题、tab标签页标题、面包屑基于菜单状态**自动生成**,但也提供了对应的修改方式,详见[页面开发](PAGE.md);
15 | - 通过菜单配置,支持内嵌**iframe**打开第三方页面、**a标签**方式打开第三方页面;
16 |
17 | ## 菜单字段说明
18 |
19 | 开发人员可以根据自己的需要,配置所需字段。
20 |
21 | 字段|必须|说明
22 | ---|---|---
23 | id|是|需要唯一
24 | parentId|否|用于关联父级
25 | path|是|菜单对应的路由地址 或 第三方网站地址
26 | title|是|菜单标题
27 | icon|否|菜单图标配置
28 | target|否|配合url使用,菜单将为a标签 `{title} `
29 | order|否|菜单排序,数值越大越靠前显示
30 |
--------------------------------------------------------------------------------
/docs/MOCK.md:
--------------------------------------------------------------------------------
1 | # Mock 模拟数据
2 | 前后端并行开发,为了方便后端快速开发,不需要等待后端接口,系统提供了mock功能。基于[mockjs](http://mockjs.com/)
3 |
4 | ## 编写模拟数据
5 | 在/src/mock目录下进行mock数据编写,比如:
6 | ```js
7 | import {getUsersByPageSize} from './mockdata/user';
8 |
9 | export default {
10 | 'post /mock/login': (config) => {
11 | const {
12 | userName,
13 | password,
14 | } = JSON.parse(config.data);
15 | return new Promise((resolve, reject) => {
16 | if (userName !== 'test' || password !== '111') {
17 | setTimeout(() => {
18 | reject({
19 | code: 1001,
20 | message: '用户名或密码错误',
21 | });
22 | }, 1000);
23 | } else {
24 | setTimeout(() => {
25 | resolve([200, {
26 | id: '1234567890abcde',
27 | name: 'MOCK 用户',
28 | loginName: 'MOCK 登录名',
29 | }]);
30 | }, 1000);
31 | }
32 | });
33 | },
34 | 'post /mock/logout': {},
35 |
36 | 'get /mock/user-center': (config) => {
37 | const {
38 | pageSize,
39 | pageNum,
40 | } = config.params;
41 |
42 |
43 | return new Promise((resolve) => {
44 | setTimeout(() => {
45 | resolve([200, {
46 | pageNum,
47 | pageSize,
48 | total: 888,
49 | list: getUsersByPageSize(pageSize),
50 | }]);
51 | }, 1000);
52 | });
53 | },
54 | 'get re:/mock/user-center/.+': {id: 1, name: '熊大', age: 22, job: '前端'},
55 | 'post /mock/user-center': true,
56 | 'put /mock/user-center': true,
57 | 'delete re:/mock/user-center/.+': 'id',
58 | }
59 | ```
60 |
61 | ## 简化
62 | 为了方便mock接口编写,系统提供了简化脚本(/src/mock/simplify.js),上面的例子就是简化写法
63 |
64 | 对象的key由 method url delay,各部分组成,以空格隔开
65 |
66 | 字段|说明
67 | ---|---
68 | method| 请求方法 get post等
69 | url|请求的url
70 | delay|模拟延迟,毫秒 默认1000
71 |
72 | ## 调用
73 | 系统封装的ajax可以通过以下两种方式,自动区分是mock数据,还是真实后端数据,无需其他配置
74 |
75 | mock请求:
76 | - url以/mock/开头的请求
77 | - /src/mock/url-config.js中配置的请求
78 |
79 | ```js
80 | this.props.ajax.get('/mock/users').then(...);
81 | ```
82 | 如果后端真实接口准备好之后,去掉url中的/mock即可
83 |
84 | 注:mock功能只有开发模式下开启了,生产模式不会开启mock功能,如果其他环境要开启mock 使用MOCK=true参数,比如 `MOCK=true yarn build`
85 |
--------------------------------------------------------------------------------
/docs/MODAL_PAGE.md:
--------------------------------------------------------------------------------
1 | # 弹框页面开发
2 | 添加、修改等场景,往往会用到弹框,antd Modal组件使用不当会产生脏数据问题(两次弹框渲染数据互相干扰)
3 |
4 | 系统提供了基于modal封装的高阶组件,每次弹框关闭,都会销毁弹框内容,避免互相干扰
5 |
6 |
7 | ## modal高阶组件
8 | modal高阶组件集成到了config中,也可以单独引用:`import {modal} from '@ra-lib/hoc';`
9 |
10 | ```jsx
11 | import React from 'react';
12 | import config from 'src/commons/config-hoc';
13 | import {ModalContent} from 'src/library/components';
14 |
15 | export default config({
16 | modal: {
17 | title: '弹框标题',
18 | },
19 | })(props => {
20 | const {onOk, onCancel} = props;
21 |
22 | return (
23 |
27 | 弹框内容
28 |
29 | );
30 | });
31 | ```
32 |
33 | modal所有参数说明如下:
34 |
35 | 1. 如果是string,作为modal的title
36 | 1. 如果是函数,函数返回值作为 Modal参数
37 | 1. 如果是对象,为Modal相关配置,具体参考 [antd Modal组件](https://ant-design.gitee.io/components/modal-cn/#API)
38 | 1. options.fullScreen boolean 默认false,是否全屏显示弹框
39 |
40 | ## ModalContent
41 | 弹框内容通过 ModalContent包裹,具体参数如下:
42 |
43 | 参数|类型|默认值|说明
44 | ---|---|---|---
45 | fitHeight|boolean|false|是否使用屏幕垂直方向剩余空间
46 | otherHeight|number|-|除了主体内容之外的其他高度,用于计算主体高度;
47 | loading|boolean|false|加载中
48 | loadingTip|-|-|加载提示文案
49 | footer|-|-|底部
50 | okText|string|-|确定按钮文案
51 | onOk|function|-|确定按钮事件
52 | cancelText|string|-|取消按钮文案
53 | onCancel|function|-|取消按钮事件
54 | resetText|string|-|重置按钮文案
55 | onReset|function|-|重置按钮事件
56 | style|object|-|最外层容器样式
57 | bodyStyle|object|-|内容容器样式
58 |
--------------------------------------------------------------------------------
/docs/MODEL.md:
--------------------------------------------------------------------------------
1 | # models(redux) 封装
2 | 基于[redux](https://redux.js.org/)进行封装,不改变redux源码,可以结合使用redux社区中其他解决方案。
3 |
4 | 注:一般情况下,用不到redux~
5 |
6 | 参见[@ra-lib/model](https://sxfad.github.io/ra-lib/model)
7 |
--------------------------------------------------------------------------------
/docs/NGINX.md:
--------------------------------------------------------------------------------
1 | # nginx配置参考
2 | 这里只是参考文件,根据自己的项目需求自行配置
3 |
4 | ## 一个域名对应单个项目
5 | ### 目录结构
6 | ```
7 | .
8 | ├── /usr/local/nginx/html
9 | │ ├── static
10 | │ ├── index.html
11 | │ └── favicon.ico
12 | ```
13 |
14 | ### nginx 配置
15 | ```nginx
16 | # 后端服务地址
17 | upstream api_service {
18 | server xxx.xxx.xxx.xxx:xxxx;
19 | keepalive 2000;
20 | }
21 |
22 | server {
23 | listen 80;
24 | server_name www.shubin.wang shubin.wang; # 域名地址
25 | root /usr/local/nginx/html; # 前端静态文件目录
26 | location / {
27 | index index.html;
28 | try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404
29 | }
30 |
31 | # 静态文件缓存,启用Cache-Control: max-age、Expires
32 | location ~ ^/static/(css|js|media)/ {
33 | expires 10y;
34 | access_log off;
35 | add_header Cache-Control "public";
36 | }
37 |
38 | # 代理ajax请求 前端ajax请求以 /api 开头
39 | location ^~/api {
40 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
41 | proxy_pass http://api_service/;
42 | proxy_set_header Host $http_host;
43 | proxy_set_header Connection close;
44 | proxy_set_header X-Real-IP $remote_addr;
45 | proxy_set_header X-Forwarded-Server $host;
46 | }
47 | }
48 | ```
49 |
50 | ## 一个域名对应多个项目
51 | 多个项目挂载到同一个域名下,可以通过子目录方式区分
52 |
53 | 比如,如下地址各对应一个项目
54 |
55 | - http://shubin.wang
56 | - http://shubin.wang/project1
57 | - http://shubin.wang/project2
58 |
59 | 前端项目构建时,添加BASE_NAME PUBLIC_URL参数
60 | ```bash
61 | BASE_NAME=/project1 PUBLIC_URL=/project1 yarn build
62 | ```
63 | ### nginx 静态文件目录结构
64 | ```
65 | .
66 | ├── /home/ubuntu/react-admin
67 | │ ├── build // 主项目 静态文件目录
68 | │ │ ├── static
69 | │ │ ├── index.html
70 | │ │ └── favicon.ico
71 | │ ├── project1 // 子项目静态目录 名称与 location /project1 location ~ ^/project1/static/.* 配置对应
72 | │ │ ├── static
73 | │ │ ├── index.html
74 | │ │ └── favicon.ico
75 | ```
76 |
77 | ### nginx 配置
78 | ```nginx
79 | upstream api_service {
80 | server xxx.xxx.xxx.xxx:xxxx;
81 | keepalive 2000;
82 | }
83 |
84 | upstream api_service_project1 {
85 | server xxx.xxx.xxx.xxx:xxxx;
86 | keepalive 2000;
87 | }
88 | server {
89 | listen 80;
90 | server_name www.shubin.wang shubin.wang; # 域名地址
91 | # Allow file uploads
92 | client_max_body_size 100M;
93 |
94 | # 主项目配置,访问地址 http://www.shubin.wang
95 | location / {
96 | root /home/ubuntu/react-admin/build;
97 | index index.html;
98 | try_files $uri $uri/ /index.html;
99 | }
100 | # 静态文件缓存,启用Cache-Control: max-age、Expires
101 | location ~ ^/static/.* {
102 | root /home/ubuntu/react-admin/build;
103 | expires 20y;
104 | access_log off;
105 | add_header Cache-Control "public";
106 | }
107 | # 代理ajax请求 前端ajax请求以/api开头
108 | location ^~/api {
109 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
110 | proxy_pass http://api_service/;
111 | proxy_set_header Host $http_host;
112 | proxy_set_header Connection close;
113 | proxy_set_header X-Real-IP $remote_addr;
114 | proxy_set_header X-Forwarded-Server $host;
115 | }
116 |
117 | # 子项目配置 访问地址 http://www.shubin.wang/project1
118 | location /project1 {
119 | root /home/ubuntu/react-admin;
120 | index index.html;
121 | try_files $uri $uri/ /project1/index.html;
122 | }
123 | # 静态文件缓存,启用Cache-Control: max-age、Expires
124 | location ~ ^/project1/static/.* {
125 | root /home/ubuntu/react-admin;
126 | expires 10y;
127 | access_log off;
128 | add_header Cache-Control "public";
129 | }
130 | # 代理ajax请求 前端ajax请求以 /project1_api 开头
131 | location ^~/project1_api {
132 | rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
133 | proxy_pass http://api_service_project1/;
134 | proxy_set_header Host $http_host;
135 | proxy_set_header Connection close;
136 | proxy_set_header X-Real-IP $remote_addr;
137 | proxy_set_header X-Forwarded-Server $host;
138 | }
139 | }
140 | ```
141 |
--------------------------------------------------------------------------------
/docs/OTHER.md:
--------------------------------------------------------------------------------
1 | # 其他
2 |
3 | 其他的一些说明
4 |
5 | ## 页面打印
6 |
7 | 通过给元素添加相应的class,控制打印内容:
8 |
9 | - `.just-print` 只在打印时显示
10 | - `.no-print` 在打印时不显示
11 |
12 | ## 组件
13 |
14 | 目录`src/library/antd`中基于Ant Design 扩展了一些常用组件
15 |
16 | 编写这些组件时,注意一下几点:
17 |
18 | - 通用组件不使用css module,方便使用过程中的样式覆盖;
19 | - 统一各个组件的目录结构,便于文档、demo生成;
20 | - `src/pages/example/antd`、`src/menus-ant-design-example.js` 通过脚本 `src/library/antd/generator-demos.js`生成;
21 |
22 | ## Webpack
23 |
24 | ### 使用了alias {src:'/path/to/src'}
25 |
26 | - 方便路径书写,不必关心相对路径结构
27 | - 复制粘贴到其他文件,不必修改路径
28 |
29 | ### 支持判断运算符
30 |
31 | ```js
32 | const name = res?.data?.user?.name || '匿名';
33 | ```
34 |
35 | ## ESLint 说明
36 |
37 | 如果前端项目,不是git根目录,在提交的时候,会报错 `Not a git repository`
38 |
39 | 修改package.json,lint-staged 如下即可
40 |
41 | ```json
42 | "lint-staged": {
43 | "gitDir": "../",
44 | "linters": {
45 | "**/*.{js,jsx}": "lint-staged:js",
46 | "**/*.less": "stylelint --syntax less"
47 | }
48 | },
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/PAGE.md:
--------------------------------------------------------------------------------
1 | # 页面开发
2 |
3 | 页面指的是路由对应的页面组件。业务开发接触最多的就是页面,通过一些封装,简化开发。
4 |
5 | ## 配置高阶组件
6 |
7 | 将组件所需要的一些功能,通过配置装饰器的方式实现,用法如下:
8 |
9 | hooks:
10 |
11 | ```
12 | import config from 'src/commons/config-hoc';
13 |
14 | export default config({
15 | path: '/page/path',
16 | ...
17 | })(function SomePage(props) {
18 | ...
19 | });
20 | ```
21 |
22 | class:
23 |
24 | ```
25 | import React, {Component} from 'react';
26 | import config from 'src/commons/config-hoc';
27 |
28 | @config({
29 | path: '/page/path',
30 | title: '页面title', // 默认基于菜单
31 | ajax: true, // 默认true
32 | ...
33 | })
34 | export default class SomePage extend Component {
35 | componentDidMount(){
36 | this.props.ajax
37 | .get(...)
38 | .then(...)
39 | }
40 | ...
41 | }
42 | ```
43 |
44 | 所有参数如下:
45 |
46 | ```js
47 | // config 所有可用参数,以及默认值
48 | const {
49 | // 路由地址
50 | path,
51 | // 是否需要登录
52 | auth,
53 | // 是否显示顶部
54 | header,
55 | // 是否显示标签
56 | tab,
57 | // 是否显示页面头部
58 | pageHeader,
59 | // 是否显示侧边栏
60 | side,
61 | // 侧边栏是否收起
62 | sideCollapsed,
63 | // 设置选中菜单,默认基于 window.location.pathname选中 用于设置非菜单的子页面,菜单选中状态
64 | selectedMenuPath,
65 | // 设置页面、tab标题,默认基于选中菜单,也可以通过query string 设置 /xxx?title=页面标题
66 | title,
67 | // 自定义面包屑导航,默认基于选中菜单,false:不显示,[{icon, title, path}, ...]
68 | breadcrumb,
69 | // 基于菜单,追加面包屑导航
70 | appendBreadcrumb,
71 | // 页面保持,不销毁,需要设置config.KEEP_PAGE_ALIVE === true 才生效
72 | keepAlive,
73 | // 是否添加withRouter高级组件
74 | router = false,
75 | // props是否注入ajax
76 | ajax = CONFIG_HOC.ajax,
77 | // 连接models,扩展 props.action
78 | connect = CONFIG_HOC.connect,
79 | // 弹框高阶组件
80 | modal,
81 | // 抽屉高级组件
82 | drawer,
83 | ...others
84 | } = options;
85 | ```
86 |
87 | ## 页面保持
88 |
89 | 页面渲染一次之后会保持状态,再次跳转到此页面不会重新创建或重新渲染。
90 |
91 | ### 开启方式
92 |
93 | `/src/config CONFIG_HOC.keepAlive`设置为true
94 |
95 | 注:部分不需要keepAlive页面,可以在config高阶组件中设置`keepAlive: false`
96 |
97 | ### 页面显示/隐藏事件
98 |
99 | 开启keepAlive功能的页面,会接受到 active props。可以根据active值变化,判断当前页面状态
100 |
101 | active值说明
102 |
103 | - `undefined`页面初始化,第一次加载
104 | - `false` 页面被隐藏
105 | - `true` 页面被显示
106 |
107 | ## 页面容器PageContent
108 |
109 | 系统提供了页面的跟节点PageContent,有如下特性:
110 |
111 | - 添加了margin padding 样式;
112 | - 支持页面loading;
113 | - 添加最小高度,始终使页面撑满全屏
114 | - fitHeight功能,是页面撑满全屏,内容过长时,PageContent将显示滚动条
115 |
116 | 显示loading
117 |
118 | ```js
119 | const {loading} = this.state;
120 |
121 |
122 | ...
123 |
124 | ```
125 |
126 |
127 |
--------------------------------------------------------------------------------
/docs/PERMISSION.md:
--------------------------------------------------------------------------------
1 | # 权限控制
2 |
3 | 系统菜单、具体功能点都可以进行权限控制。
4 |
5 | ## 菜单权限
6 |
7 | 如果菜单由后端提供(一般系统都是后端提供),可以通过userId获取用户的菜单权限;页面只显示获取到的菜单;
8 |
9 | 系统提供了一个基础的菜单、权限管理页面,需要后端配合存储数据。
10 |
11 | ## 功能权限
12 |
13 | 函数可以通过 `src/commons#hasPermission`方法进行权限判断
14 |
15 | 组件可以通过`src/components/permission`组件对功能的权限进行控制
16 |
17 | ```js
18 | import React, {Component} from 'react';
19 | import Permission from 'src/components';
20 | import {hasPermission} from 'src/commons/index';
21 |
22 | export default class SomePage extends Component {
23 |
24 | someFunc() {
25 | const show = hasPermission('USER_ADD');
26 | }
27 |
28 | render() {
29 | return (
30 |
35 | );
36 | }
37 | }
38 | ```
39 |
40 | 注:权限的code前端使用时会硬编码,注意语义化、唯一性。
41 |
42 | ## 角色
43 |
44 | 一般系统都会提供角色管理功能,系统中提供了一个基础的角色管理功能,稍作修改即可使用。
45 |
--------------------------------------------------------------------------------
/docs/PROXY.md:
--------------------------------------------------------------------------------
1 | # 开发代理
2 | 开发时,要与后端进行接口对接,可以通过代理与后端进行连接,开发代理配置在`src/setupProxy.js`中编写
3 |
4 | ```js
5 | const proxy = require('http-proxy-middleware');
6 |
7 | // 前端web服务代理配置
8 | module.exports = function(app) {
9 | app.use(proxy('/api',
10 | {
11 | target: 'http://localhost:8081/', // 目标服务器
12 | pathRewrite: {
13 | '^/api': '', // 如果后端接口无前缀,可以通过这种方式去掉
14 | },
15 | changeOrigin: true,
16 | secure: false, // 是否验证证书
17 | ws: true, // 启用websocket
18 | },
19 | ));
20 | };
21 |
22 | ```
23 |
24 | 注:更多代理配置请参考[http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware)
25 |
--------------------------------------------------------------------------------
/docs/QIANKUN.md:
--------------------------------------------------------------------------------
1 | # 微前端
2 |
3 | 当前框架既可以作为乾坤主项目,又可以作为乾坤子项目
4 |
5 | ## 作为乾坤子系统时
6 |
7 | **新创建子项目时注意修改 package.json 文件中 name 属性**
8 |
9 | 约定:package.json name 作为:
10 |
11 | - 子系统的BASE_NAME
12 | - 子系统注册到主系统中的 name 、 activeRule
13 | - ant、ra-lib组件库class前缀
14 |
15 | ## 服务器设置
16 |
17 | ### 开发模式
18 |
19 | - 开发devServer
20 |
21 | ```js
22 | devServer: (devServerConfig, {env, paths, proxy, allowedHost}) => {
23 | if (!devServerConfig.headers) devServerConfig.headers = {};
24 | devServerConfig.headers['Access-Control-Allow-Origin'] = '*';
25 | return devServerConfig;
26 | }
27 | ```
28 |
29 | - 开发代理服务器
30 |
31 | ```js
32 | module.exports = function(app) {
33 | app.use(proxy('/api',
34 | {
35 | target: 'http://172.16.40.72:8080',
36 | pathRewrite: {
37 | '^/api': '', // 如果后端接口无前缀,可以通过这种方式去掉
38 | },
39 | changeOrigin: true,
40 | secure: false, // 是否验证证书
41 | ws: true, // 启用websocket
42 | // 作为子系统时,需要设置允许跨域
43 | onProxyRes(proxyRes, req, res) {
44 | proxyRes.headers['Access-Control-Allow-Origin'] = '*';
45 | proxyRes.headers['Access-Control-Allow-Methods'] = '*';
46 | proxyRes.headers['Access-Control-Allow-Headers'] = '*';
47 | },
48 | },
49 | ));
50 | };
51 | ```
52 |
53 | ### 生产(测试)部署
54 |
55 | - nginx 配置
56 |
57 | ```
58 | # 如果是微前端子项目,要设置允许跨域
59 | add_header Access-Control-Allow-Origin *;
60 | add_header Access-Control-Allow-Methods *;
61 | add_header Access-Control-Allow-Headers *;
62 |
63 | # 删除接口服务器header
64 | proxy_hide_header Access-Control-Allow-Origin;
65 | proxy_hide_header Access-Control-Allow-Methods;
66 | proxy_hide_header Access-Control-Allow-Headers;
67 | ```
68 |
69 | - 后端接口服务
70 |
71 | 如果使用ng,在ng上设置即可,不需要特殊设置,如果前端直接请求后端接口服务,则需要设置跨域
72 |
73 | ## 乾坤微前端的坑
74 |
75 | -[ ] 子系统卸载时,控制台会报提醒 `[qiankun] Set window.event while sandbox destroyed or inactive in xxx! `
76 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 简介
2 |
3 | react-admin是基于[React17.x](https://reactjs.org)、[Ant Design4.x](https://ant.design/)的管理系统架构。
4 | 采用前后端分离,内置了许多管理系统常用功能,通过一些脚本、封装帮助开发人员快速开发管理系统,集中精力处理业务逻辑。
5 |
6 | ## 项目结构
7 |
8 | ```
9 | .
10 | ├── build // 构建生成的静态文件目录
11 | ├── docs // 说明不稳当
12 | ├── node_modules // 项目依赖
13 | ├── public // 静态文件,非webpack构建文件,构建时会原样copy到build目录中
14 | ├── src // 项目源码目录
15 | │ ├── commons // 项目公共文件目录,一些公共js
16 | │ ├── components // 项目公共组件
17 | │ ├── config // 项目配置
18 | │ ├── mock // 开发时mock数据
19 | │ ├── models // 数据管理,基于redux,复杂的页面会用到
20 | │ ├── pages // 页面文件
21 | │ ├── router // 路由相关
22 | │ ├── App.jsx // 根组件,做一些项目一些初始化工作
23 | │ ├── App.less // 全局样式
24 | │ ├── index.js // 项目入口文件
25 | │ ├── menus.js // 菜单配置文件
26 | │ ├── reportWebVitals.js
27 | │ ├── setupProxy.js // 开发模式,接口对接代理设置
28 | │ ├── setupTests.js // 测试相关
29 | │ └── theme.less // 主题变量
30 | ├── craco.config.js // webpack等构建相关配置
31 | ├── package.json
32 | ├── README.md
33 | └── yarn.lock
34 | ```
35 |
36 | ## 预览
37 |
38 | 部分页面截图,完整项目预览地址[戳这里](https://sxfad.github.io/react-admin/build/#/login)
39 |
40 |
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docs/ROUTE.md:
--------------------------------------------------------------------------------
1 | # 系统路由
2 | 系统路由使用 [react-router](https://reacttraining.com/react-router/web/guides/quick-start)。
3 | 路由使用最多的场景就是一个path对应一个页面,但是开发人员不得不去编写配置文件,多人协作时,配置文件还经常冲突。
4 | 系统通过脚本,简化路由配置。
5 |
6 | ## 路由配置方式
7 | 通过config-loader 将高阶组件参数填充到`/src/pages/page-configs.js`文件。
8 |
9 | 页面config装饰器
10 | ```js
11 | @config({
12 | path: '/path',
13 | })
14 | export default class SomePage extends React.Component {
15 | ...
16 | }
17 | ```
18 |
19 | 比如SomePage.jsx中有上面其中任意一种写法,config-loader最终会在`/src/pages/page-configs.js`文件中填充如下路由配置:
20 | ```js
21 | {
22 | path: '/path',
23 | component: () => import('/path/to/SomePage.jsx'),
24 | },
25 | ```
26 |
27 | ## 二级页面
28 |
29 | 二级页面如果要保持父级菜单的选中状态,在config高阶组件中设置 selectedMenuPath 参数
30 |
--------------------------------------------------------------------------------
/docs/START.md:
--------------------------------------------------------------------------------
1 | # 快速开始
2 |
3 | 从[github](https://github.com/sxfad/react-admin)上clone代码,通过下面介绍的命令,进行开发或者生产构建。
4 |
5 | ## 环境
6 |
7 | - [yarn](https://yarnpkg.com) v1.22.10
8 | - [node](https://nodejs.org) v12.14.0
9 |
10 | ## 下载
11 |
12 | ```bash
13 | $ git clone https://github.com/sxfad/react-admin.git
14 | ```
15 |
16 | ## 安装依赖
17 |
18 | ```bash
19 | $ cd react-admin
20 | $ yarn
21 | // 使用国内淘宝镜像镜像
22 | $ yarn --registry https://registry.npm.taobao.org
23 | ```
24 |
25 | 注:首次使用yarn安装依赖可能比较慢,可以切换到国内镜像,或者科学上网。
26 |
27 | ## 开发启动
28 |
29 | ```bash
30 | $ cd react-admin
31 | $ yarn start
32 |
33 | # 指定端口
34 | $ PORT=8080 yarn start
35 |
36 | # HTTPS方式启动
37 | $ HTTPS=true yarn start
38 | ```
39 |
40 | 注:启动会有点慢,耐心等待一会儿,启动成功后会自动打开浏览器。 windows环境下可以使用 [cross-env](https://www.npmjs.com/package/cross-env)设置命令行变量。
41 |
42 | ## 生产构建
43 |
44 | ```bash
45 | $ cd react-admin
46 |
47 | $ yarn build
48 |
49 | # 构建输入到指定目录
50 | $ BUILD_PATH=../dist yarn build
51 | ```
52 |
53 | 注:默认构建生成的文件在 `/react-admin/build` 目录下;[nginx配置参考](NGINX.md)。
54 |
55 | ## 域名子目录发布项目
56 |
57 | 如果项目需要挂载到域名的一个子目录下,比如 `http://xxx.com/react-admin`。添加命令行参数 REACT_APP_BASE_NAME=/react-admin
58 |
59 | 注:如果前端构建完成的静态页面,放在nginx静态目录的react-admin目录下,添加命令行参数 PUBLIC_URL=/react-admin;
60 |
61 |
62 |
--------------------------------------------------------------------------------
/docs/STYLE.md:
--------------------------------------------------------------------------------
1 | # 样式
2 | 系统使用[less](http://lesscss.org/)进行样式的编写。
3 |
4 | ## css 模块化
5 | 为了避免多人合作样式冲突,系统对src下的less文件启用了Css Module,css文件没有使用Css Module。
6 |
7 | style.less
8 | ```less
9 | .root{
10 | width: 100%;
11 | height: 100%;
12 | }
13 | ```
14 | Some.jsx
15 | ```jsx
16 | import styles from '/path/to/style.less';
17 |
18 | export default class Some extends React.Component {
19 | render() {
20 | return (
21 |
22 | );
23 | }
24 | }
25 | ```
26 |
27 | ## 主题
28 | 使用less,通过样式覆盖来实现。
29 |
30 | ### 编写主题
31 | - less文件中使用主题相关变量;
32 | - 编写`/src/theme.less`通过[less-loader](https://github.com/webpack-contrib/less-loader)的`modifyVars`覆盖less中的变量;
33 |
34 | ### 参考
35 | - Ant Design 主题 参考:https://ant-design.gitee.io/docs/react/customize-theme-cn
36 |
--------------------------------------------------------------------------------
/docs/_coverpage.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # React-Admin
4 |
5 | > 管理系统架构
6 |
7 | - 基于[React17.x](https://reactjs.org)、[Ant Design4.x](https://ant.design/)
8 | - 简单 · 高效
9 |
10 | [GitHub](https://github.com/sxfad/react-admin)
11 | [快速开始](#简介)
12 |
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 | * [简介](README.md)
2 | * [快速开始](START.md)
3 | * [菜单配置](MENU.md)
4 | * [页面开发](PAGE.md)
5 | * [弹框页面开发](MODAL_PAGE.md)
6 | * [系统路由](ROUTE.md)
7 | * [Ajax请求](AJAX.md)
8 | * [Mock模拟数据](MOCK.md)
9 | * [样式&主题](STYLE.md)
10 | * [导航布局](LAYOUT.md)
11 | * [Model(Redux)](MODEL.md)
12 | * [权限控制](PERMISSION.md)
13 | * [Nginx配置](NGINX.md)
14 | * [开发代理](PROXY.md)
15 | * [微前端](QIANKUN.md)
16 | [comment]: <> (* [代码生成](GEN.md))
17 | * [其他](OTHER.md)
18 |
--------------------------------------------------------------------------------
/docs/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "static/js/0.c00a972e.chunk.js": "/react-admin/build/static/js/0.c00a972e.chunk.js",
4 | "static/js/0.c00a972e.chunk.js.map": "/react-admin/build/static/js/0.c00a972e.chunk.js.map",
5 | "main.js": "/react-admin/build/static/js/main.e9dfe8d6.chunk.js",
6 | "main.js.map": "/react-admin/build/static/js/main.e9dfe8d6.chunk.js.map",
7 | "runtime-main.js": "/react-admin/build/static/js/runtime-main.bd4dd1cc.js",
8 | "runtime-main.js.map": "/react-admin/build/static/js/runtime-main.bd4dd1cc.js.map",
9 | "static/css/3.6178f109.chunk.css": "/react-admin/build/static/css/3.6178f109.chunk.css",
10 | "static/js/3.a462f62e.chunk.js": "/react-admin/build/static/js/3.a462f62e.chunk.js",
11 | "static/js/3.a462f62e.chunk.js.map": "/react-admin/build/static/js/3.a462f62e.chunk.js.map",
12 | "static/css/4.185ef8b6.chunk.css": "/react-admin/build/static/css/4.185ef8b6.chunk.css",
13 | "static/js/4.dafdb96a.chunk.js": "/react-admin/build/static/js/4.dafdb96a.chunk.js",
14 | "static/js/4.dafdb96a.chunk.js.map": "/react-admin/build/static/js/4.dafdb96a.chunk.js.map",
15 | "static/css/5.185ef8b6.chunk.css": "/react-admin/build/static/css/5.185ef8b6.chunk.css",
16 | "static/js/5.0bf26507.chunk.js": "/react-admin/build/static/js/5.0bf26507.chunk.js",
17 | "static/js/5.0bf26507.chunk.js.map": "/react-admin/build/static/js/5.0bf26507.chunk.js.map",
18 | "static/css/6.9a1388c0.chunk.css": "/react-admin/build/static/css/6.9a1388c0.chunk.css",
19 | "static/js/6.fa51b8dd.chunk.js": "/react-admin/build/static/js/6.fa51b8dd.chunk.js",
20 | "static/js/6.fa51b8dd.chunk.js.map": "/react-admin/build/static/js/6.fa51b8dd.chunk.js.map",
21 | "static/js/7.019a448a.chunk.js": "/react-admin/build/static/js/7.019a448a.chunk.js",
22 | "static/js/7.019a448a.chunk.js.map": "/react-admin/build/static/js/7.019a448a.chunk.js.map",
23 | "static/js/8.91f49de0.chunk.js": "/react-admin/build/static/js/8.91f49de0.chunk.js",
24 | "static/js/8.91f49de0.chunk.js.map": "/react-admin/build/static/js/8.91f49de0.chunk.js.map",
25 | "static/js/9.5fa78ebf.chunk.js": "/react-admin/build/static/js/9.5fa78ebf.chunk.js",
26 | "static/js/9.5fa78ebf.chunk.js.map": "/react-admin/build/static/js/9.5fa78ebf.chunk.js.map",
27 | "static/js/10.515a043b.chunk.js": "/react-admin/build/static/js/10.515a043b.chunk.js",
28 | "static/js/10.515a043b.chunk.js.map": "/react-admin/build/static/js/10.515a043b.chunk.js.map",
29 | "static/js/11.df3576a8.chunk.js": "/react-admin/build/static/js/11.df3576a8.chunk.js",
30 | "static/js/11.df3576a8.chunk.js.map": "/react-admin/build/static/js/11.df3576a8.chunk.js.map",
31 | "static/js/12.99cc42a7.chunk.js": "/react-admin/build/static/js/12.99cc42a7.chunk.js",
32 | "static/js/12.99cc42a7.chunk.js.map": "/react-admin/build/static/js/12.99cc42a7.chunk.js.map",
33 | "static/js/13.bff1e6dc.chunk.js": "/react-admin/build/static/js/13.bff1e6dc.chunk.js",
34 | "static/js/13.bff1e6dc.chunk.js.map": "/react-admin/build/static/js/13.bff1e6dc.chunk.js.map",
35 | "static/js/14.e62adf89.chunk.js": "/react-admin/build/static/js/14.e62adf89.chunk.js",
36 | "static/js/14.e62adf89.chunk.js.map": "/react-admin/build/static/js/14.e62adf89.chunk.js.map",
37 | "static/js/15.510c57f0.chunk.js": "/react-admin/build/static/js/15.510c57f0.chunk.js",
38 | "static/js/15.510c57f0.chunk.js.map": "/react-admin/build/static/js/15.510c57f0.chunk.js.map",
39 | "index.html": "/react-admin/build/index.html",
40 | "static/css/3.6178f109.chunk.css.map": "/react-admin/build/static/css/3.6178f109.chunk.css.map",
41 | "static/css/4.185ef8b6.chunk.css.map": "/react-admin/build/static/css/4.185ef8b6.chunk.css.map",
42 | "static/css/5.185ef8b6.chunk.css.map": "/react-admin/build/static/css/5.185ef8b6.chunk.css.map",
43 | "static/css/6.9a1388c0.chunk.css.map": "/react-admin/build/static/css/6.9a1388c0.chunk.css.map",
44 | "static/js/3.a462f62e.chunk.js.LICENSE.txt": "/react-admin/build/static/js/3.a462f62e.chunk.js.LICENSE.txt",
45 | "static/media/defaultErrorImage.d83d2d78.png": "/react-admin/build/static/media/defaultErrorImage.d83d2d78.png",
46 | "static/media/login-bg.jpg": "/react-admin/build/static/media/login-bg.3ab1656e.jpg"
47 | },
48 | "entrypoints": [
49 | "static/js/runtime-main.bd4dd1cc.js",
50 | "static/css/3.6178f109.chunk.css",
51 | "static/js/3.a462f62e.chunk.js",
52 | "static/js/main.e9dfe8d6.chunk.js"
53 | ]
54 | }
--------------------------------------------------------------------------------
/docs/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/docs/build/favicon.ico
--------------------------------------------------------------------------------
/docs/build/index.html:
--------------------------------------------------------------------------------
1 | 加载中... You need to enable JavaScript to run this app.
--------------------------------------------------------------------------------
/docs/build/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/docs/build/logo192.png
--------------------------------------------------------------------------------
/docs/build/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/docs/build/logo512.png
--------------------------------------------------------------------------------
/docs/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/docs/build/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/docs/build/static/css/6.9a1388c0.chunk.css:
--------------------------------------------------------------------------------
1 | .react-admin-alert{box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum","tnum";position:relative;display:flex;align-items:center;padding:8px 15px;word-wrap:break-word;border-radius:2px}.react-admin-alert-content{flex:1 1;min-width:0}.react-admin-alert-icon{margin-right:8px}.react-admin-alert-description{display:none;font-size:14px;line-height:22px}.react-admin-alert-success{background-color:#f6ffed;border:1px solid #b7eb8f}.react-admin-alert-success .react-admin-alert-icon{color:#52c41a}.react-admin-alert-info{background-color:#e6f7ff;border:1px solid #91d5ff}.react-admin-alert-info .react-admin-alert-icon{color:#1890ff}.react-admin-alert-warning{background-color:#fffbe6;border:1px solid #ffe58f}.react-admin-alert-warning .react-admin-alert-icon{color:#faad14}.react-admin-alert-error{background-color:#fff2f0;border:1px solid #ffccc7}.react-admin-alert-error .react-admin-alert-icon{color:#ff4d4f}.react-admin-alert-error .react-admin-alert-description>pre{margin:0;padding:0}.react-admin-alert-action{margin-left:8px}.react-admin-alert-close-icon{margin-left:8px;padding:0;overflow:hidden;font-size:12px;line-height:12px;background-color:transparent;border:none;outline:none;cursor:pointer}.react-admin-alert-close-icon .anticon-close{color:rgba(0,0,0,.45);transition:color .3s}.react-admin-alert-close-icon .anticon-close:hover{color:rgba(0,0,0,.75)}.react-admin-alert-close-text{color:rgba(0,0,0,.45);transition:color .3s}.react-admin-alert-close-text:hover{color:rgba(0,0,0,.75)}.react-admin-alert-with-description{align-items:flex-start;padding:15px 15px 15px 24px}.react-admin-alert-with-description.react-admin-alert-no-icon{padding:15px}.react-admin-alert-with-description .react-admin-alert-icon{margin-right:15px;font-size:24px}.react-admin-alert-with-description .react-admin-alert-message{display:block;margin-bottom:4px;color:rgba(0,0,0,.85);font-size:16px}.react-admin-alert-message{color:rgba(0,0,0,.85)}.react-admin-alert-with-description .react-admin-alert-description{display:block}.react-admin-alert.react-admin-alert-motion-leave{overflow:hidden;opacity:1;transition:max-height .3s cubic-bezier(.78,.14,.15,.86),opacity .3s cubic-bezier(.78,.14,.15,.86),padding-top .3s cubic-bezier(.78,.14,.15,.86),padding-bottom .3s cubic-bezier(.78,.14,.15,.86),margin-bottom .3s cubic-bezier(.78,.14,.15,.86)}.react-admin-alert.react-admin-alert-motion-leave-active{max-height:0;margin-bottom:0!important;padding-top:0;padding-bottom:0;opacity:0}.react-admin-alert-banner{margin-bottom:0;border:0;border-radius:0}.react-admin-alert.react-admin-alert-rtl{direction:rtl}.react-admin-alert-rtl.react-admin-alert.react-admin-alert-no-icon{padding:8px 15px}.react-admin-alert-rtl .react-admin-alert-icon{margin-right:auto;margin-left:8px}.react-admin-alert-rtl .react-admin-alert-action,.react-admin-alert-rtl .react-admin-alert-close-icon{margin-right:8px;margin-left:auto}.react-admin-alert-rtl.react-admin-alert-with-description .react-admin-alert-icon{margin-right:auto;margin-left:15px}
2 | /*# sourceMappingURL=6.9a1388c0.chunk.css.map */
--------------------------------------------------------------------------------
/docs/build/static/js/11.df3576a8.chunk.js:
--------------------------------------------------------------------------------
1 | (this["webpackJsonp_react-admin"]=this["webpackJsonp_react-admin"]||[]).push([[11],{958:function(e,n,t){"use strict";t.r(n);t(104);var r=t(37),a=t(5),s=t.n(a),c=t(14),i=t(1),o=t.n(i),l=t(13),u=t(163),A=t(995),p=t.n(A),f=t(9);n.default=Object(u.a)({path:"/",title:"\u9996\u9875"})((function(e){return Object(f.jsxs)(l.m,{className:o()(p.a.root),children:[Object(f.jsx)("h1",{children:"\u9996\u9875"}),Object(f.jsx)(r.a,{onClick:Object(c.a)(s.a.mark((function n(){return s.a.wrap((function(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,e.ajax.post("/initDB",null,{successTip:"\u6570\u636e\u5e93\u91cd\u7f6e\u6210\u529f\uff01"});case 2:setTimeout((function(){return window.location.reload()}),2e3);case 3:case"end":return n.stop()}}),n)}))),children:"\u91cd\u7f6e\u6570\u636e\u5e93"})]})}))},995:function(e,n,t){var r=t(190),a=t(996);"string"===typeof(a=a.__esModule?a.default:a)&&(a=[[e.i,a,""]]);var s={insert:"head",singleton:!1};r(a,s);e.exports=a.locals||{}},996:function(e,n,t){"use strict";t.r(n);var r=t(111),a=t.n(r)()(!0);a.push([e.i,".root_IP7AU {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n}\n","",{version:3,sources:["webpack://src/pages/home/style.less"],names:[],mappings:"AAEA;EACI,aAAA;EACA,sBAAA;EACA,uBAAA;EACA,mBAAA;AAIJ",sourcesContent:['@import "src/theme.less";\n\n.root {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n}\n\n@packageName: react-admin;'],sourceRoot:""}]),a.locals={antPrefix:"react-admin",raLibPrefix:"react-admin-ra",primaryColor:"#1890ff",root:"root_IP7AU"},n.default=a}}]);
2 | //# sourceMappingURL=11.df3576a8.chunk.js.map
--------------------------------------------------------------------------------
/docs/build/static/js/11.df3576a8.chunk.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["pages/home/index.jsx","webpack://react-admin/./src/pages/home/style.less?5718","pages/home/style.less"],"names":["config","path","title","props","styles","root","onClick","a","ajax","post","successTip","setTimeout","window","location","reload","api","content","__esModule","default","module","i","options","exports","locals","___CSS_LOADER_EXPORT___","push"],"mappings":"iOAMeA,sBAAO,CAClBC,KAAM,IACNC,MAAO,gBAFIF,EAGZ,SAAcG,GAGb,sBACK,IADL,eAC4BC,IAAOC,MADnC,UAEQ,8CAEI,mBACIC,QAAO,sBAAE,sBAAAC,EAAA,sEACCJ,EAAMK,KAAKC,KAAK,UAAW,KAAM,CAAEC,WAAY,qDADhD,OAELC,YAAW,kBAAMC,OAAOC,SAASC,WAAU,KAFtC,2CADb,mD,oBChBhB,IAAIC,EAAM,EAAQ,KACFC,EAAU,EAAQ,KAIC,kBAFvBA,EAAUA,EAAQC,WAAaD,EAAQE,QAAUF,KAG/CA,EAAU,CAAC,CAACG,EAAOC,EAAIJ,EAAS,MAG9C,IAAIK,EAAU,CAEd,OAAiB,OACjB,WAAoB,GAEPN,EAAIC,EAASK,GAI1BF,EAAOG,QAAUN,EAAQO,QAAU,I,iCClBnC,oBAEIC,EAFJ,MAE8B,IAA4B,GAE1DA,EAAwBC,KAAK,CAACN,EAAOC,EAAI,sHAAuH,GAAG,CAAC,QAAU,EAAE,QAAU,CAAC,uCAAuC,MAAQ,GAAG,SAAW,uDAAuD,eAAiB,CAAC,kLAAoL,WAAa,MAElgBI,EAAwBD,OAAS,CAChC,UAAa,cACb,YAAe,iBACf,aAAgB,UAChB,KAAQ,cAEM","file":"static/js/11.df3576a8.chunk.js","sourcesContent":["// import {Redirect} from 'react-router-dom';\nimport { Button } from 'antd';\nimport { PageContent } from '@ra-lib/admin';\nimport config from 'src/commons/config-hoc';\nimport styles from './style.less';\n\nexport default config({\n path: '/',\n title: '首页',\n})(function Home(props) {\n // 如果其他页面作为首页,直接重定向,config中不要设置title,否则tab页中会多个首页\n // return ;\n return (\n \n 首页 \n {process.env.REACT_APP_MOCK ? (\n {\n await props.ajax.post('/initDB', null, { successTip: '数据库重置成功!' });\n setTimeout(() => window.location.reload(), 2000);\n }}\n >\n 重置数据库\n \n ) : null}\n \n );\n});\n","var api = require(\"!../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n var content = require(\"!!../../../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-10-1!../../../node_modules/less-loader/dist/cjs.js??ref--5-oneOf-10-2!./style.less\");\n\n content = content.__esModule ? content.default : content;\n\n if (typeof content === 'string') {\n content = [[module.id, content, '']];\n }\n\nvar options = {};\n\noptions.insert = \"head\";\noptions.singleton = false;\n\nvar update = api(content, options);\n\n\n\nmodule.exports = content.locals || {};","// Imports\nimport ___CSS_LOADER_API_IMPORT___ from \"../../../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(true);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".root_IP7AU {\\n display: flex;\\n flex-direction: column;\\n justify-content: center;\\n align-items: center;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://src/pages/home/style.less\"],\"names\":[],\"mappings\":\"AAEA;EACI,aAAA;EACA,sBAAA;EACA,uBAAA;EACA,mBAAA;AAIJ\",\"sourcesContent\":[\"@import \\\"src/theme.less\\\";\\n\\n.root {\\n display: flex;\\n flex-direction: column;\\n justify-content: center;\\n align-items: center;\\n}\\n\\n@packageName: react-admin;\"],\"sourceRoot\":\"\"}]);\n// Exports\n___CSS_LOADER_EXPORT___.locals = {\n\t\"antPrefix\": \"react-admin\",\n\t\"raLibPrefix\": \"react-admin-ra\",\n\t\"primaryColor\": \"#1890ff\",\n\t\"root\": \"root_IP7AU\"\n};\nexport default ___CSS_LOADER_EXPORT___;\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/docs/build/static/js/13.bff1e6dc.chunk.js:
--------------------------------------------------------------------------------
1 | (this["webpackJsonp_react-admin"]=this["webpackJsonp_react-admin"]||[]).push([[13],{1002:function(e,t,n){"use strict";n.r(t),n.d(t,"default",(function(){return d}));n(364);var i=n(255),c=n(13),r=n(9);function d(e){var t,n=((null===e||void 0===e||null===(t=e.match)||void 0===t?void 0:t.params)||{}).src;if((n=window.decodeURIComponent(n))&&"undefined"!==n){var d=Object(c.P)({token:Object(c.J)()});n="".concat(n).concat(n.includes("?")?"&":"?").concat(d)}return Object(r.jsx)(c.m,{fitHeight:!0,style:{padding:0,display:"flex"},children:n&&"undefined"!==n?Object(r.jsx)("iframe",{allowFullScreen:!0,title:n,src:n,style:{border:0,width:"100%",height:"100%",boxSizing:"border-box"}},n):Object(r.jsx)("div",{style:{height:"100%",width:"100%",display:"flex",alignItems:"center",justifyContent:"center"},children:Object(r.jsx)(i.a,{status:"error",title:"\u9875\u9762\u52a0\u8f7d\u5931\u8d25",subTitle:"\u4f20\u9012\u6b63\u786e\u7684 src\uff0c\u5f53\u524d\u83b7\u53d6\u5230\u300c".concat(n,"\u300d")})})})}}}]);
2 | //# sourceMappingURL=13.bff1e6dc.chunk.js.map
--------------------------------------------------------------------------------
/docs/build/static/js/13.bff1e6dc.chunk.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["components/iframe/index.jsx"],"names":["IFrame","props","src","match","params","window","decodeURIComponent","queryStr","queryStringify","token","getToken","includes","fitHeight","style","padding","display","allowFullScreen","title","border","width","height","boxSizing","alignItems","justifyContent","status","subTitle"],"mappings":"wMAGe,SAASA,EAAOC,GAAQ,IAAD,EAC5BC,IAAa,OAALD,QAAK,IAALA,GAAA,UAAAA,EAAOE,aAAP,eAAcC,SAAU,IAAhCF,IAKN,IAHAA,EAAMG,OAAOC,mBAAmBJ,KAGb,cAARA,EAAqB,CAC5B,IAAMK,EAAWC,YAAe,CAC5BC,MAAOC,gBAGXR,EAAG,UAAMA,GAAN,OAAYA,EAAIS,SAAS,KAAO,IAAM,KAAtC,OAA4CJ,GAGnD,OACI,cAAC,IAAD,CACIK,WAAS,EACTC,MAAO,CACHC,QAAS,EACTC,QAAS,QAJjB,SAOKb,GAAe,cAARA,EACJ,wBAEIc,iBAAe,EACfC,MAAOf,EACPA,IAAKA,EACLW,MAAO,CACHK,OAAQ,EACRC,MAAO,OACPC,OAAQ,OACRC,UAAW,eARVnB,GAYT,qBACIW,MAAO,CACHO,OAAQ,OACRD,MAAO,OACPJ,QAAS,OACTO,WAAY,SACZC,eAAgB,UANxB,SASI,mBAAQC,OAAO,QAAQP,MAAM,uCAASQ,SAAQ,sFAAqBvB,EAArB","file":"static/js/13.bff1e6dc.chunk.js","sourcesContent":["import { Result } from 'antd';\nimport { getToken, queryStringify, PageContent } from '@ra-lib/admin';\n\nexport default function IFrame(props) {\n let { src } = props?.match?.params || {};\n\n src = window.decodeURIComponent(src);\n\n // 通过src 传递token\n if (src && src !== 'undefined') {\n const queryStr = queryStringify({\n token: getToken(),\n });\n\n src = `${src}${src.includes('?') ? '&' : '?'}${queryStr}`;\n }\n\n return (\n \n {src && src !== 'undefined' ? (\n \n ) : (\n \n \n
\n )}\n \n );\n}\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/docs/build/static/js/14.e62adf89.chunk.js:
--------------------------------------------------------------------------------
1 | (this["webpackJsonp_react-admin"]=this["webpackJsonp_react-admin"]||[]).push([[14],{959:function(t,n,e){"use strict";e.r(n),e.d(n,"default",(function(){return j}));var c,s=e(365),a=e(366),i=e(367),o=e(368),r=e(0),u=e.n(r),l=e(163),h=e(9);function b(){console.log(this)}var j=Object(l.a)({path:"/test/class"})(c=function(t){Object(i.a)(e,t);var n=Object(o.a)(e);function e(){var t;Object(s.a)(this,e);for(var c=arguments.length,a=new Array(c),i=0;i\n this.test()}>点击 \n 点击 \n 点击 \n {count}\n \n );\n }\n}\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/docs/build/static/js/15.510c57f0.chunk.js:
--------------------------------------------------------------------------------
1 | (this["webpackJsonp_react-admin"]=this["webpackJsonp_react-admin"]||[]).push([[15],{960:function(e,n,t){"use strict";t.r(n);var c=t(13),i=t(163),s=t(9);n.default=Object(i.a)({path:"/auto-deps",connect:function(e){return{name:e.demo.present.name,user:e.demo.present.user}}})((function(e){var n=e.name,t=e.user;return Object(s.jsxs)(c.m,{children:[Object(s.jsx)("div",{onClick:function(){e.action.demo.setName("\u662f\u554a")},children:"setName"}),Object(s.jsx)("div",{onClick:function(){e.action.demo.getUser()},children:"getUser"}),Object(s.jsx)("div",{children:n}),Object(s.jsx)("div",{children:JSON.stringify(t)})]})}))}}]);
2 | //# sourceMappingURL=15.510c57f0.chunk.js.map
--------------------------------------------------------------------------------
/docs/build/static/js/15.510c57f0.chunk.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["pages/local/TestHooks.jsx"],"names":["config","path","connect","state","name","demo","present","user","props","onClick","action","setName","getUser","JSON","stringify"],"mappings":"qHAAA,mCAGeA,sBAAO,CAClBC,KAAM,aACNC,QAAS,SAACC,GACN,MAAO,CACHC,KAAMD,EAAME,KAAKC,QAAQF,KACzBG,KAAMJ,EAAME,KAAKC,QAAQC,QALtBP,EAQZ,SAAkBQ,GAAQ,IACjBJ,EAAeI,EAAfJ,KAAMG,EAASC,EAATD,KAUd,OACI,eAAC,IAAD,WACI,qBAAKE,QAVb,WACID,EAAME,OAAOL,KAAKM,QAAQ,iBAStB,qBACA,qBAAKF,QAPb,WACID,EAAME,OAAOL,KAAKO,WAMd,qBACA,8BAAMR,IACN,8BAAMS,KAAKC,UAAUP","file":"static/js/15.510c57f0.chunk.js","sourcesContent":["import { PageContent } from '@ra-lib/admin';\nimport config from 'src/commons/config-hoc';\n\nexport default config({\n path: '/auto-deps',\n connect: (state) => {\n return {\n name: state.demo.present.name,\n user: state.demo.present.user,\n };\n },\n})(function AutoDeps(props) {\n const { name, user } = props;\n\n function handleClick() {\n props.action.demo.setName('是啊');\n }\n\n function handleClick2() {\n props.action.demo.getUser();\n }\n\n return (\n \n setName
\n getUser
\n {name}
\n {JSON.stringify(user)}
\n \n );\n});\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/docs/build/static/js/3.a462f62e.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
8 | * @license MIT */
9 |
10 | /*!
11 | Copyright (c) 2018 Jed Watson.
12 | Licensed under the MIT License (MIT), see
13 | http://jedwatson.github.io/classnames
14 | */
15 |
16 | /*!
17 | * Determine if an object is a Buffer
18 | *
19 | * @author Feross Aboukhadijeh
20 | * @license MIT
21 | */
22 |
23 | /*! *****************************************************************************
24 | Copyright (c) Microsoft Corporation.
25 |
26 | Permission to use, copy, modify, and/or distribute this software for any
27 | purpose with or without fee is hereby granted.
28 |
29 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
30 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
31 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
32 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
33 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
34 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
35 | PERFORMANCE OF THIS SOFTWARE.
36 | ***************************************************************************** */
37 |
38 | /** @license React v0.20.2
39 | * scheduler.production.min.js
40 | *
41 | * Copyright (c) Facebook, Inc. and its affiliates.
42 | *
43 | * This source code is licensed under the MIT license found in the
44 | * LICENSE file in the root directory of this source tree.
45 | */
46 |
47 | /** @license React v16.13.1
48 | * react-is.production.min.js
49 | *
50 | * Copyright (c) Facebook, Inc. and its affiliates.
51 | *
52 | * This source code is licensed under the MIT license found in the
53 | * LICENSE file in the root directory of this source tree.
54 | */
55 |
56 | /** @license React v17.0.2
57 | * react-dom.production.min.js
58 | *
59 | * Copyright (c) Facebook, Inc. and its affiliates.
60 | *
61 | * This source code is licensed under the MIT license found in the
62 | * LICENSE file in the root directory of this source tree.
63 | */
64 |
65 | /** @license React v17.0.2
66 | * react-jsx-runtime.production.min.js
67 | *
68 | * Copyright (c) Facebook, Inc. and its affiliates.
69 | *
70 | * This source code is licensed under the MIT license found in the
71 | * LICENSE file in the root directory of this source tree.
72 | */
73 |
74 | /** @license React v17.0.2
75 | * react.production.min.js
76 | *
77 | * Copyright (c) Facebook, Inc. and its affiliates.
78 | *
79 | * This source code is licensed under the MIT license found in the
80 | * LICENSE file in the root directory of this source tree.
81 | */
82 |
83 | //! moment.js
84 |
85 | //! moment.js locale configuration
86 |
--------------------------------------------------------------------------------
/docs/build/static/js/runtime-main.bd4dd1cc.js:
--------------------------------------------------------------------------------
1 | !function(e){function t(t){for(var n,o,u=t[0],f=t[1],i=t[2],l=0,s=[];l
2 |
3 |
4 |
5 |
6 | docsify
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 | Loading ...
28 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/docs/lib/css/dark-mode@0.6.1.css:
--------------------------------------------------------------------------------
1 | :root{--text_color:var(--theme-color);--docsify_dark_mode_bg:#fff;--docsify_dark_mode_btn:var(--theme-color)}.sidebar,.sidebar-toggle,aside,body,html,main{background:#fff;background:var(--docsify_dark_mode_bg)}#dark_mode>input[type=checkbox]{height:0;width:0;visibility:hidden}#dark_mode>label{cursor:pointer;text-indent:-9999px;width:55px;height:30px;background:var(--theme-color);background:var(--docsify_dark_mode_btn);margin:0 auto;display:flex;justify-content:center;align-items:center;border-radius:100px;position:relative}#dark_mode>label:after{content:"";background:#fff;width:20px;height:20px;border-radius:50%;position:absolute;top:5px;left:4px;transition:.32s cubic-bezier(.68,-.55,.27,1.55)}#dark_mode>input:checked+label{background:var(--theme-color);background:var(--docsify_dark_mode_btn)}#dark_mode>input:checked+label:after{left:calc(100% - 5px);transform:translateX(-100%)}html.transition,html.transition *,html.transition :after,html.transition :before{transition:.42s cubic-bezier(.68,-.55,.27,1.55)!important;transition-delay:0!important}#dark_mode{position:absolute;right:0;top:0}p{color:var(--theme-color);color:var(--text_color)}
--------------------------------------------------------------------------------
/docs/lib/js/codefund.js:
--------------------------------------------------------------------------------
1 | ;(function(window) {
2 | window.DocsifyCodefund = {
3 | scriptEl: null,
4 | create: function(codefundId) {
5 | return function(hook, vm) {
6 | hook.ready(function() {
7 | window.DocsifyCodefund.injectCodeFundStyle();
8 | })
9 |
10 | hook.doneEach(function () {
11 | window.DocsifyCodefund.injectCodeFundScript(codefundId);
12 | window.DocsifyCodefund.injectCodeFundContainer();
13 | })
14 | }
15 | },
16 |
17 | injectCodeFundContainer() {
18 | if (document.getElementById("codefund") === null) {
19 | let nav = document.getElementsByClassName('sidebar-nav');
20 | let codefundContainer = document.createElement('div');
21 | codefundContainer.id = 'codefund';
22 | nav[0].insertBefore(codefundContainer, nav[0].firstChild);
23 | }
24 |
25 | if (document.getElementsByClassName('cf-wrapper').length === 0) {
26 | window._codefund && window._codefund.serve();
27 | }
28 | },
29 |
30 | injectCodeFundScript(codefundId) {
31 | if (window.DocsifyCodefund.scriptEl) {
32 | window.DocsifyCodefund.scriptEl.remove()
33 | }
34 |
35 | var script = document.createElement('script')
36 | script.src = "https://codefund.io/properties/" + codefundId + "/funder.js?template=docsify"
37 | script.async = "async"
38 | document.body.appendChild(script)
39 | window.DocsifyCodefund.scriptEl = script
40 | },
41 |
42 | injectCodeFundStyle() {
43 | var style = document.createElement('style');
44 | window.DocsifyCodefund.injectCodeFundContainer();
45 | var css = '#cf span.cf-wrapper { background-color: transparent!important } #cf a.cf-text { color: #444!important; font-weight: 400!important; font-size: 10px!important }';
46 |
47 | style.type = 'text/css';
48 | if (style.styleSheet){
49 | style.styleSheet.cssText = css;
50 | } else {
51 | style.appendChild(document.createTextNode(css));
52 | }
53 | document.head.appendChild(style);
54 | }
55 | }
56 | })(window)
--------------------------------------------------------------------------------
/docs/lib/js/dark-mode@0.6.1.js:
--------------------------------------------------------------------------------
1 | "use strict";window.$docsify.plugins=[].concat((t,e)=>{var o,n=()=>{document.documentElement.classList.add("transition"),window.setTimeout(()=>{document.documentElement.classList.remove("transition")},800)},c=({background:t,toggleBtnBg:e,textColor:o})=>{document.documentElement.style.setProperty("--docsify_dark_mode_bg",t),document.documentElement.style.setProperty("--docsify_dark_mode_btn",e),document.documentElement.style.setProperty("--text_color",o)};o={dark:{background:"#1c2022",toggleBtnBg:"#34495e",textColor:"#b4b4b4"},light:{background:"white",toggleBtnBg:"var(--theme-color)",textColor:"var(--theme-color)"},...e.config.darkMode},t.afterEach((function(t,e){e(t=` \n \n Toggle \n
${t}`)})),t.doneEach((function(){var t;localStorage.getItem("DOCSIFY_DARK_MODE")?(t=localStorage.getItem("DOCSIFY_DARK_MODE"),c(o[`${t}`])):(t="light",c(o.light)),document.querySelector("input[name=mode]").addEventListener("change",(function(){"light"===t?(n(),c(o.dark),localStorage.setItem("DOCSIFY_DARK_MODE","dark"),t="dark"):(n(),c(o.light),localStorage.setItem("DOCSIFY_DARK_MODE","light"),t="light")}))}))},window.$docsify.plugins);
--------------------------------------------------------------------------------
/docs/lib/js/prism-markdown.min.js:
--------------------------------------------------------------------------------
1 | !function(d){function n(n,e){return n=n.replace(//g,"(?:\\\\.|[^\\\\\\n\r]|(?:\r?\n|\r)(?!\r?\n|\r))"),e&&(n=n+"|"+n.replace(/_/g,"\\*")),RegExp("((?:^|[^\\\\])(?:\\\\{2})*)(?:"+n+")")}var e="(?:\\\\.|``.+?``|`[^`\r\\n]+`|[^\\\\|\r\\n`])+",t="\\|?__(?:\\|__)+\\|?(?:(?:\r?\n|\r)|$)".replace(/__/g,e),a="\\|?[ \t]*:?-{3,}:?[ \t]*(?:\\|[ \t]*:?-{3,}:?[ \t]*)+\\|?(?:\r?\n|\r)";d.languages.markdown=d.languages.extend("markup",{}),d.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+t+a+"(?:"+t+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+t+a+")(?:"+t+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(e),inside:d.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+t+")"+a+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+t+"$"),inside:{"table-header":{pattern:RegExp(e),alias:"important",inside:d.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/(^[ \t]*(?:\r?\n|\r))(?: {4}|\t).+(?:(?:\r?\n|\r)(?: {4}|\t).+)*/m,lookbehind:!0,alias:"keyword"},{pattern:/``.+?``|`[^`\r\n]+`/,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\r?\n|\r))[\s\S]+?(?=(?:\r?\n|\r)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\r?\n|\r)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:n("__(?:(?!_)|_(?:(?!_))+_)+__",!0),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:n("_(?:(?!_)|__(?:(?!_))+__)+_",!0),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:n("(~~?)(?:(?!~))+?\\2",!1),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},url:{pattern:n('!?\\[(?:(?!\\]))+\\](?:\\([^\\s)]+(?:[\t ]+"(?:\\\\.|[^"\\\\])*")?\\)| ?\\[(?:(?!\\]))+\\])',!1),lookbehind:!0,greedy:!0,inside:{variable:{pattern:/(\[)[^\]]+(?=\]$)/,lookbehind:!0},content:{pattern:/(^!?\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),["url","bold","italic","strike"].forEach(function(e){["url","bold","italic","strike"].forEach(function(n){e!==n&&(d.languages.markdown[e].inside.content.inside[n]=d.languages.markdown[n])})}),d.hooks.add("after-tokenize",function(n){"markdown"!==n.language&&"md"!==n.language||!function n(e){if(e&&"string"!=typeof e)for(var t=0,a=e.length;t"==d[0]?null!=u?u[v]=_(l.shift()):r[v]=_(l.shift()):null!=u?u[v]=N(d):r[v]=N(d)}else null!=u?u[v]=e(l):r[v]=e(l)}else{if(g.match(/^-\s*$/)){f&&(f=!1,void 0===r.length&&(r=[])),null!=u&&r.push(u),u={},f=!0;continue}if(t=g.match(/^-\s*(.*)/)){null!=u?u.push(N(t[1])):(f&&(f=!1,void 0===r.length&&(r=[])),r.push(N(t[1])));continue}}}null!=u&&(f&&(f=!1,void 0===r.length&&(r=[])),r.push(u))}for(h=s.length-1;0<=h;--h)n.splice.call(n,s[h],1);return r}(e.children)}function l(e){b=[],y=[],t=(new Date).getTime();var n=r(function(e){var n,t=R.regLevel,r=R.invalidLine,i=e.split("\n"),l=0,u=0,a=[],s=new m(-1),f=new m(0);s.addChild(f);var h=[],o="";a.push(f),h.push(l);for(var c=0,p=i.length;c
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
24 | 加载中...
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import { ConfigProvider } from 'antd';
3 | import { Helmet } from 'react-helmet';
4 | import { Provider } from 'react-redux';
5 | import zhCN from 'antd/lib/locale-provider/zh_CN';
6 | import moment from 'moment';
7 | import 'moment/locale/zh-cn'; // 解决antd日期相关组件国际化问题
8 | import { ComponentProvider, Loading, getLoginUser, setLoginUser /*queryParse,*/ } from '@ra-lib/admin';
9 | import { Generator } from 'src/components';
10 | import { isNoAuthPage } from 'src/commons';
11 | import AppRouter from './router/AppRouter';
12 | import { APP_NAME, CONFIG_HOC, IS_MOBILE } from 'src/config';
13 | import { store } from 'src/models';
14 | import api from 'src/api';
15 | import theme from 'src/theme.less';
16 | import './App.less';
17 |
18 | // 设置语言
19 | moment.locale('zh-cn');
20 |
21 | // 设置 Modal、Message、Notification rootPrefixCls。
22 | ConfigProvider.config({
23 | prefixCls: theme.antPrefix,
24 | });
25 |
26 | export default function App(props) {
27 | const { children } = props;
28 | const [loading, setLoading] = useState(true);
29 | const [menus, setMenus] = useState([]);
30 | const [collectedMenus, setCollectedMenus] = useState(CONFIG_HOC.showCollectedMenus ? [] : null);
31 | const handleMenuCollect = useCallback(async (menu, collected) => {
32 | await api.saveCollectedMenu({ menuId: menu.id, collected });
33 |
34 | const collectedMenus = await api.getCollectedMenus();
35 | setCollectedMenus(collectedMenus);
36 | }, []);
37 |
38 | // 一些初始化工作
39 | useEffect(() => {
40 | // 不需要登录的页面不请求
41 | if (isNoAuthPage()) return setLoading(false);
42 |
43 | // 获取用户菜单、权限等
44 | (async () => {
45 | try {
46 | let loginUser = getLoginUser();
47 | if (!loginUser) {
48 | // 嵌入iframe等方式,没有经过登录页面,没有设置loginUser,需要请求loginUser
49 | // 发请求,获取loginUser
50 | // loginUser = await api.getLoginUser();
51 | //
52 | // const {token} = queryParse();
53 | // if (token) loginUser.token = token;
54 | //
55 | // setLoginUser(loginUser);
56 |
57 | return setLoading(false);
58 | }
59 |
60 | // 用户收藏菜单 使用then catch 防止报错后续接口阻断
61 | // 用户收藏菜单
62 | if (CONFIG_HOC.showCollectedMenus) {
63 | await api.getCollectedMenus().then(setCollectedMenus).catch(console.error);
64 | }
65 |
66 | // 获取用户菜单
67 | await api.getMenus().then(setMenus).catch(console.error);
68 |
69 | // 获取用户权限
70 | await api
71 | .getPermissions()
72 | .then((res) => {
73 | loginUser.permissions = res;
74 | setLoginUser(loginUser);
75 | })
76 | .catch(console.error);
77 | } finally {
78 | setLoading(false);
79 | }
80 | })();
81 | }, []);
82 |
83 | // 加载完成后渲染,确保能拿到permissions等数据
84 | return (
85 |
86 |
87 |
88 |
93 | {loading ? (
94 |
95 | ) : children ? (
96 | children
97 | ) : (
98 |
99 | )}
100 | {process.env.NODE_ENV === 'development' ? : null}
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/App.less:
--------------------------------------------------------------------------------
1 | @import 'src/theme';
2 |
3 | :global {
4 | // 全局样式,轻易不要编写
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | min-height: 100vh;
12 | /*overflow-y: scroll;*/
13 | }
14 |
15 | #root {
16 | overflow: auto; // BFC PageContent margin-bottom 不会被合并
17 | }
18 |
19 | code {
20 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
21 | monospace;
22 | }
23 |
24 | /* 只在打印时显示 */
25 |
26 | .just-print {
27 | display: none !important;
28 | }
29 |
30 | @media print {
31 | body {
32 | padding: 0 !important;
33 | background: none;
34 | }
35 |
36 | /* 打印时不显示 */
37 | .no-print {
38 | display: none !important;
39 | }
40 |
41 | .just-print {
42 | display: block !important;
43 | }
44 |
45 | .@{ant-prefix}-message {
46 | display: none !important;
47 | }
48 |
49 | .@{ant-prefix}-modal-mask {
50 | display: none !important;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 使用 require.context 自动引入所有model文件
3 | * */
4 | const result = {};
5 |
6 | // src/models目录下,不支持子文件夹
7 | const req = require.context('./', false, /\.js$/);
8 | req.keys().forEach((key) => {
9 | if (['./index.js'].includes(key)) return;
10 |
11 | const model = req(key);
12 |
13 | const options = model.default;
14 | Object.entries(options).forEach(([k, value]) => {
15 | if (k in result) throw Error(`${key} 文件中 key 「${k}」已被使用!请更换!`);
16 | result[k] = value;
17 | });
18 | });
19 |
20 | export default result;
21 |
--------------------------------------------------------------------------------
/src/api/system.js:
--------------------------------------------------------------------------------
1 | import ajax from 'src/commons/ajax';
2 | import { getLoginUser, isLoginPage, formatMenus, getContainerId } from '@ra-lib/admin';
3 | import { isNoAuthPage } from 'src/commons';
4 | import { IS_SUB } from 'src/config';
5 |
6 | export default {
7 | /**
8 | * 获取菜单
9 | * @returns {Promise<*[]|*>}
10 | */
11 | async getMenuData() {
12 | // 非登录页面,不加载菜单
13 | if (isNoAuthPage()) return [];
14 |
15 | // 作为子应用,不加载
16 | if (IS_SUB) return [];
17 |
18 | // 获取服务端数据,并做缓存,防止多次调用接口
19 | return (this.getMenuData.__CACHE =
20 | this.getMenuData.__CACHE ||
21 | ajax
22 | .get('/authority/queryUserMenus', { userId: getLoginUser()?.id })
23 | .then((res) => res.map((item) => ({ ...item, order: item.order ?? item.ord ?? item.sort })))
24 | .catch(() => []));
25 |
26 | // 前端硬编码菜单
27 | // return [
28 | // {id: 1, title: '系统管理', order: 900, type: 1},
29 | // {id: 2, parentId: 1, title: '用户管理', path: '/users', order: 900, type: 1},
30 | // {id: 3, parentId: 1, title: '角色管理', path: '/roles', order: 900, type: 1},
31 | // {id: 4, parentId: 1, title: '菜单管理', path: '/menus', order: 900, type: 1},
32 | // ];
33 | },
34 | /**
35 | * 获取系统菜单
36 | * @returns {Promise}
37 | */
38 | async getMenus() {
39 | // mock时,做个延迟处理,否则菜单请求无法走mock
40 | if (process.env.REACT_APP_MOCK) await new Promise((resolve) => setTimeout(resolve));
41 |
42 | const serverMenus = await this.getMenuData();
43 | const menus = serverMenus
44 | .filter((item) => !item.type || item.type === 1)
45 | .map((item) => {
46 | return {
47 | ...item,
48 | id: `${item.id}`,
49 | parentId: `${item.parentId}`,
50 | };
51 | });
52 |
53 | return formatMenus(menus);
54 | },
55 | /**
56 | * 获取用户收藏菜单
57 | * @returns {Promise<*>}
58 | */
59 | async getCollectedMenus() {
60 | // 登录页面,不加载
61 | if (isLoginPage()) return [];
62 |
63 | // 作为子应用,不加载
64 | if (IS_SUB) return [];
65 |
66 | const loginUser = getLoginUser();
67 | const data = await ajax.get('/authority/queryUserCollectedMenus', { userId: loginUser?.id });
68 | // const data = [];
69 |
70 | const menus = data.filter((item) => item.type === 1).map((item) => ({ ...item, isCollectedMenu: true }));
71 |
72 | return formatMenus(menus);
73 | },
74 | /**
75 | * 保存用户收藏菜单
76 | * @param menuId
77 | * @param collected
78 | * @returns {Promise}
79 | */
80 | async saveCollectedMenu({ menuId, collected }) {
81 | await ajax.post('/authority/addUserCollectMenu', { userId: getLoginUser()?.id, menuId, collected });
82 | },
83 | /**
84 | * 获取用户权限码
85 | * @returns {Promise<*[string]>}
86 | */
87 | async getPermissions() {
88 | const serverMenus = await this.getMenuData();
89 | return serverMenus.filter((item) => item.type === 2).map((item) => item.code);
90 | },
91 | /**
92 | * 获取子应用配置
93 | * @returns {Promise<*[{title, name, entry}]>}
94 | */
95 | async getSubApps() {
96 | // 从菜单数据中获取需要注册的乾坤子项目
97 | const menuTreeData = (await this.getMenus()) || [];
98 |
99 | // 传递给子应用的数据
100 | const loginUser = getLoginUser();
101 | const props = {
102 | mainApp: {
103 | loginUser: loginUser,
104 | token: loginUser?.token,
105 | },
106 | };
107 | let result = [];
108 | const loop = (nodes) =>
109 | nodes.forEach((node) => {
110 | const { _target, children } = node;
111 | if (_target === 'qiankun') {
112 | const { title, name, entry } = node;
113 | const container = `#${getContainerId(name)}`;
114 | const activeRule = `/${name}`;
115 |
116 | result.push({
117 | title,
118 | name,
119 | entry,
120 | container,
121 | activeRule,
122 | props,
123 | });
124 | }
125 | if (children?.length) loop(children);
126 | });
127 | loop(menuTreeData);
128 |
129 | return result;
130 | },
131 | };
132 |
--------------------------------------------------------------------------------
/src/commons/ajax.js:
--------------------------------------------------------------------------------
1 | import { getToken, Ajax, createAjaxHoc, createAjaxHooks } from '@ra-lib/admin';
2 | import { AJAX_PREFIX, AJAX_TIMEOUT } from 'src/config';
3 | import handleError from './handle-error';
4 | import handleSuccess from './handle-success';
5 |
6 | // 创建Ajax实例,设置默认值
7 | const ajax = new Ajax({
8 | baseURL: AJAX_PREFIX,
9 | timeout: AJAX_TIMEOUT,
10 | onError: handleError,
11 | onSuccess: handleSuccess,
12 | // withCredentials: true, // 跨域携带cookie,对应后端 Access-Control-Allow-Origin不可以为 '*',需要指定为具体域名
13 | });
14 |
15 | // 请求拦截
16 | ajax.instance.interceptors.request.use(
17 | (cfg) => {
18 | if (!cfg.headers) cfg.headers = {};
19 | // 这里每次请求都会动态获取,放到创建实例中,只加载一次,有时候会出问题。
20 | cfg.headers['auth-token'] = getToken();
21 | return cfg;
22 | },
23 | (error) => {
24 | // Do something with request error
25 | return Promise.reject(error);
26 | },
27 | );
28 |
29 | // 响应拦截
30 | ajax.instance.interceptors.response.use(
31 | (res) => {
32 | // Do something before response
33 |
34 | // 后端自定义失败,前端直接抛出,走handleError逻辑
35 | // if (typeof res.data === 'object' && 'code' in res.data && res.data.code !== 0) return Promise.reject(res.data);
36 |
37 | return res;
38 | },
39 | (error) => {
40 | // Do something with response error
41 | return Promise.reject(error);
42 | },
43 | );
44 |
45 | const hooks = createAjaxHooks(ajax);
46 | const hoc = createAjaxHoc(ajax);
47 |
48 | export default ajax;
49 |
50 | export const ajaxHoc = hoc;
51 |
52 | export const get = ajax.get;
53 | export const post = ajax.post;
54 | export const put = ajax.put;
55 | export const del = ajax.del;
56 | export const patch = ajax.patch;
57 | export const download = ajax.download;
58 |
59 | export const useGet = hooks.useGet;
60 | export const usePost = hooks.usePost;
61 | export const usePut = hooks.usePut;
62 | export const useDel = hooks.useDel;
63 | export const usePatch = hooks.usePatch;
64 | export const useDownload = hooks.useDownload;
65 |
--------------------------------------------------------------------------------
/src/commons/config-hoc.js:
--------------------------------------------------------------------------------
1 | import { withRouter } from 'react-router-dom';
2 | import { createConfigHoc, modal as modalHoc, drawer as drawerHoc, getQuery, getLoginUser } from '@ra-lib/admin';
3 | import { ajaxHoc } from 'src/commons/ajax';
4 | import { connect as reduxConnect } from 'src/models';
5 | import { CONFIG_HOC, IS_MOBILE } from 'src/config';
6 | import { layoutHoc } from 'src/components/layout';
7 | import React from 'react';
8 |
9 | // 公共高阶组件,注入一些常用数据,比如 query loginUser等
10 | function commonHoc(options) {
11 | const { query, loginUser } = options;
12 | return (WrappedComponent) => {
13 | const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
14 |
15 | const WithLayout = (props) => {
16 | // 默认添加属性到props中的属性
17 | const extendProps = {};
18 | if (query !== false) extendProps.query = getQuery();
19 | if (loginUser !== false) extendProps.loginUser = getLoginUser();
20 |
21 | return ;
22 | };
23 |
24 | WithLayout.displayName = `WithCommon(${componentName})`;
25 |
26 | return WithLayout;
27 | };
28 | }
29 |
30 | export default function configHoc(options = {}) {
31 | // config 所有可用参数,以及默认值
32 | const {
33 | // 路由地址
34 | path,
35 | // 是否需要登录
36 | auth,
37 | // 是否显示顶部
38 | header,
39 | // 是否显示标签
40 | tab,
41 | // 是否显示页面头部
42 | pageHeader,
43 | // 是否显示侧边栏
44 | side,
45 | // 侧边栏是否收起
46 | sideCollapsed,
47 | // 设置选中菜单,默认基于 window.location选中 用于设置非菜单的子页面,菜单选中状态
48 | selectedMenuPath,
49 | // 设置页面、tab标题,默认基于选中菜单,也可以通过query string 设置 /xxx?title=页面标题
50 | title,
51 | // 自定义面包屑导航,默认基于选中菜单,false:不显示,[{icon, title, path}, ...]
52 | breadcrumb,
53 | // 基于菜单,追加面包屑导航
54 | appendBreadcrumb,
55 | // 页面保持,不销毁,需要设置config.KEEP_PAGE_ALIVE === true 才生效
56 | keepAlive,
57 | // 是否添加withRouter高级组件
58 | router = true,
59 | // props是否注入ajax
60 | ajax = CONFIG_HOC.ajax,
61 | // 连接models,扩展 props.action
62 | connect = CONFIG_HOC.connect,
63 | // 弹框高阶组件
64 | modal,
65 | // 抽屉高级组件
66 | drawer,
67 | ...others
68 | } = options;
69 |
70 | // config 传递 参数校验
71 | if (modal && drawer) throw Error('[config hoc] modal and drawer config can not be used together!');
72 |
73 | const hoc = [];
74 |
75 | // 公共高阶组件
76 | hoc.push(commonHoc(options));
77 |
78 | // 弹框高阶组件
79 | if (modal) hoc.push(modalHoc(modal, IS_MOBILE));
80 |
81 | // 抽屉高阶组件
82 | if (drawer) hoc.push(drawerHoc(drawer));
83 |
84 | // redux 连接高阶组件
85 | if (connect === true) hoc.push(reduxConnect());
86 | if (typeof connect === 'function') hoc.push(reduxConnect(connect));
87 |
88 | // ajax高阶组件
89 | if (ajax) hoc.push(ajaxHoc());
90 |
91 | // 路由高阶组件
92 | if (router) hoc.push(withRouter);
93 |
94 | // 放到最后,一些函数式配置,可以获取到更多的props数据
95 | hoc.push(layoutHoc(options));
96 |
97 | return createConfigHoc({
98 | hoc,
99 | onConstructor: () => void 0,
100 | onDidMount: () => void 0,
101 | onUnmount: () => void 0,
102 | })({ ...options, ...others });
103 | }
104 |
--------------------------------------------------------------------------------
/src/commons/handle-error.js:
--------------------------------------------------------------------------------
1 | import { notification, Modal } from 'antd';
2 | import { toLogin } from './index';
3 |
4 | const ERROR_SERVER = '系统开小差了,请稍后再试或联系管理员!';
5 | const ERROR_NOT_FOUND = '您访问的资源不存在!';
6 | const ERROR_FORBIDDEN = '您无权访问!';
7 | const ERROR_UNKNOWN = '未知错误';
8 | const TIP_TITLE = '温馨提示';
9 | const TIP = '失败';
10 |
11 | function getErrorTip(error, tip) {
12 | if (tip && tip !== true) return tip;
13 |
14 | // http 状态码相关
15 | if (error?.response) {
16 | const { status } = error.response;
17 |
18 | if (status === 401) return toLogin();
19 | if (status === 403) return ERROR_FORBIDDEN;
20 | if (status === 404) return ERROR_NOT_FOUND;
21 | if (status >= 500) return ERROR_SERVER;
22 | }
23 |
24 | // 后端自定义信息
25 | const data = error?.response?.data || error;
26 |
27 | if (typeof data === 'string') return data;
28 | if (data?.message) return data.message;
29 | if (data?.msg) return data.msg;
30 |
31 | return ERROR_UNKNOWN;
32 | }
33 |
34 | export default function handleError({ error, tip, options = {} }) {
35 | const description = getErrorTip(error, tip);
36 | const { errorModal } = options;
37 |
38 | if (!description && !errorModal) return;
39 |
40 | // 避免卡顿
41 | setTimeout(() => {
42 | // 弹框提示
43 | if (errorModal) {
44 | // 详细配置
45 | if (typeof errorModal === 'object') {
46 | return Modal.error({
47 | title: TIP_TITLE,
48 | content: description,
49 | ...errorModal,
50 | });
51 | }
52 |
53 | return Modal.error({
54 | title: TIP_TITLE,
55 | content: description,
56 | });
57 | }
58 |
59 | // 右上角滑出提示
60 | notification.error({
61 | message: TIP,
62 | description,
63 | duration: 2,
64 | });
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/src/commons/handle-success.js:
--------------------------------------------------------------------------------
1 | import { notification, Modal } from 'antd';
2 |
3 | const TIP_TITLE = '温馨提示';
4 | const TIP = '成功';
5 |
6 | export default function handleSuccess({ tip, options = {} }) {
7 | const { successModal } = options;
8 |
9 | if (!tip && !successModal) return;
10 |
11 | // 避免卡顿
12 | setTimeout(() => {
13 | // 弹框方式显示提示
14 | if (successModal) {
15 | // 详细配置
16 | if (typeof successModal === 'object') {
17 | return Modal.success({
18 | title: TIP_TITLE,
19 | content: tip,
20 | ...successModal,
21 | });
22 | }
23 |
24 | return Modal.success({
25 | title: TIP_TITLE,
26 | content: successModal,
27 | });
28 | }
29 |
30 | notification.success({
31 | message: TIP,
32 | description: tip,
33 | duration: 2,
34 | });
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/commons/index.js:
--------------------------------------------------------------------------------
1 | import {match} from 'path-to-regexp';
2 | import moment from 'moment';
3 | import {isActiveApp} from '../qiankun';
4 | import api from 'src/api';
5 | import {BASE_NAME, HASH_ROUTER, IS_SUB, NO_AUTH_ROUTES} from '../config';
6 | import {getMainApp, isLoginPage, getParentOrigin} from '@ra-lib/admin';
7 | import pageConfigs from 'src/pages/page-configs';
8 |
9 | /**
10 | * 浏览器跳转,携带baseName hash等
11 | * @param href
12 | * @returns {string|*}
13 | */
14 | export function locationHref(href) {
15 | if (href?.startsWith('http')) return (window.location.href = href);
16 |
17 | if (href && BASE_NAME && href.startsWith(BASE_NAME)) href = href.replace(BASE_NAME, '');
18 |
19 | const hash = HASH_ROUTER ? '#' : '';
20 |
21 | return (window.location.href = `${BASE_NAME}${hash}${href}`);
22 | }
23 |
24 | /**
25 | * 进入首页
26 | */
27 | export function toHome() {
28 | // 跳转页面,优先跳转上次登出页面
29 | let lastHref = window.sessionStorage.getItem('last-href') || '/';
30 |
31 | const url = lastHref.startsWith('http') ? new URL(lastHref) : {pathname: '/'};
32 |
33 | // 上次是非登录页面,直接跳转首页
34 | if (isNoAuthPage(url.pathname)) lastHref = '/';
35 |
36 | locationHref(lastHref);
37 |
38 | if (HASH_ROUTER) window.location.reload();
39 | }
40 |
41 | /**
42 | * 跳转到登录页面
43 | */
44 | export function toLogin() {
45 | const loginPath = '/login';
46 |
47 | // 判断当前页面是否已经是login页面,如果是,直接返回,不进行跳转,防止出现跳转死循环
48 | if (isLoginPage()) return null;
49 |
50 | // 清除相关数据
51 | window.sessionStorage.clear();
52 | window.sessionStorage.setItem('last-href', window.location.href);
53 |
54 | if (IS_SUB) {
55 | // 微前端,跳转主应用登录
56 | const mainToLogin = getMainApp()?.toLogin;
57 | if (mainToLogin) return mainToLogin();
58 |
59 | // 嵌入iframe中
60 | const parentOrigin = getParentOrigin();
61 | if (parentOrigin) return (window.location.href = `${parentOrigin}/error-401.html`);
62 | }
63 |
64 | locationHref(loginPath);
65 |
66 | if (HASH_ROUTER) window.location.reload();
67 |
68 | return null;
69 | }
70 |
71 | /**
72 | * 检测路由配置冲突
73 | * @param result
74 | * @returns {string|boolean}
75 | */
76 | export async function checkPath(result) {
77 | const subApps = await api.getSubApps();
78 |
79 | const hasHome = result.some(({path}) => path === '/');
80 | if (!hasHome) throw Error(`必须含有首页路由,path: '/', 如果需要其他页面做首页,可以进行 Redirect`);
81 |
82 | result
83 | .filter(({path}) => !!path)
84 | .forEach(({path, filePath}) => {
85 | // 是否与子项目配置冲突
86 | const app = subApps.find((item) => isActiveApp(item, path));
87 | if (app)
88 | throw Error(
89 | `路由地址:「${path}」 与 子项目 「${
90 | app.title || app.name
91 | }」 激活规则配置冲突,对应文件文件如下:\n${filePath}`,
92 | );
93 |
94 | // 自身路由配置是否冲突
95 | const exit = result.find(({filePath: f, path: p}) => {
96 | if (f === filePath) return false;
97 |
98 | if (!p || !path) return false;
99 |
100 | if (p === path) return true;
101 |
102 | return match(path, {decode: decodeURIComponent})(p) || match(p, {decode: decodeURIComponent})(path);
103 | });
104 | if (exit)
105 | throw Error(
106 | `路由地址:「${path}」 与 「${exit.path}」 配置冲突,对应文件文件如下:\n${filePath}\n${exit.filePath}`,
107 | );
108 | });
109 | }
110 |
111 | /**
112 | * 基于 window.location.pathname pageConfig 获取当前页面config高级组件参数
113 | * @returns {{}|*}
114 | */
115 | export function getCurrentPageConfig() {
116 | let {pathname, hash} = window.location;
117 | if (HASH_ROUTER) {
118 | pathname = hash.replace('#', '').split('?')[0];
119 | } else if (BASE_NAME) {
120 | pathname = pathname.replace(BASE_NAME, '');
121 | }
122 |
123 | const config = pageConfigs.find(({path}) => path && match(path, {decode: decodeURIComponent})(pathname));
124 |
125 | return config || {};
126 | }
127 |
128 | /**
129 | * 加载js文件
130 | * @param url
131 | * @returns {Promise}
132 | */
133 | export function loadScript(url) {
134 | return new Promise((resolve, reject) => {
135 | const script = document.createElement('script');
136 | script.onload = resolve;
137 | script.src = url;
138 | script.onerror = reject;
139 | document.head.appendChild(script);
140 | });
141 | }
142 |
143 | /**
144 | * 不需要登录的页面配置
145 | * @param pathname
146 | * @returns {boolean}
147 | */
148 | export function isNoAuthPage(pathname) {
149 | return NO_AUTH_ROUTES.includes(pathname || window.location.pathname);
150 | }
151 |
152 | // table column 渲染时间
153 | export function renderTime(format = 'YYYY-MM-DD HH:mm:ss') {
154 | return (value) => {
155 | if (!value) return '-';
156 |
157 | return moment(value).format(format);
158 | };
159 | }
160 |
161 | // table column 渲染日期
162 | export function renderDate(format = 'YYYY-MM-DD') {
163 | return (value) => {
164 | if (!value) return '-';
165 |
166 | return moment(value).format(format);
167 | };
168 | }
169 |
--------------------------------------------------------------------------------
/src/components/error/Error404.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Error404 } from '@ra-lib/admin';
3 | import { getCurrentActiveSubApp } from 'src/qiankun';
4 | import { IS_SUB } from 'src/config';
5 |
6 | export default function MyError404() {
7 | const [isSubApp, setIsSubApp] = useState(true);
8 |
9 | const { pathname } = window.location;
10 |
11 | // 检测是否是子应用
12 | useEffect(() => {
13 | (async () => {
14 | const subApp = await getCurrentActiveSubApp();
15 | setIsSubApp(!!subApp);
16 | })();
17 | }, [pathname]);
18 |
19 | if (isSubApp && !IS_SUB) return null;
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/footer/index.jsx:
--------------------------------------------------------------------------------
1 | // import styles from './style.less';
2 |
3 | export default function Footer(props) {
4 | return null;
5 | // return (
6 | //
7 | // 京公网安备00000000000001号 京ICP证000001号 ©2021 xxx
8 | //
9 | // );
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/footer/style.less:
--------------------------------------------------------------------------------
1 | .root {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | font-size: 12px;
6 | padding-bottom: 8px;
7 | color: #bbb;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/generator/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Modal, Button } from 'antd';
3 | import { CodeOutlined } from '@ant-design/icons';
4 | import s from './style.less';
5 |
6 | export default function Generator() {
7 | const [visible, setVisible] = useState(false);
8 | return (
9 | <>
10 | setVisible(!visible)}>
11 |
12 |
13 |
24 |
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/generator/style.less:
--------------------------------------------------------------------------------
1 | @import 'src/theme';
2 |
3 | .root {
4 | position: fixed;
5 | right: 24px;
6 | bottom: 24px;
7 | z-index: 9999;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | width: 50px;
12 | height: 50px;
13 | border-radius: 50%;
14 | box-shadow: 0 0 10px #666;
15 | font-size: 20px;
16 | transition: .3s;
17 | color: #fff;
18 |
19 | &:hover {
20 | box-shadow: 0 0 10px @primary-color;
21 | }
22 | }
23 |
24 | .modal {
25 | width: 100vw !important;
26 | max-width: 100vw;
27 | margin: 0;
28 | padding: 0;
29 | top: 0;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/header/PasswordModal.js:
--------------------------------------------------------------------------------
1 | import { Form } from 'antd';
2 | import { ModalContent, FormItem } from '@ra-lib/admin';
3 | import config from 'src/commons/config-hoc';
4 |
5 | export default config({
6 | modal: {
7 | title: '修改密码',
8 | width: 500,
9 | },
10 | })(function PasswordModal(props) {
11 | const { onOk, onCancel } = props;
12 |
13 | function handleSubmit(values) {
14 | alert('TODO 接口对接');
15 | console.log(props.ajax);
16 | onOk();
17 | }
18 |
19 | const layout = {
20 | labelCol: { flex: '100px' },
21 | };
22 |
23 | return (
24 |
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/src/components/header/index.jsx:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 | import {Space, Dropdown, Menu, Avatar} from 'antd';
3 | import {DownOutlined, LockOutlined, LogoutOutlined} from '@ant-design/icons';
4 | import {getColor, FullScreen} from '@ra-lib/admin';
5 | import {IS_MOBILE} from 'src/config';
6 | import config from 'src/commons/config-hoc';
7 | import {toLogin} from 'src/commons';
8 | import PasswordModal from './PasswordModal';
9 | import styles from './style.less';
10 | import {Proxy} from 'src/components';
11 |
12 | export default config({
13 | router: true,
14 | })(function Header(props) {
15 | const {loginUser = {}} = props;
16 | const [passwordVisible, setPasswordVisible] = useState(false);
17 |
18 | async function handleLogout() {
19 | try {
20 | // await props.ajax.post('/logout', null, {errorTip: false});
21 | alert('TODO 退出登录接口!');
22 | } finally {
23 | // 无论退出成功失败,都跳转登录页面
24 | toLogin();
25 | }
26 | }
27 |
28 | const menu = (
29 |
30 | } onClick={() => setPasswordVisible(true)}>
31 | 修改密码
32 |
33 |
34 | } onClick={handleLogout}>
35 | 退出登录
36 |
37 |
38 | );
39 |
40 | const {avatar, name = ''} = loginUser;
41 |
42 | return (
43 |
50 |
51 |
52 | {IS_MOBILE ? null : (
53 | <>
54 |
55 |
56 |
57 | >
58 | )}
59 |
60 |
61 |
62 | {avatar ? (
63 |
64 | ) : (
65 |
66 | {(name[0] || '').toUpperCase()}
67 |
68 | )}
69 | {IS_MOBILE ? null : (
70 | <>
71 |
{name}
72 |
73 | >
74 | )}
75 |
76 |
77 | setPasswordVisible(false)}
80 | onOk={() => setPasswordVisible(false)}
81 | />
82 |
83 | );
84 | });
85 |
--------------------------------------------------------------------------------
/src/components/header/style.less:
--------------------------------------------------------------------------------
1 | .root {
2 | display: flex;
3 | align-items: center;
4 | justify-content: flex-end;
5 | }
6 |
7 | .action {
8 | cursor: pointer;
9 | display: flex;
10 | align-items: center;
11 |
12 | &:hover {
13 | //opacity: .7;
14 | }
15 | }
16 |
17 | .avatar {
18 | margin-right: 4px;
19 | }
20 |
21 | .userName {
22 | margin-right: 4px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/icon/index.jsx:
--------------------------------------------------------------------------------
1 | import { createFromIconfontCN } from '@ant-design/icons';
2 | import PropTypes from 'prop-types';
3 |
4 | /**
5 | * font icon使用方法:
6 | *
7 | * 参考:
8 | * https://ant.design/components/icon-cn/#components-icon-demo-iconfont
9 | * https://www.iconfont.cn/
10 | * */
11 | const IconFont = createFromIconfontCN({
12 | // 具体项目自己在 iconfont.cn 上创建 scriptUrl
13 | scriptUrl: '//at.alicdn.com/t/font_2363884_0xyjyu18wny.js',
14 | });
15 |
16 | IconFont.propTypes = {
17 | type: PropTypes.string.isRequired,
18 | className: PropTypes.string,
19 | style: PropTypes.object,
20 | rotate: PropTypes.number,
21 | spin: PropTypes.bool,
22 | twoToneColor: PropTypes.string,
23 | };
24 |
25 | export default IconFont;
26 |
--------------------------------------------------------------------------------
/src/components/iframe/index.jsx:
--------------------------------------------------------------------------------
1 | import { Result } from 'antd';
2 | import { getToken, queryStringify, PageContent } from '@ra-lib/admin';
3 |
4 | export default function IFrame(props) {
5 | let { src } = props?.match?.params || {};
6 |
7 | src = window.decodeURIComponent(src);
8 |
9 | // 通过src 传递token
10 | if (src && src !== 'undefined') {
11 | const queryStr = queryStringify({
12 | token: getToken(),
13 | });
14 |
15 | src = `${src}${src.includes('?') ? '&' : '?'}${queryStr}`;
16 | }
17 |
18 | return (
19 |
26 | {src && src !== 'undefined' ? (
27 |
39 | ) : (
40 |
49 |
50 |
51 | )}
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Icon } from './icon';
2 | export { default as Footer } from './footer';
3 | export { default as Logo } from './logo';
4 | export { default as Header } from './header';
5 | export { default as LayoutError404 } from './error/Error404';
6 | export { default as AdminLayout } from './layout';
7 | export { default as Permission } from './permission';
8 | export { default as SubApp } from './sub-app';
9 | export { default as SubError } from './sub-app/SubError';
10 | export { default as Generator } from './generator';
11 | export { default as Proxy } from './proxy';
12 |
--------------------------------------------------------------------------------
/src/components/layout/layout-setting/index.jsx:
--------------------------------------------------------------------------------
1 | import { Tooltip } from 'antd';
2 | import { SettingOutlined } from '@ant-design/icons';
3 | import config from 'src/commons/config-hoc';
4 |
5 | export default config({
6 | router: true,
7 | })(function LayoutSetting(props) {
8 | return (
9 |
10 | props.history.push('/layout/setting?title=布局设置')}
13 | />
14 |
15 | );
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/logo/index.jsx:
--------------------------------------------------------------------------------
1 | import logo from './logo.png';
2 | import { APP_NAME } from 'src/config';
3 | import styles from './style.less';
4 |
5 | export default function Logo(props) {
6 | if (props.image) return logo;
7 |
8 | return (
9 |
10 |
11 |
{APP_NAME}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/src/components/logo/logo.png
--------------------------------------------------------------------------------
/src/components/logo/style.less:
--------------------------------------------------------------------------------
1 | .root {
2 | display: flex;
3 | align-items: center;
4 |
5 | img {
6 | width: 50px;
7 | margin-right: 16px;
8 | }
9 |
10 | h1 {
11 | color: #fff;
12 | margin: 0;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/permission/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {hasPermission} from '@ra-lib/admin';
4 |
5 | /**
6 | * 根据hasPermission 和code 来判断children是否显示
7 | * 一般用于前端权限控制是否显示某个按钮等,一般的项目权限控制到菜单级别即可,很少会控制到功能级别
8 | */
9 |
10 | Permission.propTypes = {
11 | code: PropTypes.string.isRequired,
12 | useDisabled: PropTypes.bool,
13 | };
14 |
15 | Permission.defaultProps = {
16 | useDisabled: false,
17 | };
18 |
19 | export default function Permission(props) {
20 | let {code, useDisabled, children} = props;
21 |
22 | if (!useDisabled) {
23 | return hasPermission(code) ? children : null;
24 | }
25 |
26 | children = Array.isArray(children) ? children : [children];
27 |
28 | return children.map((item) => {
29 | const {key, ref} = item;
30 | return React.cloneElement(item, {
31 | disabled: !hasPermission(code),
32 | key,
33 | ref,
34 | });
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/proxy/index.jsx:
--------------------------------------------------------------------------------
1 | import theme from 'src/theme.less';
2 | import { ApiOutlined, DownOutlined } from '@ant-design/icons';
3 | import proxyConfig from 'src/setupProxyConfig.json';
4 | import { Dropdown, Menu } from 'antd';
5 | import { useState } from 'react';
6 | import { SHOW_PROXY } from 'src/config';
7 |
8 | export default function Proxy(props) {
9 | const { className } = props;
10 | const [selectedKeys, setSelectedKeys] = useState([window.localStorage.getItem('AJAX_PREFIX') || '/api']);
11 |
12 | // 非开发 测试环境 不显示
13 | if (!SHOW_PROXY) return null;
14 |
15 | const serverMenu = (
16 |
17 | {proxyConfig
18 | .filter((item) => !item.disabled)
19 | .map((item) => {
20 | const { baseUrl, name } = item;
21 | return (
22 | }
25 | onClick={() => {
26 | setSelectedKeys([baseUrl]);
27 | window.localStorage.setItem('AJAX_PREFIX', baseUrl);
28 | window.location.reload();
29 | }}
30 | >
31 | {name}
32 |
33 | );
34 | })}
35 |
36 | );
37 |
38 | return (
39 |
40 |
41 |
55 |
56 |
57 |
58 | {proxyConfig.find((item) => selectedKeys?.includes(item.baseUrl))?.name}
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/sub-app/SubError.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { Result } from 'antd';
3 | import { PageContent } from '@ra-lib/admin';
4 | import { getAppByName } from 'src/qiankun';
5 |
6 | export default function SubError(props) {
7 | const { name } = props;
8 | const [app, setApp] = useState(null);
9 | useEffect(() => {
10 | (async () => {
11 | const app = await getAppByName(name);
12 | setApp(app);
13 | })();
14 | }, [name]);
15 |
16 | return (
17 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/sub-app/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getCurrentActiveSubApp } from 'src/qiankun';
3 | import { isActiveApp, getContainerId } from '@ra-lib/admin';
4 | import api from 'src/api';
5 | import config from 'src/commons/config-hoc';
6 |
7 | export default config()(function SubApp() {
8 | const [apps, setApps] = useState([]);
9 | const [activeAppNames, setActiveAppNames] = useState([]);
10 | useEffect(() => {
11 | (async () => {
12 | const apps = await api.getSubApps();
13 | setApps(apps);
14 | })();
15 | }, []);
16 |
17 | const pathname = window.location.pathname;
18 | useEffect(() => {
19 | (async () => {
20 | const app = await getCurrentActiveSubApp();
21 | if (app && !activeAppNames.includes(app.name)) {
22 | setActiveAppNames([...activeAppNames, app.name]);
23 | }
24 | })();
25 | }, [pathname, activeAppNames]);
26 |
27 | return apps.map((app) => {
28 | const { name } = app;
29 | const isActive = isActiveApp(app);
30 | const style = {
31 | display: isActive ? 'block' : 'none',
32 | };
33 |
34 | if (!activeAppNames.includes(name)) return null;
35 |
36 | return
;
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/config/config.development.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/src/config/config.development.js
--------------------------------------------------------------------------------
/src/config/config.production.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/src/config/config.production.js
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | import * as development from './config.development';
2 | import * as production from './config.production';
3 | import appPackage from '../../package.json';
4 | import {storage, getConfigValue, LAYOUT_TYPE} from '@ra-lib/admin';
5 |
6 | const allEnvConfig = {development, production};
7 | const configEnv = process.env.REACT_APP_CONFIG_ENV || process.env.NODE_ENV;
8 | const envConfig = allEnvConfig[configEnv] || {};
9 | const isQianKun = window.__POWERED_BY_QIANKUN__;
10 | const isQianKunPublicPath = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
11 | const appName = appPackage.name;
12 | const isIframe = window.self !== window.top;
13 |
14 | // 从 命令行、环境配置文件中获取配置信息
15 | const c = (key, defaultValue, parse = (value) => value) => getConfigValue(envConfig, key, defaultValue, parse);
16 |
17 | /**
18 | * 所有配置均可通过命令行参数传递,需要添加 REACT_APP_ 前缀,比如:REACT_APP_CONFIG_ENV=test yarn build
19 | * 配置优先级 命令行 > 环境文件 > 默认
20 | * 当前文件为默认配置,会被环境配置、命令行参数覆盖
21 | * */
22 |
23 | // 不需要登录的页面路由白名单
24 | export const NO_AUTH_ROUTES = [
25 | '/login', // 登录
26 | '/register', // 注册
27 | '/password-retrieval', // 找回密码
28 | ];
29 |
30 | // node环境
31 | export const NODE_ENV = process.env.NODE_ENV;
32 | // 实际运行环境,测试、预发布等环境时 NODE_ENV 也为 production,无法区分
33 | export const RUN_ENV = process.env.REACT_APP_RUN_ENV;
34 | // 应用名称
35 | export const APP_NAME = c('APP_NAME', 'React Admin');
36 | // ajax 请求前缀
37 | // 开发环境 或者 测试环境使用 localStorage中存储的前缀
38 | export const SHOW_PROXY = NODE_ENV === 'development' || window.location.hostname === '172.16.143.44';
39 | export const AJAX_PREFIX = SHOW_PROXY
40 | ? window.localStorage.getItem('AJAX_PREFIX') || '/api'
41 | : c('AJAX_PREFIX', isQianKun ? `${isQianKunPublicPath}api` : '/api');
42 | // ajax 超时时间
43 | export const AJAX_TIMEOUT = c('AJAX_TIMEOUT', 1000 * 60, Number);
44 | // 配置环境
45 | export const CONFIG_ENV = process.env.REACT_APP_CONFIG_ENV;
46 | // config-hoc 配置存储key
47 | export const CONFIG_HOC_STORAGE_KEY = 'CONFIG_HOC_STORAGE_KEY';
48 | // 是否有系统概念,顶级菜单将作为系统,角色有系统概念,默认添加子系统管理员角色
49 | export const WITH_SYSTEMS = c('WITH_SYSTEMS', false);
50 | // 页面路由前缀
51 | export const BASE_NAME = c('BASE_NAME', isQianKun ? `/${appName}` : '');
52 | // 是否使用hash路由
53 | export const HASH_ROUTER = c('HASH_ROUTER', false);
54 | // 静态文件前缀
55 | export const PUBLIC_URL = c('PUBLIC_URL', '');
56 | // 是否是开发环境
57 | export const IS_DEV = c('RUN_ENV', RUN_ENV === 'development', (value) => value === 'development');
58 | // 是否是生产环境
59 | export const IS_PROD = c('RUN_ENV', RUN_ENV === 'production', (value) => value === 'production');
60 | // 是否是测试环境
61 | export const IS_TEST = c('RUN_ENV', RUN_ENV === 'test', (value) => value === 'test');
62 | // 是否是预览
63 | export const IS_PREVIEW = c('RUN_ENV', RUN_ENV === 'preview', (value) => value === 'preview');
64 |
65 | // 是否作为乾坤子项目,或者嵌入在iframe中
66 | export const IS_SUB = c('IS_SUB', isQianKun || isIframe);
67 | // 是否是手机布局
68 | export const IS_MOBILE = c('IS_MOBILE', window.document.body.clientWidth <= 575);
69 |
70 | const mobileConfig = IS_MOBILE
71 | ? {
72 | layoutType: LAYOUT_TYPE.SIDE_MENU,
73 | header: true,
74 | side: false,
75 | tab: false,
76 | headerTheme: 'dark',
77 | }
78 | : {};
79 |
80 | // config-hoc 高阶组件、布局默认配置
81 | export const CONFIG_HOC = {
82 | // 是否需要登录
83 | auth: true,
84 | // props是否注入ajax
85 | ajax: true,
86 | // 是否与model连接
87 | connect: true,
88 | // 启用页面保持功能,无特殊需求,尽量不要开启
89 | keepAlive: false,
90 | // layout布局方式 LAYOUT_TYPE.SIDE_MENU LAYOUT_TYPE.TOP_MENU LAYOUT_TYPE.TOP_SIDE_MENU
91 | layoutType: LAYOUT_TYPE.SIDE_MENU,
92 | // 头部是否显示
93 | header: true,
94 | // 侧边栏是否显示
95 | side: true,
96 | // Tabs是否显示
97 | tab: false,
98 | // 持久化 Tabs记录
99 | persistTab: true,
100 | // tab左侧显示展开收起菜单按钮
101 | tabSideToggle: true,
102 | // tab右侧显示额外头部内容
103 | tabHeaderExtra: true,
104 | // tab高度
105 | tabHeight: 40,
106 | // 页面头部是否显示
107 | pageHeader: false,
108 | // 头部主题
109 | headerTheme: 'default', // dark
110 | // 侧边栏主题
111 | sideTheme: 'dark', // dark
112 | // logo主题
113 | logoTheme: 'dark',
114 | // 侧边栏展开宽度
115 | sideMaxWidth: 210,
116 | // 头部显示菜单展开收起按钮
117 | headerSideToggle: true,
118 | // 保持菜单展开状态
119 | keepMenuOpen: true,
120 | // 左侧菜单是否收起
121 | sideCollapsed: false,
122 | // 是否显示搜索菜单
123 | searchMenu: true,
124 | // 是否显示我的收藏菜单
125 | showCollectedMenus: false,
126 | // PageContent组件 fitHeight 时,计算高度所用到的额外高度值,如果页面显示统一的footer,这里设置footer的高度
127 | pageOtherHeight: 0, // 默认footer高度 26
128 |
129 | ...mobileConfig,
130 | ...(storage.local.getItem(CONFIG_HOC_STORAGE_KEY) || {}),
131 | };
132 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | if (window.__POWERED_BY_QIANKUN__) {
2 | // 动态设置 webpack publicPath,防止资源加载出错
3 |
4 | const { PUBLIC_URL = '' } = process.env;
5 |
6 | let publicUrl = PUBLIC_URL.replace('/', '');
7 |
8 | if (publicUrl && !publicUrl.endsWith('/')) publicUrl = `${publicUrl}/`;
9 |
10 | // eslint-disable-next-line no-undef
11 | __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ + publicUrl;
12 | }
13 | /* eslint-disable import/first */
14 | import 'react-app-polyfill/ie11';
15 | import 'react-app-polyfill/stable';
16 | import React from 'react';
17 | import ReactDOM from 'react-dom';
18 | import { notification, Modal, message } from 'antd';
19 | import App from './App';
20 | import { setMainApp } from '@ra-lib/admin';
21 | import qiankun from './qiankun';
22 |
23 | // 开启mock,这个判断不要修改,否则会把mock相关js打入生产包,很大
24 | if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_MOCK) {
25 | require('./mock/index');
26 | console.warn('mock is enabled!!!');
27 | }
28 |
29 | function getRootDom(props) {
30 | const rootId = '#root';
31 | const { container } = props;
32 | return container ? container.querySelector(rootId) : document.querySelector(rootId);
33 | }
34 |
35 | function render(props = {}) {
36 | ReactDOM.render( , getRootDom(props));
37 | }
38 |
39 | // 单独运行时,渲染
40 | if (!window.__POWERED_BY_QIANKUN__) {
41 | render();
42 | }
43 |
44 | // 乾坤主应用
45 | qiankun();
46 |
47 | // 作为乾坤子应用
48 | export async function bootstrap(props) {}
49 |
50 | export async function mount(props) {
51 | setMainApp(props.mainApp);
52 | render(props);
53 | }
54 |
55 | export async function unmount(props) {
56 | // 清理工作
57 | notification.destroy();
58 | message.destroy();
59 | Modal.destroyAll();
60 |
61 | ReactDOM.unmountComponentAtNode(getRootDom(props));
62 | }
63 |
--------------------------------------------------------------------------------
/src/mock/index.js:
--------------------------------------------------------------------------------
1 | import MockAdapter from 'axios-mock-adapter';
2 | import ajax from 'src/commons/ajax';
3 | import { simplify } from './util';
4 |
5 | const mock = new MockAdapter(ajax.instance);
6 |
7 | simplify(mock, [require('./mock-users').default, require('./mock-roles').default, require('./mock-menus').default]);
8 |
--------------------------------------------------------------------------------
/src/mock/mock-roles.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import executeSql from 'src/mock/web-sql';
3 |
4 | export default {
5 | // 获取列表
6 | 'get /role/queryRoleByPage': async (config) => {
7 | const {pageSize, pageNum, name = ''} = config.params;
8 |
9 | const where = `where name like '%${name}%'`;
10 |
11 | if (!pageSize && !pageNum) {
12 | const list = await executeSql(`
13 | select *
14 | from roles ${where}
15 | order by updatedAt desc`);
16 |
17 | await addSystemName(list);
18 |
19 | return [200, list];
20 | }
21 |
22 | const list = await executeSql(
23 | `
24 | select *
25 | from roles ${where}
26 | order by updatedAt desc
27 | limit ? offset ?`,
28 | [pageSize, (pageNum - 1) * pageSize],
29 | );
30 |
31 | const countResult = await executeSql(`
32 | select count(*)
33 | from roles ${where}`);
34 |
35 | const total = countResult[0]['count(*)'] || 0;
36 |
37 | await addSystemName(list);
38 |
39 | return [
40 | 200,
41 | {
42 | totalElements: total,
43 | content: list,
44 | },
45 | ];
46 | },
47 | 'get /role/queryEnabledRoles': async (config) => {
48 | const list = await executeSql(`
49 | select *
50 | from roles
51 | where enabled = 1
52 | order by updatedAt desc
53 | `);
54 |
55 | await addSystemName(list);
56 |
57 | return [200, list];
58 | },
59 | // 获取详情
60 | 'get /role/getRoleDetailById': async (config) => {
61 | const {id} = config.params;
62 |
63 | const result = await executeSql('select * from roles where id = ?', [id]);
64 |
65 | if (!result[0]) return [200, null];
66 |
67 | const roleMenus = await executeSql('select * from role_menus where roleId = ?', [id]);
68 | result[0].menuIds = roleMenus.map((item) => item.menuId);
69 |
70 | return [200, result[0]];
71 | },
72 | // 根据name获取
73 | 'get /role/getOneRole': async (config) => {
74 | const {name, systemId} = config.params;
75 |
76 | const result = await executeSql('select * from roles where name = ? and systemId=?', [name, systemId]);
77 | return [200, result[0]];
78 | },
79 | // 添加
80 | 'post /role/addRole': async (config) => {
81 | const {name, remark = '', enabled, systemId, menuIds} = JSON.parse(config.data);
82 | const args = [systemId, 3, name, remark, enabled ? 1 : 0];
83 | const result = await executeSql(
84 | 'INSERT INTO roles (systemId, type, name, remark, enabled) VALUES (?, ?, ?, ?, ?)',
85 | args,
86 | true,
87 | );
88 | const {insertId: roleId} = result;
89 |
90 | if (menuIds?.length) {
91 | for (let menuId of menuIds) {
92 | await executeSql('INSERT INTO role_menus (roleId, menuId) VALUES (?,?)', [roleId, menuId]);
93 | }
94 | }
95 |
96 | return [200, roleId];
97 | },
98 | // 修改
99 | 'post /role/updateRoleById': async (config) => {
100 | const {id, name, remark = '', enabled, systemId, menuIds} = JSON.parse(config.data);
101 | const args = [enabled ? 1 : 0, systemId, name, remark, moment().format('YYYY-MM-DD HH:mm:ss'), id];
102 |
103 | await executeSql('UPDATE roles SET enabled=?, systemId=?, name=?, remark=?, updatedAt=? WHERE id=?', args);
104 | await executeSql('DELETE FROM role_menus WHERE roleId=?', [id]);
105 |
106 | if (menuIds?.length) {
107 | for (let menuId of menuIds) {
108 | await executeSql('INSERT INTO role_menus (roleId, menuId) VALUES (?,?)', [id, menuId]);
109 | }
110 | }
111 |
112 | return [200, true];
113 | },
114 | // 删除
115 | 'delete re:/role/.+': async (config) => {
116 | const id = config.url.split('/')[2];
117 | await executeSql('DELETE FROM roles WHERE id=?', [id]);
118 | await executeSql('DELETE FROM role_menus WHERE roleId=?', [id]);
119 | return [200, true];
120 | },
121 | };
122 |
123 | async function addSystemName(list) {
124 | const systemIds = list.map((item) => item.systemId).filter((item) => !!item && item !== 'undefined');
125 | if (systemIds && systemIds.length) {
126 | const systems = await executeSql(`
127 | select *
128 | from menus
129 | where id in (${systemIds})
130 | `);
131 | list.forEach((item) => {
132 | const {systemId} = item;
133 | if (systemId) {
134 | const system = systems.find((sys) => sys.id === systemId);
135 | if (system) item.systemName = system.title;
136 | }
137 | });
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/mock/mock-users.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import executeSql, { initDB } from 'src/mock/web-sql';
3 |
4 | export default {
5 | // 重置数据库
6 | 'post /initDB': async (config) => {
7 | await initDB(true);
8 | return [200, true];
9 | },
10 | // 用户登录
11 | 'post /login': async (config) => {
12 | const { account, password } = JSON.parse(config.data);
13 |
14 | const result = await executeSql('select * from users where account=? and password=?', [account, password]);
15 | if (!result?.length) return [400, { message: '用户名或密码错误' }];
16 |
17 | const user = result[0];
18 | user.token = 'test token';
19 |
20 | return [200, user];
21 | },
22 | // 退出登录
23 | 'post /logout': {},
24 | // 获取列表
25 | 'get /user/queryUsersByPage': async (config) => {
26 | const { pageSize = 10, pageNum = 1, account = '', name = '', mobile = '' } = config.params;
27 |
28 | const where = `
29 | where name like '%${name}%'
30 | and mobile like '%${mobile}%'
31 | and account like '%${account}%'
32 | `;
33 |
34 | const list = await executeSql(
35 | `
36 | select *
37 | from users ${where}
38 | order by updatedAt desc
39 | limit ? offset ?`,
40 | [pageSize, (pageNum - 1) * pageSize],
41 | );
42 |
43 | const countResult = await executeSql(`
44 | select count(*)
45 | from users ${where}
46 | `);
47 |
48 | const total = countResult[0]['count(*)'] || 0;
49 |
50 | return [
51 | 200,
52 | {
53 | totalElements: total,
54 | content: list,
55 | },
56 | ];
57 | },
58 | // 获取详情
59 | 'get user/getUserById': async (config) => {
60 | const { id } = config.params;
61 |
62 | const result = await executeSql('select * from users where id = ?', [id]);
63 |
64 | if (!result[0]) return [200, null];
65 |
66 | const userRoles = await executeSql('select * from user_roles where userId = ?', [id]);
67 | result[0].roleIds = userRoles.map((item) => item.roleId);
68 |
69 | return [200, result[0]];
70 | },
71 | // 根据account获取
72 | 'get /user/getOneUser': async (config) => {
73 | const { account } = config.params;
74 |
75 | const result = await executeSql('select * from users where account = ?', [account]);
76 | return [200, result[0]];
77 | },
78 | // 保存用户
79 | 'post /user/addUser': async (config) => {
80 | const { account, name, password, email, mobile, roleIds } = JSON.parse(config.data);
81 | const args = [account, name, password, mobile, email, 1];
82 | const result = await executeSql(
83 | 'INSERT INTO users (account, name, password, mobile, email, enabled) VALUES (?, ?, ?, ?, ?, ?)',
84 | args,
85 | true,
86 | );
87 | const { insertId: userId } = result;
88 |
89 | if (roleIds?.length) {
90 | for (let roleId of roleIds) {
91 | await executeSql('INSERT INTO user_roles (roleId, userId) VALUES (?,?)', [roleId, userId]);
92 | }
93 | }
94 |
95 | return [200, userId];
96 | },
97 | // 修改用户
98 | 'post /user/updateUserById': async (config) => {
99 | const { id, account, name, password, email, mobile, roleIds } = JSON.parse(config.data);
100 | const args = [account, name, password, mobile, email, moment().format('YYYY-MM-DD HH:mm:ss'), id];
101 |
102 | await executeSql(
103 | 'UPDATE users SET account=?, name=?, password=?, mobile=?, email=?, updatedAt=? WHERE id=?',
104 | args,
105 | );
106 | await executeSql('DELETE FROM user_roles WHERE userId=?', [id]);
107 |
108 | if (roleIds?.length) {
109 | for (let roleId of roleIds) {
110 | await executeSql('INSERT INTO user_roles (roleId, userId) VALUES (?,?)', [roleId, id]);
111 | }
112 | }
113 |
114 | return [200, true];
115 | },
116 | // 删除用户
117 | 'delete re:/user/.+': async (config) => {
118 | const id = config.url.split('/')[2];
119 | await executeSql('DELETE FROM users WHERE id=?', [id]);
120 | await executeSql('DELETE FROM user_roles WHERE userId=?', [id]);
121 | return [200, true];
122 | },
123 | };
124 |
--------------------------------------------------------------------------------
/src/mock/mockdata/organizations.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | id: '57c13d74ccf96eb264cbe75a',
4 | key: '1472281567463',
5 | name: '公司总部',
6 | description: '',
7 | remark: '',
8 | update_at: '2016-08-27T07:12:52.525Z',
9 | create_at: '2016-08-27T07:12:52.525Z',
10 | is_deleted: false,
11 | },
12 | {
13 | id: '57c13d74ccf96eb264cbe75b',
14 | key: '1472281647960',
15 | parentKey: '1472281567463',
16 | name: '北京分公司',
17 | update_at: '2016-08-27T07:12:52.525Z',
18 | create_at: '2016-08-27T07:12:52.525Z',
19 | is_deleted: false,
20 | },
21 | {
22 | id: '57c13d74ccf96eb264cbe75c',
23 | key: '1472281687190',
24 | parentKey: '1472281647960',
25 | name: '财务部',
26 | update_at: '2016-08-27T07:12:52.526Z',
27 | create_at: '2016-08-27T07:12:52.526Z',
28 | is_deleted: false,
29 | },
30 | {
31 | id: '57c13d74ccf96eb264cbe75d',
32 | key: '1472281695582',
33 | parentKey: '1472281647960',
34 | name: '人事部',
35 | update_at: '2016-08-27T07:12:52.526Z',
36 | create_at: '2016-08-27T07:12:52.526Z',
37 | is_deleted: false,
38 | },
39 | {
40 | id: '57c13d74ccf96eb264cbe75e',
41 | key: '1472281703110',
42 | parentKey: '1472281647960',
43 | name: '市场部',
44 | update_at: '2016-08-27T07:12:52.526Z',
45 | create_at: '2016-08-27T07:12:52.526Z',
46 | is_deleted: false,
47 | },
48 | {
49 | id: '57c13d74ccf96eb264cbe75f',
50 | key: '1472281711294',
51 | parentKey: '1472281647960',
52 | name: '技术部',
53 | update_at: '2016-08-27T07:12:52.527Z',
54 | create_at: '2016-08-27T07:12:52.527Z',
55 | is_deleted: false,
56 | },
57 | {
58 | id: '57c13d74ccf96eb264cbe760',
59 | key: '1472281736694',
60 | parentKey: '1472281647960',
61 | name: '运维部',
62 | update_at: '2016-08-27T07:12:52.527Z',
63 | create_at: '2016-08-27T07:12:52.527Z',
64 | is_deleted: false,
65 | },
66 | ];
67 |
--------------------------------------------------------------------------------
/src/mock/mockdata/roles.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs';
2 |
3 | export function getRolesByPageSize(pageSize) {
4 | const users = [];
5 | for (let i = 0; i < pageSize; i++) {
6 | users.push(
7 | Mock.mock({
8 | id: Mock.Random.guid(),
9 | name: Mock.Random.cname(),
10 | description: Mock.Random.cparagraph(1),
11 | }),
12 | );
13 | }
14 | return users;
15 | }
16 |
--------------------------------------------------------------------------------
/src/mock/mockdata/user.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs';
2 |
3 | const random = Mock.Random;
4 | const LoginUsers = [
5 | {
6 | id: '5754195b570b367345584998',
7 | name: '超级管理员',
8 | loginName: 'admin',
9 | gender: 'male',
10 | org_key: '1464444570801',
11 | roleId: '5753f4706df8f6094bf3fc54',
12 | pass: '111111',
13 | salt: '03525b62-15aa-4ffe-a1ee-0385498e01f9',
14 | update_at: '2016-08-23T13:44:18.819Z',
15 | create_at: '2016-06-05T12:21:47.666Z',
16 | is_deleted: false,
17 | is_locked: false,
18 | is_first_login: false,
19 | position: 'CEO',
20 | mobile: '88888888888',
21 | email: 'admin@163.com',
22 | permissions: [
23 | 'users-add',
24 | 'users-delete',
25 | 'users-update',
26 | 'users-search',
27 | 'users-reset-pass',
28 | 'users-toggle-lock',
29 | 'organization-add',
30 | 'organization-delete',
31 | 'organization-update',
32 | 'organization-search',
33 | 'role-add',
34 | 'role-update',
35 | 'role-delete',
36 | 'role-search',
37 | 'system',
38 | 'system-004002',
39 | 'system-004001',
40 | 'system-002',
41 | ],
42 | },
43 | {
44 | id: '5754195b570b367345584998',
45 | name: '超级管理员',
46 | loginName: 'admin2',
47 | gender: 'male',
48 | org_key: '1464444570801',
49 | roleId: '5753f4706df8f6094bf3fc54',
50 | pass: '111111',
51 | salt: '03525b62-15aa-4ffe-a1ee-0385498e01f9',
52 | update_at: '2016-08-23T13:44:18.819Z',
53 | create_at: '2016-06-05T12:21:47.666Z',
54 | is_deleted: false,
55 | is_locked: false,
56 | is_first_login: false,
57 | position: 'CEO',
58 | mobile: '88888888888',
59 | email: 'admin@163.com',
60 | permissions: [
61 | 'users-add',
62 | 'users-delete',
63 | 'users-update',
64 | 'users-search',
65 | 'users-reset-pass',
66 | 'users-toggle-lock',
67 | 'organization-add',
68 | 'organization-delete',
69 | 'organization-update',
70 | 'organization-search',
71 | 'role-add',
72 | 'role-update',
73 | 'role-delete',
74 | 'role-search',
75 | 'system',
76 | 'system-004002',
77 | 'system-004001',
78 | 'system-002',
79 | ],
80 | },
81 | ];
82 |
83 | export function getUsersByPageSize(pageSize) {
84 | const users = [];
85 | for (let i = 0; i < pageSize; i++) {
86 | users.push(
87 | Mock.mock({
88 | id: random.guid(),
89 | name: random.cname(),
90 | 'age|18-60': 1,
91 | 'job|1': ['程序员', '人事', '测试', '销售'],
92 | loginName: random.word(),
93 | email: random.email(),
94 | mobile: '18611438888',
95 | 'gender|1': ['male', 'female'],
96 | position: 'ceo',
97 | is_locked: false,
98 | is_first_login: false,
99 | remark: random.cparagraph(1),
100 | //
101 | // address: Mock.mock('@county(true)'),
102 | // 'age|18-60': 1,
103 | // 'group|1': ['jiagou', 'yanfa'],
104 | // birth: random.date(),
105 | // sex: random.integer(0, 1),
106 | }),
107 | );
108 | }
109 | return users;
110 | }
111 |
112 | export { LoginUsers };
113 |
--------------------------------------------------------------------------------
/src/mock/util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 简化mock请求写法
3 | *
4 | * 约定:method url delay,各部分以单个空格隔开
5 | * url 以re:开头,将被转换为正则,比如:re:/mock/user-center/.+ -> /\/mock\/user-center\/.+/
6 | *
7 | * @example
8 | * 'get /mock/users 1000' : users,
9 | *
10 | * @param mock
11 | * @param mocks
12 | */
13 | export const simplify = (mock, mocks) =>
14 | mocks.forEach((item) =>
15 | Object.keys(item).forEach((key) => {
16 | let method = key.split(' ')[0];
17 | let url = key.split(' ')[1];
18 | const delay = key.split(' ')[2] || 300;
19 | const result = item[key];
20 |
21 | method = `on${method.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())}`;
22 |
23 | if (url.startsWith('re:')) {
24 | url = new RegExp(url.replace('re:', ''));
25 | }
26 |
27 | if (typeof result === 'function') {
28 | mock[method](url).reply(result);
29 | } else {
30 | mock[method](url).reply(() => {
31 | // 加入延迟
32 | return new Promise((resolve) => {
33 | setTimeout(() => {
34 | resolve([200, result]);
35 | }, delay);
36 | });
37 | });
38 | }
39 | }),
40 | );
41 |
42 | /**
43 | * 获取随机数
44 | * @param max
45 | * @returns {number}
46 | */
47 | export function randomNumber(max) {
48 | return Math.ceil(Math.random() * max);
49 | }
50 |
51 | /**
52 | * 随机获取 数组中 count 个元素
53 | * @param arr
54 | * @param count
55 | * @returns {FlatArray<*[], 1>[]}
56 | */
57 | export function randomArray(arr, count) {
58 | const source = [...arr];
59 | const result = [];
60 |
61 | for (let i = 0; i < count; i++) {
62 | const randomIndex = randomNumber(source.length - 1);
63 | result.push(source.splice(randomIndex, 1));
64 | }
65 | return result.flat();
66 | }
67 |
--------------------------------------------------------------------------------
/src/mock/web-sql/index.js:
--------------------------------------------------------------------------------
1 | import createTableSql, { initDataSql } from './init-sql';
2 | import appPackage from '../../../package.json';
3 |
4 | const packageName = appPackage.name;
5 |
6 | const db = openDatabase(packageName, '1.0', packageName + '测试数据库', 2 * 1024 * 1024);
7 | let CACHE_INI_DB;
8 |
9 | const tables = ['menus', 'roles', 'users', 'role_menus', 'user_roles', 'user_collect_menus'];
10 |
11 | export default async function executeSql(sql, args, fullResult) {
12 | CACHE_INI_DB = CACHE_INI_DB || initDB();
13 |
14 | await CACHE_INI_DB;
15 |
16 | return new Promise((resolve, reject) => {
17 | db.transaction(function (tx) {
18 | tx.executeSql(
19 | sql,
20 | args,
21 | (transaction, resultSet) => resolve(fullResult ? resultSet : Array.from(resultSet.rows)),
22 | (transaction, error) => reject(error),
23 | );
24 | });
25 | });
26 | }
27 |
28 | // 初始化数据库
29 | export async function initDB(init) {
30 | const hasInitData = await usersHasData();
31 |
32 | if (init) await dropAllTables();
33 |
34 | // 创建表
35 | await executeSplit(createTableSql, 'create table');
36 |
37 | if (init || !hasInitData) await initTablesData();
38 | }
39 |
40 | export async function usersHasData() {
41 | return new Promise((resolve, reject) => {
42 | db.transaction(function (tx) {
43 | tx.executeSql(
44 | 'select * from users',
45 | null,
46 | (transaction, resultSet) => {
47 | resultSet.rows.length ? resolve(true) : resolve(false);
48 | },
49 | (transaction, error) => {
50 | resolve(false);
51 | },
52 | );
53 | });
54 | });
55 | }
56 |
57 | // 删除所有数据库表
58 | export async function dropAllTables() {
59 | tables.forEach((table) => {
60 | db.transaction(function (tx) {
61 | tx.executeSql(`drop table ${table}`);
62 | });
63 | });
64 | }
65 |
66 | // 插入初始化数据
67 | export async function initTablesData() {
68 | for (let table of tables) {
69 | const sql = initDataSql[table];
70 | await executeSplit(sql, 'INSERT INTO');
71 | }
72 | }
73 |
74 | async function executeSplit(sql, keyWord) {
75 | const arr = sql
76 | .split(keyWord)
77 | .filter((item) => !!item.trim())
78 | .map((item) => keyWord + item);
79 |
80 | for (let sql of arr) {
81 | await new Promise((resolve, reject) => {
82 | db.transaction(function (tx) {
83 | tx.executeSql(
84 | sql,
85 | null,
86 | (transaction, resultSet) => resolve(resultSet),
87 | (transaction, error) => reject(error),
88 | );
89 | });
90 | });
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/models/demo.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * 初始化 state
4 | *
5 | * 推荐将model中用到的所有state,都进行初始化:
6 | * 1. 合理的初始化值,可以避免使用错误;
7 | * 2. 可以通过state列表快速知晓当前model提供的数据结构
8 | * */
9 | state: {
10 | name: '默认值',
11 | user: null,
12 | syncObj: {
13 | good: 123,
14 | bar: {
15 | a: {
16 | aa: 'aa',
17 | aaa: 'aaa',
18 | aaaa: ['a', 'a1', 'a2'],
19 | },
20 | },
21 | foo: ['f', 'f1'],
22 | },
23 | },
24 | /**
25 | * 将 state 同步到 localStorage中
26 | * 默认false,两种传值方式:
27 | * 1. true 所有当前model state 都同步
28 | * 2. [path, path, ...] 指定jsonpath同步,详见:https://lodash.com/docs/4.17.15#get
29 | */
30 | syncLocal: ['syncObj.bar.a.aaaa[1]', 'syncObj.foo', 'name', 'user'],
31 | /**
32 | * 配置同 syncLocal,同步到sessionStorage中
33 | */
34 | // syncSession: true,
35 |
36 | /**
37 | * 同步、异步都做try捕获,出现错误是否调用handleError方法进行错误处理,
38 | * 默认 true,全部处理,可以单独指定哪些需要自动处理
39 | */
40 | errorTip: {
41 | async: true, // 异步提示
42 | sync: true, // 同步提示
43 | getUser: '获取用户失败了吧!!!', // getUser提示 优先级最高
44 | },
45 |
46 | /**
47 | * 操作成功提示,默认false,规则同errorTip
48 | */
49 | successTip: {
50 | async: false, // 异步提示
51 | sync: false, // 同步提示
52 | getUser: '获取用户成功!!!', // getUser提示 默认 "操作成功"
53 | setUser: '设置用户成功',
54 | },
55 | /**
56 | * 撤销、重做
57 | * 默认false,两种赋值方式:
58 | * 1. true,当前model所有方法都将触发撤销、重做历史记录,配置将使用redux-undo默认值
59 | * 2. object,指定配置,详细参考 https://github.com/omnidan/redux-undo
60 | * 新增include、exclude两个配置,对标 redux-undo filter。一般只配置一个即可,如果两个同时存在,include生效
61 | *
62 | * 如果配置了 undoable
63 | * 1. state数据结构将被转化成:(只影响connect取值,不影响method返回值)
64 | * demo = {
65 | * future: []
66 | * group: null
67 | * index: 4
68 | * limit: 5
69 | * past: (4) [{…}, {…}, {…}, {…}]
70 | * present: {name: 1618027715626, ...}
71 | * }
72 | * 2. 新增方法:(model中不要定义如下一些方法,会被覆盖掉)
73 | * `${modelName}Undo` (demoUndo)
74 | * `${modelName}Redo`
75 | * `${modelName}Jump`
76 | * `${modelName}JumpToPast`
77 | * `${modelName}JumpToFuture`
78 | * `${modelName}ClearHistory`
79 | * */
80 | undoable: {
81 | // https://github.com/omnidan/redux-undo
82 | include: ['setName'],
83 | exclude: ['setOptions'], // include exclude 同时存在,include将覆盖exclude
84 | limit: 5,
85 | },
86 | /**
87 | * 异步防抖,默认true,异步还是会执行,只是确保结果的顺序
88 | * 两种传参方式:
89 | * 1. true 异步全部防抖
90 | * 2. ['getUser'] 指定getUser方法防抖,其他不防抖
91 | */
92 | // debounce: [
93 | // // 'testAsync',
94 | // ],
95 | /**
96 | * 同步方法,返回一个对象,或者 其他,如果返回的不是对象,将不会合并到state中
97 | * 内部ActionType: action_user_setName (action_模块名_函数名)
98 | * @param name 自定义参数
99 | * @param state 当前model state 数据
100 | * @returns {{name}} 返回:1. 对象 将于state进行合并;2. falsity 假值,不做state处理
101 | */
102 | setName(name, state) {
103 | console.log('同步方法获取的参数', name, state);
104 | return { name };
105 | },
106 | setOptions: (options) => {
107 | console.log('setOptions 方法被调用');
108 | return { options };
109 | },
110 |
111 | /**
112 | * 异步方法,返回promise
113 | * 数据中自动添加 state.getUserLoading、state.getUserError 数据
114 | * 内部ActionType: action_user_getUser_resolve action_user_getUser_reject action_user_getUser_padding
115 | * 设置debounce,进行防抖:连续多次调用,如果上次未结束,结果将被抛弃,最后一次调用结果将合并到state中。
116 | * @param id
117 | * @param state
118 | * @returns {Promise<{name: number, age: number}>} promise resolve 值逻辑与同步方法相同
119 | */
120 | async getUser(id, state) {
121 | console.log('getUser state', state);
122 | return new Promise((resolve, reject) => {
123 | setTimeout(() => {
124 | Math.random() > 0.5 ? resolve({ user: { name: 123, age: 23 } }) : reject(new Error('获取用户失败!'));
125 | }, 2000);
126 | });
127 | },
128 | testAsync: async (time) => {
129 | return new Promise((resolve, reject) => {
130 | setTimeout(() => {
131 | resolve({ asyncResult: `${time}秒执行结果` });
132 | }, time);
133 | });
134 | },
135 | };
136 |
--------------------------------------------------------------------------------
/src/models/index.js:
--------------------------------------------------------------------------------
1 | import models from './models';
2 | import handleSuccess from 'src/commons/handle-success';
3 | import handleError from 'src/commons/handle-error';
4 | import { storage, createStoreByModels } from '@ra-lib/admin';
5 |
6 | const result = createStoreByModels(models, {
7 | // middlewares: [
8 | // thunk,
9 | // ],
10 | // enhancers: [], // 与 middlewares 进行compose运算的方法: const enhancer = compose(applyMiddleware(...middlewares), ...enhancers);
11 | // reducers: {todos}, // 额外的reducers
12 | // localStorage: window.localStorage,
13 | // sessionStorage: window.sessionStorage,
14 | // serialize: JSON.stringify,
15 | // deserialize: JSON.parse,
16 | localStorage: storage.local,
17 | sessionStorage: storage.session,
18 | serialize: (data) => data,
19 | deserialize: (data) => data,
20 | onError: handleError,
21 | onSuccess: handleSuccess,
22 | });
23 |
24 | export const connect = result.connect;
25 |
26 | /**
27 | * 导出 storage actions 给非组件环境使用
28 | const demoState = store.getState()?.demo
29 | const demoAction = actions.demo
30 | */
31 | export const store = result.store;
32 | export const actions = result.actions;
33 |
--------------------------------------------------------------------------------
/src/models/models.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 使用 require.context 自动引入所有model文件
3 | * */
4 | const result = {};
5 |
6 | // src/models目录下,不支持子文件夹
7 | const req = require.context('./', false, /\.js$/);
8 | req.keys().forEach((key) => {
9 | if (['./index.js', './models.js'].includes(key)) return;
10 | const model = req(key);
11 | const name = getModelName(key);
12 | result[name] = model.default;
13 | });
14 |
15 | // src/pages目录下,支持子文件夹
16 | const reqPages = require.context('../pages', true, /\.js$/);
17 | reqPages.keys().forEach((key) => {
18 | if (!key.endsWith('model.js')) return;
19 |
20 | const model = reqPages(key);
21 | const name = getModelName(key);
22 |
23 | result[name] = model.default;
24 | });
25 |
26 | export default result;
27 |
28 | /**
29 | * 获取模块名
30 | * @param filePath
31 | */
32 | function getModelName(filePath) {
33 | // models/page.js 情况
34 | let name = filePath.replace('./', '').replace('.js', '');
35 |
36 | const names = filePath.split('/');
37 | const fileName = names[names.length - 1];
38 | const folderName = names[names.length - 2];
39 |
40 | // users/model.js 情况
41 | if (fileName === 'model.js') name = folderName;
42 |
43 | // users/center.model.js 情况
44 | if (fileName.endsWith('.model.js')) {
45 | name = fileName.replace('.model.js', '').replace(/\./g, '-');
46 | }
47 |
48 | return name.replace(/-(\w)/g, (a, b) => b.toUpperCase());
49 | }
50 |
--------------------------------------------------------------------------------
/src/options/index.js:
--------------------------------------------------------------------------------
1 | import { wrapperOptions } from '@ra-lib/admin';
2 |
3 | /**
4 | * 使用 require.context 自动引入所有model文件
5 | * */
6 | const result = {};
7 |
8 | // src/models目录下,不支持子文件夹
9 | const req = require.context('./', false, /\.js$/);
10 | req.keys().forEach((key) => {
11 | if (['./index.js'].includes(key)) return;
12 |
13 | const model = req(key);
14 |
15 | const options = model.default;
16 | Object.entries(options).forEach(([k, value]) => {
17 | if (k in result) throw Error(`${key} 文件中 key 「${k}」已被使用!请更换!`);
18 | result[k] = value;
19 | });
20 | });
21 |
22 | wrapperOptions(result, 1000 * 5);
23 |
24 | export default result;
25 |
--------------------------------------------------------------------------------
/src/options/system.js:
--------------------------------------------------------------------------------
1 | import { Tag } from 'antd';
2 | import ajax from 'src/commons/ajax';
3 |
4 | /**
5 | * 项目中可能用到的一些枚举类数据
6 | * 约定只含有三个参数,
7 | * {
8 | * value: 1, // 必须且不可重复
9 | * label: '名称', // 必须
10 | * meta: {}, // 其他数据,可缺省
11 | * tag: Tag // 标签
12 | * };
13 | *
14 | * // 扩展方法
15 | * options.menuTarget.getLabel('menu')
16 | * options.yesNo.getTag(true);
17 | * */
18 | export default {
19 | // 菜单目标
20 | menuTarget: [
21 | { value: 'menu', label: '应用菜单' },
22 | { value: 'qiankun', label: '乾坤子应用' },
23 | { value: 'iframe', label: 'iframe内嵌第三方' },
24 | { value: '_self', label: '当前窗口打开第三方' },
25 | { value: '_blank', label: '新开窗口打开第三方' },
26 | ],
27 | // 是否
28 | yesNo: [
29 | { value: true, label: '是', tag: 是 },
30 | { value: false, label: '否', tag: 否 },
31 | ],
32 | // 启用、禁用
33 | enabled: [
34 | { value: true, label: '启用', tag: 启用 },
35 | { value: false, label: '禁用', tag: 禁用 },
36 | ],
37 | // 性别
38 | sex: [
39 | { value: '1', label: '男' },
40 | { value: '2', label: '女' },
41 | { value: '3', label: '未知' },
42 | ],
43 | // 可以是函数,异步或同步都可以
44 | async system() {
45 | const list = await ajax.get('/menu/queryTopMenus');
46 | return list.map((item) => {
47 | return {
48 | value: item.id,
49 | label: item.title,
50 | meta: item,
51 | };
52 | });
53 | },
54 | action() {
55 | return [{ value: 'add', label: '添加' }];
56 | // throw Error('获取失败了');
57 | },
58 | // 使用 get
59 | get demo() {
60 | return [];
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/src/pages/home/index.jsx:
--------------------------------------------------------------------------------
1 | // import {Redirect} from 'react-router-dom';
2 | import { Button } from 'antd';
3 | import { PageContent } from '@ra-lib/admin';
4 | import config from 'src/commons/config-hoc';
5 | import styles from './style.less';
6 |
7 | export default config({
8 | path: '/',
9 | title: '首页',
10 | })(function Home(props) {
11 | // 如果其他页面作为首页,直接重定向,config中不要设置title,否则tab页中会多个首页
12 | // return ;
13 | return (
14 |
15 | 首页
16 | {process.env.REACT_APP_MOCK ? (
17 | {
19 | await props.ajax.post('/initDB', null, { successTip: '数据库重置成功!' });
20 | setTimeout(() => window.location.reload(), 2000);
21 | }}
22 | >
23 | 重置数据库
24 |
25 | ) : null}
26 |
27 | );
28 | });
29 |
--------------------------------------------------------------------------------
/src/pages/home/style.less:
--------------------------------------------------------------------------------
1 | @import "src/theme.less";
2 |
3 | .root {
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/login/index-portal.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect, useCallback} from 'react';
2 | import {Helmet} from 'react-helmet';
3 | import {Button, Form} from 'antd';
4 | import {LockOutlined, UserOutlined} from '@ant-design/icons';
5 | import {FormItem, setLoginUser, queryStringify} from '@ra-lib/admin';
6 | import config from 'src/commons/config-hoc';
7 | import {toHome} from 'src/commons';
8 | import {Logo} from 'src/components';
9 | import s from './style.less';
10 | import {IS_DEV, IS_PREVIEW, IS_TEST} from 'src/config';
11 |
12 | // 开发模式下,默认填充的账号密码
13 | const formValues = {
14 | account: 'P101282',
15 | password: '0000',
16 | };
17 |
18 | // 登录接口所需的其他参数
19 | const queryString = queryStringify({
20 | phoneCode: '0000',
21 | captchaCode: '0000',
22 | captchaId: '578721818865569792',
23 | srandNumFlagId: 1,
24 | isPhone: false,
25 | isCheck: true,
26 | });
27 |
28 | export default config({
29 | path: '/p/login',
30 | auth: false,
31 | layout: false,
32 | })(function Login(props) {
33 | const login = props.ajax.usePost('/login/login?' + queryString, null, {baseURL: '/portal', errorTip: false});
34 |
35 | const [message, setMessage] = useState();
36 | const [isMount, setIsMount] = useState(false);
37 | const [form] = Form.useForm();
38 |
39 | const handleSubmit = useCallback(
40 | (values) => {
41 | if (login.loading) return;
42 |
43 | const {account, password} = values;
44 | const params = {
45 | loginName: account,
46 | password,
47 | };
48 |
49 | login
50 | .run(params, {errorTip: false})
51 | .then((res) => {
52 | const {id, loginName: name, token, ...others} = res;
53 | setLoginUser({
54 | id, // 必须字段
55 | name, // 必须字段
56 | token,
57 | ...others,
58 | // 其他字段按需添加
59 | });
60 | toHome();
61 | })
62 | .catch((err) => {
63 | console.error(err);
64 | setMessage(err.response?.data?.message || '用户名或密码错误');
65 | });
66 | },
67 | [login],
68 | );
69 |
70 | useEffect(() => {
71 | // 开发时默认填入数据
72 | if (IS_DEV || IS_TEST || IS_PREVIEW) {
73 | form.setFieldsValue(formValues);
74 | }
75 |
76 | setTimeout(() => setIsMount(true), 300);
77 | }, [form]);
78 |
79 | const formItemClass = [s.formItem, {[s.active]: isMount}];
80 |
81 | return (
82 |
83 |
84 |
85 |
86 |
87 |
88 |
132 |
{message}
133 |
134 |
135 | );
136 | });
137 |
--------------------------------------------------------------------------------
/src/pages/login/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect, useCallback} from 'react';
2 | import {Helmet} from 'react-helmet';
3 | import {Button, Form} from 'antd';
4 | import {LockOutlined, UserOutlined} from '@ant-design/icons';
5 | import {FormItem, setLoginUser} from '@ra-lib/admin';
6 | import config from 'src/commons/config-hoc';
7 | import {toHome} from 'src/commons';
8 | import {Logo, Proxy} from 'src/components';
9 | import {IS_DEV, IS_TEST, IS_PREVIEW} from 'src/config';
10 | import s from './style.less';
11 |
12 | // 开发模式 默认填充的用户名密码
13 | const formValues = {
14 | account: 'admin',
15 | password: '123456',
16 | };
17 |
18 | export default config({
19 | path: '/login',
20 | auth: false,
21 | layout: false,
22 | })(function Login(props) {
23 | const [message, setMessage] = useState();
24 | const [isMount, setIsMount] = useState(false);
25 | const [form] = Form.useForm();
26 |
27 | const login = props.ajax.usePost('/login');
28 |
29 | const handleSubmit = useCallback(
30 | (values) => {
31 | if (login.loading) return;
32 |
33 | const params = {
34 | ...values,
35 | };
36 |
37 | alert('TODO 登录');
38 | login.run = async () => ({id: 1, name: '测试', token: 'test'});
39 |
40 | login
41 | .run(params, {errorTip: false})
42 | .then((res) => {
43 | const {id, name, token, ...others} = res;
44 | setLoginUser({
45 | id, // 必须字段
46 | name, // 必须字段
47 | token,
48 | ...others,
49 | // 其他字段按需添加
50 | });
51 | toHome();
52 | })
53 | .catch((err) => {
54 | console.error(err);
55 | setMessage(err.response?.data?.message || '用户名或密码错误');
56 | });
57 | },
58 | [login],
59 | );
60 |
61 | useEffect(() => {
62 | // 开发时默认填入数据
63 | if (IS_DEV || IS_TEST || IS_PREVIEW) {
64 | form.setFieldsValue(formValues);
65 | }
66 |
67 | setTimeout(() => setIsMount(true), 300);
68 | }, [form]);
69 |
70 | const formItemClass = [s.formItem, {[s.active]: isMount}];
71 |
72 | return (
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
124 |
{message}
125 |
126 |
127 | );
128 | });
129 |
--------------------------------------------------------------------------------
/src/pages/login/login-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sxfad/react-admin/83660823b2b3d315fece226a7841c41a66676136/src/pages/login/login-bg.jpg
--------------------------------------------------------------------------------
/src/pages/login/style.less:
--------------------------------------------------------------------------------
1 | @import "src/theme";
2 |
3 | .root {
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | margin: 0;
8 | height: 100vh;
9 | background-image: url("./login-bg.jpg");
10 | background-size: cover;
11 | background-position: center;
12 | background-repeat: no-repeat;
13 | }
14 |
15 | .logo {
16 | position: absolute;
17 | top: 16px;
18 | left: 16px;
19 | }
20 |
21 | .proxy {
22 | position: absolute;
23 | cursor: pointer;
24 | top: 16px;
25 | right: 16px;
26 | display: flex;
27 | align-items: center;
28 | background: #fff;
29 | padding: 8px 16px;
30 | border-radius: 3px;
31 | }
32 |
33 | .box {
34 | padding: 16px 40px;
35 | width: 350px;
36 |
37 | border-radius: 0;
38 | // background: #f8f8f9;
39 | z-index: 100;
40 |
41 | .header {
42 | font-size: 25px;
43 | text-align: center;
44 | color: #fff;
45 | font-weight: bolder;
46 | }
47 |
48 | .submitBtn {
49 | width: 100%;
50 | }
51 |
52 | .errorTip {
53 | height: 30px;
54 | line-height: 30px;
55 | text-align: center;
56 | color: red;
57 | }
58 |
59 | .formItem {
60 | transition-delay: 5s;
61 | transition: 500ms;
62 | transform: translateX(100%);
63 | margin-bottom: 24px;
64 |
65 | &:nth-child(2n) {
66 | transform: translateX(-100%);
67 | }
68 |
69 | &.active {
70 | transform: translateX(0);
71 | }
72 | }
73 |
74 | :global {
75 | .@{ant-prefix}-input {
76 | height: 34px;
77 | }
78 |
79 | .@{ant-prefix}-btn {
80 | height: 44px;
81 | }
82 |
83 | .@{ant-prefix}-form-item-explain {
84 | position: absolute !important;
85 | bottom: -24px !important;
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/pages/menus/ActionEdit.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 | import { Button, Empty, Form, Space } from 'antd';
3 | import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
4 | import { FormItem, Content } from '@ra-lib/admin';
5 | import config from 'src/commons/config-hoc';
6 | import styles from './style.less';
7 |
8 | export default config()(function ActionEdit(props) {
9 | const [form] = Form.useForm();
10 | const { isAdd, selectedMenu, onSubmit, onValuesChange } = props;
11 | const [loading, setLoading] = useState(false);
12 | const { run: save } = props.ajax.usePost('/menu/updateSubActions', null, {
13 | setLoading,
14 | successTip: '功能保存成功!',
15 | });
16 |
17 | const handleSubmit = useCallback(
18 | async (values) => {
19 | const { actions } = values;
20 | const parentId = selectedMenu?.id;
21 |
22 | await save({ actions, parentId });
23 |
24 | onSubmit && onSubmit(actions);
25 | },
26 | [onSubmit, selectedMenu, save],
27 | );
28 |
29 | useEffect(() => {
30 | form.resetFields();
31 |
32 | if (!selectedMenu) return;
33 |
34 | form.setFieldsValue({ actions: selectedMenu?.actions });
35 | }, [selectedMenu, form]);
36 |
37 | return (
38 |
97 | );
98 | });
99 |
--------------------------------------------------------------------------------
/src/pages/menus/SystemSelect.jsx:
--------------------------------------------------------------------------------
1 | import { Select } from 'antd';
2 | import { useGet } from 'src/commons/ajax';
3 |
4 | export default function SystemSelect(props) {
5 | const { placeholder = '请选择系统', ...others } = props;
6 | const { data: options } = useGet('/menu/queryTopMenus', { pageNum: 1, pageSize: 100000 }, [], {
7 | formatResult: (res) => {
8 | return (res.content || []).map((item) => {
9 | return {
10 | meta: item,
11 | value: item.id,
12 | label: item.name,
13 | };
14 | });
15 | },
16 | });
17 |
18 | return ;
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/menus/style.less:
--------------------------------------------------------------------------------
1 | @import "src/theme";
2 |
3 | .menuRoot {
4 | display: flex;
5 | }
6 |
7 | .menu {
8 | flex: 0 0 210px;
9 | width: 210px;
10 | display: flex;
11 | flex-direction: column;
12 | border-right: 1px solid @border-color-base;
13 |
14 | .menuTop {
15 | padding: 8px;
16 | margin-right: 8px;
17 | display: flex;
18 | align-items: center;
19 | border-bottom: 1px solid @border-color-base;
20 | }
21 |
22 | .menuContent {
23 | flex: 1;
24 | overflow: auto;
25 | }
26 |
27 | :global {
28 | .@{ant-prefix}-menu-submenu {
29 | position: relative;
30 |
31 | &:after {
32 | position: absolute;
33 | top: 0;
34 | right: 0;
35 | height: 44px;
36 | border-right: 3px solid @primary-color;
37 | transform: scaleY(0.0001);
38 | opacity: 0;
39 | transition: transform 0.15s cubic-bezier(0.215, 0.61, 0.355, 1), opacity 0.15s cubic-bezier(0.215, 0.61, 0.355, 1);
40 | content: '';
41 | }
42 | }
43 |
44 | .@{ant-prefix}-menu-submenu.@{ant-prefix}-menu-item-selected:after {
45 | opacity: 1;
46 | transform: scaleY(1);
47 | }
48 | }
49 | }
50 |
51 | .pane {
52 | flex: 1;
53 | display: flex;
54 | flex-direction: column;
55 | border-right: 1px solid @border-color-base;
56 | }
57 |
58 | .title {
59 | flex: 0 0 40px;
60 | display: flex;
61 | justify-content: center;
62 | align-items: center;
63 | padding-top: 8px;
64 | }
65 |
66 | .content {
67 | flex: 1;
68 | overflow: auto;
69 | padding: 8px 32px;
70 | }
71 |
72 | .footerAction {
73 | flex: 0 0 50px;
74 | justify-content: flex-end;
75 | border-top: 1px solid @border-color-base;
76 | padding: 0 10px;
77 | margin: 0 8px;
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/src/pages/page-configs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 当前文件会由config-loader处理,编辑无效
3 | * 返回示例如下:
4 | * */
5 | export default [
6 | {
7 | path: '/users', // 路由地址
8 | component: () => import('/../src/pages/home/user/index.jsx'),
9 | filePath: '/../src/pages/home/user/index.jsx', // 文件绝对路径
10 | // ... 其他config高级组件参数
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/src/qiankun.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import { addGlobalUncaughtErrorHandler, registerMicroApps, start } from 'qiankun';
3 | import { SubError } from './components';
4 | import { getContainerId, PageContent } from '@ra-lib/admin';
5 | import { CONFIG_HOC } from './config';
6 | import api from 'src/api';
7 | import App from './App';
8 |
9 | /**
10 | * 获取当前激活子应用
11 | * @param pathname
12 | * @returns {Promise<*>}
13 | */
14 | export async function getCurrentActiveSubApp(pathname = window.location.pathname) {
15 | const name = `${pathname.split('/')[1]}`;
16 | const subApps = await api.getSubApps();
17 |
18 | return subApps.find((item) => item.name === name);
19 | }
20 |
21 | /**
22 | * 根据name判断,是否是激活子项目
23 | * @param app
24 | * @param pathname
25 | * @returns {*}
26 | */
27 | export function isActiveApp(app, pathname = window.location.pathname) {
28 | return pathname.startsWith(`/${app.name}`);
29 | }
30 |
31 | /**
32 | * 根据name 获取app配置
33 | * @param name
34 | */
35 | export async function getAppByName(name) {
36 | const apps = await api.getSubApps();
37 |
38 | return apps.find((item) => item.name === name);
39 | }
40 |
41 | export default async function () {
42 | // 获取子应用
43 | const subApps = await api.getSubApps();
44 |
45 | // 注册子应用
46 | registerMicroApps(subApps, {
47 | // 显示loading提示
48 | beforeLoad: (app) => {
49 | const { title = '子应用', name } = app;
50 |
51 | // 要通过App包裹,否则缺少必要环境
52 | ReactDOM.render(
53 |
54 |
55 | ,
56 | document.getElementById(getContainerId(name)),
57 | );
58 | },
59 | });
60 |
61 | // 启动应用
62 | start({
63 | // 是否同时只加载一个应用
64 | singular: !CONFIG_HOC.keepAlive,
65 | // 是否预加载
66 | prefetch: false,
67 | });
68 |
69 | // 全局错误处理
70 | addGlobalUncaughtErrorHandler((event) => {
71 | // 子应用加载失败
72 | if (event?.message?.includes('died in status LOADING_SOURCE_CODE')) {
73 | const name = event.error.appOrParcelName;
74 | ReactDOM.render(
75 |
76 |
77 | ,
78 | document.getElementById(getContainerId(name)),
79 | );
80 | }
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/src/router/AppRouter.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, HashRouter, Route, Switch } from 'react-router-dom';
2 | import { LayoutError404 as Error404, AdminLayout as Layout, SubApp } from '../components';
3 | import { BASE_NAME, CONFIG_HOC, HASH_ROUTER } from 'src/config';
4 | import routes from './routes';
5 | import { Footer } from 'src/components';
6 |
7 | const Router = HASH_ROUTER ? HashRouter : BrowserRouter;
8 | const baseName = HASH_ROUTER ? '' : BASE_NAME;
9 |
10 | export default function AppRouter(props) {
11 | const { menus, collectedMenus, onMenuCollect } = props;
12 |
13 | return (
14 |
15 | {
18 | return (
19 | }
23 | baseName={baseName}
24 | menus={menus}
25 | collectedMenus={collectedMenus}
26 | onMenuCollect={onMenuCollect}
27 | />
28 | );
29 | }}
30 | />
31 | {!CONFIG_HOC.keepAlive ? (
32 |
33 | {routes.map((item) => {
34 | const { path, component } = item;
35 |
36 | return ;
37 | })}
38 |
39 |
40 | ) : null}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/router/routes.js:
--------------------------------------------------------------------------------
1 | import { Loading } from '@ra-lib/admin';
2 | import loadable from '@loadable/component';
3 | import { checkPath } from '../commons';
4 | import pageConfigs from 'src/pages/page-configs';
5 |
6 | // 检测路由配置是否有冲突
7 | checkPath(pageConfigs);
8 |
9 | // 抓取到的页面路由
10 | const pageRoutes = pageConfigs.filter(({ path }) => !!path);
11 |
12 | // 所有人都可以访问的页面
13 | export const commonPaths = ['/', '/login'];
14 |
15 | /*
16 | * 非脚本抓取的路由,可以在这里编辑,
17 | * 脚本抓取的路由在./src/pages/page-configs.js中
18 | * */
19 | export default [
20 | // {path: '/', component: ()=> import('./path-to-component')},
21 | { path: '/iframe_page_/:src', component: () => import('../components/iframe') },
22 | { path: '/layout/setting', component: () => import('../components/layout/layout-setting/SettingPage') },
23 | ]
24 | .concat(pageRoutes)
25 | .map((item) => {
26 | return {
27 | ...item,
28 | component: loadable(item.component, { fallback: }),
29 | };
30 | });
31 |
--------------------------------------------------------------------------------
/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const proxy = require('http-proxy-middleware');
2 | const proxyConfig = require('./setupProxyConfig.json');
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | // 同步修改测试环境NG代理
7 | modifyTestNg();
8 |
9 | // 前端web服务代理配置
10 | module.exports = function (app) {
11 | proxyConfig
12 | .filter((item) => !item.disabled)
13 | .forEach(({ baseUrl, target }) => {
14 | app.use(
15 | proxy(baseUrl, {
16 | target,
17 | pathRewrite: {
18 | ['^' + baseUrl]: '',
19 | },
20 | changeOrigin: true,
21 | secure: false, // 是否验证证书
22 | ws: true, // 启用websocket
23 | // 作为子系统时,需要设置允许跨域
24 | // onProxyRes(proxyRes, req, res) {
25 | // proxyRes.headers['Access-Control-Allow-Origin'] = '*';
26 | // proxyRes.headers['Access-Control-Allow-Methods'] = '*';
27 | // proxyRes.headers['Access-Control-Allow-Headers'] = '*';
28 | // },
29 | }),
30 | );
31 | });
32 |
33 | app.use(
34 | proxy('/portal', {
35 | target: 'http://172.16.143.44:32328', // 测试门户后端
36 | pathRewrite: {
37 | '^/portal': '',
38 | },
39 | changeOrigin: true,
40 | secure: false, // 是否验证证书
41 | ws: true, // 启用websocket
42 | }),
43 | );
44 | };
45 |
46 | function modifyTestNg() {
47 | const ngConfigPath = path.resolve(__dirname, '..', 'deploy', 'rancher', 'nginx.conf');
48 | const ngConfig = fs.readFileSync(ngConfigPath, 'UTF-8');
49 | const locations = proxyConfig
50 | .filter((item) => !item.disabled)
51 | .map(({ name, baseUrl, target }) => {
52 | return `
53 | # 代理ajax请求 ${name}
54 | location ^~${baseUrl} {
55 | rewrite ^${baseUrl}/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀
56 | proxy_pass ${target};
57 | proxy_set_header Host $http_host;
58 | proxy_set_header Connection close;
59 | proxy_set_header X-Real-IP $remote_addr;
60 | proxy_set_header X-Forwarded-Server $host;
61 | }
62 | `;
63 | })
64 | .join('');
65 | const startIndex = ngConfig.indexOf('# 代理ajax请求');
66 |
67 | const nextNgConfig = `
68 | ${ngConfig.substring(0, startIndex)}${locations.trim()}
69 | }
70 | `;
71 | fs.writeFileSync(ngConfigPath, nextNgConfig.trim(), 'UTF-8');
72 | }
73 |
--------------------------------------------------------------------------------
/src/setupProxyConfig.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "张三",
4 | "disabled": false,
5 | "baseUrl": "/zhangsan",
6 | "target": "http://127.0.0.1:8080"
7 | },
8 | {
9 | "name": "测试环境",
10 | "baseUrl": "/api",
11 | "target": "http://127.0.0.1:8080"
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/theme.less:
--------------------------------------------------------------------------------
1 | // 样式前缀,做微前端,防止各个系统样式冲突时,各个系统修改为不同的值
2 | @packageName: ''; // lessLoader 会改为 package.json name属性
3 | @ant-prefix: ~'@{packageName}';
4 | @ra-lib-prefix: ~'@{packageName}-ra';
5 |
6 | // 自定义变量
7 | //@page-content-margin: 0;
8 | //@page-content-padding: 0;
9 | //@content-margin: 0px;
10 | //@content-padding: 8px;
11 | //@layout-z-index: 999;
12 | //@layout-transition-duration: 0;
13 |
14 | // ant 官方变量
15 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
16 | @border-color-base: hsv(0, 0, 85%);
17 |
18 | //@primary-color: #42b983;
19 | //@layout-header-background: #42b983;
20 | //@menu-dark-inline-submenu-bg: #349368;
21 |
22 | @primary-color: #1890ff; // 主题色
23 | @layout-header-background: #001529; // 头部背景色
24 | @menu-dark-inline-submenu-bg: #000c17; // 菜单背景色
25 |
26 | // 字体颜色
27 | @text-color: fade(#000, 65%);
28 | @body-background: #f0f2f5;
29 |
30 | :export {
31 | antPrefix: @ant-prefix;
32 | raLibPrefix: @ra-lib-prefix;
33 | primaryColor: @primary-color;
34 | }
35 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | /**
4 | * 用于WebStorm 等IDE识别import使用,并不不是构建配置,
5 | * 构建配置在 craco.config.js 中
6 | * */
7 | module.exports = {
8 | resolve: {
9 | alias: {
10 | src: path.resolve(__dirname, 'src'),
11 | },
12 | },
13 | };
14 |
--------------------------------------------------------------------------------