├── .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 |
99 |
100 |
101 |
404
102 |
对不起,您访问的页面不存在。
103 |
108 |
109 |
110 |
111 |
112 |
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 |
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 |
176 | );
177 |
178 | return (
179 |
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 |
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 |

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 |
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 |
--------------------------------------------------------------------------------