🔖 react app

pull/2/head
Zhang Peng 2018-07-11 16:07:16 +08:00
parent 52094ec137
commit 2aa5760a7a
71 changed files with 3044 additions and 0 deletions

View File

@ -0,0 +1,35 @@
{
"presets": [
[
"env",
{
"modules": false
}
],
"react",
"stage-0"
],
"plugins": [
"react-hot-loader/babel",
"syntax-dynamic-import",
"transform-runtime",
[
"import",
[
{
"libraryName": "antd",
"style": "css"
}
]
]
],
"env": {
"test": {
"presets": [
"env",
"react",
"stage-0"
]
}
}
}

View File

@ -0,0 +1,30 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# http://editorconfig.org
# 所有文件换行以 Unix like 风格LFwin 格式特定的除外bat
# 缩进 java 4 个空格,其他所有文件 2 个空格
root = true
[*]
# Unix-style newlines with a newline ending every file
end_of_line = lf
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.bat]
end_of_line = crlf
[*.java]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,94 @@
{
"extends": [
"eslint-config-airbnb"
],
"parser": "babel-eslint",
"globals": {
"Action": false,
"__DEV__": false,
"__DEV__LOG__": false,
"__DEV__LOG__DIFF__": true,
"__DEV__IMMUTABLE_CHECK__": false,
"__PROD__": false,
"__DEBUG__": false,
"__DEBUG_NEW_WINDOW__": false,
"__BASENAME__": false,
"Image": {},
"FileReader": {},
"Request": {},
"fetch": {},
"XMLHttpRequest": {}
},
"rules": {
"arrow-body-style": [
"off"
],
"global-require": [
"warn"
],
"no-underscore-dangle": [
"off"
],
"no-case-declarations": [
"warn"
],
"max-len": [
"warn",
120,
{
"ignoreUrls": true
}
],
"no-unused-vars": [
"warn"
],
"no-nested-ternary": [
"warn"
],
"no-class-assign": [
"off"
],
"no-use-before-define": [
"error",
{
"functions": false,
"classes": true
}
],
"new-cap": [
"error",
{
"capIsNewExceptions": [
"List",
"Map",
"OrderedMap",
"Set",
"OrderedSet",
"Stack",
"Range",
"Repeat",
"Record",
"Seq"
]
}
],
"linebreak-style": [
"off"
],
"import/prefer-default-export": [
"off"
],
"react/prefer-stateless-function": [
"off"
],
"react/jsx-curly-spacing": [
"off"
],
"react/forbid-prop-types": [
"off"
],
"react/no-unused-prop-types": [
"warn"
]
}
}

68
demos/reactapp/.gitattributes vendored 100644
View File

@ -0,0 +1,68 @@
* text=auto eol=lf
# plan text
*.txt text
*.java text
*.scala text
*.groovy text
*.gradle text
*.xml text
*.xsd text
*.tld text
*.yaml text
*.yml text
*.wsdd text
*.wsdl text
*.jsp text
*.jspf text
*.js text
*.jsx text
*.json text
*.css text
*.less text
*.sql text
*.properties text
# unix style
*.sh text eol=lf
# win style
*.bat text eol=crlf
# don't handle
*.der -text
*.jks -text
*.pfx -text
*.map -text
*.patch -text
*.dat -text
*.data -text
*.db -text
# binary
*.jar binary
*.war binary
*.zip binary
*.tar binary
*.tar.gz binary
*.gz binary
*.apk binary
*.bin binary
*.exe binary
# 图片
*.png binary
*.jpg binary
*.ico binary
*.gif binary
# 音视频
*.mp3 binary
*.swf binary
# other doc
*.pdf binary
*.doc binary
*.docx binary
*.xls binary
*.xlsx binary

31
demos/reactapp/.gitignore vendored 100644
View File

@ -0,0 +1,31 @@
# project
node_modules
# build
dist/
coverage
# IDE files
.idea/
*.iml
.ipr
.iws
# temp and sys
*~
~*
*.diff
*.patch
*.bak
.DS_Store
Thumbs.db
*.log
# other
.project
.*proj
.svn/
*.swp
*.swo
*.pyc
*.pyo

View File

@ -0,0 +1,25 @@
/**
* @file app 的全局配置
* @author Zhang Peng
*/
module.exports = {
/**
* 打印日志开关
*/
log: true,
http: {
/**
* 请求超时时间
*/
timeout: 5000,
/**
* 服务器的host
*/
baseURL: 'http://localhost:8080/api',
}
};

View File

@ -0,0 +1,113 @@
/**
* Created by Zhang Peng on 2017/6/14.
*/
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 这里应用程序开始执行
// webpack 开始打包
// 本例中 entry 为多入口
entry: {
// 第三方库
vendor: ['babel-polyfill']
},
// webpack 如何输出结果的相关选项
output: {
// 所有输出文件的目标路径
// 必须是绝对路径(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "../dist"),
// 「入口分块(entry chunk)」的文件名模板(出口分块?)
// filename: "bundle.min.js",
// filename: "[name].js", // 用于多个入口点(entry point)(出口点?)
// filename: "[chunkhash].js", // 用于长效缓存
filename: "[name].[hash:8].js",
// 「source map 位置」的文件名模板
sourceMapFilename: "[name].map",
},
// 关于模块配置
module: {
// 模块规则(配置 loader、解析器等选项
rules: [
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test 和 include 具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于 test 和 include
// 最佳实践:
// - 只在 test 和 文件名匹配 中使用正则表达式
// - 在 include 和 exclude 中使用绝对路径数组
// - 尽量避免 exclude更倾向于使用 include
{
// 语义解释器,将 js/jsx 文件中的 es2015/react 语法自动转为浏览器可识别的 Javascript 语法
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
exclude: /node_modules/,
// 应该应用的 loader它相对上下文解析
// 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
// 查看 webpack 1 升级指南。
loader: "babel-loader",
},
{
// css 加载
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
// less 加载
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
// 字体加载
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:8].[ext]'
}
}
]
},
// 解析模块请求的选项
// (不适用于对 loader 解析)
resolve: {
// 使用的扩展名
extensions: ['.js', '.jsx', '.json'],
alias: {
"@": path.resolve(__dirname, "../src")
}
},
// 附加插件列表
plugins: [
/**
* https://doc.webpack-china.org/plugins/html-webpack-plugin/
* 用于简化 HTML 文件index.html的创建提供访问 bundle 的服务
*/
new HtmlWebpackPlugin({
title: "reactapp",
template: "public/index.html",
favicon: "public/favicon.ico",
}),
// 将多个入口起点之间共享的公共模块,生成为一些 chunk并且分离到单独的 bundle 中
new webpack.optimize.CommonsChunkPlugin({
name: "vendor" // 指定公共 bundle 的名字
}),
],
};

View File

@ -0,0 +1,93 @@
/**
* Created by Zhang Peng on 2017/6/14.
*/
const path = require('path');
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const OpenBrowserPlugin = require('open-browser-webpack-plugin');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = webpackMerge(baseWebpackConfig, {
// 这里应用程序开始执行
// webpack 开始打包
// 本例中 entry 为多入口
entry: {
main: [
// App 入口
path.resolve(__dirname, "../src/index"),
// 开启 React 代码的模块热替换(HMR)
'react-hot-loader/patch',
// 为 webpack-dev-server 的环境打包代码
// 然后连接到指定服务器域名与端口
'webpack-dev-server/client?http://localhost:9000',
// 为热替换(HMR)打包好代码
// only- 意味着只有成功更新运行代码才会执行热替换(HMR)
'webpack/hot/only-dev-server',
],
},
output: {
// 对于热替换(HMR)是必须的,让 webpack 知道在哪里载入热更新的模块(chunk)
publicPath: "/",
},
// 关于模块配置
module: {
// 模块规则(配置 loader、解析器等选项
rules: [
{
// 图片加载 + 图片压缩
test: /\.(png|svg|jpg|gif|ico)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/images/[name].[hash:8].[ext]'
}
},
]
},
// 附加插件列表
plugins: [
// 定义环境变量
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
}),
// 开启全局的模块热替换(HMR)
new webpack.HotModuleReplacementPlugin(),
// 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息
new webpack.NamedModulesPlugin(),
// 自动打开浏览器
new OpenBrowserPlugin({
url: "http://localhost:9000"
}),
],
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// devtool: "source-map", // 牺牲了构建速度的 `source-map' 是最详细的
// devtool: "inline-source-map", // 嵌入到源文件中
// devtool: "eval-source-map", // 将 SourceMap 嵌入到每个模块中
// devtool: "hidden-source-map", // SourceMap 不在源文件中引用
// devtool: "cheap-source-map", // 没有模块映射(module mappings)的 SourceMap 低级变体(cheap-variant)
devtool: "eval-source-map", // 有模块映射(module mappings)的 SourceMap 低级变体
// devtool: "eval", // 没有模块映射,而是命名模块。以牺牲细节达到最快。
devServer: {
contentBase: [path.join(__dirname, "../dist")],
compress: true,
port: 9000, // 启动端口号
hot: true, // 启用 webpack 的模块热替换特性
inline: true,
publicPath: "/", // 和上文 output 的“publicPath”值保持一致
historyApiFallback: true
}
});

