├── README.md
├── gulpfile.js
├── images
└── ex1.png
├── mock
├── modules
│ ├── index.js
│ ├── login
│ │ ├── login.js
│ │ └── users.json
│ └── util
│ │ └── UUID.js
└── server.js
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.js
├── Store.js
├── assets
├── css
│ └── common.css
├── i18n
│ ├── en.json
│ ├── parts
│ │ ├── en.json
│ │ ├── en_login.json
│ │ ├── en_overview.json
│ │ ├── en_topo.json
│ │ ├── zh.json
│ │ ├── zh_login.json
│ │ ├── zh_overview.json
│ │ └── zh_topo.json
│ └── zh.json
└── images
│ └── logo.svg
├── components
├── header
│ ├── header.module.css
│ ├── index.js
│ └── view.js
├── layout
│ ├── index.js
│ └── view.js
├── loader
│ ├── index.js
│ ├── loader.module.css
│ └── view.js
├── loading
│ ├── actionTypes.js
│ ├── actions.js
│ ├── index.js
│ ├── loading.module.css
│ ├── reducer.js
│ └── view.js
└── sidebar
│ ├── actionTypes.js
│ ├── actions.js
│ ├── data.json
│ ├── index.js
│ ├── reducer.js
│ ├── sidebar.module.css
│ └── view.js
├── enhancer
└── reset.js
├── index.js
├── login
├── actions.js
├── index.js
├── login.module.css
└── view.js
├── pages
├── home.module.css
├── index.js
├── overview
│ ├── dashboard.module.css
│ ├── index.js
│ └── view.js
├── topo
│ ├── index.js
│ └── view.js
└── view.js
├── plugins
├── bar-chart
│ └── view.js
├── index.js
├── line-chart
│ └── view.js
└── pie-chart
│ └── view.js
├── serviceWorker.js
└── util
├── fetch.js
└── localstorage.js
/README.md:
--------------------------------------------------------------------------------
1 | ## React Dev Dnd
2 | A demo to develop the UI page just by drag & drop plugins。
3 | ## Installation
4 | ```
5 | git clone https://github.com/sunnut/react-dev-dnd.git
6 | cd react-dev-dnd
7 | npm install
8 | npm run mock
9 | npm start
10 | ```
11 | ## Examples
12 | #### demo
13 | 
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var runSequence =require('run-sequence');
3 | var rimraf = require('gulp-rimraf');
4 | var merge = require('gulp-merge-json');
5 |
6 | //监听词条文件变更
7 | gulp.task('i18n-json-merge-watch',function(){
8 | gulp.watch('./src/assets/i18n/parts/*.json',function () {
9 | runSequence(
10 | 'i18n-json-merge'
11 | )
12 | });
13 | });
14 |
15 | //合并词条
16 | gulp.task('i18n-json-merge',function(){
17 | runSequence(
18 | 'i18n-json-clean',
19 | 'i18n-zh-json-merge',
20 | 'i18n-en-json-merge'
21 | );
22 | });
23 |
24 | gulp.task('i18n-json-clean', function() {
25 | return gulp.src('./src/assets/i18n/*.json', { read: false })
26 | .pipe(rimraf({ force: true }));
27 | });
28 |
29 | gulp.task('i18n-zh-json-merge',function(){
30 | gulp.src('./src/assets/i18n/parts/zh*.json')
31 | .pipe(merge({fileName: 'zh.json'}))
32 | .pipe(gulp.dest('./src/assets/i18n/'));
33 | });
34 |
35 | gulp.task('i18n-en-json-merge',function(){
36 | gulp.src('./src/assets/i18n/parts/en*.json')
37 | .pipe(merge({fileName: 'en.json'}))
38 | .pipe(gulp.dest('./src/assets/i18n/'));
39 | });
--------------------------------------------------------------------------------
/images/ex1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunnut/react-dev-dnd/45c34707233b739682059b9532d532b1e494d906/images/ex1.png
--------------------------------------------------------------------------------
/mock/modules/index.js:
--------------------------------------------------------------------------------
1 | var login = require('./login/login');
2 |
3 | module.exports = function (server) {
4 | login(server);
5 | };
--------------------------------------------------------------------------------
/mock/modules/login/login.js:
--------------------------------------------------------------------------------
1 | var userList = require('./users.json');
2 |
3 | module.exports = function (server) {
4 | server.post('/api/login', function (req, res) {
5 | req.body = {};
6 | res.send({
7 | data: req.body,
8 | message: null,
9 | success: true
10 | });
11 | });
12 | };
--------------------------------------------------------------------------------
/mock/modules/login/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "1",
4 | "name": "admin",
5 | "password": "123456"
6 | },
7 | {
8 | "id": "2",
9 | "name": "user",
10 | "password": "123456"
11 | }
12 | ]
--------------------------------------------------------------------------------
/mock/modules/util/UUID.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | var s = [];
3 | var hexDigits = "0123456789abcdef";
4 | for (var i = 0; i < 36; i++) {
5 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
6 | }
7 | s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
8 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
9 | s[8] = s[13] = s[18] = s[23] = "-";
10 |
11 | var uuid = s.join("");
12 | return uuid;
13 | };
--------------------------------------------------------------------------------
/mock/server.js:
--------------------------------------------------------------------------------
1 | var restify = require('restify');
2 | var partition = require('./modules/index');
3 | var server = restify.createServer();
4 |
5 | server.listen(8889, function () {
6 | console.log('');
7 | console.log('');
8 | console.log('Mock Server is Started at 8889!!!');
9 | console.log('');
10 | console.log('');
11 | });
12 | partition(server);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-easy-start",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "ant-design-pro": "^2.3.2",
7 | "antd": "^3.10.8",
8 | "gulp": "^3.9.1",
9 | "gulp-merge-json": "^1.3.1",
10 | "gulp-rimraf": "^0.2.2",
11 | "json-dup-key-validator": "^1.0.2",
12 | "moment": "^2.24.0",
13 | "react": "^16.7.0-alpha",
14 | "react-dom": "^16.7.0-alpha",
15 | "react-grid-layout": "^0.16.6",
16 | "react-intl": "^2.7.2",
17 | "react-loadable": "^5.5.0",
18 | "react-redux": "^5.1.1",
19 | "react-resizable": "^1.8.0",
20 | "react-router-dom": "^4.3.1",
21 | "react-router-redux": "^4.0.8",
22 | "react-scripts": "2.1.1",
23 | "react-sizeme": "^2.6.7",
24 | "redux": "^4.0.1",
25 | "redux-immutable-state-invariant": "^2.1.0",
26 | "redux-thunk": "^2.3.0",
27 | "restify": "^7.2.3",
28 | "run-sequence": "^2.2.1"
29 | },
30 | "scripts": {
31 | "start": "npm run i18n && react-scripts start",
32 | "build": "react-scripts build",
33 | "test": "react-scripts test",
34 | "eject": "react-scripts eject",
35 | "mock": "node mock/server.js",
36 | "i18n": "gulp i18n-json-merge"
37 | },
38 | "eslintConfig": {
39 | "extends": "react-app"
40 | },
41 | "browserslist": [
42 | ">0.2%",
43 | "not dead",
44 | "not ie <= 11",
45 | "not op_mini all"
46 | ],
47 | "proxy": "http://localhost:8889"
48 | }
49 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunnut/react-dev-dnd/45c34707233b739682059b9532d532b1e494d906/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React Dev Dnd
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/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 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router-dom';
3 | import { view as Loading } from './components/loading';
4 | import { view as Login } from './login';
5 | import { view as Home } from './pages';
6 |
7 | const App = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default App;
--------------------------------------------------------------------------------
/src/Store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, combineReducers, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { routerReducer } from 'react-router-redux';
4 | import resetEnhancer from './enhancer/reset.js';
5 | import { reducer as loadingReducer } from './components/loading';
6 | import { reducer as sidebarReducer } from './components/sidebar';
7 | import data from './components/sidebar/data';
8 |
9 | const originalReducers = {
10 | loading: loadingReducer,
11 | sidebar: sidebarReducer,
12 | routing: routerReducer
13 | };
14 | const reducer = combineReducers(originalReducers);
15 | const win = window;
16 | const middlewares = [thunk];
17 |
18 | if (process.env.NODE_ENV !== 'production') {
19 | middlewares.push(require('redux-immutable-state-invariant').default());
20 | }
21 |
22 | const storeEnhancers = compose(
23 | resetEnhancer,
24 | applyMiddleware(...middlewares),
25 | (win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f
26 | );
27 |
28 | const initialState = {sidebar: data};
29 | const store = createStore(reducer, initialState, storeEnhancers);
30 | store._reducers = originalReducers;
31 | export default store;
--------------------------------------------------------------------------------
/src/assets/css/common.css:
--------------------------------------------------------------------------------
1 | a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite,
2 | code, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4,
3 | h5, h6, header, hgroup, html, i, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, output, p,
4 | pre, q, ruby, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead,
5 | time, tr, tt, u, ul, var, video {
6 | border: 0;
7 | font: inherit;
8 | font-size: 100%;
9 | margin: 0;
10 | padding: 0;
11 | vertical-align: baseline;
12 | }
13 |
14 | *, :after, :before {
15 | box-sizing: border-box;
16 | }
17 |
18 | body, html, #root {
19 | width: 100%;
20 | height: 100%;
21 | }
22 |
23 | html {
24 | font-family: sans-serif;
25 | }
26 |
27 | body {
28 | font-weight: 400;
29 | font-size: 1em;
30 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;
31 | }
32 |
33 | a {
34 | color: #1890ff;
35 | background-color: transparent;
36 | text-decoration: none;
37 | outline: none;
38 | cursor: pointer;
39 | -webkit-transition: color .3s;
40 | transition: color .3s;
41 | -webkit-text-decoration-skip: objects;
42 | }
43 |
44 | a:hover, a:focus {
45 | text-decoration: none;
46 | }
47 |
48 | img {
49 | vertical-align: middle;
50 | border-style: none;
51 | box-sizing: content-box;
52 | max-width: 100%;
53 | }
54 |
55 | h1, h2, h3, h4, h5, h6 {
56 | color: inherit;
57 | font-weight: 600;
58 | line-height: 1.25;
59 | margin-bottom: 16px;
60 | margin-top: 1.5em;
61 | }
62 |
63 | h2 {
64 | font-size: 24px;
65 | }
66 |
67 | ol, ul {
68 | margin-bottom: 1em;
69 | margin-top: 0;
70 | padding-left: 2em;
71 | }
72 |
73 | .ant-dropdown-menu-item {
74 | min-width: 160px;
75 | }
76 |
77 | .antd-pro-charts-timeline-chart-timelineChart {
78 | margin-top: -22px;
79 | }
--------------------------------------------------------------------------------
/src/assets/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "Name",
3 | "username": "User name",
4 | "overview.title": "Overview",
5 | "topo.title": "Topo"
6 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "Name"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/en_login.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "User name"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/en_overview.json:
--------------------------------------------------------------------------------
1 | {
2 | "overview.title": "Overview"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/en_topo.json:
--------------------------------------------------------------------------------
1 | {
2 | "topo.title": "Topo"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "名称"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/zh_login.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "用户名"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/zh_overview.json:
--------------------------------------------------------------------------------
1 | {
2 | "overview.title": "总览"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/parts/zh_topo.json:
--------------------------------------------------------------------------------
1 | {
2 | "topo.title": "拓扑"
3 | }
--------------------------------------------------------------------------------
/src/assets/i18n/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "名称",
3 | "username": "用户名",
4 | "overview.title": "总览",
5 | "topo.title": "拓扑"
6 | }
--------------------------------------------------------------------------------
/src/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/header/header.module.css:
--------------------------------------------------------------------------------
1 | .header-wrapper {
2 | position: relative;
3 | padding: 0;
4 | height: 64px;
5 | background: #fff;
6 | box-shadow: 0 1px 4px rgba(0,21,41,.08);
7 | }
8 |
9 | .header-collapsed {
10 | padding: 22px 24px;
11 | height: 64px;
12 | cursor: pointer;
13 | transition: all .3s,padding 0s;
14 | font-size: 20px;
15 | }
16 |
17 | .header-collapsed:hover, .header-dropdown-link:hover {
18 | background-color: rgba(0, 0, 0, .025);
19 | }
20 |
21 | .header-user-info {
22 | float: right;
23 | margin-right: 20px;
24 | }
25 |
26 | .header-dropdown-link {
27 | display: inline-block;
28 | padding: 0 12px;
29 | height: 100%;
30 | text-decoration: none;
31 | color: rgba(0, 0, 0, .65);
32 | transition: all .3s;
33 | cursor: pointer;
34 | }
35 |
36 | .header-dropdown-link i:first-child {
37 | font-size: 20px !important;
38 | }
--------------------------------------------------------------------------------
/src/components/header/index.js:
--------------------------------------------------------------------------------
1 | import view from './view';
2 |
3 | export {view};
--------------------------------------------------------------------------------
/src/components/header/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Dropdown, Icon, Menu, Tooltip } from 'antd';
4 | import { Link } from 'react-router-dom';
5 | import * as LocalStorage from '../../util/localstorage';
6 | import { actions } from '../sidebar';
7 | import styles from './header.module.css';
8 |
9 | const menu = (
10 |
23 | );
24 |
25 | const Header = ({collapsed, setCollapsed, addMenu}) => {
26 | return (
27 |
28 |
29 | setCollapsed(!collapsed)}>
30 |
31 |
32 |
33 |
34 | addMenu({
35 | "icon": "area-chart",
36 | "key": "menu-new-" + new Date().getTime(),
37 | "label": "New Menu",
38 | "url": "/home/overview/test"
39 | })}>
40 |
41 |
42 |
43 |
44 |
45 |
46 | {LocalStorage.get('TA-username') }
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | const mapDispatchToProps = (dispatch, props) => ({
55 | collapsed: props.collapsed,
56 | setCollapsed: props.setCollapsed,
57 | addMenu: (menuItem) => {
58 | dispatch(actions.addMenu(menuItem));
59 | }
60 | });
61 |
62 | export default connect(null, mapDispatchToProps)(Header);
--------------------------------------------------------------------------------
/src/components/layout/index.js:
--------------------------------------------------------------------------------
1 | import view from './view';
2 |
3 | export {view};
--------------------------------------------------------------------------------
/src/components/layout/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactGridLayout from 'react-grid-layout';
3 | import sizeMe from 'react-sizeme';
4 |
5 | let lastGridWidth = 1200;
6 |
7 | const GridWrapper = ({
8 | size,
9 | layout,
10 | onLayoutChange,
11 | children,
12 | onDragStop,
13 | onResize,
14 | onResizeStop,
15 | onWidthChange,
16 | className,
17 | isResizable,
18 | isDraggable,
19 | }) => {
20 | const width = size.width > 0 ? size.width : lastGridWidth;
21 |
22 | if (width !== lastGridWidth) {
23 | lastGridWidth = width;
24 | }
25 |
26 | return (
27 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | export default sizeMe({ monitorWidth: true })(GridWrapper);
--------------------------------------------------------------------------------
/src/components/loader/index.js:
--------------------------------------------------------------------------------
1 | import view from './view';
2 |
3 | export {view};
--------------------------------------------------------------------------------
/src/components/loader/loader.module.css:
--------------------------------------------------------------------------------
1 | .loader-wrapper {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | min-height: 30px;
6 | }
7 |
8 | .loader-content {
9 | position: absolute;
10 | top: 50%;
11 | left: 50%;
12 | margin-top: -10px;
13 | margin-left: -20px;
14 | }
--------------------------------------------------------------------------------
/src/components/loader/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spin } from 'antd';
3 | import styles from './loader.module.css';
4 |
5 | const loader = () => {
6 | return (
7 |
12 | );
13 | };
14 |
15 | export default loader;
--------------------------------------------------------------------------------
/src/components/loading/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const SHOW_LOADING = 'LOADING/SHOW';
2 | export const HIDE_LOADING = 'LOADING/HIDE';
--------------------------------------------------------------------------------
/src/components/loading/actions.js:
--------------------------------------------------------------------------------
1 | import { HIDE_LOADING, SHOW_LOADING } from './actionTypes';
2 |
3 | export const showLoading = () => ({
4 | type: SHOW_LOADING
5 | });
6 |
7 | export const hideLoading = () => ({
8 | type: HIDE_LOADING
9 | });
--------------------------------------------------------------------------------
/src/components/loading/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import reducer from './reducer';
3 | import view from './view';
4 |
5 | export {actions, reducer, view};
--------------------------------------------------------------------------------
/src/components/loading/loading.module.css:
--------------------------------------------------------------------------------
1 | .loading-wrapper {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | z-index: 1050;
8 | -webkit-overflow-scrolling: touch;
9 | outline: 0;
10 | background: rgba(0, 0, 0, 0.7);
11 | }
12 |
13 | .loading-content {
14 | position: absolute;
15 | left: 50%;
16 | top: 50%;
17 | margin-left: -100px;
18 | margin-top: -10px;
19 | width: 200px;
20 | height: 20px;
21 | z-index: 20000;
22 | text-align: center;
23 | }
--------------------------------------------------------------------------------
/src/components/loading/reducer.js:
--------------------------------------------------------------------------------
1 | import { HIDE_LOADING, SHOW_LOADING } from './actionTypes';
2 |
3 | export default (state = {show: false}, action) => {
4 | switch (action.type) {
5 | case SHOW_LOADING:
6 | return {show: true};
7 | case HIDE_LOADING:
8 | return {show: false};
9 | default:
10 | return state;
11 | }
12 | };
--------------------------------------------------------------------------------
/src/components/loading/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Spin } from 'antd';
4 | import styles from './loading.module.css';
5 |
6 | const Loading = (props) => {
7 | const loadingStyle = {
8 | display: props.show ? 'block' : 'none'
9 | };
10 |
11 | return (
12 |
17 | );
18 | };
19 |
20 | const mapStateToProps = (state) => {
21 | const loadingData = state.loading;
22 |
23 | return {
24 | show: loadingData.show
25 | };
26 | };
27 |
28 | export default connect(mapStateToProps)(Loading);
--------------------------------------------------------------------------------
/src/components/sidebar/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_MENU = 'MENU/ADD';
--------------------------------------------------------------------------------
/src/components/sidebar/actions.js:
--------------------------------------------------------------------------------
1 | import {ADD_MENU} from './actionTypes';
2 |
3 | export const addMenu = (menu) => ({
4 | type: ADD_MENU,
5 | value: menu
6 | });
--------------------------------------------------------------------------------
/src/components/sidebar/data.json:
--------------------------------------------------------------------------------
1 | [{
2 | "icon": "home",
3 | "key": "overview",
4 | "label": "总览",
5 | "url": "/home/overview"
6 | }, {
7 | "icon": "fork",
8 | "key": "topo",
9 | "label": "拓扑",
10 | "url": "/home/topo"
11 | }, {
12 | "icon": "area-chart",
13 | "key": "sub-res",
14 | "label": "一级菜单",
15 | "children": [{
16 | "key": "rrs",
17 | "label": "二级菜单001",
18 | "url": "/home/rrs"
19 | }, {
20 | "key": "hms",
21 | "label": "二级菜单002",
22 | "url": "/home/hms"
23 | }, {
24 | "key": "mm",
25 | "label": "二级菜单003",
26 | "url": "/home/mm"
27 | }]
28 | }, {
29 | "icon": "setting",
30 | "key": "setting",
31 | "label": "设置",
32 | "url": "/home/setting"
33 | }]
--------------------------------------------------------------------------------
/src/components/sidebar/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import reducer from './reducer';
3 | import view from './view';
4 |
5 | export {actions, reducer, view};
--------------------------------------------------------------------------------
/src/components/sidebar/reducer.js:
--------------------------------------------------------------------------------
1 | import {ADD_MENU} from './actionTypes';
2 |
3 | export default (state = [], action) => {
4 | switch (action.type) {
5 | case ADD_MENU:
6 | return [action.value, ...state];
7 | default:
8 | return state;
9 | }
10 | };
--------------------------------------------------------------------------------
/src/components/sidebar/sidebar.module.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | position: relative;
3 | padding-left: 14px;
4 | height: 64px;
5 | line-height: 64px;
6 | transition: all .3s;
7 | background: #002140;
8 | overflow: hidden;
9 | }
10 |
11 | .logo img {
12 | display: inline-block;
13 | height: 36px;
14 | }
15 |
16 | .logo h1 {
17 | display: inline-block;
18 | margin: 0;
19 | color: #fff;
20 | vertical-align: middle;
21 | font-size: 20px;
22 | font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif;
23 | font-weight: 600;
24 | }
--------------------------------------------------------------------------------
/src/components/sidebar/view.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import { Icon, Menu } from 'antd';
5 | import styles from './sidebar.module.css';
6 | import logo from '../../assets/images/logo.svg';
7 | const { SubMenu } = Menu;
8 |
9 | const Sidebar = ({data, collapsed}) => {
10 | const [current, setCurrent] = useState('overview');
11 |
12 | return (
13 |
14 |
20 |
56 |
57 | );
58 | };
59 |
60 | const mapStateToProps = (state, props) => ({
61 | data: state.sidebar,
62 | collapsed: props.collapsed
63 | });
64 |
65 | export default connect(mapStateToProps)(Sidebar);
--------------------------------------------------------------------------------
/src/enhancer/reset.js:
--------------------------------------------------------------------------------
1 | const RESET_ACTION_TYPE = '@@RESET';
2 |
3 | const resetReducerCreator = (reducer, resetState) => (state, action) => {
4 | if (action.type === RESET_ACTION_TYPE) {
5 | return resetState;
6 | } else {
7 | return reducer(state, action);
8 | }
9 | };
10 |
11 | const reset = (createStore) => (reducer, preloadedState, enhancer) => {
12 | const store = createStore(reducer, preloadedState, enhancer);
13 |
14 | const resetInner = (resetReducer, resetState) => {
15 | const newReducer = resetReducerCreator(resetReducer, resetState);
16 | store.replaceReducer(newReducer);
17 | store.dispatch({type: RESET_ACTION_TYPE, state: resetState});
18 | };
19 |
20 | return {
21 | ...store,
22 | resetInner
23 | };
24 | };
25 |
26 | export default reset;
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 | import { LocaleProvider } from 'antd';
6 | import zh_CN from 'antd/lib/locale-provider/zh_CN';
7 | import en_US from 'antd/lib/locale-provider/en_US';
8 | import moment from 'moment';
9 | import 'moment/locale/zh-cn';
10 | import 'moment/locale/en-au';
11 | import { addLocaleData, IntlProvider } from 'react-intl';
12 | import en from 'react-intl/locale-data/en';
13 | import zh from 'react-intl/locale-data/zh';
14 | import ZH_WORDS from './assets/i18n/zh.json';
15 | import EN_WORDS from './assets/i18n/en.json';
16 | import store from './Store.js';
17 | import App from './App';
18 | import 'antd/dist/antd.css';
19 | import 'ant-design-pro/dist/ant-design-pro.css';
20 | import 'react-grid-layout/css/styles.css';
21 | import 'react-resizable/css/styles.css';
22 | import './assets/css/common.css';
23 |
24 | addLocaleData([...en, ...zh]);
25 |
26 | const userLang = navigator.language || '';
27 | const language = userLang.toLowerCase().substr(0, 2);
28 | let localAntD;
29 | let locale;
30 | let messages;
31 |
32 | if (language === 'zh') {
33 | locale = 'zh';
34 | messages = ZH_WORDS;
35 | moment.locale('zh-cn');
36 | localAntD = zh_CN;
37 | } else if (language === 'en') {
38 | locale = 'en';
39 | messages = EN_WORDS;
40 | moment.locale('en-au');
41 | localAntD = en_US;
42 | }
43 |
44 | ReactDOM.render(
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ,
54 | document.getElementById('root')
55 | );
--------------------------------------------------------------------------------
/src/login/actions.js:
--------------------------------------------------------------------------------
1 | import { actions as loadingActions } from '../components/loading/index';
2 | import * as Fetch from '../util/fetch';
3 | import * as LocalStorage from '../util/localstorage';
4 |
5 | export const login = (formVal, history) => {
6 | return (dispatch) => {
7 | dispatch(loadingActions.showLoading());
8 |
9 | Fetch.post('/api/login', formVal).then((response) => {
10 | dispatch(loadingActions.hideLoading());
11 |
12 | if (response) {
13 | LocalStorage.put('TA-username', formVal.userName);
14 | // 没有发生异常,跳转至主页
15 | history.push('/home/overview');
16 | }
17 | });
18 | };
19 | };
--------------------------------------------------------------------------------
/src/login/index.js:
--------------------------------------------------------------------------------
1 | import * as actions from './actions';
2 | import view from './view';
3 |
4 | export {actions, view};
--------------------------------------------------------------------------------
/src/login/login.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | position: fixed;
3 | width: 100%;
4 | min-height: 50px;
5 | padding: 8px 0;
6 | color: #fff;
7 | background: #20232a;
8 | }
9 |
10 | .header-wrapper {
11 | max-width: 1100px;
12 | margin: 0 auto;
13 | padding: 0 20px;
14 | }
15 |
16 | .header-wrapper header {
17 | position: relative;
18 | display: flex;
19 | flex-flow: row nowrap;
20 | text-align: left;
21 | }
22 |
23 | .header-wrapper header a {
24 | display: flex;
25 | height: 34px;
26 | align-items: center;
27 | border: 0;
28 | color: #fff;
29 | flex-flow: row nowrap;
30 | }
31 |
32 | .header-wrapper header img {
33 | height: 100%;
34 | margin-right: 10px;
35 | }
36 |
37 | .header-wrapper header h2 {
38 | display: block;
39 | position: relative;
40 | line-height: 18px;
41 | margin: 0;
42 | font-size: 1.25em;
43 | white-space: nowrap;
44 | }
45 |
46 | .nav-wrapper {
47 | position: relative;
48 | height: 34px;
49 | margin-left: auto;
50 | }
51 |
52 | .nav-wrapper nav {
53 | position: fixed;
54 | top: 0;
55 | right: 0;
56 | bottom: auto;
57 | left: 0;
58 | box-sizing: border-box;
59 | }
60 |
61 | @media only screen and (min-width: 1024px) {
62 | .nav-wrapper nav {
63 | position: relative;
64 | right: auto;
65 | top: auto;
66 | width: auto;
67 | height: auto;
68 | background: none;
69 | }
70 | }
71 |
72 | .nav-wrapper nav ul {
73 | display: flex;
74 | flex-wrap: nowrap;
75 | list-style: none;
76 | margin-top: 50px;
77 | padding: 0;
78 | width: 100%;
79 | background: #61dafb;
80 | box-sizing: border-box;
81 | color: #fff;
82 | }
83 |
84 | @media only screen and (min-width: 1024px) {
85 | .nav-wrapper nav ul {
86 | display: flex;
87 | flex-flow: row nowrap;
88 | margin: 0;
89 | padding: 0;
90 | width: auto;
91 | background: none;
92 | }
93 | }
94 |
95 | .nav-wrapper nav ul li {
96 | flex: 1 1 auto;
97 | margin: 0;
98 | text-align: center;
99 | white-space: nowrap;
100 | }
101 |
102 | .nav-wrapper nav ul li a {
103 | display: flex;
104 | margin: 0;
105 | padding: 10px;
106 | align-items: center;
107 | box-sizing: border-box;
108 | color: inherit;
109 | font-size: .9em;
110 | height: 50px;
111 | justify-content: center;
112 | transition: background-color .3s;
113 | }
114 |
115 | .nav-wrapper nav ul li a:hover {
116 | color: #ffffff;
117 | }
118 |
119 | @media only screen and (min-width: 1024px) {
120 | .nav-wrapper nav ul li a {
121 | display: flex;
122 | margin: 0;
123 | padding: 6px 10px;
124 | height: 32px;
125 | line-height: 1.2em;
126 | border: 0;
127 | color: hsla(0,0%,100%,.8);
128 | font-size: 1em;
129 | font-weight: 300;
130 | }
131 | }
132 |
133 | .content {
134 | position: absolute;
135 | left: 50%;
136 | top: 50%;
137 | margin-left: -200px;
138 | margin-top: -250px;
139 | width: 400px;
140 | height: 500px;
141 | padding: 20px;
142 | color: #EEE;
143 | }
144 |
145 | .login-form h3 {
146 | height: 20px;
147 | line-height: 20px;
148 | padding: 0 0 35px 0;
149 | text-align: center;
150 | font: 20px "microsoft yahei",Helvetica,Tahoma,Arial,"Microsoft jhengHei",sans-serif;
151 | }
152 |
153 | .login-form-forgot {
154 | float: right;
155 | }
156 |
157 | .login-form-button {
158 | width: 100%;
159 | }
160 |
161 | .footer {
162 | position: fixed;
163 | bottom: 0;
164 | width: 100%;
165 | height: 70px;
166 | font-size: 12px;
167 | line-height: 70px;
168 | color: #999;
169 | text-align: center;
170 | clear: both;
171 | }
--------------------------------------------------------------------------------
/src/login/view.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Checkbox, Button, Form, Icon, Input } from 'antd';
3 | import { connect } from 'react-redux';
4 | import { actions as loginActions } from './index';
5 | import logo from '../assets/images/logo.svg';
6 | import styles from './login.module.css';
7 | const FormItem = Form.Item;
8 |
9 | const LoginPage = ({login}) => {
10 | let userNameInput = null;
11 | const [userName, setUserName] = useState('');
12 | const [password, setPassword] = useState('');
13 |
14 | const emitEmptyUserName = () => {
15 | userNameInput.focus();
16 | setUserName('');
17 | };
18 |
19 | const gotoLogin = (e) => {
20 | e.preventDefault();
21 | login({userName, password});
22 | };
23 |
24 | const userNameSuffix = userName ? : null;
25 |
26 | return (
27 | <>
28 |
50 |
84 |
85 | 版权所有 © XXX有限公司 2018
86 |
87 | >
88 | );
89 | };
90 |
91 | const mapDispachToProps = (dispatch, props) => ({
92 | login: (formValue) => {
93 | // 等待
94 | dispatch(loginActions.login(formValue, props.history));
95 | }
96 | });
97 |
98 | export default connect(null, mapDispachToProps)(LoginPage);
--------------------------------------------------------------------------------
/src/pages/home.module.css:
--------------------------------------------------------------------------------
1 | .content-wrapper {
2 | min-height: 100vh !important;
3 | }
4 |
5 | .header {
6 | padding: 0 !important;
7 | width: 100%;
8 | }
9 |
10 | .content {
11 | padding-top: 0;
12 | margin: 24px;
13 | }
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import view from './view';
2 |
3 | export {view};
--------------------------------------------------------------------------------
/src/pages/overview/dashboard.module.css:
--------------------------------------------------------------------------------
1 | .toolbar-wrapper {
2 | padding: 8px;
3 | margin-bottom: 10px;
4 | border: 1px solid #e6e9ed;
5 | background-color: #ffffff;
6 | }
7 |
8 | .toolbar {
9 | align-items: center;
10 | }
11 |
12 | .toolbar i {
13 | display: inline-block;
14 | margin: 0 6px;
15 | width: 27px;
16 | height: 27px;
17 | line-height: 27px;
18 | text-align: center;
19 | vertical-align: middle;
20 | cursor: pointer;
21 | }
22 |
23 | .toolbar i:hover {
24 | border: 1px solid #e6e9ed;
25 | }
--------------------------------------------------------------------------------
/src/pages/overview/index.js:
--------------------------------------------------------------------------------
1 | import Loadable from 'react-loadable';
2 | import { view as Loader } from '../../components/loader';
3 |
4 | const view = Loadable({
5 | loader: () => import('./view'),
6 | loading: Loader
7 | });
8 |
9 | export {view};
--------------------------------------------------------------------------------
/src/pages/overview/view.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect, useState } from 'react';
2 | import { Card, Form, Input, Icon, Modal, Tooltip } from 'antd';
3 | import { view as GridLayout } from '../../components/layout';
4 | import { pluginMap, pluginInfo } from '../../plugins';
5 | import styles from './dashboard.module.css';
6 |
7 | const FormItem = Form.Item;
8 |
9 | //================================================================
10 | // 获取点击事件源
11 | //================================================================
12 | const getEventTarget = (e) => {
13 | let targetEle = e.target;
14 |
15 | if (targetEle.nodeName === 'svg') {
16 | targetEle = targetEle.parentNode;
17 | } else if (targetEle.nodeName === 'path') {
18 | targetEle = targetEle.parentNode.parentNode;
19 | }
20 |
21 | return targetEle;
22 | };
23 |
24 | const formItemLayout = {
25 | labelCol: {
26 | xs: { span: 24 },
27 | sm: { span: 6 },
28 | },
29 | wrapperCol: {
30 | xs: { span: 24 },
31 | sm: { span: 14 },
32 | },
33 | };
34 |
35 | //================================================================
36 | // Dashboard组件
37 | //================================================================
38 | const OverviewComponent = (props) => {
39 | // 页面ID
40 | let pageName = props.match.params.id;
41 | //let currentPanelId;
42 | const [visible, setVisible] = useState(false);
43 | const [panelList, setPanelList] = useState([]);
44 |
45 | useEffect(
46 | () => {
47 | setTimeout(() => {
48 | setPanelList([{
49 | id: 'b',
50 | title: '折线图',
51 | type: 'line-chart',
52 | sql: '123',
53 | gridPos: {x: 0, y: 0, w: 8, h: 8}
54 | }, {
55 | id: 'a',
56 | title: '柱状图',
57 | type: 'bar-chart',
58 | sql: '123',
59 | gridPos: {x: 8, y: 0, w: 8, h: 8}
60 | }, {
61 | id: 'c',
62 | title: '饼图',
63 | type: 'pie-chart',
64 | sql: '123',
65 | gridPos: {x: 16, y: 0, w: 8, h: 8}
66 | }]);
67 | }, 500)
68 | },
69 | [pageName]
70 | );
71 |
72 | let panelMap = {};
73 |
74 | //================================================================
75 | // 根据panel信息生成其排版位置信息
76 | //================================================================
77 | const buildLayout = function() {
78 | const layout = [];
79 |
80 | for (let panel of panelList) {
81 | let stringId = panel.id.toString();
82 | panelMap[stringId] = panel;
83 |
84 | if (!panel.gridPos) {
85 | continue;
86 | }
87 |
88 | let panelPos = {
89 | i: stringId,
90 | x: panel.gridPos.x,
91 | y: panel.gridPos.y,
92 | w: panel.gridPos.w,
93 | h: panel.gridPos.h,
94 | };
95 |
96 | layout.push(panelPos);
97 | }
98 |
99 | return layout;
100 | };
101 |
102 | //================================================================
103 | // 新增Panel
104 | //================================================================
105 | const addPanel = function(e) {
106 | e.preventDefault();
107 | let targetEle = getEventTarget(e);
108 |
109 | const panelType = targetEle.getAttribute('data-id');
110 | setPanelList([{
111 | id: '' + new Date().getTime(),
112 | title: 'New Chart',
113 | type: panelType,
114 | sql: '1232',
115 | gridPos: {x: 0, y: 0, w: 8, h: 8}
116 | }, ...panelList]);
117 | return false;
118 | };
119 |
120 | //================================================================
121 | // 移除Panel
122 | //================================================================
123 | const removePanel = function(e) {
124 | e.preventDefault();
125 | let targetEle = getEventTarget(e);
126 | const panelId = targetEle.getAttribute('data-id');
127 | setPanelList(panelList.filter(x => x.id !== panelId));
128 | return false;
129 | };
130 |
131 | //================================================================
132 | // 编辑Panel
133 | //================================================================
134 | const editPanel = function(e) {
135 | e.preventDefault();
136 | //let targetEle = getEventTarget(e);
137 | //currentPanelId = targetEle.getAttribute('data-id');
138 | setVisible(true);
139 | return false;
140 | };
141 |
142 | //================================================================
143 | // 提交编辑Panel
144 | //================================================================
145 | const editSubmit = function () {
146 | // null
147 | };
148 |
149 | //================================================================
150 | // 全屏或退出
151 | //================================================================
152 | const toggleFullScreen = function(e) {
153 | e.preventDefault();
154 | // let targetEle = getEventTarget(e);
155 | // const panelId = targetEle.getAttribute('data-id');
156 | return false;
157 | };
158 |
159 | //================================================================
160 | // Panel操作工具栏
161 | //================================================================
162 | const operateToolBar = (panelId) => {
163 | return (
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | );
179 | };
180 |
181 | return (
182 | <>
183 |
184 |
185 |
186 | {
187 | pluginInfo.map((plugin) => (
188 |
189 |
191 |
192 | ))
193 | }
194 | (<-- 点击添加组件,组件可拖拽 & 缩放)
195 |
196 |
197 |
198 |
199 |
200 |
201 | {
202 | panelList.length > 0 && (
203 | loading}>
204 |
209 | {
210 | panelList.map((panel) => {
211 | // const PanelCmp = React.lazy(() => import('../../plugins/' + panel.type + '/view'));
212 | const PanelCmp = pluginMap[panel.type];
213 |
214 | return (
215 |
220 |
221 |
222 | )
223 | })
224 | }
225 |
226 |
227 | )
228 | }
229 | setVisible(false)}
234 | >
235 |
245 |
246 | >
247 | );
248 | };
249 |
250 | export default OverviewComponent;
--------------------------------------------------------------------------------
/src/pages/topo/index.js:
--------------------------------------------------------------------------------
1 | import Loadable from 'react-loadable';
2 | import { view as Loader } from '../../components/loader';
3 |
4 | const view = Loadable({
5 | loader: () => import('./view'),
6 | loading: Loader
7 | });
8 |
9 | export {view};
--------------------------------------------------------------------------------
/src/pages/topo/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { injectIntl } from 'react-intl';
3 |
4 | const topoComponent = (props) => {
5 | const { intl } = props;
6 | return (
7 | { intl.formatMessage({id: 'topo.title'}) }
8 | );
9 | };
10 |
11 | export default injectIntl(topoComponent);
--------------------------------------------------------------------------------
/src/pages/view.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Route } from 'react-router-dom';
3 | import { view as Header } from '../components/header';
4 | import { view as Sidebar } from '../components/sidebar';
5 | import { view as Overview } from './overview';
6 | import { view as Topo } from './topo';
7 | import styles from './home.module.css';
8 |
9 | const HomePage = () => {
10 | const [collapsed, setCollapsed] = useState(false);
11 | const sidebarWidth = collapsed ? 80 : 256;
12 | const sidebarStyle = {
13 | flex: '0 0 ' + sidebarWidth + 'px',
14 | width: sidebarWidth + 'px'
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default HomePage;
--------------------------------------------------------------------------------
/src/plugins/bar-chart/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Bar } from 'ant-design-pro/lib/Charts';
3 |
4 | const salesData = [];
5 |
6 | for (let i = 0; i < 12; i += 1) {
7 | salesData.push({
8 | x: `${i + 1}月`,
9 | y: Math.floor(Math.random() * 1000) + 200,
10 | });
11 | }
12 |
13 | const BarComponent = (props) => {
14 | return (
15 | <>
16 |
17 | >
18 | );
19 | };
20 |
21 | export default BarComponent;
--------------------------------------------------------------------------------
/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | import Bar from './bar-chart/view';
2 | import LineChart from './line-chart/view';
3 | import PieChart from './pie-chart/view';
4 |
5 | // import React from 'react';
6 |
7 | // const BarPromise = import('./bar-chart/view');
8 | // const Bar = React.lazy(() => BarPromise);
9 | //
10 | // const LineChartPromise = import('./line-chart/view');
11 | // const LineChart = React.lazy(() => LineChartPromise);
12 | //
13 | // const PieChartPromise = import('./pie-chart/view');
14 | // const PieChart = React.lazy(() => PieChartPromise);
15 |
16 | const pluginInfo = [{
17 | name: 'bar-chart',
18 | icon: 'bar-chart',
19 | title: '柱状图'
20 | }, {
21 | name: 'line-chart',
22 | icon: 'line-chart',
23 | title: '线状图'
24 | }, {
25 | name: 'pie-chart',
26 | icon: 'pie-chart',
27 | title: '饼图'
28 | }];
29 |
30 | const pluginMap = {
31 | 'bar-chart': Bar,
32 | 'line-chart': LineChart,
33 | 'pie-chart': PieChart
34 | };
35 |
36 | export {pluginMap, pluginInfo};
--------------------------------------------------------------------------------
/src/plugins/line-chart/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MiniArea } from 'ant-design-pro/lib/Charts';
3 | import moment from 'moment';
4 |
5 | const visitData = [];
6 | const beginDay = new Date().getTime();
7 | for (let i = 0; i < 20; i += 1) {
8 | visitData.push({
9 | x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
10 | y: Math.floor(Math.random() * 100) + 10,
11 | });
12 | }
13 |
14 | const LineComponent = (props) => {
15 | return (
16 | <>
17 |
18 | >
19 | );
20 | };
21 |
22 | export default LineComponent;
--------------------------------------------------------------------------------
/src/plugins/pie-chart/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pie } from 'ant-design-pro/lib/Charts';
3 |
4 | const LineComponent = (props) => {
5 | return (
6 | <>
7 |
8 | >
9 | );
10 | };
11 |
12 | export default LineComponent;
--------------------------------------------------------------------------------
/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 http://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.1/8 is 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 http://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 http://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 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/util/fetch.js:
--------------------------------------------------------------------------------
1 | import { message } from 'antd';
2 |
3 | const request = (url, config) => {
4 | return fetch(url, config).then((res) => {
5 | if (!res.ok) {
6 | // 服务器异常返回
7 | throw Error('');
8 | }
9 |
10 | return res.json();
11 | }).then((resJson) => {
12 | if (!resJson.success) {
13 | // 项目内部认为的错误
14 | throw Error('');
15 | } else {
16 | return resJson;
17 | }
18 | }).catch(() => {
19 | // 公共错误处理
20 | message.error('内部错误,请重新登录');
21 | });
22 | };
23 |
24 | // GET请求
25 | export const get = (url) => {
26 | return request(url, {method: 'GET'});
27 | };
28 |
29 | // POST请求
30 | export const post = (url, data) => {
31 | return request(url, {
32 | body: JSON.stringify(data),
33 | headers: {
34 | 'content-type': 'application/json'
35 | },
36 | method: 'POST'
37 | });
38 | };
--------------------------------------------------------------------------------
/src/util/localstorage.js:
--------------------------------------------------------------------------------
1 | const local = window.localStorage;
2 | const json = JSON;
3 | const disable = local === undefined || json === undefined;
4 |
5 | const put = (key, data) => {
6 | if (disable) {
7 | return;
8 | }
9 |
10 | try {
11 | let jsonstr = json.stringify(data);
12 | local[key] = jsonstr;
13 | } catch (err) {
14 | // null
15 | }
16 | };
17 |
18 | const get = (key) => {
19 | if (disable) {
20 | return undefined;
21 | }
22 |
23 | try {
24 | let jsonstr = local[key];
25 | const data = json.parse(jsonstr);
26 | return data;
27 | } catch (err) {
28 | return undefined;
29 | }
30 | };
31 |
32 | const remove = (key) => {
33 | if (disable) {
34 | return;
35 | }
36 |
37 | try {
38 | local.removeItem(key);
39 | } catch (err) {
40 | // null
41 | }
42 | };
43 |
44 | const clear = () => {
45 | if (disable) {
46 | return;
47 | }
48 |
49 | local.clear();
50 | };
51 |
52 | export {put, get, remove, clear};
--------------------------------------------------------------------------------