🔖 添加 reactadmin 应用实例
parent
e6e9884def
commit
eeb32fdb84
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"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,62 @@
|
|||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'commonjs': true,
|
||||
'es6': true,
|
||||
'node': true
|
||||
},
|
||||
'extends': 'airbnb',
|
||||
'globals': {},
|
||||
'parser': 'babel-eslint',
|
||||
'plugins': [
|
||||
'import',
|
||||
'jsx-a11y',
|
||||
'react'
|
||||
],
|
||||
'rules': {
|
||||
/* ESLint 检查规则 */
|
||||
/* @see https://eslint.org/docs/rules/ */
|
||||
// Possible Errors
|
||||
'no-console': 'off', // 禁用 console
|
||||
// Best Practices
|
||||
'eqeqeq': [2, 'allow-null'], // 要求使用 === 和 !==
|
||||
'no-param-reassign': 'off', // 禁止对 function 的参数进行重新赋值
|
||||
// Variables
|
||||
'no-shadow-restricted-names': 'warn', // 禁止将标识符定义为受限的名字
|
||||
// Stylistic Issues
|
||||
'comma-dangle': ['error', 'never'], // 要求或禁止末尾逗号
|
||||
'indent': ['error', 2], // 强制使用一致的缩进,缩进空格为 2
|
||||
'max-len': ['warn', 120, { 'ignoreUrls': true }], // 强制一行的最大长度,最大长度为 120
|
||||
'no-underscore-dangle': 'off', // 禁止标识符中有悬空下划线
|
||||
// Node.js and CommonJS
|
||||
'global-require': 'off', // 要求 require() 出现在顶层模块作用域中
|
||||
// ES6(ES2015)
|
||||
'arrow-body-style': 'off', // 要求箭头函数体使用大括号
|
||||
'arrow-parens': 'off', // 要求箭头函数的参数使用圆括号
|
||||
'no-restricted-syntax': 'off',
|
||||
|
||||
/* React 语法的 ESLint 检查规则 */
|
||||
/* @see https://github.com/yannickcr/eslint-plugin-react */
|
||||
'react/no-multi-comp': 'off', // 防止每个文件有多个组件定义
|
||||
'react/prop-types': 'off', // 防止在 React 组件中没有对 props 的校验
|
||||
'react/forbid-prop-types': 'off',
|
||||
'react/prefer-stateless-function': 'off', // 强制无状态 React 组件为纯函数
|
||||
'react/jsx-filename-extension': 'off', // 限制可能包含 JSX 的文件扩展名
|
||||
'react/no-array-index-key': 'off', // 限制数组索引作为 key
|
||||
'react/no-string-refs': 'off',
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
|
||||
/* JSX 元素的 ESLint 检查规则 */
|
||||
/* @see https://github.com/evcohen/eslint-plugin-jsx-a11y */
|
||||
'jsx-a11y/href-no-hash': 'off',
|
||||
'jsx-a11y/no-noninteractive-element-interactions': 'off', // 不应该为非交互式元素分配鼠标或键盘事件监听器
|
||||
'jsx-a11y/label-has-for': 'off',
|
||||
'jsx-a11y/no-static-element-interactions': 'off',
|
||||
|
||||
/* 导入规则的 ESLint 检查规则 */
|
||||
/* @see https://github.com/benmosher/eslint-plugin-import */
|
||||
'import/no-dynamic-require': 'off', // 禁止 require() 调用表达式
|
||||
'import/no-extraneous-dependencies': 'off', // 禁止使用无关的 package
|
||||
'import/no-unresolved': [2, { commonjs: true, amd: true }] // 导入的模块可以解析为本地文件系统上的模块
|
||||
}
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const paths = require('./paths');
|
||||
|
||||
// 确保包含 env.js 之后的 paths.js 将会读取 .env 变量。
|
||||
delete require.cache[require.resolve('./paths')];
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
throw new Error(
|
||||
'The NODE_ENV environment variable is required but was not specified.'
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||
const dotenvFiles = [
|
||||
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||
`${paths.dotenv}.${NODE_ENV}`,
|
||||
|
||||
// 对于 `test` 环境,不要包含 `.env.local`,因为通常你会希望测试能为每个人产生相同的结果
|
||||
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
|
||||
paths.dotenv
|
||||
].filter(Boolean);
|
||||
|
||||
// 从 .env* 文件加载环境变量。
|
||||
// 如果此文件丢失,请使用 silent 抑制警告。
|
||||
// dotenv 将永远不会修改任何已经设置的环境变量。
|
||||
// @see https://github.com/motdotla/dotenv
|
||||
dotenvFiles.forEach(dotenvFile => {
|
||||
if (fs.existsSync(dotenvFile)) {
|
||||
require('dotenv').config({
|
||||
path: dotenvFile
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 我们支持根据 `NODE_PATH` 解析模块。
|
||||
// 这使您可以在导入大型 monorepos 中使用绝对路径:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253。
|
||||
// 它的工作类似于 Node 本身的 `NODE_PATH`:
|
||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||
// 请注意,与 Node 不同,只有来自 `NODE_PATH` 的相对路径才能得到认可。
|
||||
// 否则,我们有可能将 Node.js 的核心模块导入到应用程序而不是 Webpack 垫片。
|
||||
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
|
||||
// 我们也解决了这些问题,确保使用它们的所有工具始终如一地工作
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||
.split(path.delimiter)
|
||||
.filter(folder => folder && !path.isAbsolute(folder))
|
||||
.map(folder => path.resolve(appDirectory, folder))
|
||||
.join(path.delimiter);
|
||||
|
||||
// 抓取 NODE_ENV 和 REACT_APP_* 环境变量,
|
||||
// 并通过 Webpack 配置中的 DefinePlugin 将它们预先注入到应用程序中。
|
||||
const REACT_APP = /^REACT_APP_/i;
|
||||
|
||||
function getClientEnvironment(publicUrl) {
|
||||
const raw = Object.keys(process.env)
|
||||
.filter(key => REACT_APP.test(key))
|
||||
.reduce(
|
||||
(env, key) => {
|
||||
env[key] = process.env[key];
|
||||
return env;
|
||||
},
|
||||
{
|
||||
// 用于确定我们是否以生产模式运行。
|
||||
// 最重要的是,它将 React 切换到正确的模式。
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
|
||||
// 用于解决 `public` 中静态资源的正确路径。
|
||||
// 例如,<img src={process.env.PUBLIC_URL + '/img/logo.png'} />。
|
||||
// 这只能用作逃生舱口。通常情况下,您可以在代码中将图像放入 `src` 和 `import` ,以获取路径。
|
||||
PUBLIC_URL: publicUrl
|
||||
}
|
||||
);
|
||||
// 对所有值进行排序,以便我们可以进入 Webpack DefinePlugin。
|
||||
const stringified = {
|
||||
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||
env[key] = JSON.stringify(raw[key]);
|
||||
return env;
|
||||
}, {})
|
||||
};
|
||||
|
||||
return { raw, stringified };
|
||||
}
|
||||
|
||||
module.exports = getClientEnvironment;
|
|
@ -0,0 +1,12 @@
|
|||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/tutorial-webpack.html
|
||||
|
||||
module.exports = {
|
||||
process() {
|
||||
return 'module.exports = {};';
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return 'cssTransform';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
const path = require('path');
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
// http://facebook.github.io/jest/docs/tutorial-webpack.html
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
|
||||
// 确保解决了项目文件夹中的任何符号链接:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/637
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||
|
||||
const envPublicUrl = process.env.PUBLIC_URL;
|
||||
|
||||
function ensureSlash(servedUrl, needsSlash) {
|
||||
const hasSlash = servedUrl.endsWith('/');
|
||||
if (hasSlash && !needsSlash) {
|
||||
return servedUrl.substr(servedUrl, servedUrl.length - 1);
|
||||
} else if (!hasSlash && needsSlash) {
|
||||
return `${servedUrl}/`;
|
||||
}
|
||||
return servedUrl;
|
||||
}
|
||||
|
||||
const getPublicUrl = appPackageJson =>
|
||||
envPublicUrl || require(appPackageJson).homepage;
|
||||
|
||||
// 我们使用`PUBLIC_URL` 环境变量或 "homepage" 字段推断应用程序的“公共路径”。
|
||||
// Webpack 需要知道它可以将正确的<script> hrefs 放入 HTML,
|
||||
// 即使在单页应用程序中,可能为 /todos/42 这样的嵌套URL提供index.html。
|
||||
// 我们不在 HTML 中使用相对路径,因为我们不想加载类似 /todos/42/static/js/bundle.7289d.js 这样的文件。
|
||||
// 我们必须知道根路径。
|
||||
function getServedPath(appPackageJson) {
|
||||
const publicUrl = getPublicUrl(appPackageJson);
|
||||
const servedUrl =
|
||||
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
|
||||
return ensureSlash(servedUrl, true);
|
||||
}
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp('.env'),
|
||||
appBuild: resolveApp('build'),
|
||||
appConfig: resolveApp('config'),
|
||||
appPublic: resolveApp('public'),
|
||||
appHtml: resolveApp('public/index.html'),
|
||||
appIndexJs: resolveApp('src/index.js'),
|
||||
appPackageJson: resolveApp('package.json'),
|
||||
appSrc: resolveApp('src'),
|
||||
yarnLockFile: resolveApp('yarn.lock'),
|
||||
testsSetup: resolveApp('src/setupTests.js'),
|
||||
appNodeModules: resolveApp('node_modules'),
|
||||
publicUrl: getPublicUrl(resolveApp('package.json')),
|
||||
servedPath: getServedPath(resolveApp('package.json'))
|
||||
};
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* @file webpack 开发模式配置
|
||||
* @see https://doc.webpack-china.org/configuration/
|
||||
*/
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||
const eslintFormatter = require('react-dev-utils/eslintFormatter');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const getClientEnvironment = require('./env');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Webpack 使用 `publicPath` 来确定应用程序在哪里发送。
|
||||
// 在开发模式中,我们始终从 root 上服务。这使配置更容易。
|
||||
const publicPath = '/';
|
||||
// `publicUrl` 就像`publicPath`,但是我们将把它提供给我们的应用程序,
|
||||
// 就像 %PUBLIC_URL% 在 'index.html` 和 'process.env.PUBLIC_URL` 在 JavaScript 中 。
|
||||
// 省略尾随斜线,%PUBLIC_PATH%/xyz 看起来比 %PUBLIC_PATH%xyz 好。
|
||||
const publicUrl = '';
|
||||
// 获取环境变量以注入到应用程序中。
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// 这是开发模式配置。
|
||||
// 它专注于开发人员的经验和快速的重新构建。
|
||||
// 与生产模式配置不同,它存在于一个单独的文件中。
|
||||
module.exports = {
|
||||
|
||||
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
||||
// webpack 打包入口
|
||||
// 这意味着它们将是 JS 捆绑包中包含的根
|
||||
// 前两个入口点启用 "hot" CSS 和自动刷新 JS。
|
||||
entry: [
|
||||
|
||||
// 我们默认加载几个 polyfills
|
||||
// require.resolve('./polyfills'),
|
||||
|
||||
// 为 WebpackDevServer 添加备用客户端。
|
||||
// 客户端的工作是通过 socket 连接到 WebpackDevServer,并获得有关更改的通知。
|
||||
// 保存文件时,客户端将应用热更新(在更改CSS的情况下)或刷新页面(如果是JS更改)。
|
||||
// 当您发出语法错误时,此客户端将显示语法错误覆盖。
|
||||
// 注意:代替默认的 WebpackDevServer 客户端,我们使用自定义的方式为创建 React App 用户带来更好的体验。
|
||||
// 如果您喜欢 stock 客户端,您可以用以下两行代替以下行:
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
|
||||
// 您的应用程序的代码入口
|
||||
// 我们最后包含应用程序代码,以便如果在初始化期间有运行时错误,
|
||||
// 它不会破坏 WebpackDevServer 客户端,并且更改 JS 代码仍将触发刷新。
|
||||
paths.appIndexJs
|
||||
],
|
||||
|
||||
// webpack 如何输出结果的相关选项
|
||||
output: {
|
||||
|
||||
// 所有输出文件的目标路径
|
||||
// 必须是绝对路径(使用 Node.js 的 path 模块)
|
||||
// 下一行未在 dev 中使用,但是没有它 WebpackDevServer 会崩溃:
|
||||
path: paths.appBuild,
|
||||
|
||||
// 在输出中添加 /* filename */ 注释来生成require()。
|
||||
pathinfo: true,
|
||||
|
||||
// 「入口分块(entry chunk)」的文件名模板
|
||||
// 这不会产生真实的文件。
|
||||
// 这只是WebpackDevServer在开发中提供的虚拟路径。
|
||||
// 这是包含所有入口点的代码和Webpack运行时的JS包。
|
||||
filename: 'static/js/bundle.js',
|
||||
|
||||
// 「附加分块(additional chunk)」的文件名模板(如果你使用代码分离)
|
||||
chunkFilename: 'static/js/[name].chunk.js',
|
||||
|
||||
// 输出解析文件的目录,url 相对于 HTML 页面。开发模式下,我们使用 "/"
|
||||
publicPath,
|
||||
|
||||
// 将源映射条目指向原始磁盘位置(格式为Windows上的URL)
|
||||
devtoolModuleFilenameTemplate: info =>
|
||||
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')
|
||||
},
|
||||
|
||||
// 解析模块请求的选项
|
||||
resolve: {
|
||||
|
||||
// 用于查找模块的目录。
|
||||
// 我们把这些路径放在第二位,因为我们想要“node_modules”来“赢”
|
||||
// 如果有任何冲突。这符合 Node 的解析机制。
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253
|
||||
modules: ['node_modules', paths.appNodeModules].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
|
||||
// 使用的扩展名
|
||||
extensions: ['.js', '.json', '.jsx', '.css', '.less'],
|
||||
|
||||
// 模块别名列表
|
||||
alias: {
|
||||
// 支持 React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web'
|
||||
},
|
||||
|
||||
// 附加插件列表
|
||||
plugins: [
|
||||
|
||||
// 防止用户从 src /(或node_modules/)外部导入文件。
|
||||
// 这通常会导致混乱,因为我们只处理 src/ 中的文件与 babel。
|
||||
// 为了解决这个问题,我们阻止你从 src/ 外部导入文件。
|
||||
// 如果你非要这么做,请将文件链接到您的 node_modules/,并让 module-resolution 起作用。
|
||||
// 确保你的源文件被编译,因为它们不会以任何方式处理。
|
||||
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])
|
||||
]
|
||||
},
|
||||
|
||||
// 模块配置
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
|
||||
// 模块规则(配置 loader、解析器等选项)
|
||||
rules: [
|
||||
|
||||
// 首先,运行 ESLint
|
||||
// 在 Babel 处理 JS 之前做这件事很重要。
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
enforce: 'pre', // 标识应用这些规则,即使规则覆盖(高级选项)
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: eslintFormatter,
|
||||
eslintPath: require.resolve('eslint'),
|
||||
configFile: paths.appConfig.concat('/.eslintrc.js')
|
||||
},
|
||||
loader: require.resolve('eslint-loader')
|
||||
}
|
||||
],
|
||||
include: paths.appSrc
|
||||
},
|
||||
{
|
||||
// “oneOf”将遍历所有以下 loader,直到有一个符合要求。
|
||||
// 当没有 loader 匹配时,它会返回 loader 列表末尾的 loader。
|
||||
oneOf: [
|
||||
// url-loader 就像 file-loader 一样工作,但如果文件小于限制,则可以返回 DataURL。
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]'
|
||||
}
|
||||
},
|
||||
// JS 解析器,使得浏览器可以识别 ES6/React 等语法
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
// 这是 babel-loader(不是Babel本身)的一个功能。
|
||||
// 它允许缓存结果在 ./node_modules/.cache/babel-loader/ 目录下,使得重新构建更快。
|
||||
cacheDirectory: true
|
||||
}
|
||||
},
|
||||
// 这里的符号有点令人困惑。
|
||||
// postcss-loader 将 autoprefixer 应用于我们的CSS。
|
||||
// css-loader 解析 CSS 中的路径,并将资源添加为依赖关系。
|
||||
// style-loader 通常将 CSS 转换为注入<style>的 JS 模块,
|
||||
// 在生产模式中,我们使用一个插件将该 CSS 提取到一个文件中,
|
||||
// 但在开发模式中,style-loader 可以实现对 CSS 的热编辑。
|
||||
// @see https://github.com/webpack-contrib/style-loader
|
||||
// @see https://github.com/webpack-contrib/css-loader
|
||||
// @see https://github.com/postcss/postcss-loader
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
require.resolve('style-loader'),
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
// 导入外部 CSS 时必不可少N
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9' // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: 'no-2009'
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// less 完全兼容 css 语法。对于 less 文件,先交给 style-loader 、css-loader 处理。
|
||||
// 如果存在 less 自身的特性,则交给 less-loader 去处理。
|
||||
// @see https://github.com/webpack-contrib/less-loader
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: [{
|
||||
loader: 'style-loader'
|
||||
}, {
|
||||
loader: 'css-loader'
|
||||
}, {
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
strictMath: true,
|
||||
noIeCompat: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
// file-loader 确保资源文件最终在输出文件夹中。
|
||||
// 当您导入资产时,您将获得其文件名。
|
||||
// 这个装载器不使用“测试”,所以它将捕获所有未匹配其它 loader 的模块
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
|
||||
// 排除`js`文件以保持 css-loader 在注入时工作
|
||||
// 它是运行时,否则将通过 file-loader 处理。
|
||||
// 还可以排除`html`和`json`扩展名,以便它们通过 webpack 内部装载机得到处理
|
||||
exclude: [/\.js$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]'
|
||||
}
|
||||
}
|
||||
// 【结束】如果需要添加新的 loader,请确保在 file-loader 之前。
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// 使一些环境变量可用于JS代码。
|
||||
// 例如:if(process.env.NODE_ENV ==='production'){...}。请参阅 `./env.js`。
|
||||
// 在这里设置 NODE_ENV 是绝对必要的。否则React将以非常缓慢的开发模式进行编译。
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
|
||||
// Moment.js是一个非常受欢迎的日期相关的库,可以捆绑大型语言环境文件,默认情况下由于Webpack解释其代码。
|
||||
// 这是一个实用的解决方案,要求用户选择导入特定区域设置。
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// 如果你不使用 Moment.js,可以移除
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
|
||||
// 开启全局的模块热替换(HMR)
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
||||
// 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息
|
||||
new webpack.NamedModulesPlugin(),
|
||||
|
||||
// 在 index.html 中提供一些环境变量。
|
||||
// 公共 URL 在 index.html 中以 %PUBLIC_URL% 的形式提供,
|
||||
// 例如:<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// 在生产模式中,在 `package.json` 中指定 "homepage" ,它将是该URL的路径名;否则它将是一个空字符串,
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
|
||||
// 使用注入的<script>生成一个`index.html`文件。
|
||||
// @see https://github.com/jantimon/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml
|
||||
}),
|
||||
|
||||
// 如果您在路径中输入错误的大小写,则 Watcher 无法正常工作,因此当您尝试执行此操作时,我们使用打印错误的插件。
|
||||
// @see https://github.com/facebookincubator/create-react-app/issues/240
|
||||
new CaseSensitivePathsPlugin(),
|
||||
|
||||
// 如果您需要一个缺少的模块,然后 `npm install` 它 ,那么您仍然需要重新启动 Webpack 的开发服务器来发现它。
|
||||
// 此插件可以自动发现缺少的模块,使您不必重新启动。
|
||||
// @see https://github.com/facebookincubator/create-react-app/issues/186
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules)
|
||||
],
|
||||
|
||||
// 某些库导入 Node 模块,但不要在浏览器中使用它们。
|
||||
// 告诉 Webpack 为他们提供空的模拟,以便导入它们。
|
||||
node: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
},
|
||||
|
||||
// 在开发过程中关闭性能提示,因为我们不会为了追求速度进行代码分离或压缩。这些警告变得很麻烦。
|
||||
performance: {
|
||||
hints: false
|
||||
}
|
||||
};
|
|
@ -0,0 +1,356 @@
|
|||
/**
|
||||
* @file webpack 生产模式配置
|
||||
* @see https://doc.webpack-china.org/configuration/
|
||||
*/
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const eslintFormatter = require('react-dev-utils/eslintFormatter');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const paths = require('./paths');
|
||||
const getClientEnvironment = require('./env');
|
||||
|
||||
// Webpack 使用 `publicPath` 来确定应用程序在哪里发送。
|
||||
// 它需要尾部斜线,否则文件资源将获得不正确的路径。
|
||||
const publicPath = paths.servedPath;
|
||||
// 某些应用程序不使用 pushState 的客户端路由。
|
||||
// 对于这些,"homepage" 可以设置为 "."。以启用相对路径。
|
||||
const shouldUseRelativeAssetPaths = publicPath === './';
|
||||
// Source maps 较大,可能会导致因为源文件太大而内存不足的问题。
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||
// `publicUrl` 就像`publicPath`,但是我们将把它提供给我们的应用程序,
|
||||
// 就像 %PUBLIC_URL% 在 'index.html` 和 'process.env.PUBLIC_URL` 在 JavaScript 中 。
|
||||
// 省略尾随斜线,%PUBLIC_PATH%/xyz 看起来比 %PUBLIC_PATH%xyz 好。
|
||||
const publicUrl = publicPath.slice(0, -1);
|
||||
// 获取环境变量以注入到应用程序中。
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// 这里断言是为了安全。
|
||||
// React 的开发模式构建比较慢,不适合于生产环境
|
||||
// if (env.stringified['process.env'].NODE_ENV !== '"production"') {
|
||||
// throw new Error('Production builds must have NODE_ENV=production.');
|
||||
// }
|
||||
|
||||
// 注意:在这里定义,因为它将被使用不止一次。
|
||||
const cssFilename = 'static/css/[name].[contenthash:8].css';
|
||||
|
||||
// ExtractTextPlugin 期望构建的输出平稳。
|
||||
// (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27)
|
||||
// 但是,我们的输出结构使用 css,js 和 media 文件夹。
|
||||
// 要使这个结构可以作用于相对路径,我们必须使用自定义选项。
|
||||
const extractTextPluginOptions = shouldUseRelativeAssetPaths
|
||||
? // 确保 publicPath 返回到构建文件夹。
|
||||
{ publicPath: Array(cssFilename.split('/').length).join('../') }
|
||||
: {};
|
||||
|
||||
// 这是生产环境的配置
|
||||
// 它编译速度较慢并专注于生成一个快速和最小化的包。
|
||||
module.exports = {
|
||||
// 在第一个错误出错时抛出,而不是无视错误。
|
||||
bail: true,
|
||||
|
||||
// 我们在生产中生成 sourcemaps。这很慢,但效果不错。
|
||||
// 您可以在部署期间从构建中排除* .map文件。
|
||||
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
|
||||
devtool: shouldUseSourceMap ? 'source-map' : false,
|
||||
|
||||
// webpack 打包入口
|
||||
// 在生产环境中,我们仅仅想加载 polyfills 和 app 的代码.
|
||||
entry: [
|
||||
// require.resolve('./polyfills'),
|
||||
|
||||
// 您的应用程序的代码入口
|
||||
// 我们最后包含应用程序代码,以便如果在初始化期间有运行时错误,
|
||||
// 它不会破坏 WebpackDevServer 客户端,并且更改 JS 代码仍将触发刷新。
|
||||
paths.appIndexJs
|
||||
],
|
||||
|
||||
// webpack 如何输出结果的相关选项
|
||||
output: {
|
||||
|
||||
// 所有输出文件的目标路径
|
||||
// 必须是绝对路径(使用 Node.js 的 path 模块)
|
||||
path: paths.appBuild,
|
||||
|
||||
// 「入口分块(entry chunk)」的文件名模板
|
||||
// 生成的JS文件名(带有嵌套文件夹)将有一个主要的bundle,每个异步块有一个文件。
|
||||
// filename: "static/js/[name].js", // 用于多个入口点
|
||||
filename: 'static/js/[name].[chunkhash:8].js', // 用于长效缓存
|
||||
|
||||
// 「附加分块(additional chunk)」的文件名模板
|
||||
// chunkFilename: "static/js/[name].js", // 用于多个入口点
|
||||
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', // 用于长效缓存
|
||||
|
||||
// 输出解析文件的目录,url 相对于 HTML 页面。
|
||||
publicPath,
|
||||
|
||||
// 「devtool 中模块」的文件名模板
|
||||
devtoolModuleFilenameTemplate: info =>
|
||||
path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, '/')
|
||||
},
|
||||
|
||||
// 解析模块请求的选项
|
||||
resolve: {
|
||||
|
||||
// 用于查找模块的目录。
|
||||
// 我们把这些路径放在第二位,因为我们想要“node_modules”来“赢”
|
||||
// 如果有任何冲突。这符合 Node 的解析机制。
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253
|
||||
modules: ['node_modules', paths.appNodeModules].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
|
||||
// 使用的扩展名
|
||||
extensions: ['.js', '.json', '.jsx', '.css', '.less'],
|
||||
|
||||
// 模块别名列表
|
||||
alias: {
|
||||
// 支持 React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web'
|
||||
},
|
||||
|
||||
// 附加插件列表
|
||||
plugins: [
|
||||
|
||||
// 防止用户从 src /(或node_modules/)外部导入文件。
|
||||
// 这通常会导致混乱,因为我们只处理 src/ 中的文件与 babel。
|
||||
// 为了解决这个问题,我们阻止你从 src/ 外部导入文件。
|
||||
// 如果你非要这么做,请将文件链接到您的 node_modules/,并让 module-resolution 起作用。
|
||||
// 确保你的源文件被编译,因为它们不会以任何方式处理。
|
||||
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])
|
||||
]
|
||||
},
|
||||
|
||||
// 模块配置
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
|
||||
// 模块规则(配置 loader、解析器等选项)
|
||||
rules: [
|
||||
|
||||
// 首先,运行 ESLint
|
||||
// 在 Babel 处理 JS 之前做这件事很重要。
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
enforce: 'pre', // 标识应用这些规则,即使规则覆盖(高级选项)
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: eslintFormatter,
|
||||
eslintPath: require.resolve('eslint'),
|
||||
configFile: paths.appConfig.concat('/.eslintrc.js')
|
||||
},
|
||||
loader: require.resolve('eslint-loader')
|
||||
}
|
||||
],
|
||||
include: paths.appSrc
|
||||
},
|
||||
{
|
||||
// “oneOf”将遍历所有以下 loader,直到有一个符合要求。
|
||||
// 当没有 loader 匹配时,它会返回 loader 列表末尾的 loader。
|
||||
oneOf: [
|
||||
// url-loader 就像 file-loader 一样工作,但如果文件小于限制,则可以返回 DataURL。
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]'
|
||||
}
|
||||
},
|
||||
// JS 解析器,使得浏览器可以识别 ES6/React 等语法
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
compact: true
|
||||
}
|
||||
},
|
||||
// 这里的符号有点令人困惑。
|
||||
// postcss-loader 将 autoprefixer 应用于我们的CSS。
|
||||
// css-loader 解析 CSS 中的路径,并将资源添加为依赖关系。
|
||||
// style-loader 通常将 CSS 转换为注入<style>的 JS 模块,
|
||||
// 但与开发配置不同,我们做的不同。
|
||||
// “ExtractTextPlugin”首先应用 postcss-loader 和 css-loader(第二个参数),
|
||||
// 然后抓取 CSS 编译结果并将其放入我们构建过程中的一个单独文件。
|
||||
// 这样,我们实际上是在生产环境输出一个单独的CSS文件,而不是在JS代码中注入<style>标签。
|
||||
// 但是,如果您使用代码分割,则任何异步包仍然使用 style-loader 解析异步代码中的 CSS,
|
||||
// 因此它们不会出现在 main CSS 文件中。
|
||||
// @see https://github.com/webpack-contrib/style-loader
|
||||
// @see https://github.com/webpack-contrib/css-loader
|
||||
// @see https://github.com/postcss/postcss-loader
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ExtractTextPlugin.extract(
|
||||
Object.assign(
|
||||
{
|
||||
fallback: require.resolve('style-loader'),
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
minimize: true,
|
||||
sourceMap: shouldUseSourceMap
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9' // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: 'no-2009'
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
extractTextPluginOptions
|
||||
)
|
||||
)
|
||||
// 注意:如果`plugins`中没有 ExtractTextPlugin,这将不起作用。
|
||||
},
|
||||
// less 完全兼容 css 语法。对于 less 文件,先交给 style-loader 、css-loader 处理。
|
||||
// 如果存在 less 自身的特性,则交给 less-loader 去处理。
|
||||
// 与开发模式不同,这里使用了 ExtractTextPlugin
|
||||
// @see https://github.com/webpack-contrib/less-loader
|
||||
{
|
||||
test: /\.less$/,
|
||||
loader: ExtractTextPlugin.extract(
|
||||
Object.assign(
|
||||
{
|
||||
fallback: require.resolve('style-loader'),
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
minimize: true,
|
||||
sourceMap: shouldUseSourceMap
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: require.resolve('less-loader')
|
||||
}
|
||||
]
|
||||
},
|
||||
extractTextPluginOptions
|
||||
)
|
||||
)
|
||||
},
|
||||
// file-loader 确保资源文件最终在输出文件夹中。
|
||||
// 当您导入资产时,您将获得其文件名。
|
||||
// 这个装载器不使用“测试”,所以它将捕获所有未匹配其它 loader 的模块
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
|
||||
// 排除`js`文件以保持 css-loader 在注入时工作
|
||||
// 它是运行时,否则将通过 file-loader 处理。
|
||||
// 还可以排除`html`和`json`扩展名,以便它们通过 webpack 内部装载机得到处理
|
||||
exclude: [/\.js$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]'
|
||||
}
|
||||
}
|
||||
// 【结束】如果需要添加新的 loader,请确保在 file-loader 之前。
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// 使一些环境变量可用于JS代码。
|
||||
// 例如:if(process.env.NODE_ENV ==='production'){...}。请参阅 `./env.js`。
|
||||
// 在这里设置 NODE_ENV 是绝对必要的。否则React将以非常缓慢的开发模式进行编译。
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
|
||||
// 压缩代码大小
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false,
|
||||
// 由于Uglify出现问题而导致看似有效的代码被禁用:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2376
|
||||
// 待进一步调查:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false
|
||||
},
|
||||
output: {
|
||||
comments: false,
|
||||
// 打开它因为表情符号和正则表达式使用默认值没有正确地进行调整
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2488
|
||||
ascii_only: true
|
||||
},
|
||||
sourceMap: shouldUseSourceMap
|
||||
}),
|
||||
|
||||
// Moment.js是一个非常受欢迎的日期相关的库,可以捆绑大型语言环境文件,默认情况下由于Webpack解释其代码。
|
||||
// 这是一个实用的解决方案,要求用户选择导入特定区域设置。
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// 如果你不使用 Moment.js,可以移除
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
|
||||
// 在 index.html 中提供一些环境变量。
|
||||
// 公共 URL 在 index.html 中以 %PUBLIC_URL% 的形式提供,
|
||||
// 例如:<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// 在生产模式中,在 `package.json` 中指定 "homepage" ,它将是该URL的路径名;否则它将是一个空字符串,
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
|
||||
// 使用注入的<script>生成一个`index.html`文件。
|
||||
// @see https://github.com/jantimon/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true
|
||||
}
|
||||
}),
|
||||
|
||||
// 将文本从包中提取到文件中。
|
||||
// 注意:如果在“loader”中没有 ExtractTextPlugin.extract(..),这将不起作用。
|
||||
// @see https://github.com/webpack-contrib/extract-text-webpack-plugin
|
||||
new ExtractTextPlugin({
|
||||
filename: cssFilename
|
||||
}),
|
||||
|
||||
// 生成一个 manifest 文件,它包含所有资源文件到相应输出文件的映射清单。
|
||||
// 这使得工具可以无需解析 `index.html` 就找到资源。
|
||||
// @see https://github.com/danethurber/webpack-manifest-plugin
|
||||
new ManifestPlugin({
|
||||
fileName: 'asset-manifest.json'
|
||||
})
|
||||
],
|
||||
|
||||
// 某些库导入 Node 模块,但不要在浏览器中使用它们。
|
||||
// 告诉 Webpack 为他们提供空的模拟,以便导入它们。
|
||||
node: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
}
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @file webpack 开发服务器配置
|
||||
* @see https://doc.webpack-china.org/configuration/dev-server
|
||||
*/
|
||||
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||
const config = require('./webpack.config.dev');
|
||||
const paths = require('./paths');
|
||||
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
|
||||
module.exports = function (proxy, allowedHost) {
|
||||
return {
|
||||
|
||||
// WebpackDevServer 2.4.3引入了一个安全修复程序,可防止远程站点通过DNS重新绑定访问本地内容:
|
||||
// @see https://github.com/webpack/webpack-dev-server/issues/887
|
||||
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||
// 但是,云端环境或子域名开发中的几个现有用例显着更复杂:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2271
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2233
|
||||
// 我们正考虑更好的解决方案,现在我们将暂时妥协。
|
||||
// 由于我们的 WDS 配置只提供 `public` 文件夹中的文件,所以我们不会考虑访问一个漏洞。
|
||||
// 但是,如果您使用 `proxy` 功能,它会变得更危险,因为它可能暴露在 Django 和 Rails 等后端的远程代码执行漏洞。
|
||||
// 所以我们将正常禁用主机检查,但如果您指定了`proxy`设置,则启用它。
|
||||
// 最后,如果你真的知道你在使用一个特殊的环境变量,你可以重写它。
|
||||
disableHostCheck: !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
|
||||
|
||||
// 一切服务都启用 gzip 压缩
|
||||
compress: true,
|
||||
|
||||
// 当使用内联模式(inline mode)时,在开发工具(DevTools)的控制台(console)将显示消息,
|
||||
// 如:在重新加载之前,在一个错误之前,或者模块热替换(Hot Module Replacement)启用时。这可能显得很繁琐。
|
||||
// 这里关闭 WebpackDevServer 自己的日志,因为它们通常没什么用。
|
||||
// 但是仍然会显示编译警告和错误。
|
||||
clientLogLevel: 'none',
|
||||
|
||||
// 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。
|
||||
// devServer.publicPath 将用于确定应该从哪里提供 bundle,并且此选项优先。
|
||||
// 默认情况下,将使用当前工作目录作为提供内容的目录,但是你可以修改为其他目录:
|
||||
// 注意,推荐使用绝对路径。但是也可以从多个目录提供内容。如下:
|
||||
// contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")]
|
||||
// 默认情况下,WebpackDevServer 除了从内存中提供的所有虚拟构建产品之外,还提供来自当前目录的物理文件。
|
||||
// 这是令人困惑的,因为这些文件不会自动在生产构建文件夹中可用,除非我们复制它们。
|
||||
// 但是,复制整个项目目录是危险的,因为我们可能会暴露敏感文件。
|
||||
// 相反,我们建立一个只在 `public` 目录中提供公开文件的约定。
|
||||
// 我们的构建脚本会将 `public` 复制到 `build` 文件夹中。
|
||||
// 在 `index.html` 中,您可以使用 %PUBLIC_URL% 获取 `public` 文件夹的 URL:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">。
|
||||
// 在JavaScript代码中,您可以使用 `process.env.PUBLIC_URL' 访问它。
|
||||
// 请注意,我们建议使用`public`文件夹作为`favicon.ico`,`manifest.json` 和由于某些原因从 Webpack 无法导入的库
|
||||
// 如果您只想使用图像,请将其放在`src`中,然后从JavaScript导入。
|
||||
contentBase: paths.appPublic,
|
||||
|
||||
// 默认情况下,`contentBase` 的文件不会触发页面重新加载。
|
||||
watchContentBase: true,
|
||||
|
||||
// 启用 webpack 的模块热替换特性。
|
||||
// 它将为 WebpackDevServer 客户端提供 /sockjs-node/ 端点,以便可以了解文件的更新时间。
|
||||
// WebpackDevServer 客户端作为 Webpack 开发配置中的入口点。
|
||||
// 请注意,只有 CSS 的更改目前正在重新加载。JS 更改将刷新浏览器。
|
||||
hot: true,
|
||||
|
||||
// 告诉 WebpackDevServer 使用与我们在 config 中指定的相同的根路径是很重要的。在开发中,我们始终服务于/。
|
||||
publicPath: config.output.publicPath,
|
||||
|
||||
// 启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台。
|
||||
// 这也意味着来自 webpack 的错误或警告在控制台不可见。
|
||||
quiet: true,
|
||||
|
||||
// 据报道,这避免了某些系统上的CPU过载。
|
||||
// https://github.com/facebookincubator/create-react-app/issues/293
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
},
|
||||
|
||||
// 如果设置 HTTPS 环境变量为 true,则开启 HTTPS 开关
|
||||
https: protocol === 'https',
|
||||
host,
|
||||
overlay: false,
|
||||
historyApiFallback: {
|
||||
// 带点的路径应该仍然使用 history 回退。
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/387.
|
||||
disableDotRule: true
|
||||
},
|
||||
public: allowedHost,
|
||||
proxy,
|
||||
setup(app) {
|
||||
// 这使我们可以在运行时错误重叠中打开文件。
|
||||
app.use(errorOverlayMiddleware());
|
||||
|
||||
// service worker 文件实际上是一个“无操作”,将重置以前为同一主机注册的服务工作者:端口组合。
|
||||
// 我们在开发中这样做,以避免在使用相同的主机和端口时冲突生产缓存。
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware());
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"name": "react-admin",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"scripts": {
|
||||
"dev": "node script/dev.js",
|
||||
"prod": "node script/prod.js",
|
||||
"real": "node script/real.js",
|
||||
"test": "node script/test.js --env=jsdom",
|
||||
"deploy": "node script/deploy.js",
|
||||
"lint": "eslint -c config/.eslintrc.js --ext .js,.jsx src/"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"test/**/*.{js,jsx}"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/config/polyfills.js"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/__tests__/**/*.js?(x)",
|
||||
"<rootDir>/test/**/?(*.)(spec|test).js?(x)"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"testURL": "http://localhost",
|
||||
"transform": {
|
||||
"^.+\\.(js|jsx)$": "<rootDir>/node_modules/babel-jest",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^(?!.*\\.(js|jsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^react-native$": "react-native-web"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"web.js",
|
||||
"js",
|
||||
"json",
|
||||
"web.jsx",
|
||||
"jsx",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^2.13.4",
|
||||
"axios": "^0.16.2",
|
||||
"classnames": "^2.2.5",
|
||||
"lodash": "^4.17.4",
|
||||
"mockjs": "^1.0.1-beta3",
|
||||
"moment": "^2.19.1",
|
||||
"react": "15.6.0",
|
||||
"react-dom": "15.6.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-redux": "^5.0.0-alpha.6",
|
||||
"redux": "^3.7.2",
|
||||
"redux-actions": "^2.2.1",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.5",
|
||||
"axios-mock-adapter": "^1.9.0",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-loader": "^7.1.0",
|
||||
"babel-plugin-import": "^1.2.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.5.2",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.1",
|
||||
"chalk": "^2.1.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"dotenv": "^4.0.0",
|
||||
"eslint": "4.4.1",
|
||||
"eslint-config-airbnb": "15.1.0",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"eslint-plugin-import": "2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.2",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"extract-text-webpack-plugin": "^3.0.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"fs-extra": "^4.0.2",
|
||||
"gh-pages": "^1.0.0",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"jest": "^21.2.1",
|
||||
"less": "^3.0.0-alpha.3",
|
||||
"less-loader": "^4.0.5",
|
||||
"postcss-flexbugs-fixes": "^3.2.0",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"react-dev-utils": "^4.1.0",
|
||||
"react-hot-loader": "3.0.0-beta.6",
|
||||
"redux-logger": "^3.0.6",
|
||||
"style-loader": "^0.19.0",
|
||||
"url-loader": "^0.6.2",
|
||||
"webpack": "3.6.0",
|
||||
"webpack-dev-server": "2.8.2",
|
||||
"webpack-manifest-plugin": "1.2.1"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
|
@ -0,0 +1,40 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run prod`.
|
||||
-->
|
||||
<title>REACT ADMIN</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<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` or `yarn dev`.
|
||||
To create a production bundle, use `npm run prod` or `yarn prod`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "react admin",
|
||||
"name": "Create react admin Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
const chalk = require('chalk');
|
||||
const ghpages = require('gh-pages');
|
||||
const paths = require('../config/paths');
|
||||
|
||||
const version = require(paths.appPackageJson).version;
|
||||
console.log(chalk.bold.cyan('Deploy site to gh-pages...'));
|
||||
console.log(chalk.bold('Current Version:%s'), version);
|
||||
|
||||
ghpages.publish('build', {
|
||||
dest: 'demos/',
|
||||
message: `Deploy demo v${version}`,
|
||||
user: {
|
||||
name: 'Zhang Peng',
|
||||
email: 'forbreak@163.com'
|
||||
}
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
console.log(chalk.bold.red('Failed to deploy demo v%s.'), version);
|
||||
console.log(chalk.bold.red(err));
|
||||
throw err;
|
||||
}
|
||||
console.log(chalk.bold.yellow('Site has been deployed to \n\t https://dunwu.github.io/react-app/demos/'));
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
// 首要的事是让任何代码都知道正确的运行环境。
|
||||
process.env.BABEL_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// 脚本遇到未处理的 rejections 将直接崩溃,而不是默默的忽略它们。
|
||||
// 将来,未处理的 promise rejections 将以非零退出代码终止 Node.js 进程。
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// 确保环境变量可以被读取。
|
||||
require('../config/env');
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const clearConsole = require('react-dev-utils/clearConsole');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const paths = require('../config/paths');
|
||||
const config = require('../config/webpack.config.dev');
|
||||
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// 如果必要的文件丢失,将告警和崩溃
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 类似 Cloud9 这样的工具依赖于此
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
// 我们尝试使用默认端口,但如果端口已被占用,则通常会为用户分配一个不同的端口。
|
||||
choosePort(HOST, DEFAULT_PORT)
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// 未发现端口
|
||||
return;
|
||||
}
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
const urls = prepareUrls(protocol, HOST, port);
|
||||
// 创建一个按照自定义消息配置的 webpack 编译器。
|
||||
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
|
||||
// 加载代理配置
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
|
||||
// 通过 web 服务器提供由编译器生成的 webpack 资源。
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// 启动 WebpackDevServer.
|
||||
devServer.listen(port, HOST, err => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
return console.log(chalk.cyan('Starting the development server...\n'));
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach((sig) => {
|
||||
process.on(sig, () => {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
// 首要的事是让任何代码都知道正确的运行环境。
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// 脚本遇到未处理的 rejections 将直接崩溃,而不是默默的忽略它们。
|
||||
// 将来,未处理的 promise rejections 将以非零退出代码终止 Node.js 进程。
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// 确保环境变量可以被读取。
|
||||
require('../config/env');
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const config = require('../config/webpack.config.prod');
|
||||
const paths = require('../config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// 这些尺寸相当大。我们会警告超过限制的 bundle。
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
// 如果必要的文件丢失,将告警和崩溃
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 创建生产环境构建并打印部署说明。
|
||||
function build(previousFileSizes) {
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const messages = formatWebpackMessages(stats.toJson({}, true));
|
||||
if (messages.errors.length) {
|
||||
// 只保留第一个错误。其他错误通常指示着同样的问题,使读者感到混乱。
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
return resolve({
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 拷贝 public 文件夹内容到 build 文件夹
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml
|
||||
});
|
||||
}
|
||||
|
||||
// 首先,在构建目录中读取当前的文件大小。
|
||||
// 这让我们可以显示以后改变了多少。
|
||||
measureFileSizesBeforeBuild(paths.appBuild)
|
||||
.then(previousFileSizes => {
|
||||
// 删除 build 文件夹所有内容,但保留目录
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// 拷贝 public 文件夹内容到 build 文件夹
|
||||
copyPublicFolder();
|
||||
// 开始 webpack 构建
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
`\nSearch for the ${
|
||||
chalk.underline(chalk.yellow('keywords'))
|
||||
} to learn more about each warning.`
|
||||
);
|
||||
console.log(
|
||||
`To ignore, add ${
|
||||
chalk.cyan('// eslint-disable-next-line')
|
||||
} to the line before.\n`
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrl;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
err => {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// 首要的事是让任何代码都知道正确的运行环境。
|
||||
process.env.BABEL_ENV = 'real';
|
||||
process.env.NODE_ENV = 'real';
|
||||
|
||||
// 脚本遇到未处理的 rejections 将直接崩溃,而不是默默的忽略它们。
|
||||
// 将来,未处理的 promise rejections 将以非零退出代码终止 Node.js 进程。
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// 确保环境变量可以被读取。
|
||||
require('../config/env');
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const config = require('../config/webpack.config.prod');
|
||||
const paths = require('../config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// 这些尺寸相当大。我们会警告超过限制的 bundle。
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
// 如果必要的文件丢失,将告警和崩溃
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 创建生产环境构建并打印部署说明。
|
||||
function build(previousFileSizes) {
|
||||
console.log('Creating an optimized real build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const messages = formatWebpackMessages(stats.toJson({}, true));
|
||||
if (messages.errors.length) {
|
||||
// 只保留第一个错误。其他错误通常指示着同样的问题,使读者感到混乱。
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
return resolve({
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 拷贝 public 文件夹内容到 build 文件夹
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml
|
||||
});
|
||||
}
|
||||
|
||||
// 首先,在构建目录中读取当前的文件大小。
|
||||
// 这让我们可以显示以后改变了多少。
|
||||
measureFileSizesBeforeBuild(paths.appBuild)
|
||||
.then(previousFileSizes => {
|
||||
// 删除 build 文件夹所有内容,但保留目录
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// 拷贝 public 文件夹内容到 build 文件夹
|
||||
copyPublicFolder();
|
||||
// 开始 webpack 构建
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
`\nSearch for the ${
|
||||
chalk.underline(chalk.yellow('keywords'))
|
||||
} to learn more about each warning.`
|
||||
);
|
||||
console.log(
|
||||
`To ignore, add ${
|
||||
chalk.cyan('// eslint-disable-next-line')
|
||||
} to the line before.\n`
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrl;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
err => {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PUBLIC_URL = '';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
const jest = require('jest');
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
// Watch unless on CI or in coverage mode
|
||||
if (!process.env.CI && argv.indexOf('--coverage') < 0) {
|
||||
argv.push('--watch');
|
||||
}
|
||||
|
||||
|
||||
jest.run(argv);
|
|
@ -0,0 +1,10 @@
|
|||
let createHistory;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
createHistory = require('history/createHashHistory').default;
|
||||
} else {
|
||||
createHistory = require('history/createBrowserHistory').default;
|
||||
}
|
||||
|
||||
const history = createHistory();
|
||||
export default history;
|
|
@ -0,0 +1,4 @@
|
|||
export { default as store } from './store';
|
||||
export { default as rootReducer } from './rootReducer';
|
||||
export { default as rootRouter } from './rootRouter';
|
||||
export { default as history } from './history';
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @file redux reducer 的管理入口
|
||||
* @author Zhang Peng
|
||||
* @description redux reducer 的管理入口。
|
||||
* @see http://cn.redux.js.org/docs/basics/Reducers.html
|
||||
*/
|
||||
import { routerReducer as router } from 'react-router-redux';
|
||||
import { combineReducers } from 'redux';
|
||||
import menu from '../component/Layout/redux/reducers';
|
||||
import formview from '../view/form/redux/reducers';
|
||||
import table from '../view/ui/Table/redux/reducers';
|
||||
|
||||
const reducerMap = {
|
||||
router,
|
||||
menu,
|
||||
table,
|
||||
formview
|
||||
};
|
||||
|
||||
// combineReducers 的用法可以参考以下链接的内容
|
||||
// @see http://cn.redux.js.org/docs/recipes/reducers/UsingCombineReducers.html
|
||||
// @see http://cn.redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html
|
||||
// @see http://cn.redux.js.org/docs/api/combineReducers.html
|
||||
const rootReducer = combineReducers(reducerMap);
|
||||
export default rootReducer;
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @file react router 入口。
|
||||
* @description react router 入口。注意:本项目中使用 react router 4.x,它的 API 与以前的版本有很大的不同。
|
||||
* @author Zhang Peng
|
||||
* @see https://reacttraining.com/react-router/
|
||||
* @see https://reacttraining.cn/
|
||||
*/
|
||||
import _ from 'lodash';
|
||||
import errorRoutes from '../view/error/route';
|
||||
import formRoutes from '../view/form/route';
|
||||
import generalRoutes from '../view/general/route';
|
||||
import uiRoutes from '../view/ui/route';
|
||||
|
||||
/**
|
||||
* 合并所有子路由
|
||||
*/
|
||||
const rootRouter = _.concat(errorRoutes, generalRoutes, formRoutes, uiRoutes);
|
||||
export default rootRouter;
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* @file redux store 构造器
|
||||
* @author Zhang Peng
|
||||
* @description redux store 构造器。根据开发环境和非开发环境分别集成所需的 redux 中间件。
|
||||
* @see https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md
|
||||
* @see https://github.com/gaearon/redux-thunk
|
||||
* @see https://github.com/reactjs/react-router-redux
|
||||
* @see https://github.com/evgenyrodionov/redux-logger
|
||||
*/
|
||||
import { routerMiddleware } from 'react-router-redux';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import history from './history';
|
||||
import rootReducer from './rootReducer';
|
||||
|
||||
// 本项目所使用的基本的 redux 中间件
|
||||
const router = routerMiddleware(history);
|
||||
const middlewares = [thunk, router];
|
||||
|
||||
let enhancer;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 非生产环境额外添加 redux-logger
|
||||
const createLogger = require('redux-logger').createLogger;
|
||||
const logger = createLogger({ collapsed: true });
|
||||
middlewares.push(logger);
|
||||
|
||||
// 支持 redux 开发工具
|
||||
// @see https://github.com/zalmoxisus/redux-devtools-extension
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
enhancer = composeEnhancers(
|
||||
// Middleware you want to use in development:
|
||||
applyMiddleware(...middlewares),
|
||||
);
|
||||
} else {
|
||||
enhancer = applyMiddleware(...middlewares);
|
||||
}
|
||||
|
||||
// 创建 Redux Store,这是本应用唯一的状态管理容器
|
||||
const configStore = (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(rootReducer, initialState, enhancer);
|
||||
|
||||
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
|
||||
if (module.hot) {
|
||||
module.hot.accept('./rootReducer', () =>
|
||||
store.replaceReducer(require('./rootReducer').default)
|
||||
);
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
const store = configStore();
|
||||
export default store;
|
|
@ -0,0 +1,18 @@
|
|||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './index.less';
|
||||
|
||||
export default class FooterToolbar extends Component {
|
||||
render() {
|
||||
const { children, className, extra, ...restProps } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, 'toolbar')}
|
||||
{...restProps}
|
||||
>
|
||||
<div className="left">{extra}</div>
|
||||
<div className="right">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
@import "../../style/default.less";
|
||||
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, .03);
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 0 24px;
|
||||
transition: all .3s;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
button + button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @file 内容布局组件
|
||||
* @author Zhang Peng
|
||||
* @see https://ant.design/components/layout-cn/
|
||||
* @see https://ant.design/components/card-cn/
|
||||
*/
|
||||
import { Layout } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
/**
|
||||
* 内容布局组件
|
||||
* @class
|
||||
*/
|
||||
class CustomContent extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Content>
|
||||
{this.props.children}
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default CustomContent;
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @file 底部布局组件
|
||||
* @author Zhang Peng
|
||||
* @see https://ant.design/components/layout-cn/
|
||||
*/
|
||||
import { Layout } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const { Footer } = Layout;
|
||||
|
||||
/**
|
||||
* 底部布局组件
|
||||
* @class
|
||||
*/
|
||||
class CustomFooter extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Footer>
|
||||
Copyright © 2017 <a href="https://github.com/dunwu/" target="_blank">Zhang Peng</a><br />
|
||||
</Footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default CustomFooter;
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* @file 顶部布局组件
|
||||
* @author Zhang Peng
|
||||
* @see https://ant.design/components/layout-cn/
|
||||
*/
|
||||
import { Layout } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
/**
|
||||
* 顶部布局组件
|
||||
* @class
|
||||
*/
|
||||
class CustomHeader extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Header />
|
||||
);
|
||||
}
|
||||
}
|
||||
export default CustomHeader;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
class Logo extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Link className="sider-logo" to="/home">
|
||||
<img
|
||||
alt="antd.svg"
|
||||
src="http://oyz7npk35.bkt.clouddn.com/image/react-admin/react-admin-logo.ico"
|
||||
/>
|
||||
<span>REACT ADMIN</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Logo;
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* @file 侧边导航栏组件
|
||||
* @author Zhang Peng
|
||||
* @see https://ant.design/components/layout-cn/
|
||||
*/
|
||||
import { Icon, Menu, Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import getPathsAndKeyPath from './common';
|
||||
import { onMenuItemSelected, onMenuListSearch } from './redux/actions';
|
||||
|
||||
/**
|
||||
* 根据查询数据,动态加载菜单项
|
||||
*/
|
||||
class CustomMenu extends React.PureComponent {
|
||||
state = {
|
||||
paths: [],
|
||||
keyPath: [],
|
||||
mode: 'inline'
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.onMenuListSearch();
|
||||
this.setState({
|
||||
mode: this.props.mode
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { menu, router } = this.props;
|
||||
|
||||
// 根据选中菜单项的关键 key 数组获取对应组件
|
||||
const paths = [];
|
||||
const keyPath = [];
|
||||
getPathsAndKeyPath(router, menu, paths, keyPath);
|
||||
const getMenuItems = (list) => {
|
||||
return Array.isArray(list) && list.map((item) => {
|
||||
const menus = getMenuItems(item.children, item.key);
|
||||
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>}
|
||||
>
|
||||
{menus}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
case 'ItemGroup':
|
||||
return (
|
||||
<Menu.ItemGroup
|
||||
key={item.key}
|
||||
title={
|
||||
<span>
|
||||
<Icon type={item.icon} />
|
||||
<span className="nav-text">{item.title}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{menus}
|
||||
</Menu.ItemGroup>
|
||||
);
|
||||
case 'Divider':
|
||||
return (
|
||||
<Menu.Divider key={item.key} />
|
||||
);
|
||||
case 'Item':
|
||||
default:
|
||||
return (
|
||||
<Menu.Item key={item.key}>
|
||||
{
|
||||
item.url ? (
|
||||
<Link to={item.url}>
|
||||
{item.icon && <Icon type={item.icon} />}<span className="nav-text">{item.title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
{item.icon && <Icon type={item.icon} />}<span className="nav-text">{item.title}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={menu.loading} delay={500} size="large" tip="Loading...">
|
||||
<Menu
|
||||
className="sider-menu"
|
||||
mode={this.state.mode}
|
||||
theme="dark"
|
||||
selectedKeys={keyPath}
|
||||
defaultOpenKeys={['ui']}
|
||||
>
|
||||
{getMenuItems(menu.list)}
|
||||
</Menu>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
menu: state.menu,
|
||||
router: state.router
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onMenuListSearch: bindActionCreators(onMenuListSearch, dispatch),
|
||||
onMenuItemSelected: bindActionCreators(onMenuItemSelected, dispatch)
|
||||
};
|
||||
}
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CustomMenu));
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @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, Col, Icon, Row } from 'antd';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import getPathsAndKeyPath from './common';
|
||||
|
||||
/**
|
||||
* 面包屑组件
|
||||
* @class
|
||||
*/
|
||||
class Navpath extends React.PureComponent {
|
||||
render() {
|
||||
const { menu, router } = this.props;
|
||||
// 根据选中菜单项的关键 key 数组获取对应组件
|
||||
const paths = [];
|
||||
const keyPath = [];
|
||||
getPathsAndKeyPath(router, menu, paths, keyPath);
|
||||
|
||||
const breadcrumbItems = paths.map((item) => {
|
||||
return (
|
||||
<Breadcrumb.Item key={item.key}>
|
||||
<Icon type={item.icon} />
|
||||
<span>{item.title}</span>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
});
|
||||
|
||||
let tilte;
|
||||
if (paths && paths.length > 0) {
|
||||
tilte = paths[paths.length - 1].title;
|
||||
}
|
||||
return (
|
||||
<div className="navpath">
|
||||
<Row>
|
||||
<Col xs={0} sm={12} md={12} lg={12} xl={12}>
|
||||
<span className="title">{tilte}</span>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={12} lg={12} xl={12}>
|
||||
<Breadcrumb separator=">">
|
||||
{breadcrumbItems}
|
||||
</Breadcrumb>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
menu: state.menu,
|
||||
router: state.router
|
||||
};
|
||||
}
|
||||
export default withRouter(connect(mapStateToProps)(Navpath));
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* @file 侧边导航栏组件
|
||||
* @author Zhang Peng
|
||||
* @see https://ant.design/components/layout-cn/
|
||||
*/
|
||||
import { Layout } from 'antd';
|
||||
import React from 'react';
|
||||
import Menu from './Menu';
|
||||
import Logo from './Logo';
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
/**
|
||||
* 侧边导航栏组件。侧边栏采用的响应式布局方式,页面大小收缩到一定程度,侧边栏会隐藏。
|
||||
* @class
|
||||
*/
|
||||
class CustomSider extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
/**
|
||||
* 响应式布局
|
||||
* 说明:配置 breakpoint 属性即生效,视窗宽度小于 breakpoint 时 Sider 缩小为 collapsedWidth 宽度,
|
||||
* 若将 collapsedWidth 设置为零,会出现特殊 trigger。
|
||||
*/
|
||||
<Sider
|
||||
breakpoint="lg"
|
||||
collapsedWidth="0"
|
||||
>
|
||||
<Logo />
|
||||
<Menu mode="inline" />
|
||||
</Sider>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default CustomSider;
|
|
@ -0,0 +1,49 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
const getItemByUrl = (list, url, items) => {
|
||||
list.forEach((item) => {
|
||||
if (item.url === url) {
|
||||
items.push(item);
|
||||
}
|
||||
if (_.isArray(item.children)) {
|
||||
getItemByUrl(item.children, url, items);
|
||||
}
|
||||
});
|
||||
};
|
||||
const getItemByKey = (list, key, items) => {
|
||||
list.forEach((item) => {
|
||||
if (item.key === key) {
|
||||
items.push(item);
|
||||
}
|
||||
if (_.isArray(item.children)) {
|
||||
getItemByKey(item.children, key, items);
|
||||
}
|
||||
});
|
||||
};
|
||||
function getPaths(list, key, paths, keyPath) {
|
||||
const tmp = [];
|
||||
getItemByKey(list, key, tmp);
|
||||
if (_.isEmpty(tmp)) {
|
||||
return paths;
|
||||
}
|
||||
paths.push(tmp[0]);
|
||||
keyPath.push(tmp[0].key);
|
||||
return getPaths(list, tmp[0].parent, paths, keyPath);
|
||||
}
|
||||
function getPathsAndKeyPath(router, menu, paths, keyPath) {
|
||||
// 根据选中菜单项的关键 key 数组获取对应组件
|
||||
const items = [];
|
||||
if (router.location.pathname && menu.list) {
|
||||
getItemByUrl(menu.list, router.location.pathname, items);
|
||||
|
||||
if (!_.isEmpty(items)) {
|
||||
paths.push(items[0]);
|
||||
keyPath.push(items[0].key);
|
||||
getPaths(menu.list, items[0].parent, paths, keyPath);
|
||||
}
|
||||
paths = _.reverse(paths);
|
||||
keyPath = _.reverse(keyPath);
|
||||
}
|
||||
}
|
||||
|
||||
export default getPathsAndKeyPath;
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* @file Sider-Header-Content-Footer 式页面级整体布局
|
||||
* @author Zhang Peng
|
||||
* @see https://ant.design/components/layout-cn/
|
||||
*/
|
||||
import { Layout } from 'antd';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Content from './Content';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
import './index.less';
|
||||
import Navpath from './Navpath';
|
||||
import Sider from './Sider';
|
||||
|
||||
class CustomLayout extends React.PureComponent {
|
||||
static propTypes = {
|
||||
navpath: PropTypes.array
|
||||
};
|
||||
static defaultProps = {
|
||||
navpath: []
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navpath } = this.props;
|
||||
|
||||
return (
|
||||
<Layout className="ant-layout-has-sider">
|
||||
<Sider />
|
||||
<Layout className="ant-layout-container">
|
||||
<Header />
|
||||
<Navpath data={navpath} />
|
||||
<Content type="SHCF">
|
||||
{this.props.children}
|
||||
</Content>
|
||||
<Footer />
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state) => {
|
||||
const { menu } = state;
|
||||
return {
|
||||
navpath: menu.navpath
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = () => {
|
||||
return {};
|
||||
};
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CustomLayout));
|
|
@ -0,0 +1,111 @@
|
|||
@import "../../style/default.less";
|
||||
|
||||
.ant-layout-has-sider {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.ant-layout-container {
|
||||
background-color: @layout-content-backgroud-gray;
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
.ant-layout-sider {
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 页头栏
|
||||
.ant-layout-header {
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
background-color: @layout-header-background;
|
||||
}
|
||||
|
||||
// 内容栏
|
||||
.ant-layout-content {
|
||||
padding: 0 26px 40px 26px;
|
||||
}
|
||||
|
||||
// 页脚栏
|
||||
.ant-layout-footer {
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
background: white;
|
||||
}
|
||||
|
||||
// 侧边栏 Logo
|
||||
div .sider-logo {
|
||||
color: white;
|
||||
background-color: @layout-header-background;
|
||||
line-height: 64px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
weight: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: Lucida Family, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
// 页头导航路径
|
||||
.navpath {
|
||||
margin-top: 64px;
|
||||
line-height: 40px;
|
||||
height: 50px;
|
||||
|
||||
.title {
|
||||
line-height: 50px;
|
||||
font-size: @navpath-primary-font-size;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding-left: 36px;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
div .ant-breadcrumb {
|
||||
line-height: 50px;
|
||||
text-align: right;
|
||||
padding-right: 36px;
|
||||
|
||||
span {
|
||||
font-size: @navpath-secondary-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏菜单
|
||||
.sider-menu {
|
||||
.ant-menu-sub.ant-menu-inline .ant-menu-item-group-title {
|
||||
padding-left: 20px;
|
||||
font-size: @menu-font-size;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ant-menu-submenu-title {
|
||||
font-size: @menu-font-size;
|
||||
}
|
||||
|
||||
.ant-menu-item {
|
||||
font-size: @menu-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.content-card {
|
||||
margin: 0 10px 10px 10px;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { message } from 'antd';
|
||||
import { createAction } from 'redux-actions';
|
||||
import http from '../../../util/http/index';
|
||||
import { MENU_ITEM_SELECTED, MENU_LIST_SEARCH, MENU_LIST_SEARCH_FAILED, MENU_LIST_SEARCH_SUCCESS } from './constants';
|
||||
|
||||
// 查询菜单列表
|
||||
const menuListSearch = createAction(MENU_LIST_SEARCH);
|
||||
const menuListSearchSuccess = createAction(MENU_LIST_SEARCH_SUCCESS);
|
||||
const menuListSearchFailed = createAction(MENU_LIST_SEARCH_FAILED);
|
||||
export function onMenuListSearch(params) {
|
||||
return (dispatch) => {
|
||||
dispatch(menuListSearch());
|
||||
http.get('/menu/list', { params })
|
||||
.then((response) => {
|
||||
dispatch(menuListSearchSuccess(response.data.data));
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error('加载菜单列表失败');
|
||||
dispatch(menuListSearchFailed(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 选中菜单列表项
|
||||
const menuItemSelected = createAction(MENU_ITEM_SELECTED);
|
||||
export const onMenuItemSelected = (key, keyPath) => {
|
||||
return (dispatch) => {
|
||||
dispatch(menuItemSelected(key, keyPath));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
// 查询菜单列表
|
||||
export const MENU_LIST_SEARCH = 'MENU_LIST_SEARCH';
|
||||
export const MENU_LIST_SEARCH_SUCCESS = 'MENU_LIST_SEARCH_SUCCESS';
|
||||
export const MENU_LIST_SEARCH_FAILED = 'MENU_LIST_SEARCH_FAILED';
|
||||
|
||||
// 选中菜单列表项
|
||||
export const MENU_ITEM_SELECTED = 'MENU_ITEM_SELECTED';
|
|
@ -0,0 +1,33 @@
|
|||
import _ from 'lodash';
|
||||
import { MENU_ITEM_SELECTED, MENU_LIST_SEARCH, MENU_LIST_SEARCH_FAILED, MENU_LIST_SEARCH_SUCCESS } from './constants';
|
||||
|
||||
const MENU_ITEM_HOME_KEY = '0';
|
||||
|
||||
const initState = {
|
||||
loading: true,
|
||||
list: [],
|
||||
selected: {
|
||||
key: MENU_ITEM_HOME_KEY,
|
||||
keyPath: [MENU_ITEM_HOME_KEY]
|
||||
}
|
||||
};
|
||||
|
||||
const menu = (state = initState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case MENU_LIST_SEARCH: {
|
||||
return state;
|
||||
}
|
||||
case MENU_LIST_SEARCH_SUCCESS: {
|
||||
return { ...state, loading: false, list: _.get(action, ['payload'], action) };
|
||||
}
|
||||
case MENU_LIST_SEARCH_FAILED: {
|
||||
return { ...state, loading: false, list: [] };
|
||||
}
|
||||
case MENU_ITEM_SELECTED: {
|
||||
return { ...state, selected: _.get(action, ['payload'], state) };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
export default menu;
|
|
@ -0,0 +1,21 @@
|
|||
import { Icon } from 'antd';
|
||||
import React from 'react';
|
||||
import './index.less';
|
||||
|
||||
export default function Result({
|
||||
type, title, description, extra, actions, ...restProps
|
||||
}) {
|
||||
const iconMap = {
|
||||
failed: <Icon className="failed" type="close-circle" />,
|
||||
success: <Icon className="success" type="check-circle" />
|
||||
};
|
||||
return (
|
||||
<div className="result" {...restProps}>
|
||||
<div className="icon">{iconMap[type]}</div>
|
||||
<div className="title">{title}</div>
|
||||
{description && <div className="description">{description}</div>}
|
||||
{extra && <div className="extra">{extra}</div>}
|
||||
{actions && <div className="actions">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
@import "../../style/color.less";
|
||||
|
||||
.result {
|
||||
text-align: center;
|
||||
width: 72%;
|
||||
margin: 0 auto;
|
||||
|
||||
.icon {
|
||||
font-size: 72px;
|
||||
line-height: 72px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
& > .success {
|
||||
color: @success;
|
||||
}
|
||||
|
||||
& > .failed {
|
||||
color: @error;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.extra {
|
||||
background: #fafafa;
|
||||
padding: 24px 40px;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 32px;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @file 应用的 Root 容器
|
||||
* @author Zhang Peng
|
||||
* @see http://gaearon.github.io/react-hot-loader/getstarted/
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { ConnectedRouter } from 'react-router-redux';
|
||||
import { store, history, rootRouter } from '../common';
|
||||
|
||||
// 返回包裹了路由的 HCFLayout
|
||||
class WrappedContainer extends React.PureComponent {
|
||||
render() {
|
||||
const Layout = require('./Layout').default;
|
||||
return (
|
||||
<Layout>
|
||||
<Switch>
|
||||
{rootRouter.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
render={props => (
|
||||
<route.component {...props} routes={route.routes} />
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Redirect from="/" to="/home" />
|
||||
</Switch>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用的 Root 容器
|
||||
*/
|
||||
export default class RootContainer extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<Switch>
|
||||
<Route path="/" component={WrappedContainer} />
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export Result from './Result';
|
||||
export Layout from './Layout';
|
||||
export FooterToolbar from './FooterToolbar';
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @file App 的总入口
|
||||
* @author Zhang Peng
|
||||
* @see http://gaearon.github.io/react-hot-loader/getstarted/
|
||||
*/
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
import 'react-hot-loader/patch';
|
||||
import RootContainer from './component/RootContainer';
|
||||
import './style/index.less';
|
||||
|
||||
const render = (Container) => {
|
||||
ReactDOM.render(
|
||||
<AppContainer>
|
||||
<Container />
|
||||
</AppContainer>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
// 初次启动 App
|
||||
render(RootContainer);
|
||||
|
||||
// 热替换启动 App
|
||||
if (module.hot) {
|
||||
module.hot.accept('./component/RootContainer', () => {
|
||||
const NextRootContainer = require('./component/RootContainer');
|
||||
render(NextRootContainer);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.ant-btn {
|
||||
margin-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.ant-card {
|
||||
margin: 10px;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
@black: #000000;
|
||||
@white: #ffffff;
|
||||
@grey: #808080;
|
||||
|
||||
// 优雅的颜色
|
||||
@elegant-cream: #fdf6e3;
|
||||
@elegant-pink: #f1aaa6;
|
||||
@elegant-pink-dark: darken(@elegant-pink, 10%);
|
||||
@elegant-pink-light: lighten(@elegant-pink, 10%);
|
||||
@elegant-red: #c82333;
|
||||
@elegant-red-dark: darken(@elegant-red, 10%);
|
||||
@elegant-red-light: lighten(@elegant-red, 10%);
|
||||
@elegant-orange: #f28f27;
|
||||
@elegant-orange-dark: darken(@elegant-orange, 10%);
|
||||
@elegant-orange-light: lighten(@elegant-orange, 10%);
|
||||
@elegant-yellow: #ffe373;
|
||||
@elegant-yellow-dark: darken(@elegant-yellow, 10%);
|
||||
@elegant-yellow-light: lighten(@elegant-yellow, 10%);
|
||||
@elegant-green: #218838;
|
||||
@elegant-green-dark: darken(@elegant-green, 10%);
|
||||
@elegant-green-light: lighten(@elegant-green, 10%);
|
||||
@elegant-cyan: #339999;
|
||||
@elegant-cyan-dark: darken(@elegant-cyan, 10%);
|
||||
@elegant-cyan-light: lighten(@elegant-cyan, 10%);
|
||||
@elegant-blue: #0069d9;
|
||||
@elegant-blue-dark: darken(@elegant-blue, 10%);
|
||||
@elegant-blue-light: lighten(@elegant-blue, 10%);
|
||||
@elegant-purple: #563D7C;
|
||||
@elegant-purple-dark: darken(@elegant-purple, 10%);
|
||||
@elegant-purple-light: lighten(@elegant-purple, 10%);
|
||||
|
||||
@primary: #0069d9;
|
||||
@secondary: #727b84;
|
||||
@info: #138496;
|
||||
@success: #218838;
|
||||
@error: #f04134;
|
||||
@warn: #e0a800;
|
||||
@danger: #c82333;
|
||||
@light: #e2e6ea;
|
||||
@dark: #23272b;
|
|
@ -0,0 +1,13 @@
|
|||
@import "color.less";
|
||||
|
||||
@layout-header-background: @elegant-purple;
|
||||
|
||||
@menu-font-size: 16px;
|
||||
|
||||
@navpath-primary-font-size: 24px;
|
||||
@navpath-secondary-font-size: 14px;
|
||||
|
||||
@layout-content-backgroud-gray: #ecf0f5;
|
||||
@layout-content-backgroud-white: #ffffff;
|
||||
|
||||
@text-color-secondary: @secondary;
|
|
@ -0,0 +1,79 @@
|
|||
small {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.clear {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.y-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b-white {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.h-auto {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-v {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.h-v-5 {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.w-40 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.img-responsive {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*******************************************************************************
|
||||
这里的样式仅针对应用中的各个组件进行样式定制。
|
||||
各个视图页面的样式文件不要出现在 style 目录中,这样不利于各个页面样式的独立化。
|
||||
*******************************************************************************/
|
||||
/* 内部样式库 */
|
||||
@import "color.less";
|
||||
@import "global.less";
|
||||
@import "button.less";
|
||||
@import "card.less";
|
||||
@import "img.less";
|
||||
@import "scroll.less";
|
||||
|
||||
html, body, :global(#root) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* 美化webkit内核滚动条 */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #cccccc;
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* @file 封装支持 promise 的 http 请求工具
|
||||
* @author Zhang Peng
|
||||
* @see https://github.com/mzabriskie/axios
|
||||
* @see http://www.jianshu.com/p/df464b26ae58
|
||||
*/
|
||||
import qs from 'qs';
|
||||
|
||||
const DEFAULT_TIMEOUT = 3000;
|
||||
const DEFAULT_BASEURL = 'localhost:8080/react';
|
||||
|
||||
// 本项目的默认配置
|
||||
const axiosConfig = {
|
||||
// `url` 是请求的服务器地址
|
||||
// url: '/user',
|
||||
|
||||
// `method` 是请求资源的方式
|
||||
// method: 'get', //default
|
||||
|
||||
// 如果`url`不是绝对地址,那么`baseURL`将会加到`url`的前面
|
||||
// 当`url`是相对地址的时候,设置`baseURL`会非常的方便
|
||||
baseURL: DEFAULT_BASEURL,
|
||||
|
||||
// `transformRequest` 选项允许我们在请求发送到服务器之前对请求的数据做出一些改动
|
||||
// 该选项只适用于以下请求方式:`put/post/patch`
|
||||
// 数组里面的最后一个函数必须返回一个字符串、-一个`ArrayBuffer`或者`Stream`
|
||||
|
||||
transformRequest: [data => {
|
||||
// 序列化
|
||||
if (data) {
|
||||
console.log('[request after stringify] data: ', JSON.stringify(data));
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
return null;
|
||||
}],
|
||||
|
||||
// `transformResponse` 选项允许我们在数据传送到`then/catch`方法之前对数据进行改动
|
||||
transformResponse: [data => {
|
||||
// 反序列化
|
||||
if (data) {
|
||||
console.log('[response after parse] data: ', JSON.parse(data));
|
||||
return JSON.parse(data);
|
||||
}
|
||||
return null;
|
||||
}],
|
||||
|
||||
// `headers`选项是需要被发送的自定义请求头信息
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
|
||||
// `params`选项是要随请求一起发送的请求参数----一般链接在URL后面
|
||||
// 他的类型必须是一个纯对象或者是URLSearchParams对象
|
||||
// params: {
|
||||
// ID: 12345
|
||||
// },
|
||||
|
||||
// `paramsSerializer`是一个可选的函数,起作用是让参数(params)序列化
|
||||
// 例如(https://www.npmjs.com/package/qs,http://api.jquery.com/jquery.param)
|
||||
paramsSerializer(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: DEFAULT_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(progressEvent) {
|
||||
console.log('上传', progressEvent);
|
||||
},
|
||||
|
||||
// 下载进度的事件
|
||||
onDownloadProgress(progressEvent) {
|
||||
console.log('下载', progressEvent);
|
||||
},
|
||||
|
||||
// 相应内容的最大值
|
||||
maxContentLength: 2000,
|
||||
|
||||
// `validateStatus`定义了是否根据http相应状态码,来resolve或者reject promise
|
||||
// 如果`validateStatus`返回true(或者设置为`null`或者`undefined`),那么promise的状态将会是resolved,否则其状态就是rejected
|
||||
validateStatus(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) {
|
||||
// })
|
||||
};
|
||||
export default axiosConfig;
|
|
@ -0,0 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import axiosConfig from './axiosConfig';
|
||||
|
||||
// 本项目的默认配置
|
||||
// 使用默认配置初始化的请求
|
||||
const http = axios.create(axiosConfig);
|
||||
export default http;
|
|
@ -0,0 +1,5 @@
|
|||
if (process.env.NODE_ENV === 'real') {
|
||||
module.exports = require('./axiosInstance').default;
|
||||
} else {
|
||||
module.exports = require('./mock/index').default;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
code: 0,
|
||||
message: '成功',
|
||||
data: {
|
||||
result: true
|
||||
}
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
const mockAxios = axios.create();
|
||||
|
||||
// mock 数据
|
||||
const mockAdapter = new MockAdapter(mockAxios);
|
||||
mockAdapter.onGet('/menu/list').reply(200, require('./menu'));
|
||||
mockAdapter.onGet('/ui/table/basic/list').reply(200, require('./ui/table/basic'));
|
||||
mockAdapter.onPost('/api/form/save').reply(200, require('./form'));
|
||||
|
||||
export default mockAxios;
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* 供非真实环境使用的菜单项仿真数据。
|
||||
* 进入应用后,菜单栏会查询 /menu/list 接口获取菜单项数据。
|
||||
* 被 axios-mock-adapter 拦截请求,将返回以下的模拟应答数据。
|
||||
* @type {{code: number, message: string, data: [*]}}
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
code: 0,
|
||||
message: '成功',
|
||||
data: [{
|
||||
key: 'home',
|
||||
title: '首页',
|
||||
icon: 'home',
|
||||
type: 'Item',
|
||||
url: '/home',
|
||||
children: []
|
||||
}, {
|
||||
key: 'form',
|
||||
title: '表单',
|
||||
icon: 'edit',
|
||||
type: 'SubMenu',
|
||||
children: [
|
||||
{ key: '/form/basic', title: '基础表单', type: 'Item', url: '/form/basic', parent: 'form' },
|
||||
{ key: '/form/advanced', title: '高级表单', type: 'Item', url: '/form/advanced', parent: 'form' },
|
||||
{ key: '/form/step', title: '分步表单', type: 'Item', url: '/form/step', parent: 'form' }
|
||||
]
|
||||
}, {
|
||||
key: 'ui',
|
||||
title: '组件',
|
||||
icon: 'scan',
|
||||
type: 'SubMenu',
|
||||
children: [{
|
||||
key: '/ui/general',
|
||||
title: 'General',
|
||||
type: 'ItemGroup',
|
||||
parent: 'ui',
|
||||
children: [
|
||||
{ key: '/ui/general/button', title: '按钮', type: 'Item', url: '/ui/general/button', parent: '/ui/general' },
|
||||
{ key: '/ui/general/icon', title: '图标', type: 'Item', url: '/ui/general/icon', parent: '/ui/general' }
|
||||
]
|
||||
}, {
|
||||
key: '/ui/data',
|
||||
title: 'Data',
|
||||
type: 'ItemGroup',
|
||||
parent: 'ui',
|
||||
children: [
|
||||
{ key: '/ui/data/table', title: '表格', type: 'Item', url: '/ui/data/table', parent: '/ui/data' }
|
||||
]
|
||||
}, {
|
||||
key: '/ui/feedback',
|
||||
title: 'Feedback',
|
||||
type: 'ItemGroup',
|
||||
parent: 'ui',
|
||||
children: [
|
||||
{ key: '/ui/feedback/alert', title: '警告提示', type: 'Item', url: '/ui/feedback/alert', parent: '/ui/feedback' },
|
||||
{ key: '/ui/feedback/modal', title: '对话框', type: 'Item', url: '/ui/feedback/modal', parent: '/ui/feedback' }
|
||||
]
|
||||
}]
|
||||
}, {
|
||||
key: 'error',
|
||||
title: '错误',
|
||||
icon: 'exception',
|
||||
type: 'SubMenu',
|
||||
children: [
|
||||
{ key: '/error/403', title: '403', type: 'Item', url: '/error/403', parent: 'error' },
|
||||
{ key: '/error/404', title: '404', type: 'Item', url: '/error/404', parent: 'error' },
|
||||
{ key: '/error/500', title: '500', type: 'Item', url: '/error/500', parent: 'error' }
|
||||
]
|
||||
}, {
|
||||
key: 'general',
|
||||
title: '特殊页面',
|
||||
icon: 'laptop',
|
||||
type: 'SubMenu',
|
||||
children: [
|
||||
{ key: '/general/result', title: '结果', type: 'Item', url: '/general/result', parent: 'general' },
|
||||
{ key: '/general/welcome', title: '欢迎', type: 'Item', url: '/general/welcome', parent: 'general' }
|
||||
]
|
||||
}]
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* `/example/table/basic/list` 的 Mock 数据
|
||||
* @see https://github.com/nuysoft/Mock/wiki
|
||||
* @see http://mockjs.com/examples.html
|
||||
*/
|
||||
const Mock = require('mockjs');
|
||||
|
||||
const Random = Mock.Random;
|
||||
|
||||
const data = [];
|
||||
for (let i = 0; i < 46; i += 1) {
|
||||
const loginName = Random.name();
|
||||
data.push({
|
||||
id: i,
|
||||
avatar: Random.image('64x64', Random.color(), '#ffffff', loginName[0]),
|
||||
loginName,
|
||||
password: Random.name(),
|
||||
name: Random.cname(),
|
||||
idCard: Random.id(),
|
||||
birthday: Random.date('yyyy-MM-dd'),
|
||||
email: Random.email(),
|
||||
tel: `0${Random.string('number', 2, 3)}-${Random.string('number', 7, 7)}`,
|
||||
mobile: Random.string('number', 11, 11),
|
||||
address: Random.county(true),
|
||||
zip: Random.zip(),
|
||||
statu: Random.integer(0, 1),
|
||||
createTime: Random.datetime('yyyy-MM-dd HH:mm:ss'),
|
||||
updateTime: Random.datetime('yyyy-MM-dd HH:mm:ss'),
|
||||
description: Random.cparagraph(3, 10)
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
code: 0,
|
||||
message: '成功',
|
||||
data
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export number from './numberUtil';
|
||||
export http from './http';
|
|
@ -0,0 +1,34 @@
|
|||
function fixedZero(val) {
|
||||
return val * 1 < 10 ? `0${val}` : val;
|
||||
}
|
||||
|
||||
function digitUppercase(n) {
|
||||
const fraction = ['角', '分'];
|
||||
const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
|
||||
const unit = [
|
||||
['元', '万', '亿'],
|
||||
['', '拾', '佰', '仟']
|
||||
];
|
||||
let num = Math.abs(n);
|
||||
let s = '';
|
||||
fraction.forEach((item, index) => {
|
||||
s += (digit[Math.floor(num * 10 * (10 ** index)) % 10] + item).replace(/零./, '');
|
||||
});
|
||||
s = s || '整';
|
||||
num = Math.floor(num);
|
||||
for (let i = 0; i < unit[0].length && num > 0; i += 1) {
|
||||
let p = '';
|
||||
for (let j = 0; j < unit[1].length && num > 0; j += 1) {
|
||||
p = digit[num % 10] + unit[1][j] + p;
|
||||
num = Math.floor(num / 10);
|
||||
}
|
||||
s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s;
|
||||
}
|
||||
|
||||
return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fixedZero,
|
||||
digitUppercase
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Error from './Error';
|
||||
|
||||
export default () => (
|
||||
<Error type="403" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Error from './Error';
|
||||
|
||||
export default () => (
|
||||
<Error type="404" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Error from './Error';
|
||||
|
||||
export default () => (
|
||||
<Error type="500" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
|
||||
);
|
|
@ -0,0 +1,31 @@
|
|||
import { Button } from 'antd';
|
||||
import React, { createElement } from 'react';
|
||||
import './Error.less';
|
||||
import config from './errorConfig';
|
||||
|
||||
export default ({ linkElement = 'a', type, title, desc, img, actions, ...rest }) => {
|
||||
const pageType = type in config ? type : '404';
|
||||
return (
|
||||
<div className="exception" {...rest}>
|
||||
<div className="imgBlock">
|
||||
<div
|
||||
className="imgEle"
|
||||
style={{ backgroundImage: `url(${img || config[pageType].img})` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="content">
|
||||
<h1>{title || config[pageType].title}</h1>
|
||||
<div className="desc">{desc || config[pageType].desc}</div>
|
||||
<div className="actions">
|
||||
{
|
||||
actions ||
|
||||
createElement(linkElement, {
|
||||
to: '/',
|
||||
href: '/'
|
||||
}, <Button type="primary">返回首页</Button>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
.exception {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.imgBlock {
|
||||
flex: 0 0 62.5%;
|
||||
width: 62.5%;
|
||||
padding-right: 152px;
|
||||
}
|
||||
|
||||
.imgEle {
|
||||
height: 360px;
|
||||
width: 100%;
|
||||
max-width: 430px;
|
||||
float: right;
|
||||
background: no-repeat 50% 50%;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: auto;
|
||||
|
||||
h1 {
|
||||
color: #434e59;
|
||||
font-size: 72px;
|
||||
font-weight: 600;
|
||||
line-height: 72px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
button:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
const config = {
|
||||
403: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
|
||||
title: '403',
|
||||
desc: '抱歉,你无权访问该页面'
|
||||
},
|
||||
404: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
|
||||
title: '404',
|
||||
desc: '抱歉,你访问的页面不存在'
|
||||
},
|
||||
500: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
|
||||
title: '500',
|
||||
desc: '抱歉,服务器出错了'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,14 @@
|
|||
import Error403 from './403';
|
||||
import Error404 from './404';
|
||||
import Error500 from './500';
|
||||
|
||||
export default [{
|
||||
path: '/error/403',
|
||||
component: Error403
|
||||
}, {
|
||||
path: '/error/404',
|
||||
component: Error404
|
||||
}, {
|
||||
path: '/error/500',
|
||||
component: Error500
|
||||
}];
|
|
@ -0,0 +1,68 @@
|
|||
import { Button, Card } from 'antd';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { push } from 'react-router-redux';
|
||||
import { store } from '../../../common';
|
||||
import { Result } from '../../../component';
|
||||
import { advancedSelector } from '../redux/selectors';
|
||||
import './style.less';
|
||||
|
||||
class FormResult extends React.PureComponent {
|
||||
onFinish = () => {
|
||||
store.dispatch(push('/form/advanced'));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { formview } = this.props;
|
||||
let data;
|
||||
let time;
|
||||
if (formview) {
|
||||
data = formview.advanced.data;
|
||||
if (data.dateRange) {
|
||||
time = `${moment(data.dateRange[0]).format('YYYY-MM-DD')} ~ ${moment(data.dateRange[1]).format('YYYY-MM-DD')}`;
|
||||
}
|
||||
}
|
||||
const information = (
|
||||
<div className="information">
|
||||
<p>姓名:{data.name}</p>
|
||||
<p>仓库域名:{data.url}</p>
|
||||
<p>仓库管理员:{data.owner}</p>
|
||||
<p>审批人:{data.approver}</p>
|
||||
<p> 审批日期:{time}</p>
|
||||
<p>仓库类型:{data.type}</p>
|
||||
<p>...</p>
|
||||
</div>
|
||||
);
|
||||
const actions = (
|
||||
<div>
|
||||
<Button type="primary" onClick={this.onFinish}>
|
||||
再转一笔
|
||||
</Button>
|
||||
<Button>
|
||||
查看账单
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Card>
|
||||
<Result
|
||||
type="success"
|
||||
title="操作成功"
|
||||
extra={information}
|
||||
actions={actions}
|
||||
className="result"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
formview: {
|
||||
advanced: advancedSelector(state)
|
||||
}
|
||||
};
|
||||
}
|
||||
export default withRouter(connect(mapStateToProps)(FormResult));
|
|
@ -0,0 +1,237 @@
|
|||
import { Button, Input, message, Popconfirm, Table } from 'antd';
|
||||
import React, { PureComponent } from 'react';
|
||||
import './style.less';
|
||||
|
||||
export default class TableForm extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: props.value
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if ('value' in nextProps) {
|
||||
this.setState({
|
||||
data: nextProps.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getRowByKey(key) {
|
||||
return this.state.data.filter(item => item.key === key)[0];
|
||||
}
|
||||
|
||||
index = 0;
|
||||
cacheOriginData = {};
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll((err, values) => {
|
||||
if (!err) {
|
||||
this.props.dispatch({
|
||||
type: 'form/submit',
|
||||
payload: values
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
toggleEditable(e, key) {
|
||||
e.preventDefault();
|
||||
const target = this.getRowByKey(key);
|
||||
if (target) {
|
||||
// 进入编辑状态时保存原始数据
|
||||
if (!target.editable) {
|
||||
this.cacheOriginData[key] = { ...target };
|
||||
}
|
||||
target.editable = !target.editable;
|
||||
this.setState({ data: [...this.state.data] });
|
||||
}
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
const newData = this.state.data.filter(item => item.key !== key);
|
||||
this.setState({ data: newData });
|
||||
this.props.onChange(newData);
|
||||
}
|
||||
|
||||
newMember = () => {
|
||||
const newData = [...this.state.data];
|
||||
newData.push({
|
||||
key: `NEW_TEMP_ID_${this.index}`,
|
||||
workId: '',
|
||||
name: '',
|
||||
department: '',
|
||||
editable: true,
|
||||
isNew: true
|
||||
});
|
||||
this.index += 1;
|
||||
this.setState({ data: newData });
|
||||
};
|
||||
|
||||
handleKeyPress(e, key) {
|
||||
if (e.key === 'Enter') {
|
||||
this.saveRow(e, key);
|
||||
}
|
||||
}
|
||||
|
||||
handleFieldChange(e, fieldName, key) {
|
||||
const newData = [...this.state.data];
|
||||
const target = this.getRowByKey(key);
|
||||
if (target) {
|
||||
target[fieldName] = e.target.value;
|
||||
this.setState({ data: newData });
|
||||
}
|
||||
}
|
||||
|
||||
saveRow(e, key) {
|
||||
e.persist();
|
||||
// save field when blur input
|
||||
setTimeout(() => {
|
||||
if (document.activeElement.tagName === 'INPUT' &&
|
||||
document.activeElement !== e.target) {
|
||||
return;
|
||||
}
|
||||
if (this.clickedCancel) {
|
||||
this.clickedCancel = false;
|
||||
return;
|
||||
}
|
||||
const target = this.getRowByKey(key);
|
||||
if (!target.workId || !target.name || !target.department) {
|
||||
message.error('请填写完整成员信息。');
|
||||
e.target.focus();
|
||||
return;
|
||||
}
|
||||
delete target.isNew;
|
||||
this.toggleEditable(e, key);
|
||||
this.props.onChange(this.state.data);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
cancel(e, key) {
|
||||
this.clickedCancel = true;
|
||||
e.preventDefault();
|
||||
const target = this.getRowByKey(key);
|
||||
if (this.cacheOriginData[key]) {
|
||||
Object.assign(target, this.cacheOriginData[key]);
|
||||
target.editable = false;
|
||||
delete this.cacheOriginData[key];
|
||||
}
|
||||
this.setState({ data: [...this.state.data] });
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = [{
|
||||
title: '成员姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '20%',
|
||||
render: (text, record) => {
|
||||
if (record.editable) {
|
||||
return (
|
||||
<Input
|
||||
value={text}
|
||||
autoFocus
|
||||
onChange={e => this.handleFieldChange(e, 'name', record.key)}
|
||||
onBlur={e => this.saveRow(e, record.key)}
|
||||
onKeyPress={e => this.handleKeyPress(e, record.key)}
|
||||
placeholder="成员姓名"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}, {
|
||||
title: '工号',
|
||||
dataIndex: 'workId',
|
||||
key: 'workId',
|
||||
width: '20%',
|
||||
render: (text, record) => {
|
||||
if (record.editable) {
|
||||
return (
|
||||
<Input
|
||||
value={text}
|
||||
onChange={e => this.handleFieldChange(e, 'workId', record.key)}
|
||||
onBlur={e => this.saveRow(e, record.key)}
|
||||
onKeyPress={e => this.handleKeyPress(e, record.key)}
|
||||
placeholder="工号"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}, {
|
||||
title: '所属部门',
|
||||
dataIndex: 'department',
|
||||
key: 'department',
|
||||
width: '40%',
|
||||
render: (text, record) => {
|
||||
if (record.editable) {
|
||||
return (
|
||||
<Input
|
||||
value={text}
|
||||
onChange={e => this.handleFieldChange(e, 'department', record.key)}
|
||||
onBlur={e => this.saveRow(e, record.key)}
|
||||
onKeyPress={e => this.handleKeyPress(e, record.key)}
|
||||
placeholder="所属部门"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}, {
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (text, record) => {
|
||||
if (record.editable) {
|
||||
if (record.isNew) {
|
||||
return (
|
||||
<span>
|
||||
<a>保存</a>
|
||||
{' | '}
|
||||
<Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<a>保存</a>
|
||||
{' | '}
|
||||
<a onClick={e => this.cancel(e, record.key)}>取消</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<a onClick={e => this.toggleEditable(e, record.key)}>编辑</a>
|
||||
{' | '}
|
||||
<Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={this.state.data}
|
||||
pagination={false}
|
||||
/>
|
||||
<Button
|
||||
style={{ width: '100%', marginTop: 16, marginBottom: 8 }}
|
||||
type="dashed"
|
||||
onClick={this.newMember}
|
||||
icon="plus"
|
||||
>
|
||||
新增成员
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
import { Button, Card, Col, DatePicker, Form, Icon, Input, Popover, Row, Select, TimePicker } from 'antd';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { push } from 'react-router-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { store } from '../../../common';
|
||||
import { FooterToolbar } from '../../../component';
|
||||
import { onAdvancedFormPost, onAdvancedFormSubmit } from '../redux/actions';
|
||||
import { advancedSelector } from '../redux/selectors';
|
||||
import './style.less';
|
||||
import TableForm from './TableForm';
|
||||
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const fieldLabels = {
|
||||
name: '仓库名',
|
||||
url: '仓库域名',
|
||||
owner: '仓库管理员',
|
||||
approver: '审批人',
|
||||
dateRange: '生效日期',
|
||||
type: '仓库类型',
|
||||
name2: '任务名',
|
||||
url2: '任务描述',
|
||||
owner2: '执行人',
|
||||
approver2: '责任人',
|
||||
dateRange2: '生效日期',
|
||||
type2: '任务类型'
|
||||
};
|
||||
|
||||
const tableData = [{
|
||||
key: '1',
|
||||
workId: '00001',
|
||||
name: 'John Brown',
|
||||
department: 'New York No. 1 Lake Park'
|
||||
}, {
|
||||
key: '2',
|
||||
workId: '00002',
|
||||
name: 'Jim Green',
|
||||
department: 'London No. 1 Lake Park'
|
||||
}, {
|
||||
key: '3',
|
||||
workId: '00003',
|
||||
name: 'Joe Black',
|
||||
department: 'Sidney No. 1 Lake Park'
|
||||
}];
|
||||
|
||||
class AdvancedForm extends React.PureComponent {
|
||||
render() {
|
||||
const { form, submitting } = this.props;
|
||||
const { getFieldDecorator, validateFieldsAndScroll, getFieldsError } = form;
|
||||
const validate = () => {
|
||||
validateFieldsAndScroll((error, values) => {
|
||||
if (!error) {
|
||||
this.props.onAdvancedFormPost(values);
|
||||
this.props.onAdvancedFormSubmit(values);
|
||||
store.dispatch(push('/form/result'));
|
||||
}
|
||||
});
|
||||
};
|
||||
const errors = getFieldsError();
|
||||
const getErrorInfo = () => {
|
||||
const errorCount = Object.keys(errors).filter(key => errors[key]).length;
|
||||
if (!errors || errorCount === 0) {
|
||||
return null;
|
||||
}
|
||||
const scrollToField = (fieldKey) => {
|
||||
const labelNode = document.querySelector(`label[for="${fieldKey}"]`);
|
||||
if (labelNode) {
|
||||
labelNode.scrollIntoView(true);
|
||||
}
|
||||
};
|
||||
const errorList = Object.keys(errors).map((key) => {
|
||||
if (!errors[key]) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<li key={key} className="errorListItem" onClick={() => scrollToField(key)}>
|
||||
<Icon type="cross-circle-o" className="errorIcon" />
|
||||
<div className="errorMessage">{errors[key][0]}</div>
|
||||
<div className="errorField">{fieldLabels[key]}</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<span className="errorIcon">
|
||||
<Popover
|
||||
title="表单校验信息"
|
||||
content={errorList}
|
||||
overlayClassName="errorPopover"
|
||||
trigger="click"
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
>
|
||||
<Icon type="exclamation-circle" />
|
||||
</Popover>
|
||||
{errorCount}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Card title="仓库管理" className="card" bordered={false}>
|
||||
<Form layout="vertical" hideRequiredMark>
|
||||
<Row gutter={16}>
|
||||
<Col lg={6} md={12} sm={24}>
|
||||
<Form.Item label={fieldLabels.name}>
|
||||
{getFieldDecorator('name', {
|
||||
rules: [{ required: true, message: '请输入仓库名称' }]
|
||||
})(
|
||||
<Input placeholder="请输入仓库名称" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.url}>
|
||||
{getFieldDecorator('url', {
|
||||
rules: [{ required: true, message: '请选择' }]
|
||||
})(
|
||||
<Input
|
||||
style={{ width: '100%' }}
|
||||
addonBefore="http://"
|
||||
addonAfter=".com"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.owner}>
|
||||
{getFieldDecorator('owner', {
|
||||
rules: [{ required: true, message: '请选择管理员' }]
|
||||
})(
|
||||
<Select placeholder="请选择管理员">
|
||||
<Option value="xiao">付晓晓</Option>
|
||||
<Option value="mao">周毛毛</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col lg={6} md={12} sm={24}>
|
||||
<Form.Item label={fieldLabels.approver}>
|
||||
{getFieldDecorator('approver', {
|
||||
rules: [{ required: true, message: '请选择审批员' }]
|
||||
})(
|
||||
<Select placeholder="请选择审批员">
|
||||
<Option value="xiao">付晓晓</Option>
|
||||
<Option value="mao">周毛毛</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.dateRange}>
|
||||
{getFieldDecorator('dateRange', {
|
||||
rules: [{ required: true, message: '请选择生效日期' }]
|
||||
})(
|
||||
<RangePicker placeholder={['开始日期', '结束日期']} style={{ width: '100%' }} />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.type}>
|
||||
{getFieldDecorator('type', {
|
||||
rules: [{ required: true, message: '请选择仓库类型' }]
|
||||
})(
|
||||
<Select placeholder="请选择仓库类型">
|
||||
<Option value="private">私密</Option>
|
||||
<Option value="public">公开</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card title="任务管理" className="card" bordered={false}>
|
||||
<Form layout="vertical" hideRequiredMark>
|
||||
<Row gutter={16}>
|
||||
<Col lg={6} md={12} sm={24}>
|
||||
<Form.Item label={fieldLabels.name2}>
|
||||
{getFieldDecorator('name2', {
|
||||
rules: [{ required: true, message: '请输入' }]
|
||||
})(
|
||||
<Input placeholder="请输入" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.url2}>
|
||||
{getFieldDecorator('url2', {
|
||||
rules: [{ required: true, message: '请选择' }]
|
||||
})(
|
||||
<Input placeholder="请输入" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.owner2}>
|
||||
{getFieldDecorator('owner2', {
|
||||
rules: [{ required: true, message: '请选择管理员' }]
|
||||
})(
|
||||
<Select placeholder="请选择管理员">
|
||||
<Option value="xiao">付晓晓</Option>
|
||||
<Option value="mao">周毛毛</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col lg={6} md={12} sm={24}>
|
||||
<Form.Item label={fieldLabels.approver2}>
|
||||
{getFieldDecorator('approver2', {
|
||||
rules: [{ required: true, message: '请选择审批员' }]
|
||||
})(
|
||||
<Select placeholder="请选择审批员">
|
||||
<Option value="xiao">付晓晓</Option>
|
||||
<Option value="mao">周毛毛</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.dateRange2}>
|
||||
{getFieldDecorator('dateRange2', {
|
||||
rules: [{ required: true, message: '请输入' }]
|
||||
})(
|
||||
<TimePicker
|
||||
placeholder="提醒时间"
|
||||
style={{ width: '100%' }}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
|
||||
<Form.Item label={fieldLabels.type2}>
|
||||
{getFieldDecorator('type2', {
|
||||
rules: [{ required: true, message: '请选择仓库类型' }]
|
||||
})(
|
||||
<Select placeholder="请选择仓库类型">
|
||||
<Option value="private">私密</Option>
|
||||
<Option value="public">公开</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card title="成员管理" className="card" bordered={false}>
|
||||
{getFieldDecorator('members', {
|
||||
initialValue: tableData
|
||||
})(<TableForm />)}
|
||||
</Card>
|
||||
<FooterToolbar>
|
||||
{getErrorInfo()}
|
||||
<Button type="primary" onClick={validate} loading={submitting}>
|
||||
提交
|
||||
</Button>
|
||||
</FooterToolbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
formview: {
|
||||
advanced: advancedSelector(state)
|
||||
}
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onAdvancedFormPost: bindActionCreators(onAdvancedFormPost, dispatch),
|
||||
onAdvancedFormSubmit: bindActionCreators(onAdvancedFormSubmit, dispatch)
|
||||
};
|
||||
}
|
||||
const WrappedAdvancedForm = Form.create()(AdvancedForm);
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(WrappedAdvancedForm));
|
|
@ -0,0 +1,90 @@
|
|||
@import "../../../style/default.less";
|
||||
|
||||
.card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.steps {
|
||||
max-width: 750px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
cursor: pointer;
|
||||
color: @error;
|
||||
margin-right: 24px;
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.errorPopover {
|
||||
:global {
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
max-height: 290px;
|
||||
overflow: auto;
|
||||
min-width: 256px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errorListItem {
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
&:hover {
|
||||
background: @primary;
|
||||
}
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
.errorIcon {
|
||||
color: @error;
|
||||
float: left;
|
||||
margin-top: 4px;
|
||||
margin-right: 12px;
|
||||
padding-bottom: 22px;
|
||||
}
|
||||
.errorField {
|
||||
font-size: 12px;
|
||||
color: @secondary;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.editable {
|
||||
td {
|
||||
padding-top: 13px !important;
|
||||
padding-bottom: 12.5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// custom footer for fixed footer toolbar
|
||||
.advancedForm + div {
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.advancedForm {
|
||||
:global {
|
||||
.ant-form .ant-row:last-child .ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.ant-table td {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.optional {
|
||||
color: @text-color-secondary;
|
||||
font-style: normal;
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Cascader,
|
||||
DatePicker,
|
||||
Form,
|
||||
Icon,
|
||||
Input,
|
||||
message,
|
||||
Radio,
|
||||
Select,
|
||||
Slider,
|
||||
Tooltip,
|
||||
Upload
|
||||
} from 'antd';
|
||||
import React from 'react';
|
||||
import area from '../../util/area.json';
|
||||
import './BasicForm.less';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const Option = Select.Option;
|
||||
const RadioGroup = Radio.Group;
|
||||
|
||||
class BasicForm extends React.Component {
|
||||
state = {
|
||||
currentRecord: {}
|
||||
};
|
||||
|
||||
onChange = (key, value) => {
|
||||
console.log('onChange', key, value);
|
||||
const temp = this.state.currentRecord;
|
||||
temp[key] = value;
|
||||
this.setState({
|
||||
currentRecord: temp
|
||||
});
|
||||
};
|
||||
|
||||
onChangeArea = (value, selectedOptions) => {
|
||||
console.log('选择的省市区数据', value, selectedOptions);
|
||||
};
|
||||
|
||||
normFile = (e) => {
|
||||
console.log('Upload event:', e);
|
||||
if (Array.isArray(e)) {
|
||||
return e;
|
||||
}
|
||||
return e && e.fileList;
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
message.error('提交失败,请完善表单信息');
|
||||
} else {
|
||||
message.info('提交成功');
|
||||
console.log('提交的表单数据', values);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const prefixSelector = getFieldDecorator('prefix', {
|
||||
initialValue: '86'
|
||||
})(
|
||||
<Select style={{ width: 60 }}>
|
||||
<Option value="86">+86</Option>
|
||||
<Option value="87">+87</Option>
|
||||
</Select>
|
||||
);
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 6 }
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 15 }
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Card noHovering bordered={false}>
|
||||
<Alert
|
||||
showIcon
|
||||
closable
|
||||
message="提示"
|
||||
type="info"
|
||||
description="具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。"
|
||||
/>
|
||||
<br />
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="头像"
|
||||
extra="只能上传图片"
|
||||
>
|
||||
{getFieldDecorator('avatar', {
|
||||
valuePropName: 'fileList',
|
||||
getValueFromEvent: this.normFile
|
||||
})(
|
||||
<Upload name="avatar" action="/upload.do" listType="picture">
|
||||
<Button>
|
||||
<Icon type="upload" /> 点击上传
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
</FormItem>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="昵称"
|
||||
hasFeedback
|
||||
>
|
||||
{getFieldDecorator('nickname', {
|
||||
rules: [
|
||||
{ required: true, message: '请输入昵称!' }
|
||||
]
|
||||
})(
|
||||
<Input
|
||||
prefix={<Icon type="user" style={{ fontSize: 14 }} />}
|
||||
onChange={e => this.onChange('nickname', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="邮箱"
|
||||
hasFeedback
|
||||
>
|
||||
{getFieldDecorator('email', {
|
||||
rules: [
|
||||
{ required: true, message: '请输入邮箱!' },
|
||||
{ type: 'email', message: '输入内容不是有效的邮箱!' }
|
||||
]
|
||||
})(
|
||||
<Input onChange={e => this.onChange('email', e.target.value)} />
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="手机号"
|
||||
>
|
||||
{getFieldDecorator('mobile', {
|
||||
rules: [{ required: true, message: '请输入手机号!' }]
|
||||
})(
|
||||
<Input
|
||||
addonBefore={prefixSelector}
|
||||
style={{ width: '100%' }}
|
||||
onChange={e => this.onChange('mobile', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="性别"
|
||||
>
|
||||
{getFieldDecorator('sex', {
|
||||
rules: [
|
||||
{ required: true, message: '请选择性别' }
|
||||
]
|
||||
})(
|
||||
<RadioGroup onChange={e => this.onChange('sex', e.target.value)}>
|
||||
<Radio value="1">男</Radio>
|
||||
<Radio value="2">女</Radio>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="出生日期"
|
||||
>
|
||||
{getFieldDecorator('birthday', {
|
||||
rules: [
|
||||
{ required: true, message: '请选择出生日期!' }
|
||||
]
|
||||
})(
|
||||
<DatePicker showTime format="YYYY-MM-DD" onOk={value => this.onChange('birthday', value)} />
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="省市区"
|
||||
>
|
||||
{getFieldDecorator('area', {
|
||||
rules: [
|
||||
{ required: true, message: '请选择省市区!' }
|
||||
]
|
||||
})(
|
||||
<Cascader options={area} onChange={this.onChangeArea} placeholder="请选择省市区" />
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="兴趣"
|
||||
>
|
||||
{getFieldDecorator('hobby', {})(
|
||||
<Select mode="multiple" placeholder="请选择你的兴趣">
|
||||
<Option value="0">科技</Option>
|
||||
<Option value="1">文化</Option>
|
||||
<Option value="2">时政</Option>
|
||||
<Option value="3">军事</Option>
|
||||
<Option value="4">娱乐</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="证件照片"
|
||||
>
|
||||
<div className="dropbox">
|
||||
{getFieldDecorator('dragger', {
|
||||
valuePropName: 'fileList',
|
||||
getValueFromEvent: this.normFile
|
||||
})(
|
||||
<Upload.Dragger name="files" action="/upload.do">
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖拽到指定区域来上传文件</p>
|
||||
</Upload.Dragger>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label={
|
||||
<span>
|
||||
权重
|
||||
<Tooltip title="权重">
|
||||
<Icon type="question" style={{ marginRight: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{getFieldDecorator('power')(
|
||||
<Slider marks={{ 0: 'A', 20: 'B', 40: 'C', 60: 'D', 80: 'E', 100: 'F' }} />
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
wrapperCol={{ span: 12, offset: 6 }}
|
||||
>
|
||||
<Button type="primary" htmlType="submit">提交</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
const WrappedBasicForm = Form.create()(BasicForm);
|
||||
export default WrappedBasicForm;
|
|
@ -0,0 +1,24 @@
|
|||
.ant-form-item {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.avatar-uploader,
|
||||
.avatar-uploader-trigger,
|
||||
.avatar {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
display: block;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-uploader-trigger {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
font-size: 28px;
|
||||
color: #999999;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { Button, Form, Input, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { push } from 'react-router-redux';
|
||||
import { store } from '../../../common';
|
||||
import './style.less';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default class Step1 extends React.PureComponent {
|
||||
render() {
|
||||
const { formItemLayout, form } = this.props;
|
||||
const { getFieldDecorator, validateFields } = form;
|
||||
const onValidateForm = () => {
|
||||
validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.props.onStepFormFirst(values);
|
||||
store.dispatch(push('/form/step/second'));
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Form layout="horizontal" className="stepForm" hideRequiredMark>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="付款账户"
|
||||
>
|
||||
{getFieldDecorator('payAccount', {
|
||||
initialValue: 'ant-design@alipay.com',
|
||||
rules: [{ required: true, message: '请选择付款账户' }]
|
||||
})(
|
||||
<Select placeholder="test@example.com">
|
||||
<Option value="ant-design@alipay.com">ant-design@alipay.com</Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="收款账户"
|
||||
>
|
||||
<Input.Group compact>
|
||||
<Select defaultValue="alipay" style={{ width: 80 }}>
|
||||
<Option value="alipay">支付宝</Option>
|
||||
<Option value="bank">银行账户</Option>
|
||||
</Select>
|
||||
{getFieldDecorator('receiverAccount', {
|
||||
initialValue: 'test@example.com',
|
||||
rules: [
|
||||
{ required: true, message: '请输入收款人账户' },
|
||||
{ type: 'email', message: '账户名应为邮箱格式' }
|
||||
]
|
||||
})(
|
||||
<Input style={{ width: 'calc(100% - 80px)' }} placeholder="test@example.com" />
|
||||
)}
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="收款人姓名"
|
||||
>
|
||||
{getFieldDecorator('receiverName', {
|
||||
initialValue: 'Alex',
|
||||
rules: [{ required: true, message: '请输入收款人姓名' }]
|
||||
})(
|
||||
<Input placeholder="请输入收款人姓名" />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="转账金额"
|
||||
>
|
||||
{getFieldDecorator('amount', {
|
||||
initialValue: '500',
|
||||
rules: [
|
||||
{ required: true, message: '请输入转账金额' },
|
||||
{ pattern: /^(\d+)((?:\.\d+)?)$/, message: '请输入合法金额数字' }
|
||||
]
|
||||
})(
|
||||
<Input prefix="¥" placeholder="请输入金额" />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
xs: { span: 24, offset: 0 },
|
||||
sm: { span: formItemLayout.wrapperCol.span, offset: formItemLayout.labelCol.span }
|
||||
}}
|
||||
label=""
|
||||
>
|
||||
<Button type="primary" onClick={onValidateForm}>
|
||||
下一步
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<hr />
|
||||
<div className="desc">
|
||||
<h3>说明</h3>
|
||||
<h4>转账到支付宝账户</h4>
|
||||
<p>如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。</p>
|
||||
<h4>转账到银行卡</h4>
|
||||
<p>如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { Alert, Button, Form, Input } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { push } from 'react-router-redux';
|
||||
import { store } from '../../../common';
|
||||
import { number } from '../../../util';
|
||||
import './style.less';
|
||||
|
||||
export default class Step2 extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
const { formview } = this.props;
|
||||
if (_.isEmpty(formview.step.firstData)) {
|
||||
this.onPrev();
|
||||
}
|
||||
}
|
||||
|
||||
onPrev = () => {
|
||||
store.dispatch(push('/form/step'));
|
||||
};
|
||||
|
||||
onNext = () => {
|
||||
store.dispatch(push('/form/step/third'));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { formItemLayout, form, formview } = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
const onValidateForm = (e) => {
|
||||
e.preventDefault();
|
||||
form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.props.onStepFormSecond(values);
|
||||
const submitData = _.merge(formview.step.firstData, formview.step.secondData);
|
||||
this.props.onStepFormSubmit(submitData);
|
||||
this.onNext();
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Form layout="horizontal" className="stepForm">
|
||||
<Alert
|
||||
closable
|
||||
showIcon
|
||||
message="确认转账后,资金将直接打入对方账户,无法退回。"
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
className="stepFormText"
|
||||
label="付款账户"
|
||||
>
|
||||
{formview.step.firstData.payAccount}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
className="stepFormText"
|
||||
label="收款账户"
|
||||
>
|
||||
{formview.step.firstData.receiverAccount}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
className="stepFormText"
|
||||
label="收款人姓名"
|
||||
>
|
||||
{formview.step.firstData.receiverName}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
className="stepFormText"
|
||||
label="转账金额"
|
||||
>
|
||||
<span className="money">{formview.step.firstData.amount}</span>
|
||||
<span className="uppercase">({number.digitUppercase(formview.step.firstData.amount)})</span>
|
||||
</Form.Item>
|
||||
<hr />
|
||||
<br />
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="支付密码"
|
||||
required={false}
|
||||
>
|
||||
{getFieldDecorator('password', {
|
||||
initialValue: '123456',
|
||||
rules: [{
|
||||
required: true, message: '需要支付密码才能进行支付'
|
||||
}]
|
||||
})(
|
||||
<Input type="password" autoComplete="off" style={{ width: '80%' }} />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ marginBottom: 8 }}
|
||||
wrapperCol={{
|
||||
xs: { span: 24, offset: 0 },
|
||||
sm: { span: formItemLayout.wrapperCol.span, offset: formItemLayout.labelCol.span }
|
||||
}}
|
||||
label=""
|
||||
>
|
||||
<Button type="primary" onClick={onValidateForm} loading={this.props.submitting}>
|
||||
提交
|
||||
</Button>
|
||||
<Button onClick={this.onPrev} style={{ marginLeft: 8 }}>
|
||||
上一步
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { Button, Col, Row } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { push } from 'react-router-redux';
|
||||
import { store } from '../../../common';
|
||||
import { Result } from '../../../component';
|
||||
import './style.less';
|
||||
|
||||
export default class Step3 extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
const { formview } = this.props;
|
||||
if (_.isEmpty(formview.step.firstData) || _.isEmpty(formview.step.secondData)) {
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
onFinish = () => {
|
||||
store.dispatch(push('/form/step'));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { formview } = this.props;
|
||||
const information = (
|
||||
<div className="information">
|
||||
<Row>
|
||||
<Col span={8} className="label">付款账户:</Col>
|
||||
<Col span={16}>{formview.step.firstData.payAccount}</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={8} className="label">收款账户:</Col>
|
||||
<Col span={16}>{formview.step.firstData.receiverAccount}</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={8} className="label">收款人姓名:</Col>
|
||||
<Col span={16}>{formview.step.firstData.receiverName}</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={8} className="label">转账金额:</Col>
|
||||
<Col span={16}><span className="money">{formview.step.firstData.amount}</span> 元</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
const actions = (
|
||||
<div>
|
||||
<Button type="primary" onClick={this.onFinish}>
|
||||
再转一笔
|
||||
</Button>
|
||||
<Button>
|
||||
查看账单
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Result
|
||||
type="success"
|
||||
title="操作成功"
|
||||
description="预计两小时内到账"
|
||||
extra={information}
|
||||
actions={actions}
|
||||
className="result"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { Card, Form, Steps } from 'antd';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { onStepFormFirst, onStepFormSecond, onStepFormSubmit } from '../redux/actions';
|
||||
import { stepSelector } from '../redux/selectors';
|
||||
import Step1 from './Step1';
|
||||
import Step2 from './Step2';
|
||||
import Step3 from './Step3';
|
||||
import './style.less';
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
class StepForm extends React.PureComponent {
|
||||
getCurrentStep() {
|
||||
const { location } = this.props;
|
||||
const { pathname } = location;
|
||||
const pathList = pathname.split('/');
|
||||
switch (pathList[pathList.length - 1]) {
|
||||
case 'step':
|
||||
return 0;
|
||||
case 'second':
|
||||
return 1;
|
||||
case 'third':
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentComponent() {
|
||||
const componentMap = {
|
||||
0: Step1,
|
||||
1: Step2,
|
||||
2: Step3
|
||||
};
|
||||
return componentMap[this.getCurrentStep()];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { form, formview } = this.props;
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
span: 5
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 19
|
||||
}
|
||||
};
|
||||
const CurrentComponent = this.getCurrentComponent();
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<div>
|
||||
<Steps current={this.getCurrentStep()} className="steps">
|
||||
<Step title="填写转账信息" />
|
||||
<Step title="确认转账信息" />
|
||||
<Step title="完成" />
|
||||
</Steps>
|
||||
<CurrentComponent
|
||||
{...this.props}
|
||||
formItemLayout={formItemLayout}
|
||||
form={form}
|
||||
data={formview.step}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
formview: {
|
||||
step: stepSelector(state)
|
||||
}
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onStepFormSubmit: bindActionCreators(onStepFormSubmit, dispatch),
|
||||
onStepFormFirst: bindActionCreators(onStepFormFirst, dispatch),
|
||||
onStepFormSecond: bindActionCreators(onStepFormSecond, dispatch)
|
||||
};
|
||||
}
|
||||
const WrappedStepForm = Form.create()(StepForm);
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(WrappedStepForm));
|
|
@ -0,0 +1,68 @@
|
|||
@import "../../../style/default.less";
|
||||
|
||||
.stepForm {
|
||||
margin: 40px auto 0;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.stepFormText {
|
||||
margin-bottom: 24px;
|
||||
:global {
|
||||
.ant-form-item-label,
|
||||
.ant-form-item-control {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
margin: 0 auto;
|
||||
max-width: 560px;
|
||||
padding: 24px 0 8px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
padding: 0 56px;
|
||||
color: @text-color-secondary;
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 12px 0;
|
||||
color: @text-color-secondary;
|
||||
line-height: 32px;
|
||||
}
|
||||
h4 {
|
||||
margin: 0 0 4px 0;
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.information {
|
||||
line-height: 22px;
|
||||
:global {
|
||||
.ant-row:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.money {
|
||||
font-family: Helvetica Neue;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
font-size: 12px;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { message } from 'antd';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { http } from '../../../util';
|
||||
import {
|
||||
STEP_FORM_STEP1,
|
||||
STEP_FORM_STEP2,
|
||||
STEP_FORM_SUBMIT,
|
||||
STEP_FORM_SUBMIT_FAILED,
|
||||
STEP_FORM_SUBMIT_SUCCESS,
|
||||
ADVANCED_FORM_POST,
|
||||
ADVANCED_FORM_SUBMIT,
|
||||
ADVANCED_FORM_SUBMIT_SUCCESS,
|
||||
ADVANCED_FORM_SUBMIT_FAILED
|
||||
} from './constants';
|
||||
|
||||
export const onStepFormFirst = createAction(STEP_FORM_STEP1);
|
||||
export const onStepFormSecond = createAction(STEP_FORM_STEP2);
|
||||
const stepFormSubmit = createAction(STEP_FORM_SUBMIT);
|
||||
const stepFormSubmitSuccess = createAction(STEP_FORM_SUBMIT_SUCCESS);
|
||||
const stepFormSubmitFailed = createAction(STEP_FORM_SUBMIT_FAILED);
|
||||
export function onStepFormSubmit(params) {
|
||||
return (dispatch) => {
|
||||
dispatch(stepFormSubmit());
|
||||
http.post('/api/form/save', params)
|
||||
.then((response) => {
|
||||
dispatch(stepFormSubmitSuccess(response.data.data));
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error('请求失败');
|
||||
dispatch(stepFormSubmitFailed(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const onAdvancedFormPost = createAction(ADVANCED_FORM_POST);
|
||||
const advancedFormSubmit = createAction(ADVANCED_FORM_SUBMIT);
|
||||
const advancedFormSubmitSuccess = createAction(ADVANCED_FORM_SUBMIT_SUCCESS);
|
||||
const advancedFormSubmitFailed = createAction(ADVANCED_FORM_SUBMIT_FAILED);
|
||||
export function onAdvancedFormSubmit(params) {
|
||||
return (dispatch) => {
|
||||
dispatch(advancedFormSubmit());
|
||||
http.post('/api/form/save', params)
|
||||
.then((response) => {
|
||||
dispatch(advancedFormSubmitSuccess(response.data.data));
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error('请求失败');
|
||||
dispatch(advancedFormSubmitFailed(error));
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export const STEP_FORM_STEP1 = 'STEP_FORM_STEP1';
|
||||
export const STEP_FORM_STEP2 = 'STEP_FORM_STEP2';
|
||||
export const STEP_FORM_SUBMIT = 'STEP_FORM_SUBMIT';
|
||||
export const STEP_FORM_SUBMIT_SUCCESS = 'STEP_FORM_SUBMIT_SUCCESS';
|
||||
export const STEP_FORM_SUBMIT_FAILED = 'STEP_FORM_SUBMIT_FAILED';
|
||||
|
||||
export const ADVANCED_FORM_POST = 'ADVANCED_FORM_POST';
|
||||
export const ADVANCED_FORM_SUBMIT = 'ADVANCED_FORM_SUBMIT';
|
||||
export const ADVANCED_FORM_SUBMIT_SUCCESS = 'ADVANCED_FORM_SUBMIT_SUCCESS';
|
||||
export const ADVANCED_FORM_SUBMIT_FAILED = 'ADVANCED_FORM_SUBMIT_FAILED';
|
|
@ -0,0 +1,72 @@
|
|||
import _ from 'lodash';
|
||||
import { combineReducers } from 'redux';
|
||||
import {
|
||||
ADVANCED_FORM_SUBMIT,
|
||||
ADVANCED_FORM_SUBMIT_FAILED,
|
||||
ADVANCED_FORM_SUBMIT_SUCCESS,
|
||||
STEP_FORM_STEP1,
|
||||
STEP_FORM_STEP2,
|
||||
ADVANCED_FORM_POST,
|
||||
STEP_FORM_SUBMIT,
|
||||
STEP_FORM_SUBMIT_FAILED,
|
||||
STEP_FORM_SUBMIT_SUCCESS
|
||||
} from './constants';
|
||||
|
||||
const stepState = {
|
||||
firstData: {},
|
||||
secondData: {},
|
||||
result: false
|
||||
};
|
||||
const step = (state = stepState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case STEP_FORM_STEP1: {
|
||||
return { ...state, firstData: _.get(action, ['payload'], action) };
|
||||
}
|
||||
case STEP_FORM_STEP2: {
|
||||
return { ...state, secondData: _.get(action, ['payload'], action) };
|
||||
}
|
||||
case STEP_FORM_SUBMIT: {
|
||||
return state;
|
||||
}
|
||||
case STEP_FORM_SUBMIT_SUCCESS: {
|
||||
const ret = _.get(action, ['payload'], action);
|
||||
return { ...state, result: ret.result };
|
||||
}
|
||||
case STEP_FORM_SUBMIT_FAILED: {
|
||||
return { ...state };
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const advancedState = {
|
||||
data: {},
|
||||
result: false
|
||||
};
|
||||
const advanced = (state = advancedState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case ADVANCED_FORM_POST: {
|
||||
return { ...state, data: _.get(action, ['payload'], action) };
|
||||
}
|
||||
case ADVANCED_FORM_SUBMIT: {
|
||||
return state;
|
||||
}
|
||||
case ADVANCED_FORM_SUBMIT_SUCCESS: {
|
||||
const ret = _.get(action, ['payload'], action);
|
||||
return { ...state, result: ret.result };
|
||||
}
|
||||
case ADVANCED_FORM_SUBMIT_FAILED: {
|
||||
return { ...state };
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formview = combineReducers({
|
||||
step, advanced
|
||||
});
|
||||
export default formview;
|
|
@ -0,0 +1,5 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
export const formviewSelector = state => state.formview;
|
||||
export const stepSelector = createSelector(formviewSelector, formview => (formview.step));
|
||||
export const advancedSelector = createSelector(formviewSelector, formview => (formview.advanced));
|
|
@ -0,0 +1,30 @@
|
|||
import AdvancedForm from './AdvancedForm';
|
||||
import FormResult from './AdvancedForm/FormResult';
|
||||
import BasicForm from './BasicForm';
|
||||
import StepForm from './StepForm';
|
||||
import Step1 from './StepForm/Step1';
|
||||
import Step2 from './StepForm/Step2';
|
||||
import Step3 from './StepForm/Step3';
|
||||
|
||||
export default [{
|
||||
path: '/form/basic',
|
||||
component: BasicForm
|
||||
}, {
|
||||
path: '/form/advanced',
|
||||
component: AdvancedForm
|
||||
}, {
|
||||
path: '/form/result',
|
||||
component: FormResult
|
||||
}, {
|
||||
path: '/form/step',
|
||||
component: StepForm
|
||||
}, {
|
||||
path: '/form/step/first',
|
||||
component: Step1
|
||||
}, {
|
||||
path: '/form/step/second',
|
||||
component: Step2
|
||||
}, {
|
||||
path: '/form/step/third',
|
||||
component: Step3
|
||||
}];
|
|
@ -0,0 +1,32 @@
|
|||
import { Button, Icon } from 'antd';
|
||||
import React from 'react';
|
||||
import { Result } from '../../../component';
|
||||
|
||||
const extra = (
|
||||
<div>
|
||||
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.85)', fontWeight: '500', marginBottom: 16 }}>
|
||||
您提交的内容有如下错误:
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />您的账户已被冻结
|
||||
<a style={{ marginLeft: 16 }}>立即解冻 <Icon type="right" /></a>
|
||||
</div>
|
||||
<div>
|
||||
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />您的账户还不具备申请资格
|
||||
<a style={{ marginLeft: 16 }}>立即升级 <Icon type="right" /></a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const actions = <Button type="primary">返回修改</Button>;
|
||||
|
||||
export default () => (
|
||||
<Result
|
||||
type="failed"
|
||||
title="提交失败"
|
||||
description="请核对并修改以下信息后,再重新提交。"
|
||||
extra={extra}
|
||||
actions={actions}
|
||||
style={{ marginTop: 48, marginBottom: 16 }}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,73 @@
|
|||
import { Button, Col, Icon, Row, Steps } from 'antd';
|
||||
import React from 'react';
|
||||
import { Result } from '../../../component';
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
const desc1 = (
|
||||
<div style={{ fontSize: 12, color: 'rgba(0, 0, 0, 0.45)', position: 'relative', left: 42 }}>
|
||||
<div style={{ margin: '8px 0 4px' }}>
|
||||
曲丽丽<Icon style={{ marginLeft: 8 }} type="dingding-o" />
|
||||
</div>
|
||||
<div>2016-12-12 12:32</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const desc2 = (
|
||||
<div style={{ fontSize: 12, position: 'relative', left: 42 }}>
|
||||
<div style={{ margin: '8px 0 4px' }}>
|
||||
周毛毛<Icon type="dingding-o" style={{ color: '#00A0E9', marginLeft: 8 }} />
|
||||
</div>
|
||||
<div><a href="">催一下</a></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const extra = (
|
||||
<div>
|
||||
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.85)', fontWeight: '500', marginBottom: 20 }}>
|
||||
项目名称
|
||||
</div>
|
||||
<Row style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>项目 ID:</span>
|
||||
23421
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>负责人:</span>
|
||||
曲丽丽
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={12}>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>生效时间:</span>
|
||||
2016-12-12 ~ 2017-12-12
|
||||
</Col>
|
||||
</Row>
|
||||
<Steps style={{ marginLeft: -42, width: 'calc(100% + 84px)' }} progressDot current={1}>
|
||||
<Step title={<span style={{ fontSize: 14 }}>创建项目</span>} description={desc1} />
|
||||
<Step title={<span style={{ fontSize: 14 }}>部门初审</span>} description={desc2} />
|
||||
<Step title={<span style={{ fontSize: 14 }}>财务复核</span>} />
|
||||
<Step title={<span style={{ fontSize: 14 }}>完成</span>} />
|
||||
</Steps>
|
||||
</div>
|
||||
);
|
||||
|
||||
const actions = (
|
||||
<div>
|
||||
<Button type="primary">返回列表</Button>
|
||||
<Button>查看项目</Button>
|
||||
<Button>打 印</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default () => (
|
||||
<Result
|
||||
type="success"
|
||||
title="提交成功"
|
||||
description="提交结果页用于反馈一系列操作任务的处理结果,
|
||||
如果仅是简单操作,使用 Message 全局提示反馈即可。
|
||||
本文字区域可以展示简单的补充说明,如果有类似展示
|
||||
“单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。"
|
||||
extra={extra}
|
||||
actions={actions}
|
||||
style={{ marginTop: 48, marginBottom: 16 }}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,15 @@
|
|||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import Failed from './Failed';
|
||||
import Success from './Success';
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Card noHovering bordered={false} title="成功示例">
|
||||
<Success />
|
||||
</Card>
|
||||
<Card noHovering bordered={false} title="失败示例">
|
||||
<Failed />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import './index.less';
|
||||
import logo from './logo.svg';
|
||||
|
||||
export default () => (
|
||||
<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,13 @@
|
|||
import ResultView from './Result';
|
||||
import WelcomeView from './Welcome';
|
||||
|
||||
export default [{
|
||||
path: '/home',
|
||||
component: WelcomeView
|
||||
}, {
|
||||
path: '/general/welcome',
|
||||
component: WelcomeView
|
||||
}, {
|
||||
path: '/general/result',
|
||||
component: ResultView
|
||||
}];
|
|
@ -0,0 +1,83 @@
|
|||
import { Alert, Card, Col, Row } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
class AlertView extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Card title="顶部公告">
|
||||
<Alert message="Warning text" banner />
|
||||
<br />
|
||||
<Alert message="Very long warning text warning text text text text text text text" banner closable />
|
||||
<br />
|
||||
<Alert showIcon={false} message="Warning text without icon" banner />
|
||||
<br />
|
||||
<Alert type="error" message="Error text" banner />
|
||||
</Card>
|
||||
<Row gutter={16}>
|
||||
<Col xs={12}>
|
||||
<Card title="图标">
|
||||
<Alert message="Success Tips" type="success" showIcon />
|
||||
<Alert message="Informational Notes" type="info" showIcon />
|
||||
<Alert message="Warning" type="warning" showIcon />
|
||||
<Alert message="Error" type="error" showIcon />
|
||||
<Alert
|
||||
message="success tips"
|
||||
description="Detailed description and advices about successful copywriting."
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
<Alert
|
||||
message="Informational Notes"
|
||||
description="Additional description and informations about copywriting."
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
<Alert
|
||||
message="Warning"
|
||||
description="This is a warning notice about copywriting."
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
<Alert
|
||||
message="Error"
|
||||
description="This is an error message about copywriting."
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="含有辅助性文字介绍">
|
||||
<Alert
|
||||
message="Success Text"
|
||||
description="Success Description Success Description Success Description"
|
||||
type="success"
|
||||
closeText="Close Now"
|
||||
/>
|
||||
<Alert
|
||||
message="Info Text"
|
||||
description="Info Description Info Description Info Description Info Description"
|
||||
type="info"
|
||||
closeText="Close Now"
|
||||
/>
|
||||
<Alert
|
||||
message="Warning Text"
|
||||
description="Warning Description Warning Description Warning Description Warning Description"
|
||||
type="warning"
|
||||
closeText="Close Now"
|
||||
/>
|
||||
<Alert
|
||||
message="Error Text"
|
||||
description="Error Description Error Description Error Description Error Description"
|
||||
type="error"
|
||||
closeText="Close Now"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default AlertView;
|
|
@ -0,0 +1,165 @@
|
|||
import { Button, Card, Col, Dropdown, Icon, Menu, Radio, Row } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
class ButtonView extends React.PureComponent {
|
||||
state = {
|
||||
size: 'large',
|
||||
loading: false,
|
||||
iconLoading: false
|
||||
};
|
||||
|
||||
handleSizeChange = (e) => {
|
||||
this.setState({ size: e.target.value });
|
||||
};
|
||||
|
||||
enterLoading = () => {
|
||||
this.setState({ loading: true });
|
||||
};
|
||||
|
||||
enterIconLoading = () => {
|
||||
this.setState({ iconLoading: true });
|
||||
};
|
||||
|
||||
handleMenuClick = (e) => {
|
||||
console.log('click', e);
|
||||
};
|
||||
|
||||
render() {
|
||||
const size = this.state.size;
|
||||
const menu = (
|
||||
<Menu onClick={this.handleMenuClick}>
|
||||
<Menu.Item key="1">1st item</Menu.Item>
|
||||
<Menu.Item key="2">2nd item</Menu.Item>
|
||||
<Menu.Item key="3">3rd item</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col xs={12}>
|
||||
<Card title="按钮类型">
|
||||
<Button type="primary">Primary</Button>
|
||||
<Button>Default</Button>
|
||||
<Button type="dashed">Dashed</Button>
|
||||
<Button type="danger">Danger</Button>
|
||||
</Card>
|
||||
<Card title="图标按钮">
|
||||
<Button type="primary" shape="circle" icon="search" />
|
||||
<Button type="primary" icon="search">Search</Button>
|
||||
<Button shape="circle" icon="search" />
|
||||
<Button icon="search">Search</Button>
|
||||
<br />
|
||||
<Button shape="circle" icon="search" />
|
||||
<Button icon="search">Search</Button>
|
||||
<Button type="dashed" shape="circle" icon="search" />
|
||||
<Button type="dashed" icon="search">Search</Button>
|
||||
</Card>
|
||||
<Card title="按钮尺寸">
|
||||
<Radio.Group value={size} onChange={this.handleSizeChange}>
|
||||
<Radio.Button value="large">Large</Radio.Button>
|
||||
<Radio.Button value="default">Default</Radio.Button>
|
||||
<Radio.Button value="small">Small</Radio.Button>
|
||||
</Radio.Group>
|
||||
<br /> <br />
|
||||
<Button type="primary" size={size}>Primary</Button>
|
||||
<Button size={size}>Normal</Button>
|
||||
<Button type="dashed" size={size}>Dashed</Button>
|
||||
<Button type="danger" size={size}>Danger</Button>
|
||||
<br />
|
||||
<Button type="primary" shape="circle" icon="download" size={size} />
|
||||
<Button type="primary" icon="download" size={size}>Download</Button>
|
||||
<br />
|
||||
<Button.Group size={size}>
|
||||
<Button type="primary">
|
||||
<Icon type="left" />Backward
|
||||
</Button>
|
||||
<Button type="primary">
|
||||
Forward<Icon type="right" />
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Card>
|
||||
<Card title="多个按钮组合">
|
||||
<Button type="primary">primary</Button>
|
||||
<Button>secondary</Button>
|
||||
<Dropdown overlay={menu}>
|
||||
<Button>
|
||||
more <Icon type="down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="幽灵按钮" style={{ background: 'rgb(190, 200, 200)' }}>
|
||||
<Button type="primary" ghost>Primary</Button>
|
||||
<Button ghost>Default</Button>
|
||||
<Button type="dashed" ghost>Dashed</Button>
|
||||
<Button type="danger" ghost>danger</Button>
|
||||
</Card>
|
||||
<Card title="不可用状态">
|
||||
<Button type="primary">Primary</Button>
|
||||
<Button>Default</Button>
|
||||
<Button type="dashed">Dashed</Button>
|
||||
<Button type="danger">Danger</Button>
|
||||
<br /><br />
|
||||
<Button type="primary" disabled>Primary(disabled)</Button>
|
||||
<Button disabled>Default(disabled)</Button>
|
||||
<Button disabled>Ghost(disabled)</Button>
|
||||
<Button type="dashed" disabled>Dashed(disabled)</Button>
|
||||
</Card>
|
||||
<Card title="加载中状态">
|
||||
<Button type="primary" loading>
|
||||
Loading
|
||||
</Button>
|
||||
<Button type="primary" size="small" loading>
|
||||
Loading
|
||||
</Button>
|
||||
<br /><br />
|
||||
<Button type="primary" loading={this.state.loading} onClick={this.enterLoading}>
|
||||
Click me!
|
||||
</Button>
|
||||
<Button type="primary" icon="poweroff" loading={this.state.iconLoading} onClick={this.enterIconLoading}>
|
||||
Click me!
|
||||
</Button>
|
||||
<br />
|
||||
<Button shape="circle" loading />
|
||||
<Button type="primary" shape="circle" loading />
|
||||
</Card>
|
||||
<Card title="按钮组合">
|
||||
<h4>Basic</h4>
|
||||
<Button.Group>
|
||||
<Button>Cancel</Button>
|
||||
<Button type="primary">OK</Button>
|
||||
</Button.Group>
|
||||
<Button.Group>
|
||||
<Button disabled>L</Button>
|
||||
<Button disabled>M</Button>
|
||||
<Button disabled>R</Button>
|
||||
</Button.Group>
|
||||
<Button.Group>
|
||||
<Button type="primary">L</Button>
|
||||
<Button>M</Button>
|
||||
<Button>M</Button>
|
||||
<Button type="dashed">R</Button>
|
||||
</Button.Group>
|
||||
|
||||
<h4>With Icon</h4>
|
||||
<Button.Group>
|
||||
<Button type="primary">
|
||||
<Icon type="left" />Go back
|
||||
</Button>
|
||||
<Button type="primary">
|
||||
Go forward<Icon type="right" />
|
||||
</Button>
|
||||
</Button.Group>
|
||||
<Button.Group>
|
||||
<Button type="primary" icon="cloud" />
|
||||
<Button type="primary" icon="cloud-download" />
|
||||
</Button.Group>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default ButtonView;
|
|
@ -0,0 +1,69 @@
|
|||
import { Card, Col, Icon, Row } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
const directionIcons = ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt',
|
||||
'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle',
|
||||
'left-circle', 'right-circle', 'up-circle-o', 'down-circle-o', 'right-circle-o', 'left-circle-o', 'double-right',
|
||||
'double-left', 'verticle-left', 'verticle-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap',
|
||||
'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'play-circle-o',
|
||||
'up-square', 'down-square', 'left-square', 'right-square', 'up-square-o', 'down-square-o', 'left-square-o',
|
||||
'right-square-o', 'login', 'logout', 'menu-fold', 'menu-unfold'];
|
||||
const suggestionIcons = ['question', 'question-circle-o', 'question-circle', 'plus', 'plus-circle-o', 'plus-circle',
|
||||
'pause', 'pause-circle-o', 'pause-circle', 'minus', 'minus-circle-o', 'minus-circle', 'plus-square', 'plus-square-o',
|
||||
'minus-square', 'minus-square-o', 'info', 'info-circle-o', 'info-circle', 'exclamation', 'exclamation-circle-o',
|
||||
'exclamation-circle', 'close', 'close-circle', 'close-circle-o', 'close-square', 'close-square-o', 'check',
|
||||
'check-circle', 'check-circle-o', 'check-square', 'check-square-o', 'clock-circle-o', 'clock-circle'];
|
||||
const otherIcons = ['lock', 'unlock', 'area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'bars', 'book', 'calendar',
|
||||
'cloud', 'cloud-download', 'code', 'code-o', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'edit',
|
||||
'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-excel', 'file-jpg', 'file-ppt', 'file-add',
|
||||
'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'frown-o', 'meh', 'meh-o', 'smile', 'smile-o', 'inbox',
|
||||
'laptop', 'appstore-o', 'appstore', 'line-chart', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture',
|
||||
'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tag-o', 'tags', 'tags-o',
|
||||
'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload-o',
|
||||
'cloud-download-o', 'cloud-upload', 'cloud-o', 'star-o', 'star', 'heart-o', 'heart', 'environment', 'environment-o',
|
||||
'eye', 'eye-o', 'camera', 'camera-o', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export',
|
||||
'customer-service', 'qrcode', 'scan', 'like', 'like-o', 'dislike', 'dislike-o', 'message', 'pay-circle',
|
||||
'pay-circle-o', 'calculator', 'pushpin', 'pushpin-o', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect',
|
||||
'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool',
|
||||
'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman',
|
||||
'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet',
|
||||
'bank', 'trophy', 'contacts', 'global', 'shake', 'api'];
|
||||
const logoIcons = ['android', 'android-o', 'apple', 'apple-o', 'windows', 'windows-o', 'ie', 'chrome', 'github',
|
||||
'aliwangwang', 'aliwangwang-o', 'dingding', 'dingding-o'];
|
||||
|
||||
const getIconList = (data, title, colorStr) => {
|
||||
const groups = _.chunk(data, 6);
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
<p style={{ fontSize: 24, fontWeight: 500, color: colorStr }}>
|
||||
{title}
|
||||
</p>
|
||||
</span>
|
||||
{groups.map((row, i) => (
|
||||
<Row key={`row-${title}-${i}`} gutter={8}>
|
||||
{row.map(icon => (
|
||||
<Col key={icon} span={4}>
|
||||
<Card style={{ textAlign: 'center' }}>
|
||||
<Icon type={icon} style={{ fontSize: 24, color: colorStr }} /><br />
|
||||
<span style={{ fontSize: 12, color: colorStr }}>{icon}</span>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
))}
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IconView = () => (
|
||||
<Card title="图标示例">
|
||||
{getIconList(directionIcons, '方向性图标', '#de6f64')}
|
||||
{getIconList(suggestionIcons, '提示建议性图标', '#399ab2')}
|
||||
{getIconList(otherIcons, '网站通用图标', '#346093')}
|
||||
{getIconList(logoIcons, '品牌和标识', '#78943a')}
|
||||
</Card>
|
||||
);
|
||||
export default IconView;
|
|
@ -0,0 +1,218 @@
|
|||
import { Button, Card, Modal } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const confirm = Modal.confirm;
|
||||
|
||||
class ModalView extends React.PureComponent {
|
||||
state = {
|
||||
visible: false,
|
||||
ModalText2: 'Content of the modal dialog',
|
||||
visible2: false,
|
||||
loading3: false,
|
||||
visible3: false,
|
||||
modal1Visible: false,
|
||||
modal2Visible: false
|
||||
};
|
||||
|
||||
setModal1Visible = (modal1Visible) => {
|
||||
this.setState({ modal1Visible });
|
||||
};
|
||||
setModal2Visible = (modal2Visible) => {
|
||||
this.setState({ modal2Visible });
|
||||
};
|
||||
showModal = () => {
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
};
|
||||
handleOk = (e) => {
|
||||
console.log(e);
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
};
|
||||
handleCancel = (e) => {
|
||||
console.log(e);
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
};
|
||||
showModal2 = () => {
|
||||
this.setState({
|
||||
visible2: true
|
||||
});
|
||||
};
|
||||
handleOk2 = () => {
|
||||
this.setState({
|
||||
ModalText2: 'The modal dialog will be closed after two seconds',
|
||||
confirmLoading2: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
visible2: false,
|
||||
confirmLoading2: false
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
handleCancel2 = () => {
|
||||
console.log('Clicked cancel button');
|
||||
this.setState({
|
||||
visible2: false
|
||||
});
|
||||
};
|
||||
showModal3 = () => {
|
||||
this.setState({
|
||||
visible3: true
|
||||
});
|
||||
};
|
||||
handleOk3 = () => {
|
||||
this.setState({ loading3: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ loading3: false, visible3: false });
|
||||
}, 3000);
|
||||
};
|
||||
handleCancel3 = () => {
|
||||
this.setState({ visible3: false });
|
||||
};
|
||||
showConfirm4 = () => {
|
||||
confirm({
|
||||
title: 'Want to delete these items?',
|
||||
content: 'some descriptions',
|
||||
onOk() {
|
||||
console.log('OK');
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
}
|
||||
});
|
||||
};
|
||||
info = () => {
|
||||
Modal.info({
|
||||
title: 'This is a notification message',
|
||||
content: (
|
||||
<div>
|
||||
<p>some messages...some messages...</p>
|
||||
<p>some messages...some messages...</p>
|
||||
</div>
|
||||
),
|
||||
onOk() {}
|
||||
});
|
||||
};
|
||||
success = () => {
|
||||
Modal.success({
|
||||
title: 'This is a success message',
|
||||
content: 'some messages...some messages...'
|
||||
});
|
||||
};
|
||||
error = () => {
|
||||
Modal.error({
|
||||
title: 'This is an error message',
|
||||
content: 'some messages...some messages...'
|
||||
});
|
||||
};
|
||||
warning = () => {
|
||||
Modal.warning({
|
||||
title: 'This is a warning message',
|
||||
content: 'some messages...some messages...'
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Card title="对话框示例">
|
||||
<p>
|
||||
<Button type="primary" onClick={this.showModal}>基本用法</Button>
|
||||
<Modal
|
||||
title="Basic Modal"
|
||||
visible={this.state.visible}
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
>
|
||||
<p>some contents...</p>
|
||||
<p>some contents...</p>
|
||||
<p>some contents...</p>
|
||||
</Modal>
|
||||
</p>
|
||||
<p>
|
||||
<Button type="primary" onClick={this.showModal2}>异步关闭</Button>
|
||||
<Modal
|
||||
title="Title of the modal dialog"
|
||||
visible={this.state.visible2}
|
||||
onOk={this.handleOk2}
|
||||
confirmLoading={this.state.confirmLoading2}
|
||||
onCancel={this.handleCancel2}
|
||||
>
|
||||
<p>{this.state.ModalText2}</p>
|
||||
</Modal>
|
||||
</p>
|
||||
<p>
|
||||
<Button type="primary" onClick={this.showModal3}>
|
||||
自定义页脚
|
||||
</Button>
|
||||
<Modal
|
||||
visible={this.state.visible3}
|
||||
title="Title"
|
||||
onOk={this.handleOk3}
|
||||
onCancel={this.handleCancel3}
|
||||
footer={[
|
||||
<Button key="back" size="large" onClick={this.handleCancel3}>Return</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={this.state.loading3}
|
||||
onClick={this.handleOk3}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
</Modal>
|
||||
</p>
|
||||
<p>
|
||||
<Button onClick={this.showConfirm4}>
|
||||
确认框
|
||||
</Button>
|
||||
</p>
|
||||
<p>
|
||||
<Button onClick={this.info}>信息提示</Button>
|
||||
<Button onClick={this.success}>成功</Button>
|
||||
<Button onClick={this.error}>失败</Button>
|
||||
<Button onClick={this.warning}>警告</Button>
|
||||
</p>
|
||||
<p>
|
||||
<Button type="primary" onClick={() => this.setModal1Visible(true)}>距离顶部20px</Button>
|
||||
<Modal
|
||||
title="20px to Top"
|
||||
style={{ top: 20 }}
|
||||
visible={this.state.modal1Visible}
|
||||
onOk={() => this.setModal1Visible(false)}
|
||||
onCancel={() => this.setModal1Visible(false)}
|
||||
>
|
||||
<p>some contents...</p>
|
||||
<p>some contents...</p>
|
||||
<p>some contents...</p>
|
||||
</Modal>
|
||||
<Button type="primary" onClick={() => this.setModal2Visible(true)}>垂直居中</Button>
|
||||
<Modal
|
||||
title="Vertically centered modal dialog"
|
||||
wrapClassName="vertical-center-modal"
|
||||
visible={this.state.modal2Visible}
|
||||
onOk={() => this.setModal2Visible(false)}
|
||||
onCancel={() => this.setModal2Visible(false)}
|
||||
>
|
||||
<p>some contents...</p>
|
||||
<p>some contents...</p>
|
||||
<p>some contents...</p>
|
||||
</Modal>
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default ModalView;
|
|
@ -0,0 +1,182 @@
|
|||
import { Avatar, Card, Col, Row, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import onPersonListSearch from './redux/actions';
|
||||
|
||||
class BasicTable extends React.PureComponent {
|
||||
state = {
|
||||
selectedRowKeys: [],
|
||||
filteredInfo: null,
|
||||
sortedInfo: null
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.onPersonListSearch();
|
||||
}
|
||||
|
||||
onSelectChange = (selectedRowKeys) => {
|
||||
console.log('selectedRowKeys changed: ', selectedRowKeys);
|
||||
this.setState({ selectedRowKeys });
|
||||
};
|
||||
|
||||
onChange = (pagination, filters, sorter) => {
|
||||
console.log('onChange parameters', pagination, filters, sorter);
|
||||
this.setState({
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter
|
||||
});
|
||||
};
|
||||
|
||||
onExpand = (record) => {
|
||||
console.log('onExpand', record);
|
||||
const nodes = [];
|
||||
_.forIn(record, (value, key) => {
|
||||
nodes.push({ key, value });
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<Card width="100%" title="详细信息" style={{ fontSize: '16px' }}>
|
||||
{nodes.map((node) => {
|
||||
return (
|
||||
<Row key={node.key}>
|
||||
<Col span={6}>
|
||||
<h3 style={{ fontWeight: '600' }}>{node.key}</h3>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<span>{node.value}</span>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
clearAll = () => {
|
||||
this.setState({
|
||||
filteredInfo: null,
|
||||
sortedInfo: null
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedRowKeys } = this.state;
|
||||
const { table } = this.props;
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: this.onSelectChange,
|
||||
hideDefaultSelections: true,
|
||||
selections: [{
|
||||
key: 'all-data',
|
||||
text: 'Select All Data',
|
||||
onSelect: () => {
|
||||
this.setState({
|
||||
selectedRowKeys: [...Array(46).keys()] // 0...45
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'odd',
|
||||
text: 'Select Odd Row',
|
||||
onSelect: (changableRowKeys) => {
|
||||
let newSelectedRowKeys = [];
|
||||
newSelectedRowKeys = changableRowKeys.filter((key, index) => {
|
||||
if (index % 2 !== 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.setState({ selectedRowKeys: newSelectedRowKeys });
|
||||
}
|
||||
}, {
|
||||
key: 'even',
|
||||
text: 'Select Even Row',
|
||||
onSelect: (changableRowKeys) => {
|
||||
let newSelectedRowKeys = [];
|
||||
newSelectedRowKeys = changableRowKeys.filter((key, index) => {
|
||||
if (index % 2 !== 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.setState({ selectedRowKeys: newSelectedRowKeys });
|
||||
}
|
||||
}],
|
||||
onSelection: this.onSelection
|
||||
};
|
||||
let { sortedInfo } = this.state;
|
||||
sortedInfo = sortedInfo || {};
|
||||
const columns = [{
|
||||
title: '头像',
|
||||
dataIndex: 'avatar',
|
||||
key: 'avatar',
|
||||
render(text, record) {
|
||||
return (<Avatar shape="square" size="large" src={record.avatar} icon="user" />);
|
||||
}
|
||||
}, {
|
||||
title: '登录名',
|
||||
dataIndex: 'loginName',
|
||||
key: 'loginName',
|
||||
sorter: (a, b) => a.loginName.length - b.loginName.length,
|
||||
sortOrder: sortedInfo.columnKey === 'loginName' && sortedInfo.order
|
||||
}, {
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: (a, b) => a.name.length - b.name.length,
|
||||
sortOrder: sortedInfo.columnKey === 'name' && sortedInfo.order
|
||||
}, {
|
||||
title: '生日',
|
||||
dataIndex: 'birthday',
|
||||
key: 'birthday',
|
||||
sorter: (a, b) => a.birthday - b.birthday,
|
||||
sortOrder: sortedInfo.columnKey === 'birthday' && sortedInfo.order
|
||||
}, {
|
||||
title: '邮件',
|
||||
dataIndex: 'email',
|
||||
key: 'email'
|
||||
}, {
|
||||
title: '电话',
|
||||
dataIndex: 'tel',
|
||||
key: 'tel'
|
||||
}, {
|
||||
title: '手机',
|
||||
dataIndex: 'mobile',
|
||||
key: 'mobile'
|
||||
}, {
|
||||
title: '地址',
|
||||
dataIndex: 'address',
|
||||
key: 'address'
|
||||
}];
|
||||
return (
|
||||
<div>
|
||||
<Card title="基本表格示例">
|
||||
<Table
|
||||
rowKey="loginName"
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={table.list}
|
||||
loading={table.loading}
|
||||
onChange={this.onChange}
|
||||
expandedRowRender={this.onExpand}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
table: state.table
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onPersonListSearch: bindActionCreators(onPersonListSearch, dispatch)
|
||||
};
|
||||
}
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(BasicTable));
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import BasicTable from './BasicTable';
|
||||
|
||||
class TableView extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<BasicTable />
|
||||
);
|
||||
}
|
||||
}
|
||||
export default TableView;
|
|
@ -0,0 +1,25 @@
|
|||
import { message } from 'antd';
|
||||
import { createAction } from 'redux-actions';
|
||||
import http from '../../../../util/http';
|
||||
import {
|
||||
EXAMPLE_BASIC_TABLE_LIST_SEARCH,
|
||||
EXAMPLE_BASIC_TABLE_LIST_SEARCH_FAILED,
|
||||
EXAMPLE_BASIC_TABLE_LIST_SEARCH_SUCCESS
|
||||
} from './constants';
|
||||
|
||||
const basicTableListSearch = createAction(EXAMPLE_BASIC_TABLE_LIST_SEARCH);
|
||||
const basicTableListSearchSuccess = createAction(EXAMPLE_BASIC_TABLE_LIST_SEARCH_SUCCESS);
|
||||
const basicTableListSearchFailed = createAction(EXAMPLE_BASIC_TABLE_LIST_SEARCH_FAILED);
|
||||
export default function onBasicTableListSearch(params) {
|
||||
return (dispatch) => {
|
||||
dispatch(basicTableListSearch());
|
||||
http.get('/ui/table/basic/list', { params })
|
||||
.then((response) => {
|
||||
dispatch(basicTableListSearchSuccess(response.data.data));
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error('查询表格数据失败');
|
||||
dispatch(basicTableListSearchFailed(error));
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const EXAMPLE_BASIC_TABLE_LIST_SEARCH = 'EXAMPLE_BASIC_TABLE_LIST_SEARCH';
|
||||
export const EXAMPLE_BASIC_TABLE_LIST_SEARCH_SUCCESS = 'EXAMPLE_BASIC_TABLE_LIST_SEARCH_SUCCESS';
|
||||
export const EXAMPLE_BASIC_TABLE_LIST_SEARCH_FAILED = 'EXAMPLE_BASIC_TABLE_LIST_SEARCH_FAILED';
|
|
@ -0,0 +1,28 @@
|
|||
import _ from 'lodash';
|
||||
import {
|
||||
EXAMPLE_BASIC_TABLE_LIST_SEARCH,
|
||||
EXAMPLE_BASIC_TABLE_LIST_SEARCH_FAILED,
|
||||
EXAMPLE_BASIC_TABLE_LIST_SEARCH_SUCCESS
|
||||
} from './constants';
|
||||
|
||||
const initState = {
|
||||
loading: true,
|
||||
list: []
|
||||
};
|
||||
|
||||
const table = (state = initState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case EXAMPLE_BASIC_TABLE_LIST_SEARCH: {
|
||||
return state;
|
||||
}
|
||||
case EXAMPLE_BASIC_TABLE_LIST_SEARCH_SUCCESS: {
|
||||
return { ...state, loading: false, list: _.get(action, ['payload'], action) };
|
||||
}
|
||||
case EXAMPLE_BASIC_TABLE_LIST_SEARCH_FAILED: {
|
||||
return { ...state, loading: false, list: [] };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
export default table;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue