├── .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 | [](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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
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 |
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 |
2 |
3 |
4 | 置顶
6 | ·
7 | 精华
9 | ·
10 | {{ this.tabTypes[article.tab] }} ·
11 |
12 |
{{ article.last_reply_at | timeFormat}}
13 |
14 | {{ article.title }}
17 |
18 |
20 |
![article.author.loginname]()
23 |
{{ article.author.loginname }}
24 |
25 |
27 |
28 | {{ article.visit_count }}
29 |
30 | {{ article.reply_count }}
31 |
32 |
33 |
34 |
35 |
70 |
71 |
148 |
--------------------------------------------------------------------------------
/src/components/ArticleCard/index.js:
--------------------------------------------------------------------------------
1 | export default from './ArticleCard.vue';
2 |
--------------------------------------------------------------------------------
/src/components/BackBar/BackBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ title }}
5 |
6 |
7 |
8 |
9 |
28 |
29 |
67 |
--------------------------------------------------------------------------------
/src/components/BackBar/index.js:
--------------------------------------------------------------------------------
1 | export default from './BackBar.vue';
2 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
玩命加载中
6 |
7 |
8 |
9 |
10 |
22 |
23 |
49 |
--------------------------------------------------------------------------------
/src/components/Loading/loading.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/MessageCard/MessageCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
{{ data.author.loginname }}
7 | 回复了你的文章
8 |
{{ data.topic.title }}
11 |
回复于{{ data.reply.create_at | timeFormat }}
12 |
标记已读
15 |
20 |
21 |
22 |
23 |
63 |
64 |
105 |
--------------------------------------------------------------------------------
/src/components/MessageCard/index.js:
--------------------------------------------------------------------------------
1 | export default from './MessageCard.vue';
2 |
--------------------------------------------------------------------------------
/src/components/Page/Page.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
59 |
--------------------------------------------------------------------------------
/src/components/Page/index.js:
--------------------------------------------------------------------------------
1 | export default from './Page.vue';
2 |
--------------------------------------------------------------------------------
/src/components/TopBar/TopBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

9 |
11 |
12 |
{{ messageCount }}
14 |
15 |
17 |
19 |
![user-avatar]()
23 |
24 |
25 |
46 |
47 |
48 |
49 |
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 |
2 |
37 |
38 |
39 |
92 |
93 |
157 |
--------------------------------------------------------------------------------
/src/views/ArticleDetail/BottomBar/BottomBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | 编辑
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
165 |
166 |
203 |
--------------------------------------------------------------------------------
/src/views/ArticleDetail/CommentSorter.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
31 |
32 |
50 |
--------------------------------------------------------------------------------
/src/views/ArticleDetail/Comments.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ comments.length }} 回复
4 |
5 |
30 |
31 |
32 |
33 |
98 |
99 |
165 |
--------------------------------------------------------------------------------
/src/views/ArticleDetail/index.js:
--------------------------------------------------------------------------------
1 | export default from './ArticleDetail.vue';
2 |
--------------------------------------------------------------------------------
/src/views/ArticlePublish/ArticlePublish.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
文章标题
8 |
13 |
14 |
15 |
文章类型
16 |
24 |
25 |
26 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
167 |
168 |
229 |
--------------------------------------------------------------------------------
/src/views/ArticlePublish/index.js:
--------------------------------------------------------------------------------
1 | export default from './ArticlePublish.vue';
2 |
--------------------------------------------------------------------------------
/src/views/HomePage/HomePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
全部
10 |
精华
13 |
分享
16 |
问答
19 |
招聘
22 |
客户端测试
25 |
26 |
27 |
42 |
43 |
44 |
45 |
190 |
191 |
259 |
--------------------------------------------------------------------------------
/src/views/HomePage/index.js:
--------------------------------------------------------------------------------
1 | export default from './HomePage.vue';
2 |
--------------------------------------------------------------------------------
/src/views/MyCollection/MyCollection.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
43 |
44 |
51 |
--------------------------------------------------------------------------------
/src/views/MyCollection/index.js:
--------------------------------------------------------------------------------
1 | export default from './MyCollection.vue';
2 |
--------------------------------------------------------------------------------
/src/views/UserDetail/UserDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
个人信息
7 |
![avatar]()
10 |
{{ userDetailData.loginname }}
11 |
{{ userDetailData.score }} 积分
12 |
17 |
注册时间{{ userDetailData.create_at | timeFormat }}
18 |
19 |
20 |
最近创建的话题
21 |
29 |
无话题
31 |
32 |
33 |
最近参与的话题
34 |
42 |
无话题
44 |
45 |
46 |
47 |
48 |
49 |
90 |
91 |
141 |
--------------------------------------------------------------------------------
/src/views/UserDetail/index.js:
--------------------------------------------------------------------------------
1 | export default from './UserDetail.vue';
2 |
--------------------------------------------------------------------------------
/src/views/UserLogin/UserLogin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |

10 |
请到CNode中文官网,将设置选项中的Access Token的字符串拷贝至输入框中,并点击登录,完成用户验证。(30天内免登陆)
11 |
12 |
13 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
63 |
64 |
133 |
--------------------------------------------------------------------------------
/src/views/UserLogin/index.js:
--------------------------------------------------------------------------------
1 | export default from './UserLogin.vue';
2 |
--------------------------------------------------------------------------------
/src/views/UserNotification/UserNotification.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
未读消息
6 |
标记所有已读
9 |
18 |
暂无消息
20 |
21 |
22 |
已读消息
23 |
31 |
暂无消息
33 |
34 |
35 |
36 |
37 |
85 |
86 |
127 |
--------------------------------------------------------------------------------
/src/views/UserNotification/index.js:
--------------------------------------------------------------------------------
1 | export default from './UserNotification.vue';
2 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------