├── .all-contributorsrc ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── config ├── PATHS.js ├── webpack.common.js ├── webpack.dev.js ├── webpack.doc.js └── webpack.prod.js ├── docs ├── favicon.png ├── index.html ├── main.06f7d04799b361eab94f.css ├── main.b2df5318.js ├── vendor.06f7d04799b361eab94f.css └── vendor.669b4d80.js ├── en.md ├── package-lock.json ├── package.json └── src ├── asset ├── image │ └── favicon.png ├── stylesheet │ ├── app.less │ └── cantd.less └── template │ └── index.html ├── component ├── AppFooter │ ├── index.js │ └── style.less ├── Loading │ └── index.js ├── OSSUpload │ ├── index.js │ └── style.less └── PlanForm │ └── index.js ├── doc.js ├── index.js ├── layout ├── App │ ├── index.js │ └── style.less ├── Root │ ├── DocRoot.js │ └── index.js └── SiderMenu │ └── index.js ├── page ├── 403 │ └── index.js ├── 404 │ └── index.js ├── 500 │ └── index.js ├── Account │ └── index.js ├── BasicForm │ ├── index.js │ └── style.less ├── DataReport │ ├── DataTable.js │ ├── LineChart.js │ ├── index.js │ └── style.less ├── Home │ ├── Dashboard.js │ ├── LineChart.js │ ├── SummaryCard.js │ ├── index.js │ └── style.less ├── Login │ ├── index.js │ └── style.less ├── Result │ ├── Error.js │ └── Success.js ├── SearchList │ ├── DataTable.js │ ├── FilterForm.js │ ├── index.js │ └── style.less └── StepForm │ ├── index.js │ └── style.less ├── store ├── BasicFormStore.js ├── HomeStore.js ├── LoginStore.js ├── ReportStore.js ├── SearchListStore.js ├── StepFormStore.js └── index.js └── util ├── api ├── ajax.js ├── errorHint.js ├── index.js └── mock.map.js ├── index.js └── login.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "antd-pro-mobx", 3 | "projectOwner": "gzgogo", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "LeoChowChina", 15 | "name": "Leo Chow", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/5009640?v=4", 17 | "profile": "https://github.com/LeoChowChina", 18 | "contributions": [ 19 | "translation" 20 | ] 21 | }, 22 | { 23 | "login": "soguagirl", 24 | "name": "soguagirl", 25 | "avatar_url": "https://avatars2.githubusercontent.com/u/7897851?v=4", 26 | "profile": "https://github.com/soguagirl", 27 | "contributions": [ 28 | "ideas" 29 | ] 30 | } 31 | ], 32 | "contributorsPerLine": 7 33 | } 34 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-0", "react"], 3 | 4 | "plugins": [ 5 | "syntax-dynamic-import", 6 | "react-hot-loader/babel", 7 | "transform-decorators-legacy", 8 | ["import", { "libraryName": "antd"}], 9 | ["import", { 10 | "libraryName": "ant-design-pro", 11 | "libraryDirectory": "lib", 12 | "style": true, 13 | "camel2DashComponentName": false 14 | }] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | config -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-ali/react", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "es6": true, 6 | "browser": true, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 7, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "rules": { 17 | "comma-dangle": ["error", "never"], 18 | "object-curly-spacing": ["error", "always"], 19 | "no-void": "warn", 20 | "new-cap": ["warn", { "newIsCap": true, "properties": false }], 21 | "no-plusplus": "warn", 22 | "no-mixed-operators": ["error",{"allowSamePrecedence": true}], 23 | "no-fallthrough": ["error", { "commentPattern": "break[\\s\\w]*omitted" }], 24 | "no-nested-ternary": "warn", 25 | "no-console": "off", 26 | "no-param-reassign": "off", 27 | "eqeqeq": "error", 28 | "react/prop-types": "off", 29 | "no-script-url": 0 30 | } 31 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist 3 | .DS_Store 4 | yarn-error.log 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zhen.gong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./en.md) 2 | 3 |

Antd Pro Mobx

4 | 5 |

6 | wechat 7 | wechat 8 | wechat 9 |

10 | 11 |

12 | 一个精简版的基于mobx的Ant Design Pro 13 |

14 | 15 | ## 使用方法 16 | * `npm run mock`: 使用[rap2](http://rap2.taobao.org/)工具mock接口 17 | * `npm run dev`: 使用实际接口,需要将`webpack.dev.js`文件第27行的 ’http://pre.xxx.com‘ 修改为实际地址 18 | * `npm run build`: 构建打包,可将生成的dist目录的内容交给后端 19 | * `npm run doc`: 该命令用于预览 20 | 21 | ## 为什么做这个项目? 22 | [Antd Pro](https://pro.ant.design/index-cn) 是一个大而全,且高度封装的脚手架,帮开发者做了很多基础工作,但不免提升了学习成本, 尤其内部依赖了`dva `和`umi`,限制住了开发者的同时也让开发者失去了对 webpack 的绝对控制权。所 以我利用业务时间做了这个基于`mobx`的精简版antd pro,简化了登录注册流程,将`dva`换成了class based 的`mobx`,使代码结构更清晰更易组织,去掉了底层 umi,使用者可以直接控制 webpack,更灵活,降低了学习成本,开发者可以快速上手,投入进业务开发。且内置了友盟统计,可以看到网站的 基本使用情况并使用高级分析,分群,画像,推送等高级功能。目前已有两家企业基于此开发并上线。 23 | 24 | ## 适合哪些人使用? 25 | 1. 不喜欢`dva`,更喜欢用基于类的`mobx`做状态管理 26 | 2. 对`umi`框架不熟悉,更想直接操作webpack 27 | 3. 前期不需要适配手机端,希望PC版尽快上线 28 | 29 | ## 相比Antd Pro,去掉了哪些东西? 30 | 1. 状态管理从`dva`换成了`mobx` 31 | 2. 去掉了`umi`,改成了直接操作webpack 32 | 3. mock改用阿里推出的 [rap2](http://rap2.taobao.org/) 33 | 3. 去掉了手机端的适配,方便快速完成PC版 34 | 4. 去掉了测试相关的东西 35 | 5. 去掉了多语言相关的东西 36 | >此项目的目的是帮助开发者尽快完成PC网站的开发,去掉的东西后期如果需要,可以参考[Antd Pro](https://pro.ant.design/index-cn) 项目逐渐迭代回去。 37 | 38 | ## 相比Antd Pro,做了哪些改进和补充? 39 | 1. CSS Modules使用了[react-css-modules](https://github.com/gajus/react-css-modules)方案,相比css-loader的modules方案更灵活。 40 | 2. 增加了异步路由,方便首页做进一步优化。 41 | 3. 增加了OSS上传组件,使用[STS](https://help.aliyun.com/document_detail/32077.html?spm=a2c4g.11186623.6.788.qrBaau)方案上传,需要开发者自行购买资源。 42 | 4. 图表库由`BizChart`改用了`highcharts`,这个算不上优化,但是相对于阿里外的开发者`highcharts`可能更好用一些 43 | 5. 内置了[友盟](https://udplus.umeng.com/)统计,为前端路由跳转增加了page load事件 44 | 45 | ## 有哪些地方可以完善? 46 | 1. 登录目前只有手机号+验证码直接登录,可以补充其他登录方式,但与此同时,你还要提供注册+密码找回+修改密码 47 | 2. 目前路由已经收敛进一个组件,但还不够集中,最好可以像router3一样集中管理(请不要问我为什么不直接用router3…,不做怎么知道后悔) 48 | 3. CSS Modules使用了[react-css-modules](https://github.com/gajus/react-css-modules)方案,相比antd pro更灵活了一些,但是[babel-plugin-react-css-modules](https://github.com/gajus/babel-plugin-react-css-modules)应该是更好的方案,这样css就相对于js透明了 49 | 4. 分包方案:目前提供了同步路由和异步路由两种方式,欢迎这方面的大牛进一步改进。同时分包依赖业务,针对业务应该会有更优的分包方案。 50 | 5. 肯定还有很多我未想到或发现的,欢迎各位大神指点并贡献代码,我会积极merge大家的pr。 51 | 52 | ## 主要的依赖及版本 53 | 1. webpack 4 54 | 2. router 4 55 | 3. react 16 56 | 4. mobx 5 57 | 5. axios 58 | 6. antd + ant-design-pro 59 | 7. ali-oss 6 60 | 61 | ## Contributors ✨ 62 | 63 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
Leo Chow
Leo Chow

🌍
soguagirl
soguagirl

