├── .editorconfig ├── .eslintignore ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.vue ├── common │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ └── iconfont.woff │ ├── pictures │ │ └── cnodejs_light.svg │ ├── style │ │ ├── animation.scss │ │ ├── base.scss │ │ └── icon.scss │ └── utils │ │ ├── cookie.js │ │ ├── deepCopy.js │ │ └── timeFormat.js ├── components │ ├── ArticleCard │ │ ├── ArticleCard.vue │ │ └── index.js │ ├── BackBar │ │ ├── BackBar.vue │ │ └── index.js │ ├── Loading │ │ ├── Loading.vue │ │ └── loading.svg │ ├── MessageCard │ │ ├── MessageCard.vue │ │ └── index.js │ ├── Page │ │ ├── Page.vue │ │ └── index.js │ └── TopBar │ │ ├── TopBar.vue │ │ └── index.js ├── main.js ├── pic │ ├── CNode-requirement-analysis.png │ └── QR-Code.png ├── router │ └── index.js ├── store │ ├── modules │ │ ├── article │ │ │ ├── article-mutation-types.js │ │ │ └── article.js │ │ ├── content │ │ │ ├── content-mutation-types.js │ │ │ └── content.js │ │ ├── login │ │ │ ├── login-mutation-types.js │ │ │ └── login.js │ │ ├── messages │ │ │ ├── messages-mutation-types.js │ │ │ └── messages.js │ │ ├── navbar │ │ │ ├── navbar-mutation-types.js │ │ │ └── navbar.js │ │ ├── notification │ │ │ ├── notification-mutation-types.js │ │ │ └── notification.js │ │ └── user │ │ │ ├── user-mutation-types.js │ │ │ └── user.js │ └── store.js └── views │ ├── ArticleDetail │ ├── ArticleDetail.vue │ ├── BottomBar │ │ └── BottomBar.vue │ ├── CommentSorter.vue │ ├── Comments.vue │ └── index.js │ ├── ArticlePublish │ ├── ArticlePublish.vue │ └── index.js │ ├── HomePage │ ├── HomePage.vue │ └── index.js │ ├── MyCollection │ ├── MyCollection.vue │ └── index.js │ ├── UserDetail │ ├── UserDetail.vue │ └── index.js │ ├── UserLogin │ ├── UserLogin.vue │ └── index.js │ └── UserNotification │ ├── UserNotification.vue │ └── index.js ├── vue.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | .vscode 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CNode-Vue 2 | 3 | ## 前言 4 | > 感谢[CNode社区](https://cnodejs.org/)提供的API,项目的来源就是因为在CNode官网看到了API接口,所以才萌生了做一个Vue项目的想法。现在项目的基本功能都已经做完,但仍需要后续的完善。 5 | 6 | ## 项目地址 7 | 8 | **源码地址**:[用力点我](https://github.com/SuperJerryshen/Vue-CNode) 9 | 10 | **预览地址**:[使劲点我](http://cnode.jerryshen.cn) 11 | 12 | 你也可以扫描下面的二维码预览线上项目: 13 | 14 | [![二维码](https://github.com/SuperJerryshen/Vue-CNode/blob/master/src/pic/QR-Code.png?raw=true)](http://106.14.179.237:8082) 15 | 16 | ## 技术栈 17 | ``` 18 | Vue2.0: 构建项目,属于底层框架 19 | Vue-Router: 通过hash值的变化,从而改变页面结构的路由。 20 | Vuex: Vue官方提供的状态管理模式。 21 | Axios, Vue-Axios: http请求模块 22 | ES6: 较新的Javascript语法 23 | Sass: CSS预编译器 24 | ``` 25 | 26 | > 用到的一些工具包括iconfont来做字体图标,引入mavon-editor插件,优化编辑文章时的markdown书写体验。 27 | 28 | ## 版本 29 | 30 | v0.4.1 beta 31 | 32 | ## 功能需求分析 33 | > 根据需求,我做了一张分析图,如下。 34 | 35 | ![CNode需求分析图](/src/pic/CNode-requirement-analysis.png) 36 | 37 | ## 项目结构 38 | ``` 39 | . 40 | ├── build // webpack设置 41 | │   ├── build.js 42 | │   ├── check-versions.js 43 | │   ├── dev-client.js 44 | │   ├── dev-server.js 45 | │   ├── utils.js 46 | │   ├── vue-loader.conf.js 47 | │   ├── webpack.base.conf.js 48 | │   ├── webpack.dev.conf.js 49 | │   └── webpack.prod.conf.js 50 | ├── config // 项目开发和打包设置 51 | │   ├── dev.env.js 52 | │   ├── index.js 53 | │   └── prod.env.js 54 | ├── src // 项目文件位置 55 | │   ├── App.vue // 组件总入口 56 | │   ├── common // 通用文件 57 | │   │   ├── fonts // 字体 58 | │   │   │   ├── iconfont.eot 59 | │   │   │   ├── iconfont.svg 60 | │   │   │   ├── iconfont.ttf 61 | │   │   │   └── iconfont.woff 62 | │   │   ├── style // 样式 63 | │   │   │   ├── animation.scss // 动画 64 | │   │   │   ├── base.scss // 基本样式 65 | │   │   │   └── icon.scss // iconfont的字体图标样式 66 | │   │   └── utils // 工具函数 67 | │   │   ├── cookie.js // cookie存取和删除 68 | │   │   └── timeFormat.js // 格式化时间函数 69 | │   ├── components // 所有组件 70 | │   │   ├── AboutMe // 关于 71 | │   │   │   └── AboutMe.vue 72 | │   │   ├── Article // 文章详情页 73 | │   │   │   └── Article.vue 74 | │   │   ├── ArticleCard // 文章列表的单个文章卡片 75 | │   │   │   └── ArticleCard.vue 76 | │   │   ├── BackBar // 顶部的返回栏(返回主页和后退) 77 | │   │   │   └── BackBar.vue 78 | │   │   ├── BottomBar // 底部的回复栏(还包含收藏和编辑文件) 79 | │   │   │   └── BottomBar.vue 80 | │   │   ├── Content // 主页 81 | │   │   │   └── Content.vue 82 | │   │   ├── Loading // 正在加载组件 83 | │   │   │   ├── Loading.vue 84 | │   │   │   └── loading.svg 85 | │   │   ├── Login // 登录 86 | │   │   │   └── Login.vue 87 | │   │   ├── MessageCard // 单个通知的详情卡片 88 | │   │   │   └── MessageCard.vue 89 | │   │   ├── MyCollect // 我的收藏页 90 | │   │   │   └── MyCollect.vue 91 | │   │   ├── Notification // 通知页 92 | │   │   │   └── Notification.vue 93 | │   │   ├── Publish // 发布文章和发布更新页 94 | │   │   │   └── Publish.vue 95 | │   │   ├── UserDetail // 用户详情页 96 | │   │   │   └── UserDetail.vue 97 | │   │   └── navBar // 主页的顶部导航栏 98 | │   │   ├── cnodejs_light.svg 99 | │   │   └── navBar.vue 100 | │   ├── main.js // 项目的总入口 101 | │   ├── pic // 和代码无关,README.md中的图片 102 | │   │   ├── CNode-requirement-analysis.png 103 | │   │   └── QR-Code.png 104 | │   ├── router // 路由设置 105 | │   │   └── index.js 106 | │   └── store // 状态管理 107 | │   ├── modules 108 | │   │   ├── article // 文章详情页 109 | │   │   │   ├── article-mutation-types.js 110 | │   │   │   └── article.js 111 | │   │   ├── content // 主页 112 | │   │   │   ├── content-mutation-types.js 113 | │   │   │   └── content.js 114 | │   │   ├── login // 登录页 115 | │   │   │   ├── login-mutation-types.js 116 | │   │   │   └── login.js 117 | │   │   ├── navbar // 主页导航栏 118 | │   │   │   ├── navbar-mutation-types.js 119 | │   │   │   └── navbar.js 120 | │   │   ├── notification // 通知页 121 | │   │   │   ├── notification-mutation-types.js 122 | │   │   │   └── notification.js 123 | │   │   └── user // 用户详情页 124 | │   │   ├── user-mutation-types.js 125 | │   │   └── user.js 126 | │   └── store.js // 状态管理总入口 127 | ├── README.md 128 | ├── index.html 129 | └── package.json 130 | ``` 131 | 132 | ## 功能实现情况 133 | - [x] 首页列表 134 | - [x] 无限懒加载文章列表 135 | - [x] 切换内容主题 136 | - [x] 文章详情 137 | - [x] 在文章详情页时,可以后退至主页 138 | - [x] 回到顶部功能,并添加动画效果 139 | - [x] 关于 140 | - [x] 用户登录 141 | - [x] 用户退出 142 | - [x] 个人主页 143 | - [x] 我的收藏 144 | - [x] 点击用户头像,可以进入该用户的简介页面 145 | - [x] 登陆后,可在文章详情页点赞和评论 146 | - [x] 登陆后,在主页显示发布主题按钮,可以发布主题 147 | - [x] 消息通知,消息设置已读功能 148 | - [x] 对自己的文章可以进行编辑更新 149 | - [x] 操作成功或失败后的消息提醒 150 | - [x] 增加markdown的编辑器组件和预览器组件,依赖于`mavon-editor` 151 | - [ ] 评论排序功能 152 | 153 | 154 | ## 心得体会&技术难点 155 | 156 | > 本项目算是本人第一个完整的手机和pc都兼容,有关于文章展示的项目。整个项目做下来,遇到的Bug很多,自然收获也是很多。总结下来如下: 157 | 158 | 1.很长的单词会超出边界,导致可视区域变宽。 159 | 160 | 解决办法:通过`word-wrap: break-word;`实现打断效果。 161 | 162 | 2.第二次进入文章时,会残留。 163 | 164 | 解决办法:通过路由的钩子函数beforeRouteEnter,来获取数据,未成功获取数据时,显示Loading页面,加载完成后,显示文章详情页,从而解决这个问题。 165 | 166 | 3.回到首页时,不能保留原来的状态。 167 | 168 | 解决办法: 169 | 170 | ①此方法为容易固定高度的解决办法。(具体方法:用vuex和vue-router的钩子函数来解决这个问题,即通过scroll事件动态保存此时的scrollTop直,当路由的beforeRouteEnter出发时,恢复其scrollTop的值。) 171 | 172 | ② 如果没有固定高度,直接通过Vue自带的keep-alive组件,保留组件状态。 173 | 174 | 4.载入中的动画效果如何做? 175 | 176 | 解决办法:之前是通过CSS3绘制一个图形,但是后来发现太丑了,就直接用了Iconfont上的svg图,并添加了动画效果。 177 | 178 | 5.如何实现主页文章列表的懒加载? 179 | 180 | 解决办法:判断滑动的总高度 - 滑动距离顶部的距离 <= 屏幕的可用高度,也就是以下公式: 181 | ```javascript 182 | document.documentElement.offsetHeight - window.scrollY 183 | <= window.screen.height 184 | ``` 185 | 这里会出现一个bug,满足条件时,继续滑动,会加载多次。在此可以加入一个状态,表示此时正在加载(详细参见源代码),从而解决此bug。 186 | 187 | 6.回到顶部的动画怎么做? 188 | 189 | 解决办法:可以把现在的`window.scrollY`分成`n`份,然后再设置一个定时器,每隔`m`秒,向上滚动一份的高度,当`window.scrollY >= 0`时,再终止定时器。(其中的`m, n`为任意数,根据情况设定) 190 | 191 | 7.如何控制正在加载页面的显示? 192 | 193 | 解决办法:因为加载数据是异步的,可以在加载之前和加载之后,分别更改一个类似于`isLoading`(名称自己设定)的状态,从而控制加载页面的显示。 194 | 195 | 8.如何设置登录功能? 196 | 197 | 解决办法:因为官方只提供了`access-token`,所以可以将此值和一些用户相关的数值,存入`document.cookie`中,存入的函数我单独写了一个`cookie`的工具函数,代码如下: 198 | ```javascript 199 | /** 200 | * Created by jerryshen on 2017/7/15. 201 | * 用户本地cookie的存取以及清空 202 | * 函数的功能分别是: 203 | * 设置单个,获取所有,获取单个,删除所有,删除单个 204 | */ 205 | 206 | export function setCookie (name, value, exdays = 30) { 207 | var time = new Date() 208 | time.setTime(time.getTime() + exdays * 24 * 3600 * 1000) 209 | var expires = 'expires=' + time.toGMTString() 210 | document.cookie = name + '=' + value + ';' + expires 211 | } 212 | 213 | export function getAllCookies () { 214 | if (document.cookie === '') { 215 | return {} 216 | } 217 | const cookies = document.cookie.split(';') 218 | const newCookies = {} 219 | for (let i = 0; i < cookies.length; i++) { 220 | let cookie = cookies[i].trim() 221 | const splitCookie = cookie.split('=') 222 | newCookies[splitCookie[0]] = splitCookie[1] 223 | } 224 | return newCookies 225 | } 226 | 227 | export function getCookie (name) { 228 | const cname = name + '=' 229 | const cookies = document.cookie.split(';') 230 | for (let i = 0; i < cookies.length; i++) { 231 | let cookie = cookies[i].trim() 232 | if (cookie.indexOf(cname) === 0) { 233 | return { 234 | success: true, 235 | cookie: { 236 | name, 237 | value: cookie.split(cname)[1] 238 | } 239 | } 240 | } else { 241 | return { 242 | success: false, 243 | cookie: { 244 | name, 245 | value: undefined 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | export function deleteAllCookie () { 253 | document.cookie += ';expires=Thu, 01 Jan 1970 00:00:00 GMT' 254 | } 255 | 256 | export function deleteCookie (name) { 257 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT` 258 | } 259 | ``` 260 | 261 | 9.如何将API中的时间转换成 => ..年前,..月前,..天前等等,这种类型的格式呢? 262 | 263 | 解决办法:我自己写了一个格式化的工具函数,代码如下: 264 | ```javascript 265 | export default function timeFormat (date) { 266 | // 获取当前时间和所传时间的Date对象 267 | const nowTime = new Date() 268 | const inDate = new Date(date) 269 | if (nowTime.getYear() - inDate.getYear() > 0) { 270 | // 年份差值 > 0,返回年 271 | return `${nowTime.getFullYear() - inDate.getFullYear()}年前` 272 | } else if (nowTime.getMonth() - inDate.getMonth() > 0) { 273 | // 月份差值 > 0,返回月 274 | return `${nowTime.getMonth() - inDate.getMonth()}个月前` 275 | } else if (nowTime.getDate() - inDate.getDate() > 0) { 276 | // 日期差值 > 0,返回日 277 | return `${nowTime.getDate() - inDate.getDate()}天前` 278 | } else if (nowTime.getHours() - inDate.getHours() > 0) { 279 | // 小时差值 > 0,返回时 280 | return `${nowTime.getHours() - inDate.getHours()}个小时前` 281 | } else if (nowTime.getMinutes() - inDate.getMinutes() > 0) { 282 | // 分钟差值 > 0,返回分钟 283 | return `${nowTime.getMinutes() - inDate.getMinutes()}分钟前` 284 | } else { 285 | // 其他情况,也就是秒数差值 > 0,返回秒钟 286 | return `${nowTime.getSeconds() - inDate.getSeconds()}秒前` 287 | } 288 | } 289 | ``` 290 | 291 | 10.BUG:当进入其他路由时,仍然会触发主页的scroll事件。 292 | 293 | 解决办法:之前生命周期钩子用的是`mounted`,因此进入其他路由时,scroll事件仍然存在。所以现在改用`beforeRouteEnter`和`beforeRouteLeave`这两个路由的生命周期钩子,分别实现载入路由时的scroll事件挂载、离开路由时的scroll事件卸载。从而防止主页内容的懒加载一直触发。 294 | 295 | 11.发布新文章或更新跳转至文章详情页面后,再按后退,怎么实现回到主页? 296 | 297 | 解决办法:现在初步是使用,路由跳转的时候,先跳到主页,再跳到文章详情页,再按后退时,就会回到主页。 298 | 299 | 12.如何实现点击评论右侧的回复按钮,添加@信息,并focus输入框? 300 | 301 | 解决办法:通过vuex来实时记录回复相关的信息,并通过watch输入框的value来判断是否focus。 302 | 303 | 13.有一个很奇怪的bug:ios下,如果在文章详情页返回主页时,此时的`window.scrollY`会保持文章详情页时的`window.scrollY`,如果此值满足异步加载更多数据的条件时,会导致异常加载数据。 304 | 305 | 解决办法:不得已,只好在`beforeRouteEnter`钩子中,绑定滚动事件的函数加一个定时器,使其在100ms后绑定事件,所以此时的`window.scrollY`就会变成之前的值。 306 | 307 | 14.如何实现全局的消息提醒? 308 | 309 | 解决办法:我是通过一个和路由同级的组件`Messages`,并且创建了一个状态管理的模块`messages`,在其中通过`state: messages`存放现在显示的的通知数据,利用`Messages`组件和`vuex`的`actions`控制其显示。 310 | 311 | 15.因为Publish组件中加入了`mavon-editor`,所以使得整个应用完全加载会非常耗时,怎样实现异步加载Publish组件? 312 | 313 | 解决办法:可以通过webpack提供的`code-split`,具体代码如下: 314 | ```javascript 315 | // 通过this.a.app来访问Vue实例对象,实现dispatch来增加加载状态 316 | const Publish = resolve => { 317 | this.a.app.$store.dispatch('changeLoadingStatus', true) 318 | require.ensure(['../components/Publish/Publish'], () => { 319 | resolve(require('../components/Publish/Publish')) 320 | }).then(() => { 321 | // this.$store.dispatch('changeLoadingStatus', false) 322 | this.a.app.$store.dispatch('changeLoadingStatus', false) 323 | }) 324 | } 325 | ``` 326 | 16.bug:进入用户详情页时,再进入另外一个用户的详情页时,因为用的是同一个组件,路由变化了,但是数据没有变化。 327 | 328 | 解决办法:通过`watch`,监听`route`变化,发生变化就请求数据并更新数据。并且将`UserDetail`设置为不保存其状态的组件,即`keep-alive`选项中添加`exclude="UserDetail"`(因为保存其在内存中的话,会出现路由`/user/undefined`,会出现一直显示正在加载页面的bug)。 329 | 330 | 具体代码如下: 331 | ```javascript 332 | export default { 333 | // 其他代码省略 334 | methods: { 335 | getUserData () { 336 | // 异步获取数据代码,此处省略 337 | } 338 | }, 339 | watch: { 340 | '$route': 'getUserData' 341 | }, 342 | created () { 343 | this.getUserData() 344 | } 345 | } 346 | ``` 347 | 348 | ## 安装 349 | 350 | ``` bash 351 | # install dependencies 352 | npm install 353 | 354 | # serve with hot reload at localhost:8080 355 | npm run dev 356 | 357 | # build for production with minification 358 | npm run build 359 | 360 | # build for production and view the bundle analyzer report 361 | npm run build --report 362 | ``` 363 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | plugins: [ 4 | '@babel/plugin-proposal-export-default-from', 5 | [ 6 | 'import', 7 | { 8 | libraryName: 'vant', 9 | libraryDirectory: 'es', 10 | style: true, 11 | }, 12 | 'vant', 13 | ], 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cnode", 3 | "version": "1.0.0-beta.0", 4 | "scripts": { 5 | "build": "vue-cli-service build", 6 | "lint": "vue-cli-service lint", 7 | "serve": "vue-cli-service serve" 8 | }, 9 | "dependencies": { 10 | "@jerryshen520/animate-scroll": "^2.0.0", 11 | "axios": "^0.18.0", 12 | "mavon-editor": "^2.1.10", 13 | "vant": "^1.3.1", 14 | "vue": "^2.5.17", 15 | "vue-axios": "^2.1.3", 16 | "vue-router": "^3.0.1", 17 | "vuex": "^3.0.1" 18 | }, 19 | "devDependencies": { 20 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 21 | "@vue/cli-plugin-babel": "^3.0.2", 22 | "@vue/cli-plugin-eslint": "^3.0.2", 23 | "@vue/cli-service": "^3.0.2", 24 | "@vue/eslint-config-airbnb": "^3.0.2", 25 | "babel-plugin-import": "^1.8.0", 26 | "node-sass": "^4.9.3", 27 | "sass-loader": "^7.1.0", 28 | "vue-template-compiler": "^2.5.17" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "@vue/airbnb" 38 | ], 39 | "rules": { 40 | "comma-dangle": [ 41 | "error", 42 | { 43 | "arrays": "always-multiline", 44 | "objects": "always-multiline", 45 | "imports": "always-multiline", 46 | "exports": "always-multiline", 47 | "functions": "ignore" 48 | } 49 | ] 50 | }, 51 | "parserOptions": { 52 | "parser": "babel-eslint" 53 | } 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions", 58 | "not ie <= 8" 59 | ], 60 | "author": { 61 | "name": "Jerry Shen", 62 | "email": "327538014@qq.com", 63 | "url": "https://juejin.im/user/57de657179bc440065e34999" 64 | }, 65 | "description": "A Vue.js project of CNode.", 66 | "engines": { 67 | "node": ">= 4.0.0", 68 | "npm": ">= 3.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CNode中文社区 6 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 27 | -------------------------------------------------------------------------------- /src/common/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperJerryshen/vue-cnode/129c645f730dcfb1d0dc709e35161e668456ce57/src/common/fonts/iconfont.eot -------------------------------------------------------------------------------- /src/common/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Fri Jul 21 11:27:17 2017 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 41 | 44 | 48 | 54 | 58 | 62 | 66 | 68 | 72 | 75 | 79 | 83 | 85 | 87 | 89 | 91 | 94 | 96 | 98 | 101 | 103 | 106 | 110 | 114 | 122 | 125 | 127 | 130 | 135 | 141 | 144 | 147 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/common/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperJerryshen/vue-cnode/129c645f730dcfb1d0dc709e35161e668456ce57/src/common/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/common/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperJerryshen/vue-cnode/129c645f730dcfb1d0dc709e35161e668456ce57/src/common/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/common/pictures/cnodejs_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 18 | 25 | 28 | 36 | 42 | 45 | 54 | 55 | -------------------------------------------------------------------------------- /src/common/style/animation.scss: -------------------------------------------------------------------------------- 1 | // zoom-in 2 | .fadeIn-enter-active, .fadeIn-leave-active { 3 | transition: opacity .5s ease; 4 | } 5 | .fadeIn-enter, .fadeIn-leave-to /* .fade-leave-active 在 <2.1.8 中 */ { 6 | opacity: 0; 7 | } 8 | // slide 9 | .slide-enter-active, .slide-leave-active { 10 | transition: top .5s ease, opacity .5s ease; 11 | } 12 | .slide-enter, .slide-leave-to { 13 | top: 0; 14 | opacity: 0; 15 | } 16 | // pulldown 17 | .pulldown-enter-active, .pulldown-leave-active { 18 | transition: transform .5s ease; 19 | transform-origin: top left; 20 | } 21 | .pulldown-enter, .pulldown-leave-to { 22 | transform: scaleY(0); 23 | } 24 | -------------------------------------------------------------------------------- /src/common/style/base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | outline: none; 6 | border: none; 7 | font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; 8 | } 9 | 10 | html, body { 11 | width: 100%; 12 | overflow-x: hidden; 13 | } 14 | 15 | button { 16 | cursor: pointer; 17 | } 18 | 19 | a { 20 | position: relative; 21 | text-decoration: none; 22 | color: #324057; 23 | cursor: pointer; 24 | } 25 | 26 | a::after { 27 | content: ""; 28 | position: absolute; 29 | left: 0; 30 | bottom: 0; 31 | width: 100%; 32 | height: 2px; 33 | background-color: #475669; 34 | transform: scaleX(0); 35 | transition: .3s ease-in-out; 36 | } 37 | a:hover { 38 | color: #20A0FF!important; 39 | } 40 | 41 | @keyframes loading { 42 | from { 43 | transform: rotate(0); 44 | } 45 | to { 46 | transform: rotate(360deg); 47 | } 48 | } 49 | 50 | .markdown-text { 51 | width: 100%; 52 | border-radius: 0 0 4px 4px; 53 | padding: 5px 8px; 54 | border-bottom: 1px solid rgba(0, 0, 0, .1); 55 | margin-bottom: 10px; 56 | background-color: white; 57 | word-wrap: break-word; 58 | li { 59 | list-style: none; 60 | } 61 | img { 62 | 63 | width: 95%; 64 | } 65 | li, p { 66 | text-indent: 1em; 67 | line-height: 28px; 68 | } 69 | h1, h2, h3, h4, h5, h6 { 70 | line-height: 48px; 71 | } 72 | h1 { 73 | font-size: 150%; 74 | } 75 | h2 { 76 | font-size: 140%; 77 | } 78 | h3 { 79 | font-size: 130%; 80 | } 81 | h4 { 82 | font-size: 120%; 83 | } 84 | h5 { 85 | font-size: 110%; 86 | } 87 | table { 88 | width: 100%; 89 | // padding: 0 3px; 90 | border-radius: 4px; 91 | margin-bottom: 6px; 92 | border-collapse:collapse; 93 | th { 94 | border: none; 95 | background-color: #009688; 96 | color: white; 97 | } 98 | tr:nth-of-type(odd) { 99 | background-color: #E0E0E0; 100 | border: none; 101 | } 102 | tr:nth-of-type(even) { 103 | background-color: #EFEBE9; 104 | border: none; 105 | } 106 | td { 107 | max-width: 110px; 108 | padding: 10px 4px; 109 | font-size: 80%; 110 | overflow: auto; 111 | text-align: center; 112 | } 113 | } 114 | code { 115 | overflow: auto; 116 | background: #f4f4f4; 117 | padding: 0 5px; 118 | border: 1px solid #eee; 119 | } 120 | pre { 121 | background: #f4f4f4; 122 | overflow: auto; 123 | padding: 5px 10px; 124 | } 125 | pre > code {line-height: 20px; 126 | border:none;} 127 | blockquote { 128 | border-left: 8px solid #D3DCE6; 129 | color: #475669; 130 | padding: 10px 10px 10px 20px; 131 | background: #EFF2F7; 132 | margin: 15px 0; 133 | p { 134 | font-size: 16px; 135 | line-height: 24px; 136 | margin-bottom: 0; 137 | } 138 | } 139 | } 140 | 141 | @media screen and (min-width: 760px) { 142 | .markdown-text { 143 | padding: 10px 50px; 144 | } 145 | } 146 | 147 | #star { 148 | position: absolute; 149 | top: 0; 150 | bottom: 0; 151 | left: 0; 152 | right: 0; 153 | } 154 | 155 | .slide-fade-enter-active { 156 | transition: all .6s ease; 157 | } 158 | .slide-fade-leave-active { 159 | transition: all .5s ease; 160 | } 161 | .slide-fade-enter, .slide-fade-leave-active { 162 | transform: translateX(60px); 163 | opacity: 0; 164 | } 165 | 166 | .slide-left-enter-active { 167 | transition: all .3s ease; 168 | } 169 | .slide-left-leave-active { 170 | transition: all .2s ease-out; 171 | } 172 | .slide-left-enter, .slide-left-leave-active { 173 | transform: translateX(-150px); 174 | opacity: 0; 175 | } 176 | 177 | .slide-top-enter-active { 178 | transition: all .5s ease; 179 | } 180 | .slide-top-leave-active { 181 | transition: all .2s ease-out; 182 | } 183 | .slide-top-enter, .slide-top-leave-active { 184 | transform: translateY(-5px); 185 | opacity: 0; 186 | } 187 | -------------------------------------------------------------------------------- /src/common/style/icon.scss: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('../fonts/iconfont.eot?t=1499837256785'); /* IE9*/ 3 | src: url('../fonts/iconfont.eot?t=1499837256785#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('../fonts/iconfont.woff?t=1499837256785') format('woff'), /* chrome, firefox */ 5 | url('../fonts/iconfont.ttf?t=1499837256785') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 6 | url('../fonts/iconfont.svg?t=1499837256785#iconfont') format('svg'); /* iOS 4.1- */ 7 | } 8 | 9 | .iconfont { 10 | font-family:"iconfont" !important; 11 | font-size:16px; 12 | font-style:normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-loading:before { content: "\e64f"; } 18 | 19 | .icon-fail:before { content: "\e658"; } 20 | 21 | .icon-user:before { content: "\e619"; } 22 | 23 | .icon-edit1:before { content: "\e686"; } 24 | 25 | .icon-about:before { content: "\e614"; } 26 | 27 | .icon-token:before { content: "\e6aa"; } 28 | 29 | .icon-pinglun:before { content: "\e605"; } 30 | 31 | .icon-houtui:before { content: "\e749"; } 32 | 33 | .icon-back-to-top:before { content: "\e631"; } 34 | 35 | .icon-home:before { content: "\e600"; } 36 | 37 | .icon-delete:before { content: "\e68c"; } 38 | 39 | .icon-notification:before { content: "\e7d8"; } 40 | 41 | .icon-fabu1:before { content: "\e62f"; } 42 | 43 | .icon-edit:before { content: "\e608"; } 44 | 45 | .icon-format_bold:before { content: "\ea55"; } 46 | 47 | .icon-format_italic:before { content: "\ea5c"; } 48 | 49 | .icon-format_list_bulleted:before { content: "\ea5e"; } 50 | 51 | .icon-format_list_numbered:before { content: "\ea5f"; } 52 | 53 | .icon-format_quote:before { content: "\ea60"; } 54 | 55 | .icon-insert_link:before { content: "\ea68"; } 56 | 57 | .icon-insert_photo:before { content: "\ea69"; } 58 | 59 | .icon-more:before { content: "\e7e3"; } 60 | 61 | .icon-github:before { content: "\e732"; } 62 | 63 | .icon-code:before { content: "\e62d"; } 64 | 65 | .icon-option:before { content: "\e6a7"; } 66 | 67 | .icon-praise:before { content: "\e71b"; } 68 | 69 | .icon-collected:before { content: "\e86d"; } 70 | 71 | .icon-collect:before { content: "\e86e"; } 72 | 73 | .icon-exit:before { content: "\e601"; } 74 | 75 | .icon-clickQuery:before { content: "\e604"; } 76 | 77 | .icon-reply:before { content: "\e66c"; } 78 | 79 | .icon-success:before { content: "\e6b3"; } 80 | 81 | .icon-warn:before { content: "\e6b4"; } 82 | -------------------------------------------------------------------------------- /src/common/utils/cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/15. 3 | * 用户本地cookie的存取以及清空 4 | * 函数的功能分别是: 5 | * 设置单个,获取所有,获取单个,删除所有,删除单个 6 | */ 7 | 8 | export function setCookie(name, value, exdays = 30) { 9 | const time = new Date(); 10 | const addTime = exdays * 24 * 3600 * 1000; 11 | time.setTime(time.getTime() + addTime); 12 | const expires = `expires=${time.toGMTString()}`; 13 | document.cookie = `${name}=${value};${expires}`; 14 | } 15 | 16 | export function getAllCookies() { 17 | if (document.cookie === '') { 18 | return {}; 19 | } 20 | const cookies = document.cookie.split(';'); 21 | const newCookies = {}; 22 | for (let i = 0; i < cookies.length; i += 1) { 23 | const cookie = cookies[i].trim(); 24 | const splitCookie = cookie.split('='); 25 | const [key, val] = splitCookie; 26 | newCookies[key] = val; 27 | } 28 | return newCookies; 29 | } 30 | 31 | export function getCookie(name) { 32 | const cname = `${name}=`; 33 | const cookies = document.cookie.split(';'); 34 | for (let i = 0; i < cookies.length; i += 1) { 35 | const cookie = cookies[i].trim(); 36 | if (cookie.indexOf(cname) === 0) { 37 | return { 38 | success: true, 39 | cookie: { 40 | name, 41 | value: cookie.split(cname)[1], 42 | }, 43 | }; 44 | } 45 | } 46 | return { 47 | success: false, 48 | cookie: { 49 | name, 50 | value: undefined, 51 | }, 52 | }; 53 | } 54 | 55 | export function deleteAllCookie() { 56 | document.cookie += ';expires=Thu, 01 Jan 1970 00:00:00 GMT'; 57 | } 58 | 59 | export function deleteCookie(name) { 60 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`; 61 | } 62 | -------------------------------------------------------------------------------- /src/common/utils/deepCopy.js: -------------------------------------------------------------------------------- 1 | // 用于深拷贝 2 | 3 | export function deepCopy(obj) { 4 | if (typeof obj !== 'object' || obj === null) { 5 | return obj; 6 | } 7 | let c; 8 | if (obj instanceof Array) { 9 | c = []; 10 | } else { 11 | c = {}; 12 | } 13 | Object.keys(obj).forEach((key) => { 14 | c[key] = deepCopy(obj[key]); 15 | }); 16 | return c; 17 | } 18 | 19 | export default { 20 | deepCopy, 21 | }; 22 | -------------------------------------------------------------------------------- /src/common/utils/timeFormat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/16. 3 | */ 4 | export default function timeFormat(date) { 5 | // 获取当前时间和所传时间的Date对象 6 | const nowTime = new Date(); 7 | const inDate = new Date(date); 8 | if (nowTime.getYear() - inDate.getYear() > 0) { 9 | // 年份差值 > 0,返回年 10 | return `${nowTime.getFullYear() - inDate.getFullYear()}年前`; 11 | } else if (nowTime.getMonth() - inDate.getMonth() > 0) { 12 | // 月份差值 > 0,返回月 13 | return `${nowTime.getMonth() - inDate.getMonth()}个月前`; 14 | } else if (nowTime.getDate() - inDate.getDate() > 0) { 15 | // 日期差值 > 0,返回日 16 | return `${nowTime.getDate() - inDate.getDate()}天前`; 17 | } else if (nowTime.getHours() - inDate.getHours() > 0) { 18 | // 小时差值 > 0,返回时 19 | return `${nowTime.getHours() - inDate.getHours()}个小时前`; 20 | } else if (nowTime.getMinutes() - inDate.getMinutes() > 0) { 21 | // 分钟差值 > 0,返回分钟 22 | return `${nowTime.getMinutes() - inDate.getMinutes()}分钟前`; 23 | } 24 | // 其他情况,也就是秒数差值 > 0,返回秒钟 25 | return `${nowTime.getSeconds() - inDate.getSeconds()}秒前`; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ArticleCard/ArticleCard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 70 | 71 | 148 | -------------------------------------------------------------------------------- /src/components/ArticleCard/index.js: -------------------------------------------------------------------------------- 1 | export default from './ArticleCard.vue'; 2 | -------------------------------------------------------------------------------- /src/components/BackBar/BackBar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 67 | -------------------------------------------------------------------------------- /src/components/BackBar/index.js: -------------------------------------------------------------------------------- 1 | export default from './BackBar.vue'; 2 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 49 | -------------------------------------------------------------------------------- /src/components/Loading/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/MessageCard/MessageCard.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 63 | 64 | 105 | -------------------------------------------------------------------------------- /src/components/MessageCard/index.js: -------------------------------------------------------------------------------- 1 | export default from './MessageCard.vue'; 2 | -------------------------------------------------------------------------------- /src/components/Page/Page.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /src/components/Page/index.js: -------------------------------------------------------------------------------- 1 | export default from './Page.vue'; 2 | -------------------------------------------------------------------------------- /src/components/TopBar/TopBar.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 120 | 121 | 225 | -------------------------------------------------------------------------------- /src/components/TopBar/index.js: -------------------------------------------------------------------------------- 1 | export default from './TopBar.vue'; 2 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import axios from 'axios'; 3 | import VueAxios from 'vue-axios'; 4 | import Vuex from 'vuex'; 5 | import { Toast } from 'vant'; 6 | 7 | import App from './App.vue'; 8 | import router from './router'; 9 | import store from './store/store'; 10 | import Page from './components/Page'; 11 | 12 | Vue.use(VueAxios, axios); 13 | Vue.use(Vuex); 14 | Vue.use(Toast); 15 | 16 | Vue.component('page', Page); 17 | 18 | Vue.config.productionTip = false; 19 | 20 | new Vue({ 21 | router, 22 | store, 23 | render: h => h(App), 24 | }).$mount('#app'); 25 | -------------------------------------------------------------------------------- /src/pic/CNode-requirement-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperJerryshen/vue-cnode/129c645f730dcfb1d0dc709e35161e668456ce57/src/pic/CNode-requirement-analysis.png -------------------------------------------------------------------------------- /src/pic/QR-Code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperJerryshen/vue-cnode/129c645f730dcfb1d0dc709e35161e668456ce57/src/pic/QR-Code.png -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | const HomePage = () => import('@/views/HomePage'); 5 | const UserLogin = () => import('@/views/UserLogin'); 6 | const UserDetail = () => import('@/views/UserDetail'); 7 | const UserNotification = () => import('@/views/UserNotification'); 8 | const MyCollection = () => import('@/views/MyCollection'); 9 | const ArticlePublish = () => import('@/views/ArticlePublish'); 10 | const ArticleDetail = () => import('@/views/ArticleDetail'); 11 | 12 | Vue.use(Router); 13 | 14 | // 输出七个组件的路由: 15 | // ① 主页 16 | // ② 文章详情页 17 | // ③ 用户详情页 18 | // ④ 用户登录页 19 | // ⑤ 发布文章页 20 | // ⑥ 用户收藏页 21 | // ⑦ 我的通知页 22 | 23 | export default new Router({ 24 | scrollBehavior: () => ({ y: 0 }), 25 | mode: 'history', 26 | routes: [ 27 | { 28 | path: '/', 29 | name: 'homePage', 30 | component: HomePage, 31 | }, 32 | { 33 | path: '/article/:id', 34 | name: 'article', 35 | component: ArticleDetail, 36 | }, 37 | { 38 | path: '/user/:loginname', 39 | name: 'user', 40 | component: UserDetail, 41 | }, 42 | { 43 | path: '/login', 44 | name: 'userLogin', 45 | component: UserLogin, 46 | }, 47 | { 48 | path: '/publish', 49 | name: 'publish', 50 | component: ArticlePublish, 51 | }, 52 | { 53 | path: '/collect/:loginname', 54 | name: 'collect', 55 | component: MyCollection, 56 | }, 57 | { 58 | path: '/notification', 59 | name: 'notification', 60 | component: UserNotification, 61 | }, 62 | ], 63 | }); 64 | -------------------------------------------------------------------------------- /src/store/modules/article/article-mutation-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/13. 3 | */ 4 | // 初始化文章的数据 5 | export const INIT_ARTICLE_DATA = 'INIT_ARTICLE_DATA'; 6 | // 收藏 7 | export const COLLECT = 'COLLECT'; 8 | // 取消收藏 9 | export const DE_COLLECT = 'DE_COLLECT'; 10 | // 回复AT某人 11 | export const REPLY_AT = 'REPLY_AT'; 12 | // 取消回复评论 13 | export const CANCEL_REPLY_AT = 'CANCEL_REPLY_AT'; 14 | // 添加评论 15 | export const ADD_REPLY = 'ADD_REPLY'; 16 | // 同步回复评论的数据 17 | export const SYNC_REPLY_DATA = 'SYNC_REPLY_DATA'; 18 | // 点赞,同步vuex中评论赞的boolean值 19 | export const SYNC_REPLY_UP = 'SYNC_REPLY_UP'; 20 | // 当输入框失去焦点时,改变isFocus的状态到false 21 | export const FOCUS_IS_FALSE = 'FOCUS_IS_FALSE'; 22 | // 改变评论的分类方式 23 | export const CHANGE_COMMENT_SORTING = 'CHANGE_COMMENT_SORTING'; 24 | -------------------------------------------------------------------------------- /src/store/modules/article/article.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/13. 3 | */ 4 | import * as types from './article-mutation-types'; 5 | 6 | const state = { 7 | articleData: { 8 | id: '', 9 | author_id: '', 10 | tab: 'all', 11 | content: '', 12 | title: '', 13 | last_reply_at: '1970-00-00T00:00:00.000Z', 14 | good: true, 15 | top: false, 16 | reply_count: 0, 17 | visit_count: 0, 18 | create_at: '1970-00-00T00:00:00.000Z', 19 | author: { 20 | loginname: '', 21 | avatar_url: '', 22 | }, 23 | replies: [], 24 | is_collect: false, 25 | }, 26 | replyData: '', 27 | replyAtId: '', 28 | addReplyAt: 0, 29 | isFocus: false, 30 | sortingMethod: 'default', 31 | }; 32 | 33 | const getters = { 34 | articleData: state => state.articleData, 35 | replyData: state => state.replyData, 36 | replyAtId: state => state.replyAtId, 37 | addReplyAt: state => state.addReplyAt, 38 | isFocus: state => state.isFocus, 39 | sortingMethod: state => state.sortingMethod, 40 | }; 41 | 42 | const mutations = { 43 | [types.INIT_ARTICLE_DATA](state, data) { 44 | state.articleData = data; 45 | }, 46 | [types.COLLECT](state) { 47 | state.articleData.is_collect = true; 48 | }, 49 | [types.DE_COLLECT](state) { 50 | state.articleData.is_collect = false; 51 | }, 52 | [types.REPLY_AT](state, replyUser) { 53 | state.replyData = `@${replyUser.name} `; 54 | state.replyAtId = replyUser.id; 55 | state.addReplyAt = replyUser.num; 56 | state.isFocus = true; 57 | }, 58 | [types.CANCEL_REPLY_AT](state) { 59 | state.replyAtId = ''; 60 | state.replyData = ''; 61 | state.addReplyAt = 0; 62 | }, 63 | [types.ADD_REPLY](state, reply) { 64 | state.articleData.replies.splice(reply.idx, 0, reply.data); 65 | }, 66 | [types.SYNC_REPLY_DATA](state, data) { 67 | state.replyData = data; 68 | }, 69 | [types.SYNC_REPLY_UP](state, data) { 70 | state.articleData.replies.forEach((item) => { 71 | if (item.id === data.id) { 72 | if (data.action === 'up') { 73 | item.is_uped = true; 74 | item.ups.push(data.uper); 75 | } else if (data.action === 'down') { 76 | item.is_uped = false; 77 | item.ups.forEach((up, idx, arr) => { 78 | if (up === data.uper) { 79 | arr.splice(idx, 1); 80 | } 81 | }); 82 | } 83 | } 84 | return true; 85 | }); 86 | }, 87 | [types.FOCUS_IS_FALSE](state) { 88 | state.isFocus = false; 89 | }, 90 | [types.CHANGE_COMMENT_SORTING](state, method) { 91 | state.sortingMethod = method; 92 | }, 93 | }; 94 | 95 | const actions = { 96 | initArticleData({ commit }, data) { 97 | commit(types.INIT_ARTICLE_DATA, data); 98 | }, 99 | collect({ commit }) { 100 | commit(types.COLLECT); 101 | }, 102 | deCollect({ commit }) { 103 | commit(types.DE_COLLECT); 104 | }, 105 | reply_at({ commit }, username) { 106 | commit(types.REPLY_AT, username); 107 | }, 108 | cancel_reply_at({ commit }) { 109 | commit(types.CANCEL_REPLY_AT); 110 | }, 111 | add_reply({ commit }, reply) { 112 | commit(types.ADD_REPLY, reply); 113 | }, 114 | sync_reply_data({ commit }, data) { 115 | commit(types.SYNC_REPLY_DATA, data); 116 | }, 117 | sync_reply_up({ commit }, data) { 118 | commit(types.SYNC_REPLY_UP, data); 119 | }, 120 | focus_is_false({ commit }) { 121 | commit(types.FOCUS_IS_FALSE); 122 | }, 123 | change_comment_sorting({ commit }, method) { 124 | commit(types.CHANGE_COMMENT_SORTING, method); 125 | }, 126 | }; 127 | 128 | export default { 129 | state, 130 | getters, 131 | mutations, 132 | actions, 133 | }; 134 | -------------------------------------------------------------------------------- /src/store/modules/content/content-mutation-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/12. 3 | */ 4 | // 更换tab内容 5 | export const CHANGE_TAB = 'CHANGE_TAB'; 6 | // 更新tab对应的数据 7 | export const CHANGE_TAB_DATA = 'CHANGE_TAB_DATA'; 8 | // 获得并加载更多的文章内容 9 | export const LOAD_MORE_DATA = 'LOAD_MORE_DATA'; 10 | // 使正在加载中的状态修改为true,表示正在加载数据 11 | export const CHANGE_LOAD_STATUS = 'CHANGE_LOADING_STATUS'; 12 | // 记录主页的滑动位置,用于返回时定位 13 | export const RECORD_SCROLL_TOP = 'RECORD_SCROLL_TOP'; 14 | // 回到顶部 15 | export const BACK_TO_TOP = 'BACK_TO_TOP'; 16 | // 异步请求懒加载数据 17 | export const ASYNC_REQUEST_DATA = 'ASYNC_REQUEST_DATA'; 18 | -------------------------------------------------------------------------------- /src/store/modules/content/content.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/12. 3 | */ 4 | import * as types from './content-mutation-types'; 5 | 6 | const state = { 7 | /* 8 | * articleLists: 展示的文章列表的数据 9 | * selectedTab: 选中显示的内容,默认为全部 10 | * pageCount: 表示当前已经载入内容的页数 11 | * isLoading: 表示是否正在获取数据,防止多次请求数据 12 | * isTopShow: 表示回到顶部按钮是否显示 13 | * */ 14 | articleLists: [], 15 | selectedTab: 'all', 16 | pageCount: 1, 17 | isLoading: false, 18 | isTopShow: false, 19 | homeScrollTop: 0, 20 | isRequesting: false, 21 | }; 22 | 23 | const getters = { 24 | articleLists: state => state.articleLists, 25 | selectedTab: state => state.selectedTab, 26 | pageCount: state => state.pageCount, 27 | isLoading: state => state.isLoading, 28 | isTopShow: state => state.isTopShow, 29 | homeScrollTop: state => state.homeScrollTop, 30 | isRequesting: state => state.isRequesting, 31 | }; 32 | 33 | const mutations = { 34 | [types.CHANGE_TAB](state, tab) { 35 | state.selectedTab = tab; 36 | state.pageCount = 1; 37 | }, 38 | [types.CHANGE_TAB_DATA](state, tabData) { 39 | state.articleLists = tabData; 40 | }, 41 | [types.LOAD_MORE_DATA](state, data) { 42 | data.forEach((item) => { 43 | state.articleLists.push(item); 44 | }); 45 | state.pageCount++; 46 | state.isLoading = false; 47 | }, 48 | [types.ASYNC_REQUEST_DATA](state, boolean) { 49 | state.isRequesting = boolean; 50 | }, 51 | [types.CHANGE_LOAD_STATUS](state, boolean) { 52 | state.isLoading = boolean; 53 | }, 54 | [types.BACK_TO_TOP](state, boolean) { 55 | state.isTopShow = boolean; 56 | }, 57 | [types.RECORD_SCROLL_TOP](state, count) { 58 | state.homeScrollTop = count; 59 | }, 60 | }; 61 | 62 | const actions = { 63 | changeTab({ commit }, tab) { 64 | commit(types.CHANGE_TAB, tab); 65 | }, 66 | changeTabData({ commit }, tabData) { 67 | commit(types.CHANGE_TAB_DATA, tabData); 68 | }, 69 | loadMoreData({ commit }, data) { 70 | commit(types.LOAD_MORE_DATA, data); 71 | }, 72 | async_request_data({ commit }, boolean) { 73 | commit(types.ASYNC_REQUEST_DATA, boolean); 74 | }, 75 | changeLoadingStatus({ commit }, boolean) { 76 | commit(types.CHANGE_LOAD_STATUS, boolean); 77 | }, 78 | backToTop({ commit }, boolean) { 79 | commit(types.BACK_TO_TOP, boolean); 80 | }, 81 | record_scroll_top({ commit }, count) { 82 | commit(types.RECORD_SCROLL_TOP, count); 83 | }, 84 | }; 85 | 86 | export default { 87 | state, 88 | getters, 89 | mutations, 90 | actions, 91 | }; 92 | -------------------------------------------------------------------------------- /src/store/modules/login/login-mutation-types.js: -------------------------------------------------------------------------------- 1 | // 登录验证成功时,初始化用户登录信息并保存至cookie中 2 | export const INIT_USER_DATA = 'INIT_USER_DATA'; 3 | // 退出登录 4 | export const LOGIN_OUT = 'LOGIN_OUT'; 5 | // 初始化我的收藏数据 6 | export const INIT_MY_COLLECTIONS = 'INIT_MY_COLLECTIONS'; 7 | -------------------------------------------------------------------------------- /src/store/modules/login/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/17. 3 | */ 4 | import * as types from './login-mutation-types'; 5 | import * as cookie from '../../../common/utils/cookie'; 6 | 7 | const allCookie = cookie.getAllCookies(); 8 | 9 | const state = { 10 | isLogin: false, 11 | userData: { 12 | avatar_url: '', 13 | id: '', 14 | loginname: '', 15 | accesstoken: '', 16 | }, 17 | myCollections: [], 18 | }; 19 | 20 | if (allCookie.isLogin === 'true') { 21 | state.isLogin = true; 22 | state.userData = { 23 | avatar_url: allCookie.avatar_url, 24 | id: allCookie.id, 25 | loginname: allCookie.loginname, 26 | accesstoken: allCookie.accesstoken, 27 | }; 28 | } 29 | 30 | const getters = { 31 | isLogin: state => state.isLogin, 32 | userData: state => state.userData, 33 | myCollections: state => state.myCollections, 34 | }; 35 | 36 | const mutations = { 37 | [types.INIT_USER_DATA](state, data) { 38 | // 首先修改登录状态(包括vuex和cookie) 39 | state.isLogin = true; 40 | cookie.setCookie('isLogin', true); 41 | // 再修改用户数据 42 | state.userData = data; 43 | Object.keys(data).forEach((i) => { 44 | cookie.setCookie(i, data[i]); 45 | }); 46 | }, 47 | [types.LOGIN_OUT](state) { 48 | state.isLogin = false; 49 | state.userData = { 50 | avatar_url: '', 51 | id: '', 52 | loginname: '', 53 | accesstoken: '', 54 | }; 55 | cookie.setCookie('isLogin', false); 56 | Object.keys(state.userData).forEach((i) => { 57 | cookie.deleteCookie(i); 58 | }); 59 | }, 60 | [types.INIT_MY_COLLECTIONS](state, data) { 61 | state.myCollections = data; 62 | }, 63 | }; 64 | 65 | const actions = { 66 | initUserData({ commit }, data) { 67 | commit(types.INIT_USER_DATA, data); 68 | }, 69 | loginOut({ commit }) { 70 | commit(types.LOGIN_OUT); 71 | }, 72 | init_my_collections({ commit }, data) { 73 | commit(types.INIT_MY_COLLECTIONS, data); 74 | }, 75 | }; 76 | 77 | export default { 78 | state, 79 | getters, 80 | mutations, 81 | actions, 82 | }; 83 | -------------------------------------------------------------------------------- /src/store/modules/messages/messages-mutation-types.js: -------------------------------------------------------------------------------- 1 | // 添加消息 2 | export const ADD_NEW_MESSAGE = 'ADD_NEW_MESSAGE'; 3 | // 删除消息 4 | export const DELETE_NEW_MESSAGE = 'DELETE_NEW_MESSAGE'; 5 | -------------------------------------------------------------------------------- /src/store/modules/messages/messages.js: -------------------------------------------------------------------------------- 1 | import * as types from './messages-mutation-types'; 2 | 3 | const state = { 4 | messages: [], 5 | }; 6 | 7 | const getters = { 8 | messages: state => state.messages, 9 | }; 10 | 11 | const mutations = { 12 | [types.ADD_NEW_MESSAGE](state, data) { 13 | state.messages.push({ 14 | type: data.type, 15 | content: data.content, 16 | }); 17 | }, 18 | [types.DELETE_NEW_MESSAGE](state) { 19 | state.messages.shift(); 20 | }, 21 | }; 22 | 23 | const actions = { 24 | add_warn({ commit }, data) { 25 | data.type = 'warn'; 26 | commit(types.ADD_NEW_MESSAGE, data); 27 | window.setTimeout(() => { 28 | commit(types.DELETE_NEW_MESSAGE); 29 | }, 1500); 30 | }, 31 | add_success({ commit }, data) { 32 | data.type = 'success'; 33 | commit(types.ADD_NEW_MESSAGE, data); 34 | window.setTimeout(() => { 35 | commit(types.DELETE_NEW_MESSAGE); 36 | }, 1500); 37 | }, 38 | add_fail({ commit }, data) { 39 | data.type = 'fail'; 40 | commit(types.ADD_NEW_MESSAGE, data); 41 | window.setTimeout(() => { 42 | commit(types.DELETE_NEW_MESSAGE); 43 | }, 1500); 44 | }, 45 | add_loading({ commit }) { 46 | commit(types.ADD_NEW_MESSAGE, { 47 | type: 'loading', 48 | content: '加载中...', 49 | }); 50 | }, 51 | delete_message({ commit }) { 52 | commit(types.DELETE_NEW_MESSAGE); 53 | }, 54 | connect_fail({ commit }) { 55 | commit(types.ADD_NEW_MESSAGE, { 56 | type: 'fail', 57 | content: '连接失败!', 58 | }); 59 | window.setTimeout(() => { 60 | commit(types.DELETE_NEW_MESSAGE); 61 | }, 1500); 62 | }, 63 | }; 64 | 65 | export default { 66 | state, 67 | getters, 68 | mutations, 69 | actions, 70 | }; 71 | -------------------------------------------------------------------------------- /src/store/modules/navbar/navbar-mutation-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/17. 3 | */ 4 | // 控制AboutMe组件的显示 5 | export const CHANGE_ABOUT_ME_SHOW = 'CHANGE_ABOUT_ME'; 6 | // 获取并记录未读消息数 7 | export const GET_MESSAGE_COUNT = 'GET_MESSAGE_COUNT'; 8 | -------------------------------------------------------------------------------- /src/store/modules/navbar/navbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/17. 3 | */ 4 | import * as types from './navbar-mutation-types'; 5 | 6 | const state = { 7 | isAboutMeShow: false, 8 | messageCount: 0, 9 | }; 10 | 11 | const getters = { 12 | isAboutMeShow: state => state.isAboutMeShow, 13 | messageCount: state => state.messageCount, 14 | }; 15 | 16 | const mutations = { 17 | [types.CHANGE_ABOUT_ME_SHOW](state) { 18 | state.isAboutMeShow = !state.isAboutMeShow; 19 | }, 20 | [types.GET_MESSAGE_COUNT](state, count) { 21 | state.messageCount = count; 22 | }, 23 | }; 24 | 25 | const actions = { 26 | changeAboutMeShow({ commit }) { 27 | commit(types.CHANGE_ABOUT_ME_SHOW); 28 | }, 29 | get_message_count({ commit }, count) { 30 | commit(types.GET_MESSAGE_COUNT, count); 31 | }, 32 | }; 33 | 34 | export default { 35 | state, 36 | getters, 37 | mutations, 38 | actions, 39 | }; 40 | -------------------------------------------------------------------------------- /src/store/modules/notification/notification-mutation-types.js: -------------------------------------------------------------------------------- 1 | // 获得提醒消息的数据 2 | export const GET_MESSAGES = 'GET_MESSAGES'; 3 | // 标记单个消息已读 4 | export const MARK_ONE = 'MARK_ONE'; 5 | // 标记所有消息已读 6 | export const MARK_ALL = 'MARK_ALL'; 7 | -------------------------------------------------------------------------------- /src/store/modules/notification/notification.js: -------------------------------------------------------------------------------- 1 | import * as types from './notification-mutation-types'; 2 | 3 | const state = { 4 | messageData: { 5 | has_read_messages: [], 6 | hasnot_read_messages: [], 7 | }, 8 | }; 9 | 10 | const getters = { 11 | messageData: state => state.messageData, 12 | }; 13 | 14 | const mutations = { 15 | [types.GET_MESSAGES](state, data) { 16 | state.messageData = data; 17 | }, 18 | [types.MARK_ONE](state, id) { 19 | // 移除已读并加入已读列表 20 | state.messageData.hasnot_read_messages.forEach((item, idx, arr) => { 21 | if (item.id === id) { 22 | arr.splice(idx, 1); 23 | state.messageData.has_read_messages.unshift(item); 24 | } 25 | }); 26 | }, 27 | [types.MARK_ALL](state) { 28 | // 合并未读消息和已读消息 29 | state.messageData.has_read_messages = state.messageData.hasnot_read_messages.concat(state.messageData.has_read_messages); 30 | state.messageData.hasnot_read_messages = []; 31 | }, 32 | }; 33 | 34 | const actions = { 35 | get_messages({ commit }, data) { 36 | commit(types.GET_MESSAGES, data); 37 | }, 38 | mark_one({ commit }, id) { 39 | commit(types.MARK_ONE, id); 40 | }, 41 | mark_all({ commit }) { 42 | commit(types.MARK_ALL); 43 | }, 44 | }; 45 | 46 | export default { 47 | state, 48 | getters, 49 | mutations, 50 | actions, 51 | }; 52 | -------------------------------------------------------------------------------- /src/store/modules/user/user-mutation-types.js: -------------------------------------------------------------------------------- 1 | // 初始化用户详细的数据 2 | export const INIT_USER_DETAIL_DATA = 'INIT_USER_DETAIL_DATA'; 3 | export default { 4 | INIT_USER_DETAIL_DATA, 5 | }; 6 | -------------------------------------------------------------------------------- /src/store/modules/user/user.js: -------------------------------------------------------------------------------- 1 | import * as types from './user-mutation-types'; 2 | 3 | const state = { 4 | userDetailData: { 5 | loginname: '', 6 | avatar_url: '', 7 | githubUsername: '', 8 | create_at: '', 9 | score: 0, 10 | recent_topics: [], 11 | recent_replies: [], 12 | }, 13 | }; 14 | 15 | const getters = { 16 | userDetailData: state => state.userDetailData, 17 | }; 18 | 19 | const mutations = { 20 | [types.INIT_USER_DETAIL_DATA](state, data) { 21 | state.userDetailData = data; 22 | }, 23 | }; 24 | 25 | const actions = { 26 | initUserDetailData({ commit }, data) { 27 | commit(types.INIT_USER_DETAIL_DATA, data); 28 | }, 29 | }; 30 | 31 | export default { 32 | state, 33 | getters, 34 | mutations, 35 | actions, 36 | }; 37 | -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jerryshen on 2017/7/12. 3 | */ 4 | import Vue from 'vue'; 5 | import Vuex from 'vuex'; 6 | import content from './modules/content/content'; 7 | import article from './modules/article/article'; 8 | import navbar from './modules/navbar/navbar'; 9 | import login from './modules/login/login'; 10 | import user from './modules/user/user'; 11 | import notification from './modules/notification/notification'; 12 | import messages from './modules/messages/messages'; 13 | 14 | Vue.use(Vuex); 15 | 16 | export default new Vuex.Store({ 17 | modules: { 18 | content, 19 | article, 20 | navbar, 21 | login, 22 | user, 23 | notification, 24 | messages, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/views/ArticleDetail/ArticleDetail.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 92 | 93 | 157 | -------------------------------------------------------------------------------- /src/views/ArticleDetail/BottomBar/BottomBar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 165 | 166 | 203 | -------------------------------------------------------------------------------- /src/views/ArticleDetail/CommentSorter.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /src/views/ArticleDetail/Comments.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 98 | 99 | 165 | -------------------------------------------------------------------------------- /src/views/ArticleDetail/index.js: -------------------------------------------------------------------------------- 1 | export default from './ArticleDetail.vue'; 2 | -------------------------------------------------------------------------------- /src/views/ArticlePublish/ArticlePublish.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 167 | 168 | 229 | -------------------------------------------------------------------------------- /src/views/ArticlePublish/index.js: -------------------------------------------------------------------------------- 1 | export default from './ArticlePublish.vue'; 2 | -------------------------------------------------------------------------------- /src/views/HomePage/HomePage.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 190 | 191 | 259 | -------------------------------------------------------------------------------- /src/views/HomePage/index.js: -------------------------------------------------------------------------------- 1 | export default from './HomePage.vue'; 2 | -------------------------------------------------------------------------------- /src/views/MyCollection/MyCollection.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 43 | 44 | 51 | -------------------------------------------------------------------------------- /src/views/MyCollection/index.js: -------------------------------------------------------------------------------- 1 | export default from './MyCollection.vue'; 2 | -------------------------------------------------------------------------------- /src/views/UserDetail/UserDetail.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 90 | 91 | 141 | -------------------------------------------------------------------------------- /src/views/UserDetail/index.js: -------------------------------------------------------------------------------- 1 | export default from './UserDetail.vue'; 2 | -------------------------------------------------------------------------------- /src/views/UserLogin/UserLogin.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 63 | 64 | 133 | -------------------------------------------------------------------------------- /src/views/UserLogin/index.js: -------------------------------------------------------------------------------- 1 | export default from './UserLogin.vue'; 2 | -------------------------------------------------------------------------------- /src/views/UserNotification/UserNotification.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 85 | 86 | 127 | -------------------------------------------------------------------------------- /src/views/UserNotification/index.js: -------------------------------------------------------------------------------- 1 | export default from './UserNotification.vue'; 2 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | --------------------------------------------------------------------------------