├── .editorconfig ├── .env-cmdrc.js ├── .github └── workflows │ └── updatePages.yml ├── .gitignore ├── LICENSE ├── README.md ├── color.js ├── config ├── env.js ├── getHttpsConfig.js ├── jest │ ├── babelTransform.js │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── jsconfig.json ├── package.json ├── public ├── favicon.ico ├── iconfont.js ├── index.html ├── less.min.js ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── scripts ├── build.js ├── start.js └── test.js └── src ├── App.js ├── api └── index.js ├── assets ├── images │ ├── bg.svg │ ├── layout1.jpg │ ├── layout2.jpg │ ├── layout3.jpg │ └── logo.svg ├── json │ └── iconfont.json └── theme │ └── var.less ├── common ├── ajax.js ├── index.js └── var.js ├── components ├── color │ ├── index.js │ └── index.less ├── contextMenu │ ├── index.js │ └── index.less ├── dnd │ └── free.js ├── echarts │ ├── bar │ │ └── index.js │ ├── index.js │ ├── line │ │ └── index.js │ └── pie │ │ └── index.js ├── form │ └── index.js ├── icon │ └── index.js ├── layout-set │ ├── index.js │ └── index.less ├── menu-dnd │ ├── index.js │ └── index.less ├── modal │ ├── feedback │ │ └── index.js │ ├── menu │ │ ├── index.js │ │ └── index.less │ ├── type │ │ └── index.js │ └── user │ │ └── index.js ├── pagination │ ├── index.js │ └── index.less ├── table │ ├── index.js │ └── index.less └── theme │ ├── index.js │ └── index.less ├── index.js ├── layout ├── footer.js ├── header.js ├── index.js ├── index.less ├── mode │ ├── fullScreen.js │ ├── index.js │ ├── singleColumn.js │ ├── twoColumn.js │ └── twoFlanks.js ├── siderMenu.js └── topMenu.js ├── mock └── index.js ├── pages ├── details │ ├── index.less │ └── person.js ├── err │ └── index.js ├── form │ ├── index.js │ └── index.less ├── icons │ ├── index.js │ └── index.less ├── list │ ├── card.js │ ├── index.less │ └── search.js ├── login │ ├── index.js │ └── index.less ├── power │ ├── index.less │ ├── menu.js │ ├── type.js │ └── user.js └── statistics │ ├── feedback.js │ ├── index.less │ └── vistor.js ├── router ├── appRouter.js ├── index.js ├── intercept.js └── list.js ├── setupProxy.js ├── store ├── action.js ├── getters │ ├── index.js │ ├── layout.js │ ├── menu.js │ ├── user.js │ └── visibel.js ├── hooks │ ├── index.js │ ├── layout.js │ ├── menu.js │ ├── user.js │ └── visibel.js ├── index.js ├── layout │ ├── action.js │ ├── actionTypes.js │ └── reducer.js ├── menu │ ├── action.js │ ├── actionTypes.js │ └── reducer.js ├── user │ ├── action.js │ ├── actionTypes.js │ └── reducer.js └── visibel │ ├── action.js │ └── reducer.js └── utils └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.env-cmdrc.js: -------------------------------------------------------------------------------- 1 | 2 | const devConfig = { 3 | PORT: 3000, // 启动端口 4 | HOST: "0.0.0.0", // 监听地址 5 | REACT_APP_ROUTERBASE: "/react-ant-admin", // react路由基础路径 6 | REACT_APP_API_BASEURL: "http://127.0.0.1:8081/api/react-ant-admin", //请求地址 7 | PUBLIC_URL: "/react-ant-admin",// 静态文件路径 8 | } 9 | const proConfig = { 10 | REACT_APP_ROUTERBASE: "/react-ant-admin", // react路由基础路径 11 | REACT_APP_API_BASEURL: "/api/react-ant-admin", //请求地址 12 | PUBLIC_URL: "/react-ant-admin",// 静态文件路径 13 | BUILD_PATH: "react-ant-admin", // 打包 文件夹名称 14 | } 15 | 16 | /** 17 | * env-cmd 文档地址 https://github.com/toddbluhm/env-cmd#-help 18 | * 命令行使用: env-cmd --verbose -e mode_name node file.js 19 | * mode_name: 对应 mode 里面的 属性(key) 例如 development development_color 20 | * 运行结果: 21 | * 取出 对应 mode_name 的 值(value) Object.keys方法 把 key-value 绑定到 process.env 上 22 | * 如 : development(mode_name): { test : "123" } => process.env.test = "123" 23 | * 最终能够在整个项目中 使用 process.env.test 24 | */ 25 | const mode = { 26 | 27 | // 本地接口正常运行 没有mock 没有 主题色 28 | development: devConfig, 29 | 30 | // 本地接口 启用主题色运行 31 | development_color: { 32 | ...devConfig, 33 | COLOR: "true", // "true" 为 启动 34 | }, 35 | 36 | // 本地mock 运行 37 | development_mock: { 38 | ...devConfig, 39 | REACT_APP_MOCK: "1", // 1 为开启mock 40 | }, 41 | 42 | // 主题色 和 本地mock 运行 43 | development_color_mock: { 44 | ...devConfig, 45 | COLOR: "true", 46 | REACT_APP_MOCK: "1", 47 | }, 48 | 49 | // 打包 :无主题 无mock 50 | production: proConfig, 51 | 52 | // 打包 : 有主题 无mock 53 | production_color: { 54 | ...proConfig, 55 | COLOR: "true", // "true" 为 启动 56 | }, 57 | 58 | // 打包 : 有主题 有mock 纯本地模式打包 59 | production_color_mock: { 60 | ...proConfig, 61 | COLOR: "true", 62 | REACT_APP_MOCK: "1", 63 | }, 64 | 65 | // GitHub pages 打包 博主使用 66 | production_github: { 67 | ...proConfig, 68 | COLOR: "true", 69 | REACT_APP_API_BASEURL: "https://z3web.cn/api/react-ant-admin", 70 | REACT_APP_ROUTER_ISHASH: "1", // 启用哈希模式 71 | REACT_APP_ROUTERBASE: "/" 72 | } 73 | } 74 | 75 | 76 | module.exports = Promise.resolve(mode) -------------------------------------------------------------------------------- /.github/workflows/updatePages.yml: -------------------------------------------------------------------------------- 1 | name: update pages 2 | 3 | on: 4 | push: 5 | branches: [webpack] 6 | paths: ["src/**","public"] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [12] 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Build 24 | run: | 25 | npm install 26 | ./node_modules/.bin/env-cmd --verbose -e production_github --use-shell 'node color.js && node scripts/build.js' 27 | 28 | - name: Deploy 🚀 29 | uses: JamesIves/github-pages-deploy-action@v4.3.0 30 | with: 31 | branch: gh-pages # The branch the action should deploy to. 32 | folder: react-ant-admin # The folder the action should deploy. 33 | clean-exclude: | # exclude files 34 | .gitignore 35 | -------------------------------------------------------------------------------- /.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 | yarn.lock 8 | package-lock.json 9 | /color.js* 10 | /src/assets/theme/*.json 11 | # testing 12 | /coverage 13 | /src/router/auto* 14 | 15 | # production 16 | /react-ant-admin 17 | /public/color.less 18 | /public/index.html 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 孔乙己拉夫米 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-ant-admin 2 | 3 | [![GitHub star](https://img.shields.io/github/stars/kongyijilafumi/react-ant-admin?label=GitHub%20Star)](https://github.com/kongyijilafumi/react-ant-admin) 4 | [![GitHub fork](https://img.shields.io/github/forks/kongyijilafumi/react-ant-admin?label=GitHub%20fork)](https://github.com/kongyijilafumi/react-ant-admin/network/members) 5 | [![Gitee star](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin/badge/star.svg?theme=dark)](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin/stargazers) 6 | [![Gitee fork](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin/badge/fork.svg?theme=dark)](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin/members) 7 | ![](https://img.shields.io/github/license/kongyijilafumi/react-ant-admin) 8 | 9 | TypeScript 版[GitHub(国外地址)](https://github.com/kongyijilafumi/react-ant-admin-ts) | 10 | TypeScript 版[码云(国内镜像)](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin-ts) 11 | 12 | JavaScript 版[GitHub(国外地址)](https://github.com/kongyijilafumi/react-ant-admin) | 13 | JavaScript 版[码云(国内镜像)](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin) 14 | 15 | 此框架使用与二次开发,前端框架使用 react,UI 框架使用 ant-design,全局数据状态管理使用 redux,ajax 使用库为 axios。用于快速搭建中后台页面。欢迎各位提[issue](https://github.com/kongyijilafumi/react-ant-admin/issues) 16 | 17 | - [react](https://react.docschina.org/) 18 | - [react-router-cache-route](https://www.npmjs.com/package/react-router-cache-route) 19 | - [ant-design](https://ant.design/index-cn) 20 | - [redux](https://redux.js.org/) 21 | - [axios](http://www.axios-js.com/) 22 | 23 | ## 预览地址 24 | 25 | [react-ant-admin](http://z3web.cn/react-ant-admin/) 26 | 27 | nodejs 后台 web 服务:[react-ant-admin-server](https://gitee.com/kong_yiji_and_lavmi/react-ant-admin-server) 28 | 29 | ## 二次开发视频讲解地址 30 | 31 | - [百度云视频下载地址](https://pan.baidu.com/s/1aJHIhCnv0q1wojUdJknidQ?pwd=y9mx ) 32 | - [阿里云视频下载地址](https://www.aliyundrive.com/s/kvfHSoa7fP3) 33 | 34 | 35 | ## 文档地址 36 | 37 | [react-ant-admin 文档地址](https://z3web.cn/doc-react-ant-admin/) 38 | 39 | 更多建议欢迎骚扰~ 40 | [qq 交流群:564048130](https://jq.qq.com/?_wv=1027&k=pzP2acC5) 41 | 42 | 欢迎各位提出建议与问题! 43 | 44 | ## [接口文档地址](https://www.apifox.cn/apidoc/project-927261) 45 | 46 | ## 特性 47 | 48 | - 菜单配置:扁平化数据组织,方便编写,存库,页面菜单,标题,侧边栏,顶部导航栏同步 49 | - 页面懒加载:使用[@loadable/component](https://loadable-components.com/docs/getting-started/)来解决首次打开页面过慢的问题. 50 | - Ajax 请求:restful 规范,自动错误提示,提示可配置;自动打断未完成的请求; 51 | - 权限控制: 根据不用角色的功能类型显示菜单,路由页面拦截. 52 | - 自定义主题,可以自己定义界面颜色。 53 | - 代理转发,解决前端请求跨域问题。 54 | - 路由自动生成,去中心化。 55 | 56 | 系统提供了一些基础的页面 57 | 58 | - 登录页 59 | - 详情页 60 | - 表单页 61 | - 列表页 62 | - 权限管理 63 | - 结果页 64 | 65 | ## 切换 Vite 版本 66 | 67 | 1. 切换分支 68 | 69 | ```bash 70 | D:\react-ant-admin>git checkout vite 71 | ``` 72 | 73 | 2. 安装依赖 74 | 75 | ```bash 76 | D:\react-ant-admin>cnpm i 77 | ``` 78 | 79 | 3. 启动 80 | 81 | ```bash 82 | D:\react-ant-admin>npm run dev 83 | ``` 84 | 85 | ## 快速使用 86 | 87 | 1. 下载本项目到本地 88 | 89 | ```bash 90 | D:> git clone https://github.com/kongyijilafumi/react-ant-admin.git #github地址 慢 91 | D:> git clone https://gitee.com/kong_yiji_and_lavmi/react-ant-admin.git #码云地址 快 92 | ``` 93 | 94 | 2. 安装依赖 95 | 96 | ```bash 97 | # npm 慢 98 | npm i 99 | # cnpm 国内镜像 快 100 | cnpm i 101 | ``` 102 | 103 | 3. 启动 104 | 105 | ```bash 106 | npm run "start:mock" # 启动本地mock数据 (暂时没有后台接口,请用此模式预览项目) 107 | npm run start # 启动本地API接口来获取数据 108 | ``` 109 | 110 | 浏览器打开 `http://localhost:3000` 即可 111 | 112 | ## 创建一个新的页面 113 | 114 | 1. 在 src/pages 文件夹下创建一个 test.js 文件,代码如下 115 | 116 | ```js 117 | // 函数组件 118 | import React from "react"; 119 | 120 | export default function Test() { 121 | return
test页面
; 122 | } 123 | 124 | // 类组件 125 | export default class Test extends React.Component { 126 | render() { 127 | return
test页面
; 128 | } 129 | } 130 | 131 | /** 132 | * MENU_* 开头信息在package.json 文件中找到 133 | * 给 pages 组件追加路由信息 134 | * export default 组件的原型上添加route信息,或者向外暴露一个 route 135 | * 会被webpack的webpack-router-generator插件捕获信息 136 | */ 137 | 138 | // 1.被捕获 export default 原型上的route 139 | Test.route={ 140 | [MENU_TITLE] : "test页面", 141 | [MENU_KEY] : 11, 142 | [MENU_PATH]: "/test" 143 | } 144 | 145 | // 2.被捕获 暴露的route信息 优先级比上面高 146 | export const route = { 147 | [MENU_TITLE] : "test页面", 148 | [MENU_KEY] : 11, 149 | [MENU_PATH]: "/test" 150 | } 151 | ``` 152 | 153 | 2. 浏览器访问 `http://localhost:3000/react-ant-admin/test` 即可 154 | 155 | ## 创建一个菜单 156 | 157 | 该添加方式适用于 `npm run "start:mock"`启动的项目 158 | 159 | 1. 在`src/mock/index.js` 找到`menu`变量,往里添加一条菜单信息.代码如下所示 160 | 161 | ```js 162 | let menu = [ 163 | { 164 | [MENU_TITLE]: "列表页", 165 | [MENU_PATH]: "/list", 166 | [MENU_KEY]: 9, // 菜单的唯一标识 167 | [MENU_PARENTKEY]: null, 168 | [MENU_ICON]: "icon_list", 169 | [MENU_KEEPALIVE]: "false", 170 | [MENU_LAYOUT]:"FULLSCREEN" // 页面内容主题全屏显示 布局 171 | [MENU_ORDER]: 1, 172 | }, 173 | { 174 | [MENU_TITLE]: "卡片列表", 175 | [MENU_PATH]: "/card", 176 | [MENU_KEY]: 10, 177 | [MENU_PARENTKEY]: 9, // 父菜单的唯一标识 178 | [MENU_ICON]: null, 179 | [MENU_LAYOUT]:"TWO_COLUMN" // 拥有侧边栏的 布局 此属性默认可以不填 在 src/layout/index.js defualt 项导出一个默认布局 180 | [MENU_KEEPALIVE]: "false", 181 | [MENU_ORDER]: 5485, 182 | }, 183 | // .... 开始添加菜单信息 .... 184 | { 185 | [MENU_TITLE]: "test", // 标题 186 | [MENU_PATH]: "/test", // 访问路径 187 | [MENU_KEY]: 11, // 菜单的唯一标识 188 | [MENU_PARENTKEY]: null, // 空表示 为主菜单而非子菜单 189 | [MENU_ICON]: "icon_infopersonal", // 菜单图标 190 | [MENU_ORDER]: 1, // 菜单排序 越小越靠前 number 191 | [MENU_KEEPALIVE]: "true", // 页面保持状态 192 | }, 193 | // ..... 194 | ]; 195 | ``` 196 | 197 | 2. 由于菜单会走本地会话存储`window.sessionStorage`,所以保存代码后需要关闭当前窗口,重新打开地址 `http://localhost:3000/react-ant-admin` 198 | 199 | > 打开之后,会发现菜单会多出一个`test`栏目,点击会打开之前我们创建的 test 页面.这样就完成了菜单和页面的编写. 200 | 201 | ## 脚本启动 202 | 203 | 在完成依赖安装之后,有以下几种启动方式。 204 | 205 | - npm run start 206 | 207 | 请求接口数据,通过后台返回数据显示项目信息 208 | 209 | - npm run "start:color" 210 | 211 | 请求接口数据,通过后台返回数据显示项目信息,并且开启主题色配置。 212 | 213 | - npm run "start:mock" 214 | 215 | 本地模拟数据,假数据来显示项目信息 216 | 217 | - npm run "start:mock_color" 218 | 219 | 本地模拟数据,假数据来显示项目信息,并且开启主题色配置。 220 | 221 | - npm run build 222 | 223 | 普通打包模式。 224 | 225 | - npm run "build:color" 226 | 227 | 打包主题色。项目体积会有所增加。 228 | 229 | ### vscode 快速启动项目 230 | 231 | 使用[vscode 编辑器](https://code.visualstudio.com/)[下载地址](https://blog.csdn.net/bielaiwuyang1999/article/details/117814237) 232 | 233 | 把此项目文件夹拖入`vscode编辑器`,找到左下角`npm 脚本栏目`选择快速启动,免命令。 234 | ![免命令示例图](https://raw.githubusercontent.com/kongyijilafumi/my-image/master/run.png) 235 | 236 | ## 项目截图 237 | 238 | - 登录 239 | 240 | ![登录](https://raw.githubusercontent.com/kongyijilafumi/my-image/master/react-ant-admin-doc01.png) 241 | 242 | - 详情页 243 | 244 | ![详情页](https://raw.githubusercontent.com/kongyijilafumi/my-image/master/react-ant-admin-detail.png) 245 | 246 | - 列表 247 | 248 | ![表格](https://raw.githubusercontent.com/kongyijilafumi/my-image/master/react-ant-admin-list.png) 249 | 250 | - 权限管理 251 | 252 | ![权限管理](https://raw.githubusercontent.com/kongyijilafumi/my-image/master/react-ant-admin-power.png) 253 | 254 | - 结果页 255 | 256 | ![结果页](https://raw.githubusercontent.com/kongyijilafumi/my-image/master/react-ant-admin-result1.png) 257 | -------------------------------------------------------------------------------- /color.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const { generateTheme, getLessVars } = require("ant-theme-generator"); 4 | 5 | const showColorSet = process.env.COLOR === "true"; 6 | 7 | // 变量文件夹 8 | const varFile = path.join(__dirname, "./src/assets/theme/var.less"); 9 | const varJsonPath = path.join(__dirname, "./color.json.js"); 10 | const outputFilePath = path.join(__dirname, "./public/color.less"); 11 | // antd 默认主题 json 文件 12 | const defaultPath = path.join(__dirname, "./src/assets/theme/default.json"); 13 | const darkPath = path.join(__dirname, "./src/assets/theme/dark.json"); 14 | // antd 主题 less文件 15 | const antdDefaultPath = "./node_modules/antd/lib/style/themes/default.less"; 16 | const antdDarkPath = "./node_modules/antd/lib/style/themes/dark.less"; 17 | const antdLightPath = "./node_modules/antd/lib/style/themes/compact.less"; 18 | 19 | function colorStart() { 20 | try { 21 | var varStr = fs.readFileSync(varFile, "utf-8"); 22 | } catch (error) { 23 | throw error; 24 | } 25 | 26 | // 读取less变量文件 提取变量 信息 27 | let varStrArr = varStr.split(/\n/); 28 | let scipteReg = /\/\/\s+script/g; 29 | let slice = []; 30 | varStrArr.forEach((i, index) => { 31 | if (scipteReg.test(i)) { 32 | slice.push(index); 33 | } 34 | }); 35 | varStrArr = varStrArr.slice(...slice).filter((i) => i[0] === "@"); 36 | let colorsReg = /(.*?)\s*:\s*(.*?);\s*\/\/\s*([\u4e00-\u9fa5]*)/g; 37 | let varColors = []; 38 | varStrArr.forEach((item) => { 39 | colorsReg.lastIndex = 0; 40 | let execRes = colorsReg.exec(item); 41 | if (execRes) { 42 | varColors.push({ 43 | title: execRes[3], 44 | key: execRes[1], 45 | value: execRes[2], 46 | }); 47 | } 48 | }); 49 | // 变量配置信息 写入 color.json.js 文件 50 | var tips = "\n// 本文件由脚本自动生成"; 51 | let exportStr = "module.exports=" + JSON.stringify(varColors) + tips; 52 | fs.writeFileSync(varJsonPath, exportStr); 53 | const defaultVars = getLessVars(antdDefaultPath); 54 | const darkVars = { 55 | ...getLessVars(antdDarkPath), 56 | "@primary-color": defaultVars["@primary-color"], 57 | "@picker-basic-cell-active-with-range-color": "darken(@primary-color, 20%)", 58 | }; 59 | const lightVars = { 60 | ...getLessVars(antdLightPath), 61 | "@primary-color": defaultVars["@primary-color"], 62 | }; 63 | const configVars = reduceMap(varColors, "key", "value"); 64 | try { 65 | // 写入json文件 66 | fs.writeFileSync( 67 | defaultPath, 68 | JSON.stringify({ 69 | ...defaultVars, 70 | ...lightVars, 71 | ...configVars, 72 | }) 73 | ); 74 | fs.writeFileSync( 75 | darkPath, 76 | JSON.stringify({ 77 | ...defaultVars, 78 | ...darkVars, 79 | ...configVars, 80 | }) 81 | ); 82 | } catch (error) { 83 | throw error; 84 | } 85 | 86 | // 使用 antd-theme-generator 87 | const options = { 88 | antDir: path.join(__dirname, "./node_modules/antd"), 89 | stylesDir: path.join(__dirname, "./src"), 90 | varFile, 91 | themeVariables: Array.from( 92 | new Set([ 93 | ...Object.keys(darkVars), 94 | ...Object.keys(lightVars), 95 | ...Object.keys(defaultVars), 96 | ...varColors.map((i) => i.key), 97 | ]) 98 | ), //需要动态切换的主题变量 99 | indexFileName: "index.html", 100 | outputFilePath, 101 | }; 102 | 103 | generateTheme(options) 104 | .then((less) => { 105 | console.log("Theme generated successfully"); 106 | }) 107 | .catch((error) => { 108 | console.log("Error", error); 109 | }); 110 | } 111 | 112 | function reduceMap(listObj, key, value) { 113 | return listObj.reduce((a, c) => { 114 | a[c[key]] = c[value]; 115 | return a; 116 | }, {}); 117 | } 118 | 119 | function delFile(path) { 120 | if (typeof path === "string") { 121 | if (fs.existsSync(path)) { 122 | fs.unlinkSync(path); 123 | } 124 | return; 125 | } 126 | if (Array.isArray(path)) { 127 | path.forEach((item) => delFile(item)); 128 | } 129 | } 130 | 131 | if (showColorSet) { 132 | colorStart(); 133 | } else { 134 | delFile([varJsonPath, outputFilePath, defaultPath, darkPath]); 135 | } 136 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const paths = require("./paths"); 6 | const appPackageJson = require(paths.appPackageJson) 7 | const showColorSet = process.env.COLOR === "true"; 8 | try { 9 | var varColors = require("../color.json.js"); 10 | } catch (error) { } 11 | // Make sure that including paths.js after env.js will read .env variables. 12 | delete require.cache[require.resolve("./paths")]; 13 | 14 | const NODE_ENV = process.env.NODE_ENV; 15 | if (!NODE_ENV) { 16 | throw new Error( 17 | "The NODE_ENV environment variable is required but was not specified." 18 | ); 19 | } 20 | 21 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 22 | const dotenvFiles = [ 23 | `${paths.dotenv}.${NODE_ENV}.local`, 24 | // Don't include `.env.local` for `test` environment 25 | // since normally you expect tests to produce the same 26 | // results for everyone 27 | NODE_ENV !== "test" && `${paths.dotenv}.local`, 28 | `${paths.dotenv}.${NODE_ENV}`, 29 | paths.dotenv, 30 | ].filter(Boolean); 31 | 32 | // Load environment variables from .env* files. Suppress warnings using silent 33 | // if this file is missing. dotenv will never modify any environment variables 34 | // that have already been set. Variable expansion is supported in .env files. 35 | // https://github.com/motdotla/dotenv 36 | // https://github.com/motdotla/dotenv-expand 37 | dotenvFiles.forEach((dotenvFile) => { 38 | if (fs.existsSync(dotenvFile)) { 39 | require("dotenv-expand")( 40 | require("dotenv").config({ 41 | path: dotenvFile, 42 | }) 43 | ); 44 | } 45 | }); 46 | 47 | // We support resolving modules according to `NODE_PATH`. 48 | // This lets you use absolute paths in imports inside large monorepos: 49 | // https://github.com/facebook/create-react-app/issues/253. 50 | // It works similar to `NODE_PATH` in Node itself: 51 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 52 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 53 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 54 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 55 | // We also resolve them to make sure all tools using them work consistently. 56 | const appDirectory = fs.realpathSync(process.cwd()); 57 | process.env.NODE_PATH = (process.env.NODE_PATH || "") 58 | .split(path.delimiter) 59 | .filter((folder) => folder && !path.isAbsolute(folder)) 60 | .map((folder) => path.resolve(appDirectory, folder)) 61 | .join(path.delimiter); 62 | 63 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 64 | // injected into the application via DefinePlugin in webpack configuration. 65 | const REACT_APP = /^REACT_APP_/i; 66 | 67 | function getClientEnvironment(publicUrl) { 68 | const raw = Object.keys(process.env) 69 | .filter((key) => REACT_APP.test(key)) 70 | .reduce( 71 | (env, key) => { 72 | env[key] = process.env[key]; 73 | return env; 74 | }, 75 | { 76 | NODE_ENV: process.env.NODE_ENV || "development", 77 | PUBLIC_URL: publicUrl, 78 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 79 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 80 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 81 | FAST_REFRESH: process.env.FAST_REFRESH !== "false", 82 | varColors: varColors || [], 83 | showColorSet, 84 | } 85 | ); 86 | const menuStringified = Object.keys(appPackageJson.MENUDATA).reduce((a, key) => { 87 | a[key] = JSON.stringify(appPackageJson.MENUDATA[key]) 88 | return a 89 | }, {}) 90 | const stringified = { 91 | "process.env": Object.keys(raw).reduce((env, key) => { 92 | env[key] = JSON.stringify(raw[key]); 93 | return env; 94 | }, {}), 95 | ...menuStringified 96 | }; 97 | 98 | return { raw, stringified }; 99 | } 100 | 101 | module.exports = getClientEnvironment; 102 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babelJest = require('babel-jest'); 4 | 5 | const hasJsxRuntime = (() => { 6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 7 | return false; 8 | } 9 | 10 | try { 11 | require.resolve('react/jsx-runtime'); 12 | return true; 13 | } catch (e) { 14 | return false; 15 | } 16 | })(); 17 | 18 | module.exports = babelJest.createTransformer({ 19 | presets: [ 20 | [ 21 | require.resolve('babel-preset-react-app'), 22 | { 23 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 24 | }, 25 | ], 26 | ], 27 | babelrc: false, 28 | configFile: false, 29 | }); 30 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | if (!baseUrl) { 18 | return ''; 19 | } 20 | 21 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 22 | 23 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 24 | // the default behavior. 25 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 26 | return null; 27 | } 28 | 29 | // Allow the user set the `baseUrl` to `appSrc`. 30 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 31 | return [paths.appSrc]; 32 | } 33 | 34 | // If the path is equal to the root directory we ignore it here. 35 | // We don't want to allow importing from the root directly as source files are 36 | // not transpiled outside of `src`. We do allow importing them with the 37 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 38 | // an alias. 39 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 40 | return null; 41 | } 42 | 43 | // Otherwise, throw an error. 44 | throw new Error( 45 | chalk.red.bold( 46 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 47 | ' Create React App does not support other values at this time.' 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 54 | * 55 | * @param {*} options 56 | */ 57 | function getWebpackAliases(options = {}) { 58 | const baseUrl = options.baseUrl; 59 | 60 | if (!baseUrl) { 61 | return {}; 62 | } 63 | 64 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 65 | 66 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 67 | return { 68 | src: paths.appSrc, 69 | }; 70 | } 71 | } 72 | 73 | /** 74 | * Get jest aliases based on the baseUrl of a compilerOptions object. 75 | * 76 | * @param {*} options 77 | */ 78 | function getJestAliases(options = {}) { 79 | const baseUrl = options.baseUrl; 80 | 81 | if (!baseUrl) { 82 | return {}; 83 | } 84 | 85 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 86 | 87 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 88 | return { 89 | '^src/(.*)$': '/src/$1', 90 | }; 91 | } 92 | } 93 | 94 | function getModules() { 95 | // Check if TypeScript is setup 96 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 97 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 98 | 99 | if (hasTsConfig && hasJsConfig) { 100 | throw new Error( 101 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 102 | ); 103 | } 104 | 105 | let config; 106 | 107 | // If there's a tsconfig.json we assume it's a 108 | // TypeScript project and set up the config 109 | // based on tsconfig.json 110 | if (hasTsConfig) { 111 | const ts = require(resolve.sync('typescript', { 112 | basedir: paths.appNodeModules, 113 | })); 114 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 115 | // Otherwise we'll check if there is jsconfig.json 116 | // for non TS projects. 117 | } else if (hasJsConfig) { 118 | config = require(paths.appJsConfig); 119 | } 120 | 121 | config = config || {}; 122 | const options = config.compilerOptions || {}; 123 | 124 | const additionalModulePaths = getAdditionalModulePaths(options); 125 | 126 | return { 127 | additionalModulePaths: additionalModulePaths, 128 | webpackAliases: getWebpackAliases(options), 129 | jestAliases: getJestAliases(options), 130 | hasTsConfig, 131 | }; 132 | } 133 | 134 | module.exports = getModules(); 135 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right 109 | 110 | <%}%> 111 | 112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kongyijilafumi/react-ant-admin/f4acf55a7810647f8211efb8e6c2c503b9e013b9/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kongyijilafumi/react-ant-admin/f4acf55a7810647f8211efb8e6c2c503b9e013b9/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 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Do this as the first thing so that any code reading it knows the right env. 3 | process.env.BABEL_ENV = 'production'; 4 | process.env.NODE_ENV = 'production'; 5 | process.env.GENERATE_SOURCEMAP = "false" 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on('unhandledRejection', err => { 10 | throw err; 11 | }); 12 | 13 | // Ensure environment variables are read. 14 | require('../config/env'); 15 | 16 | 17 | const path = require('path'); 18 | const chalk = require('react-dev-utils/chalk'); 19 | const fs = require('fs-extra'); 20 | const bfj = require('bfj'); 21 | const webpack = require('webpack'); 22 | const configFactory = require('../config/webpack.config'); 23 | const paths = require('../config/paths'); 24 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 25 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 26 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 27 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 28 | const printBuildError = require('react-dev-utils/printBuildError'); 29 | 30 | const measureFileSizesBeforeBuild = 31 | FileSizeReporter.measureFileSizesBeforeBuild; 32 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 33 | const useYarn = fs.existsSync(paths.yarnLockFile); 34 | 35 | // These sizes are pretty large. We'll warn for bundles exceeding them. 36 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 37 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 38 | 39 | const isInteractive = process.stdout.isTTY; 40 | 41 | // Warn and crash if required files are missing 42 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 43 | process.exit(1); 44 | } 45 | 46 | const argv = process.argv.slice(2); 47 | const writeStatsJson = argv.indexOf('--stats') !== -1; 48 | 49 | // Generate configuration 50 | const config = configFactory('production'); 51 | 52 | // We require that you explicitly set browsers and do not fall back to 53 | // browserslist defaults. 54 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 55 | checkBrowsers(paths.appPath, isInteractive) 56 | .then(() => { 57 | // First, read the current file sizes in build directory. 58 | // This lets us display how much they changed later. 59 | return measureFileSizesBeforeBuild(paths.appBuild); 60 | }) 61 | .then(previousFileSizes => { 62 | // Remove all content but keep the directory so that 63 | // if you're in it, you don't end up in Trash 64 | fs.emptyDirSync(paths.appBuild); 65 | // Merge with the public folder 66 | copyPublicFolder(); 67 | // Start the webpack build 68 | return build(previousFileSizes); 69 | }) 70 | .then( 71 | ({ stats, previousFileSizes, warnings }) => { 72 | if (warnings.length) { 73 | console.log(chalk.yellow('Compiled with warnings.\n')); 74 | console.log(warnings.join('\n\n')); 75 | console.log( 76 | '\nSearch for the ' + 77 | chalk.underline(chalk.yellow('keywords')) + 78 | ' to learn more about each warning.' 79 | ); 80 | console.log( 81 | 'To ignore, add ' + 82 | chalk.cyan('// eslint-disable-next-line') + 83 | ' to the line before.\n' 84 | ); 85 | } else { 86 | console.log(chalk.green('Compiled successfully.\n')); 87 | } 88 | 89 | console.log('File sizes after gzip:\n'); 90 | printFileSizesAfterBuild( 91 | stats, 92 | previousFileSizes, 93 | paths.appBuild, 94 | WARN_AFTER_BUNDLE_GZIP_SIZE, 95 | WARN_AFTER_CHUNK_GZIP_SIZE 96 | ); 97 | console.log(); 98 | 99 | const appPackage = require(paths.appPackageJson); 100 | const publicUrl = paths.publicUrlOrPath; 101 | const publicPath = config.output.publicPath; 102 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 103 | printHostingInstructions( 104 | appPackage, 105 | publicUrl, 106 | publicPath, 107 | buildFolder, 108 | useYarn 109 | ); 110 | }, 111 | err => { 112 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; 113 | if (tscCompileOnError) { 114 | console.log( 115 | chalk.yellow( 116 | 'Compiled with the following type errors (you may want to check these before deploying your app):\n' 117 | ) 118 | ); 119 | printBuildError(err); 120 | } else { 121 | console.log(chalk.red('Failed to compile.\n')); 122 | printBuildError(err); 123 | process.exit(1); 124 | } 125 | } 126 | ) 127 | .catch(err => { 128 | if (err && err.message) { 129 | console.log(err.message); 130 | } 131 | process.exit(1); 132 | }); 133 | 134 | // Create the production build and print the deployment instructions. 135 | function build(previousFileSizes) { 136 | console.log('Creating an optimized production build...'); 137 | 138 | const compiler = webpack(config); 139 | return new Promise((resolve, reject) => { 140 | compiler.run((err, stats) => { 141 | let messages; 142 | if (err) { 143 | if (!err.message) { 144 | return reject(err); 145 | } 146 | 147 | let errMessage = err.message; 148 | 149 | // Add additional information for postcss errors 150 | if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) { 151 | errMessage += 152 | '\nCompileError: Begins at CSS selector ' + 153 | err['postcssNode'].selector; 154 | } 155 | 156 | messages = formatWebpackMessages({ 157 | errors: [errMessage], 158 | warnings: [], 159 | }); 160 | } else { 161 | messages = formatWebpackMessages( 162 | stats.toJson({ all: false, warnings: true, errors: true }) 163 | ); 164 | } 165 | if (messages.errors.length) { 166 | // Only keep the first error. Others are often indicative 167 | // of the same problem, but confuse the reader with noise. 168 | if (messages.errors.length > 1) { 169 | messages.errors.length = 1; 170 | } 171 | return reject(new Error(messages.errors.join('\n\n'))); 172 | } 173 | if ( 174 | process.env.CI && 175 | (typeof process.env.CI !== 'string' || 176 | process.env.CI.toLowerCase() !== 'false') && 177 | messages.warnings.length 178 | ) { 179 | console.log( 180 | chalk.yellow( 181 | '\nTreating warnings as errors because process.env.CI = true.\n' + 182 | 'Most CI servers set it automatically.\n' 183 | ) 184 | ); 185 | return reject(new Error(messages.warnings.join('\n\n'))); 186 | } 187 | 188 | const resolveArgs = { 189 | stats, 190 | previousFileSizes, 191 | warnings: messages.warnings, 192 | }; 193 | 194 | if (writeStatsJson) { 195 | return bfj 196 | .write(paths.appBuild + '/bundle-stats.json', stats.toJson()) 197 | .then(() => resolve(resolveArgs)) 198 | .catch(error => reject(new Error(error))); 199 | } 200 | 201 | return resolve(resolveArgs); 202 | }); 203 | }); 204 | } 205 | 206 | function copyPublicFolder() { 207 | fs.copySync(paths.appPublic, paths.appBuild, { 208 | dereference: true, 209 | filter: file => file !== paths.appHtml, 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const semver = require('semver'); 32 | const paths = require('../config/paths'); 33 | const configFactory = require('../config/webpack.config'); 34 | const createDevServerConfig = require('../config/webpackDevServer.config'); 35 | const getClientEnvironment = require('../config/env'); 36 | const react = require(require.resolve('react', { paths: [paths.appPath] })); 37 | 38 | const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); 39 | const useYarn = fs.existsSync(paths.yarnLockFile); 40 | const isInteractive = process.stdout.isTTY; 41 | 42 | // Warn and crash if required files are missing 43 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 44 | process.exit(1); 45 | } 46 | 47 | // Tools like Cloud9 rely on this. 48 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 49 | const HOST = process.env.HOST || '0.0.0.0'; 50 | 51 | if (process.env.HOST) { 52 | console.log( 53 | chalk.cyan( 54 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 55 | chalk.bold(process.env.HOST) 56 | )}` 57 | ) 58 | ); 59 | console.log( 60 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 61 | ); 62 | console.log( 63 | `Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}` 64 | ); 65 | console.log(); 66 | } 67 | 68 | // We require that you explicitly set browsers and do not fall back to 69 | // browserslist defaults. 70 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 71 | checkBrowsers(paths.appPath, isInteractive) 72 | .then(() => { 73 | // We attempt to use the default port but if it is busy, we offer the user to 74 | // run on a different port. `choosePort()` Promise resolves to the next free port. 75 | return choosePort(HOST, DEFAULT_PORT); 76 | }) 77 | .then(port => { 78 | if (port == null) { 79 | // We have not found a port. 80 | return; 81 | } 82 | 83 | const config = configFactory('development'); 84 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 85 | const appName = require(paths.appPackageJson).name; 86 | 87 | const useTypeScript = fs.existsSync(paths.appTsConfig); 88 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; 89 | const urls = prepareUrls( 90 | protocol, 91 | HOST, 92 | port, 93 | paths.publicUrlOrPath.slice(0, -1) 94 | ); 95 | const devSocket = { 96 | warnings: warnings => 97 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 98 | errors: errors => 99 | devServer.sockWrite(devServer.sockets, 'errors', errors), 100 | }; 101 | // Create a webpack compiler that is configured with custom messages. 102 | const compiler = createCompiler({ 103 | appName, 104 | config, 105 | devSocket, 106 | urls, 107 | useYarn, 108 | useTypeScript, 109 | tscCompileOnError, 110 | webpack, 111 | }); 112 | // Load proxy config 113 | const proxySetting = require(paths.appPackageJson).proxy; 114 | const proxyConfig = prepareProxy( 115 | proxySetting, 116 | paths.appPublic, 117 | paths.publicUrlOrPath 118 | ); 119 | // Serve webpack assets generated by the compiler over a web server. 120 | const serverConfig = createDevServerConfig( 121 | proxyConfig, 122 | urls.lanUrlForConfig 123 | ); 124 | const devServer = new WebpackDevServer(compiler, serverConfig); 125 | // Launch WebpackDevServer. 126 | devServer.listen(port, HOST, err => { 127 | if (err) { 128 | return console.log(err); 129 | } 130 | if (isInteractive) { 131 | clearConsole(); 132 | } 133 | 134 | if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { 135 | console.log( 136 | chalk.yellow( 137 | `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.` 138 | ) 139 | ); 140 | } 141 | 142 | console.log(chalk.cyan('Starting the development server...\n')); 143 | openBrowser(urls.localUrlForBrowser); 144 | }); 145 | 146 | ['SIGINT', 'SIGTERM'].forEach(function (sig) { 147 | process.on(sig, function () { 148 | devServer.close(); 149 | process.exit(); 150 | }); 151 | }); 152 | 153 | if (process.env.CI !== 'true') { 154 | // Gracefully exit when stdin ends 155 | process.stdin.on('end', function () { 156 | devServer.close(); 157 | process.exit(); 158 | }); 159 | } 160 | }) 161 | .catch(err => { 162 | if (err && err.message) { 163 | console.log(err.message); 164 | } 165 | process.exit(1); 166 | }); 167 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Provider } from "react-redux"; 2 | import store from "./store"; 3 | import loadable from "@loadable/component"; 4 | import LayoutSet from "./components/layout-set"; 5 | const AppRouter = loadable(() => import("./router/appRouter")); 6 | 7 | function Theme() { 8 | if (process.env.showColorSet) { 9 | const Com = loadable(() => import("@/components/theme")) 10 | return 11 | } 12 | return null 13 | } 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import ajax from "@/common/ajax"; 2 | import mock from "../mock/index"; 3 | const request = process.env.REACT_APP_MOCK === "1" ? mock : ajax; 4 | const getMenu = () => request.get("/getmenu"); 5 | const getMenuList = () => request.get("/getmenulist"); 6 | const login = (data) => request.post("/login", data); 7 | const addMenu = (data) => request.post("/addmenu", data); 8 | const addMsg = (data) => request.post("/addmessage", data); 9 | const getMsg = (data) => request.get("/getmessage", data); 10 | const getPower = () => request.get("/getpower"); 11 | const delMenu = (data) => request.post("/delmenu", data); 12 | const getMenuInfo = (data) => request.get("/getmenuinfo", data); 13 | const editMenu = (data) => request.post("/editmenuinfo", data); 14 | const getVisitorList = (data) => request.get("/getiplist", data); 15 | const getVisitorData = () => request.get("/getvisitordata"); 16 | const getUserList = (data) => request.get("/getuserlist", data); 17 | const addUser = (data) => request.post("/adduserinfo", data); 18 | const getUser = (data) => request.get("/getuserinfo", data); 19 | const editUser = (data) => request.post("/edituserinfo", data); 20 | const editType = (data) => request.post("/edittype", data); 21 | const addType = (data) => request.post("/addtype", data); 22 | const getFeedBack = (data) => request.post("/getfeedback", data); 23 | const reply = (data) => request.post("/reply", data); 24 | export { 25 | getMenu, 26 | login, 27 | addMenu, 28 | addMsg, 29 | getMsg, 30 | getPower, 31 | delMenu, 32 | getMenuInfo, 33 | editMenu, 34 | getVisitorList, 35 | getVisitorData, 36 | getUserList, 37 | addUser, 38 | getUser, 39 | editUser, 40 | editType, 41 | addType, 42 | getMenuList, 43 | getFeedBack, 44 | reply, 45 | }; 46 | -------------------------------------------------------------------------------- /src/assets/images/layout1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kongyijilafumi/react-ant-admin/f4acf55a7810647f8211efb8e6c2c503b9e013b9/src/assets/images/layout1.jpg -------------------------------------------------------------------------------- /src/assets/images/layout2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kongyijilafumi/react-ant-admin/f4acf55a7810647f8211efb8e6c2c503b9e013b9/src/assets/images/layout2.jpg -------------------------------------------------------------------------------- /src/assets/images/layout3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kongyijilafumi/react-ant-admin/f4acf55a7810647f8211efb8e6c2c503b9e013b9/src/assets/images/layout3.jpg -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 28 Copy 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | -------------------------------------------------------------------------------- /src/assets/theme/var.less: -------------------------------------------------------------------------------- 1 | // 脚本抓取变量 -- 开始 2 | // script-start 3 | /** 4 | 在这里写自定义less全局变量。参照一下格式 颜色值最好为十六进制或者rgb格式 如: #000 rgb(0,2,0) 5 | 因为用优先级关系,antd的变量放在下面,最上面的优先级最大 6 | **/ 7 | // 布局 8 | @layout-aside-activeBg: #e6f7ff; // 侧边栏选中背景色 9 | @test : red; // 测试颜色 10 | @import "~antd/lib/style/themes/default.less"; 11 | // script-end 12 | 13 | // 脚本抓取变量 -- 结束 14 | 15 | // 全局样式 16 | .hide-scrollbar { 17 | scrollbar-width : none; 18 | -ms-overflow-style: none; 19 | 20 | &::-webkit-scrollbar { 21 | display: none; 22 | } 23 | } 24 | 25 | #root { 26 | position: relative; 27 | } 28 | 29 | .loading-page { 30 | 31 | width : 100%; 32 | height : 100vh; 33 | z-index: 1; 34 | 35 | .ant-spin-text { 36 | margin-top: 20px; 37 | font-size : 15px; 38 | color : #000; 39 | } 40 | } 41 | 42 | .mixin-text-over() { 43 | white-space : nowrap; 44 | overflow : hidden; 45 | text-overflow: ellipsis; 46 | } -------------------------------------------------------------------------------- /src/common/ajax.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { message, notification } from "antd"; 3 | import { getToken, clearLocalDatas } from "@/utils"; 4 | import { USER_INFO, TOKEN, MENU } from "@/common" 5 | import qs from "qs"; 6 | // 请求地址 7 | const BASE_URL = process.env.REACT_APP_API_BASEURL || "/api/react-ant-admin"; 8 | 9 | // 错误信息 10 | const codeMessage = { 11 | 200: "服务器成功返回请求的数据。", 12 | 201: "新建或修改数据成功。", 13 | 202: "一个请求已经进入后台排队(异步任务)。", 14 | 204: "删除数据成功。", 15 | 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。", 16 | 401: "用户没有权限(令牌、用户名、密码错误)。", 17 | 403: "用户得到授权,但是访问是被禁止的。", 18 | 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。", 19 | 406: "请求的格式不可得。", 20 | 410: "请求的资源被永久删除,且不会再得到的。", 21 | 422: "当创建一个对象时,发生一个验证错误。", 22 | 500: "服务器发生错误,请检查服务器。", 23 | 502: "网关错误。", 24 | 503: "服务不可用,服务器暂时过载或维护。", 25 | 504: "网关超时。", 26 | }; 27 | 28 | // 请求配置文件 29 | const config = { 30 | // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。 31 | // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL 32 | baseURL: BASE_URL, 33 | 34 | timeout: 1000 * 15, 35 | 36 | // `withCredentials` 表示跨域请求时是否需要使用凭证 37 | withCredentials: false, 38 | 39 | // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目 40 | // 如果设置为0,将不会 follow 任何重定向 41 | maxRedirects: 3, 42 | headers: { 43 | "Content-Type": " application/json;charset=UTF-8", 44 | }, 45 | }; 46 | 47 | // 创建ajax实例 48 | const instance = axios.create(config); 49 | instance.interceptors.request.use( 50 | function (config) { 51 | // 在发送请求之前做些什么 52 | let token = getToken(); 53 | if (token) { 54 | config.headers["authorization"] = token; 55 | } 56 | return config; 57 | }, 58 | function (error) { 59 | // 对请求错误做些什么 60 | return Promise.reject(error); 61 | } 62 | ); 63 | 64 | instance.interceptors.response.use( 65 | function (response) { 66 | if (response.data) { 67 | let { msg, status } = response.data; 68 | if (status === 1) { 69 | message.error(msg); 70 | } 71 | } 72 | return response && response.data; 73 | }, 74 | function (error) { 75 | const { response, message } = error; 76 | if (response && response.status) { 77 | const errorText = codeMessage[response.status] || response.statusText; 78 | const { status, config } = response; 79 | notification.error({ 80 | message: `请求错误 ${status}: ${config.url}`, 81 | description: errorText, 82 | }); 83 | if (response.status === 401 || response.status === 403) { 84 | clearLocalDatas([USER_INFO, TOKEN, MENU]); 85 | setTimeout(() => { 86 | window.location.reload(); 87 | }, 1000); 88 | } 89 | } else if (!response) { 90 | let description = 91 | message === "Network Error" 92 | ? "网络错误,请检查客户端是否存在网络故障或服务端无法响应" 93 | : "客户端出现错误"; 94 | clearLocalDatas(["token"]); 95 | notification.error({ 96 | description, 97 | message: "状态异常", 98 | }); 99 | } 100 | // 对响应错误做点什么 101 | return Promise.reject(error); 102 | } 103 | ); 104 | const rewirteGet = instance.get; 105 | instance.get = function (url, data, ...any) { 106 | let query = qs.stringify(data, { addQueryPrefix: true }); 107 | return rewirteGet(url + query, ...any); 108 | }; 109 | export default instance; 110 | -------------------------------------------------------------------------------- /src/common/index.js: -------------------------------------------------------------------------------- 1 | import { getLocalMenu, saveLocalMenu } from "../utils"; 2 | import { getMenu } from "@/api"; 3 | 4 | // 解决 多次请求 菜单问题 5 | let currentMenuJob 6 | function getMenus() { 7 | if (currentMenuJob) { 8 | return currentMenuJob 9 | } 10 | const job = new Promise((resolve) => { 11 | let localMenu = getLocalMenu(); 12 | if (localMenu) { 13 | return resolve(localMenu); 14 | } 15 | getMenu() 16 | .then((result) => { 17 | if (result) { 18 | saveLocalMenu(result); 19 | resolve(result); 20 | } 21 | }) 22 | .catch((err) => { 23 | resolve([]); 24 | }); 25 | }) 26 | currentMenuJob = job 27 | job.finally(() => currentMenuJob = null) 28 | return job 29 | } 30 | 31 | export { getMenus }; 32 | export * from "./var" 33 | export { default as ajax } from "./ajax" 34 | -------------------------------------------------------------------------------- /src/common/var.js: -------------------------------------------------------------------------------- 1 | export const USER_INFO = "USER_INFO"; // 用户信息 2 | export const TOKEN = "REACT_ADMIN_TOKEN"; // token 3 | export const MENU = "MENU"; // 菜单 4 | export const COMPONENTS_VISIBEL = "COMPONENTS_VISIBEL"; // 组件显示 5 | export const LAYOUT_MODE = "LAYOUT_MODE" // 布局模式 -------------------------------------------------------------------------------- /src/components/color/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useState } from "react"; 2 | import { SketchPicker } from "react-color"; 3 | import { Row, Col, Button } from "antd"; 4 | import "./index.less"; 5 | 6 | const getPositon = (x, y) => ({ left: x, top: y }); 7 | 8 | function Color({ 9 | color, 10 | pageX, 11 | pageY, 12 | onSureChange, 13 | colorKey, 14 | isShow, 15 | onClose, 16 | }) { 17 | const [changeColor, setColor] = useState(color); 18 | 19 | // 变化重置 20 | useEffect(() => { 21 | if (color) { 22 | setColor(color); 23 | } 24 | }, [color]); 25 | 26 | // 同步改变 27 | const onChange = useCallback((v) => { 28 | setColor(v); 29 | }, []); 30 | 31 | // 确认 32 | const onChangeComplete = useCallback(() => { 33 | onSureChange(changeColor, colorKey); 34 | }, [changeColor, onSureChange, colorKey]); 35 | 36 | return ( 37 | e.stopPropagation()} 41 | > 42 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 | ); 55 | } 56 | export default memo( 57 | Color, 58 | (prev, next) => 59 | prev.color === next.color && 60 | prev.colorKey === next.colorKey && 61 | prev.isShow === next.isShow && 62 | prev.pageY === next.pageY && 63 | prev.pageX === next.pageX && 64 | prev.onSureChange === next.onSureChange && 65 | prev.onClose === next.onClose 66 | ); 67 | -------------------------------------------------------------------------------- /src/components/color/index.less: -------------------------------------------------------------------------------- 1 | .fixed-wrapper { 2 | position: fixed; 3 | top : 0; 4 | left : 0; 5 | display : none; 6 | z-index : 999; 7 | width : 200px; 8 | 9 | &.show { 10 | display: block; 11 | } 12 | } -------------------------------------------------------------------------------- /src/components/contextMenu/index.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useRef, useState } from "react" 2 | import "./index.less" 3 | 4 | function onContextMenu(e) { 5 | e.stopPropagation() 6 | e.preventDefault() 7 | return false 8 | } 9 | export default function ContextMenu({ isCurrent, visible, x, y, setVisible, onClose }) { 10 | const ref = useRef() 11 | const [style, setStyle] = useState({}) 12 | 13 | const display = useMemo(() => { 14 | if (visible) { 15 | return "block" 16 | } 17 | return "none" 18 | }, [visible]) 19 | 20 | const visibility = useMemo(() => { 21 | if (visible) { 22 | document.body.style.overflow = "hidden" 23 | return "visible" 24 | } 25 | document.body.style.overflow = "unset" 26 | return "hidden" 27 | }, [visible]) 28 | 29 | useEffect(() => { 30 | const wwidth = window.screen.availWidth || document.body.offsetWidth 31 | const width = ref.current.offsetWidth 32 | let left = x, top = y; 33 | if (x + width > wwidth) { 34 | left = x - width 35 | } 36 | const newStyle = { left, top, visibility } 37 | setStyle(newStyle) 38 | }, [x, y, visibility, ref]) 39 | // 关闭 菜单 40 | const closeMenu = useCallback(() => { 41 | if (visibility === "visible") { 42 | console.log("关闭弹窗"); 43 | setVisible(false) 44 | } 45 | return false 46 | }, [setVisible, visibility]) 47 | 48 | // 关闭所有选项 49 | const closeAll = useCallback((e) => { 50 | e.stopPropagation() 51 | console.log("关闭全部"); 52 | onClose("all") 53 | closeMenu() 54 | }, [closeMenu, onClose]) 55 | 56 | // 关闭右侧 选项 57 | const closeRight = useCallback((e) => { 58 | e.stopPropagation() 59 | console.log("右"); 60 | onClose("right") 61 | closeMenu() 62 | }, [closeMenu, onClose]) 63 | 64 | // 关闭左侧 选项 65 | const closeLeft = useCallback((e) => { 66 | e.stopPropagation() 67 | console.log("左"); 68 | onClose("left") 69 | closeMenu() 70 | }, [closeMenu, onClose]) 71 | 72 | // 关闭当前选项 73 | const closeCurrent = useCallback((e) => { 74 | e.stopPropagation() 75 | console.log("当前"); 76 | onClose("current") 77 | closeMenu() 78 | }, [closeMenu, onClose]) 79 | 80 | return
86 |
    87 |
  • 关闭所有
  • 88 |
  • 关闭右侧
  • 89 |
  • 关闭左侧
  • 90 | { 91 | isCurrent &&
  • 关闭当前
  • 92 | } 93 |
94 |
95 | } -------------------------------------------------------------------------------- /src/components/contextMenu/index.less: -------------------------------------------------------------------------------- 1 | .centext-menu { 2 | // display : none; 3 | position: fixed; 4 | top : 0; 5 | left : 0; 6 | right : 0; 7 | bottom : 0; 8 | z-index : 999; 9 | overflow: hidden; 10 | // border : 1px solid @primary-color ; 11 | 12 | ul { 13 | position : absolute; 14 | width : 120px; 15 | padding : 0; 16 | margin : 0; 17 | background-color: @body-background; 18 | overflow : hidden; 19 | border-radius : @border-radius-base; 20 | box-shadow : 2px 0 8px @shadow-color; 21 | z-index : 1000; 22 | border : 1px solid @border-color-base; 23 | } 24 | 25 | li { 26 | height : 38px; 27 | line-height : 38px; 28 | font-size : 14px; 29 | text-align : center; 30 | border-bottom: 1px solid @border-color-split; 31 | list-style : none; 32 | color : @text-color; 33 | cursor : pointer; 34 | transition : all @ease-base-in; 35 | 36 | &:last-child { 37 | border: none; 38 | } 39 | 40 | &:hover { 41 | background-color: @item-hover-bg; 42 | color : @link-hover-color; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/components/dnd/free.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DraggableArea } from "react-draggable-tags"; 3 | 4 | export default function FreeDnd({ data, onChange, renderItem }) { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/echarts/bar/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from "react"; 2 | import ReactEChartsCore from "echarts-for-react/lib/core"; 3 | import * as echarts from "echarts/core"; 4 | import { BarChart } from "echarts/charts"; 5 | import { TooltipComponent, GridComponent } from "echarts/components"; 6 | import { CanvasRenderer } from "echarts/renderers"; 7 | 8 | echarts.use([TooltipComponent, GridComponent, BarChart, CanvasRenderer]); 9 | 10 | function Line({ theme = "light", style = {}, option = {} }) { 11 | const [echartRef, setRef] = useState(null); 12 | useEffect(() => { 13 | if (echartRef) { 14 | echartRef.getEchartsInstance().setOption(option); 15 | } 16 | // eslint-disable-next-line 17 | }, [option]); 18 | return ( 19 | setRef(e)} 22 | echarts={echarts} 23 | option={option} 24 | theme={theme} 25 | style={style} 26 | notMerge={true} 27 | lazyUpdate={true} 28 | /> 29 | ); 30 | } 31 | 32 | export default memo(Line, (prev, next) => prev.option === next.option); 33 | -------------------------------------------------------------------------------- /src/components/echarts/index.js: -------------------------------------------------------------------------------- 1 | import loadable from "@loadable/component"; 2 | import { Spin } from "antd"; 3 | 4 | const loaddingCom = ( 5 | 15 | ); 16 | const Line = loadable(() => import("./line"), { fallback: loaddingCom }); 17 | const Bar = loadable(() => import("./bar"), { fallback: loaddingCom }); 18 | const Pie = loadable(() => import("./pie"), { fallback: loaddingCom }); 19 | 20 | export { Line, Bar, Pie }; 21 | -------------------------------------------------------------------------------- /src/components/echarts/line/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from "react"; 2 | import ReactEChartsCore from "echarts-for-react/lib/core"; 3 | import * as echarts from "echarts/core"; 4 | import { LineChart } from "echarts/charts"; 5 | import { TooltipComponent, GridComponent } from "echarts/components"; 6 | import { CanvasRenderer } from "echarts/renderers"; 7 | 8 | echarts.use([TooltipComponent, GridComponent, LineChart, CanvasRenderer]); 9 | 10 | function Line({ theme = "light", style = {}, option = {} }) { 11 | const [echartRef, setRef] = useState(null); 12 | useEffect(() => { 13 | if (echartRef) { 14 | echartRef.getEchartsInstance().setOption(option); 15 | } 16 | // eslint-disable-next-line 17 | }, [option]); 18 | return ( 19 | setRef(e)} 22 | echarts={echarts} 23 | option={option} 24 | theme={theme} 25 | style={style} 26 | notMerge={true} 27 | lazyUpdate={true} 28 | /> 29 | ); 30 | } 31 | 32 | export default memo(Line, (prev, next) => prev.option === next.option); 33 | -------------------------------------------------------------------------------- /src/components/echarts/pie/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from "react"; 2 | import ReactEChartsCore from "echarts-for-react/lib/core"; 3 | import * as echarts from "echarts/core"; 4 | import { PieChart } from "echarts/charts"; 5 | import { TooltipComponent, GridComponent } from "echarts/components"; 6 | import { CanvasRenderer } from "echarts/renderers"; 7 | 8 | echarts.use([TooltipComponent, GridComponent, PieChart, CanvasRenderer]); 9 | 10 | function Line({ theme = "light", style = {}, option = {} }) { 11 | const [echartRef, setRef] = useState(null); 12 | useEffect(() => { 13 | if (echartRef) { 14 | echartRef.getEchartsInstance().setOption(option); 15 | } 16 | // eslint-disable-next-line 17 | }, [option]); 18 | return ( 19 | setRef(e)} 22 | echarts={echarts} 23 | option={option} 24 | theme={theme} 25 | style={style} 26 | notMerge={true} 27 | lazyUpdate={true} 28 | /> 29 | ); 30 | } 31 | 32 | export default memo(Line, (prev, next) => prev.option === next.option); 33 | -------------------------------------------------------------------------------- /src/components/form/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Form, Input, Select, Radio, Switch, InputNumber } from "antd"; 3 | 4 | function getChild(type) { 5 | switch (type) { 6 | case "input": 7 | return Input; 8 | case "select": 9 | return Select; 10 | case "radio": 11 | return Radio.Group; 12 | case "switch": 13 | return Switch; 14 | case "inputNumber": 15 | return InputNumber; 16 | case "inputText": 17 | return Input.TextArea 18 | default: 19 | return null; 20 | } 21 | } 22 | function renderItem({ itemType, childProps, itemProps }) { 23 | const Child = getChild(itemType); 24 | if (!Child) return Child; 25 | return ( 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export default function MyForm({ items, handleInstance, children, ...props }) { 33 | const [formInstance] = Form.useForm(); 34 | const [formBody, setFormBody] = useState(null); 35 | useEffect(() => { 36 | if (formInstance && handleInstance) { 37 | handleInstance(formInstance); 38 | } 39 | }, [formInstance, handleInstance]); 40 | useEffect(() => { 41 | if (Array.isArray(items) && items.length) { 42 | const body = items.map(renderItem).filter(Boolean); 43 | setFormBody(body); 44 | } 45 | }, [items]); 46 | return ( 47 |
48 | {formBody} 49 | {children} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/icon/index.js: -------------------------------------------------------------------------------- 1 | import { createFromIconfontCN } from "@ant-design/icons"; 2 | 3 | const MyIcon = createFromIconfontCN({ 4 | scriptUrl: process.env.PUBLIC_URL+"/iconfont.js", // 在 iconfont.cn 上生成 "//at.alicdn.com/t/font_2467607_sf5ou36jx9q.js" 5 | }); 6 | 7 | export default function Icon({ type, ...itemProps }) { 8 | if (!type) return null; 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/layout-set/index.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from "react"; 2 | import MyIcon from "../icon"; 3 | import { Button, Drawer, message, Row, Radio } from "antd"; 4 | import * as Types from "@/store/layout/actionTypes"; 5 | import { setLayoutMode, setCompVisibel as util_setCompVisibel } from "@/utils"; 6 | import singImg from "@/assets/images/layout2.jpg"; 7 | import twoImg from "@/assets/images/layout1.jpg"; 8 | import twoFlanksImg from "@/assets/images/layout3.jpg"; 9 | import "./index.less"; 10 | import { useDispatchLayout, useDispatchVisibel, useStateLayout, useStateVisibel } from "@/store/hooks"; 11 | 12 | const modes = [ 13 | { 14 | img: singImg, 15 | mode: Types.SINGLE_COLUMN, 16 | alt: "单列布局", 17 | }, 18 | { 19 | img: twoImg, 20 | mode: Types.TWO_COLUMN, 21 | alt: "两列布局", 22 | }, 23 | { 24 | img: twoFlanksImg, 25 | mode: Types.TWO_FLANKS, 26 | alt: "两侧布局", 27 | }, 28 | ]; 29 | const RadioArray = [ 30 | { 31 | l: "显示", 32 | v: true, 33 | }, 34 | { 35 | l: "隐藏", 36 | v: false, 37 | }, 38 | ]; 39 | 40 | export default function LayoutSet() { 41 | const [visible, setVisible] = useState(false); 42 | // state 43 | const componentsVisible = useStateVisibel() 44 | const layoutMode = useStateLayout() 45 | const { stateChangeLayout: setMode } = useDispatchLayout() 46 | const { stateSetVisible } = useDispatchVisibel() 47 | const wakeup = useCallback(() => setVisible(true), []); 48 | const onClose = useCallback(() => setVisible(false), []); 49 | const onChange = useMemo(() => { 50 | return (key) => { 51 | return (e) => { 52 | const val = e.target.value 53 | stateSetVisible(key, val) 54 | } 55 | } 56 | }, [stateSetVisible]) 57 | const saveLayout = useCallback(() => { 58 | setLayoutMode(layoutMode); 59 | util_setCompVisibel(componentsVisible); 60 | message.success("布局保存本地成功!"); 61 | }, [layoutMode, componentsVisible]); 62 | const setLayout = useCallback((mode) => { 63 | setMode("push", mode); 64 | message.success("布局设置成功!"); 65 | }, [setMode]); 66 | 67 | return ( 68 |
69 | 70 | 79 |

选择布局

80 | 81 | {modes.map((m) => ( 82 |
setLayout(m.mode)} 85 | className={m.mode === layoutMode ? "col active" : "col"} 86 | > 87 | {m.alt} 88 |
89 | ))} 90 |
91 |

组件显示

92 | {Object.keys(componentsVisible).map((key) => ( 93 | 94 | {key === "footer" ? "底部:" : "顶部切换导航:"} 95 | 99 | {RadioArray.map((i) => ( 100 | 101 | {i.l} 102 | 103 | ))} 104 | 105 | 106 | ))} 107 | 108 | 111 | 112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/components/layout-set/index.less: -------------------------------------------------------------------------------- 1 | .layoutset-container { 2 | position: fixed; 3 | right : 0; 4 | top : 20%; 5 | 6 | svg { 7 | width : 40px; 8 | height : 40px; 9 | cursor : pointer; 10 | background-color: @primary-3; 11 | transition : background-color .3s; 12 | 13 | &:hover { 14 | background-color: @primary-4; 15 | } 16 | } 17 | } 18 | 19 | .layoutset-drawer { 20 | .title { 21 | margin : 1em 0; 22 | font-size : 16px; 23 | font-weight: bold; 24 | } 25 | 26 | .visibel-list { 27 | margin: 10px 0; 28 | } 29 | 30 | .col { 31 | width : 50px; 32 | height : 40px; 33 | border-radius: 4px; 34 | overflow : hidden; 35 | cursor : pointer; 36 | 37 | &.active { 38 | position : relative; 39 | background: @primary-2; 40 | 41 | &::after { 42 | content : ''; 43 | position : absolute; 44 | right : 10px; 45 | bottom : 5px; 46 | width : 10px; 47 | height : 20px; 48 | border-right : 4px solid @primary-4; 49 | border-bottom: 4px solid @primary-4; 50 | transform : rotate(45deg); 51 | 52 | } 53 | } 54 | 55 | img { 56 | width : 100%; 57 | height: auto; 58 | } 59 | } 60 | 61 | .save { 62 | margin-top: 20px; 63 | } 64 | } -------------------------------------------------------------------------------- /src/components/menu-dnd/index.less: -------------------------------------------------------------------------------- 1 | .dnd-body { 2 | height : 35px; 3 | box-sizing : content-box; 4 | white-space: nowrap; 5 | overflow-x : scroll; 6 | overflow-y : hidden; 7 | font-size : 0; 8 | 9 | .dnd-items { 10 | position : relative; 11 | display : inline-block; 12 | padding : 10px 40px 5px 10px; 13 | font-size : 12px; 14 | margin-right : 2px; 15 | background-color: @body-background; 16 | box-sizing : border-box; 17 | cursor : pointer !important; 18 | color : @text-color-secondary; 19 | vertical-align : text-bottom; 20 | height : 35px; 21 | 22 | &.active { 23 | background-color: @background-color-base; 24 | color : @text-color; 25 | 26 | .anticon-close { 27 | background-color: @error-color; 28 | } 29 | 30 | .anticon-close svg { 31 | color: @heading-color; 32 | } 33 | } 34 | 35 | &:focus { 36 | outline: unset; 37 | } 38 | 39 | 40 | .anticon-close { 41 | position : absolute; 42 | top : 5px; 43 | right : 5px; 44 | width : 15px; 45 | height : 15px; 46 | border-radius: 50%; 47 | 48 | svg { 49 | width : inherit; 50 | height: inherit; 51 | color : @text-color-secondary; 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/components/modal/feedback/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Modal } from "antd"; 3 | import MyForm from "@/components/form"; 4 | const initFormItems = [ 5 | { 6 | itemType: "inputText", 7 | itemProps: { 8 | name: "f_back", 9 | }, 10 | childProps: { 11 | placeholder: "回复内容", 12 | rows: 4, 13 | }, 14 | }, 15 | { 16 | itemType: "input", 17 | itemProps: { 18 | name: "fd_id", 19 | hidden: true, 20 | }, 21 | }, 22 | ]; 23 | 24 | export default function FeedbackModal({ id, isShow, onSubmit, onCancel }) { 25 | const [form, setForm] = useState(null); 26 | useEffect(() => { 27 | if (id && form) { 28 | form.setFieldsValue({ fd_id: id }); 29 | } 30 | }, [id, form]); 31 | 32 | const submit = () => { 33 | const data = form.getFieldsValue(); 34 | onSubmit(data); 35 | }; 36 | 37 | const close = () => { 38 | form.resetFields(); 39 | onCancel(null, false); 40 | }; 41 | return ( 42 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/modal/menu/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import MyIcon from "@/components/icon"; 3 | import MyForm from "@/components/form"; 4 | import { Modal, Select, message } from "antd"; 5 | import { addMenu, getMenuInfo, editMenu } from "@/api"; 6 | import "./index.less"; 7 | 8 | const ICON_JSON = require("@/assets/json/iconfont.json"); 9 | const ICON_PREFIX = ICON_JSON.css_prefix_text; 10 | const ICON_DATA = ICON_JSON.glyphs; 11 | 12 | const { Option } = Select; 13 | const initFormItems = [ 14 | { 15 | itemType: "input", 16 | itemProps: { 17 | rules: [{ required: true, message: "请填写菜单标题" }], 18 | label: "菜单标题", 19 | name: "title", 20 | }, 21 | childProps: { 22 | placeholder: "菜单标题", 23 | }, 24 | }, 25 | { 26 | itemType: "input", 27 | itemProps: { 28 | rules: [{ required: true, message: "请填写菜单路径" }], 29 | label: "菜单路径", 30 | name: "path", 31 | }, 32 | childProps: { 33 | placeholder: "菜单路径", 34 | }, 35 | }, 36 | { 37 | itemType: "select", 38 | itemProps: { 39 | label: "父级菜单", 40 | name: MENU_PARENTKEY, 41 | }, 42 | childProps: { 43 | placeholder: "父级菜单", 44 | }, 45 | }, 46 | { 47 | itemType: "select", 48 | itemProps: { 49 | label: "菜单图标", 50 | name: "icon", 51 | }, 52 | childProps: { 53 | placeholder: "图标", 54 | allowClear: true, 55 | showSearch: true, 56 | getPopupContainer: (v) => v, 57 | children: ICON_DATA.map((icon) => ( 58 | 64 | )), 65 | }, 66 | }, 67 | { 68 | itemType: "radio", 69 | itemProps: { 70 | rules: [{ required: true, message: "请选择菜单缓存模式" }], 71 | name: MENU_KEEPALIVE, 72 | label: "页面是否缓存", 73 | }, 74 | childProps: { 75 | options: [ 76 | { label: "是", value: "true" }, 77 | { label: "否", value: "false" }, 78 | ], 79 | }, 80 | }, 81 | { 82 | itemType: "inputNumber", 83 | itemProps: { 84 | className: "ipt-number", 85 | rules: [ 86 | { 87 | type: "number", 88 | min: 0, 89 | max: 10000, 90 | message: "请正确填写菜单排序大小", 91 | }, 92 | { required: true, message: "请填写菜单排序大小" }, 93 | ], 94 | name: MENU_ORDER, 95 | label: "菜单排序", 96 | }, 97 | childProps: { 98 | placeholder: "数值越小越靠前", 99 | }, 100 | }, 101 | ]; 102 | const titleType = { 103 | add: "新增菜单", 104 | addChild: "新增子菜单", 105 | edit: "修改菜单信息", 106 | }; 107 | 108 | function getMenuList(list, id) { 109 | let menu = [] 110 | const findList = (ls) => { 111 | return ls.some(item => { 112 | let l = item[MENU_CHILDREN] 113 | if (item[MENU_KEY] === id) { 114 | menu = ls 115 | return true 116 | } else if (Array.isArray(l) && l.length) { 117 | let d = findList(l) 118 | if (d) { 119 | menu = l 120 | } 121 | return d 122 | } 123 | return false 124 | }) 125 | } 126 | findList(list) 127 | return menu 128 | } 129 | 130 | export default function MenuModal({ 131 | info, 132 | modalType = "add", 133 | isShow, 134 | setShow, 135 | updateMenu, 136 | menus = [], 137 | }) { 138 | const [form, setForm] = useState(null); 139 | const [formItems, setItems] = useState([]); 140 | // form item 141 | useEffect(() => { 142 | if (modalType !== "add" && menus && info) { 143 | let items = [...initFormItems.map((i) => ({ ...i }))]; 144 | items.forEach((i) => { 145 | if (i.itemProps.name === MENU_PARENTKEY) { 146 | let disabled = modalType === "addChild" || (modalType === "edit" && info.isParent); 147 | i.childProps.disabled = disabled 148 | let childrenList = modalType === "addChild" ? getMenuList(menus, info[MENU_KEY]) : menus 149 | i.childProps.children = childrenList.map((menu) => ( 150 | 156 | )); 157 | } 158 | }); 159 | items.push({ 160 | itemType: "input", 161 | itemProps: { 162 | hidden: true, 163 | label: "菜单id", 164 | name: MENU_KEY, 165 | }, 166 | }) 167 | setItems(items); 168 | } else if (info && modalType === "add" && menus) { 169 | let items = [...initFormItems.map((i) => ({ ...i }))]; 170 | items = items.filter((i) => i.itemProps.name !== MENU_PARENTKEY); 171 | setItems(items); 172 | } 173 | }, [modalType, info, menus]); 174 | 175 | useEffect(() => { 176 | if (modalType === "edit" && isShow && form) { 177 | getMenuInfo({ [MENU_KEY]: info[MENU_KEY] }).then((res) => { 178 | if (res.status === 0 && res.data) { 179 | form.setFieldsValue(res.data); 180 | } 181 | }); 182 | } else if (modalType === "addChild" && isShow && form) { 183 | form.setFieldsValue({ 184 | [MENU_PARENTKEY]: info[MENU_KEY], 185 | }); 186 | } 187 | }, [modalType, isShow, info, form]); 188 | // 提交表单 189 | const submit = () => { 190 | form.validateFields().then((values) => { 191 | const activeFn = { add, edit, addChild: add }; 192 | let fn = activeFn[modalType]; 193 | fn(values); 194 | }); 195 | }; 196 | 197 | const onCancel = () => { 198 | form.resetFields(); 199 | setShow(false); 200 | }; 201 | function edit(data) { 202 | editMenu(data).then((res) => { 203 | const { status, msg } = res; 204 | if (status === 0) { 205 | message.success(msg); 206 | onCancel(); 207 | updateMenu(); 208 | } 209 | }); 210 | } 211 | function add(data) { 212 | addMenu(data).then((res) => { 213 | const { status, msg } = res; 214 | if (status === 0) { 215 | message.success(msg); 216 | onCancel(); 217 | updateMenu(); 218 | } 219 | }); 220 | } 221 | return ( 222 | 231 | 232 | 233 | ); 234 | } 235 | -------------------------------------------------------------------------------- /src/components/modal/menu/index.less: -------------------------------------------------------------------------------- 1 | .icons { 2 | display : flex; 3 | align-items: center; 4 | 5 | .title { 6 | padding : 0 10px; 7 | font-size: 14px; 8 | } 9 | 10 | svg { 11 | width : 20px; 12 | height: 20px; 13 | } 14 | } 15 | .ipt-number{ 16 | .ant-input-number{ 17 | width: 160px; 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/modal/type/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { Modal, message, Tree } from "antd"; 3 | import MyForm from "@/components/form"; 4 | import { addType, editType } from "@/api"; 5 | import { reduceMenuList } from "@/utils" 6 | const initFormItems = [ 7 | { 8 | itemType: "input", 9 | itemProps: { 10 | rules: [{ required: true, message: "请填写权限名称" }], 11 | label: "权限名称", 12 | name: "name", 13 | }, 14 | childProps: { 15 | placeholder: "权限名称", 16 | }, 17 | }, 18 | { 19 | itemType: "input", 20 | itemProps: { 21 | name: "type_id", 22 | hidden: true, 23 | }, 24 | }, 25 | ]; 26 | 27 | function pushParentId(checkList, list, id) { 28 | const info = list.find(item => item.key === id) 29 | if (!info) { 30 | return 31 | } 32 | 33 | const parentId = info.parentId 34 | if (parentId && !checkList.includes(parentId)) { 35 | checkList.push(parentId) 36 | pushParentId(checkList, list, parentId) 37 | } 38 | } 39 | function filterParentId(parent, list, id) { 40 | const find = list.find(item => item.key === id) 41 | if (!find) { 42 | return 43 | } 44 | const pid = find.parentId 45 | if (pid) { 46 | if (!parent.includes(pid)) { 47 | parent.push(pid) 48 | } 49 | filterParentId(parent, list, pid) 50 | } 51 | } 52 | 53 | export default function UserModal({ info, isShow, onCancel, onOk, menuList }) { 54 | const [form, setForm] = useState(null); 55 | const [menuId, setMenuId] = useState([]); 56 | 57 | const reducerList = useMemo(() => { 58 | if (menuList) { 59 | return reduceMenuList(menuList) 60 | } 61 | return [] 62 | }, [menuList]) 63 | 64 | useEffect(() => { 65 | if (info && form && reducerList) { 66 | const parentId = [], childId = [] 67 | const checkId = info.menu_id.split(",").map(Number) 68 | checkId.forEach(id => { 69 | filterParentId(parentId, reducerList, id) 70 | if (!parentId.includes(id) && !childId.includes(id)) { 71 | childId.push(id) 72 | } 73 | }) 74 | setMenuId(childId); 75 | form.setFieldsValue(info); 76 | } else { 77 | setMenuId([]) 78 | } 79 | }, [info, form, reducerList]); 80 | 81 | const submit = () => { 82 | form.validateFields().then((values) => { 83 | let fn = Boolean(info) ? editType : addType; 84 | let checkMenuId = [] 85 | 86 | menuId.forEach(id => { 87 | if (!checkMenuId.includes(id)) { 88 | checkMenuId.push(id) 89 | } 90 | pushParentId(checkMenuId, reducerList, id) 91 | }) 92 | console.log(checkMenuId); 93 | fn({ ...values, menu_id: checkMenuId }).then((res) => { 94 | if (res.status === 0) { 95 | message.success(res.msg); 96 | close(); 97 | onOk(); 98 | } 99 | }); 100 | }); 101 | }; 102 | const onCheck = (checked) => { 103 | setMenuId(checked); 104 | }; 105 | const close = () => { 106 | form.resetFields(); 107 | setMenuId([]); 108 | onCancel(null, false); 109 | }; 110 | return ( 111 | 120 | 121 | 134 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/modal/user/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Modal, Select, message } from "antd"; 3 | import { getPower, addUser, getUser, editUser } from "@/api"; 4 | import MyForm from "@/components/form"; 5 | const { Option } = Select; 6 | 7 | const paswdRule = [{ required: true, message: "请填写登录密码" }]; 8 | const initFormItems = [ 9 | { 10 | itemType: "input", 11 | itemProps: { 12 | name: "username", 13 | rules: [{ required: true, message: "请填写用户名" }], 14 | label: "用户名", 15 | }, 16 | childProps: { 17 | placeholder: "用户名", 18 | }, 19 | }, 20 | { 21 | itemType: "input", 22 | itemProps: { 23 | name: "account", 24 | rules: [{ required: true, message: "请填写登录账号" }], 25 | label: "登录账号", 26 | }, 27 | childProps: { 28 | placeholder: "登录账号", 29 | }, 30 | }, 31 | { 32 | itemType: "input", 33 | itemProps: { 34 | name: "pswd", 35 | label: "登录密码", 36 | }, 37 | childProps: { 38 | placeholder: "登录密码,若填写则表示修改", 39 | type: "password", 40 | }, 41 | }, 42 | { 43 | itemType: "select", 44 | itemProps: { 45 | rules: [{ required: true, message: "请选择菜单权限" }], 46 | name: "type_id", 47 | label: "菜单权限", 48 | }, 49 | childProps: { 50 | placeholder: "菜单权限", 51 | }, 52 | }, 53 | ]; 54 | export default function UserModal({ user_id, isShow, onCancel, onOk }) { 55 | const [form, setForm] = useState(null); 56 | const [formItems, setItems] = useState([]); 57 | useEffect(() => { 58 | if (isShow) { 59 | getPower().then((res) => { 60 | const { data, status } = res; 61 | if (status === 0) { 62 | let items = initFormItems.map((i) => ({ ...i })); 63 | items.forEach((i) => { 64 | if (i.itemProps.name === "type_id") { 65 | i.childProps.children = data.map((power) => ( 66 | 69 | )); 70 | } 71 | }); 72 | setItems(items); 73 | } 74 | }); 75 | } 76 | }, [isShow]); 77 | 78 | useEffect(() => { 79 | if (user_id && form) { 80 | getUser({ user_id }).then((res) => { 81 | if (res.data) { 82 | form.setFieldsValue(res.data); 83 | } 84 | }); 85 | let items = initFormItems.map((i) => ({ ...i })); 86 | items.forEach((i) => { 87 | if (i.itemProps.name === "pswd") { 88 | i.itemProps.rules = null; 89 | } 90 | }); 91 | setItems(items); 92 | } else if (!user_id) { 93 | // set formItem 94 | let items = initFormItems.map((i) => ({ ...i })); 95 | items.forEach((i) => { 96 | if (i.itemProps.name === "pswd") { 97 | i.itemProps.rules = paswdRule; 98 | } 99 | }); 100 | setItems(items); 101 | } 102 | }, [user_id, form]); 103 | 104 | const submit = () => { 105 | form.validateFields().then((values) => { 106 | let modify = Boolean(user_id); 107 | let fn = modify ? editUser : addUser; 108 | if (modify) { 109 | values.user_id = user_id; 110 | } 111 | fn(values).then((res) => { 112 | if (res.status === 0) { 113 | message.success(res.msg); 114 | close(); 115 | onOk(); 116 | } 117 | }); 118 | }); 119 | }; 120 | const close = () => { 121 | form.resetFields(); 122 | onCancel(null, false); 123 | }; 124 | return ( 125 | 134 | 135 | 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /src/components/pagination/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Row, Pagination } from "antd"; 3 | import "./index.less"; 4 | const pageSizeOptions = [10, 20, 50, 100]; 5 | 6 | export default function MyPagination({ total, page = 1, change, immediately }) { 7 | const [pagesize, setSize] = useState(pageSizeOptions[0]); 8 | useEffect(() => { 9 | if (typeof immediately === "function") { 10 | immediately({ page, pagesize }); 11 | } 12 | // eslint-disable-next-line 13 | }, []); 14 | const pageChange = (page, pagesize) => { 15 | setSize(pagesize); 16 | if (typeof change === "function") { 17 | change({ page, pagesize }); 18 | } 19 | }; 20 | return ( 21 | 22 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/pagination/index.less: -------------------------------------------------------------------------------- 1 | .pagination-wapper { 2 | padding: 20px 0; 3 | } -------------------------------------------------------------------------------- /src/components/table/index.less: -------------------------------------------------------------------------------- 1 | .set { 2 | padding: 10px; 3 | 4 | 5 | 6 | .anticon svg { 7 | width : 30px; 8 | height : 30px; 9 | color : @text-color; 10 | background-color: @primary-color; 11 | 12 | transition: all .3s; 13 | cursor : pointer; 14 | 15 | &:hover { 16 | background-color: @primary-4; 17 | color : @text-color-secondary; 18 | } 19 | } 20 | 21 | } 22 | 23 | .table-show-container { 24 | .hidden { 25 | display: none !important; 26 | } 27 | } 28 | 29 | .table-drawer { 30 | 31 | 32 | .drag-sort svg { 33 | width : 25px; 34 | height: 25px; 35 | } 36 | 37 | .mt20 { 38 | margin-top: 20px; 39 | } 40 | 41 | .mt10 { 42 | margin-top: 5px; 43 | 44 | svg { 45 | width : 15px; 46 | height: 15px; 47 | } 48 | } 49 | .del{ 50 | margin-left: 20px; 51 | } 52 | } 53 | 54 | .row-dragging { 55 | background: #fafafa; 56 | border : 1px solid #ccc; 57 | z-index : 10001; 58 | 59 | .drag-visible { 60 | visibility: visible; 61 | } 62 | 63 | td { 64 | padding : 16px; 65 | visibility: hidden; 66 | 67 | .drag-sort svg { 68 | width : 25px; 69 | height: 25px; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/components/theme/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import { Drawer, Col, Row, message, Button, Radio } from "antd"; 3 | import MyIcon from "@/components/icon"; 4 | import Color from "@/components/color"; 5 | import { getKey, setKey, rmKey } from "@/utils"; 6 | import "./index.less"; 7 | 8 | const darkTheme = process.env.showColorSet 9 | ? require("@/assets/theme/dark.json") 10 | : {}; 11 | const defaultTheme = process.env.showColorSet 12 | ? require("@/assets/theme/default.json") 13 | : {}; 14 | 15 | const Themes = [ 16 | { label: "默认", value: "default", colorList: defaultTheme }, 17 | { label: "暗黑", value: "dark", colorList: darkTheme }, 18 | ]; 19 | 20 | function findInfoColor(list, obj) { 21 | return list.map((item) => { 22 | let key = item.key; 23 | let value = obj[key]; 24 | if (value) { 25 | item.value = value; 26 | } 27 | return item; 28 | }); 29 | } 30 | 31 | function setObjVal(list, obj) { 32 | list.forEach((i) => { 33 | if (obj[i.key]) { 34 | obj[i.key] = i.value; 35 | } 36 | }); 37 | } 38 | 39 | const getColor = (color) => ({ 40 | background: color, 41 | }); 42 | const THEMENAMEKEY = "theme-name"; 43 | const THEMDATAKEY = "theme-data"; 44 | const THEME_NAME = getKey(true, THEMENAMEKEY); 45 | const THEME = getKey(true, THEMDATAKEY); 46 | function SetTheme() { 47 | const [visible, setVisible] = useState(false); 48 | const [selectInfo, setInfo] = useState({}); 49 | const [colorShow, setColorShow] = useState(false); 50 | const [colorList, setColor] = useState(process.env.varColors); 51 | const [themeStyle, setStyle] = useState(THEME_NAME || Themes[0].value); 52 | // 关闭色板 53 | const onCloseColor = useCallback(() => { 54 | setInfo({}); 55 | setColorShow(false); 56 | }, []); 57 | 58 | // 设置主题 59 | const setTheme = useCallback( 60 | (obj, list, tip = true) => { 61 | window.less 62 | .modifyVars(obj) 63 | .then((res) => { 64 | tip && message.success("修改主题色成功"); 65 | setColor(list); 66 | onCloseColor(); 67 | }) 68 | .catch((err) => { 69 | tip && message.error("修改失败"); 70 | }); 71 | }, 72 | [onCloseColor] 73 | ); 74 | // 初始化主题 75 | useEffect(() => { 76 | if (THEME && THEME_NAME) { 77 | let newColorList = [...colorList.map((i) => ({ ...i }))]; 78 | newColorList = findInfoColor(newColorList, THEME); 79 | let newColorObj = { 80 | ...Themes.find((i) => i.value === THEME_NAME).colorList, 81 | }; 82 | setTheme(newColorObj, newColorList, false); 83 | setStyle(THEME_NAME); 84 | } 85 | // eslint-disable-next-line 86 | }, []); 87 | 88 | // 关闭抽屉 89 | const onClose = useCallback(() => { 90 | setVisible(false); 91 | }, []); 92 | 93 | // 显示抽屉 94 | const showDrawer = useCallback(() => { 95 | setVisible(true); 96 | }, []); 97 | 98 | // 自定义颜色选中 99 | const onChangeComplete = useCallback( 100 | (v, k) => { 101 | let newColorList = [...colorList.map((i) => ({ ...i }))]; 102 | newColorList.forEach((i) => { 103 | if (i.key === k) { 104 | i.value = v.hex; 105 | } 106 | }); 107 | let colorObj = { 108 | ...Themes.find((i) => i.value === themeStyle).colorList, 109 | }; 110 | setObjVal(newColorList, colorObj); 111 | setTheme(colorObj, newColorList); 112 | }, 113 | [colorList, setTheme, themeStyle] 114 | ); 115 | 116 | // 选中 117 | const onSelect = useCallback((e, info) => { 118 | const height = window.innerHeight; 119 | const width = window.innerWidth; 120 | let { clientX: pageX, clientY: pageY } = e; 121 | if (pageY + 350 > height) { 122 | pageY -= 320; 123 | } 124 | if (pageX + 250 > width) { 125 | pageX -= 220; 126 | } 127 | setInfo({ ...info, pageX, pageY }); 128 | setColorShow(true); 129 | }, []); 130 | 131 | // 保存本地 132 | const saveLocalTheme = useCallback(() => { 133 | let themeObj = { ...Themes.find((i) => i.value === themeStyle).colorList }; 134 | themeObj = colorList.reduce((a, c) => { 135 | a[c.key] = c.value; 136 | return a; 137 | }, themeObj); 138 | setKey(true, THEMENAMEKEY, themeStyle); 139 | setKey(true, THEMDATAKEY, themeObj); 140 | message.success("主题成功保存到本地!"); 141 | }, [themeStyle, colorList]); 142 | 143 | // 选择主题 144 | const themeChange = useCallback( 145 | (e) => { 146 | const { value } = e.target; 147 | const colorObj = { 148 | ...Themes.find((i) => i.value === value).colorList, 149 | }; 150 | setObjVal(colorList, colorObj); 151 | setTheme(colorObj, colorList); 152 | setStyle(value); 153 | }, 154 | [colorList, setTheme] 155 | ); 156 | const delTheme = () => { 157 | let initColorObj = { ...Themes[0].colorList }; 158 | process.env.varColors.forEach((item) => { 159 | initColorObj[item.key] = item.value; 160 | }); 161 | window.less 162 | .modifyVars(initColorObj) 163 | .then((res) => { 164 | message.success("修改主题色成功"); 165 | rmKey(true, THEMDATAKEY); 166 | rmKey(true, THEMENAMEKEY); 167 | setColor(process.env.varColors); 168 | setStyle(Themes[0].value); 169 | }) 170 | .catch((err) => { 171 | message.error("修改失败"); 172 | }); 173 | }; 174 | return ( 175 |
176 |
177 | 178 |
179 | 189 | 196 | 自定义Less变量: 197 | {colorList.map((i) => ( 198 | 199 | {i.title}: 200 | { 203 | e.stopPropagation(); 204 | onSelect(e, i); 205 | }} 206 | style={getColor(i.value)} 207 | > 208 | 209 | ))} 210 | 211 | 212 | 215 | 218 | 219 | 228 | 229 |
230 | ); 231 | } 232 | export default SetTheme; 233 | -------------------------------------------------------------------------------- /src/components/theme/index.less: -------------------------------------------------------------------------------- 1 | .set-theme { 2 | 3 | .icon { 4 | position : fixed; 5 | display : flex; 6 | flex-direction : column; 7 | justify-content : center; 8 | right : 20px; 9 | bottom : 50px; 10 | width : 45px; 11 | height : 45px; 12 | background-color: #fff; 13 | box-shadow : 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05); 14 | border-radius : 50%; 15 | cursor : pointer; 16 | 17 | svg { 18 | width : 45px; 19 | height: 45px; 20 | } 21 | } 22 | 23 | 24 | 25 | } 26 | 27 | .drawer { 28 | .ant-drawer-content { 29 | background-color: @body-background; 30 | } 31 | 32 | .del { 33 | margin-left: 15px; 34 | } 35 | } 36 | 37 | .color-row { 38 | padding : 10px; 39 | align-items: center; 40 | color : @heading-color; 41 | 42 | &.primary { 43 | color : @primary-color; 44 | padding: 20px 10px 0; 45 | } 46 | 47 | .color-btn { 48 | margin-left : 5px; 49 | width : 100px; 50 | height : 30px; 51 | border : 5px solid @border-color-base ; 52 | border-radius: 5px; 53 | } 54 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import 'core-js/es' 5 | import 'react-app-polyfill/ie9' 6 | import 'react-app-polyfill/stable' 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /src/layout/footer.js: -------------------------------------------------------------------------------- 1 | import { Layout } from "antd"; 2 | const { Footer } = Layout; 3 | 4 | export default function BottomFooter() { 5 | return ( 6 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/layout/header.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { Layout, Menu, Dropdown } from "antd"; 3 | import logo from "@/assets/images/logo.svg"; 4 | import MyIcon from "@/components/icon/"; 5 | import { clearLocalDatas } from "@/utils"; 6 | import { USER_INFO, TOKEN, MENU, } from "@/common" 7 | import { useDispatchUser, useStateUserInfo } from "@/store/hooks"; 8 | const { Header } = Layout; 9 | 10 | const RightMenu = ({ logout }) => ( 11 | 12 | } 16 | > 17 | 退出登录 18 | 19 | 20 | ); 21 | 22 | const getPopupContainer = (HTMLElement) => HTMLElement; 23 | 24 | const LayoutHeader = ({ children }) => { 25 | const userInfo = useStateUserInfo() 26 | const { stateClearUser: clearStateUser } = useDispatchUser() 27 | const logout = useCallback(() => { 28 | clearLocalDatas([USER_INFO, TOKEN, MENU]); 29 | window.location.reload(); 30 | clearStateUser(); 31 | }, [clearStateUser]); 32 | return ( 33 |
34 |
35 | logo 36 | react-ant-admin 37 |
38 | {children} 39 |
40 | } 44 | > 45 |
管理员:{userInfo.username}
46 |
47 |
48 |
49 | ); 50 | }; 51 | export default LayoutHeader 52 | -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | import { FullScreen, SingleColumn, TowColumn, TwoFlanks } from "./mode"; 2 | import * as ActionTypes from "../store/layout/actionTypes"; 3 | import "./index.less"; 4 | import { useStateLayout, useStateVisibel } from "@/store/hooks"; 5 | 6 | export default function LayoutContainer() { 7 | const LayoutMode = useStateLayout() 8 | const visibel = useStateVisibel() 9 | switch (LayoutMode) { 10 | case ActionTypes.SINGLE_COLUMN: 11 | return ; 12 | case ActionTypes.TWO_COLUMN: 13 | return ; 14 | case ActionTypes.TWO_FLANKS: 15 | return ; 16 | case ActionTypes.FULL_SCREEN: 17 | return 18 | default: 19 | return ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/layout/index.less: -------------------------------------------------------------------------------- 1 | .ant-menu-item { 2 | align-items: center; 3 | display : flex; 4 | 5 | .anticon { 6 | font-size: 24px; 7 | } 8 | } 9 | 10 | .fl { 11 | float: left; 12 | } 13 | 14 | .my-layout-body { 15 | min-height: 100vh; 16 | 17 | &.twoflanks { 18 | .layout-silder-menu { 19 | height: 100vh; 20 | } 21 | .top-menu-wrapper{ 22 | margin:0 20px; 23 | } 24 | } 25 | 26 | .ant-menu { 27 | &.layout-silder-menu { 28 | border-color: @border-color-split ; 29 | 30 | &.col { 31 | height : 64px; 32 | padding-bottom: 0; 33 | overflow : unset; 34 | } 35 | } 36 | 37 | .ant-menu-submenu .ant-menu-submenu-title .anticon { 38 | font-size : 24px; 39 | vertical-align: middle; 40 | 41 | } 42 | 43 | .ant-menu-item { 44 | align-items: center; 45 | display : flex; 46 | 47 | .anticon { 48 | font-size: 24px; 49 | } 50 | } 51 | } 52 | 53 | .ant-layout-sider-children { 54 | position: relative; 55 | height : auto; 56 | } 57 | 58 | .layout-content-wrap { 59 | padding : 0 24px 24px; 60 | margin : 0; 61 | min-height: 200px; 62 | 63 | &.reset-padding { 64 | padding: 0; 65 | 66 | .layout-content-body { 67 | margin: 10px 20px 20px; 68 | } 69 | 70 | } 71 | } 72 | 73 | .layout-content-body { 74 | margin-top: 10px; 75 | 76 | &>div { 77 | padding : 20px; 78 | min-height : 100%; 79 | background-color: @body-background; 80 | border : 1px solid @border-color-split; 81 | } 82 | } 83 | 84 | .layout-silder-menu { 85 | height : calc(100vh - 64px); 86 | padding-bottom: 32px; 87 | overflow-y : auto; 88 | } 89 | 90 | .ant-affix .layout-silder-menu { 91 | height: 100vh; 92 | } 93 | 94 | .fold-control { 95 | &.fixed { 96 | position : absolute; 97 | bottom : 0; 98 | right : 0; 99 | width : 100%; 100 | background : inherit; 101 | border-right : 1px solid @border-color-split; 102 | background-color: @body-background; 103 | } 104 | 105 | .ant-btn { 106 | width : 100%; 107 | background: unset; 108 | color : @text-color ; 109 | border : 0; 110 | outline : none; 111 | transition: none; 112 | 113 | } 114 | } 115 | 116 | .top-menu { 117 | margin : 10px 0 0; 118 | overflow-x: auto; 119 | background: @body-background; 120 | } 121 | 122 | .top-breadcrumb { 123 | background-color: @body-background; 124 | margin-top : 10px; 125 | padding : 10px; 126 | font-size : 16px; 127 | display : flex; 128 | align-items : center; 129 | 130 | svg { 131 | width : 20px; 132 | height: 20px; 133 | } 134 | 135 | &>span { 136 | 137 | .anticon, 138 | .title { 139 | vertical-align: middle; 140 | margin : 0; 141 | } 142 | 143 | & .ant-breadcrumb-separator { 144 | vertical-align: bottom; 145 | } 146 | 147 | &>span { 148 | display: inline-block; 149 | } 150 | } 151 | } 152 | 153 | .header { 154 | color : @text-color ; 155 | background-color: @body-background; 156 | border-bottom : 1px solid @border-color-base; 157 | 158 | .logo { 159 | float: left; 160 | 161 | 162 | span { 163 | vertical-align: middle; 164 | padding : 0 10px; 165 | font-size : 16px; 166 | } 167 | 168 | img { 169 | width : 40px; 170 | height: 40px; 171 | } 172 | } 173 | 174 | .right { 175 | float: right; 176 | } 177 | 178 | .right-down { 179 | background-color: @border-color-split; 180 | } 181 | } 182 | 183 | .footer { 184 | text-align: center; 185 | 186 | p { 187 | margin-bottom: 5px; 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /src/layout/mode/fullScreen.js: -------------------------------------------------------------------------------- 1 | import Router from "@/router"; 2 | 3 | export default Router 4 | -------------------------------------------------------------------------------- /src/layout/mode/index.js: -------------------------------------------------------------------------------- 1 | export { default as SingleColumn } from "./singleColumn.js"; 2 | export { default as TowColumn } from "./twoColumn.js"; 3 | export { default as TwoFlanks } from "./twoFlanks"; 4 | export { default as FullScreen } from "./fullScreen"; 5 | -------------------------------------------------------------------------------- /src/layout/mode/singleColumn.js: -------------------------------------------------------------------------------- 1 | import { Layout } from "antd"; 2 | import Header from "../header"; 3 | import Menu from "../siderMenu"; 4 | import TopMenu from "../topMenu"; 5 | import Footer from "../footer"; 6 | import Router from "@/router"; 7 | 8 | const { Content } = Layout; 9 | 10 | const SingleColumn = ({ visibel }) => { 11 | return ( 12 | 13 |
} /> 14 | 15 | 16 | {visibel.topMenu && } 17 | 18 | 19 | 20 | {visibel.footer &&