🤔
73 | 74 | 75 | 76 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 77 | 78 | ## LICENSE 79 | [MIT](LICENSE) 80 | -------------------------------------------------------------------------------- /config/PATHS.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | root: path.resolve(__dirname, "../"), 5 | src: path.resolve(__dirname, '../src'), 6 | dist: path.resolve(__dirname, '../dist'), 7 | doc: path.resolve(__dirname, '../docs') 8 | }; -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require("path"); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const PATHS = require("./PATHS"); 5 | 6 | module.exports = { 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.js$/, 11 | exclude: /node_modules/, 12 | use: { 13 | loader: "babel-loader" 14 | } 15 | }, 16 | { 17 | test: /\.json$/, 18 | use: [ 19 | { 20 | loader: 'json-loader' 21 | } 22 | ] 23 | }, 24 | { 25 | test: /\.(png|gif|jpg)$/, 26 | use: [ 27 | { 28 | loader: 'url-loader', 29 | options: { 30 | limit: 10240, 31 | name: path.normalize('assets/[name].[ext]') 32 | } 33 | } 34 | ] 35 | }, 36 | { 37 | test: /\.(woff|woff2|ttf|eot|svg)$/, 38 | use: [ 39 | { 40 | loader: 'url-loader', 41 | options: { 42 | limit: 10240, 43 | name: path.normalize('assets/[name].[ext]') 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | resolve: { 51 | extensions: ['.js', '.jsx'], 52 | alias: { 53 | stylesheet: path.resolve(PATHS.src, 'asset/stylesheet'), 54 | image: path.resolve(PATHS.src, 'asset/image'), 55 | layout: path.resolve(PATHS.src, 'layout'), 56 | component: path.resolve(PATHS.src, 'component'), 57 | page: path.resolve(PATHS.src, 'page'), 58 | util: path.resolve(PATHS.src, 'util'), 59 | constant: path.resolve(PATHS.src, 'constant'), 60 | store: path.resolve(PATHS.src, 'store') 61 | } 62 | }, 63 | plugins: [ 64 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 65 | ] 66 | }; 67 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 5 | const autoprefixer = require('autoprefixer'); 6 | const common = require('./webpack.common'); 7 | const PATHS = require('./PATHS'); 8 | // const OpenBrowserPlugin = require('open-browser-webpack-plugin'); 9 | 10 | module.exports = env => { 11 | const API = (env || {}).API || 'mock'; 12 | 13 | console.log('API %s', API); 14 | 15 | const devServer = { 16 | contentBase: path.resolve(PATHS.dist), 17 | historyApiFallback: true, 18 | // compress: true, 19 | hot: true, 20 | inline: true, 21 | disableHostCheck: true, 22 | // progress: true 23 | }; 24 | 25 | if (API === 'dev') { 26 | devServer.proxy = { 27 | '/api': 'http://pre.xxx.com' // 预发地址 28 | }; 29 | } /* else { 30 | devServer.proxy = { 31 | '/api': { 32 | target: 'http://rap2api.taobao.org', 33 | pathRewrite: { 34 | '^/api' : '/app/mock/84445/api' 35 | } 36 | // changeOrigin: true, 37 | // onProxyRes: function(proxyReq, req, res) { 38 | // console.log('--------------------------------'); 39 | // console.log(proxyReq); 40 | // console.log(req); 41 | // // console.log(res); 42 | // console.log('--------------------------------'); 43 | // } 44 | } 45 | }; 46 | } */ 47 | 48 | return merge(common, { 49 | entry: { 50 | main: ['@babel/polyfill', path.resolve(PATHS.src, 'index.js')] 51 | }, 52 | output: { 53 | filename: '[name].js', 54 | path: path.resolve(PATHS.dist), 55 | publicPath: '/' 56 | }, 57 | mode: 'development', 58 | devtool: 'inline-source-map', 59 | devServer: devServer, 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.css$/, 64 | use: [ 65 | { 66 | loader: "style-loader" 67 | }, 68 | { 69 | loader: "css-loader" 70 | } 71 | ] 72 | }, 73 | { 74 | test: /\.less$/, 75 | exclude: path.resolve(PATHS.src, 'asset/stylesheet'), 76 | use: [ 77 | { 78 | loader: "style-loader" 79 | }, 80 | { 81 | loader: "css-loader", 82 | options: { 83 | modules: true, 84 | importLoaders: 1, 85 | localIdentName: "[local]_[hash:base64:5]", 86 | sourceMap: true, 87 | minimize: true 88 | } 89 | }, 90 | { 91 | loader: 'postcss-loader', 92 | options: { 93 | plugins: [autoprefixer('last 2 version')], 94 | sourceMap: true 95 | } 96 | }, 97 | { 98 | loader: "less-loader", 99 | options: { 100 | javascriptEnabled: true 101 | } 102 | } 103 | ] 104 | }, 105 | { 106 | test: /\.less$/, 107 | include: path.resolve(PATHS.src, 'asset/stylesheet'), 108 | use: [ 109 | { 110 | loader: "style-loader" 111 | }, 112 | { 113 | loader: "css-loader", 114 | }, 115 | { 116 | loader: "less-loader", 117 | options: { 118 | javascriptEnabled: true 119 | } 120 | } 121 | ] 122 | }, 123 | ] 124 | }, 125 | plugins: [ 126 | new webpack.HotModuleReplacementPlugin(), 127 | // new OpenBrowserPlugin({ 128 | // url: 'http://localhost:8080', 129 | // browser: "Google Chrome", 130 | // }), 131 | new webpack.DefinePlugin({ // 为项目注入环境变量 132 | 'process.env.API': JSON.stringify(API) 133 | }), 134 | new HtmlWebPackPlugin({ 135 | template: path.resolve(PATHS.src, 'asset/template/index.html'), 136 | filename: path.resolve(PATHS.dist, 'index.html'), 137 | favicon: path.resolve(PATHS.src, 'asset/image/favicon.png') 138 | }) 139 | ] 140 | }); 141 | }; 142 | -------------------------------------------------------------------------------- /config/webpack.doc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require('webpack-merge'); 3 | const webpack = require('webpack'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin"); 8 | const autoprefixer = require('autoprefixer'); 9 | const common = require('./webpack.common'); 10 | const PATHS = require("./PATHS"); 11 | 12 | module.exports = merge(common, { 13 | entry: { 14 | main: ['@babel/polyfill', path.resolve(PATHS.src, 'doc.js')] 15 | }, 16 | output: { 17 | filename: '[name].[chunkhash:8].js', 18 | path: path.resolve(PATHS.doc), 19 | // publicPath: '/' 20 | }, 21 | mode: 'production', 22 | // devtool: 'inline-source-map', 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.css$/, 27 | use: ExtractTextPlugin.extract({ 28 | fallback: 'style-loader', 29 | use: [ 30 | { loader: "css-loader" } 31 | ] 32 | }) 33 | }, 34 | { 35 | test: /\.less$/, 36 | exclude: path.resolve(PATHS.src, 'asset/stylesheet'), 37 | use: ExtractTextPlugin.extract({ 38 | fallback: 'style-loader', 39 | use: [ 40 | { 41 | loader: "css-loader", 42 | options: { 43 | modules: true, 44 | importLoaders: 1, 45 | localIdentName: "[local]_[hash:base64:5]", 46 | sourceMap: true, 47 | minimize: true 48 | } 49 | }, 50 | { 51 | loader: 'postcss-loader', 52 | options: { 53 | ident: 'postcss', 54 | plugins: [autoprefixer('last 2 version')], 55 | sourceMap: true 56 | } 57 | }, 58 | { 59 | loader: "less-loader", 60 | options: { 61 | javascriptEnabled: true 62 | } 63 | } 64 | ] 65 | }) 66 | }, 67 | { 68 | test: /\.less$/, 69 | include: path.resolve(PATHS.src, 'asset/stylesheet'), 70 | use: ExtractTextPlugin.extract({ 71 | fallback: 'style-loader', 72 | use: [ 73 | { loader: "css-loader" }, 74 | { 75 | loader: "less-loader", 76 | options: { 77 | javascriptEnabled: true 78 | } 79 | } 80 | ] 81 | }) 82 | }, 83 | ] 84 | }, 85 | optimization: { 86 | moduleIds: 'hashed', 87 | runtimeChunk: { 88 | name: 'runtime' 89 | }, 90 | splitChunks: { 91 | cacheGroups: { 92 | vendor: { 93 | test: /[\\/]node_modules[\\/]/, 94 | priority: 10, 95 | chunks: 'initial', 96 | name: 'vendor' 97 | } 98 | } 99 | } 100 | }, 101 | performance: { 102 | hints: false 103 | }, 104 | plugins: [ 105 | new CleanWebpackPlugin(['docs'], { 106 | root: PATHS.root 107 | }), 108 | new HtmlWebPackPlugin({ 109 | template: path.resolve(PATHS.src, 'asset/template/index.html'), 110 | filename: path.resolve(PATHS.doc, 'index.html'), 111 | favicon: path.resolve(PATHS.src, 'asset/image/favicon.png') 112 | }), 113 | new ExtractTextPlugin({ 114 | filename: '[name].[hash].css', 115 | allChunks: true, 116 | }), 117 | new webpack.DefinePlugin({ // 为项目注入环境变量 118 | 'process.env.API': JSON.stringify('mock') 119 | }), 120 | // 注意一定要在HtmlWebpackPlugin之后引用 121 | // inline的name和runtimeChunk的name保持一致 122 | new ScriptExtHtmlWebpackPlugin({ 123 | inline: /runtime\..*\.js$/ 124 | }) 125 | ] 126 | }); 127 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require('webpack-merge'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin"); 7 | const autoprefixer = require('autoprefixer'); 8 | const common = require('./webpack.common'); 9 | const PATHS = require("./PATHS"); 10 | 11 | module.exports = merge(common, { 12 | entry: { 13 | main: ['@babel/polyfill', path.resolve(PATHS.src, 'index.js')] 14 | }, 15 | output: { 16 | filename: '[name].[chunkhash:8].js', 17 | path: path.resolve(PATHS.dist), 18 | publicPath: '/' 19 | }, 20 | mode: 'production', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.css$/, 25 | use: ExtractTextPlugin.extract({ 26 | fallback: 'style-loader', 27 | use: [ 28 | { loader: "css-loader" } 29 | ] 30 | }) 31 | }, 32 | { 33 | test: /\.less$/, 34 | exclude: path.resolve(PATHS.src, 'asset/stylesheet'), 35 | use: ExtractTextPlugin.extract({ 36 | fallback: 'style-loader', 37 | use: [ 38 | { 39 | loader: "css-loader", 40 | options: { 41 | modules: true, 42 | importLoaders: 1, 43 | localIdentName: "[local]_[hash:base64:5]", 44 | sourceMap: true, 45 | minimize: true 46 | } 47 | }, 48 | { 49 | loader: 'postcss-loader', 50 | options: { 51 | ident: 'postcss', 52 | plugins: [autoprefixer('last 2 version')], 53 | sourceMap: true 54 | } 55 | }, 56 | { 57 | loader: "less-loader", 58 | options: { 59 | javascriptEnabled: true 60 | } 61 | } 62 | ] 63 | }) 64 | }, 65 | { 66 | test: /\.less$/, 67 | include: path.resolve(PATHS.src, 'asset/stylesheet'), 68 | use: ExtractTextPlugin.extract({ 69 | fallback: 'style-loader', 70 | use: [ 71 | { loader: "css-loader" }, 72 | { 73 | loader: "less-loader", 74 | options: { 75 | javascriptEnabled: true 76 | } 77 | } 78 | ] 79 | }) 80 | }, 81 | ] 82 | }, 83 | optimization: { 84 | moduleIds: 'hashed', 85 | runtimeChunk: { 86 | name: 'runtime' 87 | }, 88 | splitChunks: { 89 | cacheGroups: { 90 | vendor: { 91 | test: /[\\/]node_modules[\\/]/, 92 | priority: 10, 93 | chunks: 'initial', 94 | name: 'vendor' 95 | } 96 | } 97 | } 98 | }, 99 | performance: { 100 | hints: false 101 | }, 102 | plugins: [ 103 | new CleanWebpackPlugin(['dist'], { 104 | root: PATHS.root 105 | }), 106 | new ExtractTextPlugin({ 107 | filename: '[name].[hash].css', 108 | allChunks: true, 109 | }), 110 | new HtmlWebPackPlugin({ 111 | template: path.resolve(PATHS.src, 'asset/template/index.html'), 112 | filename: path.resolve(PATHS.dist, 'index.html'), 113 | favicon: path.resolve(PATHS.src, 'asset/image/favicon.png') 114 | }), 115 | // 注意一定要在HtmlWebpackPlugin之后引用 116 | // inline的name和runtimeChunk的name保持一致 117 | new ScriptExtHtmlWebpackPlugin({ 118 | inline: /runtime\..*\.js$/ 119 | }) 120 | ] 121 | }); 122 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gzgogo/antd-pro-mobx/a6b34be08e096abd46a5929768b04dc426dc8dc4/docs/favicon.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | antd-pro-mobx 8 | 18 | 19 | 25 | 26 | 27 |
28 | 30 | 31 | -------------------------------------------------------------------------------- /docs/main.06f7d04799b361eab94f.css: -------------------------------------------------------------------------------- 1 | .logo_3TQSW{height:64px;position:relative;line-height:64px;padding-left:24px;-webkit-transition:all .3s;transition:all .3s;background:#002140;overflow:hidden}.logo_3TQSW img{height:32px}.logo_3TQSW h1,.logo_3TQSW img{display:inline-block;vertical-align:middle}.logo_3TQSW h1{color:#fff;font-size:20px;margin:0 0 0 12px;font-family:Myriad Pro,Helvetica Neue,Arial,Helvetica,sans-serif;font-weight:400}.icon-menu-trigger_1sLJf{font-size:20px;line-height:64px;height:64px;cursor:pointer;-webkit-transition:all .3s,padding 0s;transition:all .3s,padding 0s;padding:22px 24px}.icon-menu-trigger_1sLJf:hover{background:rgba(0,0,0,.025)}.right_tSwQ4{float:right;height:100%}.right_tSwQ4 .action_8m7hh{cursor:pointer;padding:0 12px;display:inline-block;-webkit-transition:all .3s;transition:all .3s;height:100%}.right_tSwQ4 .action_8m7hh>i{font-size:18px;vertical-align:middle;color:rgba(0,0,0,.65)}.right_tSwQ4 .account_37oG4 .avatar_G7w1G{margin:20px 8px 20px 0;color:#1890ff;background:hsla(0,0%,100%,.85);vertical-align:middle}.summary-card_PHQNF{position:relative}.summary-card_PHQNF .chart-top_4_yBH{position:relative;overflow:hidden;width:100%}.summary-card_PHQNF .meta-wrap_3bfvj{float:left}.summary-card_PHQNF .meta_1pEP0{color:rgba(0,0,0,.45);font-size:14px;line-height:22px;height:22px}.summary-card_PHQNF .action_13A_3{cursor:pointer;position:absolute;top:0;right:0}.summary-card_PHQNF .total_30MOw{overflow:hidden;text-overflow:ellipsis;word-break:break-all;white-space:nowrap;color:rgba(0,0,0,.85);margin-top:4px;margin-bottom:0;font-size:30px;line-height:38px;height:38px}.chart-toolbar_2i0iw{height:60px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.chart_2SNjH{margin-top:30px}.global-footer_3CaAT{padding:0 16px;margin:48px 0 24px;text-align:center}.global-footer_3CaAT .links_3akUD{margin-bottom:8px}.global-footer_3CaAT .links_3akUD a{color:rgba(0,0,0,.45);-webkit-transition:all .3s;transition:all .3s}.global-footer_3CaAT .links_3akUD a:not(:last-child){margin-right:40px}.global-footer_3CaAT .links_3akUD a:hover{color:rgba(0,0,0,.65)}.global-footer_3CaAT .copyright_3u074{color:rgba(0,0,0,.45);font-size:14px}.container_B-rp-{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;height:100vh;overflow:auto;background:#f0f2f5}.content_2PZWW{padding:72px 0;-webkit-box-flex:1;-ms-flex:1;flex:1}.top_1gAef{text-align:center}.header_3kaXy{height:44px;line-height:44px}.header_3kaXy a{text-decoration:none}.logo_dyUmT{height:44px;vertical-align:top;margin-right:16px}.title_9PLsv{font-size:33px;color:rgba(0,0,0,.85);font-family:Myriad Pro,Helvetica Neue,Arial,Helvetica,sans-serif;font-weight:600;position:relative;top:2px}.desc_235zA{font-size:14px;color:rgba(0,0,0,.45);margin-top:12px;margin-bottom:40px}.login_FgzT_{width:368px;margin:0 auto}.upload_1JgTr{display:inline-block}.desc_3j9CC{line-height:12px}.preview_pKdtb{margin-top:14px}.preview_pKdtb img{max-width:200px;max-height:150px}.actions_1vkbJ{text-align:center}.actions_1vkbJ .btn-next_2PnZ2{margin-left:20px}.actions_23kDq{text-align:center}.actions_23kDq .btn-next_esfAW{margin-left:20px}.btn-create_kHGY4{margin:50px 0 10px}.chart-toolbar_rsPei{height:60px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.chart_3IBki{margin-top:30px}.ant-layout, 2 | .ant-layout-sider { 3 | min-height: 100%; 4 | } 5 | .ant-steps { 6 | max-width: 750px; 7 | margin: 0px auto; 8 | } 9 | .ant-form { 10 | max-width: 650px; 11 | margin: 0px auto; 12 | } 13 | .user-menu .anticon { 14 | margin-right: 8px; 15 | } 16 | .user-menu .ant-dropdown-menu-item { 17 | width: 160px; 18 | } 19 | /* stylelint-disable at-rule-empty-line-before,at-rule-name-space-after,at-rule-no-unknown */ 20 | /* stylelint-disable no-duplicate-selectors */ 21 | /* stylelint-disable */ 22 | /* stylelint-disable declaration-bang-space-before,no-duplicate-selectors,string-no-newline */ 23 | .content-card { 24 | margin: 24px 16px; 25 | padding: 24px; 26 | background: #fff; 27 | min-height: 280px; 28 | } 29 | .chart-tab .ant-tabs-ink-bar { 30 | bottom: auto; 31 | } 32 | .chart-tab .ant-tabs-bar { 33 | border-bottom: none; 34 | } 35 | .chart-tab .ant-tabs-nav-container-scrolling { 36 | padding-left: 40px; 37 | padding-right: 40px; 38 | } 39 | .chart-tab .ant-tabs-tab-prev-icon:before { 40 | position: relative; 41 | left: 6px; 42 | } 43 | .chart-tab .ant-tabs-tab-next-icon:before { 44 | position: relative; 45 | right: 6px; 46 | } 47 | .chart-tab .ant-tabs-tab-active h4 { 48 | color: #1890ff; 49 | } 50 | -------------------------------------------------------------------------------- /en.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.md) 2 | 3 |

Antd Pro Mobx

4 | 5 | [Ant Design Pro](https://pro.ant.design/index-cn) lite base on Mobx 6 | 7 | [Preview](http://gongzhen.coding.me) 8 | 9 | ## Available scripts 10 | * `npm run mock`: mock API by [rap2](http://rap2.taobao.org/) . 11 | * `npm run dev`: Runs the app in development mode. To use the real world API,please change ‘http://pre.xxx.com’ (line 27th. of web pack.dev.js) to your project API url. 12 | * `npm run build`: Builds the app for production to the `dist` folder. The `dist` folder is ready for deployment. 13 | * `npm run doc`: Previews the demo. 14 | 15 | ## Why Antd Pro Mobx? 16 | [Antd Pro](https://pro.ant.design/index-cn) is a large and complete, highly-encapsulated scaffold, which helps developers to do lots of basic works, but it increases the costs per learner inevitably. The more so, its dependences on `dva` and `umi` which limited developers technologies stacks, also developers can't configure webpack directly. I build Antd Pro Mobx to resolve the issues, it simplified the login and registration process, replaced `dva` to `mobx` which is based on class, made the code structure clearer and easier to organize, removed the underlying `umi`, developers can configure webpack directly and flexiblely, it reduces the learning costs. Developers can get started quickly and focus on business development more deeply. And built-in alliance statistics, you can get the basic situation of your app, and use advanced analysis, clustering, portrait, push and other advanced functions. At present, there are two enterprises have implemented Antd Pro Mobx. 17 | 18 | ## Users of Antd Pro Mobx 19 | 20 | 1. You don't like `dva`, you prefer to use `mobx` which is based on class. 21 | 2. You are not familiar with `umi`, and wanna configure webpack directly. 22 | 3. You want to build an online app which only includes a desktop version ASAP. 23 | 24 | ## What has been removed as Antd Pro? 25 | 1. replaced `dva` to `mobx` . 26 | 2. removed `umi`, developers can configure webpack directly. 27 | 3. Use [rap2](http://rap2.taobao.org/) for mock data. 28 | 4. Removed mobile version. 29 | 5. Removed test related code. 30 | 6. Removed multiple languages. 31 | 32 | >The purpose of this project is to help developers to develop a desktop version app ASAP. You can refer to [Antd Pro](https://pro.ant.design/index-cn) to add what has been removed. 33 | 34 | ## What has been added as Antd Pro? 35 | 1. Replaced css-loader modules to [react-css-modules](https://github.com/gajus/react-css-modules). 36 | 2. Added asynchronous routing to optimize the home page. 37 | 3. Added OSS upload component to upload files by [STS](https://help.aliyun.com/document_detail/32077.html?spm=a2c4g.11186623.6.788.qrBaau). You have to purchase STS. 38 | 4. Replaced `BizChart` to `highcharts` . 39 | 5. Built-in [umeng](https://www.umeng.com/) statistics, Add `page load` event for font-end routing. 40 | 41 | ## What can be more perfect? 42 | 1. This project only support `mobile number+captcha` for user login, you can add more other ways for login, but you have to provide a solution for registration, forgot password and change password. 43 | 2. Routes are built in a components, but they are not centralization, you may change them as same as router 3 ( please don't ask me why don't use router 3 directly, there were no regret medicine to take forever in the world). 44 | 3. Though css-loader modules has been replaced to [react-css-modules](https://github.com/gajus/react-css-modules). But there is another better solution which is called [babel-plugin-react-css-modules](https://github.com/gajus/babel-plugin-react-css-modules). 45 | 4. Code-splitting solution: synchronous routing and asynchronous routing are both supported now. You can build more solutions for your project. 46 | 5. I'd love to have your helping hand on `Antd Pro Mobx`! Welcome to pull your request. 47 | 48 | ## Dependences 49 | 1. webpack 4 50 | 2. router 4 51 | 3. react 16 52 | 4. mobx 5 53 | 5. axios 54 | 6. antd + ant-design-pro 55 | 7. ali-oss 6 56 | 57 | ## LICENSE 58 | [MIT](LICENSE) 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antd-pro-mobx", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "author": "gongzhen", 7 | "license": "ISC", 8 | "scripts": { 9 | "mock": "webpack-dev-server --open --config config/webpack.dev.js", 10 | "dev": "webpack-dev-server --open --config config/webpack.dev.js --env.API=dev", 11 | "build": "webpack --progress --config config/webpack.prod.js", 12 | "doc": "webpack --progress --config config/webpack.doc.js" 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "lint-staged" 17 | } 18 | }, 19 | "lint-staged": { 20 | "src/**/*.js": [ 21 | "eslint" 22 | ] 23 | }, 24 | "devDependencies": { 25 | "@babel/plugin-transform-runtime": "^7.0.0", 26 | "autoprefixer": "^9.3.1", 27 | "babel-core": "^6.26.0", 28 | "babel-eslint": "^9.0.0", 29 | "babel-loader": "^7.1.4", 30 | "babel-plugin-import": "^1.8.0", 31 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 32 | "babel-preset-env": "^1.6.1", 33 | "babel-preset-react": "^6.24.1", 34 | "babel-preset-stage-0": "^6.24.1", 35 | "clean-webpack-plugin": "^1.0.0", 36 | "css-loader": "^0.28.11", 37 | "eslint": "^5.5.0", 38 | "eslint-config-ali": "^3.1.0", 39 | "eslint-plugin-import": "^2.14.0", 40 | "eslint-plugin-react": "^7.11.1", 41 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 42 | "html-webpack-plugin": "^3.1.0", 43 | "husky": "^1.0.0-rc.14", 44 | "json-loader": "^0.5.7", 45 | "less": "^3.8.1", 46 | "less-loader": "^4.1.0", 47 | "lint-staged": "^7.2.2", 48 | "postcss-less": "^2.0.0", 49 | "postcss-loader": "^3.0.0", 50 | "react-css-modules": "^4.7.7", 51 | "react-hot-loader": "^4.3.8", 52 | "script-ext-html-webpack-plugin": "^2.1.2", 53 | "style-loader": "^0.20.3", 54 | "url-loader": "^1.1.1", 55 | "webpack-cli": "3.3.0", 56 | "webpack-dev-server": "^3.1.10", 57 | "webpack-merge": "^4.1.4" 58 | }, 59 | "dependencies": { 60 | "@babel/polyfill": "^7.0.0", 61 | "ali-oss": "^6.0.1", 62 | "ant-design-pro": "2.0.0", 63 | "antd": "^3.9.2", 64 | "axios": "^0.18.0", 65 | "babel-plugin-react-css-modules": "^3.4.2", 66 | "mobx": "^5.1.0", 67 | "mobx-react": "^5.2.8", 68 | "open-browser-webpack-plugin": "^0.0.5", 69 | "react": "^16.2.0", 70 | "react-dom": "^16.2.0", 71 | "react-highcharts": "^16.0.2", 72 | "react-loadable": "^5.5.0", 73 | "react-router-dom": "^4.3.1", 74 | "shortid": "^2.2.13", 75 | "webpack": "^4.25.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/asset/image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gzgogo/antd-pro-mobx/a6b34be08e096abd46a5929768b04dc426dc8dc4/src/asset/image/favicon.png -------------------------------------------------------------------------------- /src/asset/stylesheet/app.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .content-card { 4 | margin: 24px 16px; 5 | padding: 24px; 6 | background: #fff; 7 | min-height: 280px; 8 | } 9 | 10 | .chart-tab { 11 | .ant-tabs-ink-bar { 12 | bottom: auto; 13 | } 14 | .ant-tabs-bar { 15 | border-bottom: none; 16 | } 17 | .ant-tabs-nav-container-scrolling { 18 | padding-left: 40px; 19 | padding-right: 40px; 20 | } 21 | .ant-tabs-tab-prev-icon:before { 22 | position: relative; 23 | left: 6px; 24 | } 25 | .ant-tabs-tab-next-icon:before { 26 | position: relative; 27 | right: 6px; 28 | } 29 | .ant-tabs-tab-active h4 { 30 | color: @primary-color; 31 | } 32 | } -------------------------------------------------------------------------------- /src/asset/stylesheet/cantd.less: -------------------------------------------------------------------------------- 1 | // .ant-form-item .ant-form-item-label { 2 | // width: 108px; 3 | // } 4 | .ant-layout, .ant-layout-sider { 5 | min-height: 100%; 6 | } 7 | 8 | .ant-steps { 9 | max-width: 750px; 10 | margin: 0px auto; 11 | } 12 | 13 | .ant-form { 14 | max-width: 650px; 15 | margin: 0px auto; 16 | } 17 | 18 | .user-menu { 19 | .anticon { 20 | margin-right: 8px; 21 | } 22 | .ant-dropdown-menu-item { 23 | width: 160px; 24 | } 25 | } -------------------------------------------------------------------------------- /src/asset/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | antd-pro-mobx 8 | 9 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/component/AppFooter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import { Icon } from 'antd'; 4 | import { GlobalFooter } from 'ant-design-pro'; 5 | import styles from './style.less'; 6 | 7 | @cssModules(styles) 8 | class AppFooter extends React.Component { 9 | static defaultProps = { 10 | links: [ 11 | { 12 | key: 'help', 13 | title: '帮助', 14 | href: '' 15 | }, 16 | { 17 | key: 'privacy', 18 | title: '隐私', 19 | href: '' 20 | }, 21 | { 22 | key: 'terms', 23 | title: '条款', 24 | href: '' 25 | } 26 | ], 27 | copyright: ( 28 |
29 | Copyright Copyright 2018 蚂蚁金服体验技术部出品 30 |
31 | ) 32 | } 33 | 34 | render() { 35 | const { links, copyright } = this.props; 36 | 37 | return ; 38 | } 39 | } 40 | 41 | export default AppFooter; 42 | -------------------------------------------------------------------------------- /src/component/AppFooter/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .global-footer { 4 | padding: 0 16px; 5 | margin: 48px 0 24px 0; 6 | text-align: center; 7 | 8 | .links { 9 | margin-bottom: 8px; 10 | 11 | a { 12 | color: @text-color-secondary; 13 | transition: all 0.3s; 14 | 15 | &:not(:last-child) { 16 | margin-right: 40px; 17 | } 18 | 19 | &:hover { 20 | color: @text-color; 21 | } 22 | } 23 | } 24 | 25 | .copyright { 26 | color: @text-color-secondary; 27 | font-size: @font-size-base; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/component/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon, Modal } from 'antd'; 3 | 4 | const Loading = ({ pastDelay, timedOut, error }) => { 5 | if (pastDelay) { 6 | return ( 7 | 15 | 16 |

Loading...

17 |
18 | ); 19 | } else if (timedOut) { 20 | return
Taking a long time...
; 21 | } else if (error) { 22 | return
Error!
; 23 | } 24 | return null; 25 | }; 26 | 27 | export default Loading; 28 | -------------------------------------------------------------------------------- /src/component/OSSUpload/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import OSS from 'ali-oss'; 3 | import shortid from 'shortid'; 4 | import cssModules from 'react-css-modules'; 5 | import { Upload, Icon, Modal } from 'antd'; 6 | import { getSTS } from 'util/api'; 7 | import styles from './style.less'; 8 | 9 | function getFileExt(filename) { 10 | let ext = ''; 11 | const pos = filename.lastIndexOf('.'); 12 | if (pos > 0) { 13 | ext = filename.substring(pos, filename.length); 14 | } 15 | 16 | return ext; 17 | } 18 | 19 | @cssModules(styles) 20 | class OSSUpload extends Component { 21 | state = { 22 | imageDataUrl: '', 23 | loading: false 24 | 25 | } 26 | 27 | beforeUpload = (file) => { 28 | const image = file.type.indexOf('image') > -1; 29 | if (!image) { 30 | Modal.error({ 31 | content: '只能上传图片文件' 32 | }); 33 | return false; 34 | } 35 | 36 | const empty = file.size <= 0; 37 | if (empty) { 38 | Modal.error({ 39 | content: '不能上传空文件' 40 | }); 41 | return false; 42 | } 43 | 44 | const limit = file.size / 1024 / 1024 < 10; 45 | if (!limit) { 46 | Modal.error({ 47 | content: '文件大小不能超过10M' 48 | }); 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | getBase64 = img => ( 56 | new Promise((resolve) => { 57 | try { 58 | const reader = new FileReader(); 59 | reader.onload = () => resolve(reader.result); 60 | reader.readAsDataURL(img); 61 | } catch (error) { 62 | resolve(null); 63 | } 64 | }) 65 | ) 66 | 67 | handleChange = async (info) => { 68 | const { onChange } = this.props; 69 | let { file } = info; 70 | 71 | if (file.status === 'done') { 72 | if (file.response && file.response.url) { 73 | file.ossName = file.response.name; 74 | file.url = file.response.url; 75 | 76 | const imageDataUrl = await this.getBase64(info.file.originFileObj); 77 | 78 | this.setState({ 79 | imageDataUrl 80 | }); 81 | } else { 82 | file = null; 83 | } 84 | } 85 | 86 | typeof onChange === 'function' && onChange(file); 87 | } 88 | 89 | customRequest = async (option) => { 90 | this.setState({ 91 | loading: true 92 | }); 93 | 94 | const res = await getSTS(); 95 | 96 | if (res && res.code === 0 && res.data) { 97 | const client = new OSS({ 98 | ...res.data, 99 | expire: '' 100 | }); 101 | 102 | // const fileName = `upload/${shortid.generate()}${getFileExt(option.file.name)}`; 103 | const fileName = `${shortid.generate()}${getFileExt(option.file.name)}`; 104 | const result = await client.put(fileName, option.file); 105 | 106 | if (result.res && result.res.status === 200) { 107 | option.onSuccess(result); 108 | } else { 109 | option.onError(result); 110 | } 111 | } else { 112 | console.error('请实现自己的STS接口,详情可参考 https://help.aliyun.com/document_detail/32077.html?spm=a2c4g.11186623.6.788.qrBaau'); 113 | } 114 | 115 | this.setState({ 116 | loading: false 117 | }); 118 | } 119 | 120 | render() { 121 | const { disabled, listType, beforeUpload, value, desc } = this.props; 122 | const { loading, imageDataUrl } = this.state; 123 | 124 | const loadingNode = ( 125 |
126 | 127 |
上传中
128 |
129 | ); 130 | 131 | return ( 132 |
133 | 142 | { loading ? loadingNode : this.props.children } 143 | 144 | { desc &&
{desc}
} 145 | { (imageDataUrl || (value && value.thumbnailUrl)) &&
preview
} 146 |
147 | ); 148 | } 149 | } 150 | 151 | export default OSSUpload; 152 | -------------------------------------------------------------------------------- /src/component/OSSUpload/style.less: -------------------------------------------------------------------------------- 1 | .upload { 2 | display: inline-block; 3 | } 4 | 5 | .desc { 6 | line-height: 12px; 7 | } 8 | 9 | .preview { 10 | margin-top: 14px; 11 | img { 12 | max-width: 200px; 13 | max-height: 150px; 14 | } 15 | } -------------------------------------------------------------------------------- /src/component/PlanForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | import { Input, Form, Select, Radio, Icon, Modal } from 'antd'; 4 | import OSSUpload from 'component/OSSUpload'; 5 | 6 | const { Option } = Select; 7 | const RadioGroup = Radio.Group; 8 | const FormItem = Form.Item; 9 | 10 | class PlanForm extends Component { 11 | static defaultProps = { 12 | enabledFields: [] 13 | } 14 | 15 | static displayName = 'ExchangeForm'; 16 | 17 | beforeUpload = (file) => { 18 | const image = file.type.indexOf('image') > -1; 19 | if (!image) { 20 | Modal.error({ 21 | content: '只能上传图片文件' 22 | }); 23 | return false; 24 | } 25 | 26 | const empty = file.size <= 0; 27 | if (empty) { 28 | Modal.error({ 29 | content: '不能上传空文件' 30 | }); 31 | return false; 32 | } 33 | 34 | const limit = file.size / 1024 / 1024 < 10; 35 | if (!limit) { 36 | Modal.error({ 37 | content: '文件大小不能超过10M' 38 | }); 39 | return false; 40 | } 41 | 42 | return true; 43 | } 44 | 45 | checkInputLength(field, maxLength, rule, value, callback) { 46 | if (value && value.toString().length > maxLength) { 47 | callback(`不能超过${maxLength}个字符`); 48 | 49 | // 不能在这里setFieldsValue,否则错误提示会被清空 50 | // let obj = {}; 51 | // obj[field] = value.substr(0, maxLength); 52 | // this.props.form.setFieldsValue(obj); 53 | } else { 54 | callback(); 55 | } 56 | } 57 | 58 | renderName(formItemLayout, disabled) { 59 | const { enabledFields, mode, name } = this.props; 60 | const { getFieldDecorator } = this.props.form; 61 | 62 | return ( 63 | 68 | { 69 | mode === 'view' 70 | ? {name.value} 71 | : ( 72 | getFieldDecorator('name', { 73 | initialValue: '', 74 | validateTrigger: ['onChange'], 75 | rules: [ 76 | { required: true, message: '请输入名称' }, 77 | { pattern: '^[a-zA-Z0-9_\\u4e00-\\u9fa5_\\\\-]+$', message: '仅支持中英文、数字和下划线' }, 78 | { validator: (rule, value, callback) => this.checkInputLength('name', 40, rule, value, callback) } 79 | ] 80 | })( 81 | 86 | ) 87 | ) 88 | } 89 | 90 | ); 91 | } 92 | 93 | renderDescription(formItemLayout, disabled) { 94 | const { enabledFields, mode, description } = this.props; 95 | const { getFieldDecorator } = this.props.form; 96 | 97 | return ( 98 | 103 | { 104 | mode === 'view' 105 | ? {description.value} 106 | : ( 107 | getFieldDecorator('description', { 108 | initialValue: '', 109 | validateTrigger: ['onChange'], 110 | rules: [ 111 | { required: true, message: '请输入描述' }, 112 | { pattern: '^[a-zA-Z0-9_\\u4e00-\\u9fa5_\\\\-]+$', message: '仅支持中英文、数字和下划线' }, 113 | { validator: (rule, value, callback) => this.checkInputLength('name', 40, rule, value, callback) } 114 | ] 115 | })( 116 | 121 | ) 122 | ) 123 | } 124 | 125 | ); 126 | } 127 | 128 | renderType(formItemLayout, disabled) { 129 | const { form, mode, type, types } = this.props; 130 | const { getFieldDecorator } = form; 131 | 132 | const typeNodes = types.map(item => ({item.name})); 133 | 134 | return ( 135 | 141 | { 142 | mode === 'view' 143 | ? ( 144 | {type.value === 0 ? '类型1' : '类型2'} 145 | ) 146 | : ( 147 | getFieldDecorator('type', { 148 | initialValue: '', 149 | rules: [ 150 | { required: true, message: '请选择类型' } 151 | ] 152 | })( 153 | 154 | {typeNodes} 155 | 156 | ) 157 | ) 158 | } 159 | 160 | ); 161 | } 162 | 163 | renderSubType(formItemLayout, disabled) { 164 | const { subTypes, mode, subType } = this.props; 165 | const { getFieldDecorator } = this.props.form; 166 | 167 | const subTypeOptions = subTypes.map(item => ); 168 | 169 | return ( 170 | 174 | { 175 | mode === 'view' 176 | ? ( 177 | {subType.value} 178 | ) 179 | : ( 180 | getFieldDecorator('subType', { 181 | rules: [ 182 | { required: true, message: '请选择子类型' } 183 | ] 184 | })( 185 | 196 | ) 197 | ) 198 | } 199 | 200 | ); 201 | } 202 | 203 | renderLogoUpload(formItemLayout, disabled) { 204 | const { mode } = this.props; 205 | const { getFieldDecorator } = this.props.form; 206 | 207 | return ( 208 | 213 | { 214 | getFieldDecorator('logo', { 215 | rules: [ 216 | { required: true, message: '图片未上传' } 217 | ] 218 | })( 219 | 225 | { 226 | mode === 'view' 227 | ? null 228 | : ( 229 |
230 | 231 |
上传图片
232 |
233 | ) 234 | } 235 |
236 | )} 237 |
238 | ); 239 | } 240 | 241 | renderImageUpload(formItemLayout, disabled) { 242 | const { mode } = this.props; 243 | const { getFieldDecorator } = this.props.form; 244 | 245 | return ( 246 | 251 | { 252 | getFieldDecorator('image', { 253 | rules: [ 254 | { required: true, message: '图片未上传' } 255 | ] 256 | })( 257 | 263 | { 264 | mode === 'view' 265 | ? null 266 | : ( 267 |
268 | 269 |
上传图片
270 |
271 | ) 272 | } 273 |
274 | )} 275 |
276 | ); 277 | } 278 | 279 | render() { 280 | const { disabled } = this.props; 281 | const formItemLayout = { labelCol: { span: 6 }, wrapperCol: { span: 14 } }; 282 | 283 | return ( 284 |
285 | { this.renderName(formItemLayout, disabled) } 286 | { this.renderDescription(formItemLayout, disabled) } 287 | { this.renderType(formItemLayout, disabled) } 288 | { this.renderSubType(formItemLayout, disabled) } 289 | { this.renderLogoUpload(formItemLayout, disabled) } 290 | { this.renderImageUpload(formItemLayout, disabled) } 291 |
292 | ); 293 | } 294 | } 295 | 296 | export default Form.create({ 297 | onFieldsChange(props, changedFields) { 298 | props.onChange(changedFields); 299 | }, 300 | 301 | mapPropsToFields(props) { 302 | return { 303 | name: Form.createFormField({ 304 | ...props.name, 305 | value: props.name.value 306 | }), 307 | description: Form.createFormField({ 308 | ...props.description, 309 | value: props.description.value 310 | }), 311 | type: Form.createFormField({ 312 | ...props.type, 313 | value: props.type.value 314 | }), 315 | subType: Form.createFormField({ 316 | ...props.subType, 317 | value: props.subType.value 318 | }), 319 | logo: Form.createFormField({ 320 | ...props.logo, 321 | value: props.logo.value 322 | }), 323 | image: Form.createFormField({ 324 | ...props.image, 325 | value: props.image.value 326 | }) 327 | }; 328 | } 329 | })(PlanForm); 330 | -------------------------------------------------------------------------------- /src/doc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 此文件用于github pages,与业务无关 3 | */ 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import Root from 'layout/Root/DocRoot'; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('app') 12 | ); 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from 'layout/Root'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('app') 8 | ); 9 | -------------------------------------------------------------------------------- /src/layout/App/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { /* Link, */ withRouter } from 'react-router-dom'; 4 | import cssModules from 'react-css-modules'; 5 | import { Layout, Icon, Menu, Dropdown, /* Avatar, */Tooltip } from 'antd'; 6 | import SiderMenu from 'layout/SiderMenu'; 7 | import loginUtil from 'util/login'; 8 | import styles from './style.less'; 9 | 10 | const { Header, Sider, Content } = Layout; 11 | 12 | @withRouter 13 | @cssModules(styles, { 14 | allowMultiple: true 15 | }) 16 | class App extends Component { 17 | state = { 18 | collapsed: false 19 | }; 20 | 21 | toggle = () => { 22 | this.setState({ 23 | collapsed: !this.state.collapsed 24 | }); 25 | } 26 | 27 | handleMenuClick = ({ key }) => { 28 | if (key === 'logout') { 29 | loginUtil.logout(); 30 | this.props.history.push('/login'); 31 | } 32 | } 33 | 34 | render() { 35 | const { collapsed } = this.state; 36 | 37 | const userInfo = loginUtil.getUserInfo() || {}; 38 | 39 | const menu = ( 40 | 41 | {/* 42 | 个人中心 43 | 44 | 45 | 个人设置 46 | 47 | */} 48 | 49 | 50 | 退出登录 51 | 52 | 53 | ); 54 | 55 | return ( 56 | 57 | 63 |
64 | logo 65 |

{collapsed ? '' : 'Ant Design Pro'}

66 |
67 | 68 |
69 | 70 |
71 | 76 |
77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | {/* */} 97 | {userInfo.name} 98 | 99 | 100 |
101 |
102 | 103 | {this.props.children} 104 | 105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | export default App; 112 | -------------------------------------------------------------------------------- /src/layout/App/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .logo { 4 | height: 64px; 5 | position: relative; 6 | line-height: 64px; 7 | padding-left: 24px; 8 | -webkit-transition: all 0.3s; 9 | transition: all 0.3s; 10 | background: #002140; 11 | overflow: hidden; 12 | 13 | img { 14 | display: inline-block; 15 | vertical-align: middle; 16 | height: 32px; 17 | } 18 | 19 | h1 { 20 | color: white; 21 | display: inline-block; 22 | vertical-align: middle; 23 | font-size: 20px; 24 | margin: 0 0 0 12px; 25 | font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; 26 | font-weight: 400; 27 | } 28 | } 29 | 30 | .icon-menu-trigger { 31 | font-size: 20px; 32 | line-height: 64px; 33 | height: 64px; 34 | cursor: pointer; 35 | -webkit-transition: all 0.3s, padding 0s; 36 | transition: all 0.3s, padding 0s; 37 | padding: 22px 24px; 38 | &:hover { 39 | background: rgba(0, 0, 0, 0.025); 40 | } 41 | } 42 | 43 | .right { 44 | float: right; 45 | height: 100%; 46 | .action { 47 | cursor: pointer; 48 | padding: 0 12px; 49 | display: inline-block; 50 | transition: all 0.3s; 51 | height: 100%; 52 | > i { 53 | font-size: 18px; 54 | vertical-align: middle; 55 | color: @text-color; 56 | } 57 | // &:hover { 58 | // background: @pro-header-hover-bg; 59 | // } 60 | // :global(&.ant-popover-open) { 61 | // background: @pro-header-hover-bg; 62 | // } 63 | } 64 | // .search { 65 | // padding: 0 12px; 66 | // &:hover { 67 | // background: transparent; 68 | // } 69 | // } 70 | .account { 71 | .avatar { 72 | margin: 20px 8px 20px 0; 73 | color: @primary-color; 74 | background: rgba(255, 255, 255, 0.85); 75 | vertical-align: middle; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/layout/Root/DocRoot.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader'; 3 | import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom'; 4 | import { Provider } from 'mobx-react'; 5 | import App from 'layout/App'; 6 | import store from 'store'; 7 | import { LocaleProvider } from 'antd'; 8 | import zh_CN from 'antd/lib/locale-provider/zh_CN'; 9 | import 'moment/locale/zh-cn'; 10 | 11 | import Home from 'page/Home'; 12 | import Login from 'page/Login'; 13 | import Account from 'page/Account'; 14 | import BasicForm from 'page/BasicForm'; 15 | import StepForm from 'page/StepForm'; 16 | import SearchList from 'page/SearchList'; 17 | import DataReport from 'page/DataReport'; 18 | import Success from 'page/Result/Success'; 19 | import Error from 'page/Result/Error'; 20 | import E403 from 'page/403'; 21 | import E404 from 'page/404'; 22 | import E500 from 'page/500'; 23 | 24 | import 'antd/dist/antd.css'; 25 | import 'ant-design-pro/dist/ant-design-pro.css'; 26 | import 'stylesheet/cantd.less'; 27 | import 'stylesheet/app.less'; 28 | 29 | const Root = () => ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 51 | 52 | 57 | 58 | 63 | 64 | 69 | 70 | 75 | 76 | 81 | 86 | 87 | 92 | 97 | 102 | 103 | 104 | 105 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ); 116 | 117 | export default hot(module)(Root); 118 | -------------------------------------------------------------------------------- /src/layout/Root/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import loadable from 'react-loadable'; 3 | import Loading from 'component/Loading'; 4 | import { hot } from 'react-hot-loader'; 5 | import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom'; 6 | import { Provider } from 'mobx-react'; 7 | import App from 'layout/App'; 8 | import store from 'store'; 9 | import { LocaleProvider } from 'antd'; 10 | import loginUtil from 'util/login'; 11 | import zh_CN from 'antd/lib/locale-provider/zh_CN'; 12 | import 'moment/locale/zh-cn'; 13 | 14 | import Home from 'page/Home'; 15 | 16 | import 'antd/dist/antd.css'; 17 | import 'ant-design-pro/dist/ant-design-pro.css'; 18 | import 'stylesheet/cantd.less'; 19 | import 'stylesheet/app.less'; 20 | 21 | function getComponentAsync(loader) { 22 | return loadable({ 23 | loader: () => loader, 24 | loading: Loading, 25 | timeout: 10000 26 | }); 27 | } 28 | 29 | const Root = () => ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | { 37 | loginUtil.isLogin() 38 | ? ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | 51 | 56 | 57 | 62 | 63 | 68 | 69 | 74 | 75 | 80 | 85 | 86 | 91 | 96 | 101 | 102 | 103 | 104 | 107 | 108 | 109 | ) 110 | : 111 | } 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | 119 | export default hot(module)(Root); 120 | -------------------------------------------------------------------------------- /src/layout/SiderMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | import { Menu, Icon } from 'antd'; 4 | 5 | const { SubMenu } = Menu; 6 | 7 | // import style from './style.less'; 8 | 9 | @withRouter 10 | class SiderMenu extends Component { 11 | handleMenuClick = ({ key }) => { 12 | const { history } = this.props; 13 | 14 | if (key === '/project') { 15 | history.push(key); 16 | } 17 | } 18 | 19 | render() { 20 | const { collapsed, location } = this.props; 21 | 22 | return ( 23 | 32 | 33 | 34 | {/* 首页 */} 35 | 首页 36 | 37 | 表单页}> 38 | 基础表单 39 | 分步表单 40 | 41 | 列表页}> 42 | 查询列表 43 | 44 | 结果页}> 45 | 46 | 成功页 47 | 48 | 49 | 失败页 50 | 51 | 52 | 异常页}> 53 | 54 | 403 55 | 56 | 57 | 404 58 | 59 | 60 | 500 61 | 62 | 63 | {/* 64 | 65 | 创建小程序 66 | 67 | 68 | 创建广告位 69 | 70 | 71 | 财务管理 72 | 73 | */} 74 | 个人中心}> 75 | 修改密码 76 | 77 | 78 | ); 79 | } 80 | } 81 | 82 | export default SiderMenu; 83 | -------------------------------------------------------------------------------- /src/page/403/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Exception } from 'ant-design-pro'; 3 | 4 | export default class E403 extends Component { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/page/404/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Exception } from 'ant-design-pro'; 3 | 4 | export default class E404 extends Component { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/page/500/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Exception } from 'ant-design-pro'; 3 | 4 | export default class E500 extends Component { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/page/Account/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Input, Button, Form, Card, message, Row, Col } from 'antd'; 3 | import ajax from 'util/api/ajax'; 4 | import { putPassword } from 'util/api'; 5 | import loginUtil from 'util/login'; 6 | 7 | const formItemLayout = { 8 | labelCol: { 9 | xs: { span: 24 }, 10 | sm: { span: 5 } 11 | }, 12 | wrapperCol: { 13 | xs: { span: 24 }, 14 | sm: { span: 10 } 15 | } 16 | }; 17 | 18 | class RePassword extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | CaptchaButtonValue: '获取验证码', 23 | disabled: false, 24 | configPhoneNumber: false 25 | }; 26 | this.initial(); 27 | } 28 | 29 | initial = () => { 30 | console.log('initial with here!'); 31 | }; 32 | 33 | handleSubmit = (e) => { 34 | e.preventDefault(); 35 | this.props.form.validateFieldsAndScroll(async (err, values) => { 36 | if (!err) { 37 | console.log('Received values of form: ', values); 38 | if (!this.state.configPhoneNumber) { 39 | const res = await putPassword( 40 | { 41 | phone: values.currentPhoneNumber, 42 | oldPassWord: values.oldPassword, 43 | newPassWord: values.newPassWord 44 | } 45 | ); 46 | console.log(res); 47 | if (res.code === 0) { 48 | message.success('密码修改成功!'); 49 | } else { 50 | message.success('修改失败 请您稍后再次尝试!'); 51 | } 52 | // ajax({ 53 | // method: 'PUT', 54 | // url: '/api/v1/user/password', 55 | // data: { 56 | // phone: values.currentPhoneNumber, 57 | // oldPassWord: values.oldPassword, 58 | // newPassWord: values.newPassWord 59 | // } 60 | // }) 61 | // .then((res) => { 62 | // console.log(res); 63 | // if (res.code === 0) { 64 | // message.success('密码修改成功!'); 65 | // } else { 66 | // message.success('修改失败 请您稍后再次尝试!'); 67 | // } 68 | // }) 69 | // .catch((error) => { 70 | // console.log(error); 71 | // message.error('出了些问题,密码未能修改成功,如有需要请联系客服!'); 72 | // }); 73 | } else { 74 | ajax({ 75 | url: '/api/v1/user/phone', 76 | method: 'PUT', 77 | data: { 78 | phone: values.newPhoneNumber, 79 | password: values.password, 80 | captcha: values.captcha 81 | } 82 | }) 83 | .then((res) => { 84 | console.log(res); 85 | if (res.code === 0) { 86 | message.success('手机号修改成功!'); 87 | } else { 88 | message.success('修改失败 请您稍后再次尝试!'); 89 | } 90 | }) 91 | .catch((error) => { 92 | console.log(error); 93 | message.error('出了些问题,手机号未能修改成功,如有需要请联系客服!'); 94 | }); 95 | } 96 | } 97 | }); 98 | }; 99 | 100 | validateToNextPassword = (rule, value, callback) => { 101 | const { form } = this.props.form; 102 | if (value && this.state.confirmDirty) { 103 | form.validateFields(['compareNewPassWord'], { force: true }); 104 | } 105 | callback(); 106 | }; 107 | 108 | compareToFirstPassword = (rule, value, callback) => { 109 | const { form } = this.props.form; 110 | if (value && value !== form.getFieldValue('newPassWord')) { 111 | callback('两次密码不一致!'); 112 | } else { 113 | callback(); 114 | } 115 | }; 116 | 117 | render() { 118 | const userInfo = loginUtil.getUserInfo() || {}; 119 | const { getFieldDecorator } = this.props.form; 120 | // const { form: { validateFields } } = this.props; 121 | 122 | return ( 123 |
124 | 125 |
126 | 127 | {getFieldDecorator('currentPhoneNumber', { 128 | initialValue: userInfo.name 129 | })()} 130 | 141 | 142 | {!this.state.configPhoneNumber 143 | ? [ 144 | 145 | {getFieldDecorator('oldPassWord', { 146 | rules: [ 147 | { 148 | required: true, 149 | message: '请输入您的原密码!' 150 | } 151 | ] 152 | })()} 153 | , 154 | 155 | {getFieldDecorator('newPassWord', { 156 | rules: [ 157 | { 158 | required: true, 159 | pattern: /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{7,18}$/, 160 | message: '请输入数字+字母,8-18位的新密码!' 161 | } 162 | ] 163 | })()} 164 | , 165 | 166 | {getFieldDecorator('compareNewPassWord', { 167 | rules: [ 168 | { 169 | required: true, 170 | message: '请确认您的新密码' 171 | }, 172 | { 173 | validator: this.compareToFirstPassword 174 | } 175 | ] 176 | })()} 177 | 178 | ] 179 | : [ 180 | 181 | {getFieldDecorator('password', { 182 | rules: [ 183 | { 184 | required: true, 185 | message: '请输入登录密码' 186 | } 187 | ] 188 | })()} 189 | , 190 | 191 | {getFieldDecorator('newPhoneNumber', { 192 | rules: [ 193 | { 194 | required: true, 195 | pattern: /^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}$/, 196 | message: '请核对您的新手机号!' 197 | } 198 | ] 199 | })()} 200 | , 201 | 202 | {getFieldDecorator('captcha', { 203 | rules: [ 204 | { 205 | required: true, 206 | message: '请输入验证码!' 207 | } 208 | ] 209 | })( 210 | 211 | 212 | 213 | 214 | 215 | 272 | 273 | 274 | )} 275 | 276 | ]} 277 | 278 | 285 | 288 | 289 |
290 |
291 |
292 | ); 293 | } 294 | } 295 | 296 | export default Form.create()(RePassword); 297 | -------------------------------------------------------------------------------- /src/page/BasicForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import cssModules from 'react-css-modules'; 4 | import { Card, Button, Divider, Spin } from 'antd'; 5 | import Form from 'component/PlanForm'; 6 | import styles from './style.less'; 7 | 8 | @inject('basicFormStore') 9 | @observer 10 | @cssModules(styles) 11 | class BasicForm extends Component { 12 | state = { 13 | // value: 1 14 | } 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.store = this.props.basicFormStore; 20 | } 21 | 22 | 23 | async componentWillMount() { 24 | const { location, match, history } = this.props; 25 | 26 | await this.store.onWillMount(location, match, history); 27 | 28 | window.dplus.track('page_load', { 29 | name: '基础表单页', 30 | url: this.props.location.pathname 31 | }); 32 | } 33 | 34 | handleSubmit = () => { 35 | const { submit } = this.store; 36 | const { form } = this.exchangeForm.props; 37 | 38 | form.validateFields((err, values) => { 39 | if (!err) { 40 | submit(values); 41 | } 42 | }); 43 | } 44 | 45 | render() { 46 | // const { exchagneCreateStore: store } = this.props; 47 | const { loading, adLoading, fields, types, subTypes, onFormChange } = this.store; 48 | 49 | return ( 50 |
51 | 52 | 53 |
(this.exchangeForm = f)} 55 | adLoading={adLoading} 56 | mode="edit" 57 | {...fields} 58 | types={types} 59 | subTypes={subTypes} 60 | onChange={onFormChange} 61 | /> 62 |
63 | 64 |
65 | 66 | 67 |

说明

68 |

名称说明

69 |

描述

70 |

如果需要,这里可以放一些关于产品的常见问题说明。

71 |
72 | 73 | 74 |
75 | ); 76 | } 77 | } 78 | 79 | export default BasicForm; 80 | -------------------------------------------------------------------------------- /src/page/BasicForm/style.less: -------------------------------------------------------------------------------- 1 | .actions { 2 | text-align: center; 3 | 4 | .btn-next { 5 | margin-left: 20px; 6 | } 7 | } -------------------------------------------------------------------------------- /src/page/DataReport/DataTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, Card } from 'antd'; 3 | 4 | class DataTable extends React.Component { 5 | createColumns() { 6 | const columns = [{ 7 | title: '时间', 8 | dataIndex: 'time', 9 | key: 'time' 10 | }, { 11 | title: '访问量', 12 | dataIndex: 'value', 13 | key: 'value' 14 | }]; 15 | 16 | return columns; 17 | } 18 | 19 | render() { 20 | const { data } = this.props; 21 | 22 | return ( 23 | 24 | ('数据详细')} 26 | columns={this.createColumns()} 27 | dataSource={data} 28 | rowKey="time" 29 | /> 30 | 31 | ); 32 | } 33 | } 34 | 35 | export default DataTable; 36 | -------------------------------------------------------------------------------- /src/page/DataReport/LineChart.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import moment from 'moment'; 4 | import { Card, DatePicker, Row, Col } from 'antd'; 5 | import { NumberInfo } from 'ant-design-pro'; 6 | import ReactHighcharts from 'react-highcharts'; 7 | import styles from './style.less'; 8 | 9 | const { RangePicker } = DatePicker; 10 | 11 | @cssModules(styles) 12 | class LineChart extends Component { 13 | static defaultProps = { 14 | ranges: { 15 | 今日: [moment(), moment()], 16 | 本周: [moment().startOf('week'), moment()], 17 | 本月: [moment().startOf('month'), moment()], 18 | 本年: [moment().startOf('year'), moment()] 19 | } 20 | } 21 | 22 | renderDashboard() { 23 | const { dashboardData } = this.props; 24 | const { indicator1, indicator2, indicator3 } = dashboardData; 25 | 26 | return ( 27 | 28 | 29 | 33 | 34 | 35 | 39 | 40 | 41 | 45 | 46 | 47 | ); 48 | } 49 | 50 | render() { 51 | const { loading, ranges, chartOptions, onRangeChange } = this.props; 52 | return ( 53 | 58 |
59 | 65 |
66 | {this.renderDashboard()} 67 |
68 | 71 |
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default LineChart; 78 | -------------------------------------------------------------------------------- /src/page/DataReport/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import cssModules from 'react-css-modules'; 4 | import { PageHeader } from 'ant-design-pro'; 5 | import { Card } from 'antd'; 6 | import LineChart from './LineChart'; 7 | import DataTable from './DataTable'; 8 | 9 | import styles from './style.less'; 10 | 11 | const breadcrumbList = [{ 12 | title: '计划管理' 13 | }, { 14 | title: '计划列表', 15 | href: '/project/list/search' 16 | }, { 17 | title: '数据报表' 18 | }]; 19 | 20 | @inject('reportStore') 21 | @observer 22 | @cssModules(styles) 23 | class DataReport extends Component { 24 | async componentWillMount() { 25 | const { match, reportStore } = this.props; 26 | const { id } = match.params || {}; 27 | await reportStore.onWillMount(id); 28 | } 29 | 30 | render() { 31 | const { 32 | loading, 33 | name, 34 | data, 35 | dashboardData, 36 | chartOptions, 37 | getReportData 38 | } = this.props.reportStore; 39 | 40 | return ( 41 |
42 | 43 | 44 | 50 | 54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | export default DataReport; 61 | -------------------------------------------------------------------------------- /src/page/DataReport/style.less: -------------------------------------------------------------------------------- 1 | .chart-toolbar { 2 | height: 60px; 3 | display: flex; 4 | justify-content: flex-end; 5 | } 6 | 7 | .chart { 8 | margin-top: 30px; 9 | } -------------------------------------------------------------------------------- /src/page/Home/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Col, Row } from 'antd'; 3 | import { addThousandSeparator } from 'util'; 4 | import SummaryCard from './SummaryCard'; 5 | 6 | const Dashboard = ({ loading, amount, pv, count, rate }) => ( 7 | 8 |
9 | 15 | 16 | 17 | 23 | 24 | 25 | 31 | 32 | 33 | 39 | 40 | 41 | ); 42 | 43 | export default Dashboard; 44 | -------------------------------------------------------------------------------- /src/page/Home/LineChart.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import moment from 'moment'; 4 | import { Card, DatePicker, Row, Col, Tabs } from 'antd'; 5 | import { NumberInfo } from 'ant-design-pro'; 6 | import ReactHighcharts from 'react-highcharts'; 7 | import styles from './style.less'; 8 | 9 | const { RangePicker } = DatePicker; 10 | const { TabPane } = Tabs; 11 | 12 | @cssModules(styles) 13 | class LineChart extends Component { 14 | handleTabChange = (key) => { 15 | const { onIndicatorChange } = this.props; 16 | typeof onIndicatorChange === 'function' && onIndicatorChange(key); 17 | }; 18 | 19 | renderDashboard() { 20 | const { dashboardData } = this.props; 21 | 22 | return ( 23 | 24 | 25 | 29 | 30 | 31 | 35 | 36 | 37 | 41 | 42 | 43 | ); 44 | } 45 | 46 | renderDashboradTab() { 47 | const { indicators } = this.props; 48 | 49 | return ( 50 |
51 | 52 | {indicators.map(item => ( 53 | 56 | 57 | 58 | } 59 | key={item.id} 60 | /> 61 | ))} 62 | 63 |
64 | ); 65 | } 66 | 67 | render() { 68 | const { loading, ranges, chartOptions, onRangeChange } = this.props; 69 | return ( 70 | 75 |
76 | 82 |
83 | {this.renderDashboradTab()} 84 |
85 | 88 |
89 |
90 | ); 91 | } 92 | } 93 | 94 | export default LineChart; 95 | -------------------------------------------------------------------------------- /src/page/Home/SummaryCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import { Tooltip, Icon } from 'antd'; 4 | import { Charts } from 'ant-design-pro'; 5 | import styles from './style.less'; 6 | 7 | const { ChartCard } = Charts; 8 | 9 | @cssModules(styles) 10 | class SummaryCard extends Component { 11 | render() { 12 | const { total, title, intro, loading } = this.props; 13 | return ( 14 | 19 | 20 | 21 | } 22 | total={total} 23 | /> 24 | ); 25 | } 26 | } 27 | 28 | export default SummaryCard; 29 | -------------------------------------------------------------------------------- /src/page/Home/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import { PropTypes } from 'prop-types'; 3 | import { inject, observer } from 'mobx-react'; 4 | import cssModules from 'react-css-modules'; 5 | import moment from 'moment'; 6 | import { Card } from 'antd'; 7 | import Dashboard from './Dashboard'; 8 | import LineChart from './LineChart'; 9 | import styles from './style.less'; 10 | 11 | @inject('homeStore') 12 | @observer 13 | @cssModules(styles) 14 | class Home extends Component { 15 | async componentWillMount() { 16 | const { onWillMount } = this.props.homeStore; 17 | await onWillMount(); 18 | 19 | window.dplus.track('page_load', { 20 | name: '首页', 21 | url: this.props.location.pathname 22 | }); 23 | } 24 | 25 | onChange = () => { 26 | 27 | } 28 | 29 | getRagnes = () => { 30 | // const endDate = moment().subtract(1, 'days'); // 昨天 31 | const endDate = moment(); 32 | 33 | return { 34 | 今日: [moment(), endDate], 35 | 本周: [moment().startOf('week'), endDate], 36 | 本月: [moment().startOf('month'), endDate], 37 | 本年: [moment().startOf('year'), endDate] 38 | }; 39 | } 40 | 41 | render() { 42 | const { 43 | loading, 44 | amount, 45 | pv, 46 | rate, 47 | count, 48 | indicators, 49 | chartOptions, 50 | changeIndicator, 51 | changeDateRange 52 | } = this.props.homeStore; 53 | 54 | return ( 55 |
56 | 57 | 64 | 72 | 73 |
74 | ); 75 | } 76 | } 77 | 78 | export default Home; 79 | -------------------------------------------------------------------------------- /src/page/Home/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .summary-card { 4 | position: relative; 5 | .chart-top { 6 | position: relative; 7 | overflow: hidden; 8 | width: 100%; 9 | } 10 | .meta-wrap { 11 | float: left; 12 | } 13 | .meta { 14 | color: @text-color-secondary; 15 | font-size: @font-size-base; 16 | line-height: 22px; 17 | height: 22px; 18 | } 19 | .action { 20 | cursor: pointer; 21 | position: absolute; 22 | top: 0; 23 | right: 0; 24 | } 25 | .total { 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | word-break: break-all; 29 | white-space: nowrap; 30 | color: @heading-color; 31 | margin-top: 4px; 32 | margin-bottom: 0; 33 | font-size: 30px; 34 | line-height: 38px; 35 | height: 38px; 36 | } 37 | } 38 | 39 | .chart-toolbar { 40 | height: 60px; 41 | display: flex; 42 | justify-content: flex-end; 43 | } 44 | 45 | .chart { 46 | margin-top: 30px; 47 | } -------------------------------------------------------------------------------- /src/page/Login/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Link, withRouter } from 'react-router-dom'; 4 | import cssModules from 'react-css-modules'; 5 | import { message, Checkbox } from 'antd'; 6 | import Login from 'ant-design-pro/lib/Login'; 7 | import { login, getCaptcha } from 'util/api'; 8 | import loginUtil from 'util/login'; 9 | import AppFooter from 'component/AppFooter'; 10 | 11 | import styles from './style.less'; 12 | 13 | const { Mobile, Captcha, Submit } = Login; 14 | 15 | @withRouter 16 | @inject('loginStore') 17 | @observer 18 | @cssModules(styles) 19 | class LoginPage extends Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.store = this.props.loginStore; 24 | this.phoneRef = React.createRef(); 25 | } 26 | 27 | state = { 28 | autoLogin: true 29 | }; 30 | 31 | changeAutoLogin = (e) => { 32 | this.setState({ 33 | autoLogin: e.target.checked 34 | }); 35 | }; 36 | 37 | handleSubmit = async (err, values) => { 38 | if (!err) { 39 | const res = await login(values.phone, values.captcha); 40 | if (res.code === 0) { 41 | console.log('res ', res); 42 | const { autoLogin } = this.state; 43 | if (autoLogin) { 44 | loginUtil.saveUserInfo(res.data); 45 | } 46 | message.success('登录成功'); 47 | window.location.href = '/'; 48 | } else if (res.code === 10200002) { 49 | message.error('验证码错误!'); 50 | } 51 | } 52 | }; 53 | 54 | getCaptcha = () => 55 | new Promise((resolve, reject) => { 56 | this.phoneRef.current.validateFields(['phone'], {}, async (err, values) => { 57 | if (!err) { 58 | const res = await getCaptcha(values.phone); 59 | if (res.code === 0) { 60 | message.success('验证码已发送,请您留意查看!'); 61 | resolve(); 62 | } else { 63 | reject(err); 64 | } 65 | } else { 66 | reject(err); 67 | } 68 | }); 69 | }); 70 | 71 | render() { 72 | return ( 73 | // @TODO 74 |
75 |
76 |
77 |
78 | 79 | {/* logo */} 80 | logo 85 | Ant Design 86 | 87 |
88 |
Ant Design 是西湖区最具影响力的 Web 设计规范
89 |
90 |
91 | 92 | 93 | 99 | 104 | 登录 105 | 106 |
107 | 108 | 保持登录 109 | 110 |
111 |
112 |
113 |
114 | 115 |
116 | ); 117 | } 118 | } 119 | 120 | export default LoginPage; 121 | -------------------------------------------------------------------------------- /src/page/Login/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .content { 12 | padding: 72px 0; 13 | flex: 1; 14 | } 15 | 16 | .top { 17 | text-align: center; 18 | } 19 | 20 | .header { 21 | height: 44px; 22 | line-height: 44px; 23 | a { 24 | text-decoration: none; 25 | } 26 | } 27 | 28 | .logo { 29 | height: 44px; 30 | vertical-align: top; 31 | margin-right: 16px; 32 | } 33 | 34 | .title { 35 | font-size: 33px; 36 | color: @heading-color; 37 | font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; 38 | font-weight: 600; 39 | position: relative; 40 | top: 2px; 41 | } 42 | 43 | .desc { 44 | font-size: @font-size-base; 45 | color: @text-color-secondary; 46 | margin-top: 12px; 47 | margin-bottom: 40px; 48 | } 49 | 50 | .login { 51 | width: 368px; 52 | margin: 0 auto; 53 | } -------------------------------------------------------------------------------- /src/page/Result/Error.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Button, Icon, Card } from 'antd'; 3 | import { Result } from 'ant-design-pro'; 4 | 5 | const extra = ( 6 | 7 |
15 | 您提交的内容有如下错误: 16 |
17 |
18 | 19 | 您的账户已被冻 20 | 21 | 立即解冻 22 | 23 | 24 |
25 |
26 | 27 | 您的账户还不具备申请资格 28 | 29 | 立即升级 30 | 31 | 32 |
33 |
34 | ); 35 | 36 | const actions = ( 37 | 40 | ); 41 | 42 | export default () => ( 43 | 52 | 60 | 61 | ); 62 | -------------------------------------------------------------------------------- /src/page/Result/Success.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button, Row, Col, Icon, Steps, Card } from 'antd'; 4 | import { Result } from 'ant-design-pro'; 5 | 6 | const { Step } = Steps; 7 | 8 | const desc1 = ( 9 |
18 |
19 | 曲丽丽 20 | 21 |
22 |
2016-12-12 12:32
23 |
24 | ); 25 | 26 | const desc2 = ( 27 |
35 |
36 | 周毛毛 37 | 38 |
39 | 44 |
45 | ); 46 | 47 | const extra = ( 48 | 49 |
57 | 项目名称 58 |
59 | 60 |
61 | 62 | 项目 ID: 63 | 64 | 23421 65 | 66 | 67 | 68 | 负责人: 69 | 70 | 曲丽丽 71 | 72 | 73 | 74 | 生效时间: 75 | 76 | 2016-12-12 ~ 2017-12-12 77 | 78 | 79 | 80 | 83 | 创建项目 84 | 85 | } 86 | description={desc1} 87 | /> 88 | 91 | 部门初审 92 | 93 | } 94 | description={desc2} 95 | /> 96 | 99 | 财务复核 100 | 101 | } 102 | /> 103 | 106 | 完成 107 | 108 | } 109 | /> 110 | 111 | 112 | ); 113 | 114 | const actions = ( 115 | 116 | 119 | 122 | 125 | 126 | ); 127 | 128 | export default () => ( 129 | 138 | 146 | 147 | ); 148 | -------------------------------------------------------------------------------- /src/page/SearchList/DataTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Table, Divider, Popover, Popconfirm } from 'antd'; 4 | 5 | class DataTable extends React.Component { 6 | createColumns() { 7 | const { onDelete } = this.props; 8 | 9 | const columns = [{ 10 | title: '名称', 11 | dataIndex: 'name', 12 | key: 'name', 13 | render: (text, record) => {text} 14 | }, { 15 | title: '描述', 16 | dataIndex: 'description', 17 | key: 'description' 18 | }, { 19 | title: '类型', 20 | dataIndex: 'type.name', 21 | key: 'type' 22 | }, { 23 | title: '子类型', 24 | dataIndex: 'subType.name', 25 | key: 'subType' 26 | }, { 27 | title: 'logo', 28 | dataIndex: 'logoUrl', 29 | key: 'logoUrl', 30 | render: text => ( 31 | } trigger="click"> 32 | 33 | 34 | ) 35 | }, { 36 | title: '状态', 37 | dataIndex: 'state', 38 | key: 'state', 39 | render: (text) => { 40 | const map = { 41 | 0: '进行中', 42 | 1: '暂停', 43 | 2: '结束' 44 | }; 45 | return map[text]; 46 | } 47 | }, { 48 | title: '操作', 49 | dataIndex: 'id', 50 | key: 'id', 51 | render: (text, record) => ( 52 | 53 | 报表 54 | 55 | 修改 56 | { 57 | record.state === 2 && ( 58 | 59 | 60 | onDelete(text, record)} okText="确定" cancelText="取消"> 61 | 删除 62 | 63 | 64 | ) 65 | } 66 | 67 | ) 68 | }]; 69 | 70 | return columns; 71 | } 72 | 73 | render() { 74 | const { data } = this.props; 75 | 76 | return
; 77 | } 78 | } 79 | 80 | export default DataTable; 81 | -------------------------------------------------------------------------------- /src/page/SearchList/FilterForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Icon, Input, Button } from 'antd'; 3 | 4 | const FormItem = Form.Item; 5 | 6 | class FilterForm extends React.Component { 7 | componentDidMount() { 8 | // To disabled submit button at the beginning. 9 | // this.props.form.validateFields(); 10 | } 11 | 12 | handleSubmit = (e) => { 13 | const { onSubmit } = this.props; 14 | e.preventDefault(); 15 | this.props.form.validateFields((err, values) => { 16 | if (!err) { 17 | console.log('Received values of form: ', values); 18 | 19 | typeof onSubmit === 'function' && onSubmit(values); 20 | } 21 | }); 22 | } 23 | 24 | render() { 25 | const { getFieldDecorator } = this.props.form; 26 | 27 | return ( 28 | 33 | 34 | {getFieldDecorator('name', { 35 | rules: [ 36 | // { required: true, message: 'Please input your name!' } 37 | ] 38 | })( 39 | } placeholder="计划名称" /> 40 | )} 41 | 42 | 43 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default Form.create()(FilterForm); 56 | -------------------------------------------------------------------------------- /src/page/SearchList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import cssModules from 'react-css-modules'; 4 | import { Button } from 'antd'; 5 | import { PageHeader } from 'ant-design-pro'; 6 | import FilterForm from './FilterForm'; 7 | import DataTable from './DataTable'; 8 | 9 | import styles from './style.less'; 10 | 11 | @inject('searchListStore') 12 | @observer 13 | @cssModules(styles) 14 | class SearchList extends Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.store = this.props.searchListStore; 19 | } 20 | 21 | async componentWillMount() { 22 | const { location, match, history } = this.props; 23 | 24 | await this.store.onWillMount(location, match, history); 25 | 26 | window.dplus.track('page_load', { 27 | name: '列表页', 28 | url: this.props.location.pathname 29 | }); 30 | } 31 | 32 | render() { 33 | const { create, search, remove, data } = this.store; 34 | return ( 35 |
36 | 37 |
38 | 39 | 40 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | export default SearchList; 48 | -------------------------------------------------------------------------------- /src/page/SearchList/style.less: -------------------------------------------------------------------------------- 1 | .btn-create { 2 | margin: 50px 0px 10px 0px; 3 | } -------------------------------------------------------------------------------- /src/page/StepForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import cssModules from 'react-css-modules'; 4 | import { Card, Steps, Button, Divider, Spin } from 'antd'; 5 | import Success from 'page/Result/Success'; 6 | import Form from 'component/PlanForm'; 7 | import styles from './style.less'; 8 | 9 | const { Step } = Steps; 10 | 11 | @inject('stepFormStore') 12 | @observer 13 | @cssModules(styles) 14 | class StepForm extends Component { 15 | state = { 16 | // value: 1 17 | } 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | this.store = this.props.stepFormStore; 23 | } 24 | 25 | 26 | async componentWillMount() { 27 | const { location, match, history } = this.props; 28 | 29 | await this.store.onWillMount(location, match, history); 30 | 31 | window.dplus.track('page_load', { 32 | name: '分步表单页', 33 | url: this.props.location.pathname 34 | }); 35 | } 36 | 37 | handleNext = () => { 38 | const { next } = this.store; 39 | const { form } = this.exchangeForm.props; 40 | 41 | form.validateFields((err, values) => { 42 | if (!err) { 43 | next(values); 44 | } 45 | }); 46 | } 47 | 48 | render() { 49 | // const { exchagneCreateStore: store } = this.props; 50 | const { loading, adLoading, step, fields, types, subTypes, onFormChange, prev, submit } = this.store; 51 | 52 | return ( 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
67 |
(this.exchangeForm = f)} 69 | adLoading={adLoading} 70 | mode={step === 0 ? 'edit' : 'view'} 71 | {...fields} 72 | types={types} 73 | subTypes={subTypes} 74 | onChange={onFormChange} 75 | /> 76 |
77 | { step === 2 && } 78 |
79 | { step === 0 && } 80 | { 81 | step === 1 && ( 82 | 83 | 84 | 85 | 86 | ) 87 | } 88 |
89 | { step !== 2 && } 90 | { 91 | step !== 2 && ( 92 | 93 |

说明

94 |

名称说明

95 |

描述

96 |

如果需要,这里可以放一些关于产品的常见问题说明。

97 |
98 | ) 99 | } 100 |
101 |
102 |
103 |
104 | ); 105 | } 106 | } 107 | 108 | export default StepForm; 109 | -------------------------------------------------------------------------------- /src/page/StepForm/style.less: -------------------------------------------------------------------------------- 1 | .actions { 2 | text-align: center; 3 | 4 | .btn-next { 5 | margin-left: 20px; 6 | } 7 | } -------------------------------------------------------------------------------- /src/store/BasicFormStore.js: -------------------------------------------------------------------------------- 1 | import { extendObservable, action } from 'mobx'; 2 | import { message } from 'antd'; 3 | import { getSubTypes, createPlan, editPlan, getPlanDetail } from 'util/api'; 4 | 5 | export default class BasicFormStore { 6 | constructor() { 7 | this.reset(true); 8 | } 9 | 10 | @action 11 | onWillMount = async (location, match, history) => { 12 | this.reset(); 13 | 14 | this.setRoute(location, match, history); 15 | 16 | this.loading = true; 17 | 18 | const { id } = match.params || {}; 19 | if (id) { 20 | await this.restore(id); 21 | } 22 | 23 | if (this.fields.type.value) { 24 | await this.getSubTypes(this.fields.type.value); 25 | } 26 | 27 | this.loading = false; 28 | } 29 | 30 | @action 31 | reset = (init) => { 32 | const state = { 33 | fields: { 34 | name: { value: '' }, // 名称 35 | description: { value: '' }, // 描述 36 | type: { value: undefined }, // 类型 37 | subType: { value: undefined }, // 子类型 38 | logo: { value: null }, // logo 39 | image: { value: null } // 图片 40 | }, 41 | 42 | types: [{ 43 | name: '类型1', 44 | id: 0 45 | }, { 46 | name: '类型2', 47 | id: 1 48 | }], 49 | subTypes: [], 50 | 51 | loading: false, // 是否显示加载状态 52 | subTypeLoading: false, 53 | submiting: false 54 | }; 55 | 56 | if (init) { 57 | extendObservable(this, state); 58 | } else { 59 | Object.keys(state).forEach(key => (this[key] = state[key])); 60 | } 61 | 62 | this.formValues = null; 63 | 64 | this.location = {}; 65 | this.match = {}; 66 | this.history = {}; 67 | } 68 | 69 | setRoute = (location, match, history) => { 70 | this.location = location; 71 | this.match = match; 72 | this.history = history; 73 | } 74 | 75 | @action 76 | restore = async (id) => { 77 | const res = await getPlanDetail(id); 78 | if (res.code === 0 && res.data) { 79 | const { name, description, type, subType, logoUrl, imageUrl } = res.data; 80 | this.fields.name.value = name; 81 | this.fields.type.value = type.id; 82 | this.fields.subType.value = subType.id; 83 | this.fields.description.value = description; 84 | this.fields.logo.value = { 85 | url: logoUrl, 86 | thumbnailUrl: logoUrl 87 | }; 88 | this.fields.image.value = { 89 | url: imageUrl, 90 | thumbnailUrl: imageUrl 91 | }; 92 | } 93 | } 94 | 95 | @action 96 | getSubTypes = async (type) => { 97 | this.subTypeLoading = true; 98 | 99 | const res = await getSubTypes({ 100 | type 101 | }); 102 | 103 | if (res.code === 0 && res.data && Array.isArray(res.data)) { 104 | this.subTypes = res.data; 105 | this.fields.subType.value = res.data[0].id; 106 | } 107 | 108 | this.subTypeLoading = false; 109 | } 110 | 111 | @action 112 | // 表单 onChange 113 | onFormChange = async (changedFields) => { 114 | if (changedFields.type) { 115 | await this.getSubTypes(changedFields.type.value); 116 | } 117 | 118 | this.fields = { 119 | ...this.fields, 120 | ...changedFields 121 | }; 122 | } 123 | 124 | @action 125 | submit = async (values) => { 126 | const { id } = this.match.params || {}; 127 | 128 | const typeMap = {}; 129 | const subTypeMap = {}; 130 | this.subTypes.forEach(item => (subTypeMap[item.id] = item)); 131 | this.types.forEach(item => (typeMap[item.id] = item)); 132 | 133 | const params = { 134 | ...values, 135 | type: typeMap[values.type], 136 | subType: subTypeMap[values.subType], 137 | id 138 | }; 139 | 140 | this.loading = true; 141 | const fn = id ? editPlan : createPlan; 142 | const res = await fn(params); 143 | this.loading = false; 144 | if (res.code === 0) { 145 | message.success((this.match.params || {}).id ? '修改成功' : '创建成功'); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/store/HomeStore.js: -------------------------------------------------------------------------------- 1 | import { extendObservable, action } from 'mobx'; 2 | import moment from 'moment'; 3 | import { getSummary, getData } from 'util/api'; 4 | 5 | export default class HomeStore { 6 | constructor() { 7 | this.reset(true); 8 | } 9 | 10 | @action 11 | reset = (init) => { 12 | const state = { 13 | amount: 0, 14 | pv: 0, 15 | count: 0, 16 | rate: 0, 17 | indicators: [], 18 | chartOptions: {}, 19 | currentIndicator: null, 20 | currentRange: [moment().format('YYYY-HH-mm'), moment().format('YYYY-HH-mm')], 21 | loading: true 22 | }; 23 | 24 | if (init) { 25 | extendObservable(this, state); 26 | } else { 27 | Object.keys(state).forEach(key => (this[key] = state[key])); 28 | } 29 | } 30 | 31 | @action 32 | onWillMount = async () => { 33 | this.reset(); 34 | 35 | this.loading = true; 36 | await this.getSummary(); 37 | await this.getData(); 38 | this.loading = false; 39 | } 40 | 41 | @action 42 | getSummary = async () => { 43 | const res = await getSummary(); 44 | if (res.code === 0) { 45 | const { data } = res; 46 | 47 | this.amount = data.amount; 48 | this.pv = data.pv; 49 | this.count = data.count; 50 | this.rate = data.rate; 51 | } 52 | } 53 | 54 | @action 55 | changeIndicator = async (indicatorId) => { 56 | this.currentIndicator = indicatorId; 57 | await this.getData(); 58 | } 59 | 60 | @action 61 | changeDateRange = async (_, rangeStr = []) => { 62 | this.currentRange = rangeStr; 63 | await this.getData(); 64 | } 65 | 66 | getData = async () => { 67 | const res = await getData(this.currentIndicator, this.currentRange); 68 | if (res.code === 0 && res.data) { 69 | const { indicators, categories, series } = res.data; 70 | this.indicators = indicators; 71 | this.chartOptions = this.transform2Chart('图表数据', categories, series); 72 | } else { 73 | this.chartOptions = this.transform2Chart('图表数据', [], []); 74 | } 75 | } 76 | 77 | transform2Chart = (title, categories, series) => { 78 | const chartOptions = { 79 | title: { 80 | text: title 81 | }, 82 | xAxis: { 83 | categories 84 | }, 85 | yAxis: { 86 | title: '', 87 | gridLineDashStyle: 'Dot' 88 | }, 89 | credits: false, 90 | series 91 | }; 92 | 93 | return chartOptions; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/store/LoginStore.js: -------------------------------------------------------------------------------- 1 | // import { extendObservable, action } from 'mobx'; 2 | 3 | export default class loginStore { 4 | // constructor() { 5 | // this.reset(true); 6 | // } 7 | 8 | // @action 9 | // reset = (init) => { 10 | // const state = { 11 | // fields: { 12 | // name: { value: '' }, // 留存名称 13 | // type: { value: 0 } // 推广类型 14 | // }, 15 | 16 | // loading: false, // 是否显示加载状态 17 | // submiting: false 18 | // }; 19 | 20 | // if (init) { 21 | // extendObservable(this, state); 22 | // } else { 23 | // Object.keys(state).forEach(key => (this[key] = state[key])); 24 | // } 25 | 26 | // this.location = {}; 27 | // this.match = {}; 28 | // this.history = {}; 29 | // } 30 | 31 | // setRoute = (location, match, history) => { 32 | // this.location = location; 33 | // this.match = match; 34 | // this.history = history; 35 | // } 36 | 37 | // @action 38 | // create = () => { 39 | // this.history.push('/project/exchangemgr/create'); 40 | // } 41 | } 42 | -------------------------------------------------------------------------------- /src/store/ReportStore.js: -------------------------------------------------------------------------------- 1 | import { extendObservable, action } from 'mobx'; 2 | import { getReportData } from 'util/api'; 3 | 4 | export default class ReportStore { 5 | constructor() { 6 | this.reset(true); 7 | 8 | this.planId = undefined; 9 | } 10 | 11 | @action 12 | reset = (init) => { 13 | const state = { 14 | name: '', 15 | data: [], 16 | dashboardData: {}, 17 | chartOptions: this.transform2Chart('图表标题', { xAxis: [], series: [] }), 18 | loading: true 19 | }; 20 | 21 | if (init) { 22 | extendObservable(this, state); 23 | } else { 24 | Object.keys(state).forEach(key => (this[key] = state[key])); 25 | } 26 | } 27 | 28 | @action 29 | onWillMount = async (id) => { 30 | this.reset(); 31 | 32 | this.planId = id; 33 | 34 | this.loading = true; 35 | await this.getReportData(); 36 | this.loading = false; 37 | } 38 | 39 | @action 40 | getReportData = async (rangeStart, rangeEnd) => { 41 | const res = await getReportData(this.planId, rangeStart, rangeEnd); 42 | if (res.code === 0 && res.data) { 43 | const { name, indicator1, indicator2, indicator3, details } = res.data; 44 | this.name = name; 45 | this.data = details; 46 | this.dashboardData = { 47 | indicator1, 48 | indicator2, 49 | indicator3 50 | }; 51 | this.chartOptions = this.transform2Chart('图表标题', details); 52 | } 53 | } 54 | 55 | transform2Chart = (title, data) => { 56 | if (!Array.isArray(data)) { 57 | data = []; 58 | } 59 | 60 | const categories = []; 61 | 62 | const series = [{ 63 | name: '指标1', 64 | data: [] 65 | }]; 66 | 67 | data.forEach((item) => { 68 | categories.push(item.time); 69 | series[0].data.push(Number(item.value)); 70 | }); 71 | 72 | const chartOptions = { 73 | title: { 74 | text: title 75 | }, 76 | xAxis: { 77 | categories 78 | }, 79 | yAxis: { 80 | title: '', 81 | gridLineDashStyle: 'Dot' 82 | }, 83 | credits: false, 84 | series 85 | }; 86 | 87 | return chartOptions; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/store/SearchListStore.js: -------------------------------------------------------------------------------- 1 | import { extendObservable, action } from 'mobx'; 2 | import { getPlans, removePlan } from 'util/api'; 3 | 4 | export default class SearchListStore { 5 | constructor() { 6 | this.reset(true); 7 | } 8 | 9 | @action 10 | reset = (init) => { 11 | const state = { 12 | data: [], 13 | loading: false // 是否显示加载状态 14 | }; 15 | 16 | if (init) { 17 | extendObservable(this, state); 18 | } else { 19 | Object.keys(state).forEach(key => (this[key] = state[key])); 20 | } 21 | 22 | this.location = {}; 23 | this.match = {}; 24 | this.history = {}; 25 | } 26 | 27 | @action 28 | onWillMount = async (location, match, history) => { 29 | this.reset(); 30 | 31 | this.setRoute(location, match, history); 32 | 33 | this.loading = true; 34 | await this.getPlans(''); 35 | this.loading = false; 36 | } 37 | 38 | setRoute = (location, match, history) => { 39 | this.location = location; 40 | this.match = match; 41 | this.history = history; 42 | } 43 | 44 | @action 45 | create = () => { 46 | this.history.push('/project/form/step'); 47 | } 48 | 49 | @action 50 | search = async (values) => { 51 | // console.log(values); 52 | await this.getPlans(values.name); 53 | } 54 | 55 | @action 56 | remove = async (id) => { 57 | this.loading = true; 58 | await removePlan(id); 59 | await getPlans(''); 60 | this.loading = false; 61 | } 62 | 63 | @action 64 | async getPlans(name) { 65 | const res = await getPlans(name, 0, 10000); 66 | if (res.code === 0) { 67 | this.data = res.data; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/store/StepFormStore.js: -------------------------------------------------------------------------------- 1 | import { extendObservable, action } from 'mobx'; 2 | // import { message } from 'antd'; 3 | import { getSubTypes, createPlan, editPlan, getPlanDetail } from 'util/api'; 4 | 5 | export default class StepFormStore { 6 | constructor() { 7 | this.reset(true); 8 | } 9 | 10 | @action 11 | onWillMount = async (location, match, history) => { 12 | this.reset(); 13 | 14 | this.setRoute(location, match, history); 15 | 16 | this.loading = true; 17 | 18 | const { id } = match.params || {}; 19 | if (id) { 20 | await this.restore(id); 21 | } 22 | 23 | if (this.fields.type.value) { 24 | await this.getSubTypes(this.fields.type.value); 25 | } 26 | 27 | this.loading = false; 28 | } 29 | 30 | @action 31 | reset = (init) => { 32 | const state = { 33 | fields: { 34 | name: { value: '' }, // 名称 35 | description: { value: '' }, // 描述 36 | type: { value: undefined }, // 类型 37 | subType: { value: undefined }, // 子类型 38 | logo: { value: null }, // logo 39 | image: { value: null } // 图片 40 | }, 41 | 42 | step: 0, 43 | 44 | types: [{ 45 | name: '类型1', 46 | id: 0 47 | }, { 48 | name: '类型2', 49 | id: 1 50 | }], 51 | subTypes: [], 52 | 53 | loading: false, // 是否显示加载状态 54 | subTypeLoading: false, 55 | submiting: false 56 | }; 57 | 58 | if (init) { 59 | extendObservable(this, state); 60 | } else { 61 | Object.keys(state).forEach(key => (this[key] = state[key])); 62 | } 63 | 64 | this.formValues = null; 65 | 66 | this.location = {}; 67 | this.match = {}; 68 | this.history = {}; 69 | } 70 | 71 | setRoute = (location, match, history) => { 72 | this.location = location; 73 | this.match = match; 74 | this.history = history; 75 | } 76 | 77 | @action 78 | restore = async (id) => { 79 | const res = await getPlanDetail(id); 80 | if (res.code === 0 && res.data) { 81 | const { name, description, type, subType, logoUrl, imageUrl } = res.data; 82 | this.fields.name.value = name; 83 | this.fields.type.value = type.id; 84 | this.fields.subType.value = subType.id; 85 | this.fields.description.value = description; 86 | this.fields.logo.value = { 87 | url: logoUrl, 88 | thumbnailUrl: logoUrl 89 | }; 90 | this.fields.image.value = { 91 | url: imageUrl, 92 | thumbnailUrl: imageUrl 93 | }; 94 | } 95 | } 96 | 97 | @action 98 | getSubTypes = async (type) => { 99 | this.subTypeLoading = true; 100 | 101 | const res = await getSubTypes({ 102 | type 103 | }); 104 | 105 | if (res.code === 0 && res.data && Array.isArray(res.data)) { 106 | this.subTypes = res.data; 107 | this.fields.subType.value = res.data[0].id; 108 | } 109 | 110 | this.subTypeLoading = false; 111 | } 112 | 113 | @action 114 | // 表单 onChange 115 | onFormChange = async (changedFields) => { 116 | if (changedFields.type) { 117 | await this.getSubTypes(changedFields.type.value); 118 | } 119 | 120 | this.fields = { 121 | ...this.fields, 122 | ...changedFields 123 | }; 124 | } 125 | 126 | @action 127 | next = (values) => { 128 | if (values) { 129 | this.formValues = values; 130 | } 131 | this.step += 1; 132 | } 133 | 134 | @action 135 | prev = () => { 136 | this.step -= 1; 137 | } 138 | 139 | @action 140 | submit = async () => { 141 | if (this.formValues) { 142 | const { id } = this.match.params || {}; 143 | 144 | const typeMap = {}; 145 | const subTypeMap = {}; 146 | this.subTypes.forEach(item => (subTypeMap[item.id] = item)); 147 | this.types.forEach(item => (typeMap[item.id] = item)); 148 | 149 | const params = { 150 | ...this.formValues, 151 | type: typeMap[this.formValues.type], 152 | subType: subTypeMap[this.formValues.subType], 153 | id 154 | }; 155 | 156 | this.loading = true; 157 | const fn = id ? editPlan : createPlan; 158 | const res = await fn(params); 159 | this.loading = false; 160 | if (res.code === 0) { 161 | this.step = 2; 162 | // message.success((this.match.params || {}).id ? '修改成功' : '创建成功'); 163 | // this.history.push('/project/exchangemgr/list'); 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import BasicFormStore from './BasicFormStore'; 2 | import StepFormStore from './StepFormStore'; 3 | import SearchListStore from './SearchListStore'; 4 | import LoginStore from './LoginStore'; 5 | import HomeStore from './HomeStore'; 6 | import ReportStore from './ReportStore'; 7 | 8 | export default { 9 | basicFormStore: new BasicFormStore(), 10 | stepFormStore: new StepFormStore(), 11 | searchListStore: new SearchListStore(), 12 | loginStore: new LoginStore(), 13 | homeStore: new HomeStore(), 14 | reportStore: new ReportStore() 15 | }; 16 | -------------------------------------------------------------------------------- /src/util/api/ajax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 请求API代理 3 | * @author gongzhen 4 | * @param options 5 | * @param 参数详解:https://github.com/axios/axios 6 | * @since 2018-09-13 7 | */ 8 | import axios from 'axios'; 9 | import errorHint from './errorHint'; 10 | import loginUtil from '../login'; 11 | 12 | // 默认配置 13 | axios.defaults.method = 'post'; 14 | axios.defaults.withCredentials = true; 15 | 16 | const noNeedAuthAPI = [ 17 | '/api/v1/captcha', 18 | '/api/v1/user/auth/captcha' 19 | ]; 20 | 21 | // 添加请求拦截器 22 | axios.interceptors.request.use((config) => { 23 | // console.log('axios.interceptors.request.use ', process.env.API === 'mock'); 24 | if (process.env.API === 'mock') { 25 | config.url = `http://rap2api.taobao.org/app/mock/121297/${config.method}${config.url}`; 26 | } else { 27 | if (noNeedAuthAPI.indexOf(config.url) > -1) { 28 | return config; 29 | } 30 | 31 | const userInfo = loginUtil.getUserInfo(); 32 | if (userInfo && userInfo.token) { 33 | config.headers.common.Authorization = userInfo.token; 34 | } else { 35 | window.location.href = '/login'; 36 | } 37 | } 38 | // console.log('show request: ', config); 39 | 40 | return config; 41 | }); 42 | 43 | const request = (options, resolve) => axios({ ...options }).then((resp) => { 44 | // console.log('resp', resp); 45 | const data = resp.data || {}; 46 | resolve(data); 47 | 48 | if (data.code !== 200 && options.handle === false) { 49 | errorHint.push(data.msg); 50 | } 51 | }).catch((err) => { 52 | const data = { 53 | status: false, 54 | code: '-1', 55 | msg: `HTTP ERROR: ${err.message}` 56 | }; 57 | resolve(data); 58 | errorHint.push(data.msg); 59 | }); 60 | 61 | const ajax = options => new Promise(resolve => request(options, resolve)); 62 | 63 | export default ajax; 64 | -------------------------------------------------------------------------------- /src/util/api/errorHint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal } from 'antd'; 3 | 4 | class ErrorContent extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | msgs: [] 10 | }; 11 | 12 | ErrorContent.single = this; 13 | } 14 | 15 | push(msg) { 16 | if (this.state.msgs.indexOf(msg) < 0) { 17 | this.state.msgs.push(msg); 18 | this.setState({ msgs: this.state.msgs }); 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 | {this.state.msgs.map(item => ( 26 |
{item}
27 | ))} 28 |
29 | ); 30 | } 31 | } 32 | 33 | let isWorking = false; 34 | 35 | const push = (msg) => { 36 | if (isWorking) { 37 | ErrorContent.single.push(msg); 38 | } else { 39 | isWorking = true; 40 | Modal.error({ 41 | title: '请求错误', 42 | content: , 43 | okText: '确认', 44 | onOk: () => { 45 | isWorking = false; 46 | } 47 | }); 48 | ErrorContent.single.push(msg); 49 | } 50 | }; 51 | 52 | export default { 53 | push 54 | }; 55 | -------------------------------------------------------------------------------- /src/util/api/index.js: -------------------------------------------------------------------------------- 1 | import ajax from './ajax'; 2 | 3 | export function login(phone, captcha) { 4 | return ajax({ 5 | url: '/api/v1/login', 6 | method: 'post', 7 | data: { 8 | phone, 9 | captcha 10 | } 11 | }); 12 | } 13 | 14 | export function getCaptcha(phone) { 15 | return ajax({ 16 | url: '/api/v1/captcha', 17 | method: 'post', 18 | data: { 19 | phone 20 | } 21 | }); 22 | } 23 | 24 | export function getSubTypes(typeId) { 25 | return ajax({ 26 | url: '/api/v1/subtypes', 27 | method: 'post', 28 | data: { 29 | typeId 30 | } 31 | }); 32 | } 33 | 34 | export function createPlan(data) { 35 | return ajax({ 36 | url: '/api/v1/plan/create', 37 | method: 'post', 38 | data 39 | }); 40 | } 41 | 42 | export function editPlan(data) { 43 | return ajax({ 44 | url: '/api/v1/plan/edit', 45 | method: 'post', 46 | data 47 | }); 48 | } 49 | 50 | export function getPlanDetail(id) { 51 | return ajax({ 52 | url: '/api/v1/plan/view', 53 | method: 'post', 54 | data: { 55 | id 56 | } 57 | }); 58 | } 59 | 60 | export function removePlan(id) { 61 | return ajax({ 62 | url: '/api/v1/plan/delete', 63 | method: 'post', 64 | data: { 65 | id 66 | } 67 | }); 68 | } 69 | 70 | export function getPlans(name, pageNum, pageSize) { 71 | return ajax({ 72 | url: '/api/v1/plan/list', 73 | method: 'post', 74 | data: { 75 | name, 76 | pageNum, 77 | pageSize 78 | } 79 | }); 80 | } 81 | 82 | export function getSummary() { 83 | return ajax({ 84 | url: '/api/v1/summary', 85 | method: 'post' 86 | }); 87 | } 88 | 89 | export function getData(indicatorId, dateRange) { 90 | return ajax({ 91 | url: '/api/v1/data', 92 | method: 'post', 93 | data: { 94 | indicatorId, 95 | dateRange 96 | } 97 | }); 98 | } 99 | 100 | export function getReportData(id, dateBegin, dateEnd) { 101 | return ajax({ 102 | url: '/api/v1/report/data', 103 | method: 'post', 104 | data: { 105 | id, 106 | dateBegin, 107 | dateEnd 108 | } 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /src/util/api/mock.map.js: -------------------------------------------------------------------------------- 1 | const map = { 2 | '/v1/company': 'http://as-api-dev.gogoapp.cn/api/v1/company', 3 | '/v1/captcha': 'http://as-api-dev.gogoapp.cn/api/v1/company', 4 | '/v1/mp': 'http://as-api-dev.gogoapp.cn/api/v1/mp', 5 | '/v1/ad': 'http://as-api-dev.gogoapp.cn/api/v1/ad', 6 | '/v1/campaigns': 'http://as-api-dev.gogoapp.cn/api/v1/campaigns' 7 | }; 8 | 9 | export default map; 10 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | export function addThousandSeparator(num) { 2 | return Number(num).toLocaleString('en'); 3 | } 4 | 5 | export function doSomething() { 6 | return ''; 7 | } 8 | -------------------------------------------------------------------------------- /src/util/login.js: -------------------------------------------------------------------------------- 1 | const USER_INFO = 'user_info'; 2 | 3 | export default { 4 | isLogin: () => localStorage.getItem(USER_INFO), 5 | 6 | saveUserInfo: (user) => { 7 | localStorage.setItem(USER_INFO, JSON.stringify(user)); 8 | }, 9 | 10 | getUserInfo: () => { 11 | try { 12 | // console.log('log getUserInfo:', 13 | // localStorage.USER_INFO, 14 | // JSON.parse(localStorage.getItem(USER_INFO))); 15 | return JSON.parse(localStorage.getItem(USER_INFO)); 16 | } catch (error) { 17 | console.log(error); 18 | } 19 | 20 | return null; 21 | }, 22 | 23 | logout: () => { 24 | localStorage.removeItem(USER_INFO); 25 | } 26 | }; 27 | --------------------------------------------------------------------------------