🔖 react app
parent
52094ec137
commit
2aa5760a7a
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# http://editorconfig.org
|
||||
# 所有文件换行以 Unix like 风格(LF),win 格式特定的除外(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
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @file app 的全局配置
|
||||
* @author Zhang Peng
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* 打印日志开关
|
||||
*/
|
||||
log: true,
|
||||
|
||||
http: {
|
||||
|
||||
/**
|
||||
* 请求超时时间
|
||||
*/
|
||||
timeout: 5000,
|
||||
|
||||
/**
|
||||
* 服务器的host
|
||||
*/
|
||||
baseURL: 'http://localhost:8080/api',
|
||||
}
|
||||
};
|
|
@ -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 的名字
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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"),
|
||||
],
|
||||
});
|
|
@ -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 |
|
@ -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>
|
|
@ -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/
|
|
@ -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 text,response 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 预计错误处理,如 404,400,500,非返回 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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
.ant-layout-breadcrumb {
|
||||
z-index: 200;
|
||||
line-height: 64px;
|
||||
.ant-breadcrumb-link {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
.ant-layout-content {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
}
|
|
@ -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;
|
|
@ -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%;
|
||||
}
|
|
@ -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));
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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))
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "CoreContainer",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./CoreContainer.jsx"
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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.');
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
});
|
||||
}
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* @file 工具类总入口
|
||||
* @desc 工具类总入口。
|
||||
* <p>原则上工具类都要在此注册,然后提供给外部模块</p>
|
||||
* @author Zhang Peng
|
||||
*/
|
||||
|
||||
export { default as authHOC } from './authHOC';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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); }
|
||||
}
|
|
@ -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 |
|
@ -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))
|
|
@ -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 |
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.user-view-row {
|
||||
padding-bottom: 15px;
|
||||
}
|
|
@ -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;
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
code: 0,
|
||||
messages: ["成功"],
|
||||
data: { uid: "1", role: "ADMIN", name: "admin" }
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "webapi",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./webapi.js"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
if (process.env.NODE_ENV === 'development') {
|
||||
module.exports = require('./mock').default;
|
||||
} else {
|
||||
module.exports = require('../utils/http').default;
|
||||
}
|
Loading…
Reference in New Issue