├── .gitignore ├── LICENSE ├── README.md ├── config-overrides.js ├── package.json ├── paths.json ├── public ├── favicon.ico ├── index.html ├── logo192.png └── logo512.png ├── src ├── App.tsx ├── assets │ ├── avatar.jpg │ ├── bg.jpg │ ├── icon_selection.png │ ├── icon_sign_in_selection.png │ ├── logo.png │ └── logo_2.png ├── components │ ├── 404.tsx │ ├── Footer.tsx │ └── Header.tsx ├── declaration.d.ts ├── index.tsx ├── react-app-env.d.ts ├── router │ ├── config.js │ ├── index.js │ └── permissionAuth.js ├── store │ ├── actionTypes.js │ ├── actions │ │ ├── auth.js │ │ ├── index.js │ │ └── user.js │ ├── index.js │ └── reducers │ │ ├── index.js │ │ └── user.js ├── styles │ ├── base.less │ ├── footer.less │ ├── header.less │ ├── home.less │ └── login.less ├── utils │ ├── api.js │ ├── network.js │ ├── url.js │ └── valid.js └── views │ ├── Home.tsx │ └── Login.tsx ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Editor directories and files 26 | .idea 27 | .vscode 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | *.zip 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jack Chen 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 | # 前言 2 | NodeJS全栈开发之前端基于 React全家桶+TypeScript+Antd 构建用户界面,功能包括:登录,注册,找回密码,自动登录,登出,错误页面,todoList增删改查CRUD。本项目场景虽然简单,但涵盖功能比较齐全,适合初中级学习前端开发的小伙伴。如果觉得不错的话,请老铁们给个:heart:star,也期待与大家一起交流学习。 3 | 4 | [在线DEMO演示](http://106.55.168.13:9000/) 5 | 6 | [手摸手教你大厂都在用 React+TS+Antd 快速入门到NodeJS全栈项目实战](https://juejin.cn/post/6859354172127936526) 7 | 8 | 9 | # 部分截图 10 | 11 | ## 登录/注册 12 | 13 | 登录/注册 14 | 15 | ## 首页 16 | 17 | 首页 18 | 19 | ## 新增任务 20 | 21 | 新增任务 22 | 23 | ## 修改密码 24 | 25 | 修改密码 26 | 27 | ## 错误页面 28 | 29 | 修改密码 30 | 31 | 32 | # 目录结构 33 | 34 | ``` 35 | │ .gitignore // git忽略配置 36 | │ config-overrides.js // webpack自定义配置 37 | │ package.json // npm包管理所需模块及配置信息 38 | │ paths.json // src路径配置 39 | │ tsconfig.json // typescript配置 40 | │ yarn.lock // 自动锁定安装包版本 41 | ├─public 42 | │ favicon.ico // 图标 43 | │ index.html // 入口html文件 44 | └─src 45 | │ App.tsx // 路由组件 46 | │ declaration.d.ts // 依赖声明文件 47 | │ index.tsx // 入口主文件 48 | │ react-app-env.d.ts // 声明文件 49 | ├─assets // 存放公共图片 50 | ├─components // 公共组件 51 | │ 404.tsx // 错误页面 52 | │ Footer.tsx // 底部模板组件 53 | │ Header.tsx // 头部模板组件 54 | ├─router 55 | │ config.js // 项目路由配置 56 | │ index.js // 单页面路由注册组件 57 | │ permissionAuth.js // 登录权限控制组件 58 | ├─store // 全局store状态管理 59 | │ │ actionTypes.js // 拆分actionType的类型定义常量 60 | │ │ index.js // 创建store管理仓库 61 | │ ├─actions // 拆分action,将它封装到一个函数里面去管理 62 | │ │ auth.js // 权限action单独管理 63 | │ │ index.js // 合并多个不同action,统一对外输出 64 | │ │ user.js // 用户action单独管理 65 | │ └─reducers // 创建reducer,更新state数据操作 66 | │ index.js // 合并多个不同reducer,统一对外输出 67 | │ user.js // 创建用户reducer,更新state数据操作 68 | ├─styles 69 | │ base.less // 基础样式 70 | │ footer.less // 底部样式 71 | │ header.less // 头部样式 72 | │ home.less // 首页样式 73 | │ login.less // 登录样式 74 | ├─utils 75 | │ api.js // 统一封装API接口调用方法 76 | │ network.js // axios封装与拦截器配置 77 | │ url.js // 自动部署服务器环境 78 | │ valid.js // 统一封装公用模块方法 79 | └─views 80 | Home.tsx // 首页 81 | Login.tsx // 登录页面 82 | ``` 83 | 84 | 85 | # 技术栈 86 | * create-react-app 87 | * React v16.13 88 | * react-router-dom v5.2 89 | * redux v4.0 90 | * react-redux v7.2 91 | * react-thunk v2.3 92 | * typescript v3.7 93 | * antd v4.5 94 | * axios v0.19 95 | * react-persist v6.0 96 | 97 | # 功能模块 98 | * 登录(登出) 99 | * 注册 100 | * 记住密码 101 | * 忘记密码(修改密码) 102 | * todoList增删改查 103 | * 点亮红星标记 104 | * 查询条件筛选 105 | * 错误页面 106 | 107 | # 下载安装依赖 108 | ``` 109 | git clone https://github.com/jackchen0120/react-ts-antd 110 | cd react-ts-antd 111 | yarn / yarn install 112 | ``` 113 | 114 | ## 开发模式 115 | ``` 116 | yarn start 117 | ``` 118 | 运行之后,访问地址:http://localhost:9000 119 | 120 | ## 生产环境打包 121 | ``` 122 | yarn build 123 | ``` 124 | 125 | ## 获取更多项目实战经验及各种源码资源 126 | 127 | 欢迎关注作者公众号:**懒人码农** 128 | 129 | 公众号二维码 130 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const { 2 | override, 3 | fixBabelImports, 4 | addLessLoader, 5 | addWebpackPlugin, 6 | addWebpackAlias, 7 | addDecoratorsLegacy, 8 | overrideDevServer 9 | } = require('customize-cra'); 10 | 11 | const path = require('path'); 12 | const webpack = require('webpack'); 13 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 14 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 15 | 16 | const isProduction = process.env.NODE_ENV === 'production'; 17 | 18 | function resolve (pathUrl) { 19 | return path.join(__dirname, pathUrl); 20 | } 21 | 22 | const addCustomize = () => (config) => { 23 | // 配置打包后的文件位置 24 | // config.output.path = resolve('dist'); 25 | if (config.output.publicPath) { 26 | config.output.publicPath = '/'; 27 | // isProduction ? '/react-ts-antd-admin/' : '/'; 28 | } 29 | 30 | if (config.resolve) { 31 | config.resolve.extensions = ['.js', '.tsx', '.less', '.css']; 32 | } 33 | 34 | // 添加js、css打包gzip配置 35 | config.plugins.push( 36 | new CompressionWebpackPlugin({ 37 | algorithm: 'gzip', // 算法 38 | test: /\.js$|\.css$/, // 压缩 js 与 css 39 | threshold: 1024, // 只处理比这个值大的资源,按字节计算 40 | // minRatio: 0.8 // 只有压缩率比这个值小的资源才会被处理 41 | }), 42 | ) 43 | 44 | return config; 45 | }; 46 | 47 | // 设置代理 48 | const devServerConfig = () => config => { 49 | return { 50 | ...config, 51 | proxy: { 52 | '/api': { 53 | target: 'http://106.55.168.13:9000', 54 | changeOrigin: true, 55 | secure: false 56 | } 57 | } 58 | }; 59 | }; 60 | 61 | // 关掉 sourceMap 62 | process.env.GENERATE_SOURCEMAP = isProduction ? 'false' : 'true'; 63 | 64 | module.exports = { 65 | webpack: override( 66 | // 判断环境,只有在生产环境的时候才去使用这个插件 67 | isProduction && addWebpackPlugin(new UglifyJsPlugin({ 68 | uglifyOptions: { 69 | compress: { 70 | drop_debugger: true, 71 | drop_console: true 72 | } 73 | } 74 | })), 75 | 76 | // 启用装饰器语法 77 | addDecoratorsLegacy(), 78 | 79 | // 配置antd按需引入 80 | fixBabelImports('import', { 81 | libraryName: 'antd', 82 | libraryDirectory: 'es', 83 | style: true, // 自动打包相关的样式 84 | }), 85 | 86 | // 使用less-loader对源码中的less的变量进行重新指定 87 | addLessLoader({ 88 | javascriptEnabled: true, 89 | modifyVars: { '@primary-color': '#2d8cf0' }, 90 | }), 91 | 92 | // 配置路径别名 93 | addWebpackAlias({ 94 | '@': resolve('src'), 95 | }), 96 | 97 | // 压缩JS、CSS等 98 | addCustomize() 99 | ), 100 | 101 | // 本地启动配置,可以设置代理 102 | devServer: overrideDevServer(devServerConfig()) 103 | }; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ts-antd", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "antd": "^4.5.2", 14 | "axios": "^0.19.2", 15 | "less": "^3.12.2", 16 | "less-loader": "5.0.0", 17 | "react": "^16.13.1", 18 | "react-document-title": "^2.0.3", 19 | "react-dom": "^16.13.1", 20 | "react-redux": "^7.2.1", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "3.4.1", 23 | "redux": "^4.0.5", 24 | "redux-persist": "^6.0.0", 25 | "redux-thunk": "^2.3.0", 26 | "typescript": "~3.7.2" 27 | }, 28 | "scripts": { 29 | "start": "set BROWSER=none&& set PORT=9000&& react-app-rewired start", 30 | "build": "react-app-rewired build", 31 | "test": "react-app-rewired test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "@babel/plugin-proposal-decorators": "^7.10.5", 51 | "babel-plugin-import": "^1.13.0", 52 | "compression-webpack-plugin": "^4.0.0", 53 | "customize-cra": "^1.0.0", 54 | "react-app-rewired": "^2.1.6", 55 | "uglifyjs-webpack-plugin": "^2.2.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "@/*": ["*"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | 后台管理模板 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/public/logo512.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /* 描述: 路由组件 2 | * 作者: Jack Chen 3 | * 日期: 2020-08-01 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { BrowserRouter as Router } from 'react-router-dom'; 8 | import Routes from './router/index'; 9 | 10 | class App extends React.Component { 11 | render () { 12 | return ( 13 | 14 | 15 | 16 | ) 17 | } 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/src/assets/avatar.jpg -------------------------------------------------------------------------------- /src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/src/assets/bg.jpg -------------------------------------------------------------------------------- /src/assets/icon_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/src/assets/icon_selection.png -------------------------------------------------------------------------------- /src/assets/icon_sign_in_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/src/assets/icon_sign_in_selection.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackchen0120/react-ts-antd/c203ed2a31e0b7b54896545bb37557a57b17506e/src/assets/logo_2.png -------------------------------------------------------------------------------- /src/components/404.tsx: -------------------------------------------------------------------------------- 1 | /* 描述: 404模板 2 | * 作者: Jack Chen 3 | * 日期: 2020-08-05 4 | */ 5 | 6 | import * as React from 'react'; 7 | import DocumentTitle from 'react-document-title'; 8 | import { Button } from 'antd'; 9 | import Header from '@/components/Header'; 10 | import Footer from '@/components/Footer'; 11 | 12 | const styles: any = { 13 | width: '100%', 14 | height: '100%', 15 | display: 'flex', 16 | flexDirection: 'column', 17 | justifyContent: 'space-between' 18 | } 19 | 20 | const mainStyle: any = { 21 | width: '100%', 22 | height: 'calc(100% - 194px)', 23 | display: 'flex', 24 | justifyContent: 'center', 25 | alignItems: 'center' 26 | } 27 | 28 | export default class NotFound extends React.Component { 29 | render () { 30 | return ( 31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 |
404
102 |
对不起,您访问的页面不存在。
103 | 108 |
109 |
110 | 111 |
113 |
114 | ) 115 | } 116 | } -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | /* 描述: 底部footer模板 2 | * 作者: Jack Chen 3 | * 日期: 2020-08-02 4 | */ 5 | 6 | import * as React from 'react'; 7 | import '@/styles/footer.less'; 8 | 9 | export default class Footer extends React.Component { 10 | render () { 11 | return ( 12 |
13 |
14 |
15 | Copyright@2020-2025 微信公众号{'<懒人码农>'} 湘ICP备19016532号-1 16 |
17 |
18 |
19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | /* 描述: 头部header模板 2 | * 作者: Jack Chen 3 | * 日期: 2020-08-03 4 | */ 5 | 6 | import React, { useState } from 'react'; 7 | // import { NavLink } from 'react-router-dom'; 8 | import { Menu, Dropdown, Modal, Form, Button, Input, message } from 'antd'; 9 | import store from '@/store'; 10 | import { logout } from '@/store/actions'; 11 | import { DownOutlined } from '@ant-design/icons'; 12 | import logo from '@/assets/logo_2.png'; 13 | import avatar from '@/assets/avatar.jpg'; 14 | import '@/styles/header.less'; 15 | import { resetPwd } from '@/utils/api'; 16 | import { validPass } from '@/utils/valid'; 17 | 18 | interface Values { 19 | oldPassword: string, 20 | newPassword: string, 21 | confirmPassword: string 22 | } 23 | 24 | interface IProps { 25 | loading: boolean, 26 | visible: boolean, 27 | onOk: (values: Values) => void, 28 | onCancel: () => void 29 | } 30 | 31 | const formItemLayout = { 32 | labelCol: { 33 | sm: { span: 6 }, 34 | }, 35 | wrapperCol: { 36 | sm: { span: 12 }, 37 | }, 38 | } 39 | 40 | const ModifyUserForm: React.FC = ({ 41 | loading, 42 | visible, 43 | onOk, 44 | onCancel 45 | }) => { 46 | const [form] = Form.useForm(); 47 | 48 | const handleOk = () => { 49 | form.validateFields() 50 | .then((values: any) => { 51 | onOk(values); 52 | }) 53 | .catch(info => { 54 | console.log('Validate Failed:', info); 55 | }) 56 | } 57 | 58 | const handleCancel = () => { 59 | form.resetFields(); 60 | onCancel(); 61 | } 62 | 63 | return ( 64 | 取消, 74 | , 75 | ]} 76 | > 77 |
82 | 87 | 88 | 89 | 94 | 95 | 96 | 101 | 102 | 103 |
104 |
105 | ) 106 | } 107 | 108 | 109 | const Header = (props: any) => { 110 | const [visible, setVisible] = useState(false); 111 | const [loading, setLoading] = useState(false); 112 | const { curActive } = props; 113 | console.log('props===', props) 114 | 115 | const onOk = (values: Values) => { 116 | console.log('Received values of form: ', values); 117 | 118 | if (!validPass(values.oldPassword)) { 119 | message.error("旧密码应为8到20位字母或数字!"); 120 | return false; 121 | } else if (!validPass(values.newPassword)) { 122 | message.error("新密码应为8到20位字母或数字!"); 123 | return false; 124 | } else if (!validPass(values.confirmPassword)){ 125 | message.error("确认密码有误"); 126 | return false; 127 | } else if (values.confirmPassword !== values.newPassword){ 128 | message.error("两次密码不一致"); 129 | return false; 130 | } 131 | 132 | setLoading(true); 133 | console.log((store.getState() as any).user.data) 134 | let username = (store.getState() as any).user.data.userData.username; 135 | 136 | let data = { 137 | username: username, 138 | oldPassword: values.oldPassword, 139 | newPassword: values.confirmPassword 140 | } 141 | 142 | resetPwd(data) 143 | .then((res: any) => { 144 | console.log('修改密码===', res) 145 | setLoading(false); 146 | if (res.code === 0) { 147 | setVisible(false); 148 | message.success('修改密码成功'); 149 | } else { 150 | message.error(res.msg); 151 | } 152 | }) 153 | .catch(() => { 154 | setLoading(false); 155 | }) 156 | } 157 | 158 | const onCancel = () => { 159 | setVisible(false); 160 | } 161 | 162 | const onClick = (e: any) => { 163 | // console.log(e.key) 164 | if (e.key === '1') { 165 | setVisible(true); 166 | } else { 167 | store.dispatch(logout()); 168 | } 169 | }; 170 | 171 | const menu = ( 172 | 173 | 修改密码 174 | 退出 175 | 176 | ); 177 | 178 | return ( 179 |
180 |
181 |
182 | logo 183 | 197 |
198 | 199 | 200 | e.preventDefault() }> 201 | { (store.getState() as any).user.data.userData.username } 202 | 203 | 204 | 205 | 206 |
207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 222 | 223 |
224 | ) 225 | } 226 | 227 | export default Header 228 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-router-dom' 2 | declare module 'react-redux' 3 | declare module 'react-document-title' -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* 描述: 主文件入口 2 | * 作者: Jack Chen 3 | * 日期: 2020-08-01 4 | */ 5 | 6 | import * as React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import 'antd/dist/antd.less'; 9 | import '@/styles/base.less'; 10 | import App from './App'; 11 | import store, { persistor } from './store'; 12 | import { Provider } from 'react-redux'; 13 | import { PersistGate } from 'redux-persist/lib/integration/react'; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ); 23 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/router/config.js: -------------------------------------------------------------------------------- 1 | import Login from '../views/Login.tsx'; 2 | import Home from '../views/Home.tsx'; 3 | import NotFound from '../components/404.tsx'; 4 | 5 | export const routerConfig = [ 6 | { 7 | path: '/', 8 | component: Home, 9 | auth: true 10 | }, 11 | { 12 | path: '/login', 13 | component: Login 14 | }, 15 | { 16 | path: '/404', 17 | component: NotFound, 18 | auth: true 19 | } 20 | ] -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | 4 | import { routerConfig } from './config'; 5 | import { PermissionAuth } from './permissionAuth'; 6 | 7 | class Routes extends React.Component { 8 | render () { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | } 16 | 17 | export default Routes -------------------------------------------------------------------------------- /src/router/permissionAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import store from '@/store'; 4 | 5 | export class PermissionAuth extends React.Component { 6 | render () { 7 | const { location, config } = this.props; 8 | const { pathname } = location; 9 | const isLogin = store.getState().user.data.token; 10 | 11 | // 如果该路由不用进行权限校验,登录状态下登陆页除外 12 | // 因为登陆后,无法跳转到登陆页 13 | // 这部分代码,是为了在非登陆状态下,访问不需要权限校验的路由 14 | const targetRouterConfig = config.find(v => v.path === pathname); 15 | if (targetRouterConfig && !targetRouterConfig.auth && !isLogin) { 16 | const { component } = targetRouterConfig; 17 | return 18 | } 19 | 20 | if (isLogin) { 21 | // 如果是登陆状态,想要跳转到登陆,重定向到主页 22 | if (pathname === '/login') { 23 | return 24 | } else { 25 | // 如果路由合法,就跳转到相应的路由 26 | if (targetRouterConfig) { 27 | return 28 | } else { 29 | // 如果路由不合法,重定向到 404 页面 30 | return 31 | } 32 | } 33 | } else { 34 | // 非登陆状态下,当路由合法时且需要权限校验时,跳转到登陆页面,要求登陆 35 | if (targetRouterConfig && targetRouterConfig.auth) { 36 | return 37 | } else { 38 | // 非登陆状态下,路由不合法时,重定向至 404 39 | return 40 | } 41 | } 42 | 43 | 44 | } 45 | } -------------------------------------------------------------------------------- /src/store/actionTypes.js: -------------------------------------------------------------------------------- 1 | // 用户信息 2 | export const SET_USER_TOKEN = "SET_USER_TOKEN"; 3 | export const SET_USER_INFO = "SET_USER_INFO"; 4 | export const MODIFY_USER_INFO = "MODIFY_USER_INFO"; 5 | export const CLEAR_USER_INFO = "CLEAR_USER_INFO"; 6 | 7 | // todo增删改 8 | export const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE'; 9 | export const ADD_TODO_ITEM = 'ADD_TODO_ITEM'; 10 | export const DELETE_TODO_ITEM = 'DELETE_TODO_ITEM'; -------------------------------------------------------------------------------- /src/store/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { saveUserInfo, clearUserInfo } from './user'; 2 | import { loginUser, registerUser } from '@/utils/api'; 3 | 4 | export const login = (username, password) => (dispatch) => { 5 | return new Promise((resolve, reject) => { 6 | loginUser({ username: username.trim(), password: password }) 7 | .then(res => { 8 | console.log('登录===', res) 9 | if (res.code === 0) { 10 | dispatch(saveUserInfo(res.data)); 11 | resolve(res); 12 | } else { 13 | reject(res.msg); 14 | } 15 | }) 16 | }) 17 | } 18 | 19 | export const register = (username, password) => (dispatch) => { 20 | return new Promise((resolve, reject) => { 21 | registerUser({ username: username.trim(), password: password }) 22 | .then(res => { 23 | console.log('注册===', res) 24 | if (res.code === 0) { 25 | dispatch(saveUserInfo(res.data)); 26 | resolve(res); 27 | } else { 28 | reject(res.msg); 29 | } 30 | }) 31 | }) 32 | } 33 | 34 | export const logout = () => (dispatch) => { 35 | console.log('logout') 36 | dispatch(clearUserInfo()); 37 | window.location.href = '/login'; 38 | } -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | import { login, register, logout } from './auth'; 2 | import { saveUserInfo, clearUserInfo } from './user'; 3 | 4 | export { 5 | login, 6 | register, 7 | logout, 8 | saveUserInfo, 9 | clearUserInfo 10 | } -------------------------------------------------------------------------------- /src/store/actions/user.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actionTypes'; 2 | 3 | export const saveUserInfo = (data) => { 4 | return { 5 | type: types.SET_USER_INFO, 6 | data 7 | } 8 | } 9 | 10 | export const clearUserInfo = () => { 11 | return { 12 | type: types.CLEAR_USER_INFO 13 | } 14 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | // 创建一个store管理仓库,从redux库中引入一个createStore函数和applyMiddleware方法 2 | import { createStore, applyMiddleware } from 'redux'; 3 | // 引入thunk中间件,实现异步action、打印日志、错误报告等功能,放入applyMiddleware方法之中 4 | import thunk from 'redux-thunk'; 5 | import reducer from './reducers'; 6 | 7 | // 引入redux-persist库,全局数据持久化存储 8 | import { persistStore, persistReducer } from 'redux-persist'; 9 | import storage from 'redux-persist/lib/storage'; 10 | import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; 11 | 12 | const persistConfig = { 13 | key: 'root', 14 | storage: storage, 15 | stateReconciler: autoMergeLevel2 16 | } 17 | 18 | const myPersistReducer = persistReducer(persistConfig, reducer) 19 | 20 | // 引入createStore后,store并没有创建,需要调用createStore()后才有store 21 | const store = createStore( 22 | myPersistReducer, 23 | applyMiddleware(thunk), 24 | // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 25 | ) 26 | 27 | export const persistor = persistStore(store) 28 | 29 | export default store -------------------------------------------------------------------------------- /src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import user from './user'; 3 | 4 | export default combineReducers({ 5 | user 6 | }) -------------------------------------------------------------------------------- /src/store/reducers/user.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actionTypes'; 2 | 3 | const initUserInfo = { 4 | data: {}, 5 | isLogined: false 6 | } 7 | 8 | export default function user(state = initUserInfo, action) { 9 | switch (action.type) { 10 | case types.SET_USER_INFO: 11 | return { 12 | ...state, 13 | data: action.data, 14 | isLogined: true 15 | }; 16 | case types.MODIFY_USER_INFO: 17 | return { 18 | ...state, 19 | data: Object.assign(state.data, action.data) 20 | }; 21 | case types.CLEAR_USER_INFO: 22 | return { 23 | data: {}, 24 | isLogined: false 25 | }; 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | // export default (state = defaultState, action) => { 32 | // if (action.type === types.CHANGE_INPUT_VALUE) { 33 | // const newState = JSON.parse(JSON.stringify(state)); 34 | // newState.inputValue = action.value; 35 | // return newState; 36 | // } 37 | 38 | // if (action.type === types.ADD_TODO_ITEM) { 39 | // const newState = JSON.parse(JSON.stringify(state)); 40 | // newState.list.push(newState.inputValue); 41 | // newState.inputValue = ''; 42 | // return newState; 43 | // } 44 | 45 | // if (action.type === types.DELETE_TODO_ITEM) { 46 | // const newState = JSON.parse(JSON.stringify(state)); 47 | // newState.list.splice(action.index, 1); 48 | // return newState; 49 | // } 50 | 51 | // return state; 52 | // } -------------------------------------------------------------------------------- /src/styles/base.less: -------------------------------------------------------------------------------- 1 | /* 2 | * 描述: 基本样式 3 | * 作者:Jack Chen 4 | * 时间:2020-08-01 5 | */ 6 | 7 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { 8 | margin: 0; 9 | padding: 0; 10 | box-sizing: border-box; 11 | } 12 | ol, ul, li { 13 | list-style: none; 14 | } 15 | blockquote, q { 16 | quotes: none; 17 | } 18 | blockquote:before, blockquote:after, 19 | q:before, q:after { 20 | content: ''; 21 | content: none; 22 | } 23 | table { 24 | border-collapse: collapse; 25 | border-spacing: 0; 26 | } 27 | address, caption, cite, code, dfn, em, strong, th, var, optgroup { 28 | font-style: normal; 29 | font-weight: normal; 30 | } 31 | fieldset, img { 32 | border:0; 33 | } 34 | textarea { 35 | resize: none !important; 36 | } 37 | 38 | // body::-webkit-scrollbar { 39 | // display: none 40 | // } 41 | 42 | /* custom */ 43 | #root { 44 | width: 100vw; 45 | height: 100vh; 46 | -webkit-font-smoothing: antialiased; 47 | -moz-osx-font-smoothing: grayscale; 48 | -webkit-user-select: none; 49 | -moz-user-select: none; 50 | -ms-user-select: none; 51 | user-select: none; 52 | } 53 | a { 54 | color: #7e8c8d; 55 | text-decoration: none; 56 | -webkit-backface-visibility: hidden; 57 | backface-visibility: hidden; 58 | } 59 | // ::-webkit-scrollbar { 60 | // width: 10px; 61 | // height: 10px; 62 | // } 63 | // ::-webkit-scrollbar-track-piece { 64 | // background-color: rgba(0, 0, 0, 0.2); 65 | // -webkit-border-radius: 6px; 66 | // border-radius: 6px; 67 | // } 68 | // ::-webkit-scrollbar-thumb:vertical { 69 | // height: 5px; 70 | // background-color: rgba(125, 125, 125, 0.7); 71 | // -webkit-border-radius: 6px; 72 | // border-radius: 6px; 73 | // } 74 | // ::-webkit-scrollbar-thumb:horizontal { 75 | // width: 5px; 76 | // background-color: rgba(125, 125, 125, 0.7); 77 | // -webkit-border-radius: 6px; 78 | // border-radius: 6px; 79 | // } 80 | html, body { 81 | font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; 82 | } 83 | body { 84 | height: 100%; 85 | width: 100%; 86 | line-height: 22px; 87 | font-size: 14px; 88 | overflow: hidden; 89 | overflow-y: auto; 90 | -webkit-text-size-adjust: none; 91 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 92 | } 93 | 94 | /*清除浮动*/ 95 | .clearfix:before, 96 | .clearfix:after { 97 | content: " "; 98 | display: inline-block; 99 | height: 0; 100 | clear: both; 101 | visibility: hidden; 102 | } 103 | .clearfix { 104 | *zoom: 1; 105 | } 106 | 107 | /*隐藏*/ 108 | .dn { 109 | display: none; 110 | } 111 | 112 | .ant-table-tbody { 113 | tr.ant-table-row { 114 | &:hover { 115 | td { 116 | background: rgba(117, 175, 236, .2) !important; 117 | } 118 | } 119 | } 120 | } 121 | 122 | .ant-table-tbody { 123 | tr { 124 | td { 125 | padding: 12px 16px !important; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/styles/footer.less: -------------------------------------------------------------------------------- 1 | .footer-container { 2 | // position: fixed; 3 | // bottom: 0; 4 | // left: 0; 5 | // right: 0; 6 | .footer { 7 | .copyright { 8 | color: #fff; 9 | font-size: 14px; 10 | background: #14143f; 11 | text-align: center; 12 | padding: 30px 40px; 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/styles/header.less: -------------------------------------------------------------------------------- 1 | .header-container { 2 | .header { 3 | width: 100%; 4 | background: #17174c; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | padding: 20px 100px 20px 40px; 9 | box-sizing: border-box; 10 | .section { 11 | display: flex; 12 | ul { 13 | display: flex; 14 | align-items: center; 15 | margin-left: 60px; 16 | margin-bottom: 0; 17 | li { 18 | margin-right: 40px; 19 | a { 20 | color: #fff; 21 | opacity: .5; 22 | &:hover, &.active { 23 | opacity: 1; 24 | }; 25 | } 26 | } 27 | } 28 | } 29 | .dropdown-link { 30 | color: #fff; 31 | .username { 32 | padding-right: 10px; 33 | } 34 | .ivu-icon { 35 | margin-left: 5px; 36 | } 37 | } 38 | img { 39 | outline: none; 40 | height: 40px; 41 | &.avatar { 42 | border-radius: 50%; 43 | width: 42px; 44 | height: 42px; 45 | margin-right: 5px; 46 | vertical-align: middle; 47 | background: #eee; 48 | } 49 | } 50 | 51 | } 52 | 53 | .github-corner { 54 | svg { 55 | fill: #42b983; 56 | color: #fff; 57 | position: absolute; 58 | top: 0; 59 | border: 0; 60 | right: 0; 61 | } 62 | .octo-arm { 63 | transform-origin: 130px 106px; 64 | } 65 | &:hover { 66 | .octo-arm { 67 | -webkit-animation: octocat-wave .56s ease-in-out; 68 | animation: octocat-wave .56s ease-in-out; 69 | } 70 | } 71 | 72 | } 73 | 74 | @-webkit-keyframes octocat-wave { 75 | 0%, 100% { 76 | -webkit-transform: rotate(0); 77 | transform: rotate(0); 78 | } 79 | 20%, 60% { 80 | -webkit-transform: rotate(-25deg); 81 | transform: rotate(-25deg); 82 | } 83 | 40%, 80% { 84 | -webkit-transform: rotate(10deg); 85 | transform: rotate(10deg); 86 | } 87 | } 88 | @keyframes octocat-wave { 89 | 0%, 100% { 90 | -webkit-transform: rotate(0); 91 | transform: rotate(0); 92 | } 93 | 20%, 60% { 94 | -webkit-transform: rotate(-25deg); 95 | transform: rotate(-25deg); 96 | } 97 | 40%, 80% { 98 | -webkit-transform: rotate(10deg); 99 | transform: rotate(10deg); 100 | } 101 | } 102 | 103 | } 104 | 105 | .vertical-center-modal{ 106 | display: flex; 107 | align-items: center; 108 | justify-content: center; 109 | 110 | .ivu-modal{ 111 | top: 0; 112 | } 113 | } 114 | 115 | @media screen and (max-width: 1280px) { 116 | .home-container { 117 | .content { 118 | height: calc(100% + 100px) !important; 119 | } 120 | } 121 | .notFound { 122 | .main { 123 | height: calc(100% + 100px) !important; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /src/styles/home.less: -------------------------------------------------------------------------------- 1 | .home-container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-between; 7 | .content { 8 | height: calc(100% - 35px); 9 | padding: 20px 40px; 10 | .list { 11 | display: flex; 12 | justify-content: space-between; 13 | align-content: center; 14 | padding: 20px 0 30px; 15 | h2 { 16 | line-height: 28px; 17 | } 18 | } 19 | } 20 | 21 | } 22 | 23 | .pagination { 24 | float: right; 25 | margin: 20px 0; 26 | } 27 | 28 | .demo-drawer-footer{ 29 | width: 100%; 30 | position: absolute; 31 | bottom: 0; 32 | left: 0; 33 | border-top: 1px solid #e8e8e8; 34 | padding: 16px; 35 | background: #fff; 36 | display: flex; 37 | justify-content: space-around; 38 | } -------------------------------------------------------------------------------- /src/styles/login.less: -------------------------------------------------------------------------------- 1 | .login-container { 2 | background-image: url('../assets/bg.jpg'); 3 | background-position: center; 4 | background-size: cover; 5 | position: relative; 6 | width: 100%; 7 | height: 100%; 8 | 9 | .pageHeader { 10 | padding-top: 30px; 11 | padding-left: 40px; 12 | 13 | img { 14 | vertical-align: middle; 15 | display: inline-block; 16 | margin-right: 15px; 17 | height: 40px; 18 | } 19 | 20 | span { 21 | font-size: 18px; 22 | display: inline-block; 23 | vertical-align: -4px; 24 | color: rgba(255, 255, 255, 1); 25 | } 26 | } 27 | 28 | .login-box { 29 | position: absolute; 30 | left: 64vw; 31 | top: 50%; 32 | -webkit-transform: translateY(-50%); 33 | transform: translateY(-50%); 34 | box-sizing: border-box; 35 | text-align: center; 36 | box-shadow: 0 1px 11px rgba(0, 0, 0, 0.3); 37 | border-radius: 2px; 38 | /*margin: 100px auto 0;*/ 39 | width: 420px; 40 | background: #fff; 41 | padding: 45px 35px; 42 | .option { 43 | text-align: left; 44 | margin-top: 18px; 45 | .forget-pwd, .goback { 46 | float: right; 47 | font-size: 14px; 48 | font-weight: 400; 49 | color: #4981f2; 50 | line-height: 20px; 51 | cursor: pointer; 52 | } 53 | .protocol { 54 | color: #4981f2; 55 | cursor: pointer; 56 | } 57 | } 58 | 59 | .login-text { 60 | width: 100%; 61 | text-align: center; 62 | padding: 0 0 30px; 63 | font-size: 24px; 64 | letter-spacing: 1px; 65 | span { 66 | padding: 10px; 67 | color: #969696; 68 | cursor: pointer; 69 | &.active { 70 | font-weight: 600; 71 | color: rgba(73, 129, 242, 1); 72 | border-bottom: 2px solid rgba(73, 129, 242, 1); 73 | } 74 | &:hover { 75 | border-bottom: 2px solid rgba(73, 129, 242, 1); 76 | } 77 | } 78 | b { 79 | padding: 10px; 80 | } 81 | } 82 | .title { 83 | font-weight: 600; 84 | padding: 0 0 30px; 85 | font-size: 24px; 86 | letter-spacing: 1px; 87 | color: rgba(73, 129, 242, 1); 88 | } 89 | 90 | .input-box { 91 | .input { 92 | // &:nth-child(1) { 93 | 94 | // } 95 | &:nth-child(2), 96 | &:nth-child(3) { 97 | margin-top: 20px; 98 | } 99 | } 100 | } 101 | 102 | .loginBtn { 103 | width: 100%; 104 | height: 45px; 105 | margin-top: 40px; 106 | font-size: 15px; 107 | } 108 | 109 | .input { 110 | padding: 10px 0px; 111 | font-size: 15px; 112 | width: 350px; 113 | color: #2c2e33; 114 | outline: none; // 去除选中状态边框 115 | border: 1px solid #fff; 116 | border-bottom-color: #e7e7e7; 117 | background-color: rgba(0, 0, 0, 0); // 透明背景 118 | } 119 | 120 | input:focus { 121 | border-bottom-color: #0f52e0; 122 | outline: none; 123 | box-shadow: none; 124 | } 125 | .button { 126 | line-height: 45px; 127 | cursor: pointer; 128 | margin-top: 50px; 129 | border: none; 130 | outline: none; 131 | height: 45px; 132 | width: 350px; 133 | background: rgba(216, 216, 216, 1); 134 | border-radius: 2px; 135 | color: white; 136 | font-size: 15px; 137 | } 138 | } 139 | 140 | // 滚动条样式 141 | ::-webkit-scrollbar { 142 | width: 10px; 143 | } 144 | 145 | ::-webkit-scrollbar-track { 146 | -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.3); 147 | box-shadow: inset006pxrgba(0, 0, 0, 0.3); 148 | border-radius: 8px; 149 | } 150 | 151 | ::-webkit-scrollbar-thumb { 152 | border-radius: 10px; 153 | background: rgba(0, 0, 0, 0.2); 154 | -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5); 155 | box-shadow: inset006pxrgba(0, 0, 0, 0.5); 156 | } 157 | 158 | ::-webkit-scrollbar-thumb:window-inactive { 159 | background: rgba(0, 0, 0, 0.4); 160 | } 161 | 162 | //设置更改input 默认颜色 163 | ::-webkit-input-placeholder { 164 | /* WebKit browsers */ 165 | color: #bebebe; 166 | font-size: 16px; 167 | } 168 | 169 | ::-moz-placeholder { 170 | /* Mozilla Firefox 19+ */ 171 | color: #bebebe; 172 | font-size: 16px; 173 | } 174 | 175 | :-ms-input-placeholder { 176 | /* Internet Explorer 10+ */ 177 | color: #bebebe; 178 | font-size: 16px; 179 | } 180 | 181 | input:-webkit-autofill { 182 | box-shadow: 0 0 0px 1000px rgba(255, 255, 255, 1) inset; 183 | -webkit-box-shadow: 0 0 0px 1000px rgba(255, 255, 255, 1) inset; 184 | -webkit-text-fill-color: #2c2e33; 185 | } 186 | 187 | } -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | import network from './network'; 2 | 3 | // 登录 4 | export function loginUser(data) { 5 | return network({ 6 | url: `/login`, 7 | method: "post", 8 | data 9 | }); 10 | } 11 | 12 | // 注册 13 | export function registerUser(data) { 14 | return network({ 15 | url: `/register`, 16 | method: "post", 17 | data 18 | }) 19 | } 20 | 21 | // 密码重置 22 | export function resetPwd(data) { 23 | return network({ 24 | url: `/resetPwd`, 25 | method: "post", 26 | data 27 | }) 28 | } 29 | 30 | // 任务列表 31 | export function queryTaskList(params) { 32 | return network({ 33 | url: `/queryTaskList`, 34 | method: "get", 35 | params 36 | }) 37 | } 38 | 39 | // 添加任务 40 | export function addTask(data) { 41 | return network({ 42 | url: `/addTask`, 43 | method: "post", 44 | data 45 | }) 46 | } 47 | 48 | // 编辑任务 49 | export function editTask(data) { 50 | return network({ 51 | url: `/editTask`, 52 | method: "put", 53 | data 54 | }) 55 | } 56 | 57 | // 操作任务状态 58 | export function updateTaskStatus(data) { 59 | return network({ 60 | url: `/updateTaskStatus`, 61 | method: "put", 62 | data 63 | }) 64 | } 65 | 66 | // 点亮红星标记 67 | export function updateMark(data) { 68 | return network({ 69 | url: `/updateMark`, 70 | method: "put", 71 | data 72 | }) 73 | } 74 | 75 | // 删除任务 76 | export function deleteTask(data) { 77 | return network({ 78 | url: `/deleteTask`, 79 | method: "delete", 80 | data 81 | }) 82 | } -------------------------------------------------------------------------------- /src/utils/network.js: -------------------------------------------------------------------------------- 1 | // import * as React from 'react'; 2 | import { message } from 'antd'; 3 | import axios from 'axios'; 4 | import store from '@/store'; 5 | import { apiUrl } from './url'; 6 | import { logout } from '@/store/actions'; 7 | 8 | axios.defaults.headers['Content-Type'] = 'application/json'; 9 | 10 | // 创建axios实例 11 | const service = axios.create({ 12 | baseURL: apiUrl, 13 | timeout: 60 * 1000 14 | }); 15 | 16 | // 请求拦截器 17 | service.interceptors.request.use( 18 | config => { 19 | // console.log('store.getState()===',store.getState().user) 20 | if (store.getState().user.data.token) { 21 | config.headers['Authorization'] = store.getState().user.data.token; 22 | } 23 | return config; 24 | }, 25 | error => { 26 | return Promise.reject(error); 27 | } 28 | ) 29 | 30 | // 响应拦截器 31 | service.interceptors.response.use( 32 | response => { 33 | console.log(response.data); 34 | // 抛出401错误,因为token失效,重新刷新页面,清空缓存,跳转到登录界面 35 | if (response.data.code === 401 || response.data.code === 403) { 36 | console.log('退出登录'); 37 | store.dispatch(logout()); 38 | message.error('token失效,或长时间未操作,请重新登录'); 39 | } 40 | 41 | return response.data; 42 | }, 43 | error => { 44 | console.log('error===', error.response) 45 | const { status } = error.response; 46 | 47 | if (status === 401 || status === 403) { 48 | store.dispatch(logout()); 49 | message.error('token失效,或长时间未操作,请重新登录'); 50 | } else { 51 | message.error('网络异常,请稍后再试'); 52 | } 53 | 54 | return Promise.reject(error); 55 | } 56 | ) 57 | 58 | export default service; 59 | -------------------------------------------------------------------------------- /src/utils/url.js: -------------------------------------------------------------------------------- 1 | const isProduction = process.env.NODE_ENV === 'production'; 2 | 3 | const url = isProduction ? 'https://github.com/jackchen0120/' : 'http://localhost:8088/'; 4 | 5 | const apiUrl = '/api'; 6 | 7 | 8 | export { 9 | apiUrl, 10 | url 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/valid.js: -------------------------------------------------------------------------------- 1 | export function validEmail(val) { 2 | return /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(val); 3 | } 4 | 5 | export function validPhone(val) { 6 | return /^1[3456789]\d{9}$/.test(val); 7 | } 8 | 9 | export function validPass(val) { 10 | return /^[a-zA-Z\d]{8,20}$/.test(val); 11 | // return /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$/.test(val); 12 | // return /^.{6,16}$/.test(val); 13 | } 14 | 15 | export function validUserName(name) { 16 | return validEmail(name) || validPhone(name); 17 | } 18 | 19 | export function validCode(val) { 20 | return /^[0-9]{6}$/.test(val); 21 | } 22 | 23 | export function userName(str) { 24 | const re = /^[\u4E00-\u9FA5A-Za-z0-9]+$/ 25 | return re.test(str); 26 | } 27 | 28 | export function validateMainName2(name) { 29 | const re = /^[a-zA-Z0-9_-]{1,19}$/ 30 | return re.test(name); 31 | } 32 | 33 | export function validateNickName(name) { 34 | const re = /^[a-zA-Z0-9\u4E00-\u9FA5]{2,10}$/ 35 | return re.test(name); 36 | } 37 | 38 | export function formatDate(value) { 39 | if (!value) { 40 | return ''; 41 | } 42 | let d = new Date(value); 43 | let year = d.getFullYear(); 44 | let month = (d.getMonth() + 1) < 10 ? '0' + (d.getMonth() + 1) : (d.getMonth() + 1); 45 | let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate(); 46 | return year + '-' + month + '-' + day; 47 | } 48 | 49 | export function formatTime(value) { 50 | if (!value) { 51 | return ''; 52 | } 53 | let d = new Date(value); 54 | // let year = d.getFullYear(); 55 | let month = d.getMonth() + 1; 56 | let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate(); 57 | let hour = d.getHours() < 10 ? '0' + d.getHours() : '' + d.getHours(); 58 | // let minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : '' + d.getMinutes(); 59 | // let seconds = d.getSeconds() < 10 ? '0' + d.getSeconds() : '' + d.getSeconds(); 60 | return `${month}月${day}日 ${hour}时`; 61 | } 62 | 63 | export function formatHour(value) { 64 | if (!value) { 65 | return ''; 66 | } 67 | let d = new Date(value); 68 | // let year = d.getFullYear(); 69 | // let month = d.getMonth() + 1; 70 | // let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate(); 71 | let hour = d.getHours(); 72 | return hour; 73 | } 74 | 75 | export function timeFromNow(value) { 76 | let currentDate = Date.now(); 77 | let timestamp = currentDate - value; 78 | switch (true) { 79 | case timestamp > 86400000: 80 | return `${Math.floor(timestamp / 86400000)}天前`; 81 | case timestamp > 3600000: 82 | return `${Math.floor(timestamp / 3600000)}小时前`; 83 | case timestamp > 60000: 84 | return `${Math.floor(timestamp / 60000)}分钟前`; 85 | case timestamp > 1000: 86 | return `${Math.floor(timestamp / 1000)}秒钟前`; 87 | default: 88 | // do nothing 89 | } 90 | } 91 | 92 | 93 | export default { 94 | validEmail, 95 | validPhone, 96 | validUserName, 97 | validCode, 98 | validPass, 99 | userName, 100 | validateMainName2, 101 | validateNickName, 102 | formatDate, 103 | timeFromNow, 104 | formatHour 105 | } 106 | -------------------------------------------------------------------------------- /src/views/Home.tsx: -------------------------------------------------------------------------------- 1 | /* 描述: 首页模板 2 | * 作者: Jack Chen 3 | * 日期: 2020-08-09 4 | */ 5 | 6 | import * as React from 'react'; 7 | import DocumentTitle from 'react-document-title'; 8 | import { Drawer, Button, Table, Space, Pagination, message, Select, Form, Input, DatePicker } from 'antd'; 9 | import { StarOutlined, StarTwoTone, PlusOutlined } from '@ant-design/icons'; 10 | import { ColumnsType } from 'antd/es/table'; 11 | import moment from 'moment'; 12 | import Header from '@/components/Header'; 13 | import Footer from '@/components/Footer'; 14 | import '@/styles/home.less'; 15 | import { 16 | queryTaskList, 17 | addTask, 18 | editTask, 19 | updateTaskStatus, 20 | updateMark, 21 | deleteTask 22 | } from '@/utils/api'; 23 | import { formatDate } from '@/utils/valid'; 24 | 25 | interface Task { 26 | id: number, 27 | title: string, 28 | content: string, 29 | gmt_expire: number, 30 | status: number, 31 | is_major: any 32 | } 33 | 34 | interface Values { 35 | id?: number, 36 | title: string, 37 | date: any, 38 | content: string 39 | } 40 | 41 | interface IState { 42 | total: number, 43 | pageNo: number, 44 | pageSize: number, 45 | loading: boolean, 46 | textBtn: string, 47 | title: string, 48 | visible: boolean, 49 | currentRowData: Values, 50 | status: any, 51 | columns: ColumnsType, 52 | dataSource: Task[] 53 | } 54 | 55 | interface IProps { 56 | title: string, 57 | textBtn: string, 58 | visible: boolean, 59 | currentRowData: Values, 60 | onSubmitDrawer: (values: Values, type: number) => void, 61 | onCloseDrawer: () => void 62 | } 63 | 64 | const AddEditTaskForm: React.FC = ({ 65 | title, 66 | textBtn, 67 | visible, 68 | currentRowData, 69 | onSubmitDrawer, 70 | onCloseDrawer 71 | }) => { 72 | const [form] = Form.useForm(); 73 | 74 | console.log('currentRowData===', currentRowData) 75 | setTimeout(() => { 76 | form.setFieldsValue(currentRowData); 77 | }, 100) 78 | 79 | const onSubmit = () => { 80 | form.validateFields() 81 | .then((values: any) => { 82 | if (title === '添加任务') { 83 | onSubmitDrawer(values, 1); 84 | } else { 85 | onSubmitDrawer(values, 2); 86 | } 87 | }) 88 | .catch(info => { 89 | console.log('Validate Failed:', info); 90 | }) 91 | } 92 | 93 | const onReset = () => { 94 | form.resetFields(); 95 | } 96 | 97 | const onClose = () => { 98 | form.resetFields(); 99 | onCloseDrawer(); 100 | } 101 | 102 | return ( 103 | 113 | 114 | 115 | 116 | 117 | } 118 | > 119 |
124 | 129 | 130 | 131 | 136 | 137 | 138 | 143 | 144 | 145 |
146 |
147 | 148 | ) 149 | } 150 | 151 | class Home extends React.Component { 152 | constructor(props: any) { 153 | super(props); 154 | this.state = { 155 | total: 0, 156 | pageNo: 1, 157 | pageSize: 10, 158 | loading: false, 159 | textBtn: '提交', 160 | title: '添加任务', 161 | currentRowData: { 162 | id: -1, 163 | title: '', 164 | date: '', 165 | content: '' 166 | }, 167 | visible: false, 168 | dataSource: [], 169 | status: null, // 0:待办 1:完成 2:删除 170 | columns: [ 171 | { 172 | title: '序号', 173 | key: 'id', 174 | align: 'center', 175 | render: (text: any, record: any, index: number) => { 176 | let num = (this.state.pageNo - 1) * 10 + index + 1; 177 | return num; 178 | } 179 | }, 180 | { 181 | title: '任务名称', 182 | dataIndex: 'title', 183 | key: 'title', 184 | render: (text: any, record: any, index: number) => { 185 | const fav = this.state.dataSource[index].is_major; 186 | const style = { 187 | cursor: 'pointer', 188 | fontSize: '16px' 189 | } 190 | 191 | const icon = fav === 0 ? : ; 192 | 193 | return
this.toggleFav(record, index) }>{ icon } { record.title }
; 194 | } 195 | }, 196 | { 197 | title: '任务内容', 198 | dataIndex: 'content', 199 | key: 'content' 200 | }, 201 | { 202 | title: '截止日期', 203 | dataIndex: 'gmt_expire', 204 | key: 'gmt_expire', 205 | render: (text: any, record: any) => formatDate(record.gmt_expire) 206 | }, 207 | { 208 | title: '任务状态', 209 | dataIndex: 'status', 210 | key: 'status', 211 | width: 120, 212 | render: (text: any, record: any) => { 213 | const txt = record.status === 0 ? '待办' : record.status === 1 ? '完成' : '删除'; 214 | return txt; 215 | } 216 | }, 217 | { 218 | title: '操作', 219 | key: 'action', 220 | width: 300, 221 | align: 'center', 222 | render: (text: any, record: any, index: number) => ( 223 | 224 | 225 | 228 | 229 | 230 | ) 231 | } 232 | ] 233 | } 234 | } 235 | 236 | componentDidMount () { 237 | console.log('componentDidMount===') 238 | this.getTaskList(); 239 | } 240 | 241 | componentWillUnmount () { 242 | console.log('componentWillUnmount===') 243 | } 244 | 245 | // 重要或不重要 246 | toggleFav = (record: any, index: number) => { 247 | if (record.status === 2) { 248 | message.error('数据已删除'); 249 | } else { 250 | // is_major: 0:不重要 1:重要 251 | let data = { 252 | id: record.id, 253 | is_major: this.state.dataSource[index].is_major === 0 ? 1 : 0 254 | } 255 | 256 | updateMark(data) 257 | .then((res: any) => { 258 | console.log('操作标记===', res); 259 | if (res.code === 0) { 260 | this.setState({ 261 | pageNo: 1 262 | }, () => { 263 | this.getTaskList(); 264 | message.success('更新标记成功'); 265 | }) 266 | } else { 267 | message.error(res.msg); 268 | } 269 | }) 270 | } 271 | } 272 | 273 | // 获取任务列表数据 274 | getTaskList = () => { 275 | const { pageNo, pageSize, status } = this.state; 276 | this.setState({ 277 | loading: true 278 | }) 279 | 280 | let params = { 281 | pageNo: pageNo, 282 | pageSize: pageSize, 283 | status: status 284 | } 285 | 286 | queryTaskList(params) 287 | .then((res: any) => { 288 | console.log('任务列表===', res); 289 | this.setState({ 290 | loading: false 291 | }) 292 | 293 | if (res.code === 0 && res.data) { 294 | this.setState({ 295 | dataSource: res.data.rows, 296 | total: res.data.total 297 | }) 298 | } else { 299 | this.setState({ 300 | dataSource: [], 301 | total: 0 302 | }) 303 | } 304 | }) 305 | .catch(() => { 306 | this.setState({ 307 | loading: false 308 | }) 309 | }) 310 | } 311 | 312 | // 添加任务对话框 313 | addTask = () => { 314 | console.log('添加任务==='); 315 | this.setState({ 316 | title: '添加任务', 317 | textBtn: '提交', 318 | visible: true, 319 | currentRowData: { 320 | id: -1, 321 | title: '', 322 | date: '', 323 | content: '' 324 | } 325 | }) 326 | } 327 | 328 | // 编辑任务对话框 329 | editTask = (record: any, index: number) => { 330 | console.log('编辑任务===', record); 331 | this.setState({ 332 | title: '编辑任务', 333 | textBtn: '保存', 334 | visible: true, 335 | currentRowData: { 336 | id: record.id, 337 | title: record.title, 338 | date: moment(record.gmt_expire), 339 | content: record.content 340 | } 341 | }) 342 | } 343 | 344 | // 删除任务 345 | removeTask = (id: number) => { 346 | console.log('删除任务==='); 347 | let data = { 348 | id: id, 349 | status: 2 350 | } 351 | 352 | deleteTask(data) 353 | .then((res: any) => { 354 | console.log('删除任务===', res); 355 | if (res.code === 0) { 356 | this.setState({ 357 | pageNo: 1 358 | }, () => { 359 | this.getTaskList(); 360 | message.success('任务删除成功'); 361 | }) 362 | } else { 363 | message.error(res.msg); 364 | } 365 | }) 366 | } 367 | 368 | // 完成/待办任务 369 | completeTask = (record: any) => { 370 | console.log('完成/待办任务==='); 371 | let status = record.status === 0 ? 1 : record.status === 1 ? 0 : null; 372 | 373 | let data = { 374 | id: record.id, 375 | status: status 376 | } 377 | 378 | updateTaskStatus(data) 379 | .then((res: any) => { 380 | console.log('操作状态===', res); 381 | if (res.code === 0) { 382 | this.setState({ 383 | pageNo: 1 384 | }, () => { 385 | this.getTaskList(); 386 | message.success('更新任务状态成功'); 387 | }) 388 | } else { 389 | message.error(res.msg); 390 | } 391 | }) 392 | } 393 | 394 | // 提交添加或编辑表单 395 | onSubmit = (values: Values, type: number) => { 396 | console.log('表单提交===', values); 397 | const { currentRowData } = this.state; 398 | if (type === 1) { 399 | let data = { 400 | title: values.title, 401 | gmt_expire: moment(values.date).valueOf(), 402 | content: values.content 403 | } 404 | 405 | addTask(data) 406 | .then((res: any) => { 407 | console.log('添加任务===', res) 408 | this.setState({ 409 | visible: false 410 | }) 411 | if (res.code === 0) { 412 | this.setState({ 413 | pageNo: 1 414 | }, () => { 415 | this.getTaskList(); 416 | message.success(`新增任务 <${values.title}> 成功`); 417 | }) 418 | } else { 419 | message.error(res.msg); 420 | } 421 | }) 422 | .catch(() => { 423 | this.setState({ 424 | visible: false 425 | }) 426 | }) 427 | 428 | } else if (type === 2) { 429 | let data = { 430 | id: currentRowData.id, 431 | title: values.title, 432 | gmt_expire: moment(values.date).valueOf(), 433 | content: values.content 434 | } 435 | 436 | editTask(data) 437 | .then((res: any) => { 438 | console.log('编辑任务===', res) 439 | this.setState({ 440 | visible: false 441 | }) 442 | if (res.code === 0) { 443 | this.setState({ 444 | pageNo: 1 445 | }, () => { 446 | this.getTaskList(); 447 | message.success(`更新任务 <${values.title}> 成功`); 448 | }) 449 | } else { 450 | message.error(res.msg); 451 | } 452 | }) 453 | .catch(() => { 454 | this.setState({ 455 | visible: false 456 | }) 457 | }) 458 | } 459 | } 460 | 461 | // 关闭任务对话框 462 | onClose = () => { 463 | this.setState({ 464 | visible: false, 465 | currentRowData: { 466 | id: -1, 467 | title: '', 468 | date: '', 469 | content: '' 470 | } 471 | }) 472 | } 473 | 474 | // 页码改变的回调,返回改变后的页码 475 | changePage = (pageNo: number) => { 476 | console.log('pageNo=', pageNo) 477 | this.setState({ 478 | pageNo 479 | }, () => { 480 | this.getTaskList(); 481 | }) 482 | } 483 | 484 | // 筛选任务状态 485 | handleChange = (value: number) => { 486 | console.log('任务状态筛选===', typeof value === 'string') 487 | this.setState({ 488 | status: typeof value === 'string' ? null : value, 489 | pageNo: 1 490 | }, () => { 491 | this.getTaskList(); 492 | }) 493 | } 494 | 495 | render () { 496 | const { 497 | total, 498 | pageNo, 499 | pageSize, 500 | loading, 501 | dataSource, 502 | columns, 503 | visible, 504 | title, 505 | textBtn, 506 | currentRowData 507 | } = this.state; 508 | const { Option } = Select; 509 | 510 | return ( 511 | 512 |
513 |
514 | 515 |
516 |
517 |

