🔖 添加 reactadmin 应用实例

pull/2/head
Zhang Peng 2018-10-12 19:10:04 +08:00
parent e6e9884def
commit eeb32fdb84
103 changed files with 13158 additions and 0 deletions

View File

@ -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" ]
}
}
}

View File

@ -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 }] // 导入的模块可以解析为本地文件系统上的模块
}
};

View File

@ -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;

View File

@ -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';
}
};

View File

@ -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))};`;
}
};

View File

@ -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'))
};

View File

@ -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代码。
// 例如ifprocess.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
}
};

View File

@ -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)
// 但是,我们的输出结构使用 cssjs 和 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代码。
// 例如ifprocess.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'
}
};

View File

@ -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());
}
};
};

View File

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

View File

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

View File

@ -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"
}

View File

@ -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/'));
});

View File

@ -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);
});

View File

@ -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);
}
);

View File

@ -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);
}
);

View File

@ -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);

View File

@ -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;

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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} />&nbsp;
<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));

View File

@ -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));

View File

@ -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;

View File

@ -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;

View File

@ -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));

View File

@ -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;
}

View File

@ -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));
};
};

View File

@ -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';

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;
}
}
}

View File

@ -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>
);
}
}

View File

@ -0,0 +1,3 @@
export Result from './Result';
export Layout from './Layout';
export FooterToolbar from './FooterToolbar';

View File

@ -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);
});
}

View File

@ -0,0 +1,4 @@
.ant-btn {
margin-left: 10px;
margin-bottom: 10px;
}

View File

@ -0,0 +1,3 @@
.ant-card {
margin: 10px;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
.img-responsive {
width: 100%;
height: auto;
}

View File

@ -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;
}

View File

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

View File

@ -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;

View File

@ -0,0 +1,7 @@
import axios from 'axios';
import axiosConfig from './axiosConfig';
// 本项目的默认配置
// 使用默认配置初始化的请求
const http = axios.create(axiosConfig);
export default http;

View File

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

View File

@ -0,0 +1,7 @@
module.exports = {
code: 0,
message: '成功',
data: {
result: true
}
};

View File

@ -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;

View File

@ -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' }
]
}]
};

View File

@ -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
};

View File

@ -0,0 +1,2 @@
export number from './numberUtil';
export http from './http';

View File

@ -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
};

View File

@ -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} />
);

View File

@ -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} />
);

View File

@ -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} />
);

View File

@ -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>
);
};

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -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
}];

View File

@ -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));

View File

@ -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>
);
}
}

View File

@ -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));

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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"
/>
);
}
}

View File

@ -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));

View File

@ -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;
}

View File

@ -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));
});
};
}

View File

@ -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';

View File

@ -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;

View File

@ -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));

View File

@ -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
}];

View File

@ -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 }}
/>
);

View File

@ -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 }}
/>
);

View File

@ -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>
);

View File

@ -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>
);

View File

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

View File

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

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,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
}];

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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));

View File

@ -0,0 +1,11 @@
import React from 'react';
import BasicTable from './BasicTable';
class TableView extends React.PureComponent {
render() {
return (
<BasicTable />
);
}
}
export default TableView;

View File

@ -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));
});
};
}

View File

@ -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';

View File

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