├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── bin ├── article.js └── start.js ├── client ├── Root.js ├── backend │ ├── Admin.js │ ├── Admin.less │ ├── Layout.js │ ├── article │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── EditBase.js │ │ ├── List.js │ │ └── resources.js │ ├── dashboard │ │ └── index.js │ ├── group │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── EditBase.js │ │ └── List.js │ ├── login │ │ └── Login.js │ └── user │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── EditBase.js │ │ └── List.js ├── component │ ├── Back │ │ └── index.jsx │ └── Page │ │ ├── index.jsx │ │ └── style.less ├── frontend │ ├── Layout.js │ └── article │ │ ├── Comment.jsx │ │ ├── Comment_style.less │ │ ├── list.js │ │ ├── list_style.less │ │ ├── view.js │ │ └── view_style.less ├── index.js ├── redux │ ├── actions │ │ ├── article │ │ │ └── index.js │ │ ├── group │ │ │ └── index.js │ │ ├── login │ │ │ └── index.js │ │ └── user │ │ │ └── index.js │ ├── constants │ │ └── index.js │ ├── reducers │ │ ├── article │ │ │ └── index.js │ │ ├── group │ │ │ └── index.js │ │ ├── index.js │ │ ├── login │ │ │ └── index.js │ │ ├── system │ │ │ └── index.js │ │ └── user │ │ │ └── index.js │ ├── resouces │ │ └── article │ │ │ └── index.js │ └── store │ │ └── index.js └── routes │ └── index.js ├── config.js ├── package.json ├── public ├── avatar.png └── favicon.ico ├── server ├── middleware │ ├── auth.js │ ├── authNotStop.js │ └── multipartParser.js ├── model │ ├── Article.js │ ├── Comment.js │ ├── User.js │ ├── UserGroup.js │ ├── index.js │ └── util.js └── router │ ├── article.js │ ├── comment.js │ ├── index.js │ ├── login.js │ ├── upload.js │ └── user.js ├── upload └── .gitignore ├── util ├── fetch.js └── index.js └── webpack ├── client ├── client.config.js ├── index.js └── server.config.js ├── isomorphic.tools.config.js └── server ├── Html.js ├── handleRender.js ├── index.js └── server.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 (
59 | {this.renderLoding()} 60 |
61 |
62 |
63 |
(this.setState({title: e.target.value}))} type="text" placeholder="输入标题" />
64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
(this.setState({tags: e.target.value}))} type="text" placeholder="输入标签用空格分隔" />
74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 |
82 |
) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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 | 67 | 71 | ) 72 | })} 73 | 74 | 75 |
标题操作
{article.title} 68 | 69 | 70 |
76 |
77 | 78 |
) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 |
59 |
60 |
61 |
(this.setState({name: e.target.value}))} type="text" placeholder="输入用户组名称" />
62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |
) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /client/backend/group/List.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import { connect } from 'react-redux' 7 | import { Link } from 'react-router' 8 | import { getGroupList } from '../../redux/actions/group/' 9 | 10 | class GroupList extends Component { 11 | componentDidMount () { 12 | const { getGroupList } = this.props 13 | getGroupList() 14 | } 15 | 16 | render () { 17 | const { list } = this.props.listData 18 | 19 | return (
20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {list.map(group => { 36 | return ( 37 | 38 | 39 | 43 | ) 44 | })} 45 | 46 | 47 |
用户组名称描述操作
{group.name}{group.des} 40 | 41 | 42 |
48 |
) 49 | } 50 | } 51 | 52 | function mapStateToProps (state, ownProps) { 53 | return { 54 | listData: state.group.listData 55 | } 56 | } 57 | 58 | export default connect(mapStateToProps, { 59 | getGroupList 60 | })(GroupList) 61 | -------------------------------------------------------------------------------- /client/backend/login/Login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/14. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import {connect} from 'react-redux' 7 | import { login } from '../../redux/actions/login/' 8 | import { browserHistory } from 'react-router' 9 | 10 | class Login extends Component { 11 | constructor (props) { 12 | super(props) 13 | this.submit = this.submit.bind(this) 14 | } 15 | 16 | state = { 17 | username: '', 18 | password: '' 19 | } 20 | 21 | submit (e) { 22 | e.preventDefault() 23 | const { login } = this.props 24 | const { username, password } = this.state 25 | login(username, password) 26 | } 27 | 28 | componentWillReceiveProps (nextProps) { 29 | const { loginStatus } = nextProps 30 | switch (loginStatus.status) { 31 | case 2: 32 | setTimeout(() => { 33 | browserHistory.push('/admin/dashboard/') 34 | }) 35 | break 36 | } 37 | } 38 | 39 | render () { 40 | return (
41 |
42 |
43 |
登陆前端博客
44 |
45 |
46 | (this.setState({username: e.target.value}))} type="text" placeholder="输入用户名" /> 47 | (this.setState({password: e.target.value}))} type="password" placeholder="输入密码" /> 48 | 49 |
50 |
51 |
52 |
53 |
) 54 | } 55 | } 56 | 57 | function mapStateToProps (state, ownProps) { 58 | return { 59 | loginStatus: state.login.loginStatus 60 | } 61 | } 62 | 63 | export default connect(mapStateToProps, { 64 | login 65 | })(Login) 66 | -------------------------------------------------------------------------------- /client/backend/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/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 (
96 | {this.renderLoding()} 97 |
98 |
99 |
100 |
(this.setState({username: e.target.value}))} type="text" placeholder="输入用户名称" />
101 |
102 |
103 |
104 |
(this.setState({nickname: e.target.value}))} type="text" placeholder="输入用户昵称" />
105 |
106 |
107 |
108 |
109 | 110 | 111 | 112 |
113 |
114 |
115 |
116 |
(this.setState({password: e.target.value}))} type="password" placeholder="输入密码" />
117 |
118 | { 119 | this.state.valid 120 | ? (
121 |
122 |
(this.setState({passwordRepeat: e.target.value}))} type="password" placeholder="输入确认密码" />
123 |
请输入一致的密码
124 |
) 125 | : (
126 |
127 |
(this.setState({passwordRepeat: e.target.value}))} type="password" placeholder="输入确认密码" />
128 |
请输入一致的密码
129 |
) 130 | } 131 |
132 |
133 |
134 | 142 |
143 |
144 |
145 |
146 | 147 | 148 |
149 |
150 |
151 |
) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /client/backend/user/List.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zhangran on 16/10/16. 3 | */ 4 | 5 | import React, { Component } from 'react' 6 | import { connect } from 'react-redux' 7 | import { Link } from 'react-router' 8 | import { getUserList } from '../../redux/actions/user/' 9 | 10 | class UserList extends Component { 11 | componentDidMount () { 12 | const { getUserList } = this.props 13 | getUserList() 14 | } 15 | 16 | render () { 17 | const { list } = this.props.listData 18 | 19 | return (
20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {list.map(user => { 37 | return ( 38 | 39 | 40 | 41 | 45 | ) 46 | })} 47 | 48 | 49 |
用户名昵称所属用户组操作
{user.username}{user.nickname}{user.group && user.group.name} 42 | 43 | 44 |
50 |
) 51 | } 52 | } 53 | 54 | function mapStateToProps (state, ownProps) { 55 | return { 56 | listData: state.user.listData 57 | } 58 | } 59 | 60 | export default connect(mapStateToProps, { 61 | getUserList 62 | })(UserList) 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 108 | 109 |
110 |
111 |
112 |
113 |
114 | 115 |
116 |
117 | 118 |
119 | 120 | 121 |
122 |
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/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/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 | 72 |
73 | 74 |
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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 |
47 |

48 |
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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontoldman/blog/b0eab786e76be9ecd73da8956b18e9e0080ed021/public/avatar.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontoldman/blog/b0eab786e76be9ecd73da8956b18e9e0080ed021/public/favicon.ico -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /upload/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | * 3 | !.gitignore -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /webpack/client/server.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | stats: { 3 | colors: true 4 | }, 5 | historyApiFallback: true, 6 | hot: true 7 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 |