任务列表

518 |
519 | 520 | 526 | 527 | 528 |
529 |
530 | 531 | record.id } 534 | dataSource={ dataSource } 535 | columns={ columns } 536 | loading={ loading } 537 | pagination={ false } 538 | /> 539 | `共 ${total} 条数据`} 544 | onChange={ this.changePage } 545 | current={ pageNo } 546 | showSizeChanger={ false } 547 | defaultPageSize={ pageSize } 548 | hideOnSinglePage={ false } 549 | /> 550 | 551 | 552 |
553 | 554 | 562 | 563 | 564 | 565 | ) 566 | } 567 | } 568 | 569 | export default Home -------------------------------------------------------------------------------- /src/views/Login.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* 描述: 登录模板 3 | * 作者: Jack Chen 4 | * 日期: 2020-08-05 5 | */ 6 | 7 | 8 | import * as React from 'react'; 9 | import { Input, Button, Checkbox, message } from 'antd'; 10 | import { connect } from 'react-redux'; 11 | import { withRouter } from 'react-router-dom'; 12 | import { login, register } from '@/store/actions'; 13 | import logo from "@/assets/logo_2.png"; 14 | import '@/styles/login.less'; 15 | import { validUserName, validPass } from '@/utils/valid'; 16 | import DocumentTitle from 'react-document-title'; 17 | 18 | interface IProps { 19 | login: any, 20 | register: any, 21 | history: any 22 | } 23 | 24 | interface IState { 25 | formLogin: { 26 | userName: string, 27 | userPwd: string 28 | }, 29 | formRegister: { 30 | userName?: string, 31 | userPwd2?: string, 32 | userPwd?: string, 33 | }, 34 | typeView: number, 35 | checked: boolean, 36 | isLoading: boolean 37 | } 38 | 39 | class Login extends React.Component { 40 | constructor(props: any) { 41 | super(props); 42 | this.state = { 43 | formLogin: { 44 | userName: '', 45 | userPwd: '', 46 | }, 47 | formRegister: { 48 | userName: '', 49 | userPwd2: '', 50 | userPwd: '', 51 | }, 52 | typeView: 0, 53 | checked: false, 54 | isLoading: false 55 | } 56 | } 57 | 58 | // 设置cookie 59 | setCookie = (user_name: string, user_pwd: string, exdays: number) => { 60 | // 获取时间 61 | let exdate = new Date(); 62 | // 保存的天数 63 | exdate.setTime(exdate.getTime() + 24 * 60 * 60 * 1000 * exdays); 64 | // 字符串拼接cookie 65 | document.cookie = `userName=${user_name};path=/;expires=${exdate.toUTCString()}`; 66 | document.cookie = `userPwd=${user_pwd};path=/;expires=${exdate.toUTCString()}`; 67 | } 68 | 69 | // 读取cookie 70 | getCookie = () => { 71 | const { formLogin } = this.state; 72 | if (document.cookie.length > 0) { 73 | // 这里显示的格式需要切割一下自己可输出看下 74 | let arr = document.cookie.split('; '); 75 | console.log(arr) 76 | for (let i = 0; i < arr.length; i++) { 77 | // 再次切割 78 | let arr2 = arr[i].split('='); 79 | // 判断查找相对应的值 80 | if (arr2[0] === 'userName') { 81 | // 保存数据并赋值 82 | this.setState({ 83 | formLogin: { 84 | userName: arr2[1], 85 | userPwd: formLogin.userPwd 86 | } 87 | }) 88 | } else if (arr2[0] === 'userPwd') { 89 | this.setState({ 90 | formLogin: { 91 | userName: formLogin.userName, 92 | userPwd: arr2[1] 93 | } 94 | }) 95 | } else { 96 | 97 | } 98 | } 99 | } 100 | } 101 | 102 | //清除cookie 103 | clearCookie = () => { 104 | // 修改前2个值都为空,天数为负1天就好了 105 | this.setCookie('', '', -1); 106 | } 107 | 108 | // 立即登录 109 | handleLogin = () => { 110 | const { login, history } = this.props; 111 | const { formLogin, checked } = this.state; 112 | 113 | if (!validUserName(formLogin.userName)) { 114 | message.error('请输入正确的邮箱/手机号'); 115 | return false; 116 | } 117 | 118 | if (!validPass(formLogin.userPwd)) { 119 | message.error('密码应为8到20位字母或数字!'); 120 | return false; 121 | } 122 | 123 | // 判断复选框是否被勾选,勾选则调用配置cookie方法 124 | if (checked) { 125 | // 传入账号名,密码,和保存天数3个参数 126 | this.setCookie(formLogin.userName, formLogin.userPwd, 7); 127 | } else { 128 | // 清空Cookie 129 | this.clearCookie(); 130 | } 131 | 132 | login( 133 | formLogin.userName, 134 | formLogin.userPwd 135 | ) 136 | .then((res: any) => { 137 | console.log('login===', res); 138 | if (res.code === 0) { 139 | this.clearInput(); 140 | message.success('登录成功'); 141 | history.push('/'); 142 | } 143 | }) 144 | .catch((error: any) => { 145 | message.error(error); 146 | }) 147 | } 148 | 149 | // 立即注册 150 | handleRegister = () => { 151 | console.log(this.props) 152 | const { register, history } = this.props; 153 | const { formRegister } = this.state; 154 | 155 | if (!validUserName(formRegister.userName)) { 156 | message.error("请输入正确的邮箱/手机号"); 157 | return false; 158 | } else if (!validPass(formRegister.userPwd)) { 159 | message.error("密码应为8到20位字母或数字!"); 160 | return false; 161 | } else if (!validPass(formRegister.userPwd2)){ 162 | message.error("确认密码有误"); 163 | return false; 164 | } else if (formRegister.userPwd2 !== formRegister.userPwd){ 165 | message.error("两次密码不一致"); 166 | return false; 167 | } 168 | 169 | register( 170 | formRegister.userName, 171 | formRegister.userPwd2 172 | ) 173 | .then((res: any) => { 174 | if (res.code === 0) { 175 | this.clearInput(); 176 | message.success('注册成功'); 177 | history.push('/'); 178 | } 179 | }) 180 | .catch((error: any) => { 181 | message.error(error); 182 | }) 183 | } 184 | 185 | // 登录/注册tab切换 186 | handleTab = (type: number) => { 187 | // console.log('type===',type); 188 | this.setState({ 189 | typeView: type 190 | }) 191 | this.clearInput(); 192 | } 193 | 194 | // 是否勾选记住密码 195 | checkChange = (e: any) => { 196 | console.log(e.target.checked); 197 | this.setState({ 198 | checked: e.target.checked 199 | }) 200 | } 201 | 202 | // 清空输入框 203 | clearInput = () => { 204 | this.setState({ 205 | formLogin: { 206 | userName: '', 207 | userPwd: '', 208 | }, 209 | formRegister: { 210 | userName: '', 211 | userPwd2: '', 212 | userPwd: '', 213 | } 214 | }) 215 | } 216 | 217 | // 忘记密码界面 218 | forgetPwd = () => { 219 | message.info('忘记密码,请联系客服'); 220 | } 221 | 222 | // 监听输入登录信息 223 | handleChangeInput = (e: any, type: number) => { 224 | const { formLogin } = this.state; 225 | this.setState({ 226 | formLogin: { 227 | userName: type === 1 ? e.target.value : formLogin.userName, 228 | userPwd: type === 2 ? e.target.value : formLogin.userPwd 229 | } 230 | }) 231 | } 232 | 233 | // 监听输入注册信息 234 | handleChangeRegister = (e: any, type: number) => { 235 | const { formRegister } = this.state; 236 | this.setState({ 237 | formRegister: { 238 | userName: type === 1 ? e.target.value : formRegister.userName, 239 | userPwd: type === 2 ? e.target.value : formRegister.userPwd, 240 | userPwd2: type === 3 ? e.target.value : formRegister.userPwd2 241 | } 242 | }) 243 | } 244 | 245 | // 判断点击的键盘keyCode是否为13,是就调用登录函数 246 | handleEnterKey = (e: any, type: number) => { 247 | const { formLogin, formRegister } = this.state; 248 | if (type === 1) { 249 | if (!formLogin.userName || !formLogin.userPwd) { 250 | return; 251 | } else { 252 | if(e.nativeEvent.keyCode === 13){ //e.nativeEvent获取原生的事件对像 253 | this.handleLogin(); 254 | } 255 | } 256 | } else { 257 | if (!formRegister.userName || !formRegister.userPwd || !formRegister.userPwd2) { 258 | return; 259 | } else { 260 | if(e.nativeEvent.keyCode === 13){ //e.nativeEvent获取原生的事件对像 261 | this.handleRegister(); 262 | } 263 | } 264 | } 265 | } 266 | 267 | render () { 268 | const { formLogin, formRegister, typeView, checked } = this.state; 269 | return ( 270 | 271 |
272 |
273 | logo 274 | 后台管理模板 275 |
276 |
277 |
278 | this.handleTab(0) }>登录 279 | · 280 | this.handleTab(1) }>注册 281 |
282 | 283 | { typeView === 0 ? 284 |
285 |
286 | this.handleChangeInput(e, 1) } 291 | placeholder="请输入登录邮箱/手机号" 292 | /> 293 | this.handleChangeInput(e, 2) } 299 | onPressEnter={ (e: any) => this.handleEnterKey(e, 1) } 300 | placeholder="请输入登录密码" 301 | /> 302 |
303 | 304 |
305 | 306 | 记住我 307 | 308 | 忘记密码? 309 |
310 |
311 | : 312 |
313 |
314 | this.handleChangeRegister(e, 1) } 319 | placeholder="请输入注册邮箱/手机号" 320 | /> 321 | this.handleChangeRegister(e, 2) } 327 | placeholder="请输入密码" 328 | /> 329 | this.handleChangeRegister(e, 3) } 335 | onPressEnter={ (e: any) => this.handleEnterKey(e, 2) } 336 | placeholder="请再次确认密码" 337 | /> 338 |
339 | 340 |
341 | } 342 | 343 |
344 | 345 |
346 |
347 | ) 348 | } 349 | } 350 | 351 | 352 | export default withRouter(connect((state: any) => state.user, { login, register })(Login)) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "extends": "./paths.json", 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------