├── src ├── api │ ├── .gitkeep │ ├── demo.js │ └── login.js ├── lang │ ├── .gitkeep │ ├── zh_CN.js │ ├── en_US.js │ ├── index.js │ ├── t.js │ └── I18nProvider.js ├── store │ ├── .gitkeep │ ├── actions │ │ ├── index.js │ │ ├── app.js │ │ └── user.js │ ├── constants │ │ └── index.js │ ├── reudcers │ │ ├── index.js │ │ ├── app.js │ │ └── user.js │ └── index.js ├── components │ └── .gitkeep ├── views │ ├── Index │ │ ├── index.js │ │ └── Index.jsx │ ├── Login │ │ ├── index.js │ │ └── Login.jsx │ ├── Others │ │ ├── 404 │ │ │ ├── index.js │ │ │ └── 404.jsx │ │ └── 500 │ │ │ ├── index.js │ │ │ └── 500.jsx │ └── Test │ │ ├── Test1.jsx │ │ ├── Test21.jsx │ │ ├── Test22.jsx │ │ ├── Detail.jsx │ │ ├── AddModal.jsx │ │ └── Demo.jsx ├── layout │ ├── index.js │ ├── AppFooter.jsx │ ├── AppAside.jsx │ ├── HeaderBreadcrumb.jsx │ ├── AppHeader.jsx │ ├── SliderMenu.jsx │ └── DefaultLayout.jsx ├── assets │ └── images │ │ ├── user.png │ │ ├── logo.svg │ │ └── login.svg ├── setupTests.js ├── index.css ├── setupProxy.js ├── utils │ ├── loadable.js │ ├── app.js │ ├── pageState.js │ └── request.js ├── index.js ├── style │ ├── views │ │ └── login.less │ ├── App.less │ ├── layout.less │ └── base.less ├── routes │ └── index.js ├── config │ └── menu.js ├── App.js └── serviceWorker.js ├── .env.staging ├── .env.production ├── note ├── updatelog.md ├── react0.png └── react1.png ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .browserslistrc ├── .eslintrc ├── .env.development ├── .postcssrc.js ├── .babelrc ├── .gitignore ├── plugins └── webpack.end.js ├── LICENSE ├── README.md ├── package.json └── .rescriptsrc.js /src/api/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lang/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | ENV = staging -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | ENV = production 3 | -------------------------------------------------------------------------------- /src/lang/zh_CN.js: -------------------------------------------------------------------------------- 1 | // 中文翻译 2 | export default { 3 | 'welcome': '欢迎您!', 4 | } -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from "./user"; 2 | export * from "./app"; 3 | -------------------------------------------------------------------------------- /note/updatelog.md: -------------------------------------------------------------------------------- 1 | ### 2020-09-01 2 | - 增加curd demo,带筛选项列表和添加弹窗模块 3 | ---------------- -------------------------------------------------------------------------------- /src/lang/en_US.js: -------------------------------------------------------------------------------- 1 | // 英文翻译 2 | export default { 3 | 'welcome': 'Welcome!', 4 | } 5 | -------------------------------------------------------------------------------- /src/views/Index/index.js: -------------------------------------------------------------------------------- 1 | import Index from './Index.jsx' 2 | 3 | export default Index 4 | -------------------------------------------------------------------------------- /src/views/Login/index.js: -------------------------------------------------------------------------------- 1 | import Login from './Login.jsx' 2 | 3 | export default Login 4 | -------------------------------------------------------------------------------- /note/react0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woleicom/react-admin-template/HEAD/note/react0.png -------------------------------------------------------------------------------- /note/react1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woleicom/react-admin-template/HEAD/note/react1.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/views/Others/404/index.js: -------------------------------------------------------------------------------- 1 | import View404 from './404.jsx' 2 | 3 | export default View404 4 | -------------------------------------------------------------------------------- /src/views/Others/500/index.js: -------------------------------------------------------------------------------- 1 | import View500 from './500.jsx' 2 | 3 | export default View500 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woleicom/react-admin-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woleicom/react-admin-template/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woleicom/react-admin-template/HEAD/public/logo512.png -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # 兼容IE10、11 2 | # not ie < 10 3 | # > 0.25% 4 | 5 | defaults 6 | not dead 7 | not op_mini all -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | import DefaultLayout from './DefaultLayout.jsx' 2 | 3 | export default DefaultLayout 4 | -------------------------------------------------------------------------------- /src/assets/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woleicom/react-admin-template/HEAD/src/assets/images/user.png -------------------------------------------------------------------------------- /src/lang/index.js: -------------------------------------------------------------------------------- 1 | export const langKeys = [ 2 | {value:'zh-CN',label:'简体中文'}, 3 | {value:'en-US',label:'English'}, 4 | ] -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app"], 3 | // 禁用所有规则 4 | // "space-before-function-paren":"off", 5 | "rules": { 6 | } 7 | } -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV = development 2 | ENV = development 3 | 4 | REACT_APP_BASE_URI = http://127.0.0.1:3333 5 | REACT_APP_BASE_API = /admin/ 6 | -------------------------------------------------------------------------------- /src/lang/t.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | export default (key) => { 4 | return 5 | } -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | map: true, 3 | plugins: { 4 | "postcss-preset-env": { stage: 0 }, 5 | "autoprefixer": { 6 | "grid": true 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/store/constants/index.js: -------------------------------------------------------------------------------- 1 | export const SET_USER_INFO = "setUserInfo"; 2 | export const SET_USER_TOKEN = "setUserToken"; 3 | 4 | export const SET_APP_LANGUAGE = "setAppLanguage"; 5 | -------------------------------------------------------------------------------- /src/store/reudcers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import User from "./user"; 3 | import App from "./app"; 4 | 5 | export default combineReducers({ 6 | User, 7 | App 8 | }); 9 | -------------------------------------------------------------------------------- /src/layout/AppFooter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Layout } from 'antd' 3 | 4 | const { Footer } = Layout 5 | 6 | export default () => 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/store/actions/app.js: -------------------------------------------------------------------------------- 1 | 2 | import { SET_APP_LANGUAGE } from "../constants"; 3 | 4 | export const setAppLanguage = (res) => { 5 | return (dispatch) => { 6 | const arr = { 7 | type: SET_APP_LANGUAGE, 8 | value: res, 9 | }; 10 | dispatch(arr); 11 | }; 12 | }; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } -------------------------------------------------------------------------------- /src/views/Test/Test1.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Layout} from 'antd' 3 | import {withRouter} from 'react-router-dom'; 4 | 5 | const Test1 = (props) => { 6 | return ( 7 | 8 | test1 9 | 10 | ) 11 | }; 12 | export default withRouter(Test1); 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 7 | [ 8 | "import", 9 | { 10 | "libraryName": "antd", 11 | "libraryDirectory": "es", 12 | "style": true 13 | } 14 | ] 15 | ] 16 | } -------------------------------------------------------------------------------- /src/views/Test/Test21.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Layout} from 'antd' 3 | import {withRouter} from 'react-router-dom'; 4 | 5 | const Test21 = (props) => { 6 | return ( 7 | 8 | test21 9 | 10 | ) 11 | }; 12 | export default withRouter(Test21); 13 | -------------------------------------------------------------------------------- /src/views/Test/Test22.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Layout} from 'antd' 3 | import {withRouter} from 'react-router-dom'; 4 | 5 | const Test22 = (props) => { 6 | return ( 7 | 8 | test22 9 | 10 | ) 11 | }; 12 | export default withRouter(Test22); 13 | -------------------------------------------------------------------------------- /src/store/reudcers/app.js: -------------------------------------------------------------------------------- 1 | import { SET_APP_LANGUAGE } from "../constants"; 2 | 3 | export const defaultState = { 4 | lang: 'zh-CN' 5 | }; 6 | 7 | export default (state = defaultState, action) => { 8 | switch (action.type) { 9 | case SET_APP_LANGUAGE: 10 | return {...state, lang:action.value}; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/views/Others/500/500.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Result,Button} from 'antd'; 3 | 4 | const View500 = (props) => ( 5 | {props.history.push('/')}}>Back Home} 10 | /> 11 | ) 12 | 13 | export default View500 14 | -------------------------------------------------------------------------------- /src/views/Test/Detail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Layout,Button} from 'antd' 3 | import {withRouter} from 'react-router-dom'; 4 | 5 | const Detail = (props) => { 6 | return ( 7 | 8 | detail 9 | 10 | 11 | ) 12 | }; 13 | export default withRouter(Detail); 14 | -------------------------------------------------------------------------------- /src/views/Index/Index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Layout,DatePicker} from 'antd' 3 | import {withRouter} from 'react-router-dom'; 4 | import t from '@/lang/t' 5 | const Index = (props) => { 6 | return ( 7 | 8 | {t('welcome')} 9 | 10 | 11 | ) 12 | }; 13 | export default withRouter(Index) 14 | -------------------------------------------------------------------------------- /src/views/Others/404/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Result,Button} from 'antd'; 3 | 4 | const View404 = (props) => ( 5 | {props.history.push('/')}}>Back Home} 10 | /> 11 | ) 12 | 13 | export default View404 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | build 13 | yarn.lock* 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/store/actions/user.js: -------------------------------------------------------------------------------- 1 | import { SET_USER_INFO,SET_USER_TOKEN } from "../constants"; 2 | 3 | export const setUserInfo = (res) => { 4 | return (dispatch) => { 5 | const arr = { 6 | type: SET_USER_INFO, 7 | value: res, 8 | }; 9 | dispatch(arr); 10 | }; 11 | }; 12 | export const setUserToken = (res) => { 13 | return (dispatch) => { 14 | const arr = { 15 | type: SET_USER_TOKEN, 16 | value: res, 17 | }; 18 | dispatch(arr); 19 | }; 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const {createProxyMiddleware} = require('http-proxy-middleware'); 2 | module.exports = function(app) { 3 | app.use( 4 | '/api', 5 | createProxyMiddleware({ 6 | target: 'http://127.0.0.1:8000/api', 7 | pathRewrite: {'^/api' : ''}, 8 | changeOrigin: true, 9 | }) 10 | ); 11 | // app.use( 12 | // '/api2', 13 | // createProxyMiddleware({ 14 | // target: 'http://127.0.0.1:8000/api2', 15 | // pathRewrite: {'^/api2' : ''}, 16 | // changeOrigin: true, 17 | // }) 18 | // ); 19 | }; -------------------------------------------------------------------------------- /src/utils/loadable.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import Loadable from 'react-loadable' 3 | import NProgress from 'nprogress' 4 | import 'nprogress/nprogress.css' 5 | 6 | const useLoadingComponent = () => { 7 | useEffect(() => { 8 | NProgress.start() 9 | return () => { 10 | NProgress.done() 11 | } 12 | }, []) 13 | return
14 | } 15 | 16 | export default (loader, loading = useLoadingComponent) => { 17 | return Loadable({ 18 | loader, 19 | loading 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/store/reudcers/user.js: -------------------------------------------------------------------------------- 1 | import { SET_USER_INFO,SET_USER_TOKEN } from "../constants"; 2 | 3 | export const defaultState = { 4 | userInfo: JSON.parse(localStorage.getItem('user'))||{}, 5 | token: localStorage.getItem('token')||"" 6 | }; 7 | 8 | export default (state = defaultState, action) => { 9 | switch (action.type) { 10 | case SET_USER_INFO: 11 | const userInfo = action.value; 12 | return {...state, userInfo}; 13 | case SET_USER_TOKEN: 14 | const token = action.value; 15 | return {...state, token}; 16 | default: 17 | return state; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from "redux"; 2 | import reducer from "./reudcers"; 3 | import thunk from "redux-thunk"; 4 | 5 | const composeEnhancers = 6 | process.env.NODE_ENV !== 'production' && 7 | typeof window === 'object' && 8 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 9 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 10 | // Specify here name, actionsBlacklist, actionsCreators and other options 11 | }) : compose; 12 | 13 | const enhancer = composeEnhancers(applyMiddleware(thunk)); 14 | const store = createStore(reducer, enhancer); 15 | 16 | export default store; 17 | -------------------------------------------------------------------------------- /src/utils/app.js: -------------------------------------------------------------------------------- 1 | import {message} from 'antd'; 2 | let __history; 3 | export const setHistory = (history)=>{ 4 | __history = history; 5 | } 6 | export const getHistory = ()=>{ 7 | return __history; 8 | } 9 | export const $iscode = (res, isShowSussessMessage)=>{ 10 | if(res.code === 1){ 11 | isShowSussessMessage && message.success(res.message); 12 | return true; 13 | } else { 14 | // if(res.code.toString().search('9') == 0) { 15 | // localStorage.removeItem('token'); 16 | // __history.replace("/login"); 17 | // } 18 | isShowSussessMessage && message.error(res.message); 19 | return false; 20 | } 21 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | import { Provider } from "react-redux"; 8 | import store from "./store"; 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: https://bit.ly/CRA-PWA 19 | serviceWorker.unregister(); 20 | -------------------------------------------------------------------------------- /src/api/demo.js: -------------------------------------------------------------------------------- 1 | export const getList = (data)=>new Promise((resolve)=>{ 2 | let d = []; 3 | for(let i=(data.size*(data.page-1)+1);i{ 13 | resolve({ 14 | code: 1, 15 | message: '登录成功', 16 | data: d, 17 | total: 200 18 | }); 19 | },2000) 20 | }); 21 | export const addDemo = (data)=>new Promise((resolve)=>{ 22 | setTimeout(()=>{ 23 | resolve({ 24 | code: 1, 25 | message: '添加成功', 26 | data: {...data, id: new Date().getTime()}, 27 | }); 28 | },2000) 29 | }); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | React App 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/utils/pageState.js: -------------------------------------------------------------------------------- 1 | // const key = $pageState; 2 | //初始化 3 | let list = {}; 4 | let storage = { 5 | clearItem(key){ 6 | delete list[key]; 7 | }, 8 | getItem(key){ 9 | if(list[key]){ 10 | let value = list[key]; 11 | storage.clearItem(key); 12 | return value; 13 | }else{ 14 | return void 0; 15 | } 16 | }, 17 | setItem(key,value){ 18 | list[key] = value; 19 | }, 20 | clear(){ 21 | list = {}; 22 | } 23 | }; 24 | export const clearPageState = (key)=>{ 25 | storage.clearItem(key); 26 | } 27 | export const setPageState = (key,value)=>{ 28 | storage.setItem(key,value); 29 | } 30 | export const getPageState = (key)=>{ 31 | return storage.getItem(key); 32 | } 33 | export const clearAllPageState = ()=>{ 34 | storage.clear(); 35 | } -------------------------------------------------------------------------------- /src/style/views/login.less: -------------------------------------------------------------------------------- 1 | .login { 2 | background-image: url('../../assets/images/login.svg'); 3 | background-repeat: no-repeat; 4 | background-position: center 110px; 5 | background-size: 100%; 6 | 7 | .model { 8 | height: 100vh; 9 | overflow: hidden; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | 14 | .login-form { 15 | padding: 2rem; 16 | box-shadow: 0px 2px 13px 0px rgba(228, 228, 228, 0.6); 17 | background-color: #fff; 18 | width: 35rem; 19 | 20 | .login-form-button { 21 | width: 100%; 22 | } 23 | } 24 | 25 | .login-register { 26 | font-size: 12px; 27 | color: #999; 28 | text-align: right; 29 | cursor: pointer; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/layout/AppAside.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Layout } from 'antd' 4 | import SliderMenu from './SliderMenu' 5 | import Logo from '@/assets/images/logo.svg' 6 | 7 | const { Sider } = Layout 8 | 9 | const AppAside = props => { 10 | let { menuToggle, menu } = props 11 | let theme = 'light'; 12 | return ( 13 | 14 | 20 | 21 | 22 | ) 23 | } 24 | 25 | AppAside.propTypes = { 26 | menuToggle: PropTypes.bool, 27 | menu: PropTypes.array.isRequired 28 | } 29 | 30 | export default AppAside 31 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import loadable from '@/utils/loadable' 2 | 3 | const Index = loadable(() => import(/* webpackChunkName: 'index' */ '@/views/Index')) 4 | 5 | 6 | const Test1 = loadable(() => import('@/views/Test/Test1')) 7 | const Test21 = loadable(() => import('@/views/Test/Test21')) 8 | const Test22 = loadable(() => import('@/views/Test/Test22')) 9 | const Demo = loadable(() => import('@/views/Test/Demo')) 10 | const Detail = loadable(() => import('@/views/Test/Detail')) 11 | 12 | const routes = [ 13 | { path: '/index', exact: true, name: 'Index', component: Index }, 14 | { path: '/test/test1', exact: true, name: 'Test1', component: Test1 }, 15 | { path: '/test/test2/test21', exact: true, name: 'Test21', component: Test21 }, 16 | { path: '/test/test2/test22', exact: true, name: 'Test22', component: Test22 }, 17 | { path: '/demo/demo', exact: true, name: 'Demo', component: Demo }, 18 | { path: '/demo/demo/detail', exact: true, name: 'Detail', component: Detail }, 19 | ] 20 | 21 | export default routes 22 | -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | // export const sendLogin = (data)=>request({ url: `login`,method: 'post', data }); 3 | // export const sendLogout = (data)=>request({ url: `logout`,method: 'post', data }); 4 | // export const sendUserInfo = (data)=>request({ url: `userInfo`,method: 'post', data }); 5 | 6 | export const sendLogin = (data)=>new Promise((resolve)=>{ 7 | resolve({ 8 | code: 1, 9 | message: '登录成功', 10 | data: { 11 | token: data.username == 'admin'?'000000':'999999' 12 | } 13 | }); 14 | }); 15 | export const sendLogout = (data)=>new Promise((resolve)=>{ 16 | resolve({ 17 | code: 1, 18 | message: '登出成功', 19 | data: {} 20 | }) 21 | }); 22 | export const sendUserInfo = (data)=>new Promise((resolve)=>{ 23 | let token = localStorage.getItem('token'); 24 | let menus = require('../config/menu'); 25 | resolve({ 26 | code: 1, 27 | message: '获取用户信息成功', 28 | data: { 29 | id: 0, 30 | name: 'admin', 31 | menus: token == '000000'?menus:[menus[0]] 32 | } 33 | }) 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /plugins/webpack.end.js: -------------------------------------------------------------------------------- 1 | const compressing = require('compressing'); 2 | const moment = require('moment'); 3 | const del = require('del'); 4 | let delDist = async (dir) => { 5 | try { 6 | await del([dir]); 7 | console.log('删除'+dir+'成功!!!'); 8 | } catch (e) { 9 | console.log('删除'+dir+'错误!!!'); 10 | } 11 | } 12 | let zip = async (dir,projectName) => { 13 | let name = `${projectName}正式站包 ${moment().format('YYYY-MM-DD HH_mm_ss')}.zip`; 14 | moment(); 15 | compressing.zip.compressDir(dir, name) 16 | .then(()=>{ 17 | console.log('打包成功!!!'); 18 | delDist(dir); 19 | }).catch((e)=>{ 20 | console.log(e,'打包错误!!!') 21 | }); 22 | } 23 | let globalDir,globalProjectName; 24 | function EndWebpackPlugin(dir,projectName) { 25 | globalDir = dir; 26 | globalProjectName = projectName; 27 | }; 28 | EndWebpackPlugin.prototype.apply = function(compiler) { 29 | compiler.plugin('done', function() { 30 | setTimeout(() => { 31 | console.log('webpack编译完成!!!'); 32 | zip(globalDir,globalProjectName); 33 | }, 300); 34 | }); 35 | }; 36 | module.exports = EndWebpackPlugin; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 woleicom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lang/I18nProvider.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { connect } from 'react-redux' 3 | import { IntlProvider } from 'react-intl' 4 | import { ConfigProvider } from 'antd' 5 | // antd组件语言 6 | import antd_en_US from 'antd/es/locale/en_US' 7 | import antd_zh_CN from 'antd/es/locale/zh_CN' 8 | // react-intel本地语言及其polyfill 9 | import '@formatjs/intl-relativetimeformat/polyfill' 10 | import '@formatjs/intl-relativetimeformat/locale-data/en' 11 | import '@formatjs/intl-relativetimeformat/locale-data/zh' 12 | 13 | // 业务自定义语言 14 | import en_US from './en_US' 15 | import zh_CN from './zh_CN' 16 | 17 | const antdLocale = { 18 | 'zh-CN': antd_zh_CN, 19 | 'en-US': antd_en_US, 20 | } 21 | 22 | const messages = { 23 | 'zh-CN': zh_CN, 24 | 'en-US': en_US, 25 | } 26 | //https://www.npmjs.com/package/react-intl 27 | class I18nProvider extends PureComponent { 28 | render() { 29 | const { language } = this.props; 30 | return ( 31 | 32 | 33 | { this.props.children } 34 | 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default connect()(I18nProvider) 41 | -------------------------------------------------------------------------------- /src/layout/HeaderBreadcrumb.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Breadcrumb } from 'antd' 4 | import { Link } from 'react-router-dom' 5 | 6 | const CustomBreadcrumb = props => { 7 | return ( 8 | 9 | 10 | 首页 11 | 12 | {props.arr && 13 | props.arr.map(res => { 14 | if (typeof res === 'object') { 15 | return ( 16 | 17 | {res.title} 18 | 19 | ) 20 | } else { 21 | return {res} 22 | } 23 | })} 24 | 25 | ) 26 | } 27 | 28 | CustomBreadcrumb.propTypes = { 29 | arr: PropTypes.array.isRequired 30 | } 31 | 32 | function shouldRender(nextProps, prevProps) { 33 | if (nextProps.arr.join() === prevProps.arr.join()) { 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | export default React.memo(CustomBreadcrumb, shouldRender) 40 | -------------------------------------------------------------------------------- /src/style/App.less: -------------------------------------------------------------------------------- 1 | // 仅支持 chrome 滚动条样式 2 | 3 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ 4 | ::-webkit-scrollbar { 5 | width: 5px; 6 | background-color: #fff; 7 | } 8 | 9 | /*定义滚动条轨道 内阴影+圆角*/ 10 | ::-webkit-scrollbar-track { 11 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 12 | } 13 | 14 | /*定义滑块 内阴影+圆角*/ 15 | ::-webkit-scrollbar-thumb { 16 | border-radius: 10px; 17 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 18 | background-color: #ccc; 19 | } 20 | 21 | .base-style { 22 | background-color: #fff; 23 | margin-bottom: 1rem; 24 | padding: 1.5rem; 25 | border-radius: 5px; 26 | box-shadow: 0px 2px 13px 0px rgba(228, 228, 228, 0.6); 27 | } 28 | 29 | // @font-face { 30 | // font-family: 'webfont'; 31 | // font-display: swap; 32 | // src: url('../assets/font/webfont.eot'); /* IE9 */ 33 | // src: url('../assets/font/webfont.eot') format('embedded-opentype'), /* IE6-IE8 */ 34 | // url('../assets/font/webfont.woff2') format('woff2'), 35 | // url('../assets/font/webfont.woff') format('woff'), /* chrome、firefox */ 36 | // url('../assets/font/webfont.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ 37 | // url('../assets/font/webfont.svg') format('svg'); /* iOS 4.1- */ 38 | // } 39 | 40 | // html, 41 | // body { 42 | // font-family: 'webfont' !important; 43 | // } 44 | -------------------------------------------------------------------------------- /src/config/menu.js: -------------------------------------------------------------------------------- 1 | const menu = [ 2 | { 3 | key: '/index', 4 | title: '首页', 5 | icon: 'HomeOutlined', 6 | }, 7 | { 8 | title: 'Test', 9 | key: '/test', 10 | icon: 'BarsOutlined', 11 | subs: [ 12 | { 13 | title: 'Test1', 14 | key: '/test/test1', 15 | icon: '', 16 | }, 17 | { 18 | title: 'Test2', 19 | key: '/test/test2', 20 | icon: '', 21 | subs: [ 22 | { title: 'Test21', key: '/test/test2/test21', icon: '' }, 23 | { title: 'Test22', key: '/test/test2/test22', icon: '' }, 24 | ] 25 | } 26 | ] 27 | }, 28 | { 29 | title: 'Demo', 30 | key: '/demo', 31 | icon: 'BarsOutlined', 32 | subs: [ 33 | { 34 | title: 'Demo', 35 | key: '/demo/demo', 36 | icon: '', 37 | subs: [ 38 | { 39 | title: 'Detail', 40 | key: '/demo/demo/detail', 41 | hidden: true 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | ] 48 | 49 | module.exports = menu 50 | -------------------------------------------------------------------------------- /src/style/layout.less: -------------------------------------------------------------------------------- 1 | // @content_min_width:1000px; 2 | .app { 3 | font-size: 1.6rem; 4 | @media screen and (max-width: 960px) { 5 | font-size: 1.4rem; 6 | } 7 | .aside { 8 | position: fixed; 9 | left: 0; 10 | height: 100vh; 11 | overflow-y: auto; 12 | z-index: 10; 13 | .logo { 14 | box-sizing: border-box; 15 | height: 64px; 16 | box-sizing: border-box; 17 | border-radius: 1rem; 18 | padding: 10px 1rem; 19 | text-align: center; 20 | img { 21 | height: 100%; 22 | } 23 | } 24 | } 25 | 26 | .header { 27 | background-color: #fff; 28 | padding: 0 1.5rem; 29 | display: flex; 30 | justify-content: space-between; 31 | overflow: auto; 32 | width: 100%; 33 | .left { 34 | display: flex; 35 | align-items: center; 36 | white-space: nowrap; 37 | } 38 | .right { 39 | display: flex; 40 | align-items: center; 41 | } 42 | } 43 | 44 | .content { 45 | padding: 10px 18px; 46 | width: auto !important; 47 | overflow: auto; 48 | height: 100%; 49 | &>div,&>section{ 50 | min-width: 800px; 51 | } 52 | } 53 | 54 | .footer { 55 | text-align: center; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React,{Component} from 'react' 2 | import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; 3 | import loadable from './utils/loadable'; 4 | import { connect } from "react-redux"; 5 | import I18nProvider from './lang/I18nProvider' 6 | import 'animate.css' 7 | import './style/base.less' 8 | import './style/App.less' 9 | 10 | 11 | // 公共模块 12 | const DefaultLayout = loadable(() => import('./layout')); 13 | 14 | // 基础页面 15 | const View404 = loadable(() => import(/* webpackChunkName: '404' */'./views/Others/404')); 16 | const View500 = loadable(() => import(/* webpackChunkName: '500' */'./views/Others/500')); 17 | const Login = loadable(() => import(/* webpackChunkName: 'login' */'./views/Login')); 18 | 19 | class App extends Component { 20 | render() { 21 | console.log(process.env); 22 | return ( 23 | 24 | 25 | 26 | } /> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | }; 37 | export default connect( 38 | (state) => ({ 39 | language: state.App.lang 40 | }), 41 | )(App) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react开发脚手架二次开发后台管理系统模板 2 | #### 简介: React 16.x、Ant Design 4.x、国际化、router、redux、ui仿antd-admin-pro、动态路由、动态菜单权限、页面状态缓存 3 | ## 作者: 月下独奏 4 | ### [更新日志](./note/updatelog.md) 5 | ##### 个人认为舒服的cli会提升开发的幸福度,所以部分习惯参考了vue-cli 6 | ##### 如果喜欢vue建议参考项目:[woleicom/vue-admin-template](https://github.com/woleicom/vue-admin-template) 7 | ##### 如果喜欢vue建议直接选择vue element admin,已经做得很好了,不需要在二次开发了 8 | ----------- 9 | ### 感谢: 10 | - [React 16](https://reactjs.org/) 11 | - [Ant Design 4](https://ant.design/) 12 | ----------- 13 | ### 优化内容(所有依赖库基于2020年第一季度最新包版本) 14 | - 用rescripts库重写create-react-app cli的webpack配置部分(配置更方便,可读性更好) 15 | - .rescripts配置rescripts配置 16 | - .eslintrc配置eslint规则(文件中已备注可以忽略eslint校验规则的备注,打开注释即可忽略校验) 17 | - .babelrc配置babel配置 18 | - .postcssrc.js配置postcss配置 19 | - .browserslistrc配置浏览器支持 20 | - .env.*配置不同--mode模式下的环境变量 21 | - 增加webpack编译打包进度条 22 | - 增加@指向src目录快捷访问文件夹和文件 23 | - 增加webpack end 插件,自动压缩打包文件并命名项目名称和打包日期时间 24 | - 集成antd并可自定义theme 25 | - UI参考antd admin pro(并不太喜欢antd admin pro脚手架,但是样式没得说,很漂亮) 26 | - 集成react-router,并根据角色自动配置菜单和路由权限(权限在api中前端设置的测试数据) 27 | - 导航面包屑根据用户菜单自动获取,不在放到页面手动配置。 28 | - 集成redux(因为刚接触react不到一个月,redux还不是很上手,redux跟hooks的useReducer可能规整不是很好,导致只判断了userInfo.id不存在就跳转到登录页面) 29 | - 增加utils/pageState缓存页面状态全局变量工具,没有存储storage,刷新无效 30 | - 集成i18n国际化(使用react-intl集成国际化,导航头切换语言,首页查看效果) 31 | 32 | ### 开发中兼容性(IE兼容在单独IE分支,不在维护) 33 | - 兼容IE10、11默认关闭(通过注释src/index.js、.babelrc、.browserslistrc配置) 34 | - 兼容到IE11(通过babel兼容) 35 | - 兼容到IE10(pulic/index.html注释需要打开,因为在开发过程中create-react-app引入了一个只针对node环境的颜料库,导致在IE10中缺少相关函数对象,需要在全局静态配置polyfill,而在src中引入无效,如果只需要生产环境支持,则不需要,打包后会自动过滤掉,html中也根据是否development进行判断了) 36 | ### 相关命令 37 | ``` 38 | //启动前台服务 39 | npm run start 40 | //打包staging环境 41 | npm run build:stag 42 | //打包正式环境 43 | npm run build 44 | ``` 45 | ------------- 46 | ##### 登录账号:admin/密码随便填 或 账号随便填/密码随便填 (两个账号权限不一样) 47 | ### 展示 48 | ![登录页](./note/react0.png) 49 | ![首页](./note/react1.png) -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { message } from 'antd' 3 | const service = axios.create({ 4 | baseURL: 'http://localhost:3333/api/', 5 | timeout: 10000, 6 | headers:{ 7 | post:{ 8 | 'Content-Type':'application/json;charset=utf-8' 9 | } 10 | } 11 | }) 12 | 13 | service.interceptors.request.use( 14 | config => { 15 | config.headers['Authorization'] = localStorage.getItem('token') || ''; 16 | return config 17 | }, 18 | error => { 19 | return Promise.reject(error) 20 | } 21 | ) 22 | 23 | service.interceptors.response.use( 24 | (response) => { 25 | const res = response.data 26 | return res; 27 | }, 28 | (error) => { 29 | if (error.response.data.statusCode === 400) { 30 | let msg = error.response.data.message.map(v => { 31 | let m = []; 32 | for(let key in v.constraints) { 33 | m.push(v.constraints[key]); 34 | } 35 | return m; 36 | }); 37 | let msgString = ''; 38 | msg.forEach(v=>{ 39 | v.forEach(x => { 40 | msgString += x + "|"; 41 | }) 42 | }) 43 | message.error({ 44 | content:msgString.slice(0,-1), 45 | type: 'error', 46 | duration: 5 * 1000 47 | }) 48 | } else { 49 | console.log('err' + error) // for debug 50 | if(error.toString().search('timeout')>-1) { 51 | message.error('请求超时,请重试或联系管理员'); 52 | } if (error.toString().search('Network Error')> -1) { 53 | message.error('请连接网络'); 54 | } else { 55 | // 其他未知错误不提示 56 | message.error({ 57 | content: error.message, 58 | type: 'error', 59 | duration: 5 * 1000 60 | }) 61 | } 62 | } 63 | return Promise.reject(error) 64 | } 65 | ) 66 | let ser = (config)=>{ 67 | return service(config); 68 | } 69 | export default ser 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-admin-template", 3 | "version": "0.2.0", 4 | "author": { 5 | "name": "月下独奏", 6 | "email": "572962673@qq.com" 7 | }, 8 | "private": true, 9 | "homepage":"", 10 | "dependencies": { 11 | "@testing-library/jest-dom": "^5.11.3", 12 | "@testing-library/react": "^10.4.8", 13 | "@testing-library/user-event": "^12.1.1", 14 | "animate.css": "^4.1.0", 15 | "antd": "^4.5.4", 16 | "axios": "^0.19.2", 17 | "core-js": "3", 18 | "nprogress": "^0.2.0", 19 | "react": "^16.13.1", 20 | "react-dom": "^16.13.1", 21 | "react-intl": "^5.6.7", 22 | "react-loadable": "^5.5.0", 23 | "react-redux": "^7.2.0", 24 | "react-router-dom": "^5.1.2", 25 | "react-scripts": "3.4.3", 26 | "redux": "^4.0.5", 27 | "redux-thunk": "^2.3.0", 28 | "screenfull": "^5.0.2" 29 | }, 30 | "devDependencies": { 31 | "@babel/plugin-proposal-class-properties": "^7.8.3", 32 | "@babel/plugin-proposal-decorators": "^7.8.3", 33 | "@rescripts/cli": "^0.0.14", 34 | "@rescripts/rescript-use-babel-config": "^0.0.10", 35 | "@rescripts/rescript-use-eslint-config": "^0.0.11", 36 | "@rescripts/utilities": "^0.0.7", 37 | "autoprefixer": "^9.7.6", 38 | "babel-plugin-import": "^1.13.0", 39 | "compressing": "^1.5.0", 40 | "del": "^5.1.0", 41 | "dotenv": "^8.2.0", 42 | "http-proxy-middleware": "^1.0.5", 43 | "less": "^3.12.2", 44 | "less-loader": "^6.2.0", 45 | "moment": "^2.24.0", 46 | "postcss-preset-env": "^6.7.0", 47 | "rescript-use-postcss-config": "^1.0.0", 48 | "webpackbar": "^4.0.0" 49 | }, 50 | "scripts": { 51 | "start": "rescripts start", 52 | "build": "rescripts build", 53 | "build:stag": "rescripts build --mode staging", 54 | "test": "rescripts test --env=jsdom", 55 | "eject": "react-scripts eject" 56 | }, 57 | "eslintConfig": { 58 | "extends": "react-app" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/views/Test/AddModal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, useEffect, useRef } from 'react' 2 | import { Modal,Form,Button,Input } from 'antd' 3 | import {addDemo} from '@/api/demo' 4 | import {$iscode} from '@/utils/app' 5 | const Index = (props) => { 6 | const form = useRef(); 7 | const layout = { 8 | labelCol: { 9 | flex: "0 0 100px", 10 | }, 11 | // wrapperCol: { 12 | // flex: "auto" 13 | // } 14 | }; 15 | useEffect(()=>{ 16 | props.init(form.current) 17 | },[]) 18 | return ( 19 |
20 |
28 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 63 | 64 | 65 | 66 |
67 |
68 | ) 69 | } 70 | export default (props)=>{ 71 | let pro_res,pro_rej; 72 | let pro = new Promise((resolve, reject)=>{ 73 | pro_res = resolve; 74 | pro_rej = reject; 75 | }); 76 | let form; 77 | const init = (f)=>{ 78 | form = f; 79 | } 80 | Modal.confirm({ 81 | width: '60%', 82 | maskClosable:false, 83 | title:'', 84 | okText: '确定', 85 | ...props, 86 | style:{...props.style,minWidth:'300px'}, 87 | centered: false, // 不设置居中,否则min-width无效 88 | content: (), 89 | icon: '', 90 | footer: null, 91 | onCancel: (close)=>{ 92 | pro_rej(false) 93 | close(); 94 | }, 95 | onOk: (close)=>{ 96 | return new Promise((resolve,reject)=>{ 97 | let data = form.getFieldsValue(); 98 | form.validateFields(Object.keys(data)).then(async (values)=>{ 99 | try { 100 | let res = await addDemo(values); 101 | if ($iscode(res, true)) { 102 | pro_res(res.data) 103 | resolve() 104 | } else { 105 | reject() 106 | } 107 | } catch(e) { 108 | reject() 109 | } 110 | }).catch((error)=>{ 111 | reject() 112 | console.warn(error); 113 | }); 114 | 115 | }) 116 | } 117 | }) 118 | return pro; 119 | } -------------------------------------------------------------------------------- /.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const dotenv = require('dotenv'); 4 | const {appendWebpackPlugin,getPaths, edit} = require('@rescripts/utilities'); 5 | const WebpackBar = require('webpackbar'); 6 | const WebpackEnd = require('./plugins/webpack.end.js'); 7 | const resolve = dir => path.resolve(__dirname, dir); 8 | const theme = { 9 | "@primary-color": "#7546c9" 10 | } 11 | const getMode = ()=>{ 12 | let mode = process.env.NODE_ENV; 13 | process.argv.forEach((v,i)=>{ 14 | if(v === '--mode' && process.argv[i+1]){ 15 | mode = process.argv[i+1] 16 | } 17 | }) 18 | return mode; 19 | } 20 | const logConfig = config => { 21 | // let loader = config.module.rules.find(v=>v.oneOf); 22 | // console.log(loader.oneOf[1]); 23 | return config 24 | } 25 | const webpackBarPlugin = config => { 26 | return appendWebpackPlugin( 27 | new WebpackBar(), 28 | config 29 | ) 30 | } 31 | const webpackDotEnvPlugin = config => { 32 | let definePlugin = config.plugins.find(v=>v.constructor.name === 'DefinePlugin'); 33 | if(definePlugin) { 34 | let mode = '.env.'+getMode(); 35 | const envConfig = dotenv.parse(fs.readFileSync(resolve(mode))); 36 | for (const k in envConfig) { 37 | definePlugin.definitions['process.env'][k] = '"'+envConfig[k]+'"'; 38 | } 39 | } 40 | return config; 41 | } 42 | const webpackEndConfig = config => { 43 | return appendWebpackPlugin( 44 | new WebpackEnd('build','项目名称'), 45 | config 46 | ) 47 | } 48 | const webpackResolveAlias = config => { 49 | if(!config.resolve) { config.resolve = {}; } 50 | config.resolve.alias = { 51 | '@': resolve('src') 52 | }; 53 | return config; 54 | } 55 | const useAntd = (config) => { 56 | const styleLoaders = getPaths( 57 | // Styleloaders are in config.module.rules inside an object only containing the "oneOf" prop 58 | (inQuestion) => inQuestion && !!inQuestion.oneOf, 59 | config 60 | ) 61 | edit( 62 | (section) => { 63 | const loaders = section.oneOf 64 | // New style loaders should be added near the end of loaders, but before file-loader 65 | const fileLoaderIndex = loaders.findIndex(section => section.loader && section.loader.includes('file-loader')) 66 | const lessLoader = { 67 | test: /\.less$/, 68 | use: [ 69 | 'style-loader', 70 | 'css-loader', 71 | { 72 | loader: 'less-loader', 73 | options: { 74 | lessOptions: { 75 | // sourceMap: NODE_ENV === 'production' && GENERATE_SOURCEMAP !== 'false', 76 | javascriptEnabled: true, 77 | modifyVars: theme || {} 78 | } 79 | } 80 | } 81 | ] 82 | } 83 | loaders.splice(fileLoaderIndex, 0, lessLoader) 84 | return section 85 | }, 86 | styleLoaders, 87 | config 88 | ) 89 | return config 90 | } 91 | let configArray = [ 92 | ["use-eslint-config", ".eslintrc"], 93 | // Required for antd"s tree shaking babel-plugin-import 94 | ["use-babel-config", ".babelrc"], 95 | ['use-postcss-config'], 96 | // Provide a theme object based on this file 97 | // ["use-antd", { theme }], 98 | useAntd, 99 | webpackBarPlugin, 100 | webpackResolveAlias, 101 | webpackDotEnvPlugin 102 | ] 103 | if(getMode() === 'production') { 104 | configArray.push(webpackEndConfig); 105 | } 106 | configArray.push(logConfig); 107 | module.exports = configArray -------------------------------------------------------------------------------- /src/style/base.less: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /*! 4 | * @名称:base.css 5 | * @功能:1、重设浏览器默认样式 6 | * 2、设置通用原子类 7 | */ 8 | /* 防止用户自定义背景颜色对网页的影响,添加让用户可以自定义字体 */ 9 | html { 10 | background: white; 11 | color: black; 12 | font-size: 10px; 13 | } 14 | 15 | /* 内外边距通常让各个浏览器样式的表现位置不同 */ 16 | body, 17 | div, 18 | dl, 19 | dt, 20 | dd, 21 | ul, 22 | ol, 23 | li, 24 | h1, 25 | h2, 26 | h3, 27 | h4, 28 | h5, 29 | h6, 30 | pre, 31 | code, 32 | form, 33 | fieldset, 34 | legend, 35 | input, 36 | textarea, 37 | p, 38 | blockquote, 39 | th, 40 | td, 41 | hr, 42 | button, 43 | article, 44 | aside, 45 | details, 46 | figcaption, 47 | figure, 48 | footer, 49 | header, 50 | hgroup, 51 | menu, 52 | nav, 53 | section { 54 | margin: 0; 55 | padding: 0; 56 | } 57 | 58 | /* 要注意表单元素并不继承父级 font 的问题 */ 59 | body, 60 | button, 61 | input, 62 | select, 63 | textarea { 64 | font: 14px \5b8b\4f53, arial, sans-serif; 65 | } 66 | 67 | input, 68 | select, 69 | textarea { 70 | font-size: 100%; 71 | } 72 | 73 | /* 去掉 table cell 的边距并让其边重合 */ 74 | table { 75 | border-collapse: collapse; 76 | border-spacing: 0; 77 | } 78 | 79 | /* ie bug:th 不继承 text-align */ 80 | th { 81 | text-align: inherit; 82 | } 83 | 84 | /* 去除默认边框 */ 85 | fieldset, 86 | img { 87 | border: none; 88 | } 89 | 90 | /* ie6 7 8(q) bug 显示为行内表现 */ 91 | iframe { 92 | display: block; 93 | } 94 | 95 | /* 去掉 firefox 下此元素的边框 */ 96 | abbr, 97 | acronym { 98 | border: none; 99 | font-variant: normal; 100 | } 101 | 102 | /* 一致的 del 样式 */ 103 | del { 104 | text-decoration: line-through; 105 | } 106 | 107 | address, 108 | caption, 109 | cite, 110 | code, 111 | dfn, 112 | em, 113 | th, 114 | var { 115 | font-style: normal; 116 | font-weight: 500; 117 | } 118 | 119 | /* 去掉列表前的标识,li 会继承 */ 120 | ol, 121 | ul { 122 | list-style: none; 123 | } 124 | 125 | /* 对齐是排版最重要的因素,别让什么都居中 */ 126 | caption, 127 | th { 128 | text-align: left; 129 | } 130 | 131 | /* 来自yahoo,让标题都自定义,适应多个系统应用 */ 132 | h1, 133 | h2, 134 | h3, 135 | h4, 136 | h5, 137 | h6 { 138 | font-size: 100%; 139 | font-weight: 500; 140 | } 141 | 142 | h1 { 143 | font-size: 2.6rem; 144 | } 145 | 146 | h2 { 147 | font-size: 2.4rem; 148 | } 149 | 150 | h3 { 151 | font-size: 2rem; 152 | } 153 | 154 | q:before, 155 | q:after { 156 | content: ''; 157 | } 158 | 159 | /* 统一上标和下标 */ 160 | sub, 161 | sup { 162 | font-size: 75%; 163 | line-height: 0; 164 | position: relative; 165 | vertical-align: baseline; 166 | } 167 | 168 | sup { 169 | top: -0.5em; 170 | } 171 | 172 | sub { 173 | bottom: -0.25em; 174 | } 175 | 176 | // /* 让链接在 hover 状态下显示下划线 */ 177 | // a:hover { 178 | // text-decoration: underline; 179 | // } 180 | 181 | /* 默认不显示下划线,保持页面简洁 */ 182 | ins, 183 | a { 184 | text-decoration: none; 185 | } 186 | 187 | /* 去除 ie6 & ie7 焦点点状线 */ 188 | a:focus, 189 | *:focus { 190 | outline: none; 191 | } 192 | 193 | /* 清除浮动 */ 194 | .clearfix:before, 195 | .clearfix:after { 196 | content: ''; 197 | display: block; 198 | } 199 | 200 | .clearfix:after { 201 | clear: both; 202 | overflow: hidden; 203 | } 204 | 205 | .clearfix { 206 | zoom: 1; 207 | /* for ie6 & ie7 */ 208 | } 209 | 210 | .clear { 211 | clear: both; 212 | display: block; 213 | font-size: 0; 214 | height: 0; 215 | line-height: 0; 216 | overflow: hidden; 217 | } 218 | 219 | /* 设置显示和隐藏,通常用来与 js 配合 */ 220 | .hide { 221 | display: none; 222 | } 223 | 224 | .block { 225 | display: block; 226 | } 227 | 228 | /* 设置浮动,减少浮动带来的 bug */ 229 | .fl, 230 | .fr { 231 | display: inline; 232 | } 233 | 234 | .fl { 235 | float: left; 236 | } 237 | 238 | .fr { 239 | float: right; 240 | } 241 | 242 | .mr15 { 243 | margin-right: 15px; 244 | } 245 | .ml15{ 246 | margin-left: 15px; 247 | } -------------------------------------------------------------------------------- /src/layout/AppHeader.jsx: -------------------------------------------------------------------------------- 1 | import React,{useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Menu, Dropdown, Layout, Avatar,message } from 'antd' 4 | import { GlobalOutlined,ArrowsAltOutlined,ShrinkOutlined, EditOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, SettingOutlined,UserOutlined } from '@ant-design/icons'; 5 | import HeaderBreadcrumb from './HeaderBreadcrumb'; 6 | import screenfull from 'screenfull'; 7 | import {langKeys} from '@/lang' 8 | console.log(langKeys) 9 | const { Header } = Layout 10 | 11 | const AppHeader = props => { 12 | let [isscreenfull,setIsscreenfull] = useState(false); 13 | let { menuClick, avatar, menuToggle, loginOut } = props 14 | const menu = ( 15 | 16 | 17 | 18 | 19 | 20 | 个人设置 21 | 22 | 23 | 24 | 系统设置 25 | 26 | 27 | 28 | 29 |
30 | 退出登录 31 |
32 |
33 |
34 | ) 35 | const screenfullChange = ()=>{ 36 | if (!screenfull.isEnabled) { 37 | message.warning('你的浏览器不支持全屏'); 38 | return false 39 | } 40 | setIsscreenfull(!isscreenfull); 41 | screenfull.toggle() 42 | } 43 | const toggleLanguageChange = (lang)=>{ 44 | props.toggleLanguage(lang); 45 | } 46 | return ( 47 |
48 |
49 | { 50 | menuToggle 51 | ? 52 | : 53 | } 54 |
55 | 56 |
57 |
58 |
59 |
60 | {isscreenfull?:} 61 |
62 |
63 | 66 | {langKeys.map(v=>{toggleLanguageChange(v.value)}} 70 | >{v.label})} 71 | 72 | )} 73 | > 74 |
75 | 76 |
77 |
78 |
79 |
80 | 81 |
82 | } src={avatar} alt='avatar' style={{ cursor: 'pointer' }} /> 83 |
84 |
85 |
86 |
87 |
88 | ) 89 | } 90 | 91 | AppHeader.propTypes = { 92 | menuClick: PropTypes.func, 93 | avatar: PropTypes.string, 94 | menuToggle: PropTypes.bool, 95 | loginOut: PropTypes.func 96 | } 97 | 98 | export default AppHeader 99 | -------------------------------------------------------------------------------- /src/views/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { connect } from "react-redux"; 4 | import { setUserInfo,setUserToken } from "@/store/actions"; 5 | import { Layout, Input, Form, Button, Divider } from 'antd'; 6 | import { UserOutlined, LockOutlined } from '@ant-design/icons'; 7 | import {sendLogin, sendUserInfo} from '@/api/login'; 8 | import {$iscode} from '@/utils/app'; 9 | import '@/style/views/login.less' 10 | /** 11 | class Login extends Component { 12 | static contextTypes = { 13 | $iscode: PropTypes.func 14 | } 15 | } 16 | 17 | class Login extends Component {} 18 | Login.contextTypes = { 19 | $iscode: PropTypes.func 20 | } 21 | 22 | const Login = (props,context)=>{} 23 | Login.contextTypes = { 24 | $iscode: PropTypes.func 25 | } 26 | * */ 27 | const Login = (props) => { 28 | const [loading, setLoading] = useState(false); 29 | const handleSubmitFinish = async values => { 30 | setLoading(true); 31 | try{ 32 | let res = await sendLogin({ 33 | username: values.username, 34 | password: values.password, 35 | }); 36 | setLoading(false); 37 | if($iscode(res,true)){ 38 | localStorage.setItem('token', res.data.token); 39 | props.setUserToken(res.data.token); 40 | res = await sendUserInfo(); 41 | if($iscode(res)){ 42 | localStorage.setItem('user', JSON.stringify(res.data)); 43 | props.setUserInfo(res.data); 44 | props.history.push('/'); 45 | } 46 | }; 47 | }catch(e){ 48 | setLoading(false); 49 | }; 50 | }; 51 | 52 | const handleSubmitFinishFailed = errorInfo => { 53 | console.log('Failed:', errorInfo); 54 | }; 55 | return ( 56 | 57 |
58 |
59 |

后台管理系统

60 | 61 |
65 | 69 | } 72 | /> 73 | 74 | 78 | } 82 | /> 83 | 84 | 85 | 88 | 89 |
90 |
91 |
92 |
93 | ) 94 | } 95 | export default withRouter(connect( 96 | (state) => ({}), 97 | (dispatch) => { 98 | return { 99 | setUserInfo(res) { 100 | dispatch(setUserInfo(res)); 101 | }, 102 | setUserToken(res) { 103 | dispatch(setUserToken(res)); 104 | }, 105 | }; 106 | } 107 | )(Login)); -------------------------------------------------------------------------------- /src/layout/SliderMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Menu } from 'antd' 4 | import { Link, withRouter } from 'react-router-dom' 5 | import * as Icon from '@ant-design/icons'; 6 | import {clearPageState} from '@/utils/pageState' 7 | 8 | // 处理 pathname 9 | const getOpenKeys = string => { 10 | let newStr = '', 11 | newArr = [], 12 | arr = string.split('/').map(i => '/' + i) 13 | for (let i = 1; i < arr.length - 1; i++) { 14 | newStr += arr[i] 15 | newArr.push(newStr) 16 | } 17 | return newArr 18 | } 19 | 20 | const getPathName = (pathname, menuTree,parentPath) => { 21 | for(let i = 0; i < menuTree.length; i++){ 22 | //路径包含 23 | if(pathname.search(menuTree[i].key) === 0){ 24 | //路径相等 25 | if(pathname === menuTree[i].key){ 26 | //隐藏菜单去父路径 27 | if(menuTree[i].hidden){ 28 | return parentPath; 29 | }else { 30 | return menuTree[i].key 31 | } 32 | } else if (menuTree[i].subs && menuTree[i].subs.length>0) { 33 | let str = getPathName(pathname, menuTree[i].subs ,menuTree[i].key); 34 | if(str !== ''){ 35 | return str; 36 | } 37 | } 38 | } 39 | } 40 | return '' 41 | } 42 | const CustomMenu = props => { 43 | //初始化 44 | const [openKeys,setOpenKeys] = useState([]); 45 | const [selectedKeys,setSelectedKeys] = useState([]); 46 | useEffect(() => { 47 | let { pathname } = props.location; 48 | setOpenKeys(getOpenKeys(pathname)); 49 | setSelectedKeys([getPathName(pathname, props.menu)]); 50 | }, [props]); 51 | // 只展开一个 SubMenu 52 | const onOpenChange = v => { 53 | setOpenKeys(prevState=>{ 54 | if (v.length === 0 || v.length === 1) { 55 | return v; 56 | } 57 | const latestOpenKey = v[v.length - 1] 58 | // 这里与定义的路由规则有关 59 | if (latestOpenKey.includes(v[0])) { 60 | return v; 61 | } else { 62 | return [latestOpenKey]; 63 | } 64 | }) 65 | 66 | } 67 | 68 | const onSelectChange = ({ key }) =>{ 69 | clearPageState(key); 70 | setSelectedKeys([key]); 71 | }; 72 | 73 | const renderMenuItem = ({ key, Icon, title }) => ( 74 | 75 | {Icon?:''}{title} 76 | 77 | ) 78 | 79 | // 循环遍历数组中的子项 subs ,生成子级 menu 80 | const renderSubMenu = ({ key, Icon, title, subs }) => { 81 | return ( 82 | {Icon?:''}{title}}> 85 | {subs && 86 | subs.map(item => { 87 | return item.subs && 88 | item.subs.length > 0 && 89 | item.subs.filter(v=>v.hidden).length!==item.subs.length ? 90 | renderSubMenu(item) : 91 | renderMenuItem(item) 92 | })} 93 | 94 | ) 95 | } 96 | let _open_keys = props.menuToggle?{}:{openKeys:openKeys} 97 | return ( 98 | 106 | {props.menu && 107 | props.menu.map(item => { 108 | if (item.icon) { 109 | item.Icon = Icon[item.icon] 110 | } 111 | return item.subs && 112 | item.subs.length > 0 && 113 | item.subs.filter(v=>v.hidden).length!==item.subs.length ? 114 | renderSubMenu(item) : 115 | renderMenuItem(item) 116 | })} 117 | 118 | ) 119 | } 120 | 121 | CustomMenu.propTypes = { 122 | menuToggle: PropTypes.bool.isRequired, 123 | menu: PropTypes.array.isRequired 124 | } 125 | 126 | export default withRouter(CustomMenu) 127 | -------------------------------------------------------------------------------- /src/layout/DefaultLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, {useReducer} from 'react' 2 | import {Route, Switch, Redirect, withRouter} from 'react-router-dom' 3 | import { connect } from "react-redux"; 4 | import {Layout, BackTop} from 'antd' 5 | import {$iscode,setHistory} from '@/utils/app'; 6 | import routes from '@/routes' 7 | import avatar from '@/assets/images/user.png' 8 | import '@/style/layout.less' 9 | import {setAppLanguage} from '@/store/actions' 10 | import AppHeader from './AppHeader.jsx' 11 | import AppAside from './AppAside.jsx' 12 | import AppFooter from './AppFooter.jsx' 13 | 14 | import {sendLogout} from '@/api/login'; 15 | 16 | const {Content} = Layout; 17 | 18 | const MENU_TOGGLE = 'menuToggle'; 19 | 20 | const reducer = (state, action) => { 21 | switch (action.type) { 22 | case MENU_TOGGLE: 23 | return {...state, menuToggle: !state.menuToggle}; 24 | default: 25 | return state 26 | } 27 | }; 28 | //转化当前用户导航菜单权限tree为一维数组list 29 | const getMenuList = (menuTree,menuList) => { 30 | for(let i = 0; i< menuTree.length; i++){ 31 | menuList.push(menuTree[i]); 32 | if(menuTree[i].subs && menuTree[i].subs.length>0){ 33 | getMenuList(menuTree[i].subs,menuList); 34 | } 35 | } 36 | }; 37 | //获取当前用户所有可以访问的路由权限 38 | const getRoutes = (routeList,menuTree) => { 39 | let menuList =[]; 40 | getMenuList(menuTree,menuList); 41 | return menuList.map(v=>{ 42 | let route = routeList.find(r => r.path === v.key); 43 | return route?route:false; 44 | }).filter(v=>!!v); 45 | }; 46 | //返回除了首页之外的面包屑 47 | const getBreadCrumb = (pathname,menuTree,crumb) => { 48 | // 首页返回false 49 | if(pathname === '/index') return false; 50 | // 递归遍历远端导航菜单tree 51 | for(let i = 0; i< menuTree.length; i++){ 52 | // 符合则添加到面包屑中 53 | if(pathname.search(menuTree[i].key) === 0){ 54 | if(menuTree[i].key === pathname){ 55 | crumb.unshift(menuTree[i].title); 56 | return true; 57 | }else { 58 | // 不符合如果有子集继续查找 59 | if(menuTree[i].subs && menuTree[i].subs.length>0){ 60 | let state = getBreadCrumb(pathname, menuTree[i].subs, crumb); 61 | if(state){ 62 | crumb.unshift(menuTree[i].title); 63 | return true; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | return false; 70 | }; 71 | 72 | const DefaultLayout = props => { 73 | // 暴露history对象 74 | setHistory(props.history); 75 | const [state, dispatch] = useReducer(reducer, {menuToggle: false}); 76 | 77 | if (props.userInfo.id === undefined) { 78 | return 79 | } 80 | // 获取远端用户菜单权限tree 81 | const menu = JSON.parse(JSON.stringify(props.userInfo.menus)); 82 | 83 | const menuClick = () => { 84 | dispatch({type: 'menuToggle'}) 85 | }; 86 | 87 | const loginOut = async () => { 88 | try{ 89 | let res = await sendLogout(); 90 | if($iscode(res,true)){ 91 | localStorage.clear(); 92 | props.history.push('/login'); 93 | } 94 | }catch(e){ 95 | localStorage.clear(); 96 | props.history.push('/login'); 97 | } 98 | }; 99 | // 获取面包屑 100 | let breadCrumb = []; 101 | getBreadCrumb(props.location.pathname,menu,breadCrumb); 102 | // 获取权限路由 103 | let routesMap = getRoutes(routes,menu); 104 | return ( 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {routesMap.map(item => { 113 | return ( 114 | }/> 119 | ) 120 | })} 121 | 122 | 123 | 124 | 125 | 126 | 127 | ) 128 | }; 129 | export default withRouter(connect( 130 | (state) => ({ 131 | userInfo: state.User.userInfo, 132 | language: state.App.lang, 133 | }), 134 | (dispatch) => { 135 | return { 136 | toggleLanguage(res) { 137 | dispatch(setAppLanguage(res)); 138 | }, 139 | }; 140 | } 141 | )(DefaultLayout)) 142 | -------------------------------------------------------------------------------- /src/views/Test/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React,{ useRef, useState, useEffect} from 'react' 2 | import {Layout,Table,Card,Input,Form,Button,Pagination, message} from 'antd' 3 | import {withRouter} from 'react-router-dom'; 4 | import {setPageState,getPageState} from '@/utils/pageState' 5 | import addModal from './AddModal' 6 | import {getList} from '@/api/demo' 7 | import {$iscode} from '@/utils/app' 8 | const Demo = (props)=>{ 9 | // 初始化默认筛选项数值 10 | let defSearch = { 11 | name1: '', 12 | name2:'', 13 | page: 1, 14 | size: 10 15 | } 16 | let form = useRef(); 17 | // 初始化 分页信息和筛选项信息 18 | let [total, setTotal] = useState(0); 19 | let [search,setSearch] = useState(defSearch); 20 | // 查看是否有留存状态,有则替换 21 | let pageState = getPageState(props.history.location.pathname); 22 | if(pageState) { 23 | setSearch(Object.assign(search, pageState)); 24 | } 25 | // 列表数据和列头配置 26 | let [data, setData] = useState([]) 27 | let [listLoading, setListLoading] = useState(false); 28 | let columns = [ 29 | { 30 | title: 'ID', 31 | dataIndex: 'id', 32 | key: 'id', 33 | }, 34 | { 35 | title: '姓名', 36 | dataIndex: 'name', 37 | key: 'name', 38 | }, 39 | { 40 | title: '年龄', 41 | dataIndex: 'age', 42 | key: 'age', 43 | }, 44 | { 45 | title: '地址', 46 | dataIndex: 'address', 47 | key: 'address', 48 | }, 49 | { 50 | title: '操作', 51 | key: 'action', 52 | render: (text, record) => ( 53 | 54 | 55 | 56 | ), 57 | }, 58 | ] 59 | // 页面跳转 60 | const pageLinkChange = ()=>{ 61 | setPageState(props.history.location.pathname,{...search}) 62 | props.history.push({ pathname : '/demo/demo/detail' }); 63 | } 64 | // 页面筛选项搜索 65 | const pageSearchChange = (data) => { 66 | setSearch({...search, ...data, page: 1}) 67 | } 68 | // 页面筛选项重置 69 | const pageSearchReset = () => { 70 | form.current.setFields(Object.keys(defSearch).map(v=>({name:v, value: defSearch[v]}))); 71 | // 重置后直接请求第一页数据 72 | // setSearch({...defSearch, page: 1, size: search.size}); 73 | } 74 | // 分页当前页切换 75 | const pageCurrentChange = (page, pageSize) => { 76 | setSearch({...search, page: page}); 77 | } 78 | // 分页当前页显示多少条切换 79 | const pageSizeChange = (current, size) => { 80 | setSearch({...search, page: 1, page: size}); 81 | } 82 | // 增加列表项模态框添加 83 | const actionAddModel = ()=>{ 84 | addModal({title: '添加'}).then((res)=>{ 85 | setData([res,...data.slice(0,-1)]) 86 | },()=>{}) 87 | } 88 | // 初始化数据 89 | const initData = async () => { 90 | setListLoading(true); 91 | try { 92 | let res = await getList(search); 93 | setListLoading(false); 94 | if ($iscode(res)) { 95 | setData(res.data); 96 | setTotal(res.total); 97 | } else { 98 | message.error(res.message) 99 | } 100 | } catch (e) { 101 | setListLoading(false); 102 | } 103 | } 104 | useEffect(()=>{ 105 | initData(); 106 | },[search]) 107 | return ( 108 | 109 | 110 |
111 | 112 |
113 |
119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
132 | 138 | 145 | 146 | 147 | ) 148 | } 149 | export default withRouter(Demo); 150 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 28 Copy 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/assets/images/login.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 21 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | --------------------------------------------------------------------------------