需要实现的主要功能如下:
资讯列表、标签页切换,文章举报,频道管理、文章详情、阅读记忆,关注功能、点赞功能、评论功能、回复评论、搜索功能、登录功能、个人中心、编辑资料、小智同学 ...
今天要实现的功能主要是:文章详情,关注功能,点赞功能,评论功能
1 文章详情功能
1.1 创建views/article/article.vue 并写入以下内容
<template>
<div class="article-container">
<!-- 导航栏 -->
<van-nav-bar
fixed
left-arrow
@click-left="$router.back()"
title="文章详情"
></van-nav-bar>
<!-- /导航栏 -->
<!-- 加载中 loading -->
<van-loading class="article-loading" />
<!-- /加载中 loading -->
<!-- 文章详情 -->
<div class="detail">
<h3 class="title">标题</h3>
<div class="author">
<van-image round width="1rem" height="1rem" fit="fill" />
<div class="text">
<p class="name">作者</p>
<p class="time">4天前</p>
</div>
<van-button
round
size="small"
type="info"
>+ 关注</van-button>
</div>
<div class="content">
<div>正文</div>
</div>
<van-divider>END</van-divider>
<div class="zan">
<van-button round size="small" hairline type="primary" plain icon="good-job-o">点赞</van-button>
<van-button round size="small" hairline type="danger" plain icon="delete">不喜欢</van-button>
</div>
</div>
<!-- /文章详情 -->
</div>
</template>
<script>
export default {
name: 'ArticleIndex',
data () {
return {
loading: true, // 控制加载中的 loading 状态
article: { }
}
}
}
</script>
<style scoped lang='less'>
.article-container{
position: absolute;
left: 0;
top: 0;
overflow-y: scroll;
width: 100%;
height: 100%;
}
.article-loading {
padding-top: 100px;
text-align: center;
}
.error{
padding-top: 100px;
text-align: center;
}
.detail {
padding: 50px 10px;
.title {
font-size: 16px;
}
.zan{
text-align: center;
}
.author {
padding: 10px 0;
display: flex;
.text {
flex: 1;
padding-left: 10px;
line-height: 1.3;
.name {
font-size: 14px;
margin: 0;
}
.time {
margin: 0;
font-size: 12px;
color: #999;
}
}
}
.content {
font-size:14px;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
/deep/ img{
max-width:100%;
background: #f9f9f9;
}
}
}
</style>
1.2路由配置
{
path: '/article/:id', // 动态路由
name: 'article',
component: () => import('../views/article/article.vue')
},
1.3 测试效果
1.4 文章详情-路由跳转
views/home/articleList.vue中,点击文章列表项的时候,传递文章id跳转到文章详情页
<van-cell
v-for="(item,idx) in list"
:key="idx"
:title="item.title"
+ @click="$router.push('/article/' + item.art_id)"
>
1.5 文章详情-获取数据并显示
1.5.1 封装接口
在 api/article.js 中新增一个方法
/**
* 获取文章详情
* @param {*} articleId
*/
export const getDetail = articleId => {
return request({
method: 'GET',
url: 'v1_0/articles/' + articleId
})
}
1.5.2 调用接口
在views/article/article.vue组件中调用接口,获取文章详情
created () {
this.loadDetail()
},
methods: {
async loadDetail () {
try {
this.loading = true
const res = await getDetail(this.$route.params.id)
this.article = res.data.data
this.loading = false
} catch (err) {
this.loading = false
console.log(err)
}
}
}
1.5.3 更改模板,渲染页面
<!-- 导航栏 -->
<van-nav-bar
fixed
left-arrow
@click-left="$router.back()"
:title="'文章详情-'+ article.title"
></van-nav-bar>
<!-- /导航栏 -->
<!-- 加载中 loading -->
<van-loading v-if="loading" class="article-loading" />
<!-- /加载中 loading -->
<!-- 文章详情 -->
<div class="detail">
<h3 class="title">{{article.title}}</h3>
<div class="author">
<van-image round width="1rem" height="1rem" fit="fill" />
<div class="text">
<p class="name">{{article.aut_name}}</p>
<p class="time">{{article.pubdate | relativeTime}}</p>
</div>
<van-button
round
size="small"
type="info"
>+ 关注</van-button>
</div>
<div class="content">
<div v-html="article.content"></div>
</div>
<van-divider>END</van-divider>
<div class="zan">
<van-button round size="small" hairline type="primary" plain icon="good-job-o">点赞</van-button>
</div>
</div>
<!-- /文章详情 -->
注意:
- 文章正文是html格式字符串,需要用v-html才能正确显示
- 相对时间处理:直接使用我们在前面定义的全局过滤器即可。
1.5.4 查看效果
2 文章操作-关注&取关
- 如果is_followed为 false 表示当前登陆用户并没有关注过本文章的作者。
- 如果is_followed为true 表示当前登陆用户已经关注过本文章的作者
2.1封装接口
在 api/user.js 中新增两个方法:
// 关注
export const follow = userId => {
return request({
url: '/v1_0/user/followings', // 接口地址
method: 'POST', // 方式
data: {
target: userId
}
})
}
// 取关
export const unfollow = userId => {
return request({
url: '/v1_0/user/followings/' + userId, // 接口地址
method: 'DELETE' // 方式
})
}
2.2调用接口
在\views\article\article.vue模板中
<van-button
round
size="small"
type="info"
+ @click="toggleFollow"
+ >{{article.is_followed ? '取关' : '+ 关注'}}</van-button>
async toggleFollow () {
try {
// 检查当前的状态
const isFollowed = this.article.is_followed
const userId = this.article.aut_id
console.log(isFollowed)
if (isFollowed) {
// 取关
await unfollow(userId)
} else {
await follow(userId)
}
// 更新视图
// 1. 整体重发请求?(没有必要)
// 2. 直接修改本地数据is_followed
this.article.is_followed = !isFollowed
this.$toast.success('操作成功')
} catch (err) {
console.log(err)
this.$toast.fail('操作失败')
}
}
2.3 效果图
3 文章操作-点赞&取消点赞
从后端取回来的文章详情中有一个attitude属性用来描述用户对文章的态度,具体是:
{-1:无态度,0:不喜欢,1:点赞}。如果是做点赞,就是把attitude改成1 。取消点赞,变成无态度,就是把 attiude改成 -1。
对应的,视图上有两个地方要修改:
- 文案
- 图标
3.1 封装接口
在 api/article.js 中封装数据接口
/**
* 取消点赞
* @param {*} id 文章编号
*/
export const deleteLike = id => {
return request({
method: 'DELETE',
url: 'v1_0/article/likings/' + id
})
}
/**
* 添加点赞
* @param {*} id 文章编号
*/
export const addLike = id => {
return request({
method: 'POST',
url: 'v1_0/article/likings',
data: {
target: id
}
})
}
3.2 调用接口
然后在 views/article/article.vue 组件中
<template>
<van-button round size="small"
hairline
type="primary"
plain
:icon="article.attitude === 1 ? 'good-job': 'good-job-o'"
@click="toggleLike">{{ article.attitude == 1 ? '取消点赞' : '点赞' }} </van-button>
</template>
<script>
import { addLike, deleteLike } from '@/api/article'
export default {
// ...
methods: {
async toggleLike () {
try {
// 检查当前的状态
const attitude = this.article.attitude
const artId = this.article.art_id
if (attitude === 1) {
await deleteLike(artId)
this.article.attitude = -1
} else if (attitude === -1) {
await addLike(artId)
this.article.attitude = 1
}
this.$toast.success('操作成功')
} catch (err) {
console.log(err)
this.$toast.fail('操作失败')
}
}
}
</script>
3.3 效果图
4 处理404
4.1在views/article/article.vue 组件中
return {
+ is404: false, // 文章是否存在
loading: true, // 控制加载中的 loading 状态
article: { } // 当前文章
}
async loadDetail () {
try {
this.is404 = false
this.loading = true
const {data:{data}} = await getDetail(this.$route.params.id)
this.article = data
this.loading = false
} catch (err) {
this.loading = false
console.dir(err)
// 如何去判断本次请求是404?
if (err.response.status === 404) {
this.is404 = true
}
}
}
<!-- 加载中 loading -->
<van-loading v-if="loading" class="article-loading" />
<!-- 加载中 loading -->
<div class="error" v-if="is404">
<p>文章被外星人吃掉了</p>
<van-button @click="$router.back()">后退</van-button>
<van-button @click="$router.push('/')">回主页</van-button>
</div>
<!-- 文章详情 -->
<div class="detail" v-else>
// 省略 -----
</div>
效果图
5 文章评论
5.1 基本布局
article/comment.vue 添加一个组件来完成评论列表功能
<template>
<div class="article-comments">
<!-- 评论列表 -->
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
v-for="item in list"
:key="item.com_id"
>
<van-image
slot="icon"
round
width="30"
height="30"
style="margin-right: 10px;"
:src="item.aut_photo"
/>
<span style="color: #466b9d;" slot="title">{{item.aut_name}}</span>
<div slot="label">
<p style="color: #363636;">{{item.content}}</p>
<p>
<span style="margin-right: 10px;">{{item.pubdate | relativeTime }}</span>
</p>
</div>
<van-icon slot="right-icon" name="like-o" />
</van-cell>
</van-list>
<!-- 评论列表 -->
<!-- 发布评论 -->
<div :class="commentShow ? 'art-cmt-container-1' : 'art-cmt-container-2'">
<!-- 底部添加评论区域 - 1 -->
<div class="add-cmt-box van-hairline--top">
<van-icon name="arrow-left" size="24px" @click="$router.back()" />
<div class="ipt-cmt-div">发表评论</div>
<div class="icon-box">
<van-badge content="10" max="99">
<van-icon
name="comment-o"
size="24px"
/>
</van-badge>
<van-icon name="star-o" size="24px" />
<van-icon name="share-o" size="24px" />
</div>
</div>
<!-- 底部添加评论区域 - 2 -->
<div class="cmt-box van-hairline--top" v-show="!commentShow">
<textarea
placeholder="友善评论、理性发言、阳光心灵"
v-model.trim="commentText"
></textarea>
<van-button
type="default"
>发布</van-button>
</div>
</div>
<!-- /发布评论 -->
</div>
</template>
<script>
export default {
name: 'ArticleComment',
data () {
return {
commentText: '',
commentShow: true,
list: [], // 评论列表
loading: false, // 上拉加载更多的 loading
finished: false // 是否加载结束
}
},
methods: {
onLoad () {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
this.list.push(this.list.length + 1)
}
// 加载状态结束
this.loading = false
// 数据全部加载完成
if (this.list.length >= 40) {
this.finished = true
}
}, 1000)
}
}
}
</script>
<style scoped lang='less'>
// 发表评论的区域是固定在下端的
.publish-wrap {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
}
// 给发表评论区空出地方
.van-list {
margin-bottom: 45px;
}
/*美化 - 文章详情 - 底部的发布评论-样式 */
// 外层容器
.art-cmt-container-1 {
padding-bottom: 46px;
}
.art-cmt-container-2 {
padding-bottom: 80px;
}
// 发布评论的盒子 - 1
.add-cmt-box {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
background-color: white;
display: flex;
justify-content: space-between;
align-items: center;
height: 46px;
line-height: 46px;
padding-left: 10px;
.ipt-cmt-div {
flex: 1;
border: 1px solid #efefef;
border-radius: 15px;
height: 30px;
font-size: 12px;
line-height: 30px;
padding-left: 15px;
margin-left: 10px;
background-color: #f8f8f8;
}
.icon-box {
width: 40%;
display: flex;
justify-content: space-evenly;
line-height: 0;
}
}
.child {
width: 20px;
height: 20px;
background: #f2f3f5;
}
// 发布评论的盒子 - 2
.cmt-box {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 80px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
padding-left: 10px;
box-sizing: border-box;
background-color: white;
textarea {
flex: 1;
height: 66%;
border: 1px solid #efefef;
background-color: #f8f8f8;
resize: none;
border-radius: 6px;
padding: 5px;
}
.van-button {
height: 100%;
border: none;
}
}
</style>
5.2 注册并引入
在文章详情页面src\views\article\article.vue中加载注册文章评论子组件:
import ArticleComment from './comment'
export default {
...
components: {
ArticleComment
}
}
<div class="article-container">
...
<!-- 文章评论 -->
<article-comment></article-comment>
<!-- 文章评论 -->
</div>
效果图
5.3获取文章评论数据并显示
在 api/comment.js 中封装请求方法
/**
* 获取评论
* @param {*} articleId
* @param {*} offset
*/
export const getComment = (articleId, offset) => {
return request({
method: 'GET',
url: '/v1_0/comments',
params: {
type: 'a',
source: articleId,
offset
}
})
}
5.4 加载获取数据
在 views/article/comment.vue 组件中加载获取数据
// 数据项
data () {
return {
+ total_count: 10,
+ offset: null, // 获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据
}
}
import { getComments } from '@/api/comment'
async onLoad () {
try {
// 1. 发请求
const {data:{data}} = await getComments(this.$route.params.id, this.offset)
const arr = data.results
// 2. 追加到list
this.list.push(...arr)
// 3. loading <-false
this.loading = false
// 4. 判断是否加载完成
this.finished = !arr.length
// 5. 更新offset
this.offset = data.last_id
// 6. 总数
this.total_count = data.total_count
} catch (error) {
this.$toast('获取评论失败')
this.loading = false
}
}
模板绑定
<van-cell
v-for="item in list"
:key="item.com_id"
>
<van-image
slot="icon"
round
width="30"
height="30"
style="margin-right: 10px;"
:src="item.aut_photo"
/>
<span style="color: #466b9d;" slot="title">{{item.aut_name}}</span>
<div slot="label">
<p style="color: #363636;">{{item.content}}</p>
<p>
<span style="margin-right: 10px;">{{item.pubdate | relativeTime }}</span>
<van-button size="mini" type="default">回复</van-button>
</p>
</div>
<van-icon slot="right-icon" name="like-o" />
</van-cell>
效果图
6 发布文章评论-基本交互
用boolean来控制两个状态的切换
(1)点击发表评论:从状态1---> 状态2
(2)输入框失去焦点时,状态2 ----> 状态1
6.1 视图
<!-- 底部添加评论区域 - 1 -->
<div class="add-cmt-box van-hairline--top" v-show="commentShow">
<!-- 底部添加评论区域 - 2 -->
<div class="cmt-box van-hairline--top" v-show="!commentShow">
6.2 添加事件及属性
<!-- 发布评论 -->
<div :class="commentShow ? 'art-cmt-container-1' : 'art-cmt-container-2'">
<!-- 底部添加评论区域 - 1 -->
+ <div class="add-cmt-box van-hairline--top" v-show="commentShow">
<van-icon name="arrow-left" size="24px" @click="$router.back()" />
+ <div class="ipt-cmt-div" @click="hShowCommentArea">发表评论</div>
<div class="icon-box">
<van-badge content="10" max="99">
<van-icon name="comment-o" size="24px" />
</van-badge>
<van-icon name="star-o" size="24px" />
<van-icon name="share-o" size="24px" />
</div>
</div>
<!-- 底部添加评论区域 - 2 -->
<div class="cmt-box van-hairline--top" v-show="!commentShow">
<textarea
+ ref="txt"
+ @blur="hBlur"
placeholder="友善评论、理性发言、阳光心灵"
v-model.trim="commentText"
></textarea>
+ <van-button type="default" @click="hAddComment">发布</van-button>
</div>
</div>
<!-- /发布评论 -->
6.3 执行代码
// 用户点击添加
hAddComment () {
alert('hAddComment')
},
// 输入框失去焦点
hBlur () {
// 回到状态1
// this.$nextTick(() => {
// this.commentShow = true
// })
setTimeout(() => {
this.commentShow = true
})
},
// 用户点击了发表评论
hShowCommentArea () {
// 状态2会显示出来
this.commentShow = false // 通知视图 等会 去更新
// $nextTick(回调函数) // 更新视图之后,去执行回调函数
this.$nextTick(() => {
// 让输入框获取焦点
this.$refs.txt.focus()
})
}
6.4 发布文章评论
在 api/comment.js 中添加封装数据接口
import request from '@/utils/request.js'
/**
* 添加文章评论
* @param {*} articleId
* @param {*} content
*/
export const addComment = (articleId, content) => {
return request({
method: 'POST',
url: 'v1_0/comments',
data: {
target: articleId,
content
}
})
}
在 comment.vue 组件中,hAddComment
// 用户点击添加
async hAddComment () {
if (!this.commentText) {
return
}
try {
const {data:{data}} = await addComment(this.$route.params.id, this.commentText)
this.$toast.success('添加评论成功')
// data.new_obj // 当前评论数据
this.list.unshift(data.new_obj)
// 更新页面
} catch (err) {
console.log(err)
this.$toast.fail('添加评论失败')
}
},
6.5 实现发布评论===>滚动条滚动到发布评论位置
在comment.vue最上层加入一个id="scrollTh"的div
<div id="scrollTh"></div>
// 用户点击添加
async hAddComment () {
if (!this.commentText) {
return
}
try {
const res = await addComment(this.$route.params.id, this.commentText)
console.log(res)
this.$toast.success('添加评论成功')
this.list.unshift(res.data.data.new_obj)
// 滚动条滚动到评论发布的位置
+ this.$el.querySelector('#scrollTh').scrollIntoView({
// top: 0,
// el: '#scrollTh',
+ behavior: 'smooth', // 平滑过渡
+ block: 'start' // 上边框与视窗顶部平齐。默认值
+ })
// 更新页面
} catch (err) {
console.log(err)
this.$toast.fail('添加评论失败')
}
},
7 文章评论-点赞
在后端接口取出回来的数据中,有一个特殊的字段
is_liking表示当前用户对当前评论是否喜欢。
- 从接口中取出数据之后,根据is_liking这个字段来来更新视图
- 用户点击之后,调用接口去修改is_liking这个字段在服务器上的值,并更新视图。
7.1 封装接口
在 api/comment.js 中添加两个方法
/**
* 对文章评论进行点赞
* @param {*} commentId 评论id
*/
export const addCommentLike = commentId => {
return request({
method: 'POST',
url: 'v1_0/comment/likings',
data: {
target: commentId
}
})
}
/**
* 取消文章评论的点赞
* @param {*} commentId 评论id
*/
export const deleteCommentLike = commentId => {
return request({
method: 'DELETE',
url: 'v1_0/comment/likings/' + commentId
})
}
7.2 修改视图
在 views/article/comment.vue 组件中:
<van-icon
slot="right-icon"
:name="item.is_liking ? 'like' : 'like-o'"
@click="hToggleLike(item)"
/>
7.3 具体实现代码
<script>
import {
getComments,
addComment,
+ addCommentLike,
+ deleteCommentLike
} from '@/api/comment'
export default {
methods: {
// 用户喜欢/取消喜欢评论
async hToggleLike (item) {
try {
const isLiking = item.is_liking
const commentId = item.com_id
if (isLiking) {
await deleteCommentLike(commentId)
} else {
await addCommentLike(commentId)
}
this.$toast.success('操作成功')
// 更新本地数据
item.is_liking = !isLiking
} catch (err) {
console.log(err)
this.$toast.fail('操作失败')
}
}
}
}
</script>
效果图