diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..edae12c --- /dev/null +++ b/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + ["es2015", {"modules": false}], + "stage-2", + "react" + ], + "plugins": [ + "react-hot-loader/babel", + ["import", { "libraryName": "antd", "style": "css" }] + ] +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2a1e422 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,49 @@ +{ + "extends": "airbnb", + "ecmaFeatures": { + "jsx": true, + "modules": true, + }, + "env": { + "browser": true, + "node": true, + }, + "parser": "babel-eslint", + "plugins": [ + "react", + "import", + "jsx-a11y", + ], + "rules": { + // 缩进设置 + "indent": ["error", 4], + "react/jsx-indent": ["error", 4], + // 设置jsx中属性的缩进 + "react/jsx-indent-props": ["error", 4], + // jsx语法中属性使用双引号 + "jsx-quotes": ["error", "prefer-double"], + // 设置换行符的类型 + "linebreak-style": ["error", "unix"], + "func-names": [2, "as-needed"], + "quotes": [2, "single"], + "strict": [2, "never"], + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/react-in-jsx-scope": 2, + // 设置非交互元素不能有鼠标 键盘事件 + "jsx-a11y/no-static-element-interactions": "off", + // 设置PropTypes不可设置的类型 + "react/forbid-prop-types": ["error", { forbid: ["any"] }], + // 设置过滤加载的包 + "import/no-extraneous-dependencies": ["error", { devDependencies: ["*.js", "build/*.js"] }], + // 设置阴影变量 + "no-shadow": ["error", { "builtinGlobals": false, "hoist": "never", "allow": [] }], + }, + "settings": { + "import/resolver": { + "webpack": { + "config": "build/webpack.config.dev.js" + } + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db29d02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +.DS_Store +dist +*.swp diff --git a/build/path.js b/build/path.js new file mode 100644 index 0000000..931a7c8 --- /dev/null +++ b/build/path.js @@ -0,0 +1,56 @@ +var path = require('path'); + +// 根路径 +var ROOT_PATH = path.resolve(__dirname, '../'); + +// 源码路径 +var SRC_PATH = path.resolve(ROOT_PATH, 'src'); + +// 产出路径 +var DIST_PATH = path.resolve(ROOT_PATH, 'dist'); + +// 模块路径 +var NODE_MODULES_PATH = path.resolve(ROOT_PATH, 'node_modules'); + +// 通用组件路径 +var COMPONENT_PATH = path.resolve(SRC_PATH, 'components'); + +// 业务组件路径 +var CONTAINERS_PATH = path.resolve(SRC_PATH, 'containers'); + +// actions路径 +var ACTIONS_PATH = path.resolve(SRC_PATH, 'actions'); + +// reducers路径 +var REDUCERS_PATH = path.resolve(SRC_PATH, 'reducers'); + +// constants路径 +var CONSTANTS_PATH = path.resolve(SRC_PATH, 'constants'); + +// assets路径 +var ASSETS_PATH = path.resolve(SRC_PATH, 'assets'); + +// html tpl 路径 +var HTML_TPL_PATH = path.resolve(ROOT_PATH, 'index.html'); + +// 入口文件路径 +var IndexPath = path.resolve(SRC_PATH, 'index.jsx'); + +// 图标路径 +var faviconPath = path.resolve(ROOT_PATH, 'favicon.ico'); + +module.exports = { + ROOT_PATH, + SRC_PATH, + DIST_PATH, + NODE_MODULES_PATH, + COMPONENT_PATH, + CONTAINERS_PATH, + ACTIONS_PATH, + REDUCERS_PATH, + CONSTANTS_PATH, + ASSETS_PATH, + HTML_TPL_PATH, + IndexPath, + faviconPath, +}; diff --git a/build/webpack.config.dev.js b/build/webpack.config.dev.js new file mode 100644 index 0000000..9d2ed9e --- /dev/null +++ b/build/webpack.config.dev.js @@ -0,0 +1,153 @@ +var webpack = require('webpack'); +var chalk = require('chalk'); +var { + ROOT_PATH, + SRC_PATH, + DIST_PATH, + NODE_MODULES_PATH, + COMPONENT_PATH, + CONTAINERS_PATH, + ACTIONS_PATH, + REDUCERS_PATH, + CONSTANTS_PATH, + ASSETS_PATH, + IndexPath, +} = require('./path'); + +var port = 3001; + +console.log(chalk.green( + ` + info: + 端口 ${port} + 开发配置 + ` +)); + +module.exports = { + context: ROOT_PATH, + + entry: [ + 'react-hot-loader/patch', + + `webpack-dev-server/client?http://localhost:${port}`, + + 'webpack/hot/only-dev-server', + + IndexPath, + ], + + output: { + filename: '[name].js', + + path: DIST_PATH, + + publicPath: '/public/', + }, + + devtool: 'inline-source-map', + + module: { + rules: [{ + test: /\.jsx?$/, + use: [ + 'babel-loader', + ], + exclude: /node_modules/, + }, { + test: /\.css$/, + use: [ + "style-loader", + "css-loader", + ], + }, { + test: /\.styl$/, + use: [ + "style-loader", + "css-loader", + "stylus-loader" + ], + }, { + test: /\.(gif|jpe?g|png)$/, + include: SRC_PATH, + use: [{ + loader: 'url-loader', + options: { + limit: 10000, + }, + }, ], + }, { + test: /\.(woff|woff2)$/, + use: [{ + loader: 'url-loader', + options: { + limit: 20000, + }, + }, ], + }, { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader' + }, { + test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, + use: 'file-loader' + }, { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader' + }, + // { + // test: /\.jsx?$/, + // include: SRC_PATH, + // enforce: 'pre', + // use: [ + // { + // loader: 'eslint-loader', + // options: { + // emitWarning: true, + // }, + // }, + // ], + // } + ], + }, + + resolve: { + alias: { + Components: COMPONENT_PATH, + Containers: CONTAINERS_PATH, + Actions: ACTIONS_PATH, + Reducers: REDUCERS_PATH, + Constants: CONSTANTS_PATH, + Assets: ASSETS_PATH, + }, + extensions: [".js", ".json", ".jsx", ".css", ".scss", ], + }, + + plugins: [ + new webpack.HotModuleReplacementPlugin(), + // enable HMR globally + + new webpack.NamedModulesPlugin(), + // prints more readable module names in the browser console on HMR updates + + new webpack.NoEmitOnErrorsPlugin(), + // do not emit compiled assets that include errors + ], + + devServer: { + port: port, + host: '0.0.0.0', + historyApiFallback: true, + stats: { + colors: true + }, + proxy: { + '/api/*': { + target: 'http://127.0.0.1:8888', + secure: false, + headers: { + Host: '127.0.0.1' + } + } + } + } +}; diff --git a/build/webpack.config.production.js b/build/webpack.config.production.js new file mode 100644 index 0000000..9025e9e --- /dev/null +++ b/build/webpack.config.production.js @@ -0,0 +1,141 @@ +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const { + ROOT_PATH, + SRC_PATH, + DIST_PATH, + NODE_MODULES_PATH, + COMPONENT_PATH, + CONTAINERS_PATH, + ACTIONS_PATH, + REDUCERS_PATH, + CONSTANTS_PATH, + ASSETS_PATH, + HTML_TPL_PATH, + IndexPath, + faviconPath, +} = require('./path'); +const extractCSS = new ExtractTextPlugin('css/[name]-one.css'); +const extractSTYL = new ExtractTextPlugin('css/[name]-two.css'); +const extractSASS = new ExtractTextPlugin('css/[name]-tree.css'); + +const port = 3000; + +module.exports = { + context: ROOT_PATH, + + entry: { + main: IndexPath, + }, + + output: { + filename: '[name].js', + + path: DIST_PATH, + + publicPath: '/public/', + }, + + module: { + rules: [ + { + test: /\.jsx?$/, + use: [ + 'babel-loader', + ], + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: extractCSS .extract([ + "css-loader", + ]), + }, + { + test: /\.styl$/, + use: extractSTYL .extract([ + "css-loader", + "stylus-loader", + ]), + }, + { + test: /\.(gif|jpe?g|png)$/, + include: SRC_PATH, + use: [ + { + loader: 'url-loader', + options: { + limit: 10000, + }, + }, + ], + }, + { + test: /\.(woff|woff2)$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 20000, + }, + }, + ], + }, + { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader' + }, + { + test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, + use: 'file-loader' + }, + { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + use: 'url-loader' + }, + ], + }, + + resolve: { + alias: { + Components: COMPONENT_PATH, + Containers: CONTAINERS_PATH, + Actions: ACTIONS_PATH, + Reducers: REDUCERS_PATH, + Constants: CONSTANTS_PATH, + Assets: ASSETS_PATH, + }, + extensions: [".js", ".json", ".jsx", ".css", ".scss",], + }, + + plugins: [ + extractCSS, + + extractSTYL, + + extractSASS, + + new webpack.HotModuleReplacementPlugin(), + + new webpack.NamedModulesPlugin(), + + new webpack.NoEmitOnErrorsPlugin(), + + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor',// 指定公共 bundle 的名字。 + minChunks: function (module) { + // 该配置假定你引入的 vendor 存在于 node_modules 目录中 + return module.context && module.context.indexOf('node_modules') !== -1; + } + }), + + new HtmlWebpackPlugin({ + filename: 'index.html', + template: HTML_TPL_PATH, + inject: 'body', + favicon: faviconPath, + chunks: ['manifest', 'vendor', 'main'] + }), + ], +}; diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..96b8e8d Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..c226f87 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + 前端博客 | 专注技术分享 + + + + +
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d33ef6 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "blog", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "webpack-dev-server --config ./build/webpack.config.dev.js", + "clear": "rimraf ./dist", + "build": "cross-env NODE_ENV=production webpack -p --config ./build/webpack.config.production.js", + "lint": "eslint src --ext \".js,.jsx\" --no-ignore .eslintrc.js", + "lint:fix": "eslint --fix src --ext \".js,.jsx\" --no-ignore .eslintrc.js" + }, + "author": "Zhaohang Hao", + "license": "ISC", + "devDependencies": { + "babel-core": "^6.24.0", + "babel-eslint": "^7.1.1", + "babel-loader": "^6.4.0", + "babel-plugin-import": "^1.1.1", + "babel-preset-es2015": "^6.24.0", + "babel-preset-react": "^6.23.0", + "babel-preset-stage-2": "^6.22.0", + "chalk": "^1.1.3", + "cross-env": "^3.2.4", + "css-loader": "^0.27.3", + "eslint": "^3.17.1", + "eslint-config-airbnb": "^14.1.0", + "eslint-import-resolver-webpack": "^0.8.1", + "eslint-loader": "^1.6.3", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^4.0.0", + "eslint-plugin-react": "^6.10.0", + "extract-text-webpack-plugin": "^2.1.0", + "file-loader": "^0.10.1", + "html-webpack-plugin": "^2.28.0", + "rimraf": "^2.6.1", + "style-loader": "^0.14.1", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "url-loader": "^0.5.8", + "webpack": "^2.2.1", + "webpack-dev-server": "^2.4.2" + }, + "dependencies": { + "antd": "^2.9.0", + "babel-polyfill": "^6.23.0", + "moment": "^2.18.1", + "normalize.css": "^6.0.0", + "react": "^15.4.2", + "react-dom": "^15.4.2", + "react-hot-loader": "^3.0.0-beta.6", + "react-redux": "^5.0.3", + "react-router": "3.0.2", + "react-router-redux": "^4.0.8", + "react-thunk": "^1.0.0", + "redux": "^3.6.0", + "redux-thunk": "^2.2.0", + "wangeditor": "^2.1.23", + "whatwg-fetch": "^2.0.3" + } +} diff --git a/src/actions/content.js b/src/actions/content.js new file mode 100644 index 0000000..281bd65 --- /dev/null +++ b/src/actions/content.js @@ -0,0 +1,48 @@ +import { api, actionType } from 'Constants'; +import { get, post } from 'Assets/js/request'; +import { actionCreator } from 'Assets/js/util'; +import { message } from 'antd'; + +const addRequest = actionCreator(actionType.CONTENT_LIST_REQUEST); +const rejectRequest = actionCreator(actionType.CONTENT_LIST_FAIL); +const finishList = actionCreator(actionType.CONTENT_LIST_SUCCESS); + +const pageSize = 10; + + +// 获取文章列表 +export function fetchList(opts = {}) { + return async (dispatch) => { + dispatch(addRequest()); + + const params = Object.assign({ + pageSize, + }, opts); + let payload; + + try { + payload = await get(api.API_CONTENT, params); + } catch (e) { + dispatch(rejectRequest); + throw e; + } + + dispatch(finishList(payload)); + }; +} + +// 删除文章 +export function fetchContentDel(opts = {}) { + return async (dispatch, getState) => { + const { pageIndex } = getState().routing.locationBeforeTransitions.query; + const params = Object.assign({}, opts); + + try { + await post(api.API_CONTENT_DEL, params); + } catch (e) { + throw e; + } + + message.success('删除成功'); + }; +} diff --git a/src/actions/contentEdit.js b/src/actions/contentEdit.js new file mode 100644 index 0000000..f325e7f --- /dev/null +++ b/src/actions/contentEdit.js @@ -0,0 +1,54 @@ +import { api, actionType } from 'Constants'; +import { post, get } from 'Assets/js/request'; +import { actionCreator } from 'Assets/js/util'; +import { message } from 'antd'; + +const finishEdit = actionCreator(actionType.CONTENT_EDIT_INFO); + +// 新增文章 +export function fetchSubmit(params = {}) { + return async (dispatch) => { + try { + await post(api.API_CONTENT_ADD, params); + } catch (e) { + return; + } + + message.success('新增成功'); + + } +} + +// 获取文章信息 +export function fetchContentEdit(params = {}) { + return async (dispatch) => { + let payload = { + data: { + info: {}, + }, + }; + + if (params.id) { + try { + payload = await get(api.API_CONTENT_EDIT, params); + } catch (e) { + return; + } + } + + dispatch(finishEdit(payload)); + } +} + +// 更新文章信息 +export function fetchContentUpdate(params = {}) { + return async (dispatch) => { + try { + await post(api.API_CONTENT_UPDATE, params); + } catch (e) { + return; + } + + message.success('更改成功'); + } +} diff --git a/src/actions/router.js b/src/actions/router.js new file mode 100644 index 0000000..d93355c --- /dev/null +++ b/src/actions/router.js @@ -0,0 +1,34 @@ +import { push } from 'react-router-redux'; + +export function changeUrl(url) { + return dispatch => dispatch(push(url)); +} + +export function replaceQuery(query) { + return (dispatch, getState) => { + const { pathname, state } = getState().routing.locationBeforeTransitions; + const location = { + pathname, + state, + query, + }; + + dispatch(push(location)); + }; +} + +export function updateQuery(newQuery) { + return (dispatch, getState) => { + const { pathname, query, state } = getState().routing.locationBeforeTransitions; + const location = { + pathname, + state, + query: { + ...query, + ...newQuery, + }, + }; + + dispatch(push(location)); + }; +} diff --git a/src/actions/user.js b/src/actions/user.js new file mode 100644 index 0000000..446950a --- /dev/null +++ b/src/actions/user.js @@ -0,0 +1,48 @@ +import { api, actionType } from 'Constants'; +import { get, post } from 'Assets/js/request'; +import { actionCreator } from 'Assets/js/util'; +import { message } from 'antd'; + +const addRequest = actionCreator(actionType.USER_LIST_REQUEST); +const rejectRequest = actionCreator(actionType.USER_LIST_FAIL); +const finishUserList = actionCreator(actionType.USER_LIST_SUCCESS); + +const pageSize = 10; + + +// 获取用户列表 +export function fetchList(opts = {}) { + return async (dispatch) => { + dispatch(addRequest()); + + const params = Object.assign({ + pageSize, + }, opts); + let payload; + + try { + payload = await get(api.API_USER_LIST, params); + } catch (e) { + dispatch(rejectRequest); + throw e; + } + + dispatch(finishUserList(payload)); + }; +} + +// 删除用户 +export function fetchUserDel(opts = {}) { + return async (dispatch, getState) => { + const { pageIndex } = getState().routing.locationBeforeTransitions.query; + const params = Object.assign({}, opts); + + try { + await post(api.API_USER_DEL, params); + } catch (e) { + throw e; + } + + message.success('删除成功'); + }; +} diff --git a/src/actions/userEdit.js b/src/actions/userEdit.js new file mode 100644 index 0000000..9d57d1f --- /dev/null +++ b/src/actions/userEdit.js @@ -0,0 +1,60 @@ +import { api, actionType } from 'Constants'; +import { post, get } from 'Assets/js/request'; +import { actionCreator } from 'Assets/js/util'; +import { message } from 'antd'; + +const finishEdit = actionCreator(actionType.USER_EDIT_INFO); + +// 新增用户 +export function fetchSubmit(opts = {}) { + return async (dispatch) => { + const params = Object.assign({}, opts); + let payload; + + try { + payload = await post(api.API_USER_ADD, params); + } catch (e) { + return; + } + + message.success(payload.message); + + } +} + +// 获取用户信息 +export function fetchUserEdit(opts ={}) { + return async (dispatch) => { + const params = Object.assign({}, opts); + let payload = { + data: { + info: {} + } + }; + + if (params.id) { + try { + payload = await get(api.API_USER_EDIT, params) + } catch (e) { + throw e; + } + } + + dispatch(finishEdit(payload)); + } +} + +// 更新用户数据 +export function fetchUserUpdate(opts ={}) { + return async (dispatch) => { + const params = Object.assign({}, opts); + + try { + await post(api.API_USER_UPDATE, params) + } catch (e) { + throw e; + } + + message.success('保存成功'); + } +} diff --git a/src/assets/js/http.js b/src/assets/js/http.js new file mode 100644 index 0000000..d387829 --- /dev/null +++ b/src/assets/js/http.js @@ -0,0 +1,109 @@ +import 'whatwg-fetch'; // fetch 的polyfill,是低版本的浏览器也支持fetch + +const netErrorStatu = 1; // 网络错误 +const serverErrorStatu = 2; // 服务器错误 +const formatErrorStatu = 3; // 数据格式错误 +const logicErrorStatu = 4; // 业务逻辑错误 + +const errorMsg = { + [netErrorStatu]: '网络错误', + [serverErrorStatu]: '服务器错误', + [formatErrorStatu]: '数据格式错误', + [logicErrorStatu]: '业务逻辑错误' +}; + +class CustomFetchError { + constructor(error, data) { + this.errno = error; + this.msg = errorMsg[error]; + this.data = data; + } +} + +// 处理请求数据 +export function buildQuery(data) { + const toString = Object.prototype.toString; + const res = Object.entries(data).reduce((pre, [key, value]) => { + let newValue; + + if (Array.isArray(value) || toString.call(value) === '[object Object]') { + newValue = JSON.stringify(value); + } else { + newValue = (value === null || value === undefined) ? '' : value; + } + + pre.push(`${key}=${encodeURIComponent(newValue)}`); + + return pre; + }, []); + + return res.join('&'); +} + +// 处理请求状态 +export async function request(input, opt) { + // 设置cookies是否能跨域得到 + const init = Object.assign({ + credentials: 'include', + }, opt); + + let res; + + try { + res = await fetch(input, init); + } catch (e) { + throw new CustomFetchError(netErrorStatu, e); + } + + if (!res.ok) { + throw new CustomFetchError(serverErrorStatu, res); + } + + let data; + + try { + data = await res.json(); + } catch (e) { + throw new CustomFetchError(formatErrorStatu, e); + } + + if (!data || data.error !== 0) { + throw new CustomFetchError(logicErrorStatu, data); + } + + return data; +} + +export async function get(url, params = {}, opt = {}) { + const init = Object.assign({ + method: 'GET', + }, opt); + + const paramsStr = buildQuery(params); + + const urlWithQuery = url + (paramsStr ? `?${paramsStr}` : ''); + const res = await request(urlWithQuery, init); + + return res; +} + +export async function post(url, params = {}) { + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const init = Object.assign({ + method: 'POST', + body: buildQuery(params), + headers, + }, {}); + + const res = await request(url, init); + + return res; +} + +export default { + get, + post, +}; diff --git a/src/assets/js/request.js b/src/assets/js/request.js new file mode 100644 index 0000000..03ff316 --- /dev/null +++ b/src/assets/js/request.js @@ -0,0 +1,27 @@ +import * as http from './http'; +import { message } from 'antd'; + +async function request(method, url, params) { + let res; + + try { + res = await http[method](url, params); + } catch (e) { + message.error(e.errno === 4 ? e.data.msg : e.msg); + throw e; + } + + return res; +} + +export async function get(...arg) { + const res = await request('get', ...arg); + + return res; +} + +export async function post(...arg) { + const res = await request('post', ...arg); + + return res; +} diff --git a/src/assets/js/util.js b/src/assets/js/util.js new file mode 100644 index 0000000..cf4aed0 --- /dev/null +++ b/src/assets/js/util.js @@ -0,0 +1,6 @@ +// 创建action +export const actionCreator = type => payload => ({ type, payload }); + +// 比较两个对象下的属性是否相同 +export const equalByProps = (obj1, obj2, props) => + props.some(prop => obj1[prop] !== obj2[prop]); diff --git a/src/components/Breadcrumb/Breadcrumb.jsx b/src/components/Breadcrumb/Breadcrumb.jsx new file mode 100644 index 0000000..46b9091 --- /dev/null +++ b/src/components/Breadcrumb/Breadcrumb.jsx @@ -0,0 +1,20 @@ +import React, { PropTypes } from 'react'; + +// css +import './index.styl'; + +const Breadcrumb = ({ children }) => ( + + {children} + + ); + +Breadcrumb.propTypes = { + children: PropTypes.node, +}; + +Breadcrumb.defaultProps = { + children: [], +}; + +export default Breadcrumb; diff --git a/src/components/Breadcrumb/BreadcrumbItem.jsx b/src/components/Breadcrumb/BreadcrumbItem.jsx new file mode 100644 index 0000000..dc9e38c --- /dev/null +++ b/src/components/Breadcrumb/BreadcrumbItem.jsx @@ -0,0 +1,42 @@ +import React, { PureComponent, PropTypes } from 'react'; + +import './index.styl'; + +class BreadcrumbItem extends PureComponent { + + static defaultProps = { + separator: '/', + }; + + render() { + const { separator, children, ...restProps } = this.props; + let item; + + if ('href' in this.props) { + item = {children}; + } else { + item = {children}; + } + + return ( + + {item} + + {separator} + + + ); + } +} + +BreadcrumbItem.propTypes = { + children: PropTypes.node, + separator: PropTypes.string, +}; + +BreadcrumbItem.defaultProps = { + children: [], + separator: '/', +}; + +export default BreadcrumbItem; diff --git a/src/components/Breadcrumb/index.jsx b/src/components/Breadcrumb/index.jsx new file mode 100644 index 0000000..3530b39 --- /dev/null +++ b/src/components/Breadcrumb/index.jsx @@ -0,0 +1,6 @@ +import Breadcrumb from './Breadcrumb'; +import BreadcrumbItem from './BreadcrumbItem'; + +Breadcrumb.Item = BreadcrumbItem; + +export default Breadcrumb; diff --git a/src/components/Breadcrumb/index.styl b/src/components/Breadcrumb/index.styl new file mode 100644 index 0000000..9a2c1d1 --- /dev/null +++ b/src/components/Breadcrumb/index.styl @@ -0,0 +1,30 @@ +.breadcrumb-wrapper { + display: block; + padding: 7px 14px; + border: 1px solid #ddd; + border-radius: 3px; + background: #fbfbfb; + box-shadow: inset 0 1px 0 #fff; + font-size: 12px; + + .item { + &:last-child { + .separator { + display: none; + } + } + + a { + color: #08c; + } + + .separator { + display: inline-block; + font-family: Microsoft YaHei; + margin: 0 8px 0 6px; + color: #666; + text-indent: 4px; + font-weight: 400; + } + } +} diff --git a/src/components/Editor/index.jsx b/src/components/Editor/index.jsx new file mode 100644 index 0000000..1381438 --- /dev/null +++ b/src/components/Editor/index.jsx @@ -0,0 +1,57 @@ +import React, { Component, PropTypes } from 'react'; +import 'wangeditor'; + +// css +import style from './index.styl'; + +class Editor extends Component { + constructor(props) { + super(props); + } + + componentDidMount() { + const { id, content } = this.props; + + this.editor = new window.wangEditor(id); + const ed = this.editor; + this.editor.config.uploadImgUrl = '/api/upload'; + this.editor.config.uploadImgFns.onload = function (resultText, xhr) { + // resultText 服务器端返回的text + // xhr 是 xmlHttpRequest 对象,IE8、9中不支持 + // console.log(resultText, xhr); + // 上传图片时,已经将图片的名字存在 editor.uploadImgOriginalName + // console.log(ed); + var originalName = ed.uploadImgOriginalName || ''; + console.log(JSON.parse(resultText)); + const aa = JSON.parse(resultText).imgSrc; + // 如果 resultText 是图片的url地址,可以这样插入图片: + ed.command(null, 'InsertImage', aa); + // 如果不想要 img 的 max-width 样式,也可以这样插入: + // editor.command(null, 'InsertImage', resultText); + }; + + this.editor.create(); + + // 初始化内容 + this.editor.$txt.html(content); + } + + // 获取内容 + getContent() { + var content = this.editor.$txt.html(); + + return content; + } + + render() { + const { id, style } = this.props; + + return ( +
+
+
+ ); + } +} + +export default Editor; diff --git a/src/components/Editor/index.styl b/src/components/Editor/index.styl new file mode 100644 index 0000000..1bb3f38 --- /dev/null +++ b/src/components/Editor/index.styl @@ -0,0 +1,3 @@ +.editor-wrapper { + width: 100%; +} \ No newline at end of file diff --git a/src/constants/actionType.js b/src/constants/actionType.js new file mode 100644 index 0000000..6be852a --- /dev/null +++ b/src/constants/actionType.js @@ -0,0 +1,9 @@ +export const USER_LIST_REQUEST = 'USER_LIST_REQUEST'; +export const USER_LIST_FAIL = 'USER_LIST_FAIL'; +export const USER_LIST_SUCCESS = 'USER_LIST_SUCCESS'; +export const USER_EDIT_INFO = 'USER_EDIT_INFO'; + +export const CONTENT_LIST_REQUEST = 'CONTENT_LIST_REQUEST'; +export const CONTENT_LIST_FAIL = 'CONTENT_LIST_FAIL'; +export const CONTENT_LIST_SUCCESS = 'CONTENT_LIST_SUCCESS'; +export const CONTENT_EDIT_INFO = 'CONTENT_EDIT_INFO'; diff --git a/src/constants/api.js b/src/constants/api.js new file mode 100644 index 0000000..ee9474c --- /dev/null +++ b/src/constants/api.js @@ -0,0 +1,13 @@ +const prefix = '/api'; + +export const API_USER_LIST = `${prefix}/user`; +export const API_USER_DEL = `${prefix}/user/del`; +export const API_USER_ADD = `${prefix}/user/add`; +export const API_USER_EDIT = `${prefix}/user/edit`; +export const API_USER_UPDATE = `${prefix}/user/update`; + +export const API_CONTENT = `${prefix}/content`; +export const API_CONTENT_DEL = `${prefix}/content/del`; +export const API_CONTENT_ADD = `${prefix}/content/add`; +export const API_CONTENT_EDIT = `${prefix}/content/edit`; +export const API_CONTENT_UPDATE = `${prefix}/content/update`; diff --git a/src/constants/commons.js b/src/constants/commons.js new file mode 100644 index 0000000..0815611 --- /dev/null +++ b/src/constants/commons.js @@ -0,0 +1,23 @@ +const ARTICLE_PAGE = ''; + +const CLASSIFY_LIST = { + web: "Web前端", + mobile: "移动前端", + skill: "授人以渔", + ui: "UI设计", + wordpress: "WP视点", + record: "朝花夕拾", + code: "程序人生", +}; + +const WEEK_LIST = { + 1: "周一", + 2: "周二", + 3: "周三", + 4: "周四", + 5: "周五", + 6: "周六", + 7: "周七", +}; + +export { CLASSIFY_LIST, WEEK_LIST }; \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..35046ac --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,5 @@ +import * as api from './api'; +import * as actionType from './actionType'; +import * as commons from './commons'; + +export { api, actionType, commons }; diff --git a/src/containers/App/index.jsx b/src/containers/App/index.jsx new file mode 100644 index 0000000..efa4b22 --- /dev/null +++ b/src/containers/App/index.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Router, Route, IndexRoute, browserHistory } from 'react-router'; +import { createStore, applyMiddleware, combineReducers } from 'redux'; +import { Provider } from 'react-redux'; +import { routerReducer, routerMiddleware, syncHistoryWithStore } from 'react-router-redux'; +import thunk from 'redux-thunk'; + +import Manage from 'Containers/Manage'; +import User from 'Containers/Manage/User'; +import UserEdit from 'Containers/Manage/User/Edit'; +import Content from 'Containers/Manage/Content'; +import ContentEdit from 'Containers/Manage/Content/Edit'; + +import rootReducer from 'Reducers'; + +import './index.styl'; + +const middlewares = [thunk, routerMiddleware(browserHistory)]; + +const store = createStore( + combineReducers({ + ...rootReducer, + routing: routerReducer, + }), + {}, + applyMiddleware(...middlewares), +); + +const history = syncHistoryWithStore(browserHistory, store); + +const App = () => + ( + + + + + + + + + + + ); + +export default App; diff --git a/src/containers/App/index.styl b/src/containers/App/index.styl new file mode 100644 index 0000000..2f3f548 --- /dev/null +++ b/src/containers/App/index.styl @@ -0,0 +1,59 @@ +// css reset +body { + color: #2b2b2b; + background: #f1f1ef; +} + +body, h1, h2, h3 { + margin: 0; + font: 14px/1.5 Microsoft YaHei,Arial,Helvetica,sans-serif; +} + +ul, ol { + margin: 0; + padding: 0; +} + +li { + list-style: none; +} + +a { + text-decoration: none; +} + +img { + border: 0; +} + +// public +.clearfix { + zoom: 1; +} + +.clearfix:after { + content: ''; + display: block; + clear: both; +} + +.fl { + float: left; +} + +.fr { + float: right; +} + +.filter-containers { + padding-bottom: 25px; + display: flex; + & > div:nth-of-type(1) { + flex: 1; + } +} + +.main-containers { + padding-top: 25px; + display: flex; +} diff --git a/src/containers/Manage/Content/Edit/index.jsx b/src/containers/Manage/Content/Edit/index.jsx new file mode 100644 index 0000000..3c1a5f5 --- /dev/null +++ b/src/containers/Manage/Content/Edit/index.jsx @@ -0,0 +1,144 @@ +import React, { Component, PropTypes } from 'react'; +import { Link } from 'react-router'; +import { Form, Input, Button, Breadcrumb, Select } from 'antd'; +import { connect } from 'react-redux'; +import * as actions from 'Actions/contentEdit'; +import { goBack } from 'react-router-redux'; +import Editor from 'Components/Editor'; +import { commons } from 'Constants'; + +// css +import './index.styl'; + +const { Item: FormItem } = Form; +const { Item: BreadcrumbItem } = Breadcrumb; +const { Option } = Select; + +const editorStyle = { + width: "100%", + height: "500px", +}; + +class ContentEdit extends Component { + constructor(props) { + super(props); + this.state = { + content: '' + }; + + this.handleSubmint = this.handleSubmint.bind(this); + } + + componentDidMount() { + const { id, fetchContentEdit } = this.props; + const params = { + id, + }; + + fetchContentEdit(params); + } + + async handleSubmint() { + const { id, fetchSubmit, fetchContentUpdate, goBack, form: { validateFields } } = this.props; + const content = this.refs.edit.getContent(); + let params; + + validateFields((error, values) => { + if (error) { + return; + } + + params = { + ...values, + id, + content + }; + }) + + try { + await id ? fetchContentUpdate(params) : fetchSubmit(params); + } catch (e) { + return; + } + + goBack(); + } + + + render() { + const { content } = this.state; + const { id, info, form: { getFieldDecorator } } = this.props; + + const titleDecorator = getFieldDecorator('title', { + initialValue: info.title, + rules: [ + { required: true, type: 'string', message: '标题为必填参数' } + ] + }); + + const classifyDecorator = getFieldDecorator('classify', { + initialValue: info.classify, + rules: [ + { required: true, type: 'string', message: '标题为必填参数' } + ] + }); + + const options = Object.entries(commons.CLASSIFY_LIST).map(([value, label]) => ); + + return ( +
+ + 内容管理 + 编辑 + +
+
+ + {titleDecorator( + + )} + + + + {classifyDecorator( + + )} + +
+
+ { + info.content || !id ? : + null + } + +
+ ); + } +} + +const mapStateToProps = ({ contentEdit }, { location: { query } }) => { + const { info } = contentEdit; + const { id } = query; + + return { + id, + info, + }; +}; + +const mapDispatchToProps = { ...actions, goBack }; + +ContentEdit.propTypes = { + id: PropTypes.string, + info: PropTypes.object.isRequired, + fetchSubmit: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Form.create()(ContentEdit)); diff --git a/src/containers/Manage/Content/Edit/index.styl b/src/containers/Manage/Content/Edit/index.styl new file mode 100644 index 0000000..f5341d7 --- /dev/null +++ b/src/containers/Manage/Content/Edit/index.styl @@ -0,0 +1,5 @@ +.user-edit-wrapper { + .ant-btn { + margin: 0 10px; + } +} diff --git a/src/containers/Manage/Content/index.jsx b/src/containers/Manage/Content/index.jsx new file mode 100644 index 0000000..f706e00 --- /dev/null +++ b/src/containers/Manage/Content/index.jsx @@ -0,0 +1,214 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { Button, Table, Form, Input } from 'antd'; +import moment from 'moment'; +import * as actions from 'Actions/content'; +import * as router from 'Actions/router'; +import { equalByProps } from 'Assets/js/util'; + +// css +import './index.styl'; + +const { Item: FormItem } = Form; + +class Content extends Component { + constructor(props) { + super(props); + + this.columns = [ + { + title: 'ID', + dataIndex: '_id', + }, + { + title: '文章题目', + dataIndex: 'title', + }, + { + title: '创建时间', + dataIndex: 'createTime', + render: val => moment(val).format('YYYY-MM-DD'), + }, + { + title: '浏览量', + dataIndex: 'views', + }, + { + title: '作者', + dataIndex: 'author1', + }, + { + title: '类型', + dataIndex: 'classify', + }, + { + title: '操作', + render: ({ _id }) => + ( +
+ + + + +
+ ), + }, + ]; + + this.handleChange = this.handleChange.bind(this); + this.handleDel = this.handleDel.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentDidMount() { + const { pageIndex, title, fetchList } = this.props; + const params = { + pageIndex, + title, + }; + + fetchList(params); + } + + componentDidUpdate(preProps) { + const { pageIndex, title, fetchList } = this.props; + const fields = ['pageIndex', 'title']; + const params = { + pageIndex, + title, + }; + + if (equalByProps(preProps, this.props, fields)) { + fetchList(params); + } + } + + handleChange(val) { + const { updateQuery } = this.props; + const { current: pageIndex } = val; + const params = { + pageIndex, + }; + + updateQuery(params); + } + + async handleDel(val) { + const { pageIndex, title, fetchList, fetchContentDel } = this.props; + const params1 = { + id: val, + }; + + try { + await fetchContentDel(params1); + } catch (e) { + return; + } + + const params2 = { + pageIndex, + title, + }; + + fetchList(params2); + + } + + // 搜索 + handleSubmit(e) { + + e.preventDefault(); + const { replaceQuery, form: { validateFields } } = this.props; + + validateFields((error, values) => { + if (error) { + return; + } + + replaceQuery(values); + }) + } + + render() { + const { list, loading, total, pageIndex, title, form: { getFieldDecorator } } = this.props; + const pagination = { + total, + current: pageIndex, + }; + + const usernameDecorator = getFieldDecorator('title', { + initialValue: title, + }); + + return ( +
+
+
+
+ + {usernameDecorator()} + + + + + +
+
+
+ + + +
+
+
+ + + + ); + } +} + +const mapStateToProps = ({ contentList }, { location }) => { + const { list, total, loading } = contentList; + const { pageIndex = 1, title } = location.query; + + return { + list, + total, + loading, + title, + pageIndex: Number(pageIndex), + }; +}; + +const mapDispatchToProps = { ...actions, ...router }; + +Content.propTypes = { + list: PropTypes.array.isRequired, + total: PropTypes.number.isRequired, + loading: PropTypes.bool.isRequired, + pageIndex: PropTypes.number.isRequired, + username: PropTypes.string, + fetchList: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Form.create()(Content)); diff --git a/src/containers/Manage/Content/index.styl b/src/containers/Manage/Content/index.styl new file mode 100644 index 0000000..e69de29 diff --git a/src/containers/Manage/User/Edit/index.jsx b/src/containers/Manage/User/Edit/index.jsx new file mode 100644 index 0000000..1d7777a --- /dev/null +++ b/src/containers/Manage/User/Edit/index.jsx @@ -0,0 +1,165 @@ +import React, { Component, PropTypes } from 'react'; +import { Link } from 'react-router'; +import { Form, Input, Button, Breadcrumb, Radio } from 'antd'; +import { connect } from 'react-redux'; +import * as actions from 'Actions/userEdit'; +import { push } from 'react-router-redux'; + +// css +import './index.styl'; + +const { Item: FormItem } = Form; +const { Item: BreadcrumbItem } = Breadcrumb; +const { Group: RadioGroup } = Radio; +const formItemLayout = { + labelCol: { span: 6 }, + wrapperCol: { span: 14 }, +}; + +const tailFormItemLayout = { + wrapperCol: { + span: 24, + offset: 6, + }, +}; + +class UserEdit extends Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentDidMount() { + const { id, fetchUserEdit } = this.props; + const params = { + id + }; + + fetchUserEdit(params); + } + + async handleSubmit(e) { + // 阻止表单的默认事件 + e.preventDefault(); + + const { id, fetchSubmit, fetchUserUpdate, push, form: { validateFields } } = this.props; + let params; + + validateFields((errors, values) => { + + if (errors) { + return; + } + + params = { + ...values, + id, + } + }); + + try { + await id ? fetchUserUpdate(params) : fetchSubmit(params) ; + } catch (e) { + return; + } + + push('/manage'); + + } + + render() { + const { info: { username, password, isAdmin }, form: { getFieldDecorator } } = this.props; + + const usernameDecorator = getFieldDecorator('username', { + initialValue: username, + rules: [ + { required: true, message: '请输入账号名' } + ], + }); + + const passwordDecorator = getFieldDecorator('password', { + initialValue: password, + rules: [ + { required: true, message: '请输入密码' } + ], + }); + + const rePasswordDecorator = getFieldDecorator('rePassword', { + initialValue: password, + rules: [ + { required: true, message: '请再次输入密码' } + ], + }); + + const isAdminDecorator = getFieldDecorator('isAdmin', { + initialValue: isAdmin, + rules: [ + { required: true, message: '请选择是否是管理员' } + ], + }); + + return ( +
+ + 用户管理 + 编辑 + +
+ + {usernameDecorator( + + )} + + + + {passwordDecorator( + + )} + + + + {rePasswordDecorator( + + )} + + + + {isAdminDecorator( + + + + + )} + + + + + + + +
+ ); + } +} + +const mapStateToProps = ({ userEdit }, { location: { query } }) => { + const { info } = userEdit; + const { id } = query; + + return { + id, + status: !id, + info, + }; +}; + +const mapDispatchToProps = { ...actions, push }; + +UserEdit.propTypes = { + id: PropTypes.string, + info: PropTypes.object.isRequired, + fetchSubmit: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Form.create()(UserEdit)); diff --git a/src/containers/Manage/User/Edit/index.styl b/src/containers/Manage/User/Edit/index.styl new file mode 100644 index 0000000..f5341d7 --- /dev/null +++ b/src/containers/Manage/User/Edit/index.styl @@ -0,0 +1,5 @@ +.user-edit-wrapper { + .ant-btn { + margin: 0 10px; + } +} diff --git a/src/containers/Manage/User/index.jsx b/src/containers/Manage/User/index.jsx new file mode 100644 index 0000000..dd7a06c --- /dev/null +++ b/src/containers/Manage/User/index.jsx @@ -0,0 +1,217 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { Button, Table, Form, Input } from 'antd'; +import { fetchList, fetchUserDel, fetchSearch } from 'Actions/user'; +import { updateQuery, replaceQuery } from 'Actions/router'; +import { equalByProps } from 'Assets/js/util'; + +// css +import './index.styl'; + +const { Item: FormItem } = Form; + +class User extends Component { + constructor(props) { + super(props); + + this.columns = [ + { + title: 'ID', + dataIndex: '_id', + }, + { + title: '用户名', + dataIndex: 'username', + }, + { + title: '创建时间', + dataIndex: 'password', + }, + { + title: '是否是管理员', + dataIndex: 'isAdmin', + render: (val) => { + const res = val ? '是' : '否'; + + return res; + }, + }, + { + title: '操作', + render: ({ _id }) => + ( +
+ + + + +
+ ), + }, + ]; + + this.handleChange = this.handleChange.bind(this); + this.handleDel = this.handleDel.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentDidMount() { + const { pageIndex, username, fetchList } = this.props; + const params = { + pageIndex, + username, + }; + + fetchList(params); + } + + componentDidUpdate(preProps) { + const { pageIndex, username, fetchList } = this.props; + const fields = ['pageIndex', 'username']; + const params = { + pageIndex, + username, + }; + + if (equalByProps(preProps, this.props, fields)) { + fetchList(params); + } + } + + handleChange(val) { + const { onchange } = this.props; + const { current: pageIndex } = val; + const params = { + pageIndex, + }; + + onchange(params); + } + + async handleDel(val) { + const { pageIndex, username, fetchList, fetchUserDel } = this.props; + const params1 = { + id: val, + }; + + try { + await fetchUserDel(params1); + } catch (e) { + return; + } + + const params2 = { + pageIndex, + username, + }; + + fetchList(params2); + + } + + // 搜索 + handleSubmit(e) { + + e.preventDefault(); + const { fetchSearch, form: { validateFields } } = this.props; + + validateFields((error, values) => { + if (error) { + return; + } + + fetchSearch(values); + }) + } + + render() { + const { list, loading, total, pageIndex, username, form: { getFieldDecorator } } = this.props; + const pagination = { + total, + current: pageIndex, + }; + + const usernameDecorator = getFieldDecorator('username', { + initialValue: username, + }); + + return ( +
+
+
+
+ + {usernameDecorator()} + + + + + + +
+
+ + + +
+
+
+
+ + + ); + } +} + +const mapStateToProps = ({ userList }, { location }) => { + const { list, total, loading } = userList; + const { pageIndex = 1, username } = location.query; + + return { + list, + total, + loading, + username, + pageIndex: Number(pageIndex), + }; +}; + +const mapDispatchToProps = dispatch => ({ + fetchList: opts => dispatch(fetchList(opts)), + fetchUserDel: opts => dispatch(fetchUserDel(opts)), + fetchSearch: opts => dispatch(replaceQuery(opts)), + onchange: opts => dispatch(updateQuery(opts)), +}); + +User.propTypes = { + list: PropTypes.array.isRequired, + total: PropTypes.number.isRequired, + loading: PropTypes.bool.isRequired, + pageIndex: PropTypes.number.isRequired, + username: PropTypes.string, + fetchList: PropTypes.func.isRequired, + fetchUserDel: PropTypes.func.isRequired, + fetchSearch: PropTypes.func.isRequired, + onchange: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Form.create()(User)); diff --git a/src/containers/Manage/User/index.styl b/src/containers/Manage/User/index.styl new file mode 100644 index 0000000..af4d6ea --- /dev/null +++ b/src/containers/Manage/User/index.styl @@ -0,0 +1,5 @@ +.user-wrapper { + .ant-btn { + margin: 0 10px; + } +} diff --git a/src/containers/Manage/index.jsx b/src/containers/Manage/index.jsx new file mode 100644 index 0000000..177575f --- /dev/null +++ b/src/containers/Manage/index.jsx @@ -0,0 +1,86 @@ +import React, { Component, PropTypes } from 'react'; +import { Layout, Menu, Icon } from 'antd'; +import { Link } from 'react-router'; + +import './index.styl'; + +const { Header, Sider, Content } = Layout; +const { Item: MenuItem } = Menu; + +class Manage extends Component { + constructor(props) { + super(props); + + this.state = { + collapsed: false, + }; + + this.toggle = this.toggle.bind(this); + } + + toggle() { + const { collapsed } = this.state; + this.setState({ + collapsed: !collapsed, + }); + } + + render() { + const { collapsed } = this.state; + + const { children } = this.props; + return ( + + +
+ + + + + 用户管理 + + + + + + 分类管理 + + + + + + 内容管理 + + + + + +
+ +
+ + {children} + +
+ + ); + } +} + +Manage.propTypes = { + children: PropTypes.node, +}; + +Manage.defaultProps = { + children: [], +}; + +export default Manage; diff --git a/src/containers/Manage/index.styl b/src/containers/Manage/index.styl new file mode 100644 index 0000000..f1894d1 --- /dev/null +++ b/src/containers/Manage/index.styl @@ -0,0 +1,45 @@ +#root { + height: 100%; + + .manage-wrapper { + height: 100%; + + .manage-header { + background: #fff; + padding: 0; + } + + .manage-containers { + margin: 24px 16px; + padding: 24px; + background: #fff; + } + + .trigger { + font-size: 18px; + line-height: 64px; + padding: 0 16px; + cursor: pointer; + transition: color .3s; + + &:hover { + color: #108ee9; + } + } + + .logo { + height: 32px; + background: #333; + border-radius: 6px; + margin: 16px; + } + + .ant-layout-sider-collapsed .anticon { + font-size: 16px; + } + + .ant-layout-sider-collapsed .nav-text { + display: none; + } + } +} diff --git a/src/index.jsx b/src/index.jsx new file mode 100644 index 0000000..65a3648 --- /dev/null +++ b/src/index.jsx @@ -0,0 +1,20 @@ +import 'babel-polyfill'; // babel 只能对es高级语法进行编译,如果支持新的特性需要引入babel-polyfill +import 'normalize.css'; +import { AppContainer } from 'react-hot-loader'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from 'Containers/App'; + +const rootEl = document.getElementById('root'); +const render = Component => + ReactDOM.render( + + , + rootEl, + ); + +render(App); + +if (module.hot) { + module.hot.accept('Containers/App', () => render(App)); +} diff --git a/src/reducers/content.js b/src/reducers/content.js new file mode 100644 index 0000000..768cf88 --- /dev/null +++ b/src/reducers/content.js @@ -0,0 +1,33 @@ +import * as actionType from 'Constants/actionType'; + +const initState = { + list: [], + total: 0, + loading: false, +}; + +export default function contentList(state = initState, { type, payload }) { + switch (type) { + case actionType.CONTENT_LIST_REQUEST: + return { + ...state, + loading: true, + }; + case actionType.CONTENT_LIST_FAIL: + return { + ...state, + loading: false, + }; + case actionType.CONTENT_LIST_SUCCESS: { + const { data: { list, total } } = payload; + return { + ...state, + list, + total, + loading: false, + }; + } + default: + return state; + } +} diff --git a/src/reducers/contentEdit.js b/src/reducers/contentEdit.js new file mode 100644 index 0000000..7ddf0d6 --- /dev/null +++ b/src/reducers/contentEdit.js @@ -0,0 +1,20 @@ +import * as api from 'Constants/api'; +import * as actionType from 'Constants/actionType'; + +const initState = { + info: {}, +} + +export default function contentEdit(state = initState, { type, payload }) { + switch (type) { + case actionType.CONTENT_EDIT_INFO: { + const { data: { info } } = payload; + return { + ...state, + info + }; + } + default: + return state; + } +} diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 0000000..5cdd956 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,12 @@ +import userList from './user'; +import userEdit from './userEdit' + +import contentList from './content'; +import contentEdit from './contentEdit'; + +export default { + userList, + userEdit, + contentList, + contentEdit, +}; diff --git a/src/reducers/user.js b/src/reducers/user.js new file mode 100644 index 0000000..d8a3539 --- /dev/null +++ b/src/reducers/user.js @@ -0,0 +1,33 @@ +import * as actionType from 'Constants/actionType'; + +const initState = { + list: [], + total: 0, + loading: false, +}; + +export default function userList(state = initState, { type, payload }) { + switch (type) { + case actionType.USER_LIST_REQUEST: + return { + ...state, + loading: true, + }; + case actionType.USER_LIST_FAIL: + return { + ...state, + loading: false, + }; + case actionType.USER_LIST_SUCCESS: { + const { data: { list, total } } = payload; + return { + ...state, + list, + total, + loading: false, + }; + } + default: + return state; + } +} diff --git a/src/reducers/userEdit.js b/src/reducers/userEdit.js new file mode 100644 index 0000000..a2864bd --- /dev/null +++ b/src/reducers/userEdit.js @@ -0,0 +1,20 @@ +import * as api from 'Constants/api'; +import * as actionType from 'Constants/actionType'; + +const initState = { + info: {}, +} + +export default function userEdit(state = initState, { type, payload }) { + switch (type) { + case actionType.USER_EDIT_INFO: { + const { data: { info } } = payload; + return { + ...state, + info + }; + } + default: + return state; + } +}