View File

@ -0,0 +1,81 @@
/**
* Created by Zhang Peng on 2017/6/14.
*/
const path = require('path');
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = webpackMerge(baseWebpackConfig, {
// 这里应用程序开始执行
// webpack 开始打包
// 本例中 entry 为多入口
entry: {
main: [
// App 入口
path.resolve(__dirname, "../src/index"),
],
},
// 关于模块配置
module: {
// 模块规则(配置 loader、解析器等选项
rules: [
{
// 图片加载 + 图片压缩
test: /\.(png|svg|jpg|gif|ico)$/,
loaders: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/images/[name].[hash:8].[ext]'
}
},
{
loader: "image-webpack-loader",
query: {
progressive: true,
pngquant: {
quality: "65-90",
speed: 4
}
}
}
]
},
]
},
// 附加插件列表
plugins: [
// 定义环境变量
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
// 加载选项插件
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
// 压缩 js 插件
new webpack.optimize.UglifyJsPlugin({
output: {
comments: false, // remove all comments
},
compress: {
warnings: false
}
}),
// 将样式文件独立打包
new ExtractTextPlugin("styles.css"),
],
});

View File

@ -0,0 +1,77 @@
{
"name": "reactapp",
"version": "0.0.1",
"description": "reactapp",
"main": "index.js",
"scripts": {
"clean": "rimraf ./dist",
"lint": "eslint --ext .js,.jsx src",
"dev": "webpack-dev-server --config config/webpack.config.dev.js --color",
"prod": "npm run clean && webpack --config config/webpack.config.prod.js --color",
"start": "if-env NODE_ENV=production && npm run prod || npm run dev"
},
"repository": {
"type": "git",
"url": "https://github.com/dunwu/react-admin.git"
},
"author": "Zhang Peng",
"license": "MIT",
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"dependencies": {
"antd": "^2.12.1",
"axios": "^0.15.3",
"isomorphic-fetch": "^2.2.1",
"less": "^2.7.1",
"lodash": "^4.16.4",
"moment": "^2.18.1",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-redux": "^5.0.0",
"react-router": "^4.1.0",
"react-router-dom": "^4.1.1",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"reqwest": "^2.0.5"
},
"devDependencies": {
"axios-mock-adapter": "^1.7.1",
"babel-core": "^6.25.0",
"babel-loader": "^7.1.0",
"babel-plugin-import": "^1.2.1",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.5.2",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"css-loader": "^0.25.0",
"eslint": "3.7.1",
"eslint-config-airbnb": "12.0.0",
"eslint-plugin-import": "1.16.0",
"eslint-plugin-jsx-a11y": "2.2.3",
"eslint-plugin-react": "6.3.0",
"extract-text-webpack-plugin": "^2.1.2",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.24.1",
"if-env": "^1.0.0",
"image-webpack-loader": "^3.3.1",
"less-loader": "^2.2.3",
"open-browser-webpack-plugin": "^0.0.5",
"react-hot-loader": "^3.0.0-beta.7",
"redux-devtools": "^3.4.0",
"redux-devtools-dock-monitor": "^1.0.1",
"redux-devtools-log-monitor": "^1.0.2",
"redux-logger": "^3.0.6",
"redux-mock-store": "^1.2.3",
"rimraf": "^2.6.1",
"style-loader": "^0.18.2",
"url-loader": "^0.5.7",
"webpack": "^3.5.5",
"webpack-dev-server": "^2.7.1",
"webpack-merge": "^4.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head>
<body>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm run dev` in this folder.
To create a production bundle, use `npm run prod`.
-->
</body>
</html>

View File

@ -0,0 +1,9 @@
#!/bin/bash
ln -s /app/ck-puck-front/node_modules/ node_modules
nvm use 8.1.0
npm set registry http://192.168.51.44
npm install
npm run build
rm -rf /app/ck-puck-front/dist
mkdir -p /app/ck-puck-front/dist
cp -Rf ./dist /app/ck-puck-front/

View File

@ -0,0 +1,166 @@
import React from 'react';
import { Modal } from 'antd';
import _ from 'lodash';
import { COMMON_REQUEST_ERROR } from '../../redux/constants/commonActionTypes';
export const REQ_BASE_URL = '/api';
export const METHODS = {
GET: 'GET',
HEAD: 'HEAD',
POST: 'POST',
PUT: 'PUT',
DEL: 'DEL',
OPTIONS: 'OPTIONS',
PATCH: 'PATCH',
};
export const REQ_TYPE = {
HTML: 'html',
JSON: 'json',
JSONP: 'jsonp',
};
export const CACHE_TYPE = {
DEFAULT: 'default',
NO_STORE: 'no-store',
RELOAD: 'reload',
NO_CACHE: 'no-cache',
FORCE_CACHE: 'force-cache',
};
export const ERROR_HANDLER_TYPE = {
NO: 'NO', // ['NO' | undefined | false ] 不处理
SYSTEM: 'SYSTEM', // ['SYSTEM'] 只处理系统预料外的返回not json
SYSTEM_AND_AUTH: 'SYSTEM_AND_AUTH', // [true, 'SYSTEM_AND_AUTH'] 处理上一步,与 认证失败 比较常用所以单独列出用true
ALL: 'ALL', // [no errorHandler | 'ALL'] 所有
};
export const defaultOptions = {
url: null,
method: METHODS.GET,
headers: {},
data: null,
type: null,
contentType: null,
crossOrigin: null,
onSuccess: () => {
},
onError: () => {
},
cache: CACHE_TYPE.NO_CACHE,
};
// 在 defaultOptions 基础上多出来的, request plan textresponse json
export const defaultJsonOptions = _.merge({}, defaultOptions, {
headers: {
Accept: 'application/json, text/plain, */*',
'Cache-Control': 'no-cache',
},
type: REQ_TYPE.JSON,
});
// 在 defaultJsonOptions 基础上多出来的, request response 皆是 json
export const defaultBiJsonOptions = _.merge({}, defaultJsonOptions, {
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
reqType: REQ_TYPE.JSON,
});
// 获取真正请求的 URL
export function getRealUrl(url) {
if (!!url && !url.startsWith('http')) {
return REQ_BASE_URL + url;
}
return url;
}
/**
* 展示认证错误
* @private
*/
function _showAuthError() {
Modal.error({
title: '认证失败',
// eslint-disable-next-line react/jsx-filename-extension
content: (<p>您现在处于非认证状态<br />
如果想保留本页状态请在 <a href="/login" target="blank">新页面登陆</a> 。<br />
{ /* 否则在 <Link to="/login" >当前页登陆</Link> 。 */ }
</p>),
});
}
/**
* 防抖展示认证错误一段时间内仅一次
* @type {Function}
*/
const showAuthError = _.debounce(_showAuthError, 500, {
leading: true,
trailing: false,
});
/**
* 展示服务端错误信息
* @param e
*/
function _showServerError(e) {
Modal.error({
title: '服务端错误!',
content: `服务端错误。服务端可能未正确部署或由于其他原因响应失败!请保留现场并联系开发人员。错误信息: ${e}`,
});
}
/**
* 防抖展示服务端错误一段时间内仅一次
* @type {Function}
*/
const showServerError = _.debounce(_showServerError, 500, {
leading: true,
trailing: false,
});
/**
* 包装错误处理所有服务端应用非业务 ret 预计错误与认证错误统一处理
* 其他根据情况如果未传入错误处理函数或错误处理函数返回 true则接管处理
* 用于处理 api 请求未传入错误处理函数的情况<br />
* 如果传入 dispatch则会 dispatch 公共 action<br />
* 如果无 dispatch console.log error
* @param errorHandler
* @param dispatch
* @returns {function()}
*/
export function wrapErrorHandler(errorHandler, dispatch) {
return (e) => {
let handlerLevel = 1000; // 默认都处理
// 先看是否传入 errorHandler如果传入则执行用户 errorHandler并根据处理结果设置新的 handlerLevel
if (_.isFunction(errorHandler)) {
handlerLevel = _getErrorHandlerLevel(errorHandler(e));
}
if (handlerLevel > 0 && e instanceof XMLHttpRequest) {
// 服务端应用(非业务)非 ret 预计错误处理,如 404400500非返回 json 错误
showServerError(e.responseText);
} else if (handlerLevel > 10 && e.ret === -1) {
// 认证失败,该登陆未登录
showAuthError();
} else if (handlerLevel > 100 && dispatch) {
dispatch({ type: COMMON_REQUEST_ERROR, payload: e });
} else if (handlerLevel > 100) {
const msg = e.ret ? `[code]${e.ret}, [msg]${e.msg}` : JSON.stringify(e);
// eslint-disable-next-line no-console
console.error(`请求出错: ${msg}`);
}
};
}
function _getErrorHandlerLevel(type) {
if (type === ERROR_HANDLER_TYPE.SYSTEM) {
return 10;
} else if (type === ERROR_HANDLER_TYPE.SYSTEM_AND_AUTH || type === true) {
return 100;
} else if (type === ERROR_HANDLER_TYPE.ALL) {
return 1000;
}
return 0;
}

View File

@ -0,0 +1,37 @@
const createApi = fetchFunc => options => (...args) => {
let finalOpts;
const argsName = ['options', 'successCallBack', 'errorCallBack', 'dispatch'];
// options 可以是 url或完整的 options 对象
if (typeof options === 'string') {
finalOpts = { url: options };
} else {
finalOpts = { ...options };
}
const temArgs = {};
if (args) {
// args 第一个参数options 可以忽略
let i = 0;
if (args[0] !== null && typeof args[0] === 'object') {
i = 1;
finalOpts = Object.assign(finalOpts, args[0]);
}
// eslint-disable-next-line no-plusplus
for (let j = i; j < args.length; j++) {
// eslint-disable-next-line no-mixed-operators
temArgs[argsName[j - i + 1]] = args[j];
}
}
if (temArgs.successCallBack) {
finalOpts.onSuccess = temArgs.successCallBack;
}
if (temArgs.errorCallBack) {
finalOpts.onError = temArgs.errorCallBack;
}
fetchFunc(finalOpts, temArgs.dispatch);
};
export default createApi;

View File

@ -0,0 +1,26 @@
/**
* 错误处理帮助类
*/
import { Message } from 'antd';
import _ from 'lodash';
import { ERROR_HANDLER_TYPE } from './index';
/**
* 处理业务类型的错误(-3)通过 message 的方式展现
* @param e
* @param callBack
* @return {boolean}
*/
export function messageBizError(e, callBack) {
let continueHandler = true;
if (e && e.ret === -3) {
// 业务错误
Message.error(e.msg, 4.5);
continueHandler = ERROR_HANDLER_TYPE.NO;
}
if (_.isFunction(callBack)) {
callBack({ error: e });
}
return continueHandler;
}

View File

@ -0,0 +1,52 @@
import 'isomorphic-fetch';
import { REQ_TYPE, defaultOptions, defaultJsonOptions, getRealUrl, wrapErrorHandler } from './ajaxCommon';
function handleStatus(res) {
if (res.ok) {
return res;
}
throw new Error({ result: res.status });
}
// json 有固定的格式,所以固定处理方法
function handleJson(data) {
// noinspection JSUnresolvedVariable
if (data.ret === 0) {
return data.data;
}
throw new Error(data);
}
export function doFetch(options = {}, dispatch) {
const opts = {
...defaultOptions,
...options,
onError: wrapErrorHandler(options.onError, dispatch),
};
// 根据配置创建 Request 对象
const req = new Request(getRealUrl(opts.url), {
method: opts.method,
headers: opts.headers,
body: opts.data,
cache: opts.cache,
redirect: 'follow',
mode: 'cors',
});
if (!__DEV__) {
req.credentials = 'include';
}
// 请求
// FIXME 应该根据 response 类型自动判断是否 Json 请求
let tempRes = fetch(req).then(handleStatus);
if (options.type === REQ_TYPE.JSON) {
tempRes = tempRes.then(res => res.json()).then(handleJson);
}
tempRes.then(options.onSuccess).catch(options.onError);
}
export function doFetchJson(options = {}, dispatch) {
const opts = { ...defaultJsonOptions, ...options };
doFetch(opts, dispatch);
}

View File

@ -0,0 +1,48 @@
/**
* export 一个 API底层的实现可能会改切换为 reqwest/superagent/fetch
*/
import { METHODS, REQ_TYPE, CACHE_TYPE, ERROR_HANDLER_TYPE } from './ajaxCommon';
import { doFetch, doFetchJson, doBiJsonFetch } from './reqwestAJAX';
import createApi from './apiCreator';
/**
* 创建一个 API 函数结果可以是任何形式如果是响应是 JSON 会自动转换类似于 #createFetchJson 结果<br />
* 但是请求头不指名 Json Accept:text/javascript, text/html, application/xml, text/xml, *\/*
*/
const createFetch = createApi(doFetch);
/**
* 创建一个 API 函数明确指明函数用于获取 Json 格式数据如果结果不符合格式会转到错误处理 <br />
* 请求头Accept:application/json, text/plain, *\/*
*/
const createFetchJson = createApi(doFetchJson);
// 创建一个 API 函数, 指明客户端、服务端 内容体content body都是 Json 格式。<br />
// 在 #createFetchJson 的基础上添加Content-Type: application/json;charset=UTF-8<br />
// 同时,如果请求 data 为 Object类型会通过 JSON.stringify 转换
const createBiJsonFetch = createApi(doBiJsonFetch);
/**
* api 转换为返回 Promise 方式, 不处理 error 如果处理 error 请君自new
* @private
*/
const createPromiseAPI = api => (data) => {
return new Promise((resolve) => {
api({ data }, rs => resolve(rs));
});
};
const API = {
METHODS,
REQ_TYPE,
CACHE_TYPE,
ERROR_HANDLER_TYPE,
doFetch,
doFetchJson,
createFetch,
createFetchJson,
createBiJsonFetch,
createPromiseAPI,
};
export default API;

View File

@ -0,0 +1,58 @@
import reqwest from 'reqwest';
import _ from 'lodash';
import {
REQ_TYPE,
METHODS,
defaultOptions,
defaultJsonOptions,
defaultBiJsonOptions,
getRealUrl,
wrapErrorHandler,
} from './ajaxCommon';
function _doFetch(options = {}, dispatch, defaultMergeOption = {}) {
const opts = _.merge({}, defaultMergeOption, options, {
url: getRealUrl(options.url),
error: wrapErrorHandler(options.onError, dispatch),
});
const method = opts.method && opts.method.toUpperCase();
const data = opts.data;
if (METHODS.GET === method && opts.processData !== false && !_.isString(data)) {
// get 请求,配置 processData 不为否data 不为 String 则预处理
const newData = { ...data, ts: new Date().getTime() }; // 加入时间戳,防止浏览器缓存
opts.data = reqwest.toQueryString(newData, true); // traditional 方式,保证数组符合 spring mvc 的传参方式。
}
opts.success = (res) => {
const doSuc = options.onSuccess ? options.onSuccess : defaultOptions.onSuccess; // reqwest 名字不同
if (opts.type === REQ_TYPE.JSON || typeof res === 'object') {
// noinspection JSUnresolvedVariable
if (res.result === 0 || res.ret === 0) {
doSuc(res.data);
} else {
opts.error(res);
}
} else {
doSuc(res);
}
};
reqwest(opts);
}
export function doFetch(options = {}, dispatch) {
_doFetch(options, dispatch, defaultOptions);
}
export function doFetchJson(options = {}, dispatch) {
_doFetch(options, dispatch, defaultJsonOptions);
}
export function doBiJsonFetch(options = {}, dispatch) {
let opts = options;
if (typeof opts.data === 'object') {
opts = { ...options, data: JSON.stringify(opts.data) };
}
_doFetch(opts, dispatch, defaultBiJsonOptions);
}

View File

@ -0,0 +1,11 @@
/**
* @file 本项目二次封装的 UI 组件入口
* @author Zhang Peng
*/
/****************************** 布局组件 ******************************/
export { default as Header } from './layout/Header/Header';
export { default as Sidebar } from './layout/Sidebar/Sidebar';
export { default as Content } from './layout/Content/Content';
export { default as Footer } from './layout/Footer/Footer';
export { default as Breadcrumb } from './layout/Breadcrumb/Breadcrumb';

View File

@ -0,0 +1,50 @@
/**
* @file 面包屑组件
* @author Zhang Peng
* @see https://github.com/facebook/prop-types
* @see https://ant.design/components/breadcrumb-cn/
* @see https://ant.design/components/icon-cn/
*/
import { Breadcrumb, Icon } from 'antd';
import PropTypes from 'prop-types';
import React from 'react';
import './Breadcrumb.less';
/**
* 面包屑组件
* @class
*/
class CustomBreadcrumb extends React.PureComponent {
static propTypes = {
data: PropTypes.array
};
static defaultProps = {
data: []
};
render() {
const { data } = this.props;
const breadcrumbItems = data.map((item) => {
return (
<Breadcrumb.Item key={'bc-' + item.key}>
<Icon type={item.icon} />
<span>{item.title}</span>
</Breadcrumb.Item>
)
});
return (
<div className="ant-layout-breadcrumb">
<Breadcrumb>
<Breadcrumb.Item key='bc-0'>
<Icon type="home" />
<span>Home</span>
</Breadcrumb.Item>
{breadcrumbItems}
</Breadcrumb>
</div>
)
}
}
export default CustomBreadcrumb;

View File

@ -0,0 +1,7 @@
.ant-layout-breadcrumb {
z-index: 200;
line-height: 64px;
.ant-breadcrumb-link {
font-size: 16px;
}
}

View File

@ -0,0 +1,33 @@
/**
* @file 内容布局组件
* @author Zhang Peng
* @see https://ant.design/components/layout-cn/
* @see https://ant.design/components/card-cn/
*/
import { Card, Layout } from 'antd';
import React from 'react';
import './Content.less';
const { Content } = Layout;
/**
* 内容布局组件
* @class
*/
class CustomContent extends React.PureComponent {
constructor(props) {
super(props)
}
render() {
return (
<Content className="ant-layout-content">
<Card noHovering bordered={false} bodyStyle={{ padding: 0 }}>
{this.props.children}
</Card>
</Content>
)
}
}
export default CustomContent;

View File

@ -0,0 +1,4 @@
.ant-layout-content {
background: #fff;
padding: 24px;
}

View File

@ -0,0 +1,26 @@
/**
* @file 底部布局组件
* @author Zhang Peng
* @see https://ant.design/components/layout-cn/
*/
import { Layout } from 'antd';
import React from 'react';
import './index.less';
const { Footer } = Layout;
/**
* 底部布局组件
* @class
*/
class CustomFooter extends React.PureComponent {
render() {
return (
<Footer className="ant-layout-footer">
Ant Admin © 2017-2018 https://github.com/dunwu
</Footer>
)
}
}
export default CustomFooter;

View File

@ -0,0 +1,10 @@
.ant-layout-footer {
height: 64px;
line-height: 32px;
text-align: center;
font-size: 14px;
color: #999;
background: #fff;
border-top: 1px solid #e9e9e9;
width: 100%;
}

View File

@ -0,0 +1,128 @@
/**
* @file 顶部布局组件
* @author Zhang Peng
* @see https://ant.design/components/layout-cn/
*/
import { Avatar, Badge, Col, Dropdown, Icon, Layout, Menu, Popover, Row } from 'antd';
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import './Header.less';
import Breadcrumb from '../Breadcrumb/Breadcrumb';
import { fetchProfile, logout } from '../../../redux/actions/auth';
const { Header } = Layout;
const content = (
<div>
<p>Content</p>
<p>Content</p>
<p>Content</p>
<p>Content</p>
<p>Content</p>
</div>
);
const mapStateToProps = (state) => {
const { auth, menu } = state;
return {
auth: auth ? auth : null,
navpath: menu.navpath
};
};
const mapDispatchToProps = (dispatch) => {
return { actions: bindActionCreators({ fetchProfile, logout }, dispatch) };
};
/**
* 顶部布局组件
* @class
*/
class CustomHeader extends React.PureComponent {
static propTypes = {
auth: PropTypes.object,
actions: PropTypes.object,
navpath: PropTypes.array
};
static defaultProps = {
auth: null,
actions: null,
navpath: []
};
componentWillMount() {
const { actions } = this.props;
actions.fetchProfile();
}
handleLogOut = () => {
const { actions } = this.props;
actions.logout().payload.promise.then(() => {
this.props.history.replace('/login');
});
};
render() {
const { auth, navpath } = this.props;
let username = '';
if (auth.user) {
if(auth.user.data) {
username = auth.user.data.name
}
}
const menu = (
<Menu>
<Menu.Item key="1">
选项1
</Menu.Item>
<Menu.Item key="2">
选项2
</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout">
<a onClick={this.handleLogOut}>注销</a>
</Menu.Item>
</Menu>
);
return (
<Header className="ant-layout-header">
<Row type="flex" align="middle">
<Col xs={12} sm={12} md={12} lg={12} xl={12}>
<Breadcrumb data={navpath} />
</Col>
<Col xs={0} sm={2} md={4} lg={8} xl={8} />
<Col xs={0} sm={6} md={6} lg={4} xl={4}>
<Badge className="header-icon" count={5}>
<Link to="/pages/mailbox">
<Icon type="mail" />
</Link>
</Badge>
<Popover content={content} title="Title" trigger="click">
<Badge className="header-icon" dot>
<a href="#">
<Icon type="notification" />
</a>
</Badge>
</Popover>
<Dropdown overlay={menu}>
<a className="ant-dropdown-link" href="#">
<Avatar shape="circle" style={{ verticalAlign: 'middle', backgroundColor: '#00a2ae' }} size="large">
{username}
</Avatar>
<Icon type="down" />
</a>
</Dropdown>
</Col>
</Row>
</Header>
)
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CustomHeader));

View File

@ -0,0 +1,16 @@
.ant-layout-header {
top: 0;
right: 0;
//height: 64px;
//padding: 25px;
z-index: 150;
background: #fff;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, .3), 0 0 6px 2px rgba(0, 0, 0, .15);
}
.header-icon {
margin: 0 15px;
i {
font-size: 20px;
}
}

View File

@ -0,0 +1,165 @@
/**
* @file 侧边导航栏组件
* @author Zhang Peng
* @see https://ant.design/components/layout-cn/
*/
import {Icon, Layout, Menu} from "antd";
import PropTypes from "prop-types";
import React from "react";
import {connect} from "react-redux";
import {matchPath, withRouter} from "react-router";
import {Link} from "react-router-dom";
import {bindActionCreators} from "redux";
import "./Sidebar.less";
import logoImg from "./antd.svg";
import {refreshMenu, refreshNavPath} from "../../../redux/actions/menu";
const {Sider} = Layout;
const isActive = (path, history) => {
return matchPath(path, {
path: history.location.pathname,
exact: true,
strict: false
})
};
/**
* 侧边导航栏组件侧边栏采用的响应式布局方式页面大小收缩到一定程度侧边栏会隐藏
* @class
*/
class CustomSidebar extends React.PureComponent {
static propTypes = {
items: PropTypes.array
};
static defaultProps = {
items: []
};
state = {
openKey: "sub0",
activeKey: "menu0",
mode: 'inline',
};
componentDidMount() {
this.props.getAllMenu()
}
componentWillReceiveProps(nextProps) {
Array.isArray(nextProps.items) && nextProps.items.map((item, i) => {
Array.isArray(item.children) && item.children.map((node) => {
if (node.url && isActive(node.url, this.props.history)) {
this.menuClickHandle({
key: 'menu' + node.key,
keyPath: ['menu' + node.key, 'sub' + item.key]
})
}
})
});
}
menuClickHandle = (item) => {
this.setState({
activeKey: item.key
});
this.props.updateNavPath(item.keyPath, item.key)
};
render() {
const {items, history} = this.props;
let {activeKey, openKey} = this.state;
const _menuProcess = (nodes, pkey) => {
return Array.isArray(nodes) && nodes.map((item, i) => {
const menu = _menuProcess(item.children, item.key);
if (item.url && isActive(item.url, history)) {
activeKey = 'menu' + item.key;
openKey = 'sub' + pkey
}
switch (item.type) {
case 'SubMenu':
return (
<Menu.SubMenu
key={item.key}
title={<span><Icon type={item.icon}/><span className="nav-text">{item.title}</span></span>}
>
{menu}
</Menu.SubMenu>
);
case 'ItemGroup':
return (
<Menu.ItemGroup
key={item.key}
title={<span><Icon type={item.icon}/><span className="nav-text">{item.title}</span></span>}
>
{menu}
</Menu.ItemGroup>
);
case 'Divider':
return (
<Menu.Divider key={item.key} />
);
case 'Item':
default:
return (
<Menu.Item className="ant-menu-item" key={item.key}>
{
item.url ? <Link to={item.url}>{item.icon && <Icon type={item.icon}/>}{item.title}</Link> :
<span>{item.icon && <Icon type={item.icon}/>}{item.title}</span>
}
</Menu.Item>
);
break;
}
});
};
const menu = _menuProcess(items);
return (
/**
* 响应式布局
* 说明配置 breakpoint 属性即生效视窗宽度小于 breakpoint Sider 缩小为 collapsedWidth 宽度
* 若将 collapsedWidth 设置为零会出现特殊 trigger
*/
<Sider className="ant-layout-sider"
breakpoint="lg"
collapsedWidth="0"
>
<div className="ant-layout-logo">
<div className="logo-container">
<img src={logoImg}/>
<span>Ant Design</span>
</div>
</div>
<Menu className="ant-menu"
mode={this.state.mode} theme="dark"
selectedKeys={[activeKey]}
defaultOpenKeys={[openKey]}
onClick={this.menuClickHandle}
>
{menu}
</Menu>
</Sider>
)
}
}
function mapStateToProps(state) {
return {
items: state.menu.items
}
}
function mapDispatchToProps(dispatch) {
return {
getAllMenu: bindActionCreators(refreshMenu, dispatch),
updateNavPath: bindActionCreators(refreshNavPath, dispatch)
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CustomSidebar))

View File

@ -0,0 +1,32 @@
.ant-layout-sider {
height: 100%;
width: 300px;
}
.ant-layout-logo {
width: 200px;
height: 64px;
color: #108ee9;
display: table-cell;
vertical-align: middle;
.logo-container {
width: 150px;
margin: 0 auto;
img {
width: 40px;
height: 40px;
margin-right: 12px;
margin-top: 12px;
}
span {
float: right;
margin-top: 20px;
font-size: 16px;
font-family: Raleway, Hiragino Sans GB, sans-serif;
text-transform: uppercase;
}
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="160px" viewBox="0 0 102 102" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>a</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="121.870767%" y1="50.0000623%" x2="-8.13548721%" y2="50.0000623%" id="linearGradient-1">
<stop stop-color="#47B4E0" offset="0%"></stop>
<stop stop-color="#1588E0" offset="17.18%"></stop>
<stop stop-color="#6EB4E0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.9981983%" y1="99.9981453%" x2="49.9981983%" y2="0.00156952896%" id="linearGradient-2">
<stop stop-color="#F0776F" offset="3.22%"></stop>
<stop stop-color="#F0656F" offset="50.32%"></stop>
<stop stop-color="#F0606F" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.9999999%" y1="0.000675229319%" x2="49.9999999%" y2="99.9996626%" id="linearGradient-3">
<stop stop-color="#F0776F" offset="3.22%"></stop>
<stop stop-color="#F0656F" offset="50.32%"></stop>
<stop stop-color="#F0606F" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="a" sketch:type="MSLayerGroup">
<g id="Group" sketch:type="MSShapeGroup">
<path d="M54.172,2.025 L73.631,21.484 C75.458,23.311 75.458,26.272 73.631,28.098 L73.631,28.098 C71.804,29.925 68.843,29.925 67.017,28.098 L54.172,15.254 C52.345,13.427 49.384,13.427 47.558,15.254 L15.254,47.558 C13.427,49.385 13.427,52.346 15.254,54.172 L47.558,86.476 C49.385,88.303 52.346,88.303 54.172,86.476 L67.017,73.631 C68.844,71.804 71.805,71.804 73.631,73.631 L73.631,73.631 C75.458,75.458 75.458,78.419 73.631,80.245 L54.172,99.704 C52.345,101.531 49.384,101.531 47.558,99.704 L2.025,54.172 C0.198,52.345 0.198,49.384 2.025,47.558 L47.557,2.026 C49.384,0.199 52.346,0.199 54.172,2.025 L54.172,2.025 Z" id="Shape" fill="url(#linearGradient-1)"></path>
<path d="M80.246,34.713 L80.246,34.713 C82.073,32.886 85.034,32.886 86.86,34.713 L99.705,47.558 C101.532,49.385 101.532,52.346 99.705,54.172 L86.86,67.017 C85.033,68.844 82.072,68.844 80.246,67.017 L80.246,67.017 C78.419,65.19 78.419,62.229 80.246,60.403 L86.476,54.173 C88.303,52.346 88.303,49.385 86.476,47.559 L80.246,41.329 C78.419,39.501 78.419,36.54 80.246,34.713 L80.246,34.713 Z" id="Shape" fill="url(#linearGradient-2)"></path>
</g>
<path d="M50.865,65.678 C59.046,65.678 65.678,59.046 65.678,50.865 C65.678,42.684 59.046,36.052 50.865,36.052 C42.684,36.052 36.052,42.684 36.052,50.865 C36.052,59.046 42.684,65.678 50.865,65.678 L50.865,65.678 Z M50.865,58.564 C46.613,58.564 43.166,55.117 43.166,50.865 C43.166,46.613 46.613,43.166 50.865,43.166 C55.117,43.166 58.564,46.613 58.564,50.865 C58.564,55.117 55.117,58.564 50.865,58.564 L50.865,58.564 Z" id="Shape" fill="url(#linearGradient-3)" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,41 @@
/**
* @file 应用的核心容器组件
* @author Zhang Peng
* @see https://ant.design/components/layout-cn/
*/
import { Layout } from 'antd';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import './CoreContainer.less';
import { authHOC } from '../../utils';
import { ChildRoutes } from '../../routes';
import { Content, Footer, Header, Sidebar } from '../../components';
/**
* 应用的核心容器组件
* <p>控制整个页面的布局整体采用的是侧边布局</p>
* @class
*/
class CoreContainer extends React.PureComponent {
render() {
return (
<Layout className="ant-layout-has-sider">
<Sidebar />
<Layout>
<Header />
<Layout className="ant-layout-container">
<Content>
<Redirect to="/pages/home" />
{ChildRoutes.map((route, index) => (
<Route key={index} path={route.path} component={authHOC(route.component)} exactly={route.exactly} />
))}
</Content>
</Layout>
<Footer />
</Layout>
</Layout>
);
}
}
export default CoreContainer;

View File

@ -0,0 +1,14 @@
#root, div[data-reactroot] {
height: 100%;
}
.ant-layout-has-sider {
height: 100%;
.ant-layout-container {
padding-top: 30px;
padding-bottom: 30px;
padding-left: 50px;
padding-right: 50px;
}
}

View File

@ -0,0 +1,6 @@
{
"name": "CoreContainer",
"version": "0.0.0",
"private": true,
"main": "./CoreContainer.jsx"
}

View File

@ -0,0 +1,31 @@
/**
* @file Redux 开发工具
* @author Zhang Peng
* @see https://github.com/gaearon/redux-devtools
* @see https://github.com/gaearon/redux-devtools-dock-monitor
* @see https://github.com/gaearon/redux-devtools-log-monitor
*/
import React from 'react';
// Exported from redux-devtools
import { createDevTools } from 'redux-devtools';
// Monitors are separate packages, and you can make a custom one
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
/**
* Redux 开发工具组件
* @class
*/
// createDevTools takes a monitor and produces a DevTools component
const ReduxDevTools = createDevTools(
// Monitors are individually adjustable with props.
// Consult their repositories to learn about those props.
// Here, we put LogMonitor inside a DockMonitor.
// Note: DockMonitor is visible by default.
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
defaultIsVisible={true}>
<LogMonitor theme='tomorrow' />
</DockMonitor>
);
export default ReduxDevTools;

View File

@ -0,0 +1,32 @@
/**
* @file 开发环境的 Root 容器
* @author Zhang Peng
* @see http://gaearon.github.io/react-hot-loader/getstarted/
*/
import React from 'react';
import { Provider } from 'react-redux';
import { HashRouter as Router } from 'react-router-dom';
import Routes from '../../routes';
import ReduxDevTools from './ReduxDevTools';
import configureStore from '../../redux/store/configureStore';
const store = configureStore();
/**
* 开发环境的 Root 容器会包含 Redux 的开发工具
* @class
*/
class RootContainer extends React.PureComponent {
render() {
if (!this.routes) this.routes = Routes;
return (
<Provider store={store}>
<div>
<Router children={this.routes} />
<ReduxDevTools />
</div>
</Provider>
);
}
}
export default RootContainer;

View File

@ -0,0 +1,12 @@
/**
* @file Root 容器模块是 antd-admin 的根组件
* @author Zhang Peng
* @see http://gaearon.github.io/react-hot-loader/getstarted/
*/
if (process.env.NODE_ENV === "development") {
module.exports = require('./RootContainer.dev');
console.log('[development] Root.dev started.');
} else {
module.exports = require('./RootContainer.prod');
console.log('[production] Root.prod started.');
}

View File

@ -0,0 +1,28 @@
/**
* @file 生产环境的 Root 容器
* @author Zhang Peng
* @see http://gaearon.github.io/react-hot-loader/getstarted/
*/
import React from 'react';
import { Provider } from 'react-redux';
import { HashRouter as Router } from 'react-router-dom';
import Routes from '../../routes';
import configureStore from '../../redux/store/configureStore';
const store = configureStore();
/**
* 生产环境的 Root 容器
* @class
*/
class RootContainer extends React.PureComponent {
render() {
if (!this.routes) this.routes = Routes;
return (
<Provider store={store}>
<Router children={this.routes} />
</Provider>
);
}
}
export default RootContainer;

View File

@ -0,0 +1,31 @@
/**
* @file App 的总入口
* @author Zhang Peng
* @see http://gaearon.github.io/react-hot-loader/getstarted/
*/
import 'react-hot-loader/patch';
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import RootContainer from './containers/Root/RootContainer';
const render = Component => {
ReactDOM.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('root')
);
};
// App
render(RootContainer);
// App
if (module.hot) {
module.hot.accept('./containers/Root/RootContainer', () => {
const NextRootContainer = require('./containers/Root/RootContainer');
render(NextRootContainer)
});
}

View File

@ -0,0 +1,38 @@
const webapi = require('../../webapi');
import { FETCH_PROFILE, LOGIN, LOGOUT, UID_NOT_FOUND } from '../constants/authActionType';
export const fetchProfile = () => {
let uid = window.localStorage.getItem('uid');
if (uid === undefined) {
return { type: UID_NOT_FOUND };
}
return {
type: FETCH_PROFILE,
payload: {
promise: webapi.get('/my')
}
}
};
export const login = (username, password) => {
return {
type: LOGIN,
payload: {
promise: webapi.put('/login', {
username: username,
password: password
})
}
}
};
export const logout = () => {
return {
type: LOGOUT,
payload: {
promise: webapi.get('/logout')
}
}
};

View File

@ -0,0 +1,21 @@
const webapi = require('../../webapi');
import { REFRESH_MENU, REFRESH_NAVPATH } from '../constants/menuActionType';
export const refreshNavPath = (path, key) => {
return {
type: REFRESH_NAVPATH,
payload: {
data: path,
key: key
}
}
};
export const refreshMenu = () => {
return {
type: REFRESH_MENU,
payload: {
promise: webapi.get('/menu')
}
}
};

View File

@ -0,0 +1,16 @@
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export const FETCH_PROFILE = 'FETCH_PROFILE';
export const UID_NOT_FOUND = 'UID_NOT_FOUND';
export const LOGIN_PENDING = 'LOGIN_PENDING';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILED = 'LOGIN_FAILED';
export const LOGOUT_PENDING = 'LOGOUT_PENDING';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILED = 'LOGOUT_FAILED';
export const FETCH_PROFILE_PENDING = 'FETCH_PROFILE_PENDING';
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
export const FETCH_PROFILE_FAILED = 'FETCH_PROFILE_FAILED';

View File

@ -0,0 +1,30 @@
/**
* 公共的请求失败 Action 用于 api 请求未传入错误处理函数<br />
* 如果传入 dispatch则会 dispatch action<br />
* 如果无 dispatch console.log error
* @type {string}
*/
export const COMMON_REQUEST_ERROR = 'COMMON_REQUEST_ERROR';
/**
* 公共的 SpinModal action用于控制统一的过度形式的模态框展示
* 各类型配合 payload 中的 type 等字段控制
* @type {string}
*/
export const COMMON_SPIN_MODAL = 'COMMON_SPIN_MODAL';
/**
* 公共的 SpinModal action用于控制统一的过度形式的模态框消失
* @type {string}
*/
export const COMMON_SPIN_MODAL_DIS = 'COMMON_SPIN_MODAL_DIS';
// 校验
export const COMMON_VALIDATE_FAIL = 'COMMON_VALIDATE_FAIL';
/**
* 公共的页面离开跳转确认功能开关
* @type {string}
*/
export const COMMON_LEAVE_CONFIRM_ON = 'COMMON_LEAVE_CONFIRM_ON';
export const COMMON_LEAVE_CONFIRM_OFF = 'COMMON_LEAVE_CONFIRM_OFF';

View File

@ -0,0 +1,4 @@
export const REFRESH_MENU = 'REFRESH_MENU';
export const REFRESH_NAVPATH = 'REFRESH_NAVPATH';
export const REFRESH_MENU_SUCCESS = 'REFRESH_MENU_SUCCESS';
export const REFRESH_MENU_FAILED = 'REFRESH_MENU_FAILED';

View File

@ -0,0 +1,75 @@
const defaultTypes = ['PENDING', 'FULFILLED', 'REJECTED'];
const isPromise = (value) => {
if (value !== null && typeof value === 'object') {
return value.promise && typeof value.promise.then === 'function';
}
};
function createPromiseMiddleware(config = {}) {
const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypes;
return (_ref) => {
const dispatch = _ref.dispatch;
return next => action => {
if (!isPromise(action.payload)) {
return next(action);
}
const { type, payload, meta } = action;
const { promise, data } = payload;
const [PENDING, FULFILLED, REJECTED] = (meta || {}).promiseTypeSuffixes || promiseTypeSuffixes;
/**
* Dispatch the first async handler. This tells the
* reducer that an async action has been dispatched.
*/
next({
type: `${type}_${PENDING}`,
...!!data ? { payload: data } : {},
...!!meta ? { meta } : {}
});
const isAction = resolved => resolved && (resolved.meta || resolved.payload);
const isThunk = resolved => typeof resolved === 'function';
const getResolveAction = isError => ({
type: `${type}_${isError ? REJECTED : FULFILLED}`,
...!!meta ? { meta } : {},
...!!isError ? { error: true } : {}
});
/**
* Re-dispatch one of:
* 1. a thunk, bound to a resolved/rejected object containing ?meta and type
* 2. the resolved/rejected object, if it looks like an action, merged into action
* 3. a resolve/rejected action with the resolve/rejected object as a payload
*/
action.payload.promise = promise.then(
(resolved = {}) => {
const resolveAction = getResolveAction();
return dispatch(isThunk(resolved) ? resolved.bind(null, resolveAction) : {
...resolveAction,
...isAction(resolved) ? resolved : {
...!!resolved && { payload: resolved }
}
});
},
(rejected = {}) => {
const resolveAction = getResolveAction(true);
return dispatch(isThunk(rejected) ? rejected.bind(null, resolveAction) : {
...resolveAction,
...isAction(rejected) ? rejected : {
...!!rejected && { payload: rejected }
}
});
},
);
return action;
};
};
}
const promise = createPromiseMiddleware({ promiseTypeSuffixes: ['PENDING', 'SUCCESS', 'FAILED'] });
export default promise;

View File

@ -0,0 +1,55 @@
import {
FETCH_PROFILE_SUCCESS,
LOGIN_FAILED,
LOGIN_PENDING,
LOGIN_SUCCESS,
LOGOUT_SUCCESS
} from '../constants/authActionType';
const initialState = {
user: null,
loggingIn: false,
loggingOut: false,
message: null
};
const auth = (state = initialState, action = {}) => {
switch (action.type) {
case LOGIN_PENDING:
return Object.assign({}, state, {
loggingIn: true
});
case LOGIN_SUCCESS:
let user = action.payload.data;
window.localStorage.setItem('uid', user.uid);
return Object.assign({}, state, {
user: user,
loggingIn: false,
message: null
});
case LOGIN_FAILED:
return {
...state,
loggingIn: false,
user: null,
message: action.payload.response.data.message
};
case LOGOUT_SUCCESS:
window.localStorage.removeItem('uid');
return {
...state,
loggingOut: false,
user: null,
message: null
};
case FETCH_PROFILE_SUCCESS:
return Object.assign({}, state, {
user: action.payload.data,
loggingIn: false,
message: null
});
default:
return state;
}
};
export default auth;

View File

@ -0,0 +1,12 @@
/**
* Created by Zhang Peng on 2017/7/6.
*/
import { combineReducers } from 'redux';
import auth from './auth';
import menu from './menu';
const rootReducer = combineReducers({
auth,
menu
});
export default rootReducer;

View File

@ -0,0 +1,59 @@
import _ from 'lodash';
import { REFRESH_MENU_SUCCESS, REFRESH_NAVPATH } from '../constants/menuActionType';
const initialState = {
items: [],
navpath: []
};
const menu = (state = initialState, action = {}) => {
switch (action.type) {
case REFRESH_MENU_SUCCESS:
return Object.assign({}, initialState, {
items: action.payload.data.data
});
case REFRESH_NAVPATH:
let navpath = [], tmpOb, tmpKey, children;
if (Array.isArray(action.payload.data)) {
action.payload.data.reverse().map((item) => {
if (item.indexOf('sub') !== -1) {
tmpKey = item.replace('sub', '');
tmpOb = _.find(state.items, function (o) {
return o.key == tmpKey;
});
children = tmpOb.children;
navpath.push({
key: tmpOb.key,
title: tmpOb.title,
icon: tmpOb.icon,
type: tmpOb.type,
url: tmpOb.url,
})
}
if (item.indexOf('menu') !== -1) {
tmpKey = item.replace('menu', '');
if (children) {
tmpOb = _.find(children, function (o) {
return o.key == tmpKey;
});
navpath.push({
key: tmpOb.key,
title: tmpOb.title,
icon: tmpOb.icon,
type: tmpOb.type,
url: tmpOb.url,
});
}
}
})
}
return Object.assign({}, state, {
currentIndex: action.payload.key * 1,
navpath: navpath
});
default:
return state;
}
};
export default menu;

View File

@ -0,0 +1,51 @@
/**
* @file 开发环境的 Store 构造器
* @author Zhang Peng
* @see https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md
*/
import { applyMiddleware, compose, createStore } from 'redux';
import { persistState } from 'redux-devtools';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import DevTools from '../../containers/Root/ReduxDevTools';
import promise from '../middlewares/promiseMiddleware';
import reducers from '../reducers';
const enhancer = compose(
// Middleware you want to use in development:
applyMiddleware(thunk, logger, promise),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey())
);
function getDebugSessionKey() {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/);
return (matches && matches.length > 0) ? matches[1] : null;
}
/**
* 开发环境的 Store 构造方法与生产环境的 Store 构造方法相比将创建的 Store 中多了开发工具
* @param {Object} initialState 初始状态
* @returns {Store<S>} Redux 的状态容器一个应用只有一个
*/
function configureStore(initialState) {
// Note: only Redux >= 3.1.0 supports passing enhancer as third argument.
// See https://github.com/reactjs/redux/releases/tag/v3.1.0
const store = createStore(reducers, initialState, enhancer);
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers'))
);
}
return store;
}
export default configureStore;

View File

@ -0,0 +1,12 @@
/**
* @file Redux 创建Store入口区分开发生产环境
* @author Zhang Peng
* @see https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md
*/
// Use DefinePlugin (Webpack) or loose-envify (Browserify)
// together with Uglify to strip the dev branch in prod build.
if (process.env.NODE_ENV === 'development') {
module.exports = require('./configureStore.dev').default;
} else {
module.exports = require('./configureStore.prod').default;
}

View File

@ -0,0 +1,23 @@
/**
* @file 生产环境的 Store 构造器
* @author Zhang Peng
* @see https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md
*/
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import promise from '../middlewares/promiseMiddleware';
import reducers from '../reducers';
const enhancer = applyMiddleware(thunk, promise);
/**
* 生产环境的 Store 构造方法
* @param {Object} initialState 初始状态
* @returns {Store<S>} Redux 的状态容器一个应用只有一个
*/
function configureStore(initialState) {
return createStore(reducers, initialState, enhancer);
}
export default configureStore;

View File

@ -0,0 +1,43 @@
/**
* @file React Router 入口
* <p>本项目使用 React Router 4.x</p>
* @author Zhang Peng
* @see https://reacttraining.com/react-router/
* @see https://reacttraining.cn/
*/
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import CoreContainer from '../containers/Core';
import Login from '../views/pages/login/Login';
/**
* 子路由表
*/
export const ChildRoutes = [
{
'path': '/pages/home',
'component': require('../views/pages/home/Home').default,
'exactly': true
},
{
'path': '/pages/mailbox',
'component': require('../views/pages/mail/Mailbox').default
},
{
'path': '/pages/user',
'component': require('../views/pages/user/User').default
}
];
/**
* 默认路由
* @type {XML}
*/
const Routes = (
<Switch>
<Route path="/login" component={Login} />
<Route path="/" component={CoreContainer} />
</Switch>
);
export default Routes;

View File

@ -0,0 +1,37 @@
/**
* @file 提供异步加载组件功能的高阶组件方法
* @author Zhang Peng
* @see https://segmentfault.com/a/1190000009820646
*/
import React from 'react';
let Component = null;
export const asyncLoadHOC = loadComponent => (
class AsyncComponent extends React.Component {
static hasLoadedComponent() {
return Component !== null;
}
componentWillMount() {
if (AsyncComponent.hasLoadedComponent()) {
return;
}
loadComponent().then(
module => module.default
).then((comp) => {
Component = comp;
}).catch((err) => {
console.error(`Cannot load component in <AsyncComponent />`);
throw err;
});
}
render() {
return (Component) ? <Component {...this.props} /> : null;
}
}
);

View File

@ -0,0 +1,48 @@
/**
* @file 对组件进行认证的方法
* @author Zhang Peng
* @see http://efe.baidu.com/blog/mixins-are-dead-long-live-the-composition/
* @see https://zhuanlan.zhihu.com/p/24776678
* @see https://segmentfault.com/a/1190000004598113
*/
import React from 'react';
import { withRouter } from 'react-router-dom';
/**
* 校验方法
* <p>如果不是登录状态并且history路径名不是/login将history置为/login</p>
* @param props {PropsType<S>} 组件的props
*/
const validate = props => {
const { history } = props;
const isLoggedIn = !!window.localStorage.getItem("uid");
if (!isLoggedIn && history.location.pathname !== "/login") {
history.replace("/login");
}
};
/**
* 对组件进行认证的方法
* <p>使用 React 高阶组件技术包裹传入的组件对组件中的 props 进行校验</p>
* @param WrappedComponent {React.Component} 传入的组件
* @returns {withRouter<S>}
*/
const authHOC = WrappedComponent => {
class Authenticate extends React.Component {
componentWillMount() {
validate(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.location !== this.props.location) {
validate(nextProps);
}
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return withRouter(Authenticate);
};
export default authHOC;

View File

@ -0,0 +1,153 @@
/**
* @file 封装支持 promise http 请求工具
* @author Zhang Peng
* @see https://github.com/mzabriskie/axios
* @see http://www.jianshu.com/p/df464b26ae58
*/
const axios = require('axios');
const qs = require('qs');
import config from '../../config/app.config';
// 本项目中 axios 的默认全局配置
axios.defaults.timeout = config.http.timeout;
axios.defaults.baseURL = config.http.baseURL;
axios.defaults.headers.get['Content-Type'] = 'application/json';
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.defaults.headers.put['Content-Type'] = 'application/json';
// 本项目的默认配置
const defaultConfig = {
//`url` 是请求的服务器地址
// url: '/user',
//`method` 是请求资源的方式
// method: 'get', //default
//如果`url`不是绝对地址,那么`baseURL`将会加到`url`的前面
//当`url`是相对地址的时候,设置`baseURL`会非常的方便
baseURL: config.http.baseURL,
//`transformRequest` 选项允许我们在请求发送到服务器之前对请求的数据做出一些改动
//该选项只适用于以下请求方式:`put/post/patch`
//数组里面的最后一个函数必须返回一个字符串、-一个`ArrayBuffer`或者`Stream`
transformRequest: [function (data) {
// 序列化
if (data) {
console.log("[request after stringify] data: ", JSON.stringify(data));
return JSON.stringify(data);
}
}],
//`transformResponse` 选项允许我们在数据传送到`then/catch`方法之前对数据进行改动
transformResponse: [function (data) {
// 反序列化
if (data) {
console.log("[response after parse] data: ", JSON.parse(data));
return JSON.parse(data);
}
}],
//`headers`选项是需要被发送的自定义请求头信息
headers: { 'X-Requested-With': 'XMLHttpRequest' },
//`params`选项是要随请求一起发送的请求参数----一般链接在URL后面
//他的类型必须是一个纯对象或者是URLSearchParams对象
// params: {
// ID: 12345
// },
//`paramsSerializer`是一个可选的函数起作用是让参数params序列化
//例如(https://www.npmjs.com/package/qs,http://api.jquery.com/jquery.param)
paramsSerializer: function (params) {
const content = qs.stringify(params, { arrayFormat: 'brackets' });
console.log("[http] params 序列化后:", content);
return content;
},
//`data`选项是作为一个请求体而需要被发送的数据
//该选项只适用于方法:`put/post/patch`
//当没有设置`transformRequest`选项时dada必须是以下几种类型之一
//string/plain/object/ArrayBuffer/ArrayBufferView/URLSearchParams
//仅仅浏览器FormData/File/Bold
//仅node:Stream
// data: {
// firstName: "Fred"
// },
//`timeout` 选项定义了请求发出的延迟毫秒数
//如果请求花费的时间超过延迟的时间,那么请求会被终止
timeout: config.http.timeout,
//`withCredentails`选项表明了是否是跨域请求
// withCredentials: false,//default
//`adapter`适配器选项允许自定义处理请求,这会使得测试变得方便
//返回一个promise,并提供验证返回
// adapter: function (config) {
// /*..........*/
// },
//`auth` 表明HTTP基础的认证应该被使用并提供证书
//这会设置一个authorization头header,并覆盖你在header设置的Authorization头信息
// auth: {
// username: "zhangsan",
// password: "s00sdkf"
// },
//返回数据的格式
//其可选项是arraybuffer,blob,document,json,text,stream
// responseType: 'json',//default
//
xsrfCookieName: 'XSRF-TOKEN',//default
xsrfHeaderName: 'X-XSRF-TOKEN',//default
//`onUploadProgress`上传进度事件
onUploadProgress: function (progressEvent) {
},
//下载进度的事件
onDownloadProgress: function (progressEvent) {
},
//相应内容的最大值
maxContentLength: 2000,
//`validateStatus`定义了是否根据http相应状态码来resolve或者reject promise
//如果`validateStatus`返回true(或者设置为`null`或者`undefined`),那么promise的状态将会是resolved,否则其状态就是rejected
validateStatus: function (status) {
return status >= 200 && status < 300;//default
},
//`maxRedirects`定义了在nodejs中重定向的最大数量
// maxRedirects: 5,//default
//`httpAgent/httpsAgent`定义了当发送http/https请求要用到的自定义代理
//keeyAlive在选项中没有被默认激活
// httpAgent: new http.Agent({ keeyAlive: true }),
// httpsAgent: new https.Agent({ keeyAlive: true }),
//proxy定义了主机名字和端口号
//`auth`表明http基本认证应该与proxy代理链接并提供证书
//这将会设置一个`Proxy-Authorization` header,并且会覆盖掉已经存在的`Proxy-Authorization` header
// proxy: {
// host: '127.0.0.1',
// port: 9000,
// auth: {
// username: 'skda',
// password: 'radsd'
// }
// },
//`cancelToken`定义了一个用于取消请求的cancel token
//详见cancelation部分
// cancelToken: new CancelToken(function (cancel) {
// })
};
// 使用默认配置初始化的请求
const http = axios.create(defaultConfig);
export default http;

View File

@ -0,0 +1,8 @@
/**
* @file 工具类总入口
* @desc 工具类总入口
* <p>原则上工具类都要在此注册然后提供给外部模块</p>
* @author Zhang Peng
*/
export { default as authHOC } from './authHOC';

View File

@ -0,0 +1,27 @@
/**
* Created by Zhang Peng on 2017/7/21.
*/
import React from 'react';
import {Carousel, Col, Row} from 'antd';
import './Home.less';
import logo from './logo.svg';
export default class Home extends React.Component {
static propTypes = {};
static defaultProps = {};
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to REACT ADMIN</h1>
</header>
<p className="App-intro">
REACT ADMIN is developing
</p>
</div>
);
}
}

View File

@ -0,0 +1,29 @@
.App {
text-align: center;
min-height: 600px;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #ffebcc;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,166 @@
import { Button, Card, Col, Form, Icon, Input, message, Row } from 'antd';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { login } from '../../../redux/actions/auth';
import loginLogo from './login-logo.png';
import './Login.less';
const FormItem = Form.Item;
const propTypes = {
user: PropTypes.object,
loggingIn: PropTypes.bool,
message: PropTypes.string
};
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field]);
}
class Login extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
}
}
componentDidMount() {
// To disabled submit button at the beginning.
this.props.form.validateFields();
}
handleSubmit(e) {
e.preventDefault();
this.setState({
loading: true
});
const data = this.props.form.getFieldsValue();
this.props.login(data.user, data.password).payload.promise.then(response => {
this.setState({
loading: false
});
if (response.error) {
console.warn('login failed: ', response.payload.message);
} else {
let result = response.payload.data;
console.log("login result:", result);
if (result) {
if (0 !== result.code) {
let str = '';
if (Array.isArray(result.messages)) {
result.messages.map((item) => {
str = str + item + '\n';
})
}
message.error('登录失败: \n'+ str);
} else {
console.info('[Login] res.payload.data: ', result);
message.success('欢迎你,' + result.data.name);
this.props.history.replace('/');
}
}
}
}).catch(err => {
console.error('[Login] err: ', err);
this.setState({
loading: false
});
});
this.props.form.validateFields((err, values) => {
if (!err) {
console.info('提交表单信息', values);
} else {
console.error(err);
}
});
}
render() {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched, setFieldsValue } = this.props.form;
// Only show error after a field is touched.
const userNameError = isFieldTouched('userName') && getFieldError('userName');
const passwordError = isFieldTouched('password') && getFieldError('password');
return (
<Row className="login-row" type="flex" justify="space-around" align="middle">
<Card className="login-form">
<Row gutter={12} type="flex" justify="space-around" align="middle">
<Col span="{6}">
</Col>
<Col span="{6}">
<img alt="loginLogo" src={loginLogo} style={{ width: 50, height: 50 }}/>
</Col>
<Col span="{6}">
<h1>React 管理系统</h1>
</Col>
<Col span="{6}">
</Col>
</Row>
<Form layout="horizontal" onSubmit={this.handleSubmit.bind(this)}>
<FormItem validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}>
{getFieldDecorator('user', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input
className="input"
prefix={<Icon type="user" style={{ fontSize: 18 }} />}
ref={node => this.userNameInput = node}
placeholder="admin"
/>
)}
</FormItem>
<FormItem validateStatus={passwordError ? 'error' : ''}
help={passwordError || ''}>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your password!' }],
})(
<Input className="input" size="large"
prefix={<Icon type="lock" style={{ fontSize: 18 }} />}
type='password'
placeholder='123456' />
)}
</FormItem>
<p>
<Button className="btn-login" type='primary' size="large" icon="poweroff"
loading={this.state.loading}
htmlType='submit'
disabled={hasErrors(getFieldsError())}>确定</Button>
</p>
</Form>
</Card>
</Row>
)
}
}
Login.propTypes = propTypes;
Login = Form.create()(Login);
function mapStateToProps(state) {
const { auth } = state;
if (auth.user) {
return { user: auth.user, loggingIn: auth.loggingIn, message: '' };
}
return { user: null, loggingIn: auth.loggingIn, message: auth.message };
}
function mapDispatchToProps(dispatch) {
return {
login: bindActionCreators(login, dispatch)
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Login))

View File

@ -0,0 +1,31 @@
.login-row {
width: 100%;
height: 100%;
background-image: url("bg.jpg");
background-size: cover;
.login-form {
background: #F3F9F9;
border-radius: 18px;
.input {
width: 300px;
size: 18px;
.anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.anticon-close-circle:hover {
color: #999;
}
.anticon-close-circle:active {
color: #666;
}
}
.btn-login {
width: 300px;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,49 @@
import { Table } from 'antd';
import React from 'react';
const columns = [{
title: 'Name',
dataIndex: 'name',
}, {
title: 'Age',
dataIndex: 'age',
}, {
title: 'Address',
dataIndex: 'address',
}];
const data = [{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
}, {
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
}, {
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
}, {
key: '4',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
}, {
key: '5',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
}];
export default class MailboxPage extends React.Component {
render() {
return (
<div title="Mailbox Page">
<Table columns={columns} dataSource={data} size="small" />
</div>
);
}
}

View File

@ -0,0 +1,143 @@
import React, {PropTypes} from "react";
import {Alert, Button, Col, Input, Row, Select, Table} from "antd";
import "./User.less";
const {Option, OptGroup} = Select;
const columns = [{
title: 'Name',
dataIndex: 'name',
filters: [{
text: 'Joe',
value: 'Joe',
}, {
text: 'Jim',
value: 'Jim',
}, {
text: 'Submenu',
value: 'Submenu',
children: [{
text: 'Green',
value: 'Green',
}, {
text: 'Black',
value: 'Black',
}],
}],
// specify the condition of filtering result
// here is that finding the name started with `value`
onFilter: (value, record) => record.name.indexOf(value) === 0,
sorter: (a, b) => a.name.length - b.name.length,
}, {
title: 'Age',
dataIndex: 'age',
sorter: (a, b) => a.age - b.age,
}, {
title: 'Address',
dataIndex: 'address',
filters: [{
text: 'London',
value: 'London',
}, {
text: 'New York',
value: 'New York',
}],
filterMultiple: false,
onFilter: (value, record) => record.address.indexOf(value) === 0,
sorter: (a, b) => a.address.length - b.address.length,
}];
const data = [{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
}, {
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
}, {
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
}, {
key: '4',
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
}];
function onChange(pagination, filters, sorter) {
console.log('params', pagination, filters, sorter);
}
export default class UserView extends React.Component {
render() {
return (
<div className="user-view-row">
<div>
<Row gutter={16} className="user-view-row">
<Col span={1}>工号</Col>
<Col span={7}><Input name="user" placeholder="请输入"/></Col>
<Col span={1}>姓名</Col>
<Col span={7}><Input placeholder="请输入"/></Col>
<Col span={1}>部门</Col>
<Col span={7}>
<Select
defaultValue="lucy"
style={{width: 200}}
>
<OptGroup label="Manager">
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
</OptGroup>
<OptGroup label="Engineer">
<Option value="Yiminghe">yiminghe</Option>
</OptGroup>
</Select>
</Col>
</Row>
</div>
<div>
<Row gutter={16} className="user-view-row">
<Col span={1}>状态</Col>
<Col span={7}>
<Select
defaultValue="lucy"
style={{width: 200}}
>
</Select>
</Col>
<Col span={1}>修改时间</Col>
<Col span={7}><Input placeholder="请输入"/></Col>
<Col span={1}/>
<Col span={7}/>
</Row>
</div>
<div>
<Row gutter={16} justify="end" className="user-view-row">
<Col span="{8]"/>
<Col span="{8]"/>
<Col span="{8]">
<Button type="primary">搜索</Button>
<Button>重置</Button>
</Col>
</Row>
</div>
<div className="user-view-row"><Button type="primary">添加用户</Button></div>
<div className="user-view-row">
<Alert message="已选择 2 项" type="info" showIcon/>
</div>
<div className="user-view-row">
<Table columns={columns} dataSource={data} onChange={onChange}/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,3 @@
.user-view-row {
padding-bottom: 15px;
}

View File

@ -0,0 +1,22 @@
const axios = require('axios');
const MockAdapter = require('axios-mock-adapter');
const mockAxios = axios.create();
// mock 数据
const mock = new MockAdapter(mockAxios);
mock.onPut('/login').reply(config => {
let postData = JSON.parse(config.data);
console.info('[mock]', postData);
if (postData.username === 'admin' && postData.password === '123456') {
let result = require('./user');
console.info('[mock result]', result);
return [200, result];
} else {
return [500, { message: "Incorrect user or password" }];
}
});
mock.onGet('/logout').reply(200, {});
mock.onGet('/my').reply(200, require('./user'));
mock.onGet('/menu').reply(200, require('./menu'));
export default mockAxios;

View File

@ -0,0 +1,92 @@
module.exports = {
"code": 0,
"messages": [
"成功"
],
"data": [
{
"key": 0,
"title": "Home",
"icon": "home",
"type": "Item",
"url": "/pages/home",
"children": []
},
{
"key": 1,
"title": "Pages",
"icon": "user",
"type": "SubMenu",
"url": null,
"children": [
{
"key": 11,
"title": "Mailbox",
"icon": "mail",
"type": "Item",
"url": "/pages/mailbox",
"children": []
},
{
"key": 12,
"title": "User",
"icon": "user",
"type": "Item",
"url": "/pages/user",
"children": []
}
]
},
{
"key": 2,
"title": "Others",
"icon": "coffee",
"type": "SubMenu",
"url": null,
"children": [
{
"key": 21,
"title": "Group1",
"icon": "windows-o",
"type": "ItemGroup",
"url": null,
"children": [
{
"key": 22,
"title": "Group1-1",
"icon": null,
"type": "Item",
"url": "/pages/home",
"children": []
}
]
},
{
"key": 23,
"title": "Divider",
"icon": null,
"type": "Divider",
"url": null,
"children": []
},
{
"key": 24,
"title": "Group2",
"icon": "apple-o",
"type": "ItemGroup",
"url": null,
"children": [
{
"key": 25,
"title": "Group2-1",
"icon": null,
"type": "Item",
"url": "/pages/home",
"children": []
}
]
}
]
}
]
};

View File

@ -0,0 +1,5 @@
module.exports = {
code: 0,
messages: ["成功"],
data: { uid: "1", role: "ADMIN", name: "admin" }
};

View File

@ -0,0 +1,6 @@
{
"name": "webapi",
"version": "0.0.0",
"private": true,
"main": "./webapi.js"
}

View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'development') {
module.exports = require('./mock').default;
} else {
module.exports = require('../utils/http').default;
}