├── 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 | | {group.name} |
38 | {group.des} |
39 |
40 |
41 |
42 | |
43 |
)
44 | })}
45 |
46 |
47 |
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 |
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 | | {user.username} |
39 | {user.nickname} |
40 | {user.group && user.group.name} |
41 |
42 |
43 |
44 | |
45 |
)
46 | })}
47 |
48 |
49 |
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 |
16 | )
17 | }
18 |
19 | get styles () {
20 | const { assets } = this.props
21 | const { styles, assets: _assets } = assets
22 | const stylesArray = Object.keys(styles)
23 |
24 | // styles (will be present only in production with webpack extract text plugin)
25 | if (stylesArray.length !== 0) {
26 | return stylesArray.map((style, i) =>
27 |
28 | )
29 | }
30 |
31 | // (will be present only in development mode)
32 | // It's not mandatory but recommended to speed up loading of styles
33 | // (resolves the initial style flash (flicker) on page load in development mode)
34 | // const scssPaths = Object.keys(_assets).filter(asset => asset.includes('.css'))
35 | // return scssPaths.map((style, i) =>
36 | //
37 | // )
38 | }
39 |
40 | render () {
41 | const { component, store } = this.props
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 | 前端博客
51 |
52 | {this.styles}
53 |
54 |
55 |
56 |
57 |
58 | {this.scripts}
59 |
60 |
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/client/backend/group/EditBase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/19.
3 | */
4 |
5 | import React, { Component } from 'react'
6 | import { browserHistory } from 'react-router'
7 | import Back from '../../component/Back/index'
8 |
9 | export default class EditBase extends Component {
10 | constructor (props) {
11 | super(props)
12 | props.clearGroup()
13 | this.save = this.save.bind(this)
14 | }
15 |
16 | state = {
17 | name: this.props.detail.name || '',
18 | des: this.props.detail.des || ''
19 | }
20 |
21 | componentWillReceiveProps (nextProps) {
22 | const { groupChanged, detail } = nextProps
23 | switch (groupChanged.status) {
24 | case 2:
25 | setTimeout(() => {
26 | browserHistory.push('/admin/group/')
27 | })
28 | break
29 | case 0:
30 | case 4:
31 | this.state.name = detail.name
32 | this.state.des = detail.des
33 | break
34 | }
35 | }
36 |
37 | renderLoding () {
38 | var { status } = this.props.groupChanged
39 | switch (status) {
40 | case 1:
41 | return (
42 | 正在提交...
43 |
)
44 | case 2:
45 | return (
46 | 保存成功
47 |
)
48 | case 3:
49 | return (
50 | 保存失败
51 |
)
52 | }
53 | }
54 |
55 | render () {
56 | return (
57 | {this.renderLoding()}
58 |
76 |
)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/client/redux/actions/group/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/2.
3 | */
4 | import constants from '../../constants/'
5 | import fetch from '../../../../util/fetch'
6 |
7 | export function clearGroup () {
8 | return {type: constants.group.CLEAR}
9 | }
10 |
11 | export function addGroup (name, des) {
12 | return dispatch => {
13 | dispatch({type: constants.group.START_ADD})
14 | add(name, des)
15 | .then(data => {
16 | dispatch({type: constants.group.ADD_SUCCESS})
17 | })
18 | }
19 | }
20 |
21 | export function getGroupList () {
22 | return dispatch => {
23 | getList()
24 | .then(data => {
25 | dispatch({type: constants.group.GET_LIST_SUCCESS, list: data})
26 | })
27 | }
28 | }
29 |
30 | export function getDetail (id) {
31 | return dispatch => {
32 | getDetailById(id)
33 | .then(data => dispatch({
34 | type: constants.group.GET_DETAIL_SUCCESS,
35 | data: data
36 | }))
37 | }
38 | }
39 |
40 | export function saveDetail (id, name, des) {
41 | return dispatch => {
42 | saveDetailById(id, name, des)
43 | .then(data => dispatch({
44 | type: constants.group.ADD_SUCCESS
45 | }))
46 | }
47 | }
48 |
49 | /**
50 | * 添加用户组
51 | * @param name 名称
52 | * @param des 描述
53 | */
54 | function add (name, des) {
55 | return fetch('/api/user/group', {
56 | method: 'POST',
57 | headers: {
58 | 'Content-Type': 'application/x-www-form-urlencoded'
59 | },
60 | body: `name=${name}&des=${des}`
61 | })
62 | }
63 |
64 | /**
65 | * 获取用户组列表
66 | */
67 | function getList () {
68 | return fetch('/api/user/group', {
69 | method: 'GET',
70 | query: {
71 | pageSize: 10,
72 | pageCur: 0
73 | }
74 | })
75 | }
76 |
77 | /**
78 | * 根据用户组id获取详情
79 | * @param id
80 | */
81 | function getDetailById (id) {
82 | return fetch(`/api/user/group/${id}`, {
83 | method: 'GET'
84 | })
85 | }
86 |
87 | /**
88 | * 保存用户组详情
89 | * @param id
90 | * @param name
91 | * @param des
92 | */
93 | function saveDetailById (id, name, des) {
94 | return fetch(`/api/user/group/${id}`, {
95 | method: 'PUT',
96 | headers: {
97 | 'Content-Type': 'application/x-www-form-urlencoded'
98 | },
99 | body: `name=${name}&des=${des}`
100 | })
101 | }
102 |
--------------------------------------------------------------------------------
/client/backend/article/List.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 Page from '../../component/Page/index'
8 | import { getOwnerList, deleteArticleById } from './resources'
9 |
10 | export default class GroupList extends Component {
11 | constructor (props) {
12 | super(props)
13 | this.state = {
14 | list: [],
15 | page: {
16 | pageCount: 1,
17 | pageNumber: 1
18 | }
19 | }
20 |
21 | this.changePage = this.changePage.bind(this)
22 | this.deleteArticle = this.deleteArticle.bind(this)
23 | }
24 |
25 | componentDidMount () {
26 | this.getList(this.state.page.pageNumber)
27 | }
28 |
29 | getList (pageNumber) {
30 | getOwnerList({pageNumber})
31 | .then(data => this.setState({
32 | list: data.list,
33 | page: data.page
34 | }))
35 | }
36 |
37 | changePage (index) {
38 | this.getList(index)
39 | }
40 |
41 | deleteArticle (e, id) {
42 | deleteArticleById(id)
43 | .then(() => this.getList(this.state.page.pageNumber))
44 | }
45 |
46 | render () {
47 | const { list, page } = this.state
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | | 标题 |
59 | 操作 |
60 |
61 |
62 |
63 |
64 | {list.map(article => {
65 | return (
66 | | {article.title} |
67 |
68 |
69 |
70 | |
71 |
)
72 | })}
73 |
74 |
75 |
76 |
77 |
78 |
)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/webpack/client/client.config.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import webpack from "webpack";
3 | import WebpackIsomorphicToolsPlugin from "webpack-isomorphic-tools/plugin";
4 | import ExtractTextPlugin from "extract-text-webpack-plugin";
5 | import isomorphicToolsConfig from "../isomorphic.tools.config";
6 | import {client} from "../../config";
7 |
8 | const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(isomorphicToolsConfig)
9 |
10 | const cssLoader = [
11 | 'css?modules',
12 | 'sourceMap',
13 | 'importLoaders=1',
14 | 'localIdentName=[name]__[local]___[hash:base64:5]'
15 | ].join('&')
16 |
17 | const cssLoader2 = [
18 | 'css?modules',
19 | 'sourceMap',
20 | 'importLoaders=1',
21 | 'localIdentName=[local]'
22 | ].join('&')
23 |
24 |
25 | const config = {
26 | // 项目根目录
27 | context: path.join(__dirname, '../../'),
28 | devtool: 'cheap-module-eval-source-map',
29 | entry: [
30 | `webpack-hot-middleware/client?reload=true&path=http://${client.host}:${client.port}/__webpack_hmr`,
31 | './client/index.js'
32 | ],
33 | output: {
34 | path: path.join(__dirname, '../../build'),
35 | filename: 'index.js',
36 | publicPath: '/build/',
37 | chunkFilename: '[name]-[chunkhash:8].js'
38 | },
39 | resolve: {
40 | extensions: ['', '.js', '.jsx', '.json']
41 | },
42 | module: {
43 | preLoaders: [
44 | {
45 | test: /\.jsx?$/,
46 | exclude: /node_modules/,
47 | loader: 'eslint-loader'
48 | }
49 | ],
50 | loaders: [
51 | {
52 | test: /\.jsx?$/,
53 | loader: 'babel',
54 | exclude: [/node_modules/]
55 | },
56 | {
57 | test: webpackIsomorphicToolsPlugin.regular_expression('less'),
58 | loader: ExtractTextPlugin.extract('style', `${cssLoader}!less`)
59 | },
60 | {
61 | test: webpackIsomorphicToolsPlugin.regular_expression('css'),
62 | exclude: [/node_modules/],
63 | loader: ExtractTextPlugin.extract('style', `${cssLoader}`)
64 | },
65 | {
66 | test: webpackIsomorphicToolsPlugin.regular_expression('css'),
67 | include: [/node_modules/],
68 | loader: ExtractTextPlugin.extract('style', `${cssLoader2}`)
69 | },
70 | {
71 | test: webpackIsomorphicToolsPlugin.regular_expression('images'),
72 | loader: 'url?limit=10000'
73 | }
74 | ]
75 | },
76 | plugins: [
77 | new webpack.HotModuleReplacementPlugin(),
78 | new ExtractTextPlugin('[name].css', {
79 | allChunks: true
80 | }),
81 | webpackIsomorphicToolsPlugin
82 | ]
83 | }
84 |
85 | export default config
86 |
--------------------------------------------------------------------------------
/client/backend/article/EditBase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/19.
3 | */
4 |
5 | import React, { Component } from 'react'
6 | import { browserHistory } from 'react-router'
7 | import Back from '../../component/Back/index'
8 |
9 | export default class EditBase extends Component {
10 | constructor (props) {
11 | super(props)
12 | props.clearArticle()
13 | this.save = this.save.bind(this)
14 | }
15 |
16 | state = {
17 | title: this.props.detail.title || '',
18 | content: this.props.detail.content || '',
19 | tags: ''
20 | }
21 |
22 | componentWillReceiveProps (nextProps) {
23 | const { articleChanged, detail } = nextProps
24 | switch (articleChanged.status) {
25 | case 2:
26 | setTimeout(() => {
27 | browserHistory.push('/admin/article/')
28 | })
29 | break
30 | case 0:
31 | case 4:
32 | this.state.title = detail.title
33 | this.state.content = detail.content
34 | this.state.tags = detail.tags.join(' ') || ''
35 | break
36 | }
37 | }
38 |
39 | renderLoding () {
40 | var { status } = this.props.articleChanged
41 | switch (status) {
42 | case 1:
43 | return (
44 | 正在提交...
45 |
)
46 | case 2:
47 | return (
48 | 保存成功
49 |
)
50 | case 3:
51 | return (
52 | 保存失败
53 |
)
54 | }
55 | }
56 |
57 | render () {
58 | return ()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/client/redux/actions/user/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/2.
3 | */
4 | import constants from '../../constants/'
5 | import fetch from '../../../../util/fetch'
6 |
7 | export function clearUser () {
8 | return {type: constants.user.CLEAR}
9 | }
10 |
11 | export function addUser (username, nickname, avatar, password, groupId) {
12 | return dispatch => {
13 | dispatch({type: constants.user.START_ADD})
14 | add(username, nickname, avatar, password, groupId)
15 | .then(data => {
16 | dispatch({type: constants.user.ADD_SUCCESS})
17 | })
18 | }
19 | }
20 |
21 | export function getUserList () {
22 | return dispatch => {
23 | getList()
24 | .then(data => {
25 | dispatch({type: constants.user.GET_LIST_SUCCESS, list: data})
26 | })
27 | }
28 | }
29 |
30 | export function getDetail (id) {
31 | return dispatch => {
32 | getDetailById(id)
33 | .then(data => dispatch({
34 | type: constants.user.GET_DETAIL_SUCCESS,
35 | data: data
36 | }))
37 | }
38 | }
39 |
40 | export function saveDetail (id, username, nickname, avatar, password, groupId) {
41 | return dispatch => {
42 | saveDetailById(id, username, nickname, avatar, password, groupId)
43 | .then(data => dispatch({
44 | type: constants.user.ADD_SUCCESS
45 | }))
46 | }
47 | }
48 |
49 | /**
50 | * 添加用户组
51 | * @param name 名称
52 | * @param des 描述
53 | */
54 | function add (username, nickname, avatar, password, groupId) {
55 | return fetch('/api/user/admin', {
56 | method: 'POST',
57 | headers: {
58 | 'Content-Type': 'application/x-www-form-urlencoded'
59 | },
60 | body: {
61 | username,
62 | nickname,
63 | password,
64 | groupId,
65 | avatar
66 | }
67 | })
68 | }
69 |
70 | /**
71 | * 获取用户组列表
72 | */
73 | function getList () {
74 | return fetch('/api/user/admin', {
75 | method: 'GET',
76 | query: {
77 | pageSize: 10,
78 | pageCur: 0
79 | }
80 | })
81 | }
82 |
83 | /**
84 | * 根据用户组id获取详情
85 | * @param id
86 | */
87 | function getDetailById (id) {
88 | return fetch(`/api/user/admin/${id}`, {
89 | method: 'GET'
90 | })
91 | }
92 |
93 | /**
94 | * 保存用户组详情
95 | * @param id
96 | * @param name
97 | * @param des
98 | */
99 | function saveDetailById (id, username, nickname, avatar, password, groupId) {
100 | console.log(groupId)
101 | return fetch(`/api/user/admin/${id}`, {
102 | method: 'PUT',
103 | headers: {
104 | 'Content-Type': 'application/x-www-form-urlencoded'
105 | },
106 | body: {
107 | username,
108 | nickname,
109 | avatar,
110 | password,
111 | groupId
112 | }
113 | })
114 | }
115 |
--------------------------------------------------------------------------------
/server/router/article.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/24.
3 | */
4 |
5 | import koaRouter from 'koa-router'
6 | import Article from '../model/Article'
7 | import markdown from 'markdown'
8 |
9 | const router = koaRouter()
10 |
11 | // 添加文章
12 | router.post('/', function *(next) {
13 | var body = this.request.body
14 | var { title, content, tags } = body
15 | tags = tags || ''
16 | yield Article.create({
17 | title: title,
18 | content: content,
19 | tags: tags.trim().split(/\s+/),
20 | creater: this.session.user._id
21 | })
22 |
23 | this.body = {code: 1000}
24 | })
25 |
26 | // 获取文章列表
27 | router.get('/', function *(next) {
28 | var { pageSize, pageNumber } = this.query
29 | pageSize = parseInt(pageSize, 10)
30 | pageNumber = parseInt(pageNumber, 10)
31 | this.body = yield *queryList({}, pageSize, pageNumber)
32 | })
33 |
34 | // 前台获取文章详情
35 | router.get('/:id', function *(next) {
36 | var article = yield Article.findOne({_id: this.params.id})
37 | article.content = markdown.markdown.toHTML(article.content)
38 | this.body = article
39 | })
40 |
41 | // 管理员获取文章详情
42 | router.get('/admin/:id', function *(next) {
43 | var article = yield Article.findOne({_id: this.params.id})
44 | this.body = article
45 | })
46 |
47 | /**
48 | * 删除文章
49 | */
50 | router.delete('/:id', function *(next) {
51 | var article = yield Article.findOneAndRemove({_id: this.params.id})
52 | this.body = article
53 | })
54 |
55 | // 修改文章信息
56 | router.put('/:id', function *(next) {
57 | var body = this.request.body
58 | var { title, content, tags } = body
59 | tags = tags || ''
60 | var article = yield Article.update(
61 | {_id: this.params.id},
62 | {title, content, tags: tags.split(/\s+/)})
63 | this.body = article
64 | })
65 |
66 | // 获取管理员文章列表
67 | router.get('/admin', function *(next) {
68 | var { pageSize, pageNumber } = this.query
69 | var user = this.session.user
70 | this.body = yield *queryList({creater: user._id}, pageSize, pageNumber)
71 | })
72 |
73 | function *queryList (query, pageSize, pageNumber) {
74 | var method
75 | var pageCount
76 |
77 | function getDetail () {
78 | return Article
79 | .find(query)
80 | .skip((pageNumber - 1) * pageSize)
81 | .limit(pageSize*1)
82 | .sort({ createTime: 1 })
83 | .populate('creater', 'nickname')
84 | }
85 |
86 | function getSum () {
87 | return Article.count(query)
88 | }
89 |
90 | var result = yield Promise.all([getDetail(), getSum()])
91 | pageCount = result[1] / pageSize
92 | method = Number.isInteger(pageCount) ? Math.floor : Math.ceil
93 | pageCount = method.call(Math, pageCount)
94 |
95 | return {
96 | list: result[0],
97 | page: {
98 | pageNumber: pageNumber * 1,
99 | pageCount: pageCount
100 | }
101 | }
102 | }
103 |
104 | export default router
105 |
--------------------------------------------------------------------------------
/client/frontend/article/view.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/24.
3 | */
4 |
5 | import React, { Component } from 'react'
6 | import { bindActionCreators } from 'redux'
7 | import { connect } from 'react-redux'
8 | import { Link } from 'react-router'
9 | import constants from '../../redux/constants/'
10 | import util from '../../../util/'
11 | import { getDetailView } from '../../redux/actions/article/'
12 | import { getView } from '../../redux/resouces/article/'
13 | import Comment from './Comment'
14 | import viewStyle from './view_style.less'
15 |
16 | class ArticleView extends Component {
17 | static getInitData (params, cookie, dispatch) {
18 | return getView(params, cookie)
19 | .then(data => dispatch({
20 | type: constants.article.GET_DETAIL_VIEW_SUCCESS,
21 | data: data
22 | }))
23 | }
24 |
25 | static isomorphicComponents = [Comment]
26 |
27 | componentDidMount () {
28 | const { params, dispatch, system } = this.props
29 | if (system.serverRender.flag === 1) {
30 | this.constructor.getInitData(params, null, dispatch)
31 | }
32 | }
33 |
34 | render () {
35 | const { article } = this.props
36 |
37 | if (!article) {
38 | return null
39 | }
40 |
41 | return (
42 |
{article.title}
43 |
{article.creater.username} 创建于 {util.timestampFormat(article.createTime, 'yyyy-MM-dd hh:mm')}
44 |
最后一次编辑于 {util.timestampFormat(article.updateTime, 'yyyy-MM-dd hh:mm')}
45 |
46 |
49 |
50 |
51 | {article.tags && article.tags.map((tag, index) => {
52 | return ({tag})
53 | })}
54 |
55 |
56 |
57 |
←文章列表
58 |
59 |
)
60 | }
61 | }
62 |
63 | function mapStateToProps (state, ownProps) {
64 | var id = ownProps.params.id
65 | var article = state.article.view.data.filter(article => id === article._id)
66 | var system = state.system
67 | if (article.length) {
68 | article = article[0]
69 | } else {
70 | article = null
71 | }
72 |
73 | return {
74 | article,
75 | system
76 | }
77 | }
78 |
79 | // dispatch 可以放置到props上
80 | function mapDispatchToProps (dispatch, ownProps) {
81 | return {
82 | dispatch,
83 | ...bindActionCreators({getDetailView}, dispatch)
84 | }
85 | }
86 |
87 | export default connect(mapStateToProps, mapDispatchToProps)(ArticleView)
88 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog",
3 | "version": "0.0.1",
4 | "description": "web frontend auto release",
5 | "main": "index.js",
6 | "scripts": {
7 | "client:start": "babel-node webpack/client/index.js",
8 | "db:init": "babel-node ./bin/start.js",
9 | "db:article": "babel-node ./bin/article.js",
10 | "server:start": "nodemon --exec babel-node webpack/server/index.js",
11 | "start": "",
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "http://gitlab.kingsum.biz:81/zhangran/frontend_manage.git"
17 | },
18 | "keywords": [
19 | "frontend",
20 | "react",
21 | "redux"
22 | ],
23 | "author": "zr",
24 | "license": "ISC",
25 | "dependencies": {
26 | "cheerio": "^0.22.0",
27 | "co": "^4.6.0",
28 | "formidable": "^1.1.1",
29 | "iconv-lite": "^0.4.15",
30 | "isomorphic-fetch": "2.2.1",
31 | "koa": "^1.2.4",
32 | "koa-bodyparser": "^2.2.0",
33 | "koa-compress": "^1.0.9",
34 | "koa-ejs": "^3.0.0",
35 | "koa-favicon": "^1.2.1",
36 | "koa-logger": "^1.3.0",
37 | "koa-router": "^5.4.0",
38 | "koa-session-mongoose": "^1.0.0",
39 | "koa-session-store": "^2.0.0",
40 | "koa-static": "^2.0.0",
41 | "markdown": "^0.5.0",
42 | "mongoose": "^4.6.3",
43 | "mongoose-post-find": "0.0.2",
44 | "primer-css": "^4.2.0",
45 | "rc-upload": "^2.3.2",
46 | "react": "^15.3.1",
47 | "react-dom": "^15.3.1",
48 | "react-redux": "^4.4.5",
49 | "react-router": "^2.8.1",
50 | "react-router-redux": "^4.0.5",
51 | "redux": "^3.6.0",
52 | "redux-logger": "^2.7.0",
53 | "redux-thunk": "^2.1.0",
54 | "request": "^2.79.0"
55 | },
56 | "devDependencies": {
57 | "babel-cli": "^6.14.0",
58 | "babel-core": "^6.14.0",
59 | "babel-eslint": "^7.0.0",
60 | "babel-loader": "^6.2.5",
61 | "babel-plugin-transform-runtime": "^6.15.0",
62 | "babel-preset-es2015": "^6.14.0",
63 | "babel-preset-react": "^6.11.1",
64 | "babel-preset-react-hmre": "^1.1.1",
65 | "babel-preset-react-optimize": "^1.0.1",
66 | "babel-preset-stage-2": "^6.13.0",
67 | "babel-register": "^6.14.0",
68 | "css-loader": "^0.25.0",
69 | "eslint": "^3.7.1",
70 | "eslint-config-airbnb": "^12.0.0",
71 | "eslint-config-standard": "^6.2.0",
72 | "eslint-loader": "^1.5.0",
73 | "eslint-plugin-import": "^2.0.1",
74 | "eslint-plugin-jsx-a11y": "^2.2.3",
75 | "eslint-plugin-promise": "^3.0.0",
76 | "eslint-plugin-react": "^6.4.1",
77 | "eslint-plugin-standard": "^2.0.1",
78 | "extract-text-webpack-plugin": "^1.0.1",
79 | "file-loader": "^0.9.0",
80 | "koa-webpack-dev-middleware": "^1.2.2",
81 | "koa-webpack-hot-middleware": "^1.0.3",
82 | "less": "^2.7.2",
83 | "less-loader": "^2.2.3",
84 | "nodemon": "^1.11.0",
85 | "style-loader": "^0.13.1",
86 | "url-loader": "^0.5.7",
87 | "webpack": "^1.13.2",
88 | "webpack-isomorphic-tools": "^2.5.8"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/client/backend/Layout.js:
--------------------------------------------------------------------------------
1 |
2 | import 'primer-css/build/build.css'
3 | import AdminStyle from './Admin.less'
4 | import React, { Component } from 'react'
5 | import { connect } from 'react-redux'
6 | import constants from '../redux/constants/'
7 | import { bindActionCreators } from 'redux'
8 | import { logOut } from '../redux/actions/login/'
9 |
10 | const defaultAvatar = '/avatar.png'
11 |
12 | class Layout extends Component {
13 | state = {
14 | quitShowState: false
15 | }
16 |
17 | static contextTypes = {
18 | router: React.PropTypes.object.isRequired
19 | }
20 |
21 | handleAvatarOver (e) {
22 | this.setState({
23 | quitShowState: true
24 | })
25 | }
26 |
27 | handleAvatarOut (e) {
28 | this.setState({
29 | quitShowState: false
30 | })
31 | }
32 |
33 | handleLogOut (e) {
34 | const { logOut } = this.props
35 | this.handleAvatarOut()
36 | logOut()
37 | }
38 |
39 | componentWillReceiveProps (props) {
40 | if (props.loginStatus.status === 0 && props.location.pathname !== '/login') {
41 | this.context.router.push('/login')
42 | }
43 | }
44 |
45 | componentDidMount () {
46 | const { dispatch } = this.props
47 | dispatch({
48 | type: constants.system.SERVER_RENDERED
49 | })
50 | }
51 |
52 | renderContent () {
53 | const { children, user } = this.props
54 | var avatar = user.avatar ? user.avatar : defaultAvatar
55 |
56 | return (
57 |
58 |
59 |
前端杂记
60 |
64 |

65 |
退出登录
68 |
69 |
70 | {children}
71 |
72 | )
73 | }
74 |
75 | renderLogin () {
76 | return (
77 |
78 | {this.props.children}
79 |
80 | )
81 | }
82 |
83 | render () {
84 | return (
85 | this.props.loginStatus.status === 0 ? this.renderLogin() : this.renderContent()
86 | )
87 | }
88 | }
89 |
90 | function mapStateToProps (state, ownProps) {
91 | return {
92 | user: state.login.loginStatus.user,
93 | loginStatus: state.login.loginStatus,
94 | system: state.system
95 | }
96 | }
97 |
98 | function mapDispatchToProps (dispatch, ownProps) {
99 | return {
100 | dispatch,
101 | ...bindActionCreators({
102 | logOut
103 | }, dispatch)
104 | }
105 | }
106 |
107 | export default connect(mapStateToProps, mapDispatchToProps)(Layout)
108 |
--------------------------------------------------------------------------------
/client/frontend/article/list.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/25.
3 | */
4 |
5 | import React, { Component } from 'react'
6 | import { connect } from 'react-redux'
7 | import { Link } from 'react-router'
8 | import { bindActionCreators } from 'redux'
9 | import Page from '../../component/Page/index'
10 | import util from '../../../util/'
11 | import constants from '../../redux/constants/'
12 | import { getArticleViewList } from '../../redux/actions/article/'
13 | import { getList } from '../../redux/resouces/article/'
14 | import listStyle from './list_style.less'
15 |
16 | class ArticleList extends Component {
17 | static getInitData (params = {}, cookie, dispatch, query = {}) {
18 | return getList({
19 | ...params,
20 | ...query
21 | }, cookie)
22 | .then(data => dispatch({
23 | type: constants.article.GET_LIST_VIEW_SUCCESS,
24 | data: data
25 | }))
26 | }
27 |
28 | static contextTypes = {
29 | router: React.PropTypes.object.isRequired
30 | }
31 |
32 | constructor (props) {
33 | super(props)
34 | this.changePage = this.changePage.bind(this)
35 | }
36 |
37 | componentDidMount () {
38 | const { params, dispatch, system, location } = this.props
39 | if (system.serverRender.flag === 1) {
40 | this.constructor.getInitData(params, null, dispatch, location.query)
41 | }
42 | }
43 |
44 | changePage (pageNumber) {
45 | this.context.router.replace(`/frontend/article?pageNumber=${pageNumber}`)
46 | this.getList(pageNumber)
47 | }
48 |
49 | getList (pageNumber) {
50 | this.constructor.getInitData({
51 | pageNumber
52 | }, null, this.props.dispatch)
53 | }
54 |
55 | render () {
56 | const { listView } = this.props
57 | return (
58 |
文章列表
59 |
60 | {listView.list.map((item, index) => {
61 | return (
62 | -
63 | {item.title}
64 |
65 | {item.creater.nickname}
66 | 发表于
67 | {util.timestampFormat(item.createTime, 'yyyy-MM-dd hh:mm')}
68 |
69 |
)
70 | })}
71 |
72 |
75 |
)
76 | }
77 | }
78 |
79 | function mapStateToProps (state, ownProps) {
80 | return {
81 | listView: state.article.listView.data,
82 | system: state.system
83 | }
84 | }
85 |
86 | function mapDispatchToProps (dispatch, ownProps) {
87 | return {
88 | dispatch,
89 | ...bindActionCreators({
90 | getArticleViewList
91 | }, dispatch)
92 | }
93 | }
94 |
95 | export default connect(mapStateToProps, mapDispatchToProps)(ArticleList)
96 |
--------------------------------------------------------------------------------
/bin/article.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 17/2/3.
3 | */
4 |
5 | import url from 'url'
6 | import request from 'request'
7 | import $ from 'cheerio'
8 | import iconv from 'iconv-lite'
9 | import co from 'co'
10 | import startDB from '../server/model/'
11 | import Article from '../server/model/Article'
12 | import User from '../server/model/User'
13 |
14 | // var r = parseInt("是", 16)
15 | // console.log(r)
16 | var chunks = []
17 | var size = 0
18 | request
19 | .get('http://www.kanunu8.com/wuxia/201102/1625.html')
20 | .on('data', function(chunk) {
21 | chunks.push(chunk)
22 | size += chunk.length
23 | })
24 | .on('end', function () {
25 | var data = getData(chunks, size)
26 | var str = iconv.decode(data, 'gbk')
27 | var result = $('table[width="98%"]', str)
28 | result = $('table[bgcolor="#d4d0c8"] tr[bgcolor="#ffffff"] td a', result[0])
29 | var articles = []
30 | var getSize = 0
31 | console.log('抓取列表成功')
32 |
33 | result.each((index, item) => {
34 | var href = $(item).attr('href')
35 | articles.push({
36 | href
37 | })
38 | href = url.resolve('http://www.kanunu8.com/wuxia/201102/', href)
39 | var chunks = []
40 | var size = 0
41 | console.log(`${href} 开始抓取`)
42 | request.get(href)
43 | .on('data', function (chunk) {
44 | chunks.push(chunk)
45 | size += chunk.length
46 | })
47 | .on('end', function () {
48 | var data = getData(chunks, size)
49 | var str = iconv.decode(data, 'gbk')
50 | var $title = $('table h2 font', str)
51 | articles[index].title = $title[0].children[0].data
52 | var $content = $('table p', str)
53 | var content = $content[0].children.map(child => child.data).join('')
54 | articles[index].content = content
55 | // articles[index].creater = '5809b6c6e048547e2f54603a'
56 | getSize++
57 |
58 | console.log(`${href} 抓取成功`)
59 | console.log(getSize)
60 |
61 | if (getSize === articles.length) {
62 | console.log('获取详情成功')
63 |
64 | co(function *() {
65 | yield startDB
66 |
67 | var user = yield User.findOne({username: 'admin'})
68 |
69 | for(var i = 0;i fn(), 100)
80 | }
81 |
82 | console.log('保存成功')
83 | process.exit()
84 | }).catch(e => {
85 | console.log(e)
86 | process.exit()
87 | })
88 |
89 |
90 | }
91 | })
92 |
93 | // return false
94 | })
95 | })
96 |
97 | function getData (chunks, size) {
98 | var data = new Buffer(size)
99 | for (var i = 0, pos = 0, l = chunks.length; i < l; i++) {
100 | var chunk = chunks[i]
101 | chunk.copy(data, pos)
102 | pos += chunk.length
103 | }
104 |
105 | return data
106 | }
107 |
--------------------------------------------------------------------------------
/client/redux/actions/article/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/2.
3 | */
4 | import constants from '../../constants/'
5 | import fetch from '../../../../util/fetch'
6 |
7 | export function clearArticle () {
8 | return {type: constants.article.CLEAR}
9 | }
10 |
11 | export function addArticle (title, content, tags) {
12 | return dispatch => {
13 | dispatch({type: constants.article.START_ADD})
14 | add(title, content, tags)
15 | .then(data => {
16 | dispatch({type: constants.article.ADD_SUCCESS})
17 | })
18 | }
19 | }
20 |
21 | export function getArticleList () {
22 | return dispatch => {
23 | getList()
24 | .then(data => {
25 | dispatch({type: constants.article.GET_LIST_SUCCESS, list: data})
26 | })
27 | }
28 | }
29 |
30 | export function getDetail (id) {
31 | return dispatch => {
32 | getDetailById(id)
33 | .then(data => dispatch({
34 | type: constants.article.GET_DETAIL_SUCCESS,
35 | data: data
36 | }))
37 | }
38 | }
39 |
40 | export function saveDetail (id, title, content, tags) {
41 | return dispatch => {
42 | saveDetailById(id, title, content, tags)
43 | .then(data => dispatch({
44 | type: constants.article.ADD_SUCCESS
45 | }))
46 | }
47 | }
48 |
49 | export function getDetailView (data) {
50 | return {
51 | type: constants.article.GET_DETAIL_VIEW_SUCCESS,
52 | data: data.data
53 | }
54 | }
55 |
56 | /**
57 | *
58 | * @returns {function()}
59 | */
60 | export function getArticleViewList () {
61 | return dispatch => {
62 | getList()
63 | .then(data => {
64 | dispatch({type: constants.article.GET_LIST_VIEW_SUCCESS, data: data})
65 | })
66 | }
67 | }
68 |
69 | /**
70 | *
71 | * @returns {function()}
72 | */
73 | export function clearArticleList () {
74 | return {
75 | type: constants.article.CLEAR_LIST_VIEW
76 | }
77 | }
78 |
79 | export function clearArticleView () {
80 | return {
81 | type: constants.article.CLEAR_DETAIL_VIEW
82 | }
83 | }
84 |
85 | /**
86 | * 添加文章
87 | * @param title
88 | * @param content
89 | * @param tags
90 | */
91 | function add (title, content, tags) {
92 | return fetch('/api/article', {
93 | method: 'POST',
94 | headers: {
95 | 'Content-Type': 'application/x-www-form-urlencoded'
96 | },
97 | body: {
98 | title,
99 | content,
100 | tags
101 | }
102 | })
103 | }
104 |
105 | /**
106 | * 获取文章列表
107 | */
108 | function getList () {
109 | return fetch('/api/article', {
110 | method: 'GET',
111 | query: {
112 | pageSize: 10,
113 | pageNumber: 1
114 | }
115 | })
116 | }
117 |
118 | /**
119 | * 根据文章id获取详情
120 | * @param id
121 | */
122 | function getDetailById (id) {
123 | return fetch(`/api/article/admin/${id}`, {
124 | method: 'GET'
125 | })
126 | }
127 |
128 | /**
129 | * 保存用户组详情
130 | * @param id
131 | * @param name
132 | * @param des
133 | */
134 | function saveDetailById (id, title, content, tags) {
135 | return fetch(`/api/article/${id}`, {
136 | method: 'PUT',
137 | headers: {
138 | 'Content-Type': 'application/x-www-form-urlencoded'
139 | },
140 | body: {
141 | title,
142 | content,
143 | tags
144 | }
145 | })
146 | }
147 |
--------------------------------------------------------------------------------
/client/redux/reducers/article/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 { article } = 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 article.CLEAR:
22 | return {...state, status: 0}
23 | case article.START_ADD:
24 | return {...state, status: 1}
25 | case article.ADD_SUCCESS:
26 | return {...state, status: 2}
27 | case article.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 article.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: '', title: '', content: '', creater: '', tags: []}, action) {
52 | switch (action.type) {
53 | case article.CLEAR:
54 | return {_id: '', title: '', content: '', creater: '', tags: []}
55 | case article.GET_DETAIL_SUCCESS:
56 | return {
57 | _id: action.data._id,
58 | title: action.data.title,
59 | content: action.data.content,
60 | creater: action.data.creater,
61 | tags: action.data.tags
62 | }
63 | }
64 | return state
65 | }
66 |
67 | function view (state = {loaded: false, data: []}, action) {
68 | switch (action.type) {
69 | case article.CLEAR_DETAIL_VIEW:
70 | return {
71 | loaded: false,
72 | data: []
73 | }
74 | case article.GET_DETAIL_VIEW_SUCCESS:
75 | return {
76 | loaded: true,
77 | data: [
78 | ...state.data,
79 | action.data
80 | ]
81 | }
82 | case article.GET_COMMENT:
83 | state.data.forEach(article => {
84 | if (article._id === action.data.article) {
85 | article.comment = action.data
86 | }
87 | })
88 |
89 | return {
90 | loaded: true,
91 | data: [
92 | ...state.data
93 | ]
94 | }
95 | }
96 | return state
97 | }
98 |
99 | function listView (state = {loaded: false, data: {list: [], page: {pageCount: 0, pageNumber: 0}}}, action) {
100 | switch (action.type) {
101 | case article.CLEAR_LIST_VIEW:
102 | return {
103 | loaded: false,
104 | data: {
105 | list: [],
106 | page: {pageCount: 0, pageNumber: 0}
107 | }
108 | }
109 | case article.GET_LIST_VIEW_SUCCESS:
110 | return {
111 | loaded: true,
112 | data: action.data
113 | }
114 | }
115 | return state
116 | }
117 |
118 | function comment (state = [], action) {
119 | switch (action.type) {
120 | case article.GET_COMMENT:
121 | return [
122 | ...action.data
123 | ]
124 | }
125 |
126 | return state
127 | }
128 |
129 | export default combineReducers({
130 | changed,
131 | listData,
132 | detail,
133 | view,
134 | listView,
135 | comment
136 | })
137 |
--------------------------------------------------------------------------------
/webpack/server/handleRender.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/9/22.
3 | */
4 | import koaRouter from 'koa-router'
5 | import React from 'react'
6 | import {renderToString} from 'react-dom/server'
7 | import {match, RouterContext} from 'react-router'
8 | import {Provider} from 'react-redux'
9 | import configureStore from '../../client/redux/store'
10 | import routes from '../../client/routes'
11 | import Html from './Html'
12 | import constants from '../../client/redux/constants/'
13 |
14 | const router = koaRouter()
15 |
16 | const handleRender = function *(next) {
17 | const initialState = {}
18 | const store = configureStore(initialState)
19 |
20 | const _ctx = this
21 | const {url: location} = _ctx
22 |
23 | var matchResult = {}
24 |
25 | match({routes, location}, (error, redirectLocation, renderProps) => {
26 | matchResult = {
27 | error,
28 | redirectLocation,
29 | renderProps
30 | }
31 | })
32 |
33 | const {error, redirectLocation, renderProps} = matchResult
34 |
35 | if (error) {
36 | _ctx.status = 500
37 | _ctx.body = error.message
38 | } else if (redirectLocation) {
39 | _ctx.status = 302
40 | _ctx.redirect(`${redirectLocation.pathname}${redirectLocation.search}`)
41 | } else if (renderProps) {
42 | let isomorphicComponents = getAllIsomorphicComponents(renderProps.components)
43 |
44 | // 如果没有登录
45 | if (!this.session.user) {
46 | store.dispatch({
47 | type: constants.login.CLEAR,
48 | user: {}
49 | })
50 | } else {
51 | // 初始化当前登录用户
52 | store.dispatch({
53 | type: constants.login.SUCCESS,
54 | user: this.session.user
55 | })
56 | }
57 |
58 | yield fetchComponentData(
59 | store.dispatch,
60 | isomorphicComponents,
61 | renderProps.params,
62 | renderProps.location.query,
63 | _ctx.header)
64 |
65 | const component = (
66 |
67 |
68 |
69 | )
70 |
71 | const assets = webpackIsomorphicTools.assets()
72 |
73 | _ctx.type = 'html'
74 | _ctx.status = 200
75 | _ctx.body = renderToString()
76 | }
77 | }
78 |
79 | function fetchComponentData(dispatch, components, params, query, header) {
80 | const promises = []
81 | components.forEach((current, index) => {
82 | if (current && current.WrappedComponent && current.WrappedComponent.getInitData) {
83 | promises.push(current.WrappedComponent.getInitData)
84 | }
85 | })
86 |
87 | const fetch = promises.map(promise => {
88 | return promise(params, header.cookie, dispatch, query)
89 | })
90 |
91 | return Promise.all(fetch)
92 | }
93 |
94 | function getAllIsomorphicComponents(defaultComponents) {
95 | var newComponents = []
96 | defaultComponents.forEach((current) => {
97 | if (current && current.WrappedComponent) {
98 | var {isomorphicComponents} = current.WrappedComponent
99 | if (Array.isArray(isomorphicComponents) && isomorphicComponents.length) {
100 | newComponents = newComponents.concat(isomorphicComponents)
101 | }
102 | }
103 | })
104 |
105 | return [
106 | ...newComponents,
107 | ...defaultComponents
108 | ]
109 | }
110 |
111 | /**
112 | * 解决非get 404到达这个页面
113 | */
114 | router.get('/*', handleRender)
115 |
116 | export default router.routes()
117 |
--------------------------------------------------------------------------------
/server/router/user.js:
--------------------------------------------------------------------------------
1 | var router = require('koa-router')()
2 | var crypto = require('crypto')
3 | var UserGroup = require('../model/UserGroup')
4 | var User = require('../model/User')
5 |
6 | // 添加userGroup
7 | router.post('/group', function *(next) {
8 | var body = this.request.body,
9 | userGroup = yield UserGroup.create({
10 | name: body.name,
11 | des: body.des
12 | // creater: this.session.user._id
13 | })
14 |
15 |
16 |
17 | this.body = {code: 1000}
18 | })
19 |
20 | // 获取userGroup列表
21 | router.get('/group', function *(next) {
22 | var { pageSize, pageCur } = this.query
23 | var userGroupList = yield UserGroup
24 | .find()
25 | .skip(pageCur * pageSize)
26 | .limit((pageCur + 1) * pageSize)
27 | .populate('creater', 'nickname')
28 |
29 | this.body = userGroupList
30 | })
31 |
32 | // 获取用户组详情
33 | router.get('/group/:id', function *(next) {
34 | var userGroup = yield UserGroup.findOne({_id: this.params.id})
35 | this.body = userGroup
36 | })
37 |
38 | // 修改用户组信息
39 | router.put('/group/:id', function *(next) {
40 | var body = this.request.body
41 | var userGroup = yield UserGroup.update(
42 | {_id: this.params.id},
43 | {name: body.name, des: body.des, updateTime: new Date()})
44 | this.body = userGroup
45 | })
46 |
47 | // 删除单个用户组信息
48 | router.delete('/group/:id', function *(next) {
49 | var userGroup = yield UserGroup.findOneAndRemove({_id: this.params.id})
50 | this.body = userGroup
51 | })
52 |
53 | // 增加单个用户
54 | router.post('/admin', function *(next) {
55 | var body = this.request.body
56 |
57 | // 默认密码
58 | var defaultPassword = body.password || '1'
59 | var passwordHashed = crypto.createHash('md5').update(defaultPassword).digest('hex')
60 |
61 | var user = yield User.create({
62 | username: body.username,
63 | nickname: body.nickname,
64 | avatar: body.avatar,
65 | group: body.groupId,
66 | password: passwordHashed
67 | })
68 |
69 | yield UserGroup
70 | .update(
71 | {_id: body.groupId},
72 | {'$addToSet': {users: user._id}}
73 | )
74 |
75 | this.body = user
76 | })
77 |
78 | // 查询所有用户
79 | router.get('/admin', function *(next) {
80 | var userList = yield User
81 | .find()
82 | .populate('creater', 'nickname')
83 | .populate('group', 'name')
84 | this.body = userList
85 | })
86 |
87 | // 删除单个用户
88 | router.delete('/admin/:id', function *(next) {
89 | var user = yield User.findOneAndRemove({_id: this.params.id})
90 | yield UserGroup.update({_id: user.group}, {
91 | '$pull': {users: user._id}
92 | })
93 | this.body = user
94 | })
95 |
96 | // 查找单个用户
97 | router.get('/admin/:id', function *(next) {
98 | var user = yield User.findOne({_id: this.params.id})
99 | .populate('group')
100 | this.body = user
101 | })
102 |
103 | router.put('/admin/:id', function *(next) {
104 | var body = this.request.body
105 | var user = yield User.findOne({_id: this.params.id})
106 |
107 | // 删掉已存在UserGroup中的user id
108 | yield UserGroup.update({_id: user.groupId}, {
109 | '$pull': {users: user._id}
110 | })
111 |
112 | // 存储user
113 | user.username = body.username
114 | user.nickname = body.nickname
115 | user.avatar = body.avatar
116 | user.group = body.groupId
117 |
118 | if (typeof body.password !== 'undefined') {
119 | user.password = crypto.createHash('md5').update(body.password).digest('hex')
120 | }
121 | yield user.save()
122 |
123 | yield UserGroup.update({_id: body.groupId}, {'$addToSet': {users: user._id}})
124 | this.body = user
125 | })
126 |
127 | module.exports = router
128 |
--------------------------------------------------------------------------------
/client/component/Page/index.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 17/1/10.
3 | */
4 |
5 | import React, {Component, PropTypes} from 'react'
6 | import style from './style.less'
7 |
8 | export default class Page extends Component {
9 | static propTypes = {
10 | pageCount: PropTypes.number.isRequired,
11 | pageNumber: PropTypes.number.isRequired,
12 | pageChange: PropTypes.func.isRequired
13 | }
14 |
15 | constructor (props) {
16 | super(props)
17 |
18 | this.state = {
19 | ...props,
20 | max: 9,
21 | beginPage: {
22 | start: 7,
23 | middle: 0,
24 | end: 2
25 | },
26 | middlePage: {
27 | start: 2,
28 | middle: 2, // 当前页两侧各有2页
29 | end: 2
30 | },
31 | endPage: {
32 | start: 2,
33 | middle: 0,
34 | end: 7
35 | }
36 | }
37 |
38 | this.handlePageChange = this.handlePageChange.bind(this)
39 | }
40 |
41 | handlePageChange (e, index) {
42 | this.props.pageChange(index)
43 | }
44 |
45 | drawPageBtn (pagesEles, index, pageNumber) {
46 | var classNames = [style.page_no]
47 | if (pageNumber === index) {
48 | classNames.push(style.current)
49 | }
50 | pagesEles.push( this.handlePageChange(e, index)} className={classNames.join(' ')} key={index}>{index})
51 | }
52 |
53 | drawSketchBtns (index, pagesEles, pageRule) {
54 | const {pageNumber, pageCount} = this.props
55 | if (index <= pageRule.start) {
56 | this.drawPageBtn(pagesEles, index, pageNumber)
57 | } else if (index === pageRule.start + 1 && pageCount > this.state.max) {
58 | pagesEles.push(...)
59 | } else if (index > pageCount - pageRule.end) {
60 | this.drawPageBtn(pagesEles, index, pageNumber)
61 | }
62 | }
63 |
64 | getPages () {
65 | const {pageNumber, pageCount} = this.props
66 | var pagesEles = []
67 | var i
68 | for (i = 1; i <= pageCount; i++) {
69 | // 当前页小于开始最大页数
70 | if (pageNumber < this.state.beginPage.start) {
71 | this.drawSketchBtns(i, pagesEles, this.state.beginPage)
72 | } else if (pageNumber > pageCount - this.state.endPage.end + 1) {
73 | this.drawSketchBtns(i, pagesEles, this.state.endPage)
74 | } else {
75 | if (i <= this.state.middlePage.start) {
76 | this.drawPageBtn(pagesEles, i, pageNumber)
77 | } else if (i === this.state.middlePage.start + 1 && pageCount > this.state.max) {
78 | pagesEles.push(...)
79 | } else if (i >= pageNumber - this.state.middlePage.middle && i <= pageNumber + this.state.middlePage.middle) {
80 | this.drawPageBtn(pagesEles, i, pageNumber)
81 | } else if (i === pageNumber + this.state.middlePage.middle + 1 && pageCount > this.state.max) {
82 | pagesEles.push(...)
83 | } else if (i >= pageCount - this.state.middlePage.end) {
84 | this.drawPageBtn(pagesEles, i, pageNumber)
85 | }
86 | }
87 | }
88 | return pagesEles
89 | }
90 |
91 | render () {
92 | const {pageNumber, pageCount} = this.props
93 |
94 | return (
95 |
96 | {
97 | pageNumber > 1
98 | ? this.handlePageChange(e, pageNumber - 1)} className={style.page_no}> ◄
99 | : ◄
100 | }
101 | {this.getPages()}
102 | {
103 | pageNumber < pageCount
104 | ? this.handlePageChange(e, pageNumber + 1)} className={style.page_no}> ►
105 | : ►
106 | }
107 |
108 | )
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/webpack/isomorphic.tools.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/9/24.
3 | */
4 | import WebpackIsomorphicToolsPlugin from 'webpack-isomorphic-tools/plugin'
5 |
6 | export default {
7 | assets: {
8 | images: {
9 | extensions: ['png', 'jpg', 'jpeg', 'gif', 'ico', 'svg']
10 | },
11 | css: {
12 | extensions: ['css'],
13 | filter(module, regex, options, log) {
14 | if (options.development) {
15 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log)
16 | }
17 | // in production mode there's no webpack "style-loader",
18 | // so the module.name will be equal to the asset path
19 | return regex.test(module.name)
20 | },
21 | path(module, options, log) {
22 | if (options.development) {
23 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
24 | }
25 | // in production mode there's no Webpack "style-loader",
26 | // so `module.name`s will be equal to correct asset paths
27 | return module.name
28 | },
29 | parser(module, options, log) {
30 | if (options.development) {
31 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
32 | }
33 | // In production mode there's Webpack Extract Text Plugin
34 | // which extracts all CSS text away, so there's
35 | // only CSS style class name map left.
36 | return module.source
37 | }
38 | },
39 | less: {
40 | extensions: ['less'],
41 |
42 | // which `module`s to parse CSS style class name maps from:
43 | filter: function(module, regex, options, log)
44 | {
45 | if (options.development)
46 | {
47 | // In development mode there's Webpack "style-loader",
48 | // which outputs `module`s with `module.name == asset_path`,
49 | // but those `module`s do not contain CSS text.
50 | //
51 | // The `module`s containing CSS text are
52 | // the ones loaded with Webpack "css-loader".
53 | // (which have kinda weird `module.name`)
54 | //
55 | // Therefore using a non-default `filter` function here.
56 | //
57 | return webpack_isomorphic_tools_plugin.style_loader_filter(module, regex, options, log)
58 | }
59 |
60 | // In production mode there's no Webpack "style-loader",
61 | // so `module.name`s of the `module`s created by Webpack "css-loader"
62 | // (those which contain CSS text)
63 | // will be simply equal to the correct asset path
64 | return regex.test(module.name)
65 | },
66 |
67 | // How to correctly transform `module.name`s
68 | // into correct asset paths
69 | path: function(module, options, log)
70 | {
71 | if (options.development)
72 | {
73 | // In development mode there's Webpack "style-loader",
74 | // so `module.name`s of the `module`s created by Webpack "css-loader"
75 | // (those picked by the `filter` function above)
76 | // will be kinda weird, and this path extractor extracts
77 | // the correct asset paths from these kinda weird `module.name`s
78 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
79 | }
80 |
81 | // in production mode there's no Webpack "style-loader",
82 | // so `module.name`s will be equal to correct asset paths
83 | return module.name
84 | },
85 |
86 | // How to extract these Webpack `module`s' javascript `source` code.
87 | // Basically takes `module.source` and modifies its `module.exports` a little.
88 | parser: function(module, options, log)
89 | {
90 | if (options.development)
91 | {
92 | // In development mode it adds an extra `_style` entry
93 | // to the CSS style class name map, containing the CSS text
94 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
95 | }
96 |
97 | // In production mode there's Webpack Extract Text Plugin
98 | // which extracts all CSS text away, so there's
99 | // only CSS style class name map left.
100 | return module.source
101 | }
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/client/frontend/article/Comment.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 17/1/13.
3 | */
4 |
5 | import React, { Component, PropTypes } from 'react'
6 | import { connect } from 'react-redux'
7 | // import { browserHistory } from 'react-router'
8 | import { saveComment, getCommentList, zan, fan } from '../../redux/resouces/article/'
9 | import constants from '../../redux/constants/'
10 | import style from './Comment_style.less'
11 | import util from '../../../util/'
12 |
13 | class Comment extends Component {
14 | static propTypes = {
15 | articleId: PropTypes.any.isRequired
16 | }
17 |
18 | static contextTypes = {
19 | router: React.PropTypes.object.isRequired,
20 | location: React.PropTypes.object.isRequired
21 | }
22 |
23 | state = {
24 | commentContent: ''
25 | }
26 |
27 | static getInitData (params, cookie, dispatch) {
28 | return getCommentList({articleId: params.id}, cookie)
29 | .then(data => dispatch({
30 | type: constants.article.GET_COMMENT,
31 | data: data
32 | }))
33 | }
34 |
35 | constructor (props) {
36 | super(props)
37 | this.handleCommentSubmit = this.handleCommentSubmit.bind(this)
38 | this.handleCommentChange = this.handleCommentChange.bind(this)
39 | this.handleZan = this.handleZan.bind(this)
40 | this.handleFan = this.handleFan.bind(this)
41 | }
42 |
43 | componentDidMount () {
44 | this.fetchData()
45 | }
46 |
47 | handleCommentChange (e) {
48 | this.setState({
49 | commentContent: e.target.value
50 | })
51 | }
52 |
53 | handleCommentSubmit () {
54 | saveComment({
55 | articleId: this.props.articleId,
56 | commentContent: this.state.commentContent
57 | }).then(data => {
58 | this.fetchData()
59 | this.setState({
60 | commentContent: ''
61 | })
62 | })
63 | }
64 |
65 | handleZan (commentId) {
66 | zan({commentId})
67 | .then(data => {
68 | if (data.nModified === 1) {
69 | this.fetchData()
70 | }
71 | })
72 | }
73 |
74 | handleFan (commentId) {
75 | fan({commentId})
76 | .then(data => {
77 | if (data.nModified === 1) {
78 | this.fetchData()
79 | }
80 | })
81 | }
82 |
83 | fetchData () {
84 | const { system, articleId, dispatch } = this.props
85 | if (system.serverRender.flag === 1) {
86 | this.constructor.getInitData({id: articleId}, null, dispatch)
87 | }
88 | }
89 |
90 | render () {
91 | const { comment } = this.props
92 | return (
93 |
94 |
95 | {comment.map((item, index) => {
96 | return (-
97 |
98 |
{index + 1}楼 {util.timestampFormat(item.createTime, 'yyyy-MM-dd hh:mm')} | {item.creater.nickname}
99 |
100 | this.handleZan(item._id)} className={style.zan + ' border-gray-light border bg-blue-light mr-3'}>赞同({item.zan.length})
101 | this.handleFan(item._id)} className={style.fan + ' border-gray-light border bg-blue-light'}>反对({item.fan.length})
102 |
103 |
104 |
105 | )
106 | })}
107 |
108 |
109 |
110 |
123 |
124 |
125 | )
126 | }
127 | }
128 |
129 | function mapStateToProps (state, ownProps) {
130 | return {
131 | comment: state.article.comment,
132 | system: state.system
133 | }
134 | }
135 |
136 | // dispatch 可以放置到props上
137 | function mapDispatchToProps (dispatch, ownProps) {
138 | return {
139 | dispatch
140 | }
141 | }
142 |
143 | export default connect(mapStateToProps, mapDispatchToProps)(Comment)
144 |
--------------------------------------------------------------------------------
/client/backend/user/EditBase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by zhangran on 16/10/19.
3 | */
4 |
5 | import React, { Component } from 'react'
6 | import { browserHistory } from 'react-router'
7 | import Upload from 'rc-upload'
8 | import Back from '../../component/Back/index'
9 |
10 | const defaultAvatar = '/avatar.png'
11 |
12 | export default class EditBase extends Component {
13 | constructor (props) {
14 | super(props)
15 | props.clearUser()
16 | this.save = this.save.bind(this)
17 |
18 | var self = this
19 |
20 | this.state = {
21 | username: this.props.detail.username || '',
22 | password: '',
23 | passwordRepeat: '',
24 | groupId: this.props.detail.groupId || '',
25 | nickname: this.props.detail.nickname || '',
26 | valid: true,
27 | avatar: this.props.detail.avatar || defaultAvatar,
28 | uploadProps: {
29 | supportServerRender: true,
30 | action: '/api/upload',
31 | onSuccess (file) {
32 | self.setState({
33 | avatar: file[0][1].filename
34 | })
35 | }
36 | }
37 | }
38 | }
39 |
40 | componentWillReceiveProps (nextProps) {
41 | const { userChanged, detail } = nextProps
42 | switch (userChanged.status) {
43 | case 2:
44 | setTimeout(() => {
45 | browserHistory.push('/admin/user')
46 | })
47 | break
48 | case 0:
49 | case 4:
50 | this.state.username = detail.username
51 | this.state.groupId = detail.groupId
52 | this.state.nickname = detail.nickname
53 | this.state.avatar = detail.avatar || defaultAvatar
54 | break
55 | }
56 | }
57 |
58 | validate () {
59 | const { password, passwordRepeat } = this.state
60 | if (password.trim() !== passwordRepeat.trim()) {
61 | this.setState({
62 | valid: false
63 | })
64 | return false
65 | } else {
66 | this.setState({
67 | valid: true
68 | })
69 | }
70 |
71 | return this.state.valid
72 | }
73 |
74 | renderLoding () {
75 | var { status } = this.props.userChanged
76 | switch (status) {
77 | case 1:
78 | return (
79 | 正在提交...
80 |
)
81 | case 2:
82 | return (
83 | 保存成功
84 |
)
85 | case 3:
86 | return (
87 | 保存失败
88 |
)
89 | }
90 | }
91 |
92 | render () {
93 | const groupList = this.props.group.list
94 |
95 | return ()
152 | }
153 | }
154 |
--------------------------------------------------------------------------------