├── .gitignore
├── .npmrc
├── README.md
├── craco.config.js
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── assets
│ └── images
│ │ ├── 404.png
│ │ ├── bg1.jpg
│ │ ├── bg2.jpg
│ │ ├── bg3.jpg
│ │ ├── bg4.jpg
│ │ ├── default.png
│ │ └── logo.svg
├── components
│ ├── AsyncComponent
│ │ └── index.js
│ └── loading.js
├── index.js
├── mock
│ ├── task.js
│ └── user.js
├── routes
│ ├── components.js
│ └── index.js
├── store
│ ├── index.js
│ ├── reducers.js
│ └── sagas.js
├── style
│ ├── _variables.scss
│ └── index.scss
├── utils
│ ├── http.js
│ └── index.js
└── views
│ ├── Articles
│ ├── action.js
│ ├── api.js
│ ├── article.js
│ ├── category.js
│ ├── comment.js
│ ├── constants.js
│ ├── reducers.js
│ └── sagas.js
│ ├── Community
│ ├── action.js
│ ├── api.js
│ ├── constants.js
│ ├── message.js
│ ├── reducers.js
│ └── sagas.js
│ ├── Home
│ ├── constants.js
│ ├── index.js
│ └── style.module.scss
│ ├── Layout
│ ├── index.js
│ └── index.module.scss
│ ├── Login
│ ├── forget.js
│ ├── index.js
│ ├── login2.js
│ ├── style.module.scss
│ └── style2.scss
│ ├── NotFound
│ ├── 404.js
│ └── style.module.scss
│ ├── Setting
│ ├── basicInfo.js
│ ├── constants.js
│ ├── emailService.js
│ ├── modifyPassword.js
│ ├── style.module.scss
│ └── websiteSetting.js
│ ├── Test
│ └── sagas.js
│ └── User
│ ├── index.js
│ ├── role.js
│ ├── updateRole.js
│ └── updateUser.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /build
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | phantomjs_cdnurl=http://cnpmjs.org/downloads
2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
3 | registry=https://registry.npm.taobao.org
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | // const CracoLessPlugin = require('craco-less')
2 | const path = require('path')
3 |
4 | const resolve = pathUrl => path.join(__dirname, pathUrl)
5 |
6 | module.exports = {
7 | webpack: {
8 | alias: {
9 | '@': resolve('src'),
10 | '@assets': resolve('src/assets')
11 | },
12 | /**
13 | * 重写 webpack 任意配置
14 | * - configure 能够重写 webpack 相关的所有配置,但是,仍然推荐你优先阅读 craco 提供的快捷配置,把解决不了的配置放到 configure 里解决;
15 | * - 这里选择配置为函数,与直接定义 configure 对象方式互斥;
16 | */
17 | configure: (webpackConfig, {
18 | env, paths
19 | }) => {
20 | // paths.appPath='public'
21 | paths.appBuild = 'build'
22 | webpackConfig.output = {
23 | ...webpackConfig.output,
24 | // ...{
25 | // filename: whenDev(() => 'static/js/bundle.js', 'static/js/[name].js'),
26 | // chunkFilename: 'static/js/[name].js'
27 | // },
28 | path: path.resolve(__dirname, 'build'), // 修改输出文件目录
29 | publicPath: './'
30 | }
31 | return webpackConfig
32 | }
33 | },
34 | devServer: {
35 | port: 3002,
36 | hot: true
37 | },
38 | // plugins: [
39 | // {
40 | // plugin: CracoLessPlugin,
41 | // options: {
42 | // lessLoaderOptions: {
43 | // // modifyVars: { '@primary-color': '#1da57a' },
44 | // javascriptEnabled: true
45 | // }
46 | // }
47 | // }
48 | // ]
49 | }
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-antd-blog",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@craco/craco": "^5.6.4",
7 | "@testing-library/jest-dom": "^4.2.4",
8 | "@testing-library/react": "^9.3.2",
9 | "@testing-library/user-event": "^7.1.2",
10 | "antd": "^4.2.5",
11 | "axios": "^0.21.1",
12 | "craco-less": "^1.16.0",
13 | "echarts": "^4.8.0",
14 | "prop-types": "^15.7.2",
15 | "react": "^16.13.1",
16 | "react-dom": "^16.13.1",
17 | "react-loadable": "^5.5.0",
18 | "react-redux": "^7.2.1",
19 | "react-router-config": "^5.1.1",
20 | "react-router-dom": "^5.2.0",
21 | "react-scripts": "3.4.1",
22 | "redux": "^4.0.5",
23 | "redux-saga": "^1.1.3"
24 | },
25 | "scripts": {
26 | "start": "cross-env REACT_APP_ENV=mock craco start",
27 | "dev": "cross-env REACT_APP_ENV=development craco start",
28 | "build": "craco build",
29 | "test": "craco test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": "react-app"
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {
48 | "cross-env": "^7.0.2",
49 | "mockjs": "^1.1.0",
50 | "node-sass": "^4.14.1",
51 | "sass-loader": "^9.0.2"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React 后台管理demo
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/assets/images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/src/assets/images/404.png
--------------------------------------------------------------------------------
/src/assets/images/bg1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/src/assets/images/bg1.jpg
--------------------------------------------------------------------------------
/src/assets/images/bg2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/src/assets/images/bg2.jpg
--------------------------------------------------------------------------------
/src/assets/images/bg3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/src/assets/images/bg3.jpg
--------------------------------------------------------------------------------
/src/assets/images/bg4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/src/assets/images/bg4.jpg
--------------------------------------------------------------------------------
/src/assets/images/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuRookie/react-tutorials/889148602ee6b4d84a6b64f886be056392ca80ba/src/assets/images/default.png
--------------------------------------------------------------------------------
/src/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/components/AsyncComponent/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | const asyncComponent = (importComponent) => {
4 | return class extends Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {
8 | component: null
9 | }
10 | }
11 | componentDidMount() {
12 | const { history, route } = this.props
13 | const token = sessionStorage.getItem('token')
14 | if (route.requiredAuth && !token) {
15 | history.replace('/login')
16 | return
17 | }
18 | importComponent().then(cmp => {
19 | this.setState({
20 | component: cmp.default.WrappedComponent || cmp.default
21 | })
22 | })
23 | }
24 |
25 | render() {
26 | const C = this.state.component
27 | return C ? : null
28 | }
29 | }
30 | }
31 |
32 | export default asyncComponent
--------------------------------------------------------------------------------
/src/components/loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Spin } from 'antd'
3 |
4 | const loadingWrapper = {
5 | position: 'relative',
6 | height: '100vh'
7 | }
8 | const loadingSpin = {
9 | position: 'absolute',
10 | left: '50%',
11 | top: '45%',
12 | transform: 'translate(-50%, -45%)'
13 | }
14 |
15 | function Loading() {
16 | return (
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default Loading
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { HashRouter } from 'react-router-dom'
4 | import { renderRoutes } from "react-router-config"
5 |
6 | import { Provider } from 'react-redux'
7 |
8 | import { ConfigProvider } from 'antd'
9 | import zhCN from 'antd/es/locale/zh_CN'
10 | import http from '@/utils/http'
11 | import routes from '@/routes'
12 | import '@/style/index.scss'
13 |
14 | import configureStore from '@/store'
15 | import rootSaga from '@/store/sagas'
16 |
17 | const store = configureStore()
18 | rootSaga.map(saga => store.runSaga(saga))
19 |
20 | React.$http = http
21 |
22 | ReactDOM.render(
23 |
24 |
25 |
26 | {renderRoutes(routes)}
27 |
28 |
29 | ,
30 | document.getElementById('root')
31 | );
32 |
--------------------------------------------------------------------------------
/src/mock/task.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | function randomTaskData() {
4 | let user = []
5 | for (let i = 0; i < 7; i++) {
6 | let item = {
7 | key: Mock.mock('@id'),
8 | id: Mock.mock('@id'),
9 | taskName: Mock.mock('@cword(3, 5)'),
10 | taskTime: Mock.mock('@pick(["半小时", "一小时", "两小时", "三小时", "四小时", "五小时", "六小时"])'),
11 | taskStatus: Mock.mock('@pick(["已完成", "进行中", "未开始"])')
12 | }
13 | user.push(item)
14 | }
15 | return user
16 | }
17 |
18 | export const TaskData = randomTaskData
--------------------------------------------------------------------------------
/src/mock/user.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | function randomUserData() {
4 | let user = []
5 | for (let i = 0; i < 7; i++) {
6 | let item = {
7 | key: Mock.mock('@id'),
8 | id: Mock.mock('@id'),
9 | username: Mock.mock('@cname'),
10 | lastLoginTime: Mock.mock('@time("HH:mm")'),
11 | status: Mock.mock('@pick([0, 1])'),
12 | like: Mock.mock('@integer(15, 100)')
13 | }
14 | user.push(item)
15 | }
16 | return user
17 | }
18 |
19 | export const UserData = randomUserData
--------------------------------------------------------------------------------
/src/routes/components.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Loadable from 'react-loadable'
3 | import { Spin } from 'antd'
4 | import Loading from '@/components/loading'
5 |
6 | const Layout = Loadable({
7 | loader: () => import('@/views/Layout'),
8 | loading: Loading
9 | })
10 |
11 | const Login2 = Loadable({
12 | loader: () => import('@/views/Login'),
13 | loading() { return }
14 | })
15 |
16 | const Login = Loadable({
17 | loader: () => import('@/views/Login/login2'),
18 | loading: Loading
19 | })
20 |
21 | const Forget = Loadable({
22 | loader: () => import('@/views/Login/forget'),
23 | loading: Loading
24 | })
25 |
26 | const NotFound = Loadable({
27 | loader: () => import('@/views/NotFound/404'),
28 | loading: Loading
29 | })
30 |
31 | const Home = Loadable({
32 | loader: () => import('@/views/Home'),
33 | loading: Loading
34 | })
35 |
36 | const User = Loadable({
37 | loader: () => import('@/views/User'),
38 | loading: Loading
39 | })
40 |
41 | const Role = Loadable({
42 | loader: () => import('@/views/User/role'),
43 | loading: Loading
44 | })
45 |
46 | const WebsiteSetting = Loadable({
47 | loader: () => import('@/views/Setting/websiteSetting'),
48 | loading: Loading
49 | })
50 |
51 | const EmailService = Loadable({
52 | loader: () => import('@/views/Setting/emailService'),
53 | loading: Loading
54 | })
55 |
56 | const BasicInfo = Loadable({
57 | loader: () => import('@/views/Setting/basicInfo'),
58 | loading: Loading
59 | })
60 |
61 | const ModifyPassword = Loadable({
62 | loader: () => import('@/views/Setting/modifyPassword'),
63 | loading: Loading
64 | })
65 |
66 | const Message = Loadable({
67 | loader: () => import('@/views/Community/message'),
68 | loading: Loading
69 | })
70 |
71 | const Article = Loadable({
72 | loader: () => import('@/views/Articles/article'),
73 | loading: Loading
74 | })
75 |
76 | const Category = Loadable({
77 | loader: () => import('@/views/Articles/category'),
78 | loading: Loading
79 | })
80 |
81 | const Comment = Loadable({
82 | loader: () => import('@/views/Articles/comment'),
83 | loading: Loading
84 | })
85 |
86 | export default {
87 | Layout,
88 | Login2,
89 | Login,
90 | Forget,
91 | NotFound,
92 | Home,
93 | User,
94 | Role,
95 | WebsiteSetting,
96 | EmailService,
97 | BasicInfo,
98 | ModifyPassword,
99 | Message,
100 | Article,
101 | Category,
102 | Comment
103 | }
104 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 |
4 | import RouteComponents from './components'
5 | // import AsyncComponent from '@/components/AsyncComponent'
6 |
7 | function getToken() {
8 | return sessionStorage.getItem('token')
9 | }
10 |
11 | const routes = [
12 | {
13 | path: '/login2',
14 | requiredAuth: false,
15 | // component: AsyncComponent(() => import('@/views/Login'))
16 | component: RouteComponents.Login2
17 | },
18 | {
19 | path: '/login',
20 | requiredAuth: false,
21 | // component: AsyncComponent(() => import('@/views/Login/login2'))
22 | component: RouteComponents.Login
23 | },
24 | {
25 | path: '/forget',
26 | requiredAuth: false,
27 | // component: AsyncComponent(() => import('@/views/Login/forget'))
28 | component: RouteComponents.Forget
29 | },
30 | {
31 | path: '/404',
32 | requiredAuth: false,
33 | // component: AsyncComponent(() => import('@/views/NotFound/404'))
34 | component: RouteComponents.NotFound
35 | },
36 | {
37 | // component: AsyncComponent(() => import('@/views/Layout')),
38 | // component: Loadable({ loader: () => import('@/views/Layout'), loading() { return }}),
39 | render: (props) => {
40 | const token = getToken()
41 | if (!token) {
42 | return
43 | }
44 |
45 | return
46 | },
47 | requiredAuth: true,
48 | routes: [
49 | {
50 | path: '/',
51 | exact: true,
52 | render: () =>
53 | },
54 | {
55 | path: '/home',
56 | requiredAuth: true,
57 | // component: AsyncComponent(() => import('@/views/Home'))
58 | // component: Loadable({ loader: () => import('@/views/Home'), loading() { return }})
59 | component: RouteComponents.Home
60 | },
61 | {
62 | path: '/user',
63 | requiredAuth: true,
64 | // component: AsyncComponent(() => import('@/views/User'))
65 | component: RouteComponents.User
66 | },
67 | {
68 | path: '/roles',
69 | requiredAuth: true,
70 | // component: AsyncComponent(() => import('@/views/User/role'))
71 | component: RouteComponents.Role
72 | },
73 | {
74 | path: '/website-setting',
75 | requiredAuth: true,
76 | // component: AsyncComponent(() => import('@/views/Setting/websiteSetting'))
77 | component: RouteComponents.WebsiteSetting
78 | },
79 | {
80 | path: '/email-service',
81 | requiredAuth: true,
82 | // component: AsyncComponent(() => import('@/views/Setting/emailService'))
83 | component: RouteComponents.EmailService
84 | },
85 | {
86 | path: '/basic-info',
87 | requiredAuth: true,
88 | // component: AsyncComponent(() => import('@/views/Setting/basicInfo'))
89 | component: RouteComponents.BasicInfo
90 | },
91 | {
92 | path: '/modify-password',
93 | requiredAuth: true,
94 | // component: AsyncComponent(() => import('@/views/Setting/modifyPassword'))
95 | component: RouteComponents.ModifyPassword
96 | },
97 | {
98 | path: '/message',
99 | requiredAuth: true,
100 | // component: AsyncComponent(() => import('@/views/Community/message'))
101 | // component: Loadable({ loader: () => import('@/views/Community/message'), loading() { return }})
102 | component: RouteComponents.Message
103 | },
104 | {
105 | path: '/articles',
106 | component: RouteComponents.Article
107 | },
108 | {
109 | path: '/category',
110 | component: RouteComponents.Category
111 | },
112 | {
113 | path: '/comments',
114 | component: RouteComponents.Comment
115 | },
116 | {
117 | path: '*',
118 | render: () =>
119 | }
120 | ]
121 | },
122 | {
123 | path: '*',
124 | component: RouteComponents.NotFound
125 | }
126 | ]
127 |
128 | export default routes
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import createSagaMiddleware from 'redux-saga'
2 | import { createStore, applyMiddleware } from 'redux'
3 | import concatReducer from './reducers'
4 |
5 | export default function configureStore(initialState) {
6 | const sagaMiddleware = createSagaMiddleware()
7 | return {
8 | ...createStore(concatReducer, initialState, applyMiddleware(sagaMiddleware)),
9 | runSaga: sagaMiddleware.run
10 | }
11 | }
--------------------------------------------------------------------------------
/src/store/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import messageReducer from '@/views/Community/reducers'
4 | import articleReducer from '@/views/Articles/reducers'
5 |
6 | const concatReducers = combineReducers({
7 | messageReducer,
8 | articleReducer
9 | })
10 |
11 | export default concatReducers
--------------------------------------------------------------------------------
/src/store/sagas.js:
--------------------------------------------------------------------------------
1 | import messageSaga from '@/views/Community/sagas'
2 | import articleSaga from '@/views/Articles/sagas'
3 |
4 |
5 | export default [
6 | messageSaga,
7 | articleSaga
8 | ]
--------------------------------------------------------------------------------
/src/style/_variables.scss:
--------------------------------------------------------------------------------
1 | $colors: (
2 | "white": #fff,
3 | "primary": #1890ff,
4 | "light-grey": #eaeaea,
5 | "light-grey-1": #efefef,
6 | "light-red": #FF5722,
7 | "yellow": #FFB800,
8 | "green": #5FB878,
9 | "grey-9": #999,
10 | "grey-6": #666,
11 | "grey-3": #333,
12 | );
13 |
14 | $fontSizes: (
15 | '12': 12px,
16 | '14': 14px,
17 | '16': 16px,
18 | '18': 18px,
19 | '20': 20px,
20 | '24': 24px,
21 | '30': 30px,
22 | '36': 36px,
23 | );
24 |
25 | $spaces: (
26 | '5': 5px,
27 | '10': 10px,
28 | '15': 15px,
29 | '20': 20px,
30 | '25': 25px,
31 | '30': 30px,
32 | );
33 |
34 | $directions: (t:top, r:right, b:bottom, l:left);
--------------------------------------------------------------------------------
/src/style/index.scss:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 | @import './variables';
3 |
4 |
5 | #root {
6 | height: 100%;
7 | }
8 | // 样式初始化
9 | 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 { margin:0; padding:0; }
10 | h1, h2, h3, h4, h5, h6{ font-size:100%; }
11 | address, cite, dfn, em, var { font-style:normal; }
12 | code, kbd, pre, samp { font-family:couriernew, courier, monospace; }
13 | small{ font-size:12px; }
14 | ul, ol { list-style:none; }
15 | a { text-decoration:none; }
16 | a:hover { text-decoration: none; }
17 | sup { vertical-align:text-top; }
18 | sub{ vertical-align:text-bottom; }
19 | legend { color:#000; }
20 | fieldset, img { border:0; }
21 | button, input, select, textarea { font-size:100%; }
22 | table { border-collapse:collapse; border-spacing:0; }
23 |
24 | .w-100 {
25 | width: 100%;
26 | }
27 | .h-100 {
28 | height: 100%;
29 | }
30 | .d-flex {
31 | display: flex;
32 | }
33 | // color
34 | @each $colorKey, $colorValue in $colors {
35 | .text-#{$colorKey} {
36 | color: $colorValue;
37 | }
38 | .bg-#{$colorKey} {
39 | background-color: $colorValue;
40 | }
41 | }
42 |
43 | // font-size
44 | @each $font, $fontSize in $fontSizes {
45 | .fs-#{$font} {
46 | font-size: $fontSize;
47 | }
48 | }
49 |
50 | // text-align
51 | @each $direction, $value in $directions {
52 | .text-#{$value} {
53 | text-align: $value;
54 | }
55 | }
56 |
57 | // space
58 | @each $dKey, $dValue in $directions {
59 | @each $spaceKey, $spaceValue in $spaces {
60 | .m#{$dKey}-#{$spaceKey} {
61 | margin-#{$dValue}: $spaceValue;
62 | }
63 | .p#{$dKey}-#{$spaceKey} {
64 | padding-#{$dValue}: $spaceValue;
65 | }
66 | @if $dKey == 'l' {
67 | .mx-#{$spaceKey} {
68 | margin: 0 $spaceValue;
69 | }
70 | .px-#{$spaceKey} {
71 | padding: 0 $spaceValue;
72 | }
73 | }
74 | @if $dKey == 't' {
75 | .my-#{$spaceKey} {
76 | margin: $spaceValue 0;
77 | }
78 | .py-#{$spaceKey} {
79 | padding: $spaceValue 0;
80 | }
81 | }
82 | @if $dKey == 'r' {
83 | .m#{$spaceKey} {
84 | margin: $spaceValue;
85 | }
86 | .p#{$spaceKey} {
87 | padding: $spaceValue;
88 | }
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/src/utils/http.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { message } from 'antd'
3 | message.config({ maxCount: 3 })
4 |
5 | const MOCK_API = 'https://fastmock.site/mock/d451d648bf7d24e6bcfcd03fd9bfe605/react-tutorials/api/'
6 | const whiteApi = ['login', 'register', 'send/code', 'reset/password']
7 |
8 |
9 | const http = axios.create({
10 | baseURL: MOCK_API,
11 | // baseURL: process.env.REACT_APP_ENV === 'mock' ? MOCK_API : window.location.origin,
12 | // withCredentials: true,
13 | timeout: 1000 * 60 * 3
14 | })
15 |
16 | http.interceptors.request.use(function(config) {
17 | const { url } = config
18 | if (!whiteApi.includes(url)) {
19 | const token = window.sessionStorage.getItem('token')
20 | if (!token) {
21 | return Promise.reject({
22 | "code": 4002,
23 | "message": "为获取到令牌,请先登录",
24 | "data": null
25 | })
26 | }
27 | config.headers.Authorization = token
28 | }
29 | return config
30 | }, function(error) {
31 | return Promise.reject(error)
32 | })
33 |
34 | http.interceptors.response.use(function(response) {
35 | const data = response.data
36 | if (data.code === 200) {
37 | return data.data
38 | } else {
39 | message.error(data.message || data.desc)
40 | return Promise.reject(response)
41 | }
42 | }, function(error) {
43 | if (error.code === 4002) {
44 | message.error(error.message)
45 | window.location.href = '#/login'
46 | return Promise.reject(error)
47 | }
48 | return Promise.reject(error.response)
49 | })
50 |
51 | export default http
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | // 金额格式化
2 | export const formatCurrency = (val, n = 0) => {
3 | if (val) {
4 | const num = parseInt(val, 10)
5 | return (num.toFixed(n).toString()).replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, '$1,')
6 | }
7 | return '0.00'
8 | }
--------------------------------------------------------------------------------
/src/views/Articles/action.js:
--------------------------------------------------------------------------------
1 | import {
2 | ARTICLE_FETCH_LIST,
3 | ARTICLE_FETCH_EDIT,
4 | ARTICLE_FETCH_DELETE
5 | } from './constants'
6 |
7 | export function fetchArticleList(params, resolve) {
8 | return {
9 | type: ARTICLE_FETCH_LIST,
10 | payload: {
11 | params,
12 | resolve
13 | }
14 | }
15 | }
16 |
17 | export function fetchEditArticle(id, resolve) {
18 | return {
19 | type: ARTICLE_FETCH_EDIT,
20 | payload: {
21 | id,
22 | resolve
23 | }
24 | }
25 | }
26 |
27 | export function fetchDeleteArticle(id, resolve) {
28 | return {
29 | type: ARTICLE_FETCH_DELETE,
30 | payload: {
31 | id,
32 | resolve
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/views/Articles/api.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const requestArticleList = (params) => {
4 | const { $http } = React
5 | return $http.get('article/list', {params}).then(res => {
6 | return res
7 | })
8 | }
9 | export const requestArticleDelete = (params) => {
10 | const { $http } = React
11 | return $http.delete('article/delete', {data: params}).then(res => {
12 | return res
13 | })
14 | }
15 | export const requestArticleCreate= (params) => {
16 | const { $http } = React
17 | return $http.post('article/create', params).then(res => {
18 | return res
19 | })
20 | }
21 | export const requestArticleEdit = (params) => {
22 | const { $http } = React
23 | return $http.put('article/edit', params).then(res => {
24 | return res
25 | })
26 | }
--------------------------------------------------------------------------------
/src/views/Articles/article.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | import { Form, Row, Col, Input, Button, Select, Space, Card, Table, Tag, Modal, message } from 'antd'
5 | import {
6 | EditOutlined,
7 | PlusOutlined,
8 | DeleteOutlined,
9 | ExclamationCircleOutlined,
10 | ClearOutlined
11 | } from '@ant-design/icons'
12 |
13 | import {
14 | fetchArticleList,
15 | fetchDeleteArticle
16 | } from './action'
17 |
18 | const searchItem = [
19 | { key: 'author', name: 'author', label: '作者', defaultValue: null, type: 'input', placeholder: '请输入' },
20 | { key: 'title', name: 'title', label: '标题', defaultValue: null, type: 'input', placeholder: '请输入' },
21 | { key: 'tag', name: 'tag', label: '标签', defaultValue: -1, type: 'select', options: [
22 | { value: -1, label: '全部' },
23 | { value: 0, label: '美食' },
24 | { value: 1, label: '新闻'},
25 | { value: 2, label: '体育'},
26 | { value: 3, label: '音乐'},
27 | { value: 4, label: '八卦'}
28 | ], placeholder: '请选择' }
29 | ]
30 |
31 | const tags = {
32 | 0: '美食',
33 | 1: '新闻',
34 | 2: '体育',
35 | 3: '音乐',
36 | 4: '八卦',
37 | }
38 |
39 | class Article extends React.Component {
40 | constructor(props) {
41 | super(props)
42 | this.state = {
43 | loading: false,
44 | articleTableData: [],
45 | control: {
46 | 'edit': {
47 | name: '编辑',
48 | message: '编辑成功',
49 | fn: (params, callback) => callback
50 | },
51 | 'delete': {
52 | name: '删除',
53 | message: '删除成功',
54 | fn: props.onDeleteArticle
55 | },
56 | 'takeDown': {
57 | name: '下架',
58 | message: '下架成功',
59 | fn: this.onTakeDown
60 | }
61 | }
62 | }
63 | }
64 |
65 | formRef = React.createRef()
66 |
67 | columns = [
68 | { title: '文章标题', dataIndex: 'title' },
69 | { title: '文章标签', dataIndex: 'tag', render: (text) => {
70 | return {tags[text] || '未知'}
71 | }},
72 | { title: '文章作者', dataIndex: 'author' },
73 | { title: '创建时间', dataIndex: 'createDate' },
74 | { title: '发布状态', dataIndex: 'status', render: (text) => {
75 | const color = text === 0 ? 'green' : ''
76 | const txt = text === 0 ? '已发布' : '未发布'
77 | return (
78 | {txt}
79 | )
80 | }},
81 | { title: '操作', key: 'action', render: (text, record) => {
82 | return (
83 |
84 |
85 | {record.status === 1 ?
86 |
87 | :
88 | }
89 |
90 | )
91 | }}
92 | ]
93 |
94 | componentDidMount() {
95 | const { onLoadArticleList, pagination = {} } = this.props
96 | this.setState({loading: true})
97 | onLoadArticleList({
98 | page: pagination.current,
99 | pageSize: pagination.pageSize
100 | }, () => {
101 | this.setState({loading: false})
102 | })
103 | }
104 |
105 | onSearch = values => {
106 | const { onLoadArticleList, pagination } = this.props
107 | this.setState({loading: true})
108 | onLoadArticleList({
109 | ...values,
110 | page: pagination.current,
111 | pageSize: pagination.pageSize
112 | }, () => {
113 | this.setState({loading: false})
114 | })
115 | }
116 | onOpenModal = () => {
117 | message.error('此操作你暂无权限!')
118 | }
119 | onTakeDown = (record, callback) => {
120 | // 下架请求
121 | callback && callback()
122 | }
123 | onControl = (type, record) => {
124 | const { onLoadArticleList, pagination } = this.props
125 | const { control } = this.state
126 | if (type === 'edit') {
127 | return message.error('此操作你暂无权限!')
128 | }
129 | Modal.confirm({
130 | title: control[type]['name'] + '文章',
131 | icon: ,
132 | content: (确认{control[type]['name']}文章{record.title}吗?),
133 | onOk: () => {
134 | this.setState({loading: true})
135 | control[type].fn({id: record.id}, () => {
136 | message.success(control[type].message)
137 | const query = this.formRef.current.getFieldsValue()
138 | onLoadArticleList({
139 | ...query,
140 | page: pagination.current,
141 | pageSize: pagination.pageSize
142 | }, () => {
143 | this.setState({loading: false})
144 | })}
145 | )
146 | }
147 | })
148 |
149 | }
150 | paginationChange = (pagination) => {
151 | const { onLoadArticleList } = this.props
152 | this.setState({loading: true})
153 | const query = this.formRef.current.getFieldsValue()
154 | onLoadArticleList({
155 | ...query,
156 | page: pagination.current,
157 | pageSize: pagination.pageSize
158 | }, () => {
159 | this.setState({loading: false})
160 | })
161 | }
162 |
163 | render() {
164 | const { articleTableData, pagination } = this.props
165 | const { loading } = this.state;
166 | const getFields = () => {
167 | const children = searchItem.map(item => (
168 |
169 |
173 | {item.type === 'input' ?
174 | :
175 | }
180 |
181 |
182 | ))
183 | return children
184 | }
185 | const searchControl = (
186 |
187 |
188 |
189 |
190 |
191 |
192 | )
193 | return (
194 |
195 |
203 |
204 |
205 |
206 |
207 |
208 | )
209 | }
210 | }
211 |
212 | const mapStateToProps = (state) => {
213 | return {
214 | articleTableData: state.articleReducer.articleList,
215 | pagination: state.articleReducer.pagination
216 | }
217 | }
218 |
219 | const mapDispatchToProps = dispatch => {
220 | return {
221 | onLoadArticleList: (params, resolve) => dispatch(fetchArticleList(params, resolve)),
222 | onDeleteArticle: (params, resolve) => dispatch(fetchDeleteArticle(params, resolve)),
223 | }
224 | }
225 |
226 | export default connect(mapStateToProps, mapDispatchToProps)(Article)
--------------------------------------------------------------------------------
/src/views/Articles/category.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Form, Input, Button, Space, Card, Table, Modal, message } from 'antd'
4 | import {
5 | EditOutlined,
6 | PlusOutlined,
7 | DeleteOutlined,
8 | ExclamationCircleOutlined
9 | } from '@ant-design/icons'
10 |
11 | const { $http } = React
12 | const layout = {
13 | labelCol: {
14 | span: 4,
15 | },
16 | wrapperCol: {
17 | span: 16,
18 | },
19 | };
20 | const tailLayout = {
21 | wrapperCol: {
22 | offset: 4,
23 | span: 16,
24 | },
25 | };
26 |
27 | class Category extends React.Component {
28 | constructor(props) {
29 | super(props)
30 | this.state = {
31 | loading: false,
32 | categoryTableData: [],
33 | modalVisible: false,
34 | modalType: 'add',
35 | modalForm: {
36 | categoryName: '',
37 | categoryId: ''
38 | },
39 | modalLoading: false,
40 | }
41 | }
42 |
43 | formRef = React.createRef()
44 |
45 | columns = [
46 | { title: '分类名', dataIndex: 'categoryName' },
47 | { title: '创建时间', dataIndex: 'createDate' },
48 | { title: '操作', key: 'action', render: (text, record) => {
49 | return (
50 |
51 |
52 |
53 |
54 | )
55 | }}
56 | ]
57 |
58 | componentDidMount() {
59 | this.setState({loading: true})
60 | this.onLoadCategoryList()
61 | }
62 |
63 | onLoadCategoryList = () => {
64 | $http.get('category/list').then(res => {
65 | this.setState({ categoryTableData: res, loading: false })
66 | })
67 | }
68 |
69 | onOpenModal = () => {
70 | this.setState({
71 | modalType: 'add',
72 | modalVisible: true
73 | })
74 | }
75 | onEdit = (record) => {
76 | this.setState({
77 | modalType: 'edit',
78 | modalVisible: true,
79 | modalForm: {
80 | categoryId: record.categoryId,
81 | categoryName: record.categoryName
82 | }
83 | }, () => {
84 | this.formRef.current.setFieldsValue({
85 | categoryName: record.categoryName
86 | })
87 | })
88 | }
89 | onDelete = (record) => {
90 | Modal.confirm({
91 | title: '删除分类',
92 | icon: ,
93 | content: (确认删除分类{record.title}吗?),
94 | onOk: () => {
95 | this.setState({loading: true})
96 | $http.delete('category/delete', {data: {id: record.id}}).then(() => {
97 | message.success('删除成功')
98 | this.onLoadCategoryList()
99 | })
100 | }
101 | })
102 | }
103 | onSave = values => {
104 | const { modalType, modalForm } = this.state
105 | this.setState({ modalLoading: true })
106 | if (modalType === 'add') {
107 | $http.post('category/create', values).then(() => {
108 | message.success('添加成功')
109 | this.onCancel()
110 | this.onLoadCategoryList()
111 | })
112 | } else {
113 | values.categoryId = modalForm.categoryId
114 | $http.put('category/edit', values).then(() => {
115 | message.success('编辑成功')
116 | this.onCancel()
117 | this.onLoadCategoryList()
118 | })
119 | }
120 | }
121 | onCancel = () => {
122 | this.setState({
123 | modalType: 'add',
124 | modalVisible: false,
125 | modalLoading: false,
126 | modalForm: {
127 | categoryId: '',
128 | categoryName: ''
129 | }
130 | }, () => {
131 | this.formRef.current.setFieldsValue({
132 | categoryName: ''
133 | })
134 | })
135 | }
136 |
137 | render() {
138 | const { loading, categoryTableData, modalType, modalVisible, modalLoading } = this.state;
139 |
140 | return (
141 |
142 |
143 |
144 |
145 |
146 |
153 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | )
170 | }
171 | }
172 |
173 | export default Category
--------------------------------------------------------------------------------
/src/views/Articles/comment.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Form, Row, Col, Input, Select, Button, Space, Card, Table, Modal, message } from 'antd'
4 | import {
5 | EditOutlined,
6 | DeleteOutlined,
7 | ExclamationCircleOutlined
8 | } from '@ant-design/icons'
9 |
10 | const { $http } = React
11 | const layout = {
12 | labelCol: {
13 | span: 4,
14 | },
15 | wrapperCol: {
16 | span: 16,
17 | },
18 | };
19 | const tailLayout = {
20 | wrapperCol: {
21 | offset: 4,
22 | span: 16,
23 | },
24 | };
25 | const searchItem = [
26 | { key: 'commentator', name: 'commentator', label: '评论人', defaultValue: null, type: 'input', placeholder: '请输入' },
27 | { key: 'content', name: 'content', label: '评论内容', defaultValue: null, type: 'input', placeholder: '请输入' }
28 | ]
29 |
30 | class Comment extends React.Component {
31 | constructor(props) {
32 | super(props)
33 | this.state = {
34 | loading: false,
35 | commentTableData: [],
36 | pagination: {
37 | current: 1,
38 | pageSize: 10,
39 | total: 0
40 | },
41 | modalVisible: false,
42 | modalForm: {
43 | id: '',
44 | content: ''
45 | },
46 | modalLoading: false
47 | }
48 | }
49 |
50 | searchRef = React.createRef()
51 | formRef = React.createRef()
52 |
53 | columns = [
54 | { title: '评论人', dataIndex: 'commentator' },
55 | { title: '评论内容', dataIndex: 'content' },
56 | { title: '评论时间', dataIndex: 'createDate' },
57 | { title: '操作', key: 'action', render: (text, record) => {
58 | return (
59 |
60 |
61 |
62 |
63 | )
64 | }}
65 | ]
66 |
67 | componentDidMount() {
68 | this.setState({loading: true})
69 | this.onLoadCommentList()
70 | }
71 |
72 | onLoadCommentList = (params) => {
73 | const {pagination} = this.state
74 | const query = {
75 | ...params,
76 | page: pagination.current,
77 | pageSize: pagination.pageSize
78 | }
79 | $http.get('comment/list', {params: query}).then(res => {
80 | const {list, ...pagination} = res
81 | const {page, ...args} = pagination
82 | this.setState({
83 | commentTableData: list,
84 | pagination: {
85 | current: page,
86 | ...args
87 | },
88 | loading: false
89 | })
90 | })
91 | }
92 | onSearch = (values) => {
93 | this.setState({loading: true})
94 | this.onLoadCommentList(values)
95 | }
96 | onOpenModal = () => {
97 | this.setState({
98 | modalVisible: true
99 | })
100 | }
101 | paginationChange = (pagination) => {
102 | this.setState({
103 | loading: true,
104 | pagination: {
105 | page: pagination.page,
106 | pageSize: pagination.pageSize
107 | }
108 | }, () => {
109 | this.onLoadCommentList()
110 | })
111 | }
112 | onEdit = (record) => {
113 | this.setState({
114 | modalVisible: true,
115 | modalForm: {
116 | id: record.id,
117 | content: record.content
118 | }
119 | }, () => {
120 | this.formRef.current && this.formRef.current.setFieldsValue({
121 | id: record.id,
122 | content: record.content
123 | })
124 | })
125 | }
126 | onDelete = (record) => {
127 | Modal.confirm({
128 | title: '删除评论',
129 | icon: ,
130 | content: (确认删除此条评论吗?),
131 | onOk: () => {
132 | this.setState({loading: true})
133 | $http.delete('comment/delete', {data: {id: record.id}}).then(() => {
134 | message.success('删除成功')
135 | this.onLoadCommentList()
136 | })
137 | }
138 | })
139 | }
140 | onSave = values => {
141 | const { modalForm } = this.state
142 | this.setState({ modalLoading: true })
143 | values.id = modalForm.id
144 | $http.put('comment/edit', values).then(() => {
145 | message.success('编辑成功')
146 | this.onCancel()
147 | this.onLoadCommentList()
148 | })
149 | }
150 | onCancel = () => {
151 | this.setState({
152 | modalVisible: false,
153 | modalLoading: false,
154 | modalForm: {
155 | id: '',
156 | content: ''
157 | }
158 | }, () => {
159 | this.formRef.current.setFieldsValue({
160 | id: '',
161 | content: ''
162 | })
163 | })
164 | }
165 |
166 | render() {
167 | const { loading, commentTableData, pagination, modalVisible, modalForm, modalLoading } = this.state;
168 | const getFields = () => {
169 | const children = searchItem.map(item => (
170 |
171 |
175 | {item.type === 'input' ?
176 | :
177 | }
182 |
183 |
184 | ))
185 | return children
186 | }
187 | const searchControl = (
188 |
189 |
190 |
191 |
192 |
193 |
194 | )
195 | return (
196 |
197 |
205 |
206 |
212 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | )
226 | }
227 | }
228 |
229 | export default Comment
--------------------------------------------------------------------------------
/src/views/Articles/constants.js:
--------------------------------------------------------------------------------
1 | export const ARTICLE_FETCH_LIST = 'ARTICLE_FETCH_LIST'
2 | export const ARTICLE_FETCH_LIST_SUCCEEDED = 'ARTICLE_FETCH_LIST_SUCCEEDED'
3 | export const ARTICLE_FETCH_LIST_FAILED = 'ARTICLE_FETCH_LIST_FAILED'
4 | export const ARTICLE_FETCH_EDIT = 'ARTICLE_FETCH_EDIT'
5 | export const ARTICLE_FETCH_EDIT_SUCCEEDED = 'ARTICLE_FETCH_EDIT_SUCCEEDED'
6 | export const ARTICLE_FETCH_EDIT_FAILED = 'ARTICLE_FETCH_EDIT_FAILED'
7 | export const ARTICLE_FETCH_DELETE = 'ARTICLE_FETCH_DELETE'
8 | export const ARTICLE_FETCH_DELETE_SUCCEEDED = 'ARTICLE_FETCH_DELETE_SUCCEEDED'
9 | export const ARTICLE_FETCH_DELETE_FAILED = 'ARTICLE_FETCH_DELETE_FAILED'
--------------------------------------------------------------------------------
/src/views/Articles/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ARTICLE_FETCH_LIST_SUCCEEDED,
3 | ARTICLE_FETCH_LIST_FAILED,
4 | ARTICLE_FETCH_DELETE_SUCCEEDED,
5 | ARTICLE_FETCH_DELETE_FAILED
6 | } from './constants'
7 |
8 | const initialState = {
9 | articleList: [],
10 | pagination: {
11 | current: 1,
12 | pageSize: 10,
13 | total: 0
14 | }
15 | }
16 |
17 | export default (state = initialState, action) => {
18 | switch(action.type) {
19 | case ARTICLE_FETCH_LIST_SUCCEEDED:
20 | const { list, ...pagination } = action.result || {}
21 | const { page, ...args } = pagination
22 | return { ...state, articleList: list, pagination: {current: page, ...args}}
23 | case ARTICLE_FETCH_DELETE_SUCCEEDED:
24 | return state
25 | case ARTICLE_FETCH_LIST_FAILED:
26 | case ARTICLE_FETCH_DELETE_FAILED:
27 | return state
28 | default:
29 | return state
30 | }
31 | }
--------------------------------------------------------------------------------
/src/views/Articles/sagas.js:
--------------------------------------------------------------------------------
1 | import { put, call, takeEvery, all } from 'redux-saga/effects'
2 |
3 | import {
4 | requestArticleList,
5 | requestArticleDelete
6 | } from './api'
7 |
8 | import {
9 | ARTICLE_FETCH_LIST,
10 | ARTICLE_FETCH_LIST_SUCCEEDED,
11 | ARTICLE_FETCH_LIST_FAILED,
12 | ARTICLE_FETCH_DELETE,
13 | ARTICLE_FETCH_DELETE_SUCCEEDED,
14 | ARTICLE_FETCH_EDIT_FAILED
15 | } from './constants'
16 |
17 | function* loadArticleList({ payload }) {
18 | try {
19 | const result = yield call(requestArticleList, payload.params)
20 | yield put({ type: ARTICLE_FETCH_LIST_SUCCEEDED, result })
21 | payload.resolve()
22 | } catch (error) {
23 | yield put({ type: ARTICLE_FETCH_LIST_FAILED, message: error.message})
24 | }
25 | }
26 |
27 | function* deleteArticle({ payload }) {
28 | try {
29 | const result = yield call(requestArticleDelete, payload.id)
30 | yield put({ type: ARTICLE_FETCH_DELETE_SUCCEEDED, result })
31 | payload.resolve()
32 | } catch (error) {
33 | yield put({ type: ARTICLE_FETCH_EDIT_FAILED, message: error.message})
34 | }
35 | }
36 |
37 | const articleSaga = function* () {
38 | yield all([
39 | takeEvery(ARTICLE_FETCH_LIST, loadArticleList),
40 | takeEvery(ARTICLE_FETCH_DELETE, deleteArticle)
41 | ])
42 | }
43 |
44 | export default articleSaga
--------------------------------------------------------------------------------
/src/views/Community/action.js:
--------------------------------------------------------------------------------
1 | import {
2 | MESSAGE_FETCH_LIST,
3 | MESSAGE_FETCH_DELETE,
4 | MESSAGE_FETCH_STATUS
5 | } from './constants'
6 |
7 | export const fetchList = (params, resolve) => {
8 | return {
9 | type: MESSAGE_FETCH_LIST,
10 | payload: {
11 | params,
12 | resolve
13 | }
14 | }
15 | }
16 | export const fetchMessageDelete = (ids, resolve) => {
17 | return {
18 | type: MESSAGE_FETCH_DELETE,
19 | payload: {
20 | ids,
21 | resolve
22 | }
23 | }
24 | }
25 | export const fetchMessageStatus = (ids, resolve) => {
26 | return {
27 | type: MESSAGE_FETCH_STATUS,
28 | payload: {
29 | ids,
30 | status: 0,
31 | resolve
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/views/Community/api.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // 获取消息列表
4 | export const requestMessageList = (params) => {
5 | const { $http } = React
6 | return $http.get('message', {params}).then(res => {
7 | return res
8 | })
9 | }
10 | // 删除消息
11 | export const requestMessageDelete = (params) => {
12 | const { $http } = React
13 | return $http.delete('message/delete', {data: params}).then(res => {
14 | return res
15 | })
16 | }
17 | // 修改消息状态
18 | export const requestMessageStatus = (params) => {
19 | const { $http } = React
20 | return $http.put('message/read', params).then(res => {
21 | return res
22 | })
23 | }
--------------------------------------------------------------------------------
/src/views/Community/constants.js:
--------------------------------------------------------------------------------
1 | export const MESSAGE_FETCH_LIST = 'MESSAGE_FETCH_LIST'
2 | export const MESSAGE_FETCH_SUCCEEDED = 'MESSAGE_FETCH_SUCCEEDED'
3 | export const MESSAGE_FETCH_FAILED = 'MESSAGE_FETCH_FAILED'
4 | export const MESSAGE_FETCH_DELETE = 'MESSAGE_FETCH_DELETE'
5 | export const MESSAGE_FETCH_STATUS = 'MESSAGE_FETCH_STATUS'
6 | export const MESSAGE_FETCH_DELETE_FAILED = 'MESSAGE_FETCH_DELETE_FAILED'
7 | export const MESSAGE_FETCH_STATUS_FAILED = 'MESSAGE_FETCH_STATUS_FAILED'
--------------------------------------------------------------------------------
/src/views/Community/message.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { Space, Card, Table, Button, Row, Col, Select, Form, message } from 'antd'
4 |
5 | import {
6 | EditOutlined,
7 | DeleteOutlined,
8 | } from '@ant-design/icons'
9 | import {
10 | fetchList,
11 | fetchMessageDelete,
12 | fetchMessageStatus
13 | } from './action'
14 |
15 | const columns = [
16 | { title: '消息内容', dataIndex: 'content' },
17 | { title: '状态', dataIndex: 'status', render: (text) => {
18 | const styleClass = text === 0 ? 'text-green' : 'text-light-red'
19 | return (
20 | {text === 0 ? '已读' : '未读'}
21 | )
22 | }},
23 | { title: '时间', dataIndex: 'createDate'},
24 | ]
25 |
26 | class Message extends React.Component {
27 | constructor(props) {
28 | super(props)
29 | this.state = {
30 | status: -1,
31 | loading: true,
32 | selectedRowKeys: [],
33 | control: {
34 | delete: {
35 | message: '删除成功',
36 | fn: props.onMessageDelete
37 | },
38 | read: {
39 | message: '标记成功',
40 | fn: props.onMessageStatus
41 | }
42 | }
43 | }
44 | }
45 |
46 | formRef = React.createRef()
47 |
48 | componentDidMount() {
49 | const { onLoadMessageList, pagination = {} } = this.props
50 | onLoadMessageList({
51 | status: this.state.status,
52 | page: pagination.current,
53 | pageSize: pagination.pageSize
54 | }, () => {
55 | this.setState({loading: false})
56 | })
57 | }
58 | onSelectedChange = (selectedRowKeys) => {
59 | this.setState({ selectedRowKeys })
60 | }
61 | onSearch = values => {
62 | const { onLoadMessageList, pagination = {} } = this.props
63 | this.setState({loading: true})
64 | onLoadMessageList({
65 | status: values.status,
66 | page: pagination.current,
67 | pageSize: pagination.pageSize
68 | }, () => {
69 | this.setState({loading: false})
70 | })
71 | }
72 | paginationChange = (pagination) => {
73 | const { onLoadMessageList } = this.props
74 | this.setState({loading: true})
75 | onLoadMessageList({
76 | status: this.state.status,
77 | page: pagination.current,
78 | pageSize: pagination.pageSize
79 | }, () => {
80 | this.setState({loading: false})
81 | })
82 | }
83 | onControl = (type) => {
84 | const { selectedRowKeys, control, status } = this.state
85 | const { onLoadMessageList, pagination } = this.props
86 | if (!selectedRowKeys.length) {
87 | return message.error('请先选择消息!')
88 | }
89 | control[type].fn(selectedRowKeys, () => {
90 | message.success(control[type].message)
91 | this.setState({ selectedRowKeys: [] }, () => {
92 | this.setState({loading: true})
93 | onLoadMessageList({
94 | status: status,
95 | page: pagination.current,
96 | pageSize: pagination.pageSize
97 | }, () => {
98 | this.setState({loading: false})
99 | })}
100 | )
101 | })
102 | }
103 |
104 | render() {
105 | const { list, pagination } = this.props
106 | const { selectedRowKeys, loading, status } = this.state
107 | const rowSelection = {
108 | selectedRowKeys,
109 | onChange: this.onSelectedChange
110 | }
111 | return (
112 |
113 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | )
144 | }
145 | }
146 |
147 | function mapStateToProps(state) {
148 | return {
149 | list: state.messageReducer.list,
150 | pagination: state.messageReducer.pagination
151 | }
152 | }
153 |
154 | function mapDispatchToProps(dispatch) {
155 | return {
156 | onLoadMessageList: (query, resolve) => dispatch(fetchList(query, resolve)),
157 | onMessageDelete: (ids, resolve) => dispatch(fetchMessageDelete(ids, resolve)),
158 | onMessageStatus: (ids, resolve) => dispatch(fetchMessageStatus(ids, resolve)),
159 | }
160 | }
161 |
162 | export default connect(mapStateToProps, mapDispatchToProps)(Message)
--------------------------------------------------------------------------------
/src/views/Community/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | MESSAGE_FETCH_SUCCEEDED,
3 | MESSAGE_FETCH_FAILED
4 | } from './constants'
5 |
6 | const initialState = {
7 | list: [],
8 | pagination: {
9 | current: 1,
10 | pageSize: 10,
11 | total: 0
12 | }
13 | }
14 |
15 | export default (state = initialState, action) => {
16 | switch(action.type) {
17 | case MESSAGE_FETCH_SUCCEEDED:
18 | const { list, ...pagination } = action.result || {}
19 | const { page, ...args } = pagination
20 | return { ...state, list, pagination: {current: page, ...args}}
21 | case MESSAGE_FETCH_FAILED:
22 | return state
23 | default:
24 | return state
25 | }
26 | }
--------------------------------------------------------------------------------
/src/views/Community/sagas.js:
--------------------------------------------------------------------------------
1 | import { put, call, takeEvery, all } from 'redux-saga/effects'
2 | import {
3 | requestMessageList,
4 | requestMessageDelete,
5 | requestMessageStatus
6 | } from './api'
7 |
8 | import {
9 | MESSAGE_FETCH_LIST,
10 | MESSAGE_FETCH_SUCCEEDED,
11 | MESSAGE_FETCH_FAILED,
12 | MESSAGE_FETCH_DELETE,
13 | MESSAGE_FETCH_DELETE_FAILED,
14 | MESSAGE_FETCH_STATUS,
15 | MESSAGE_FETCH_STATUS_FAILED
16 | } from './constants'
17 |
18 | function* fetchMessageList({ payload }) {
19 | try {
20 | const result = yield call(requestMessageList, payload.params)
21 | yield put({ type: MESSAGE_FETCH_SUCCEEDED, result })
22 | payload.resolve()
23 | } catch (error) {
24 | yield put({ type: MESSAGE_FETCH_FAILED, message: error.message})
25 | }
26 | }
27 |
28 | function* deleteMessage({ payload }) {
29 | try {
30 | const { ids, resolve } = payload
31 | const result = yield call(requestMessageDelete, { ids })
32 | resolve(result)
33 | } catch (error) {
34 | yield put({ type: MESSAGE_FETCH_DELETE_FAILED, message: error.message})
35 | }
36 | }
37 |
38 | function* modifyMessageStatus({ payload }) {
39 | try {
40 | const { ids, status, resolve } = payload
41 | const result = yield call(requestMessageStatus, { ids, status })
42 | resolve(result)
43 | } catch (error) {
44 | yield put({ type: MESSAGE_FETCH_STATUS_FAILED, message: error.message})
45 | }
46 | }
47 |
48 | const messageSaga = function* () {
49 | yield all([
50 | takeEvery(MESSAGE_FETCH_LIST, fetchMessageList),
51 | takeEvery(MESSAGE_FETCH_DELETE, deleteMessage),
52 | takeEvery(MESSAGE_FETCH_STATUS, modifyMessageStatus),
53 | ])
54 | }
55 |
56 | export default messageSaga
--------------------------------------------------------------------------------
/src/views/Home/constants.js:
--------------------------------------------------------------------------------
1 | export const HEAD_BANNER = [
2 | {
3 | key: 'week',
4 | title: '访问量',
5 | tagColor: '#1E9FFF',
6 | tagValue: '周',
7 | currency: 0,
8 | aliasCurrency: 'visit',
9 | desc: '总计访问量',
10 | count: '3.2万',
11 | icon: 'Flag'
12 | },
13 | {
14 | key: 'month',
15 | title: '下载',
16 | tagColor: '#2F4056',
17 | tagValue: '月',
18 | currency: 0,
19 | aliasCurrency: 'download',
20 | desc: '新下载',
21 | count: '10%',
22 | icon: 'Smile'
23 | },
24 | {
25 | key: 'year',
26 | title: '收入',
27 | tagColor: '#009688',
28 | tagValue: '年',
29 | currency: 0,
30 | aliasCurrency: 'income',
31 | desc: '总收入',
32 | count: '***',
33 | icon: 'Pound'
34 | },
35 | {
36 | key: 'user',
37 | title: '活跃用户',
38 | tagColor: '#FFB800',
39 | tagValue: '月',
40 | currency: 0,
41 | aliasCurrency: 'activeUser',
42 | desc: '最近一个月',
43 | count: '15%',
44 | icon: 'User'
45 | }
46 | ]
47 |
48 | export const CHART_OPTION = {
49 | color: ['#009688', '#1f9fff', '#5eb878'],
50 | tooltip: {
51 | trigger: 'axis',
52 | axisPointer: {
53 | type: 'cross'
54 | }
55 | },
56 | legend: {
57 | data: ["访问量","下载量","平均访问量"]
58 | },
59 | grid: {
60 | left: '3%',
61 | right: '3%',
62 | bottom: '10%',
63 | containLabel: true
64 | },
65 | xAxis: [
66 | {
67 | type: 'category',
68 | boundaryGap: true,
69 | axisTick: {
70 | show: false,
71 | },
72 | axisLine: {
73 | lineStyle: {
74 | color: '#009688',
75 | width: 2
76 | }
77 | },
78 | splitLine: {
79 | show: true,
80 | lineStyle: {
81 | opacity: 0.3
82 | }
83 | },
84 | axisLabel: {
85 | color: '#333'
86 | },
87 | data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
88 | }
89 | ],
90 | yAxis: [
91 | {
92 | type: 'value',
93 | name: '访问量',
94 | min: 0,
95 | max: 1500,
96 | position: 'left',
97 | axisTick: {
98 | show: false,
99 | },
100 | axisLine: {
101 | lineStyle: {
102 | color: '#009688',
103 | width: 2
104 | }
105 | },
106 | splitLine: {
107 | show: true,
108 | lineStyle: {
109 | opacity: 0.3
110 | }
111 | },
112 | axisLabel: {
113 | color: '#333',
114 | formatter: '{value} 万'
115 | }
116 | },
117 | {
118 | type: 'value',
119 | name: '下载量',
120 | min: 0,
121 | max: 1250,
122 | position: 'right',
123 | axisTick: {
124 | show: false,
125 | },
126 | axisLine: {
127 | lineStyle: {
128 | color: '#009688',
129 | width: 2
130 | }
131 | },
132 | splitLine: {
133 | show: true,
134 | lineStyle: {
135 | opacity: 0.3
136 | }
137 | },
138 | axisLabel: {
139 | color: '#333',
140 | formatter: '{value} 万'
141 | }
142 | },
143 | ],
144 | series: [
145 | {
146 | name: '访问量',
147 | type: 'line',
148 | position: 'left',
149 | smooth: true,
150 | axisLabel: {
151 | formatter: '{value} ml'
152 | },
153 | data: [900, 850, 950, 1000, 1100, 1050, 1000, 1150, 1250, 1300, 1330, 1250]
154 | },
155 | {
156 | name: '下载量',
157 | type: 'line',
158 | position: 'right',
159 | smooth: true,
160 | data: [1000, 1050, 950, 1150, 1200, 1250, 1200, 1105, 1350, 1450, 1380, 1300]
161 | },
162 | {
163 | name: '平均访问量',
164 | type: 'line',
165 | smooth: true,
166 | data: [870, 850, 850, 950, 1050, 1050, 1000, 980, 1150, 1000, 1000, 1150]
167 | },
168 | ]
169 | }
--------------------------------------------------------------------------------
/src/views/Home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Card, Col, Row, Tag, Space, Progress, Radio, Table } from 'antd'
3 | import {
4 | FlagOutlined,
5 | SmileOutlined,
6 | PoundOutlined,
7 | UserOutlined,
8 | EllipsisOutlined,
9 | LikeOutlined,
10 | ClockCircleOutlined
11 | } from '@ant-design/icons';
12 |
13 | import Echarts from 'echarts'
14 | import 'echarts/lib/chart/line'
15 | import 'echarts/lib/component/tooltip'
16 | import 'echarts/lib/component/title'
17 |
18 | import { formatCurrency } from '@/utils'
19 | import style from './style.module.scss'
20 |
21 | import { HEAD_BANNER, CHART_OPTION } from './constants'
22 |
23 | const { $http } = React
24 | const UserColumns = [
25 | {
26 | title: '用户名',
27 | dataIndex: 'username',
28 | key: 'username',
29 | render: (text, record, index) => {
30 | switch(index) {
31 | case 0:
32 | return {text}
33 | case 1:
34 | return {text}
35 | case 2:
36 | return {text}
37 | default:
38 | return {text}
39 | }
40 | }
41 | },
42 | {
43 | title: '最后登录时间',
44 | dataIndex: 'lastLoginTime',
45 | key: 'lastLoginTime',
46 | render: (text) => {
47 | return (
48 |
49 |
50 | {text}
51 |
52 | )
53 | }
54 | },
55 | {
56 | title: '状态',
57 | dataIndex: 'status',
58 | key: 'status',
59 | render: (text, record) => {
60 | return (
61 |
62 | {record.status === 0 ? '离线' : '在线'}
63 |
64 | )
65 | }
66 | },
67 | {
68 | title: '获得赞',
69 | dataIndex: 'like',
70 | key: 'like',
71 | render: (text, record) => {
72 | return (
73 |
74 | {record.like}
75 |
76 |
77 | )
78 | }
79 | }
80 | ]
81 |
82 | const TaskColumns = [
83 | {
84 | title: '任务',
85 | dataIndex: 'taskName',
86 | key: 'taskName',
87 | render: (text, record, index) => {
88 | switch(index) {
89 | case 0:
90 | return {text}
91 | case 1:
92 | return {text}
93 | case 2:
94 | return {text}
95 | default:
96 | return {text}
97 | }
98 | }
99 | },
100 | {
101 | title: '所需时间',
102 | dataIndex: 'taskTime',
103 | key: 'taskTime',
104 | },
105 | {
106 | title: '完成状态',
107 | dataIndex: 'taskStatus',
108 | key: 'taskStatus',
109 | render: (text, record, index) => {
110 | const status = record.taskStatus === 0 ? '已完成' : record.taskStatus === 1 ? '进行中' : '未开始'
111 | switch(text) {
112 | case 0:
113 | return {status}
114 | case 1:
115 | return {status}
116 | case 2:
117 | return {status}
118 | default:
119 | return {status}
120 | }
121 | }
122 | },
123 | ]
124 |
125 | class Home extends React.Component {
126 | constructor() {
127 | super()
128 | this.state = {
129 | chart: null,
130 | date: '1',
131 | headerBanner: {
132 | visit: [0, '0万'],
133 | download: [0, '0%'],
134 | income: [0, '***'],
135 | activeUser: [0, '0%']
136 | },
137 | chartData: {},
138 | userTableData: [],
139 | taskTableData: []
140 | }
141 | }
142 |
143 | componentDidMount() {
144 | // 顶部统计
145 | $http.get('count').then(res => {
146 | this.setState({
147 | headerBanner: res
148 | })
149 | })
150 | // 图表
151 | this.chart = Echarts.init(document.getElementById('chart'))
152 | this.handleGetChart()
153 | // 表格
154 | $http.get('user/list').then(res => {
155 | const list = res.list.slice(0, 7)
156 | this.setState({userTableData: list})
157 | })
158 | $http.get('task/list').then(res => {
159 | const list = res.list.slice(0, 7)
160 | this.setState({taskTableData: list})
161 | })
162 | }
163 |
164 | showIcon = (type) => {
165 | switch(type) {
166 | case 'Flag':
167 | return
168 | case 'Smile':
169 | return
170 | case 'Pound':
171 | return
172 | case 'User':
173 | return
174 | default:
175 | return
176 | }
177 | }
178 |
179 | handleGetChart = () => {
180 | this.chart.showLoading({ color: '#5FB878'})
181 | $http.get('visit/chart').then(res => {
182 | this.chart.hideLoading()
183 | this.setState({ chartData: res })
184 | CHART_OPTION.series[0].data = res.visit
185 | CHART_OPTION.series[1].data = res.download
186 | CHART_OPTION.series[2].data = res.averageVisits
187 |
188 | this.chart.setOption(CHART_OPTION)
189 | }).catch(() => {
190 | this.chart.hideLoading()
191 | })
192 | }
193 | handleYearChange = (e) => {
194 | this.setState({date: e.target.value})
195 | this.handleGetChart()
196 | }
197 |
198 | render() {
199 | const { headerBanner, chartData, userTableData, taskTableData } = this.state
200 | return (
201 |
202 |
203 |
204 | {HEAD_BANNER.map(item => (
205 |
206 | {item.tagValue}}>
207 | {formatCurrency(headerBanner[item.aliasCurrency][0])}
208 |
209 |
210 | {item.desc}
211 |
212 |
213 |
214 | {headerBanner[item.aliasCurrency][1]}
215 | {this.showIcon(item.icon)}
216 |
217 |
218 |
219 |
220 |
221 | ))}
222 |
223 |
224 |
225 |
227 | 今年
228 | 去年
229 |
230 | }>
231 |
232 |
233 |
234 |
235 |
236 |
237 |
月访问数
238 |
同上期增长
239 |
240 |
241 |
242 |
月下载数
243 |
同上期增长
244 |
245 |
246 |
247 |
月收入
248 |
同上期增长
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 | )
271 | }
272 | }
273 |
274 | export default Home
--------------------------------------------------------------------------------
/src/views/Home/style.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../style//variables';
2 |
3 | .page-home {
4 |
5 |
6 | :global {
7 |
8 | .ant-card-head {
9 | padding: 0 15px;
10 | min-height: 42px;
11 | font-size: map-get($fontSizes, '14');
12 | }
13 | .ant-card-head-title {
14 | padding: 10px 0;
15 | }
16 | .ant-card-extra {
17 | padding: 10px 0;
18 | font-size: map-get($fontSizes, '12');
19 | }
20 | .ant-tag {
21 | margin-right: 0;
22 | transform: scale(0.9);
23 | }
24 | .ant-card-body {
25 | padding: 15px;
26 | }
27 | }
28 |
29 | .count {
30 | font-size: map-get($fontSizes, '36');
31 | color: #666;
32 | line-height: 36px;
33 | padding: 5px 0 10px;
34 | overflow: hidden;
35 | text-overflow: ellipsis;
36 | word-break: break-all;
37 | white-space: nowrap;
38 | }
39 |
40 | .chart-title {
41 | line-height: map-get($spaces, '20');
42 | }
43 | .table-border {
44 | border: 1px solid map-get($colors, 'light-grey-1');;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/views/Layout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { HashRouter } from 'react-router-dom'
3 | import { renderRoutes } from 'react-router-config'
4 |
5 | import { Layout, Menu, Breadcrumb, Avatar, Dropdown, Button } from 'antd'
6 | import {
7 | MenuUnfoldOutlined,
8 | MenuFoldOutlined,
9 | UserOutlined,
10 | HomeOutlined,
11 | SettingOutlined,
12 | CodepenCircleOutlined,
13 | DownOutlined,
14 | ReadOutlined,
15 | FileTextOutlined
16 | } from '@ant-design/icons';
17 | const { Header, Sider, Content } = Layout
18 | const { $http } = React
19 | const style = require('./index.module.scss')
20 |
21 | const mapMenu = [
22 | { key: 'home', name: '首页' },
23 | { key: 'user', name: '用户列表', parentName: '用户管理', parentKey: 'user-menu' },
24 | { key: 'roles', name: '角色管理', parentName: '用户管理', parentKey: 'user-menu' },
25 | { key: 'articles', name: '文章列表', parentName: '文章管理', parentKey: 'article-menu' },
26 | { key: 'category', name: '文章分类', parentName: '文章管理', parentKey: 'article-menu' },
27 | { key: 'comments', name: '文章评论', parentName: '文章管理', parentKey: 'article-menu' },
28 | { key: 'message', name: '消息中心', parentName: '社区管理', parentKey: 'community' },
29 | { key: 'website-setting', name: '网站设置', parentName: '设置管理', parentKey: 'setting-menu' },
30 | { key: 'email-service', name: '邮件服务', parentName: '设置管理', parentKey: 'setting-menu' },
31 | { key: 'basic-info', name: '基本资料', parentName: '设置管理', parentKey: 'setting-menu' },
32 | { key: 'modify-password', name: '修改密码', parentName: '设置管理', parentKey: 'setting-menu' },
33 | ]
34 |
35 | class LayoutContainer extends React.Component {
36 | constructor(props) {
37 | super(props)
38 | const currentPath = this.splitPath()
39 | const openKeys = this.handleFindOpenMenu(currentPath)
40 | const userInfo = JSON.parse(sessionStorage.getItem('userInfo')) || {}
41 | this.state = {
42 | collapsed: false,
43 | title: '后台管理系统',
44 | selectedKeys: [currentPath],
45 | defaultOpenKeys: [openKeys.menuKey],
46 | breadcrumb: openKeys.breadcrumb,
47 | userInfo
48 | }
49 | }
50 |
51 | splitPath = () => {
52 | const { location } = this.props
53 | return location.pathname.substr(1)
54 | }
55 | toggle = () => {
56 | this.setState({
57 | collapsed: !this.state.collapsed,
58 | title: this.state.collapsed ? '后台管理系统' : ''
59 | })
60 | }
61 | handleFindOpenMenu = (selectedKeys) => {
62 | const findMenu = mapMenu.find(subMenu => subMenu.key === selectedKeys)
63 | let breadcrumb = []
64 | breadcrumb.push(findMenu.parentName, findMenu.name)
65 | breadcrumb = breadcrumb.filter(v => v)
66 | return {
67 | menuKey: findMenu && findMenu.parentKey,
68 | subMenu: findMenu && findMenu.key,
69 | breadcrumb
70 | }
71 | }
72 | handleMenuClick = (item) => {
73 | const { history } = this.props
74 | const { key } = item
75 | if (key === 'logout') {
76 | $http.get('logout').then(() => {
77 | sessionStorage.clear()
78 | history.push('/login')
79 | })
80 | } else {
81 | this.setState({
82 | selectedKeys: [item.key]
83 | }, () => {
84 | history.push(item.key)
85 | })
86 | }
87 | }
88 | handleRouter = (item) => {
89 | const { history } = this.props
90 | const findMenu = mapMenu.find(subMenu => subMenu.key === item.key)
91 | let breadcrumb = []
92 | breadcrumb.push(findMenu.parentName, findMenu.name)
93 | breadcrumb = breadcrumb.filter(v => v)
94 | this.setState({
95 | selectedKeys: [item.key],
96 | breadcrumb
97 | }, () => {
98 | history.push(item.key)
99 | })
100 | }
101 |
102 | render() {
103 | const { route } = this.props
104 | const { collapsed, title, selectedKeys, userInfo, defaultOpenKeys, breadcrumb } = this.state
105 |
106 | const userDropdownMenu = (
107 |
113 | )
114 | return (
115 |
116 |
117 |
118 |
119 |
120 | {title}
121 |
122 |
184 |
185 |
186 |
187 | {React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
188 | className: style['trigger'],
189 | onClick: this.toggle,
190 | })}
191 |
192 |
193 |
194 |
document.getElementsByClassName('info')[0]}>
197 |
200 |
201 |
202 |
203 |
204 |
205 |
206 | {breadcrumb.map((item, index) => {item})}
207 |
208 |
209 | {renderRoutes(route.routes)}
210 |
211 |
212 |
213 |
214 |
215 | )
216 | }
217 | }
218 |
219 | export default LayoutContainer
220 |
--------------------------------------------------------------------------------
/src/views/Layout/index.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../style//variables';
2 |
3 | .layout-container {
4 | height: 100%;
5 |
6 | .logo {
7 | margin: 16px;
8 | height: 32px;
9 | line-height: 32px;
10 | color: map-get($colors, 'white');
11 |
12 | .icon {
13 | vertical-align: middle;
14 | font-size: map-get($fontSizes, '30');
15 | margin-right: 5px;
16 | }
17 | }
18 | .trigger {
19 | font-size: map-get($fontSizes, '18');;
20 | line-height: 64px;
21 | padding: 0 24px;
22 | cursor: pointer;
23 | transition: color 0.3s;
24 |
25 | &:hover {
26 | color: #1890ff;
27 | }
28 | }
29 |
30 | .header-right {
31 | float: right;
32 | position: relative;
33 | }
34 | .btn-user {
35 | padding: 4px 8px;
36 | }
37 | .layout-header {
38 | padding: 0;
39 | border: 1px solid map-get($colors, 'light-grey');
40 | }
41 | .layout-nav {
42 | height: 50px;
43 | line-height: 50px;
44 | padding: 0 16px;
45 | background-color: map-get($colors, 'white');
46 | box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
47 | }
48 | .layout-content--info {
49 | padding: 15px;
50 | min-height: calc(100% - 86px);
51 | }
52 | }
--------------------------------------------------------------------------------
/src/views/Login/forget.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import { message } from 'antd'
5 | import './style2.scss'
6 |
7 | const { $http } = React
8 | const PhoneRegexp = /^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/
9 |
10 | class Forget extends React.Component {
11 | constructor(){
12 | super()
13 | this.state = {
14 | validateForm: {
15 | phone: '',
16 | imgCode: '',
17 | code: ''
18 | },
19 | form: {
20 | password: '',
21 | repeatPassword: ''
22 | },
23 | formType: 'validate',
24 | codeText: '获取验证码',
25 | disabled: false
26 | }
27 | }
28 |
29 | handleNext = (event) => {
30 | event.preventDefault()
31 | const { validateForm } = this.state
32 | if (!validateForm.phone) {
33 | return message.error('请输入手机号码')
34 | }
35 | if (!validateForm.imgCode) {
36 | return message.error('请输入图形验证码')
37 | }
38 | if (!validateForm.code) {
39 | return message.error('请输入验证码')
40 | }
41 | this.setState({
42 | formType: 'password',
43 | validateForm: {
44 | ...validateForm,
45 | code: ''
46 | }
47 | })
48 | }
49 | handlePrev = (event) => {
50 | event.preventDefault()
51 | this.setState({
52 | formType: 'validate',
53 | form: {
54 | password: '',
55 | repeatPassword: ''
56 | }
57 | })
58 | }
59 | handleValidate = (e) => {
60 | const value = e.target.value
61 | if (value && !PhoneRegexp.test(value)) {
62 | message.error('手机号码格式不正确')
63 | }
64 | }
65 | handleInputChange = (event, formType, name) => {
66 | const { validateForm, form } = this.state
67 | const value = event.target.value
68 | if (formType === 'validate') {
69 | validateForm[name] = value
70 | this.setState({ validateForm })
71 | } else {
72 | form[name] = value
73 | this.setState({ form })
74 | }
75 | }
76 | // 验证码已发送到手机,请注意查收
77 | handleGetCode = () => {
78 | if (!this.state.validateForm.phone || !PhoneRegexp.test(this.state.validateForm.phone)) {
79 | return message.error('请输入正确手机号码')
80 | }
81 | let s = 59
82 | let timer = null
83 | message.success('验证码已发送到手机,请注意查收')
84 | this.setState({
85 | disabled: true,
86 | codeText: `${s}秒后重新获取`
87 | }, () => {
88 | timer = setInterval(() => {
89 | s -= 1
90 | this.setState({
91 | codeText: `${s}秒后重新获取`
92 | })
93 | if (s === 0) {
94 | clearInterval(timer)
95 | this.setState({
96 | codeText: '获取验证码',
97 | disabled: false
98 | })
99 | }
100 | }, 1000)
101 | })
102 | // 获取验证码
103 | $http.get('send/code', { params: {phone: this.state.validateForm.phone}}).then(res => {
104 | clearInterval(timer)
105 | this.setState({
106 | validateForm: {...this.state.validateForm, code: res.code},
107 | codeText: '获取验证码',
108 | disabled: false
109 | })
110 | })
111 | }
112 | handleResetPassword = (event) => {
113 | event.preventDefault()
114 | const { form } = this.state
115 | if (!form.password) {
116 | return message.error('请输入密码')
117 | }
118 | if (!form.repeatPassword) {
119 | return message.error('请输入确认密码')
120 | }
121 | $http.post('reset/password', {form}).then(() => {
122 | message.success('密码重置成功,请到重新登录账号')
123 | setTimeout(() => {
124 | this.props.history.replace('/login')
125 | }, 1500)
126 | })
127 | }
128 |
129 | render() {
130 | const { formType, validateForm, codeText, disabled, form } = this.state
131 | return (
132 |
133 |
134 |
150 |
151 |
158 |
159 |
160 |
161 | )
162 | }
163 | }
164 |
165 | export default Forget
--------------------------------------------------------------------------------
/src/views/Login/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Card, Form, Input, Button, Space, message } from 'antd'
3 |
4 | import style from './style.module.scss'
5 |
6 | const { $http } = React
7 | const layout = {
8 | labelCol: { span: 4 },
9 | wrapperCol: { span: 16 }
10 | }
11 |
12 | const footerLayout = {
13 | wrapperCol: {
14 | offset: 4,
15 | span: 16
16 | }
17 | }
18 |
19 | class Login extends React.Component {
20 | constructor() {
21 | super()
22 | this.state = {
23 | form: {
24 | username: '',
25 | password: '',
26 | validateCode: ''
27 | }
28 | }
29 | }
30 |
31 | handleSubmit = (values) => {
32 | $http.post('login', values).then(res => {
33 | const { token, ...user } = res
34 | sessionStorage.setItem('token', res.token)
35 | sessionStorage.setItem('userInfo', JSON.stringify(user))
36 | message.success('登录成功')
37 | this.props.history.push('/home')
38 | })
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 | ({
49 | validator(rule, value) {
50 | if (!value) {
51 | return Promise.reject('请输入用户名')
52 | }
53 | return Promise.resolve()
54 | }
55 | })
56 | ]}>
57 |
58 |
59 | ({
62 | validator(rule, value) {
63 | if (!value) {
64 | return Promise.reject('请输入密码')
65 | }
66 | return Promise.resolve()
67 | }
68 | })
69 | ]}>
70 |
71 |
72 | ({
75 | validator(rule, value) {
76 | if (!value) {
77 | return Promise.reject('请输入验证码')
78 | }
79 | return Promise.resolve()
80 | }
81 | })
82 | ]}>
83 |
85 | } />
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | )
96 | }
97 | }
98 |
99 | export default Login
--------------------------------------------------------------------------------
/src/views/Login/login2.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import { message, notification } from 'antd'
5 | import {
6 | LoadingOutlined
7 | } from '@ant-design/icons';
8 | import './style2.scss'
9 |
10 | const { $http } = React
11 |
12 | class Login2 extends React.Component {
13 | constructor() {
14 | super()
15 | this.state = {
16 | loginType: true,
17 | loading: false,
18 | loginForm: {
19 | username: '',
20 | password: ''
21 | },
22 | registerForm: {
23 | username: '',
24 | email: '',
25 | password: ''
26 | }
27 | }
28 | }
29 |
30 | toggleClass = () => {
31 | this.setState((state) => {
32 | if (state.loginType) {
33 | return {
34 | loginType: state.loginType,
35 | registerForm: {
36 | username: '',
37 | email: '',
38 | password: ''
39 | }
40 | }
41 | }
42 | return { loginType: state.loginType }
43 | })
44 | this.setState({ loginType: !this.state.loginType })
45 | }
46 | handleInputChange = (event, formType, labelName) => {
47 | const { loginForm, registerForm } = this.state
48 | if (formType === 'register') {
49 | registerForm[labelName] = event.target.value
50 | this.setState({ registerForm })
51 | } else {
52 | loginForm[labelName] = event.target.value
53 | this.setState({
54 | loginForm,
55 | registerForm: {
56 | username: '',
57 | email: '',
58 | password: ''
59 | }
60 | })
61 | }
62 | }
63 | handleLogin = (event) => {
64 | event.preventDefault()
65 | const { loginForm } = this.state
66 | if (!loginForm.username) {
67 | return message.error('用户名不能为空')
68 | }
69 | if (!loginForm.password) {
70 | return message.error('密码不能为空')
71 | }
72 | this.setState({loading: true})
73 | $http.post('login', { ...loginForm }).then((res) => {
74 | const { token, ...user } = res
75 | sessionStorage.setItem('token', res.token)
76 | sessionStorage.setItem('userInfo', JSON.stringify(user))
77 | message.success('登录成功')
78 | this.setState({loading: false})
79 | this.props.history.push('/home')
80 | })
81 | }
82 | handleRegister = (event) => {
83 | event.preventDefault()
84 | const { registerForm } = this.state
85 | if (!registerForm.username) {
86 | return message.error('用户名不能为空')
87 | }
88 | if (!registerForm.email) {
89 | return message.error('邮箱不能为空')
90 | }
91 | if (!registerForm.password) {
92 | return message.error('密码不能为空')
93 | }
94 | $http.post('register', { ...registerForm }).then(() => {
95 | notification.success({
96 | key: 'register',
97 | message: '注册成功!',
98 | description: '账号注册成功,正在为你登录...',
99 | })
100 | setTimeout(() => {
101 | // 注册成功后,直接登录
102 | $http.post('login', {
103 | username: registerForm.username,
104 | password: registerForm.password
105 | }).then((res) => {
106 | notification.close('register')
107 | const { token, ...user } = res
108 | sessionStorage.setItem('token', res.token)
109 | sessionStorage.setItem('userInfo', JSON.stringify(user))
110 | message.success('登录成功')
111 | this.props.history.push('/home')
112 | })
113 | }, 2000)
114 | })
115 | }
116 |
117 | render() {
118 | const { loginType, registerForm, loginForm, loading } = this.state
119 | const activeClass = !loginType ? 'right-panel-active' : ''
120 | return (
121 |
122 |
123 |
124 |
131 |
132 |
133 |
142 |
143 |
144 |
145 |
146 |
欢迎回来!
147 |
请您先登录的个人信息,进行操作。
148 |
149 |
150 |
151 |
注册新账号!
152 |
输入您的个人信息注册账号。
153 |
154 |
155 |
156 |
157 |
158 |
159 | )
160 | }
161 | }
162 |
163 | export default Login2
--------------------------------------------------------------------------------
/src/views/Login/style.module.scss:
--------------------------------------------------------------------------------
1 | .page-login {
2 | position: relative;
3 | height: 100%;
4 | background-color: #f9f9f9;
5 |
6 | .login-wrapper {
7 | width: 500px;
8 | position: absolute;
9 | top: 40%;
10 | left: 50%;
11 | transform: translate(-50%, -50%);
12 | box-shadow: 0px 5px 10px 0px #eaeaea;
13 | }
14 |
15 | :global(.ant-input-group-addon) {
16 | padding: 0;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/views/Login/style2.scss:
--------------------------------------------------------------------------------
1 | .login-wrapper {
2 | background: #f6f5f7;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | flex-direction: column;
7 | font-family: 'Montserrat', sans-serif;
8 | height: 100vh;
9 | margin: -20px 0 50px;
10 |
11 | h1 {
12 | font-size: 2em;
13 | font-weight: bold;
14 | }
15 |
16 | h2 {
17 | text-align: center;
18 | }
19 |
20 | p {
21 | font-size: 14px;
22 | font-weight: 100;
23 | line-height: 20px;
24 | letter-spacing: 0.5px;
25 | margin: 20px 0 30px;
26 | }
27 |
28 | span {
29 | font-size: 12px;
30 | }
31 |
32 | a {
33 | color: #333;
34 | font-size: 14px;
35 | /* text-decoration: none; */
36 | margin: 15px 0;
37 | }
38 |
39 | button {
40 | border-radius: 20px;
41 | border: 1px solid #eee;
42 | background-color: #fff;
43 | color: #333;
44 | font-size: 12px;
45 | font-weight: bold;
46 | padding: 12px 45px;
47 | letter-spacing: 1px;
48 | text-transform: uppercase;
49 | transition: transform 80ms ease-in;
50 | cursor: pointer;
51 | }
52 | button[data-type="primary"] {
53 | border: 1px solid #1890ff;
54 | background-color: #1890ff;
55 | color: #FFFFFF;
56 | }
57 |
58 | button:active {
59 | transform: scale(0.95);
60 | }
61 |
62 | button:focus {
63 | outline: none;
64 | }
65 |
66 | button.ghost {
67 | background-color: transparent;
68 | border-color: #FFFFFF;
69 | }
70 |
71 | form {
72 | background-color: #FFFFFF;
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | flex-direction: column;
77 | padding: 0 50px;
78 | height: 100%;
79 | text-align: center;
80 | }
81 |
82 | input {
83 | background-color: #eee;
84 | border: none;
85 | padding: 12px 15px;
86 | margin: 8px 0;
87 | width: 100%;
88 | }
89 |
90 | .container {
91 | background-color: #fff;
92 | border-radius: 10px;
93 | box-shadow: 0 14px 28px rgba(0,0,0,0.25),
94 | 0 10px 10px rgba(0,0,0,0.22);
95 | position: relative;
96 | overflow: hidden;
97 | width: 768px;
98 | max-width: 100%;
99 | min-height: 480px;
100 | }
101 |
102 | .form-container {
103 | position: absolute;
104 | top: 0;
105 | height: 100%;
106 | transition: all 0.6s ease-in-out;
107 | }
108 |
109 | .sign-in-container {
110 | left: 0;
111 | width: 50%;
112 | z-index: 2;
113 | }
114 |
115 | .container.right-panel-active .sign-in-container {
116 | transform: translateX(100%);
117 | }
118 |
119 | .sign-up-container {
120 | left: 0;
121 | width: 50%;
122 | opacity: 0;
123 | z-index: 1;
124 | }
125 |
126 | .container.right-panel-active .sign-up-container {
127 | transform: translateX(100%);
128 | opacity: 1;
129 | z-index: 5;
130 | animation: show 0.6s;
131 | }
132 |
133 | @keyframes show {
134 | 0%, 49.99% {
135 | opacity: 0;
136 | z-index: 1;
137 | }
138 |
139 | 50%, 100% {
140 | opacity: 1;
141 | z-index: 5;
142 | }
143 | }
144 |
145 | .overlay-container {
146 | position: absolute;
147 | top: 0;
148 | left: 50%;
149 | width: 50%;
150 | height: 100%;
151 | overflow: hidden;
152 | transition: transform 0.6s ease-in-out;
153 | z-index: 100;
154 | }
155 |
156 | .container.right-panel-active .overlay-container{
157 | transform: translateX(-100%);
158 | }
159 |
160 | .overlay {
161 | background: #369eff;
162 | background: -webkit-linear-gradient(to right, #1890ff, #369eff);
163 | background: linear-gradient(to right, #1890ff, #369eff);
164 | background-repeat: no-repeat;
165 | background-size: cover;
166 | background-position: 0 0;
167 | color: #FFFFFF;
168 | position: relative;
169 | left: -100%;
170 | height: 100%;
171 | width: 200%;
172 | transform: translateX(0);
173 | transition: transform 0.6s ease-in-out;
174 | }
175 |
176 | .container.right-panel-active .overlay {
177 | transform: translateX(50%);
178 | }
179 |
180 | .overlay-panel {
181 | position: absolute;
182 | display: flex;
183 | align-items: center;
184 | justify-content: center;
185 | flex-direction: column;
186 | padding: 0 40px;
187 | text-align: center;
188 | top: 0;
189 | height: 100%;
190 | width: 50%;
191 | transform: translateX(0);
192 | transition: transform 0.6s ease-in-out;
193 | }
194 |
195 | .overlay-left {
196 | transform: translateX(-20%);
197 | }
198 |
199 | .container.right-panel-active .overlay-left {
200 | transform: translateX(0);
201 | }
202 |
203 | .overlay-right {
204 | right: 0;
205 | transform: translateX(0);
206 | }
207 |
208 | .container.right-panel-active .overlay-right {
209 | transform: translateX(20%);
210 | }
211 |
212 | .social-container {
213 | margin: 20px 0;
214 | }
215 |
216 | .social-container a {
217 | border: 1px solid #DDDDDD;
218 | border-radius: 50%;
219 | display: inline-flex;
220 | justify-content: center;
221 | align-items: center;
222 | margin: 0 5px;
223 | height: 40px;
224 | width: 40px;
225 | }
226 | }
227 |
228 | .forget {
229 |
230 | .container {
231 | width: 480px;
232 | }
233 | .container.right-panel-active .validate-container {
234 | transform: translateX(-100%);
235 | }
236 | .container.right-panel-active .password-container {
237 | left: 0;
238 | }
239 |
240 | .img-code {
241 | align-self: center;
242 | height: 46px;
243 | margin-left: 10px;
244 | border: 1px solid #eee;
245 | }
246 | .code {
247 | width: 194px;
248 | height: 46px;
249 | padding: 0;
250 | align-self: center;
251 | margin-left: 10px;
252 | border-radius: 0;
253 | }
254 | .code-disabled {
255 | color: #d2d2d2;
256 | cursor: not-allowed;
257 | }
258 | .validate-container {
259 | left: 0;
260 | width: 100%;
261 | z-index: 2;
262 | }
263 | .password-container {
264 | left: 100%;
265 | width: 100%;
266 | z-index: 3;
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/src/views/NotFound/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button } from 'antd'
3 | import style from './style.module.scss'
4 |
5 | const NotFound = () => (
6 |
7 |
8 |
9 |

11 |
12 | 您访问的页面走失了
13 |
14 |
15 |
16 |
17 |
18 | )
19 |
20 | export default NotFound
--------------------------------------------------------------------------------
/src/views/NotFound/style.module.scss:
--------------------------------------------------------------------------------
1 | .page-404 {
2 |
3 | .lost {
4 | height: 80%;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | }
10 | .back {
11 | color: #fff;
12 | background-color: #727d99;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/views/Setting/basicInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Card, Form, Input, Button, Select, Space, Radio, Upload, Spin, message } from 'antd'
4 | import {
5 | LoadingOutlined,
6 | PlusOutlined
7 | } from '@ant-design/icons';
8 |
9 | import { ROLES } from './constants'
10 | import style from './style.module.scss'
11 |
12 | const { $http } = React
13 |
14 | const layout = {
15 | labelCol: { span: 4 },
16 | wrapperCol: { span: 16 }
17 | }
18 |
19 | const footerLayout = {
20 | wrapperCol: {
21 | offset: 4,
22 | span: 16
23 | }
24 | }
25 | const defaultAvatar = require('@assets/images/default.png')
26 | const EmailRegexp = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
27 | const PhoneRegexp = /^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/
28 |
29 |
30 | function getBase64(img, callback) {
31 | const reader = new FileReader();
32 | reader.addEventListener('load', () => callback(reader.result));
33 | reader.readAsDataURL(img);
34 | }
35 |
36 | function beforeUpload(file) {
37 | const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
38 | if (!isJpgOrPng) {
39 | message.error('You can only upload JPG/PNG file!');
40 | }
41 | const isLt2M = file.size / 1024 / 1024 < 2;
42 | if (!isLt2M) {
43 | message.error('Image must smaller than 2MB!');
44 | }
45 | return isJpgOrPng && isLt2M;
46 | }
47 |
48 | class EmailService extends React.Component {
49 | constructor() {
50 | super()
51 | this.state = {
52 | loading: false,
53 | spinning: true,
54 | imageUrl: defaultAvatar,
55 | form: {
56 | role: '',
57 | username: '',
58 | nickname: '',
59 | sex: 0,
60 | avatar: '',
61 | phone: '',
62 | email: '',
63 | remark: ''
64 | }
65 | }
66 | }
67 |
68 | componentDidMount() {
69 | const userInfo = JSON.parse(sessionStorage.getItem('userInfo')) || {}
70 | $http.get('user/details/' + userInfo.userId).then(res => {
71 | this.setState({imageUrl: res.avatar, spinning: false})
72 | this.formRef.current.setFieldsValue(res)
73 | })
74 | }
75 |
76 | formRef = React.createRef()
77 |
78 | handleChange = info => {
79 | if (info.file.status === 'uploading') {
80 | this.setState({ loading: true });
81 | return;
82 | }
83 | if (info.file.status === 'done') {
84 | // Get this url from response in real world.
85 | getBase64(info.file.originFileObj, imageUrl =>
86 | this.setState({
87 | imageUrl,
88 | loading: false,
89 | form: {...this.state.form, ...{avatar: imageUrl}}
90 | }),
91 | );
92 | }
93 | }
94 | handleSubmit = values => {
95 | const userInfo = JSON.parse(sessionStorage.getItem('userInfo')) || {}
96 | values.userId = userInfo.userId
97 | this.setState({spinning: true})
98 | $http.put('user/edit', values).then(res => {
99 | this.setState({ spinning: false})
100 | message.success('修改成功')
101 | })
102 | }
103 | handleReset = () => {
104 | this.formRef.current.resetFields()
105 | this.setState({imageUrl: ''})
106 | }
107 |
108 | render() {
109 | const uploadButton = (
110 |
111 | {this.state.loading ?
:
}
112 |
上传
113 |
114 | );
115 | const { imageUrl, spinning } = this.state
116 | return (
117 |
192 | )
193 | }
194 | }
195 |
196 | export default EmailService
--------------------------------------------------------------------------------
/src/views/Setting/constants.js:
--------------------------------------------------------------------------------
1 | export const ROLES = [
2 | { label: '超级管理员', value: 0 },
3 | { label: '普通管理员', value: 1 },
4 | { label: '审核员', value: 2 },
5 | { label: '业务员', value: 3 },
6 | { label: '测试员', value: 4 },
7 | ]
--------------------------------------------------------------------------------
/src/views/Setting/emailService.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Card, Form, Input, Button, Space } from 'antd'
4 |
5 | const layout = {
6 | labelCol: { span: 4 },
7 | wrapperCol: { span: 16 }
8 | }
9 |
10 | const footerLayout = {
11 | wrapperCol: {
12 | offset: 4,
13 | span: 16
14 | }
15 | }
16 |
17 | const EmailRegexp = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
18 | const DomainRegexp = /^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/
19 | const NumberRegexp = /^(0|[1-9][0-9]*)$/
20 |
21 | class EmailService extends React.Component {
22 | constructor() {
23 | super()
24 | this.state = {
25 | form: {
26 | smtpServer: 'baidu.com',
27 | smtpPort: '465',
28 | sendUserEmail: '234871928@qq.com',
29 | sendUserName: '李贤安',
30 | emailPassword: '123456'
31 | }
32 | }
33 | }
34 |
35 | formRef = React.createRef()
36 |
37 | handleSubmit = values => {
38 | console.log(values)
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 |
47 |
53 |
54 |
55 |
56 | 如:smtp.163.com
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 | 一般为 25 或 465
70 |
71 |
72 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | )
93 | }
94 | }
95 |
96 | export default EmailService
--------------------------------------------------------------------------------
/src/views/Setting/modifyPassword.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Card, Form, Input, Button, Space, Spin, message } from 'antd'
4 |
5 | const { $http } = React
6 | const layout = {
7 | labelCol: { span: 4 },
8 | wrapperCol: { span: 16 }
9 | }
10 |
11 | const footerLayout = {
12 | wrapperCol: {
13 | offset: 4,
14 | span: 16
15 | }
16 | }
17 |
18 | // 必须包含大小写字母和数字的组合,不能使用特殊字符,长度在6-16之间
19 | const Regexp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,16}$/
20 |
21 | class ModifyPassword extends React.Component {
22 | constructor() {
23 | super()
24 | this.state = {
25 | spinning: false,
26 | form: {
27 | password: '',
28 | rePassword: '',
29 | }
30 | }
31 | }
32 |
33 | formRef = React.createRef()
34 |
35 | handleSubmit = values => {
36 | this.setState({spinning: true})
37 | $http.put('password/modify', values).then(() => {
38 | message.success('密码修改成功')
39 | this.formRef.current.resetFields()
40 | this.setState({spinning: false})
41 | }).catch(() => {
42 | this.setState({spinning: false})
43 | })
44 | }
45 |
46 | render() {
47 | const { spinning } = this.state
48 | return (
49 |
50 |
51 |
52 |
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 | 必须包含大小写字母和数字的组合,不能使用特殊字符,长度在6-16之间
70 |
71 |
72 | ({
78 | validator(rule, value) {
79 |
80 | if (!value || getFieldValue('newPassword') === value) {
81 | return Promise.resolve()
82 | }
83 |
84 | return Promise.reject('两次输入密码不一致')
85 | }
86 | })
87 | ]}>
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 | }
102 |
103 | export default ModifyPassword
--------------------------------------------------------------------------------
/src/views/Setting/style.module.scss:
--------------------------------------------------------------------------------
1 | .page-basic-info {
2 |
3 | .avatar :global(.ant-form-item-label) {
4 | margin-top: 15px;
5 | }
6 | }
--------------------------------------------------------------------------------
/src/views/Setting/websiteSetting.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Card, Form, Input, InputNumber, Button, Select, Space } from 'antd'
4 |
5 | const layout = {
6 | labelCol: { span: 4 },
7 | wrapperCol: { span: 16 }
8 | }
9 |
10 | const footerLayout = {
11 | wrapperCol: {
12 | offset: 4,
13 | span: 16
14 | }
15 | }
16 |
17 | const Regexp = /^(?=^.{3,255}$)(http(s)?:\/\/)?(www\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)*(\/\w+\.\w+)*(\/)?$/
18 |
19 | class WebsiteSetting extends React.Component {
20 | constructor() {
21 | super()
22 | this.state = {
23 | form: {
24 | name: 'layuiAdmin',
25 | domain: 'https://www.baidu.com',
26 | cacheDate: 0,
27 | maxFileSizes: 2048,
28 | fileType: ['jpg', 'png', 'jpeg'],
29 | title: 'layuiAdmin 通用后台管理模板系统',
30 | keywords: 'React,React-router-dom,Redux',
31 | description: '作为 layui 官方推出的后台模板,从初版的饱受争议,到后续的埋头丰富,逐步占据了国内后台系统应用的主要市场。',
32 | copyright: '©️ 2020 layui.com MIT license'
33 | }
34 | }
35 | }
36 |
37 | websiteFormRef = React.createRef()
38 |
39 | handleSubmit = values => {
40 | console.log(values)
41 | }
42 | // handleReset = () => {
43 | // this.websiteFormRef.current.resetFields()
44 | // }
45 |
46 | render() {
47 | return (
48 |
49 |
50 |
56 |
57 |
58 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 | 分钟
76 | 本地开发一般推荐设置为 0,线上环境建议设置为 10。
77 |
78 |
79 |
80 |
86 |
87 |
88 |
89 | KB
90 | 提示: 1M = 1024KB
91 |
92 |
93 |
97 |
105 |
106 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | )
131 | }
132 | }
133 |
134 | export default WebsiteSetting
--------------------------------------------------------------------------------
/src/views/Test/sagas.js:
--------------------------------------------------------------------------------
1 | export const testSaga = function*() {
2 | console.log('hello')
3 | }
--------------------------------------------------------------------------------
/src/views/User/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Row, Col, Input, Button, Select, Space, Card, Table, Modal, message } from 'antd'
3 |
4 | import {
5 | EditOutlined,
6 | PlusOutlined,
7 | DeleteOutlined,
8 | ExclamationCircleOutlined
9 | } from '@ant-design/icons'
10 |
11 | import UpdateUser from './updateUser'
12 |
13 | const {$http} = React
14 | const searchItem = [
15 | { key: 'username', name: 'username', label: '用户名', defaultValue: null, type: 'input', placeholder: '请输入' },
16 | { key: 'email', name: 'email', label: '邮箱', defaultValue: null, type: 'input', placeholder: '请输入' },
17 | { key: 'sex', name: 'sex', label: '性别', defaultValue: -1, type: 'select', options: [
18 | { value: -1, label: '不限' },
19 | { value: 0, label: '男' },
20 | { value: 1, label: '女'}
21 | ], placeholder: '请选择' }
22 | ]
23 |
24 | class User extends React.Component {
25 | constructor() {
26 | super()
27 | this.state = {
28 | selectedRowKeys: [],
29 | loading: false,
30 | query: {
31 | username: '',
32 | email: '',
33 | sex: -1
34 | },
35 | userTableData: [],
36 | pagination: {
37 | current: 1,
38 | pageSize: 10,
39 | total: 0
40 | },
41 | columns: [
42 | { title: '用户名', dataIndex: 'username' },
43 | { title: '性别', dataIndex: 'sex', render: (text, record) => {
44 | return (
45 | {text === 0 ? '男' : '女'}
46 | )
47 | }},
48 | { title: '联系方式', dataIndex: 'phone' },
49 | { title: '邮箱地址', dataIndex: 'email' },
50 | { title: '创建时间', dataIndex: 'createDate' },
51 | { title: '操作', key: 'action', render: (text, record) => {
52 | return (
53 |
54 |
55 |
56 |
57 | )
58 | }},
59 | ],
60 | modalVisible: false,
61 | modalForm: {},
62 | modalType: 'add'
63 | }
64 | }
65 |
66 | formRef = React.createRef()
67 | modalRef = null
68 |
69 | componentDidMount() {
70 | this.getUserList()
71 | }
72 |
73 | getUserList = () => {
74 | const { query, pagination } = this.state
75 | const params = {}
76 | for (let key in query) {
77 | if (query[key]) params[key] = query[key]
78 | }
79 | params['page'] = pagination.current
80 | params['pageSize'] = pagination.pageSize
81 |
82 | this.setState({loading: true})
83 | $http.get('user/list', { params }).then(res => {
84 | const { list, ...pagination } = res
85 | this.setState({ userTableData: list, pagination: {
86 | current: pagination.page,
87 | pageSize: pagination.pageSize,
88 | total: pagination.total
89 | }, loading: false })
90 | })
91 | }
92 |
93 | onSearch = values => {
94 | this.setState({ query: values }, () => {
95 | this.getUserList()
96 | })
97 | }
98 | onSelectedChange = (selectedRowKeys) => {
99 | this.setState({ selectedRowKeys })
100 | }
101 | paginationChange = (pagination) => {
102 | this.setState({pagination: {
103 | current: pagination.current,
104 | pageSize: pagination.pageSize,
105 | }}, () => {
106 | this.getUserList()
107 | })
108 | }
109 | onEdit = (record) => {
110 | this.setState({
111 | modalVisible: true,
112 | modalForm: record,
113 | modalType: 'edit'
114 | }, () => {
115 | // ref 引用问题
116 | setTimeout(() => {
117 | this.modalRef.formRef.current.setFieldsValue({
118 | id: record.id,
119 | username: record.username,
120 | sex: record.sex,
121 | phone: record.phone,
122 | email: record.email
123 | })
124 | }, 100)
125 | })
126 | }
127 | onDelete = (record) => {
128 | Modal.confirm({
129 | title: '删除用户',
130 | icon: ,
131 | content: (确认删除用户{record.username}吗?),
132 | onOk: () => {
133 | $http.delete('user/delete', { data: {id: record.id} }).then(res => {
134 | message.success('删除成功')
135 | this.getUserList()
136 | })
137 | }
138 | })
139 | }
140 | onMultipleDelete = () => {
141 | const { selectedRowKeys } = this.state
142 | if (!selectedRowKeys.length) {
143 | return message.error('请先选择删除的用户!')
144 | }
145 | $http.delete('user/delete', { data: {ids: selectedRowKeys} }).then(res => {
146 | message.success('删除成功')
147 | this.setState({ selectedRowKeys: [] }, () => this.getUserList())
148 | })
149 | }
150 | onOpenModal = () => {
151 | this.setState({modalVisible: true, modalType: 'add', modalForm: null})
152 | }
153 | onModalSave = (values) => {
154 | const { modalForm, modalType } = this.state
155 | if (modalType === 'add') {
156 | $http.post('user/create', values).then(res => {
157 | message.success('添加成功')
158 | this.onModalCancel()
159 | })
160 | } else {
161 | values.id = modalForm.id
162 | $http.put('user/edit', values).then(res => {
163 | message.success('修改成功')
164 | this.onModalCancel()
165 | })
166 | }
167 | }
168 | onModalCancel = () => {
169 | this.modalRef.formRef.current.resetFields()
170 | this.setState({modalVisible: false, modalType: 'add', modalForm: null})
171 | }
172 |
173 | render() {
174 | const { loading, selectedRowKeys, columns, userTableData, pagination, modalVisible, modalType } = this.state;
175 | const rowSelection = {
176 | selectedRowKeys,
177 | onChange: this.onSelectedChange
178 | }
179 | const getFields = () => {
180 | const children = searchItem.map(item => (
181 |
182 |
186 | {item.type === 'input' ?
187 | :
188 | }
193 |
194 |
195 | ))
196 | return children
197 | }
198 | const searchControl = (
199 |
200 |
201 |
202 |
203 |
204 |
205 | )
206 | return (
207 |
208 |
216 |
217 |
218 |
219 |
220 |
221 | this.modalRef = f} modalType={modalType} visible={modalVisible} onSave={this.onModalSave} onCancel={this.onModalCancel} />
222 |
223 | )
224 | }
225 | }
226 |
227 | export default User
--------------------------------------------------------------------------------
/src/views/User/role.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Row, Col, Input, Button, Select, Space, Card, Table, Modal, message } from 'antd'
3 |
4 | import {
5 | EditOutlined,
6 | PlusOutlined,
7 | DeleteOutlined,
8 | ExclamationCircleOutlined
9 | } from '@ant-design/icons'
10 |
11 | import UpdateRole from './updateRole'
12 |
13 | const {$http} = React
14 | const searchItem = [
15 | { key: 'role', name: 'role', label: '角色筛选', defaultValue: -1, type: 'select', options: [
16 | { value: -1, label: '全部角色' },
17 | { value: 0, label: '超级管理员' },
18 | { value: 1, label: '管理员'},
19 | { value: 2, label: '纠错员'},
20 | { value: 3, label: '采购员'},
21 | { value: 4, label: '推销员'},
22 | { value: 5, label: '运营人员'},
23 | { value: 6, label: '编辑'},
24 | ], placeholder: '请选择' }
25 | ]
26 |
27 | class Role extends React.Component {
28 | constructor() {
29 | super()
30 | this.state = {
31 | selectedRowKeys: [],
32 | loading: false,
33 | role: -1,
34 | roleTableData: [],
35 | columns: [
36 | { title: '角色名', dataIndex: 'roleName' },
37 | { title: '拥有权限', dataIndex: 'auth' },
38 | { title: '具体描述', dataIndex: 'description' },
39 | { title: '操作', key: 'action', render: (text, record) => {
40 | return (
41 |
42 |
43 |
44 |
45 | )
46 | }},
47 | ],
48 | modalVisible: false,
49 | modalForm: null,
50 | modalType: 'add'
51 | }
52 | }
53 |
54 | formRef = React.createRef()
55 | modalRef = null
56 |
57 | componentDidMount() {
58 | this.getRoleList()
59 | }
60 |
61 | getRoleList = () => {
62 | const { query } = this.state
63 | const params = {}
64 | for (let key in query) {
65 | if (query[key]) params[key] = query[key]
66 | }
67 |
68 | this.setState({loading: true})
69 | $http.get('role/list', { params }).then(res => {
70 | const { list } = res
71 | this.setState({ roleTableData: list, loading: false })
72 | }).catch((err) => {
73 | this.setState({loading: false})
74 | const {data} = err
75 | message.error(data.desc || '列表获取失败')
76 | })
77 | }
78 |
79 | onSearch = values => {
80 | this.setState({ query: values }, () => {
81 | this.getRoleList()
82 | })
83 | }
84 | onSelectedChange = (selectedRowKeys) => {
85 | this.setState({ selectedRowKeys })
86 | }
87 | onEdit = (record) => {
88 | $http.get('role/details', {id: record.id}).then(res => {
89 | this.setState({
90 | modalVisible: true,
91 | modalForm: res,
92 | modalType: 'edit'
93 | })
94 | })
95 | }
96 | onDelete = (record) => {
97 | Modal.confirm({
98 | title: '删除角色',
99 | icon: ,
100 | content: (确认删除角色{record.roleName}吗?),
101 | onOk: () => {
102 | $http.delete('role/delete', { data: {id: record.id} }).then(res => {
103 | message.success('删除成功')
104 | this.getRoleList()
105 | })
106 | }
107 | })
108 | }
109 | onMultipleDelete = () => {
110 | const { selectedRowKeys } = this.state
111 | if (!selectedRowKeys.length) {
112 | return message.error('请先选择删除的角色!')
113 | }
114 | $http.delete('role/delete', { data: {ids: selectedRowKeys} }).then(res => {
115 | message.success('删除成功')
116 | this.setState({ selectedRowKeys: [] }, () => this.getRoleList())
117 | })
118 | }
119 | onOpenModal = () => {
120 | this.setState({modalVisible: true, modalType: 'add', modalForm: null})
121 | }
122 | onModalSave = (values) => {
123 | const { modalForm, modalType } = this.state
124 | if (modalType === 'add') {
125 | $http.post('role/create', values).then(res => {
126 | message.success('添加成功')
127 | this.onModalCancel()
128 | })
129 | } else {
130 | values.id = modalForm.id
131 | $http.put('role/edit', values).then(res => {
132 | message.success('修改成功')
133 | this.onModalCancel()
134 | })
135 | }
136 | }
137 | onModalCancel = () => {
138 | this.modalRef.formRef.current.resetFields()
139 | this.setState({modalVisible: false, modalType: 'add', modalForm: null})
140 | }
141 |
142 | render() {
143 | const { loading, selectedRowKeys, columns, roleTableData, modalVisible, modalForm } = this.state;
144 | const rowSelection = {
145 | selectedRowKeys,
146 | onChange: this.onSelectedChange
147 | }
148 | const getFields = () => {
149 | const children = searchItem.map(item => (
150 |
151 |
155 | {item.type === 'input' ?
156 | :
157 | }
162 |
163 |
164 | ))
165 | return children
166 | }
167 | const searchControl = (
168 |
169 |
170 |
171 |
172 |
173 |
174 | )
175 | return (
176 |
177 |
185 |
186 |
187 |
188 |
189 |
190 | this.modalRef = f} visible={modalVisible} modalForm={modalForm} onSave={this.onModalSave} onCancel={this.onModalCancel} />
191 |
192 | )
193 | }
194 | }
195 |
196 | export default Role
--------------------------------------------------------------------------------
/src/views/User/updateRole.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Modal, Button, Input, Space, Select, Checkbox } from 'antd'
3 |
4 | const layout = {
5 | labelCol: {
6 | span: 6,
7 | },
8 | wrapperCol: {
9 | span: 16,
10 | },
11 | }
12 | const tailLayout = {
13 | wrapperCol: {
14 | offset: 6,
15 | span: 16,
16 | },
17 | }
18 | const roleOptions = [
19 | { label: '管理员', value: 1 },
20 | { label: '超级管理员', value: 0 },
21 | { label: '纠错员', value: 2 },
22 | { label: '采购员', value: 3 },
23 | { label: '推销员', value: 4 },
24 | { label: '运营人员', value: 5 },
25 | { label: '文章撰写员', value: 6 }
26 | ]
27 | const authOptions = [
28 | { label: '内容系统', value: 1 },
29 | { label: '社区系统', value: 0 },
30 | { label: '用户', value: 2 },
31 | { label: '角色', value: 3 },
32 | { label: '评论审核', value: 4 },
33 | { label: '采购', value: 5 },
34 | { label: '系统设置', value: 6 },
35 | { label: '发送邮件', value: 7 },
36 | { label: '发送短信', value: 8 },
37 | { label: '审核', value: 9 }
38 | ]
39 |
40 | class UpdateRole extends React.PureComponent {
41 |
42 | formRef = React.createRef()
43 |
44 | render() {
45 | const { visible, onSave, onCancel, modalForm } = this.props
46 | return (
47 |
48 |
51 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | )
72 | }
73 | }
74 |
75 | export default UpdateRole
--------------------------------------------------------------------------------
/src/views/User/updateUser.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Modal, Button, Input, Radio, Space } from 'antd'
3 |
4 | const layout = {
5 | labelCol: {
6 | span: 6,
7 | },
8 | wrapperCol: {
9 | span: 16,
10 | },
11 | }
12 | const tailLayout = {
13 | wrapperCol: {
14 | offset: 6,
15 | span: 16,
16 | },
17 | }
18 | const EmailRegexp = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
19 | const PhoneRegexp = /^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/
20 |
21 | class UpdateUser extends React.PureComponent {
22 | constructor(props) {
23 | super(props)
24 | this.state = {
25 | form: {
26 | username: '',
27 | sex: 0,
28 | phone: '',
29 | email: ''
30 | }
31 | }
32 | }
33 |
34 | formRef = React.createRef()
35 |
36 | render() {
37 | const { visible, onSave, onCancel, modalType } = this.props
38 |
39 | return (
40 |
41 |
46 |
47 |
48 |
49 |
50 | 男
51 | 女
52 |
53 |
54 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | )
75 | }
76 | }
77 |
78 | export default UpdateUser
--------------------------------------------------------------------------------