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 (
+
+
+ 内容管理
+ 编辑
+
+
+
+
+ {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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 (
+
+
+ 用户管理
+ 编辑
+
+
+
+ );
+ }
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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;
+ }
+}