├── public ├── avatar.png └── favicon.ico ├── upload └── .gitignore ├── .babelrc ├── .gitignore ├── webpack ├── client │ ├── server.config.js │ ├── index.js │ └── client.config.js ├── server │ ├── index.js │ ├── server.js │ ├── Html.js │ └── handleRender.js └── isomorphic.tools.config.js ├── client ├── backend │ ├── Admin.less │ ├── dashboard │ │ └── index.js │ ├── article │ │ ├── resources.js │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── List.js │ │ └── EditBase.js │ ├── group │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── List.js │ │ └── EditBase.js │ ├── user │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── List.js │ │ └── EditBase.js │ ├── login │ │ └── Login.js │ ├── Admin.js │ └── Layout.js ├── frontend │ ├── article │ │ ├── list_style.less │ │ ├── view_style.less │ │ ├── Comment_style.less │ │ ├── view.js │ │ ├── list.js │ │ └── Comment.jsx │ └── Layout.js ├── Root.js ├── redux │ ├── reducers │ │ ├── index.js │ │ ├── system │ │ │ └── index.js │ │ ├── login │ │ │ └── index.js │ │ ├── group │ │ │ └── index.js │ │ ├── user │ │ │ └── index.js │ │ └── article │ │ │ └── index.js │ ├── store │ │ └── index.js │ ├── actions │ │ ├── login │ │ │ └── index.js │ │ ├── group │ │ │ └── index.js │ │ ├── user │ │ │ └── index.js │ │ └── article │ │ │ └── index.js │ ├── resouces │ │ └── article │ │ │ └── index.js │ └── constants │ │ └── index.js ├── index.js ├── component │ ├── Page │ │ ├── style.less │ │ └── index.jsx │ └── Back │ │ └── index.jsx └── routes │ └── index.js ├── config.js ├── server ├── router │ ├── upload.js │ ├── index.js │ ├── login.js │ ├── comment.js │ ├── article.js │ └── user.js ├── middleware │ ├── authNotStop.js │ ├── auth.js │ └── multipartParser.js └── model │ ├── index.js │ ├── UserGroup.js │ ├── User.js │ ├── Article.js │ ├── Comment.js │ └── util.js ├── README.md ├── .eslintrc ├── bin ├── start.js └── article.js ├── util ├── index.js └── fetch.js └── package.json /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontoldman/blog/HEAD/public/avatar.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontoldman/blog/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /upload/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | * 3 | !.gitignore -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | node_modules 4 | webpack-assets.json 5 | config.js 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /webpack/client/server.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | stats: { 3 | colors: true 4 | }, 5 | historyApiFallback: true, 6 | hot: true 7 | } -------------------------------------------------------------------------------- /client/backend/Admin.less: -------------------------------------------------------------------------------- 1 | .tip { 2 | position: absolute; 3 | width: 150%; 4 | left: 0; 5 | top: 72px; 6 | background: #d4d4d4; 7 | padding: 3px; 8 | cursor: pointer; 9 | display: none; 10 | } -------------------------------------------------------------------------------- /client/frontend/article/list_style.less: -------------------------------------------------------------------------------- 1 | .title{ 2 | margin: 20px; 3 | } 4 | .article_list{ 5 | li{ 6 | list-style: none; 7 | margin-top: 5px; 8 | .owner{ 9 | float: right; 10 | } 11 | } 12 | } 13 | .page{ 14 | margin-top: 20px; 15 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/9/28. 3 | */ 4 | 5 | export const client = { 6 | host: 'localhost', 7 | port: '3000' 8 | } 9 | 10 | export const server = { 11 | host: 'localhost', 12 | port: '3001' 13 | } 14 | 15 | export const db = { 16 | url: 'mongodb://127.0.0.1:27017/blog' 17 | } 18 | -------------------------------------------------------------------------------- /server/router/upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/17. 3 | */ 4 | 5 | import koaRouter from 'koa-router' 6 | 7 | const router = koaRouter() 8 | 9 | router.post('/upload', function *(next) { 10 | const { files } = this.request 11 | 12 | this.body = [ 13 | ...files 14 | ] 15 | }) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /client/frontend/article/view_style.less: -------------------------------------------------------------------------------- 1 | .article_detail{ 2 | .title{ 3 | 4 | } 5 | .content{ 6 | margin-top: 20px; 7 | } 8 | .tags{ 9 | .tag{ 10 | display: inline-block; 11 | background: #ddd; 12 | padding: 2px 5px; 13 | margin-right: 10px; 14 | cursor: pointer; 15 | } 16 | } 17 | .back{ 18 | text-align: right; 19 | } 20 | } -------------------------------------------------------------------------------- /server/middleware/authNotStop.js: -------------------------------------------------------------------------------- 1 | var User = require('../model/User') 2 | 3 | module.exports = function *(next) { 4 | var userId = this.cookies.get('userId') 5 | var user 6 | 7 | if (this.session.user) { 8 | return yield next 9 | } 10 | 11 | if (userId && (user = yield User.findOne({_id: userId}))) { 12 | this.session.user = user 13 | return yield next 14 | } 15 | 16 | yield next 17 | } 18 | -------------------------------------------------------------------------------- /client/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import {Provider} from 'react-redux' 3 | import {Router} from 'react-router' 4 | import routes from './routes' 5 | 6 | export default class Root extends Component { 7 | render () { 8 | const {history, store} = this.props 9 | return (< Provider store={store}> 10 | < Router history={history} routes={routes}/> 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog 2 | 3 | ## 使用koa react react-router redux 实现的博客系统 4 | 5 | **默认已经安装了mongodb** 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ``` 12 | npm run client:start 13 | ``` 14 | 15 | 打开新的控制台,切换到当前目录下 16 | 17 | ``` 18 | npm run server:start 19 | ``` 20 | 21 | **使用之前需要初始化数据库** 22 | 23 | 打开新的控制台,切换到当前目录下 24 | 25 | ``` 26 | npm run db:init 27 | ``` 28 | 29 | ``` 30 | npm run db:article 31 | ``` 32 | 33 | 然后访问`http://localhost:3001`就可以了看到新抓取的射雕英雄传了 34 | 35 | -------------------------------------------------------------------------------- /client/backend/dashboard/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/22. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import { connect } from 'react-redux' 7 | 8 | class DashBoard extends Component { 9 | render () { 10 | return (
11 | dashboard 12 |
) 13 | } 14 | } 15 | 16 | function mapStateToProps (state, ownProps) { 17 | return { 18 | } 19 | } 20 | 21 | export default connect(mapStateToProps, { 22 | })(DashBoard) 23 | -------------------------------------------------------------------------------- /client/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer } from 'react-router-redux' 3 | import system from './system' 4 | import group from './group' 5 | import user from './user' 6 | import login from './login' 7 | import article from './article' 8 | 9 | const rootReducer = combineReducers({ 10 | routing: routerReducer, 11 | system, 12 | group, 13 | user, 14 | login, 15 | article 16 | }) 17 | 18 | export default rootReducer 19 | -------------------------------------------------------------------------------- /client/frontend/article/Comment_style.less: -------------------------------------------------------------------------------- 1 | .list{ 2 | margin: 20px 0; 3 | li{ 4 | list-style: none; 5 | background: #fff; 6 | padding: 20px 10px; 7 | .content{ 8 | margin-left: 20px; 9 | margin-top: 10px; 10 | } 11 | } 12 | .left{ 13 | float: left; 14 | } 15 | .right{ 16 | float: right; 17 | .zan, .fan{ 18 | display: inline-block; 19 | padding: 5px 10px; 20 | cursor: pointer; 21 | border-radius: 3px; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /client/backend/article/resources.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/12. 3 | */ 4 | import fetch from '../../../util/fetch' 5 | 6 | export function getOwnerList (params = {}) { 7 | return fetch('/api/article/admin', { 8 | method: 'GET', 9 | query: { 10 | pageSize: 10, 11 | pageNumber: params.pageNumber || 1 12 | } 13 | }) 14 | } 15 | 16 | export function deleteArticleById (id) { 17 | return fetch(`/api/article/${id}`, { 18 | method: 'delete' 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | import koaRouter from 'koa-router' 2 | import upload from './upload' 3 | import user from './user' 4 | import login from './login' 5 | import article from './article' 6 | import comment from './comment' 7 | 8 | const router = koaRouter() 9 | 10 | router.use('/api', upload.routes()) 11 | router.use('/api/user', user.routes()) 12 | router.use('/api', login.routes()) 13 | router.use('/api/article', article.routes()) 14 | router.use('/api/comment', comment.routes()) 15 | 16 | module.exports = router 17 | -------------------------------------------------------------------------------- /server/model/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/12. 3 | */ 4 | 5 | import mongoose from 'mongoose' 6 | import { db } from '../../config' 7 | 8 | mongoose.Promise = global.Promise 9 | 10 | function startDB (fn) { 11 | var connection = mongoose.connect(db.url, error => { 12 | if (error) { 13 | console.log(error) 14 | fn(error) 15 | return 16 | } 17 | console.log(`connect to db on ${db.url}`) 18 | fn() 19 | }) 20 | global.connection = connection 21 | } 22 | 23 | module.exports = startDB 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | "extends": "standard", 9 | "plugins": [ 10 | "react" 11 | ], 12 | "env": { 13 | "browser": true 14 | }, 15 | "rules": { 16 | // allow paren-less arrow functions 17 | "arrow-parens": 0, 18 | "no-extra-parens": 0, 19 | "react/jsx-uses-vars": 1, 20 | "react/jsx-uses-react": "error" 21 | } 22 | } -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/9/9. 3 | */ 4 | 5 | import React from 'react' 6 | import ReactDOM from 'react-dom' 7 | import {browserHistory} from 'react-router' 8 | import {syncHistoryWithStore} from 'react-router-redux' 9 | import configureStore from './redux/store/index' 10 | import Root from './Root' 11 | 12 | const store = configureStore() 13 | const history = syncHistoryWithStore(browserHistory, store) 14 | const root = () 15 | 16 | ReactDOM.render(root, document.getElementById('root')) 17 | 18 | -------------------------------------------------------------------------------- /server/model/UserGroup.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { addPublicHook } from './util' 3 | var Schema = mongoose.Schema 4 | 5 | var UserGroupSchema = new Schema({ 6 | name: String, 7 | des: String, 8 | creater: {type: Schema.Types.ObjectId, ref: 'User'}, 9 | users: [{type: Schema.Types.ObjectId, ref: 'User'}], 10 | createTime: {type: Date, default: Date.now}, 11 | updateTime: {type: Date, default: Date.now} 12 | }) 13 | 14 | addPublicHook(UserGroupSchema) 15 | 16 | var UserGroup = mongoose.model('UserGroup', UserGroupSchema) 17 | 18 | module.exports = UserGroup 19 | -------------------------------------------------------------------------------- /client/frontend/Layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/24. 3 | */ 4 | 5 | import React, {Component} from 'react' 6 | import { Link } from 'react-router' 7 | 8 | export default class Admin extends Component { 9 | state = { 10 | 11 | } 12 | 13 | componentDidMount () { 14 | console.log('parent did mount') 15 | } 16 | 17 | render () { 18 | const { 19 | children 20 | } = this.props 21 | 22 | return ( 23 |
24 |
25 | {children} 26 |
27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/model/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { addPublicHook } from './util' 3 | var Schema = mongoose.Schema 4 | 5 | var UserSchema = new Schema({ 6 | username: String, 7 | nickname: String, 8 | password: String, 9 | avatar: String, 10 | creater: {type: Schema.Types.ObjectId, ref: 'User'}, 11 | group: {type: Schema.Types.ObjectId, ref: 'UserGroup'}, 12 | createTime: {type: Date, default: Date.now}, 13 | updateTime: {type: Date, default: Date.now} 14 | }) 15 | 16 | addPublicHook(UserSchema) 17 | 18 | var User = mongoose.model('User', UserSchema) 19 | 20 | module.exports = User 21 | -------------------------------------------------------------------------------- /client/redux/reducers/system/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/2/2. 3 | */ 4 | 5 | import {combineReducers} from 'redux' 6 | import constants from '../../constants/' 7 | 8 | /** 9 | * 如果flag是0:说明服务端已经渲染完成,不需要调用接口渲染数据, 10 | * 如果flag是1:说明跳出同构页面,需要重新调用接口 11 | * @param state 12 | * @param action 13 | * @returns {{flag: number}} 14 | */ 15 | function serverRender (state = {flag: 0}, action) { 16 | switch (action.type) { 17 | case constants.system.SERVER_RENDERED: 18 | return {flag: 1} 19 | } 20 | return state 21 | } 22 | 23 | export default combineReducers({ 24 | serverRender 25 | }) 26 | -------------------------------------------------------------------------------- /client/component/Page/style.less: -------------------------------------------------------------------------------- 1 | .page{ 2 | text-align: right; 3 | moz-user-select: -moz-none; 4 | -moz-user-select: none; 5 | -o-user-select:none; 6 | -khtml-user-select:none; 7 | -webkit-user-select:none; 8 | -ms-user-select:none; 9 | user-select:none; 10 | .page_no{ 11 | display: inline-block; 12 | margin: 0 5px; 13 | cursor: pointer; 14 | padding: 5px 10px; 15 | overflow: hidden; 16 | &.current{ 17 | background: #64cdff; 18 | border-radius: 2px; 19 | color: #fff; 20 | } 21 | &.page_disabled{ 22 | cursor: default; 23 | opacity: 0.3; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/model/Article.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { addPublicHook } from './util' 3 | 4 | const Schema = mongoose.Schema 5 | 6 | var ArticleSchema = new Schema({ 7 | title: String, 8 | content: String, 9 | inType: {type: Number, enum: [1, 2]}, // 1: pc创建 2: mobile创建 10 | tags: {type: Array, default: []}, 11 | creater: {type: Schema.Types.ObjectId, ref: 'User'}, 12 | createTime: {type: Date, default: Date.now}, 13 | updateTime: {type: Date, default: Date.now} 14 | }) 15 | 16 | addPublicHook(ArticleSchema) 17 | 18 | var Article = mongoose.model('Article', ArticleSchema) 19 | 20 | module.exports = Article 21 | -------------------------------------------------------------------------------- /client/component/Back/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/12. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | 7 | export default class index extends Component { 8 | constructor (props) { 9 | super(props) 10 | this.handleClick = this.handleClick.bind(this) 11 | } 12 | 13 | static contextTypes = { 14 | router: React.PropTypes.object.isRequired 15 | } 16 | 17 | handleClick (e) { 18 | this.context.router.goBack() 19 | } 20 | 21 | render () { 22 | return ( 23 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webpack/server/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/9/22. 3 | */ 4 | 5 | import path from 'path' 6 | import WebpackIsomorphicTools from 'webpack-isomorphic-tools' 7 | import co from 'co' 8 | import startDB from '../../server/model/' 9 | 10 | import isomorphicToolsConfig from '../isomorphic.tools.config' 11 | 12 | var basePath = path.join(__dirname, '../../') 13 | 14 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(isomorphicToolsConfig) 15 | // .development(true) 16 | .server(basePath, () => { 17 | const startServer = require('./server') 18 | co(function *() { 19 | yield startDB 20 | yield startServer 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /client/backend/group/Add.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import {connect} from 'react-redux' 6 | 7 | import EditBase from './EditBase' 8 | import { addGroup, clearGroup } from '../../redux/actions/group/' 9 | 10 | class GroupAdd extends EditBase { 11 | save (event) { 12 | const { addGroup } = this.props 13 | const { name, des } = this.state 14 | event.preventDefault() 15 | addGroup(name, des) 16 | } 17 | } 18 | 19 | function mapStateToProps (state, ownProps) { 20 | return { 21 | groupChanged: state.group.changed, 22 | detail: state.group.detail 23 | } 24 | } 25 | 26 | export default connect(mapStateToProps, { 27 | addGroup, 28 | clearGroup 29 | })(GroupAdd) 30 | -------------------------------------------------------------------------------- /server/model/Comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/13. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | var Schema = mongoose.Schema 7 | 8 | var CommentSchema = new Schema({ 9 | content: String, 10 | article: {type: Schema.Types.ObjectId, ref: 'Article'}, 11 | reply: {type: Schema.Types.ObjectId, ref: 'Comment'}, 12 | zan: [{type: Schema.Types.ObjectId, ref: 'User', default: []}], 13 | fan: [{type: Schema.Types.ObjectId, ref: 'User', default: []}], 14 | creater: {type: Schema.Types.ObjectId, ref: 'User'}, 15 | createTime: {type: Date, default: Date.now}, 16 | updateTime: {type: Date, default: Date.now} 17 | }) 18 | 19 | var Comment = mongoose.model('Comment', CommentSchema) 20 | 21 | module.exports = Comment 22 | -------------------------------------------------------------------------------- /client/backend/article/Add.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import {connect} from 'react-redux' 6 | 7 | import EditBase from './EditBase' 8 | import { addArticle, clearArticle } from '../../redux/actions/article/' 9 | 10 | class GroupAdd extends EditBase { 11 | save (event) { 12 | const { addArticle } = this.props 13 | const { title, content, tags } = this.state 14 | event.preventDefault() 15 | addArticle(title, content, tags) 16 | } 17 | } 18 | 19 | function mapStateToProps (state, ownProps) { 20 | return { 21 | articleChanged: state.article.changed, 22 | detail: state.article.detail 23 | } 24 | } 25 | 26 | export default connect(mapStateToProps, { 27 | addArticle, 28 | clearArticle 29 | })(GroupAdd) 30 | -------------------------------------------------------------------------------- /webpack/client/index.js: -------------------------------------------------------------------------------- 1 | import koa from 'koa' 2 | import webpack from 'webpack' 3 | import koaWebpackDevMiddleware from 'koa-webpack-dev-middleware' 4 | import koaWebpackHotMiddleware from 'koa-webpack-hot-middleware' 5 | import {client} from '../../config' 6 | import webpackClientConfig from './client.config.js' 7 | import webpackServerConfig from './server.config.js' 8 | 9 | var app = koa() 10 | var compile = webpack(webpackClientConfig) 11 | 12 | app.use(koaWebpackDevMiddleware(compile, { 13 | ...webpackServerConfig, 14 | publicPath: webpackClientConfig.output.publicPath 15 | })) 16 | app.use(koaWebpackHotMiddleware(compile)) 17 | 18 | app.listen(client.port, '0.0.0.0', error => { 19 | if (error) { 20 | console.log(error) 21 | return 22 | } 23 | console.log(`listening on ${client.port}`) 24 | }) 25 | 26 | 27 | -------------------------------------------------------------------------------- /client/backend/group/Edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import {connect} from 'react-redux' 6 | 7 | import EditBase from './EditBase' 8 | import { getDetail, saveDetail, clearGroup } from '../../redux/actions/group/' 9 | 10 | class GroupEdit extends EditBase { 11 | componentDidMount () { 12 | const { getDetail, routeParams } = this.props 13 | getDetail(routeParams.id) 14 | } 15 | 16 | save (event) { 17 | const { detail, saveDetail } = this.props 18 | const { name, des } = this.state 19 | event.preventDefault() 20 | saveDetail(detail._id, name, des) 21 | } 22 | 23 | } 24 | 25 | function mapStateToProps (state, ownProps) { 26 | return { 27 | groupChanged: state.group.changed, 28 | detail: state.group.detail 29 | } 30 | } 31 | 32 | export default connect(mapStateToProps, { 33 | getDetail, 34 | saveDetail, 35 | clearGroup 36 | })(GroupEdit) 37 | -------------------------------------------------------------------------------- /client/redux/reducers/login/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/21. 3 | */ 4 | 5 | import {combineReducers} from 'redux' 6 | import constants from '../../constants/' 7 | 8 | /** 9 | * 10 | * @param state 0: 初始状态 1: 开始登录 2: 登录成功 3: 登录失败 11 | * @param action 12 | */ 13 | function loginStatus (state = {status: 0, user: {}}, action) { 14 | switch (action.type) { 15 | case constants.login.CLEAR: 16 | return { 17 | status: 0, 18 | user: {} 19 | } 20 | case constants.login.START: 21 | return { 22 | status: 1, 23 | user: {} 24 | } 25 | case constants.login.SUCCESS: 26 | return { 27 | status: 2, 28 | user: action.user 29 | } 30 | case constants.login.FAIL: 31 | return { 32 | status: 3, 33 | user: {} 34 | } 35 | } 36 | return state 37 | } 38 | 39 | export default combineReducers({ 40 | loginStatus 41 | }) 42 | -------------------------------------------------------------------------------- /client/backend/article/Edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import {connect} from 'react-redux' 6 | 7 | import EditBase from './EditBase' 8 | import { getDetail, saveDetail, clearArticle } from '../../redux/actions/article/' 9 | 10 | class GroupEdit extends EditBase { 11 | componentDidMount () { 12 | const { getDetail, routeParams } = this.props 13 | getDetail(routeParams.id) 14 | } 15 | 16 | save (event) { 17 | const { detail, saveDetail } = this.props 18 | const { title, content, tags } = this.state 19 | event.preventDefault() 20 | saveDetail(detail._id, title, content, tags) 21 | } 22 | } 23 | 24 | function mapStateToProps (state, ownProps) { 25 | return { 26 | articleChanged: state.article.changed, 27 | detail: state.article.detail 28 | } 29 | } 30 | 31 | export default connect(mapStateToProps, { 32 | getDetail, 33 | saveDetail, 34 | clearArticle 35 | })(GroupEdit) 36 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | var User = require('../model/User') 2 | 3 | module.exports = function *(next) { 4 | var userId = this.cookies.get('userId') 5 | var user 6 | var path = this.request.path 7 | 8 | if (path === '/api/login') { 9 | return yield next 10 | } 11 | 12 | if (this.session.user) { 13 | if (path === '/login') { 14 | return this.redirect('/admin/dashboard') 15 | } 16 | return yield next 17 | } 18 | 19 | if (userId && (user = yield User.findOne({_id: userId}))) { 20 | this.session.user = user 21 | if (path === '/login') { 22 | return this.redirect('/admin/dashboard') 23 | } 24 | yield next 25 | } else { 26 | const { xhr } = this.request.header 27 | if (!xhr) { 28 | if (path !== '/login') { 29 | this.redirect('/login') 30 | } else { 31 | yield next 32 | } 33 | } else { 34 | this.throw('用户没有登陆', 401) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/model/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/17. 3 | */ 4 | export function convertUTCDateToLocalDate (date) { 5 | if (!(date instanceof Date)) { 6 | return 7 | } 8 | var newDate = new Date(date.getTime()) 9 | var offset = date.getTimezoneOffset() / 60 10 | var hours = date.getHours() 11 | newDate.setHours(hours - offset) 12 | return newDate 13 | // return new Date() 14 | } 15 | 16 | export function addPublicHook (Schema) { 17 | Schema.post('find', function(result) { 18 | if (Array.isArray(result)) { 19 | // result.forEach(updateTime) 20 | } else if (typeof result === 'object') { 21 | // updateTime(result) 22 | } 23 | }) 24 | 25 | Schema.pre('update', function(next) { 26 | this.update({}, {$set: {updateTime: new Date}}) 27 | next() 28 | }) 29 | } 30 | 31 | function updateTime (obj) { 32 | obj.createTime = convertUTCDateToLocalDate(obj.createTime) 33 | obj.updateTime = convertUTCDateToLocalDate(obj.updateTime) 34 | } 35 | -------------------------------------------------------------------------------- /client/redux/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore, compose, applyMiddleware} from 'redux' 2 | import thunk from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import rootReducer from '../reducers' 5 | 6 | const middlewares = [] 7 | const logger = createLogger() 8 | 9 | middlewares.push(thunk) 10 | // 服务器端不打印redux-log 11 | typeof window !== 'undefined' ? middlewares.push(logger) : '' 12 | 13 | const configureStoreProd = (initialState = window.__INITIAL_STATE__) => { 14 | const finalCreateStore = compose( 15 | applyMiddleware(...middlewares) 16 | )(createStore) 17 | 18 | const store = finalCreateStore(rootReducer, initialState) 19 | 20 | if (module.hot) { 21 | // Enable Webpack hot module replacement for reducers 22 | module.hot.accept('../reducers', () => { 23 | const nextReducer = require('../reducers').default // eslint-disable-line 24 | store.replaceReducer(nextReducer) 25 | }) 26 | } 27 | 28 | return store 29 | } 30 | 31 | export default configureStoreProd 32 | -------------------------------------------------------------------------------- /server/router/login.js: -------------------------------------------------------------------------------- 1 | var router = require('koa-router')() 2 | var crypto = require('crypto') 3 | var util = require('../../util/index') 4 | var User = require('../model/User') 5 | 6 | router.post('/login', function *(next) { 7 | var body = this.request.body 8 | var password = crypto.createHash('md5').update(body.password).digest('hex') 9 | 10 | var user = yield User.findOne({username: body.username, password: password}) 11 | 12 | if (user) { 13 | this.cookies.set('userId', user._id, { 14 | signed: false, 15 | expires: util.getDate(7) 16 | }) 17 | this.session.user = user 18 | this.body = user 19 | } else { 20 | this.status = 401 21 | this.body = { 22 | 'message': '登录失败' 23 | } 24 | } 25 | }) 26 | 27 | router.get('/logout', function *(next) { 28 | this.cookies.set('userId', 0, { 29 | signed: false, 30 | expires: util.getDate(-1) 31 | }) 32 | delete this.session.user 33 | this.body = { 34 | message: '退出成功' 35 | } 36 | }) 37 | 38 | module.exports = router 39 | -------------------------------------------------------------------------------- /bin/start.js: -------------------------------------------------------------------------------- 1 | import co from 'co' 2 | import crypto from 'crypto' 3 | import startDB from '../server/model/' 4 | import User from '../server/model/User' 5 | import UserGroup from '../server/model/UserGroup' 6 | 7 | co(function *() { 8 | yield startDB 9 | 10 | var group = yield UserGroup.create({ 11 | name: '管理员', 12 | des: '我是一个管理员' 13 | }) 14 | 15 | var password = '123456' 16 | var passwordHashed = crypto.createHash('md5').update(password).digest('hex') 17 | 18 | var user = yield User.create({ 19 | username: 'admin', 20 | nickname: '张大哥哥', 21 | avatar: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1493376166407&di=21fd37a56ba58374f0a8e960aa91cad5&imgtype=0&src=http%3A%2F%2Fimg.iecity.com%2FUpload%2FFile%2F201511%2F30%2F20151130095248246.jpg', 22 | group: group._id, 23 | password: passwordHashed 24 | }) 25 | 26 | console.log('初始化用户成功') 27 | console.log('用户名: ' + 'admin') 28 | console.log('密码: ' + password) 29 | process.exit() 30 | }).catch(function(e) { 31 | console.log('初始化用户失败') 32 | console.log(e) 33 | process.exit() 34 | }) -------------------------------------------------------------------------------- /client/backend/user/Add.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import {connect} from 'react-redux' 6 | 7 | import EditBase from './EditBase' 8 | import { addUser, clearUser } from '../../redux/actions/user/' 9 | import { getGroupList } from '../../redux/actions/group/' 10 | 11 | class UserAdd extends EditBase { 12 | componentDidMount () { 13 | const { getGroupList } = this.props 14 | getGroupList() 15 | } 16 | 17 | save (event) { 18 | const { addUser } = this.props 19 | const { username, password, groupId, nickname, avatar } = this.state 20 | 21 | var isValid = super.validate() 22 | event.preventDefault() 23 | if (isValid) { 24 | addUser(username, nickname, avatar, password, groupId) 25 | } 26 | } 27 | } 28 | 29 | function mapStateToProps (state, ownProps) { 30 | return { 31 | userChanged: state.user.changed, 32 | detail: state.user.detail, 33 | group: state.group.listData 34 | } 35 | } 36 | 37 | export default connect(mapStateToProps, { 38 | getGroupList, 39 | addUser, 40 | clearUser 41 | })(UserAdd) 42 | -------------------------------------------------------------------------------- /client/backend/user/Edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import {connect} from 'react-redux' 6 | 7 | import EditBase from './EditBase' 8 | import { clearUser, getDetail, saveDetail } from '../../redux/actions/user/' 9 | import { getGroupList } from '../../redux/actions/group/' 10 | 11 | class GroupEdit extends EditBase { 12 | componentDidMount () { 13 | const { getDetail, routeParams, getGroupList } = this.props 14 | getGroupList() 15 | getDetail(routeParams.id) 16 | } 17 | 18 | save (event) { 19 | const { detail, saveDetail } = this.props 20 | const { username, nickname, password, groupId, avatar } = this.state 21 | event.preventDefault() 22 | saveDetail(detail._id, username, nickname, avatar, password, groupId) 23 | } 24 | } 25 | 26 | function mapStateToProps (state, ownProps) { 27 | return { 28 | userChanged: state.user.changed, 29 | detail: state.user.detail, 30 | group: state.group.listData 31 | } 32 | } 33 | 34 | export default connect(mapStateToProps, { 35 | getGroupList, 36 | clearUser, 37 | getDetail, 38 | saveDetail 39 | })(GroupEdit) 40 | -------------------------------------------------------------------------------- /client/redux/actions/login/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/21. 3 | */ 4 | import constants from '../../constants/' 5 | import fetch from '../../../../util/fetch' 6 | 7 | export function login (username, password) { 8 | return dispatch => { 9 | dispatch({type: constants.login.START}) 10 | loginFetch(username, password) 11 | .then(data => { 12 | dispatch({ 13 | type: constants.login.SUCCESS, 14 | user: data 15 | }) 16 | }, e => { 17 | dispatch({ 18 | type: constants.login.FAIL 19 | }) 20 | }) 21 | } 22 | } 23 | 24 | export function logOut () { 25 | return dispatch => { 26 | logoutFetch() 27 | .then(data => { 28 | dispatch({ 29 | type: constants.login.CLEAR 30 | }) 31 | }) 32 | } 33 | } 34 | 35 | function loginFetch (username, password) { 36 | return fetch(`/api/login`, { 37 | method: 'POST', 38 | body: { 39 | username, 40 | password 41 | } 42 | }) 43 | } 44 | 45 | function logoutFetch () { 46 | return fetch('/api/logout', { 47 | method: 'GET' 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get oneDayTime () { 3 | return 1000 * 60 * 60 * 24 4 | }, 5 | getDate (day) { 6 | var now = Date.now() 7 | var time = this.oneDayTime * day + now 8 | var dateReturn = new Date() 9 | dateReturn.setTime(time) 10 | return dateReturn 11 | }, 12 | timestampFormat (timestamp, format = 'yyyy-MM-dd') { 13 | if (!timestamp) { 14 | return '' 15 | } 16 | return this.format(new Date(timestamp), format) 17 | }, 18 | format (date, format) { 19 | var o = { 20 | 'M+': date.getMonth() + 1, 21 | // month 22 | 'd+': date.getDate(), 23 | // day 24 | 'h+': date.getHours(), 25 | // hour 26 | 'm+': date.getMinutes(), 27 | // minute 28 | 's+': date.getSeconds(), 29 | // second 30 | 'q+': Math.floor((date.getMonth() + 3) / 3), 31 | // quarter 32 | 'S': date.getMilliseconds() 33 | // millisecond 34 | } 35 | if (/(y+)/i.test(format)) { 36 | format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) 37 | } 38 | for (var k in o) { 39 | if (new RegExp('(' + k + ')', 'i').test(format)) { 40 | format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)) 41 | } 42 | } 43 | return format 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/middleware/multipartParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/18. 3 | */ 4 | 5 | import path from 'path' 6 | import formidable from 'formidable' 7 | 8 | const defaultOptions = { 9 | 10 | } 11 | 12 | export default function (options) { 13 | return function * fileHandler (next) { 14 | var _options = { 15 | ...defaultOptions, 16 | ...options 17 | } 18 | var form 19 | var files 20 | 21 | if (!isValid(this.method)) { 22 | yield next 23 | } 24 | 25 | if (this.is('multipart/form-data')) { 26 | files = yield parseForm(_options, this.req) 27 | this.request.files = files 28 | } 29 | 30 | yield next 31 | } 32 | } 33 | 34 | function parseForm (options, request) { 35 | var files = [] 36 | 37 | return function (done) { 38 | var form = new formidable.IncomingForm(options) 39 | form.uploadDir = options.uploadDir 40 | form.keepExtensions = true 41 | form.on('error', done) 42 | form.on('aborted', done) 43 | form.on('file', function (name, value) { 44 | var json = value.toJSON() 45 | json.filename = '/' + path.parse(json.path).base 46 | files.push([name, json]) 47 | }) 48 | form.on('end', function () { 49 | done(null, files) 50 | }) 51 | 52 | form.parse(request) 53 | } 54 | } 55 | 56 | function isValid (method) { 57 | return ['GET', 'HEAD', 'DELETE'].indexOf(method.toUpperCase()) === -1 58 | } 59 | -------------------------------------------------------------------------------- /client/redux/resouces/article/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/26. 3 | */ 4 | import fetch from '../../../../util/fetch' 5 | 6 | export function getList (params = {}, cookie) { 7 | return fetch('/api/article/', { 8 | method: 'GET', 9 | query: { 10 | pageSize: 10, 11 | pageNumber: params.pageNumber || 1 12 | }, 13 | cookie 14 | }) 15 | } 16 | 17 | export function getView (params = {}, cookie) { 18 | return fetch(`/api/article/${params.id}`, { 19 | method: 'GET', 20 | cookie 21 | }) 22 | } 23 | 24 | export function saveComment (params = {}, cookie) { 25 | return fetch(`/api/comment/${params.articleId}`, { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/x-www-form-urlencoded' 29 | }, 30 | body: { 31 | content: params.commentContent 32 | }, 33 | cookie 34 | }) 35 | } 36 | 37 | export function getCommentList (params = {}, cookie) { 38 | return fetch(`/api/comment/${params.articleId}`, { 39 | method: 'GET', 40 | cookie 41 | }) 42 | } 43 | 44 | export function zan (params = {}) { 45 | return fetch(`/api/comment/zan/${params.commentId}`, { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/x-www-form-urlencoded' 49 | } 50 | }) 51 | } 52 | 53 | export function fan (params = {}) { 54 | return fetch(`/api/comment/fan/${params.commentId}`, { 55 | method: 'POST', 56 | headers: { 57 | 'Content-Type': 'application/x-www-form-urlencoded' 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /client/redux/reducers/group/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/17. 3 | */ 4 | 5 | import {combineReducers} from 'redux' 6 | import constants from '../../constants/' 7 | 8 | const { group } = constants 9 | 10 | var groupInitState = { 11 | status: 0 // 0: 正常状态 1: 开始保存 2: 保存成功 3: 保存失败 4: 获取成功 12 | } 13 | 14 | /** 15 | * 16 | * @param state 17 | * @param action 18 | */ 19 | function changed (state = groupInitState, action) { 20 | switch (action.type) { 21 | case group.CLEAR: 22 | return {...state, status: 0} 23 | case group.START_ADD: 24 | return {...state, status: 1} 25 | case group.ADD_SUCCESS: 26 | return {...state, status: 2} 27 | case group.GET_DETAIL_SUCCESS: 28 | return {...state, status: 4} 29 | } 30 | 31 | return state 32 | } 33 | 34 | /** 35 | * 36 | * @param state 37 | * @param action 38 | * @returns {*} 39 | */ 40 | function listData (state = {list: [], page: {}}, action) { 41 | switch (action.type) { 42 | case group.GET_LIST_SUCCESS: 43 | return {...state, list: action.list, page: action.page} 44 | } 45 | return state 46 | } 47 | 48 | /** 49 | * 50 | */ 51 | function detail (state = {_id: '', name: '', des: ''}, action) { 52 | switch (action.type) { 53 | case group.CLEAR: 54 | return {_id: '', name: '', des: ''} 55 | case group.GET_DETAIL_SUCCESS: 56 | return {_id: action.data._id, name: action.data.name, des: action.data.des} 57 | } 58 | return state 59 | } 60 | 61 | export default combineReducers({ 62 | changed, 63 | listData, 64 | detail 65 | }) 66 | -------------------------------------------------------------------------------- /client/redux/constants/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/2. 3 | */ 4 | 5 | export default { 6 | system: { 7 | SERVER_RENDERED: 'SERVER_RENDERED' 8 | }, 9 | group: { 10 | CLEAR: 'CLEAR_GROUP', 11 | START_ADD: 'START_ADD_GROUP', 12 | ADD_SUCCESS: 'ADD_GROUP_SUCCESS', 13 | ADD_FAILED: 'ADD_GROUP_FAILED', 14 | GET_LIST_SUCCESS: 'GET_GROUP_LIST_SUCCESS', 15 | GET_DETAIL_SUCCESS: 'GET_GROUP_DETAIL_SUCCESS', 16 | SAVE_DETAIL_SUCCESS: 'SAVE_GROUP_DETAIL_SUCCESS' 17 | }, 18 | user: { 19 | CLEAR: 'CLEAR_USER', 20 | START_ADD: 'START_ADD_USER', 21 | ADD_SUCCESS: 'ADD_USER_SUCCESS', 22 | ADD_FAILED: 'ADD_USER_FAILED', 23 | GET_LIST_SUCCESS: 'GET_USER_LIST_SUCCESS', 24 | GET_DETAIL_SUCCESS: 'GET_USER_DETAIL_SUCCESS', 25 | SAVE_DETAIL_SUCCESS: 'SAVE_USER_DETAIL_SUCCESS', 26 | LOGIN_USER: 'LOGIN_USER' 27 | }, 28 | login: { 29 | CLEAR: 'CLEAR_LOGIN', 30 | START: 'START_LOGIN', 31 | SUCCESS: 'LOGIN_SUCCESS', 32 | FAIL: 'LOGIN_FAIL' 33 | }, 34 | article: { 35 | CLEAR: 'CLEAR_ARTICLE', 36 | START_ADD: 'START_ADD_ARTICLE', 37 | ADD_SUCCESS: 'ADD_ARTICLE_SUCCESS', 38 | ADD_FAILED: 'ADD_ARTICLE_FAILED', 39 | GET_LIST_SUCCESS: 'GET_ARTICLE_LIST_SUCCESS', 40 | GET_DETAIL_SUCCESS: 'GET_ARTICLE_DETAIL_SUCCESS', 41 | SAVE_DETAIL_SUCCESS: 'SAVE_ARTICLE_DETAIL_SUCCESS', 42 | CLEAR_DETAIL_VIEW: 'CLEAR_ARTICLE_DETAIL_VIEW', 43 | GET_DETAIL_VIEW_SUCCESS: 'GET_ARTICLE_DETAIL_VIEW_SUCCESS', 44 | CLEAR_LIST_VIEW: 'CLEAR_ARTICLE_LIST_VIEW', 45 | GET_LIST_VIEW_SUCCESS: 'GET_ARTICLE_LIST_VIEW_SUCCESS', 46 | GET_COMMENT: 'GET_COMMENT' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webpack/server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/9/22. 3 | */ 4 | import path from 'path' 5 | import zlib from 'zlib' 6 | import Koa from 'koa' 7 | import bodyParser from 'koa-bodyparser' 8 | import logger from 'koa-logger' 9 | import session from 'koa-session-store' 10 | import MongooseStore from 'koa-session-mongoose' 11 | import favicon from 'koa-favicon' 12 | import compress from 'koa-compress' 13 | import koaStatic from 'koa-static' 14 | 15 | import {server} from '../../config' 16 | import handleRender from './handleRender' 17 | import router from '../../server/router/' 18 | import auth from '../../server/middleware/auth' 19 | import multipartParser from '../../server/middleware/multipartParser' 20 | 21 | var app = Koa() 22 | 23 | // cookie签名 24 | app.keys = ['gg', 'fat gg'] 25 | 26 | app.use(favicon(path.resolve('./public/favicon.ico'))) 27 | app.use(logger()) 28 | app.use(session({ 29 | store: new MongooseStore() 30 | })) 31 | app.use(bodyParser()) 32 | app.use(multipartParser({ 33 | uploadDir: path.resolve('./upload') 34 | })) 35 | app.use(compress({ 36 | filter: function (content_type) { 37 | return /text/i.test(content_type) 38 | }, 39 | threshold: 2048, 40 | flush: zlib.Z_SYNC_FLUSH 41 | })) 42 | app.use(koaStatic(path.resolve('./public'))) 43 | app.use(koaStatic(path.resolve('./upload'))) 44 | 45 | app.use(auth) 46 | app.use(router.routes()) 47 | app.use(handleRender) 48 | 49 | app.on('error', function(err){ 50 | console.log(err) 51 | }) 52 | 53 | export default function (fn) { 54 | app.listen(server.port, '0.0.0.0', error => { 55 | if (error) { 56 | fn(error) 57 | return 58 | } 59 | console.log(`server start on ${server.port}`) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /client/backend/group/List.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import { connect } from 'react-redux' 7 | import { Link } from 'react-router' 8 | import { getGroupList } from '../../redux/actions/group/' 9 | 10 | class GroupList extends Component { 11 | componentDidMount () { 12 | const { getGroupList } = this.props 13 | getGroupList() 14 | } 15 | 16 | render () { 17 | const { list } = this.props.listData 18 | 19 | return (
20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {list.map(group => { 36 | return ( 37 | 38 | 39 | 43 | ) 44 | })} 45 | 46 | 47 |
用户组名称描述操作
{group.name}{group.des} 40 | 41 | 42 |
48 |
) 49 | } 50 | } 51 | 52 | function mapStateToProps (state, ownProps) { 53 | return { 54 | listData: state.group.listData 55 | } 56 | } 57 | 58 | export default connect(mapStateToProps, { 59 | getGroupList 60 | })(GroupList) 61 | -------------------------------------------------------------------------------- /client/redux/reducers/user/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/17. 3 | */ 4 | 5 | import {combineReducers} from 'redux' 6 | import constants from '../../constants/' 7 | 8 | const { user } = constants 9 | 10 | var groupInitState = { 11 | status: 0 // 0: 正常状态 1: 开始保存 2: 保存成功 3: 保存失败 4: 获取成功 12 | } 13 | 14 | /** 15 | * 16 | * @param state 17 | * @param action 18 | */ 19 | function changed (state = groupInitState, action) { 20 | switch (action.type) { 21 | case user.CLEAR: 22 | return {...state, status: 0} 23 | case user.START_ADD: 24 | return {...state, status: 1} 25 | case user.ADD_SUCCESS: 26 | return {...state, status: 2} 27 | case user.GET_DETAIL_SUCCESS: 28 | return {...state, status: 4} 29 | } 30 | 31 | return state 32 | } 33 | 34 | /** 35 | * 36 | * @param state 37 | * @param action 38 | * @returns {*} 39 | */ 40 | function listData (state = {list: [], page: {}}, action) { 41 | switch (action.type) { 42 | case user.GET_LIST_SUCCESS: 43 | return {...state, list: action.list, page: action.page} 44 | } 45 | return state 46 | } 47 | 48 | /** 49 | * 50 | */ 51 | function detail (state = {_id: '', nickname: '', username: '', groupId: ''}, action) { 52 | switch (action.type) { 53 | case user.CLEAR: 54 | return {_id: '', username: '', nickname: '', groupId: ''} 55 | case user.GET_DETAIL_SUCCESS: 56 | return { 57 | _id: action.data._id, 58 | username: action.data.username, 59 | nickname: action.data.nickname, 60 | groupId: action.data.group._id, 61 | avatar: action.data.avatar 62 | } 63 | } 64 | return state 65 | } 66 | 67 | function loginUser (state = {}, action) { 68 | switch (action.type) { 69 | case user.LOGIN_USER: 70 | return {...action.data} 71 | } 72 | 73 | return state 74 | } 75 | 76 | export default combineReducers({ 77 | changed, 78 | listData, 79 | detail, 80 | loginUser 81 | }) 82 | -------------------------------------------------------------------------------- /client/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, IndexRoute, Redirect} from 'react-router' 3 | import Layout from '../backend/Layout' 4 | import Login from '../backend/login/Login' 5 | import Admin from '../backend/Admin' 6 | import Dashboard from '../backend/dashboard/' 7 | import GroupList from '../backend/group/List' 8 | import GroupAdd from '../backend/group/Add' 9 | import GroupEdit from '../backend/group/Edit' 10 | import User from '../backend/user/List' 11 | import UserAdd from '../backend/user/Add' 12 | import UserEdit from '../backend/user/Edit' 13 | import Article from '../backend/article/List' 14 | import ArticleAdd from '../backend/article/Add' 15 | import ArticleEdit from '../backend/article/Edit' 16 | 17 | import ArticleList from '../frontend/article/list' 18 | import ArticleView from '../frontend/article/view' 19 | 20 | export default ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | -------------------------------------------------------------------------------- /util/fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/17. 3 | */ 4 | 5 | import fetch from 'isomorphic-fetch' 6 | import { server as serverConfig } from '../config' 7 | 8 | module.exports = function (url, fetchConfig) { 9 | var urlPre = '' 10 | 11 | try { 12 | window 13 | } catch (e) { 14 | urlPre = `http://${serverConfig.host}:${serverConfig.port}` 15 | } 16 | 17 | const config = { 18 | credentials: 'include' 19 | } 20 | var fetchPromise 21 | var body, query, _query 22 | 23 | fetchConfig.headers = fetchConfig.headers || {} 24 | fetchConfig.headers['xhr'] = 'xhr' 25 | 26 | if (fetchConfig.cookie) { 27 | fetchConfig.headers['cookie'] = fetchConfig.cookie 28 | delete fetchConfig.cookie 29 | } 30 | 31 | if (/post|put/i.test(fetchConfig.method)) { 32 | body = fetchConfig.body 33 | if (typeof body === 'object') { 34 | fetchConfig.body = Object.keys(body).map(key => `${key}=${body[key]}`).join('&') 35 | } 36 | fetchConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded' 37 | } else { 38 | query = fetchConfig.query 39 | if (typeof query === 'object') { 40 | _query = Object.keys(query).map(key => `${key}=${query[key]}`).join('&') 41 | if (url.indexOf('?') !== -1) { 42 | url += '&' + _query 43 | } else { 44 | url += '?' + _query 45 | } 46 | } 47 | } 48 | 49 | fetchPromise = fetch(urlPre + url, { 50 | ...config, 51 | ...fetchConfig 52 | }).then( 53 | response => { 54 | if (response.ok) { 55 | return response.json() 56 | } 57 | return Promise.reject(response) 58 | } 59 | ) 60 | 61 | fetchPromise.catch(e => { 62 | // 程序内部错误 63 | if (e instanceof Response) { 64 | switch (e.status) { 65 | case 401: 66 | // browserHistory.push('/login') 67 | break 68 | case 500: 69 | break 70 | } 71 | } else { 72 | } 73 | }) 74 | 75 | return fetchPromise 76 | } 77 | -------------------------------------------------------------------------------- /client/backend/login/Login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/14. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import {connect} from 'react-redux' 7 | import { login } from '../../redux/actions/login/' 8 | import { browserHistory } from 'react-router' 9 | 10 | class Login extends Component { 11 | constructor (props) { 12 | super(props) 13 | this.submit = this.submit.bind(this) 14 | } 15 | 16 | state = { 17 | username: '', 18 | password: '' 19 | } 20 | 21 | submit (e) { 22 | e.preventDefault() 23 | const { login } = this.props 24 | const { username, password } = this.state 25 | login(username, password) 26 | } 27 | 28 | componentWillReceiveProps (nextProps) { 29 | const { loginStatus } = nextProps 30 | switch (loginStatus.status) { 31 | case 2: 32 | setTimeout(() => { 33 | browserHistory.push('/admin/dashboard/') 34 | }) 35 | break 36 | } 37 | } 38 | 39 | render () { 40 | return (
41 |
42 |
43 |
登陆前端博客
44 |
45 |
46 | (this.setState({username: e.target.value}))} type="text" placeholder="输入用户名" /> 47 | (this.setState({password: e.target.value}))} type="password" placeholder="输入密码" /> 48 | 49 |
50 |
51 |
52 |
53 |
) 54 | } 55 | } 56 | 57 | function mapStateToProps (state, ownProps) { 58 | return { 59 | loginStatus: state.login.loginStatus 60 | } 61 | } 62 | 63 | export default connect(mapStateToProps, { 64 | login 65 | })(Login) 66 | -------------------------------------------------------------------------------- /client/backend/Admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import { Link } from 'react-router' 7 | import { connect } from 'react-redux' 8 | import { bindActionCreators } from 'redux' 9 | import { logOut } from '../redux/actions/login/' 10 | 11 | class Admin extends Component { 12 | state = { 13 | menu: [{ 14 | 'path': '/admin/group', 15 | 'name': '用户组管理' 16 | }, { 17 | 'path': '/admin/user', 18 | 'name': '用户管理' 19 | }, { 20 | 'path': '/admin/article', 21 | 'name': '文章管理' 22 | }] 23 | } 24 | 25 | static contextTypes = { 26 | router: React.PropTypes.object.isRequired 27 | } 28 | 29 | checkPathMatch (path) { 30 | const { pathname } = this.props.location 31 | return path === pathname 32 | } 33 | 34 | render () { 35 | const { 36 | children 37 | } = this.props 38 | 39 | return ( 40 |
41 |
42 |
43 | 50 |
51 |
52 | {children} 53 |
54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | function mapStateToProps (state, ownProps) { 61 | return { 62 | user: state.user.loginUser, 63 | loginStatus: state.login.loginStatus, 64 | system: state.system 65 | } 66 | } 67 | 68 | function mapDispatchToProps (dispatch, ownProps) { 69 | return { 70 | dispatch, 71 | ...bindActionCreators({ 72 | logOut 73 | }, dispatch) 74 | } 75 | } 76 | 77 | export default connect(mapStateToProps, mapDispatchToProps)(Admin) 78 | -------------------------------------------------------------------------------- /client/backend/user/List.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import { connect } from 'react-redux' 7 | import { Link } from 'react-router' 8 | import { getUserList } from '../../redux/actions/user/' 9 | 10 | class UserList extends Component { 11 | componentDidMount () { 12 | const { getUserList } = this.props 13 | getUserList() 14 | } 15 | 16 | render () { 17 | const { list } = this.props.listData 18 | 19 | return (
20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {list.map(user => { 37 | return ( 38 | 39 | 40 | 41 | 45 | ) 46 | })} 47 | 48 | 49 |
用户名昵称所属用户组操作
{user.username}{user.nickname}{user.group && user.group.name} 42 | 43 | 44 |
50 |
) 51 | } 52 | } 53 | 54 | function mapStateToProps (state, ownProps) { 55 | return { 56 | listData: state.user.listData 57 | } 58 | } 59 | 60 | export default connect(mapStateToProps, { 61 | getUserList 62 | })(UserList) 63 | -------------------------------------------------------------------------------- /server/router/comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 17/1/13. 3 | */ 4 | 5 | import koaRouter from 'koa-router' 6 | import Comment from '../model/Comment' 7 | import markdown from 'markdown' 8 | 9 | const router = koaRouter() 10 | 11 | router.post('/:articleId', function *(next) { 12 | var { articleId } = this.params 13 | var body = this.request.body 14 | var { content} = body 15 | yield Comment.create({ 16 | article: articleId, 17 | content, 18 | creater: this.session.user._id 19 | }) 20 | 21 | this.body = {code: 1000} 22 | }) 23 | 24 | router.get('/:articleId', function *(next) { 25 | var { articleId } = this.params 26 | var comment = yield Comment 27 | .find({article: articleId}) 28 | .populate('creater') 29 | 30 | if (Array.isArray(comment)) { 31 | comment.forEach(item => item.content = markdown.markdown.toHTML(item.content)) 32 | } 33 | 34 | this.body = comment 35 | }) 36 | 37 | router.post('/zan/:commentId', function * (next) { 38 | var { commentId } = this.params 39 | var comment 40 | var action 41 | 42 | var result = yield Comment.find({ 43 | _id: commentId, 44 | zan: this.session.user._id 45 | }) 46 | 47 | if (result.length) { 48 | action = 'pull' 49 | } else { 50 | action = 'addToSet' 51 | } 52 | 53 | /** 54 | * mongoose 操作子文档数组的方法和js数组方法类似,还扩充了一些 55 | * push 添加一条 56 | * addToSet 也是添加一条,不过会过滤重复项 57 | */ 58 | comment = yield Comment.update({_id: commentId}, {[`$${action}`]: {zan: this.session.user._id}}) 59 | this.body = comment 60 | }) 61 | 62 | router.post('/fan/:commentId', function * (next) { 63 | var { commentId } = this.params 64 | var comment 65 | var action 66 | 67 | var result = yield Comment.find({ 68 | _id: commentId, 69 | fan: this.session.user._id 70 | }) 71 | 72 | if (result.length) { 73 | action = 'pull' 74 | } else { 75 | action = 'addToSet' 76 | } 77 | 78 | comment = yield Comment.update({_id: commentId}, {[`$${action}`]: {fan: this.session.user._id}}) 79 | this.body = comment 80 | }) 81 | 82 | export default router 83 | -------------------------------------------------------------------------------- /webpack/server/Html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/9/22. 3 | */ 4 | 5 | import React, { Component, PropTypes } from 'react' 6 | import { renderToString } from 'react-dom/server' 7 | import {client} from '../../config' 8 | 9 | export default class Html extends Component { 10 | 11 | get scripts () { 12 | const { javascript } = this.props.assets 13 | 14 | return Object.keys(javascript).map((script, i) => 15 |