├── .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 |
7 |
8 |
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 |
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 |
129 | );
130 |
131 | return (
132 |
133 |
142 | { loading ? loadingNode : this.props.children }
143 |
144 | { desc &&
{desc}
}
145 | { (imageDataUrl || (value && value.thumbnailUrl)) &&
}
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 |
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 |
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 |
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 |
53 | );
54 |
55 | return (
56 |
57 |
63 |
64 |

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 |
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 |
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 |
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 | {/*

*/}
80 |

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