├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── api └── index.js ├── assets ├── README.md └── css │ ├── common.styl │ ├── icon.styl │ ├── index.styl │ ├── mixin.styl │ ├── reset.styl │ └── simplemdecover.styl ├── components ├── README.md ├── alert.vue ├── backTop.vue ├── clientPanel.vue ├── commentList.vue ├── commonFooter.vue ├── commonHeader.vue ├── mainLayout.vue ├── markdown.vue ├── messageList.vue ├── pageNav.vue ├── panel.vue ├── tabHeader.vue ├── topicCreatePanel.vue ├── topicList.vue └── userInfoPanel.vue ├── ecosystem.json ├── filters └── index.js ├── layouts ├── README.md └── default.vue ├── middleware ├── README.md ├── auth.js └── checkRoute.js ├── mixins └── index.js ├── npm-shrinkwrap.json ├── nuxt.config.js ├── package.json ├── pages ├── README.md ├── index.vue ├── login.vue ├── topic │ ├── _id │ │ └── index.vue │ └── create.vue └── user │ ├── _id │ ├── collections.vue │ └── index.vue │ └── messages.vue ├── plugins ├── README.md ├── babel-polyfill.js ├── filter.js └── index.js ├── static ├── README.md ├── favicon.ico ├── node社区.png ├── nuxt-cnode.png ├── 数据.png └── 最后回复时间及当前时间.png ├── store ├── README.md ├── actions.js ├── getters.js ├── index.js ├── mutation-types.js ├── mutations.js └── state.js └── utils ├── axios.js ├── index.js └── scroll.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | 'plugin:vue/essential' 14 | ], 15 | // required to lint *.vue files 16 | plugins: [ 17 | 'vue' 18 | ], 19 | // add your custom rules here 20 | rules: {} 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-cnode 2 | 3 | > 基于vue的nuxt框架仿的cnode社区服务端渲染,主要是为了seo优化以及首屏加载速度 4 | 5 | 6 | 线上地址 [http://nuxt-cnode.foreversnsd.cn][1] 7 | github地址 [https://github.com/Kim09AI/nuxt-cnode][2] 8 | 9 | ### 技术栈 10 | - vue 11 | - vue-router 12 | - vuex 13 | - nuxt 14 | - axios 15 | - simplemde 16 | - ES6/7 17 | - stylus 18 | 19 | ### 目录结构 20 | ``` 21 | ├─npm-shrinkwrap.json 22 | ├─nuxt.config.js # nuxt配置文件 23 | ├─package.json 24 | ├─README.md 25 | ├─utils 26 | | ├─axios.js # axios封装 27 | | ├─index.js # 工具函数 28 | | └scroll.js # 滚动条操作函数 29 | ├─store # store 30 | | ├─actions.js 31 | | ├─getters.js 32 | | ├─index.js 33 | | ├─mutation-types.js 34 | | ├─mutations.js 35 | | ├─README.md 36 | | └state.js 37 | ├─static # 静态资源 38 | | ├─favicon.ico 39 | | └README.md 40 | ├─plugins # vue实例化之前执行的插件 41 | | ├─component.js # 注册全局组件 42 | | ├─filter.js # 注册全局filter 43 | | ├─README.md 44 | | └ssrAccessToken.js # 服务端渲染时保存access_token,供服务端请求时的api获取 45 | ├─pages # 页面级组件 46 | | ├─index.vue # 首页 47 | | ├─login.vue # 登录页 48 | | ├─README.md 49 | | ├─user 50 | | | ├─messages.vue # 未读消息页 51 | | | ├─_id 52 | | | | ├─index.vue # 用户信息页 53 | | | | └collections.vue # 用户收藏的主题页 54 | | ├─topic 55 | | | ├─create.vue # topic创建页,复用为编辑页 56 | | | ├─_id 57 | | | | └index.vue # topic详情页 58 | ├─mixins # mixins 59 | | └index.js 60 | ├─middleware # 中间件,页面渲染之前执行 61 | | ├─auth.js # 用户权限中间件 62 | | ├─checkRoute.js # 主要是对404页面的重定向 63 | | └README.md 64 | ├─layouts # 布局 65 | | ├─default.vue 66 | | └README.md 67 | ├─filters # 全局filter 68 | | └index.js 69 | ├─components 70 | | ├─alert.vue # 提示组件 71 | | ├─backTop.vue 72 | | ├─clientPanel.vue 73 | | ├─commentList.vue # 评论列表 74 | | ├─commonFooter.vue 75 | | ├─commonHeader.vue 76 | | ├─mainLayout.vue # 页面内的主布局,划分左右两栏 77 | | ├─markdown.vue # 基于simplemde封装的组件 78 | | ├─messageList.vue # 消息列表 79 | | ├─pageNav.vue # 分页组件 80 | | ├─panel.vue 81 | | ├─README.md 82 | | ├─tabHeader.vue 83 | | ├─topicCreatePanel.vue 84 | | ├─topicList.vue # 文章列表 85 | | └userInfoPanel.vue 86 | ├─assets 87 | | ├─README.md 88 | | ├─css 89 | | | ├─common.styl 90 | | | ├─icon.styl 91 | | | ├─index.styl 92 | | | ├─mixin.styl 93 | | | ├─reset.styl 94 | | | └simplemdecover.styl 95 | ├─api # 请求api 96 | | └index.js 97 | ``` 98 | 99 | ### 实现的功能 100 | - 首页 101 | - topic详情页 102 | - 新建topic 103 | - 编辑topic 104 | - 收藏topic 105 | - 用户收藏的topic 106 | - 取消收藏topic 107 | - 新建topic的评论 108 | - 新建评论的评论 109 | - 点赞评论 110 | - 个人信息及用户信息 111 | - 登录/退出 112 | - 未读消息页 113 | 114 | ### cookie的共享 115 | 只要做服务端渲染,不管是vue还是react,都必然会遇到cookie共享的问题,因为在服务器上不会为请求自动带cookie,所以需要手动来为请求带上cookie,以下方法主要是借鉴vue-srr导出一个创建app、router、store工厂函数的方法,导出一个创建axios的工厂函数,然后把创建的axios实例注入store,建立store与axios一一对应的关系, 116 | 然后就可以通过store.$axios或state.$axios去请求就会自动带cookie了 117 | 118 | #### 首先获取cookie中的东西放到store.state 119 | ```js 120 | export const nuxtServerInit = async ({ commit, dispatch, state }, { req }) => { 121 | let accessToken = parseCookieByName(req.headers.cookie, 'access_token') 122 | 123 | if (!!accessToken) { 124 | try { 125 | let res = await state.$axios.checkAccesstoken(accessToken) 126 | 127 | if (res.success) { 128 | let userDetail = await state.$axios.getUserDetail(res.loginname) 129 | userDetail.data.id = res.id 130 | 131 | // 提交登录状态及用户信息 132 | dispatch('setUserInfo', { 133 | loginState: true, 134 | user: userDetail.data, 135 | accessToken: accessToken 136 | }) 137 | } 138 | } catch (e) { 139 | console.log('fail in nuxtServerInit', e.message) 140 | } 141 | } 142 | } 143 | ``` 144 | #### 导出一个创建axios的工厂函数 145 | ```js 146 | class CreateAxios extends Api { 147 | constructor(store) { 148 | super(store) 149 | this.store = store 150 | } 151 | 152 | getAccessToken() { 153 | return this.store.state.accessToken 154 | } 155 | 156 | get(url, config = {}) { 157 | let accessToken = this.getAccessToken() 158 | 159 | config.params = config.params || {} 160 | accessToken && (config.params.accesstoken = accessToken) 161 | 162 | return axios.get(url, config) 163 | } 164 | 165 | post(url, data = {}, config = {}) { 166 | let accessToken = this.getAccessToken() 167 | 168 | accessToken && (data.accesstoken = accessToken) 169 | 170 | return axios.post(url, qs.stringify(data), config) 171 | } 172 | 173 | // 返回服务端渲染结果时会用JSON.stringify对state处理,因为store与$axios实例循环引用会导致无法序列化 174 | // 添加toJSON绕过JSON.stringify 175 | toJSON() {} 176 | } 177 | 178 | export default CreateAxios 179 | ``` 180 | #### 在创建store时创建axios并把axios注入store 181 | ```js 182 | const createStore = () => { 183 | let store = new Vuex.Store({ 184 | state, 185 | getters, 186 | mutations, 187 | actions 188 | }) 189 | 190 | store.$axios = store.state.$axios = new CreateAxios(store) 191 | 192 | if (process.browser) { 193 | let replaceState = store.replaceState.bind(store) 194 | store.replaceState = (...args) => { 195 | replaceState(...args) 196 | store.state.$axios = store.$axios 197 | replaceState = null 198 | } 199 | } 200 | 201 | return store 202 | } 203 | 204 | export default createStore 205 | ``` 206 | 之后就可以在asyncData函数中使用store.$axios、在组件内使用this.$store.$axios、在axtion中使用state.$axios或rootState.$axios发起请求了,这些请求都会自动的带上cookie中的东西 207 | 208 | > 若该项目对你有帮助,欢迎 star 209 | 210 | ## Build Setup 211 | 212 | ``` bash 213 | # install dependencies 214 | $ npm install # Or yarn install 215 | 216 | # serve with hot reload at localhost:3000 217 | $ npm run dev 218 | 219 | # build for production and launch server 220 | $ npm run build 221 | $ npm start 222 | 223 | # generate static project 224 | $ npm run generate 225 | ``` 226 | 227 | For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). 228 | 229 | 230 | [1]: http://nuxt-cnode.foreversnsd.cn/ 231 | [2]: https://github.com/Kim09AI/nuxt-cnode 232 | [3]: http://47.106.94.19:3000/node%E7%A4%BE%E5%8C%BA.png 233 | [4]: http://47.106.94.19:3000/nuxt-cnode.png 234 | [5]: http://47.106.94.19:3000/%E6%95%B0%E6%8D%AE.png 235 | [6]: http://47.106.94.19:3000/%E6%9C%80%E5%90%8E%E5%9B%9E%E5%A4%8D%E6%97%B6%E9%97%B4%E5%8F%8A%E5%BD%93%E5%89%8D%E6%97%B6%E9%97%B4.png -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | class Api { 2 | get() { 3 | throw new Error('Abstract methods must be implemented') 4 | } 5 | 6 | post() { 7 | throw new Error('Abstract methods must be implemented') 8 | } 9 | 10 | // 获取主题列表 11 | getTopics(page = 1, tab = 'all', limit = 40, mdrender = 'false') { 12 | return this.get('/topics', { 13 | params: { 14 | page, 15 | tab, 16 | limit, 17 | mdrender 18 | } 19 | }) 20 | } 21 | 22 | // 获取主题详情 23 | getTopicById(id, mdrender = true) { 24 | return this.get(`/topic/${id}`, { 25 | params: { 26 | mdrender 27 | } 28 | }) 29 | } 30 | 31 | // 收藏或取消主题 32 | topicCollect(id, collect) { 33 | let url = collect ? '/topic_collect/collect' : '/topic_collect/de_collect' 34 | return this.post(url, { 35 | topic_id: id 36 | }) 37 | } 38 | 39 | // 获取用户收藏的主题 40 | getTopicCollect(loginname) { 41 | return this.get(`/topic_collect/${loginname}`) 42 | } 43 | 44 | // 获取用户详情 45 | getUserDetail(loginname) { 46 | return this.get(`/user/${loginname}`) 47 | } 48 | 49 | // 验证accesstoken 50 | checkAccesstoken(accessToken) { 51 | return this.post('/accesstoken', { 52 | accesstoken: accessToken 53 | }) 54 | } 55 | 56 | // 创建主题 57 | createTopic(title, content) { 58 | return this.post('/topics', { 59 | title, 60 | content, 61 | tab: 'dev' 62 | }) 63 | } 64 | 65 | // 编辑主题 66 | topicUpdate(id, title, content, tab) { 67 | return this.post('/topics/update', { 68 | topic_id: id, 69 | title, 70 | content, 71 | tab 72 | }) 73 | } 74 | 75 | // 评论 76 | createReplies(topicId, content, reply_id) { 77 | return this.post(`/topic/${topicId}/replies`, { 78 | content, 79 | reply_id 80 | }) 81 | } 82 | 83 | // 点赞 84 | replyLike(reply_id) { 85 | return this.post(`/reply/${reply_id}/ups`) 86 | } 87 | 88 | // 获取消息 89 | getMessages(mdrender = false) { 90 | return this.get('/messages', { 91 | params: { 92 | mdrender 93 | } 94 | }) 95 | } 96 | 97 | // 标记全部已读 98 | messageMarkAll() { 99 | return this.post('/message/mark_all') 100 | } 101 | } 102 | 103 | export default Api 104 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/assets#webpacked 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /assets/css/common.styl: -------------------------------------------------------------------------------- 1 | body 2 | background-color: #e1e1e1 3 | font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti, sans-serif!important 4 | 5 | img 6 | max-width 100% -------------------------------------------------------------------------------- /assets/css/icon.styl: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'iconfont'; /* project id 607223 */ 3 | src: url('//at.alicdn.com/t/font_607223_m65ivlietza4te29.eot'); 4 | src: url('//at.alicdn.com/t/font_607223_m65ivlietza4te29.eot?#iefix') format('embedded-opentype'), 5 | url('//at.alicdn.com/t/font_607223_m65ivlietza4te29.woff') format('woff'), 6 | url('//at.alicdn.com/t/font_607223_m65ivlietza4te29.ttf') format('truetype'), 7 | url('//at.alicdn.com/t/font_607223_m65ivlietza4te29.svg#iconfont') format('svg'); 8 | } 9 | 10 | .iconfont 11 | font-family: "iconfont" !important 12 | font-size: 16px;font-style:normal 13 | -webkit-font-smoothing: antialiased 14 | -webkit-text-stroke-width: 0.2px 15 | -moz-osx-font-smoothing: grayscale 16 | -------------------------------------------------------------------------------- /assets/css/index.styl: -------------------------------------------------------------------------------- 1 | @import "./reset.styl" 2 | @import "./mixin.styl" 3 | @import "./common.styl" 4 | @import "./icon.styl" 5 | -------------------------------------------------------------------------------- /assets/css/mixin.styl: -------------------------------------------------------------------------------- 1 | // 不换行 2 | no-wrap() 3 | text-overflow: ellipsis 4 | overflow: hidden 5 | white-space: nowrap 6 | 7 | // 扩展点击区域 8 | extend-click() 9 | position: relative 10 | &:before 11 | content: '' 12 | position: absolute 13 | top: -10px 14 | left: -10px 15 | right: -10px 16 | bottom: -10px 17 | -------------------------------------------------------------------------------- /assets/css/reset.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video, input 18 | margin: 0 19 | padding: 0 20 | border: 0 21 | font-size: 100% 22 | font-weight: normal 23 | vertical-align: baseline 24 | 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, menu, nav, section 28 | display: block 29 | 30 | body 31 | line-height: 1 32 | 33 | blockquote, q 34 | quotes: none 35 | 36 | blockquote:before, blockquote:after, 37 | q:before, q:after 38 | content: none 39 | 40 | table 41 | border-collapse: collapse 42 | border-spacing: 0 43 | 44 | /* custom */ 45 | 46 | a 47 | color: #7e8c8d 48 | -webkit-backface-visibility: hidden 49 | text-decoration: none 50 | 51 | li 52 | list-style: none 53 | 54 | body 55 | -webkit-text-size-adjust: none 56 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 57 | -------------------------------------------------------------------------------- /assets/css/simplemdecover.styl: -------------------------------------------------------------------------------- 1 | .editor-toolbar 2 | border: none!important 3 | .CodeMirror 4 | border: none!important 5 | .CodeMirror, .CodeMirror-scroll 6 | min-height: 200px!important 7 | .CodeMirror-wrap pre 8 | word-break: break-all 9 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | The components directory contains your Vue.js Components. 4 | Nuxt.js doesn't supercharge these components. 5 | 6 | **This directory is not required, you can delete it if you don't want to use it.** 7 | -------------------------------------------------------------------------------- /components/alert.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 50 | -------------------------------------------------------------------------------- /components/backTop.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | 59 | -------------------------------------------------------------------------------- /components/clientPanel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 30 | -------------------------------------------------------------------------------- /components/commentList.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 104 | 105 | 170 | -------------------------------------------------------------------------------- /components/commonFooter.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 68 | -------------------------------------------------------------------------------- /components/commonHeader.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 101 | 102 | 156 | -------------------------------------------------------------------------------- /components/mainLayout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | -------------------------------------------------------------------------------- /components/markdown.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 85 | 86 | 115 | -------------------------------------------------------------------------------- /components/messageList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /components/pageNav.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 101 | 102 | 124 | -------------------------------------------------------------------------------- /components/panel.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 57 | -------------------------------------------------------------------------------- /components/tabHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /components/topicCreatePanel.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /components/topicList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 84 | -------------------------------------------------------------------------------- /components/userInfoPanel.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 45 | -------------------------------------------------------------------------------- /ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "nuxt-cnode", 5 | "script": "./node_modules/nuxt/bin/nuxt-start", 6 | "env": { 7 | "COMMON_VARIABLE": true 8 | }, 9 | "env_production": { 10 | "NODE_ENV": "production" 11 | } 12 | } 13 | ], 14 | "deploy": { 15 | "production": { 16 | "user": "kim", 17 | "host": ["47.106.94.19"], 18 | "port": "22", 19 | "ref": "origin/master", 20 | "repo": "git@github.com:Kim09AI/nuxt-cnode.git", 21 | "path": "/www/nuxt-cnode/production", 22 | "ssh_options": "StrictHostKeyChecking=no", 23 | "post-deploy": "cnpm i && npm run build && pm2 startOrRestart ecosystem.json --env production", 24 | "env": { 25 | "NODE_ENV": "production" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /filters/index.js: -------------------------------------------------------------------------------- 1 | import { timeFormat } from '../utils' 2 | 3 | export function timeFormatFilter(value) { 4 | return timeFormat(value) 5 | } 6 | -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | This directory contains your Application Layouts. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/views#layouts 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | This directory contains your Application Middleware. 4 | The middleware lets you define custom function to be ran before rendering a page or a group of pages (layouts). 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing#middleware 8 | 9 | **This directory is not required, you can delete it if you don't want to use it.** 10 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default function({ store, redirect }) { 2 | if (!store.state.isLogin) { 3 | redirect('/login') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /middleware/checkRoute.js: -------------------------------------------------------------------------------- 1 | export default function({ route, redirect }) { 2 | // 没有匹配到的组件,即404页面 3 | if (route.matched.length === 0) { 4 | redirect('/') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mixins/index.js: -------------------------------------------------------------------------------- 1 | import { tabs } from '~/utils' 2 | 3 | export const tabFormatMixin = { 4 | methods:{ 5 | tabFormat(tab, isTop, isGood) { 6 | if (isTop) { 7 | return '置顶' 8 | } 9 | if (isGood) { 10 | return '精华' 11 | } 12 | return tabs[tab] 13 | } 14 | } 15 | } 16 | 17 | export const axiosMixin = { 18 | beforeCreate() { 19 | if (this.$store && this.$store.$axios) { 20 | this.$axios = this.$store.$axios 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = { 4 | /* 5 | ** Headers of the page 6 | */ 7 | head: { 8 | title: 'CNode:Node.js专业中文社区', 9 | meta: [ 10 | { charset: 'utf-8' }, 11 | { 12 | name: 'viewport', 13 | content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' 14 | }, 15 | { 16 | hid: 'description', 17 | name: 'description', 18 | content: '基于vue的Nuxt框架仿cnode社区 - Nuxt.js project' 19 | }, 20 | { 21 | name: 'keywords', 22 | content: 'vue, vue-router, vuex, nuxt, cnode, blog' 23 | }, 24 | { 25 | name: 'author', 26 | content: 'Kim09AI' 27 | }, 28 | { 29 | 'http-equiv': 'Cache-Control', 30 | conent: 'no-siteapp' 31 | } 32 | ], 33 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }] 34 | }, 35 | css: [{ src: '~assets/css/index.styl', lang: 'stylus' }], 36 | /* 37 | ** Customize the progress bar color 38 | */ 39 | loading: { color: '#3B8070' }, 40 | /* 41 | ** Build configuration 42 | */ 43 | build: { 44 | vendor: ['axios', 'simplemde', 'js-cookie', 'qs', 'babel-polyfill'], 45 | /* 46 | ** Run ESLint on save 47 | */ 48 | extend(config, { isDev, isClient }) { 49 | if (isDev && isClient) { 50 | config.module.rules.push({ 51 | enforce: 'pre', 52 | test: /\.(js|vue)$/, 53 | loader: 'eslint-loader', 54 | exclude: /(node_modules)/ 55 | }) 56 | } 57 | } 58 | }, 59 | plugins: [ 60 | { src: '~plugins/babel-polyfill', ssr: false }, 61 | '~plugins/index', 62 | '~plugins/filter' 63 | ], 64 | router: { 65 | extendRoutes(routes) { 66 | // 复用topic的create页面 67 | routes.push({ 68 | name: 'edit', 69 | path: '/topic/:id/edit', 70 | component: resolve(__dirname, 'pages/topic/create.vue') 71 | }) 72 | }, 73 | middleware: 'checkRoute' 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-cnode", 3 | "version": "1.0.0", 4 | "description": "Nuxt.js project", 5 | "author": "Kim09", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "startByPm2": "pm2 start --name nuxt-cnode ./node_modules/nuxt/bin/nuxt-start", 12 | "generate": "nuxt generate", 13 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 14 | "precommit": "npm run lint" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.18.0", 18 | "babel-polyfill": "^6.26.0", 19 | "js-cookie": "^2.2.0", 20 | "nuxt": "^1.0.0", 21 | "qs": "^6.5.1", 22 | "simplemde": "^1.11.2", 23 | "babel-eslint": "^8.2.1", 24 | "eslint": "^4.15.0", 25 | "eslint-friendly-formatter": "^3.0.0", 26 | "eslint-loader": "^1.7.1", 27 | "eslint-plugin-vue": "^4.0.0", 28 | "stylus": "^0.54.5", 29 | "stylus-loader": "^3.0.2" 30 | }, 31 | "devDependencies": { 32 | 33 | }, 34 | "keywords": [ 35 | "vue", 36 | "vue-router", 37 | "vuex", 38 | "nuxt", 39 | "cnode" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the .vue files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing 8 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 114 | 115 | 132 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 80 | 81 | 119 | -------------------------------------------------------------------------------- /pages/topic/_id/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 256 | 257 | 334 | -------------------------------------------------------------------------------- /pages/topic/create.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 149 | 150 | 176 | -------------------------------------------------------------------------------- /pages/user/_id/collections.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 66 | 67 | 70 | -------------------------------------------------------------------------------- /pages/user/_id/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 87 | 88 | 142 | -------------------------------------------------------------------------------- /pages/user/messages.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 78 | 79 | 97 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | This directory contains your Javascript plugins that you want to run before instantiating the root vue.js application. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/plugins 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /plugins/babel-polyfill.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' -------------------------------------------------------------------------------- /plugins/filter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import * as filters from '../filters' 3 | 4 | Object.keys(filters).forEach(key => { 5 | Vue.filter(key, filters[key]) 6 | }) 7 | -------------------------------------------------------------------------------- /plugins/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import mainLayout from '../components/mainLayout' 3 | import panel from '../components/panel' 4 | import { axiosMixin } from '../mixins' 5 | 6 | Vue.component('main-layout', mainLayout) 7 | Vue.component('panel', panel) 8 | 9 | Vue.mixin(axiosMixin) 10 | 11 | if (process.browser) { 12 | // 注册全局的提示组件 13 | const alert = require('../components/alert').default 14 | const Alert = Vue.prototype.$Alert = new Vue(alert).$mount() 15 | document.body.appendChild(Alert.$el) 16 | } 17 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | This directory contains your static files. 4 | Each file inside this directory is mapped to /. 5 | 6 | Example: /static/robots.txt is mapped as /robots.txt. 7 | 8 | More information about the usage of this directory in the documentation: 9 | https://nuxtjs.org/guide/assets#static 10 | 11 | **This directory is not required, you can delete it if you don't want to use it.** 12 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/nuxt-cnode/c1b0daed52544e8902ba7b8c3dbe7ac954315424/static/favicon.ico -------------------------------------------------------------------------------- /static/node社区.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/nuxt-cnode/c1b0daed52544e8902ba7b8c3dbe7ac954315424/static/node社区.png -------------------------------------------------------------------------------- /static/nuxt-cnode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/nuxt-cnode/c1b0daed52544e8902ba7b8c3dbe7ac954315424/static/nuxt-cnode.png -------------------------------------------------------------------------------- /static/数据.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/nuxt-cnode/c1b0daed52544e8902ba7b8c3dbe7ac954315424/static/数据.png -------------------------------------------------------------------------------- /static/最后回复时间及当前时间.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/nuxt-cnode/c1b0daed52544e8902ba7b8c3dbe7ac954315424/static/最后回复时间及当前时间.png -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | This directory contains your Vuex Store files. 4 | Vuex Store option is implemented in the Nuxt.js framework. 5 | Creating a index.js file in this directory activate the option in the framework automatically. 6 | 7 | More information about the usage of this directory in the documentation: 8 | https://nuxtjs.org/guide/vuex-store 9 | 10 | **This directory is not required, you can delete it if you don't want to use it.** 11 | -------------------------------------------------------------------------------- /store/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | import { parseCookieByName } from '~/utils' 3 | 4 | export const nuxtServerInit = async ({ commit, dispatch, state }, { req }) => { 5 | let accessToken = parseCookieByName(req.headers.cookie, 'access_token') 6 | 7 | if (!!accessToken) { 8 | try { 9 | let res = await state.$axios.checkAccesstoken(accessToken) 10 | 11 | if (res.success) { 12 | let userDetail = await state.$axios.getUserDetail(res.loginname) 13 | userDetail.data.id = res.id 14 | 15 | // 提交登录状态及用户信息 16 | dispatch('setUserInfo', { 17 | loginState: true, 18 | user: userDetail.data, 19 | accessToken: accessToken 20 | }) 21 | } 22 | } catch (e) { 23 | console.log('fail in nuxtServerInit', e.message) 24 | } 25 | } 26 | } 27 | 28 | export const setUserInfo = ({ commit }, { loginState, user, accessToken }) => { 29 | commit(types.SET_LOGIN_STATE, loginState) 30 | commit(types.SET_USER_INFO, user) 31 | commit(types.SET_ACCESS_TOKEN, accessToken) 32 | } 33 | -------------------------------------------------------------------------------- /store/getters.js: -------------------------------------------------------------------------------- 1 | export const isLogin = state => state.isLogin 2 | 3 | export const user = state => state.user 4 | 5 | export const accessToken = state => state.accessToken 6 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import state from './state' 4 | import * as getters from './getters' 5 | import mutations from './mutations' 6 | import * as actions from './actions' 7 | import CreateAxios from '../utils/axios' 8 | 9 | Vue.use(Vuex) 10 | 11 | const createStore = () => { 12 | let store = new Vuex.Store({ 13 | state, 14 | getters, 15 | mutations, 16 | actions 17 | }) 18 | 19 | store.$axios = store.state.$axios = new CreateAxios(store) 20 | 21 | if (process.browser) { 22 | let replaceState = store.replaceState.bind(store) 23 | store.replaceState = (...args) => { 24 | replaceState(...args) 25 | store.state.$axios = store.$axios 26 | replaceState = null 27 | } 28 | } 29 | 30 | return store 31 | } 32 | 33 | export default createStore 34 | -------------------------------------------------------------------------------- /store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_LOGIN_STATE = 'SET_LOGIN_STATE' 2 | 3 | export const SET_USER_INFO = 'SET_USER_INFO' 4 | 5 | export const SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN' 6 | -------------------------------------------------------------------------------- /store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | 3 | const mutations = { 4 | [types.SET_LOGIN_STATE](state, loginState = false) { 5 | state.isLogin = loginState 6 | }, 7 | [types.SET_USER_INFO](state, user = {}) { 8 | state.user = user 9 | }, 10 | [types.SET_ACCESS_TOKEN](state, accessToken) { 11 | state.accessToken = accessToken 12 | } 13 | } 14 | 15 | export default mutations 16 | -------------------------------------------------------------------------------- /store/state.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | isLogin: false, 3 | user: {}, 4 | accessToken: '' 5 | }) 6 | 7 | export default state 8 | -------------------------------------------------------------------------------- /utils/axios.js: -------------------------------------------------------------------------------- 1 | import originAxios from 'axios' 2 | import qs from 'qs' 3 | import Api from '../api' 4 | 5 | const axios = originAxios.create({ 6 | baseURL: 'https://cnodejs.org/api/v1', 7 | headers: { 8 | post: { 9 | 'Content-Type': 'application/x-www-form-urlencoded' 10 | } 11 | } 12 | }) 13 | 14 | // 响应拦截器 15 | axios.interceptors.response.use(response => { 16 | return response && response.data 17 | }, err => { 18 | // 错误处理, 处理success为false的情况 19 | if (err.response && err.response.data) { 20 | return Promise.resolve(err.response.data) 21 | } 22 | return Promise.reject(err) 23 | }) 24 | 25 | class CreateAxios extends Api { 26 | constructor(store) { 27 | super(store) 28 | this.store = store 29 | } 30 | 31 | getAccessToken() { 32 | return this.store.state.accessToken 33 | } 34 | 35 | get(url, config = {}) { 36 | let accessToken = this.getAccessToken() 37 | 38 | config.params = config.params || {} 39 | accessToken && (config.params.accesstoken = accessToken) 40 | 41 | return axios.get(url, config) 42 | } 43 | 44 | post(url, data = {}, config = {}) { 45 | let accessToken = this.getAccessToken() 46 | 47 | accessToken && (data.accesstoken = accessToken) 48 | 49 | return axios.post(url, qs.stringify(data), config) 50 | } 51 | 52 | // 返回服务端渲染结果时会用JSON.stringify对state处理,因为store与$axios实例循环引用会导致无法序列化 53 | // 添加toJSON绕过JSON.stringify 54 | toJSON() {} 55 | } 56 | 57 | export default CreateAxios 58 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | const timeFormatArr = [0, 60, 3600, 86400, 2592000, 31104000, Number.MAX_VALUE] 2 | const timeUnit = ['刚刚', '分钟前', '小时前', '天前', '月前', '年前'] 3 | 4 | export function timeFormat(dateStr) { 5 | // 先toString转成当前时区的时间再getTime 6 | let dateTime = new Date(new Date(dateStr).toString()).getTime() 7 | let now = new Date().getTime() 8 | let time = (now - dateTime) / 1000 9 | 10 | let index = timeFormatArr.findIndex((item, index) => { 11 | return item <= time && timeFormatArr[index + 1] > time 12 | }) 13 | 14 | if (index === 0) { 15 | return timeUnit[0] 16 | } 17 | 18 | time = time / timeFormatArr[index] | 0 19 | return time + timeUnit[index] 20 | } 21 | 22 | export const tabs = { 23 | share: '分享', 24 | ask: '问答', 25 | good: '精华', 26 | job: '招聘', 27 | dev: '测试' 28 | } 29 | 30 | // 解析请求头cookie的指定name值 31 | export const parseCookieByName = (cookie, name) => { 32 | if (!cookie || !name) return '' 33 | 34 | let pattern = new RegExp(`(?:^|\\s)${name}=([^;]*)(?:;|$)`) 35 | let matched = cookie.match(pattern) || [] 36 | let value = matched[1] || '' 37 | 38 | return decodeURIComponent(value) 39 | } 40 | -------------------------------------------------------------------------------- /utils/scroll.js: -------------------------------------------------------------------------------- 1 | const isClient = process.browser 2 | 3 | const scrollFunc = (() => { 4 | if (!isClient) { 5 | return {} 6 | } 7 | window.requestAnimFrame = (function () { 8 | return window.requestAnimationFrame || 9 | window.webkitRequestAnimationFrame || 10 | window.mozRequestAnimationFrame || 11 | function (callback) { 12 | window.setTimeout(callback, 1000 / 60) 13 | } 14 | })() 15 | 16 | function scroll(top, time = 600) { 17 | let scrollTop = document.documentElement.scrollTop || document.body.scrollTop 18 | let dire = scrollTop > top ? 1 : -1 // 滚动方向,向上或向下 19 | let start 20 | function scrollTo(timestamp) { 21 | if (start === undefined) start = timestamp 22 | let progress = timestamp - start 23 | let offset = (scrollTop - top) * progress / time 24 | scrollTop -= offset 25 | if (dire * (scrollTop - top) > 0) { 26 | window.scrollTo(0, scrollTop) 27 | requestAnimationFrame(scrollTo) 28 | } else { 29 | window.scrollTo(0, top) 30 | } 31 | } 32 | 33 | requestAnimationFrame(scrollTo) 34 | } 35 | 36 | function scrollToElement(el, time) { 37 | let scrollTop = document.documentElement.scrollTop || document.body.scrollTop 38 | let top = el.getBoundingClientRect().top 39 | scroll(scrollTop + top, time) 40 | } 41 | 42 | function scrollToTop(time) { 43 | scroll(0, time) 44 | } 45 | 46 | return { 47 | scroll, 48 | scrollToElement, 49 | scrollToTop 50 | } 51 | })() 52 | 53 | export default scrollFunc 54 | --------------------------------------------------------------------------------