# 文章详情模块
# 回顾
- 编辑频道
- 删除频道
- 添加频道(处理请求参数)
- 下拉刷新的bug修复
- 搜索
- 基本布局(搜索框;搜索历史;联想列表)
- 添加搜索历史
- 删除搜索历史(删除单个的;删除所有的)
- 关键字搜索(处理防抖)
- 关键字搜索结果的高亮控制
- 正则的另一种用法 RegExp
- 跳转到结果页面
- 处理搜索结果的基本布局
- 实现搜索结果的分页加载
- page 页码
- per_page 每夜的条数
# 配置文章详情路由
目标:配置文章详情组件路由
- 创建路由组件
- 配置路由映射
- 配置跳转到详情页面的链接(主页列表项;搜索结果页列表项)
- 路由配置
import Detail from '@/views/article/index.vue'
{ path: '/detail/:id', component: Detail, props: true }
- 控制路由跳转(van-cell组件本身就支持路由跳转,类似于router-link标签的to属性)
<!-- 方法一 -->
<van-cell :to="'/detail/' + item.art_id.toString()" v-for="item in list" :key="item.art_id.toString()">
<!-- 方法二 -->
<div class="article_item" @click='$router.push("/detail/" + item.art_id.toString())'>
总结:跳转路由方式
- 基于van-cell的组件的to属性进行跳转
- 基于编程式导航进行跳转
- 基于router-link标签进行跳转
# 获取文章详情数据并渲染
目标:获取文章详情数据
- 调用接口获取文章详情数据
- 准备详情页面的基本布局
- 把数据填充到模板中
- 封装接口调用方法
export const getArticleDetail = (articleId) => {
return request({
method: 'get',
url: 'app/v1_0/articles/' + articleId
})
}
- 调用接口获取文章详情数据
methods: {
// 获取文章详情数据
async getArticleDetail () {
try {
const ret = await getArticleDetail(this.id)
this.article = ret.data
} catch {
this.$toast.fail('获取文章详情数据失败')
}
}
},
created () {
this.getArticleDetail()
}
- 渲染文章内容
<div class='container' ref="container">
<van-nav-bar fixed title="文章详情" left-arrow @click-left="$router.back()" />
<div class="detail" v-if="article">
<h3 class="title">{{article.title}}</h3>
<div class="author">
<van-image round width="1rem" height="1rem" fit="fill" :src="article.aut_photo" />
<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">
{{article.is_followed?'已关注':'+ 关注'}}
</van-button>
</div>
<div class="content" v-html="article.content"></div>
<div class="zan">
<van-button round size="small" :class="{active:article.attitude===1}" plain icon="like-o">点赞</van-button>
<van-button round size="small" :class="{active:article.attitude===0}" plain icon="delete">不喜欢</van-button>
</div>
</div>
</div>
- 详情样式
.container {
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.detail {
padding: 46px 10px 44px;
// height: 1000%;
.title {
font-size: 18px;
line-height: 2;
}
.zan{
text-align: center;
padding: 10px 0;
.active{
border-color:red;
color: red;
}
}
.author {
padding: 10px 0;
display: flex;
.text {
flex: 1;
padding-left: 10px;
line-height: 1.5;
.name {
font-size: 14px;
margin: 0;
}
.time {
margin: 0;
font-size: 12px;
color: #999;
}
}
}
.content {
padding: 20px 0;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
/deep/ img{
max-width:100%;
background: #f9f9f9;
}
/deep/ code{
white-space: pre-wrap;
}
/deep/ pre{
white-space: pre-wrap;
}
}
}
总结
- 调用接口;页面布局;填充模板
# 文章关注与取消关注
目标:实现关注与取消关注文章
- 设置关注按钮的状态
- 实现点击按钮的关注和取消关注的操作
- 绑定事件
<van-button @click="followed()" round size="small" type="info">
{{article.is_followed?'已关注':'+ 关注'}}
</van-button>
- 封装关注与取消关注接口
// 关注文章接口
export const followArticle = (authorId) => {
return request({
method: 'post',
url: 'app/v1_0/user/followings',
data: {
target: authorId
}
})
}
// 取消关注文章接口
export const unFollowArticle = (authorId) => {
return request({
method: 'delete',
url: 'app/v1_0/user/followings/' + authorId
})
}
- 实现功能
import { followArticle, unFollowArticle } from '@/api/api-article.js'
// 关注与取消关注文章
async toggleFollow (article) {
if (article.is_followed) {
// 取消关注
try {
await unFollowArticle(article.aut_id)
article.is_followed = !article.is_followed
} catch {
this.$toast.fail('取消关注失败')
}
} else {
// 进行关注
try {
await followArticle(article.aut_id)
article.is_followed = !article.is_followed
} catch {
this.$toast.fail('关注失败')
}
}
},
总结:状态展示;事件绑定;功能实现(逻辑思维:状态位)
# 文章点赞与不喜欢
目标:实现文章点赞与不喜欢功能
- 按钮的状态显示
- -1无态度(都是灰色)
- 1点赞(点赞按钮红色)
- 0不喜欢(不喜欢按钮红色)
- 点击操作
- 点赞和取消点赞
- 不喜欢和取消不喜欢
- 绑定事件
<div class="zan">
<van-button @click="praise(1)" round size="small" :class="{active:article.attitude===1}" plain icon="like-o">点赞</van-button>
<van-button @click="praise(0)" round size="small" :class="{active:article.attitude===0}" plain icon="delete">不喜欢</van-button>
</div>
- 封装点赞与不喜欢接口
// 点赞接口
export const likes = (articleId) => {
return request({
method: 'post',
url: 'app/v1_0/article/likings',
data: {
target: articleId
}
})
}
// 取消点赞接口
export const unlikes = (articleId) => {
return request({
method: 'delete',
url: 'app/v1_0/article/likings/' + articleId
})
}
// 添加【不喜欢】接口
export const dislikes = (articleId) => {
return request({
method: 'post',
url: 'app/v1_0/article/dislikes',
data: {
target: articleId
}
})
}
// 取消【不喜欢】接口
export const undislikes = (articleId) => {
return request({
method: 'delete',
url: 'app/v1_0/article/dislikes/' + articleId
})
}
- 实现功能
async handleToggle (type) {
// 点赞和取消点赞;不喜欢和取消不喜欢
if (type === 1) {
// 点赞和取消点赞
if (this.article.attitude === 1) {
// 取消点赞
try {
await unlikes(this.article.art_id)
this.article.attitude = -1
} catch {
this.$toast.fail('取消点赞失败')
}
} else {
// 点赞
try {
await likes(this.article.art_id)
this.article.attitude = 1
} catch {
this.$toast.fail('点赞失败')
}
}
} else {
// 不喜欢和取消不喜欢
if (this.article.attitude === 0) {
// 取消不喜欢
try {
await undislikes(this.article.art_id)
this.article.attitude = -1
} catch {
this.$toast.fail('取消不喜欢失败')
}
} else {
// 不喜欢
try {
await dislikes(this.article.art_id)
this.article.attitude = 0
} catch {
this.$toast.fail('不喜欢失败')
}
}
}
},
总结:按钮本身的状态显示;点击切换状态(逻辑思维:耐心和细心)
做法:先梳理逻辑分支结构(伪代码);然后做填空
# 文章评论模块
# 评论组件拆分
目标:拆分文章评论组件并实现功能
- 准备单独的组件模板
- 导入组件
- 配置组件
- 使用组件
- 组件基本布局
<div class="comment">
<!-- 评论列表 -->
<van-list v-model="loading" :finished="finished" finished-text="没有更多了">
<div class="item van-hairline--bottom van-hairline--top" v-for="index in 5" :key="index">
<van-image round width="1rem" height="1rem" fit="fill" src="https://img.yzcdn.cn/vant/cat.jpeg" />
<div class="info">
<p>
<span class="name">一阵清风</span>
<span style="float:right">
<span class="van-icon van-icon-good-job-o zan"></span>
<span class="count">10</span>
</span>
</p>
<p>评论的内容,。。。。</p>
<p>
<span class="time">两天内</span>
<van-tag plain @click="showReply=true">4 回复</van-tag>
</p>
</div>
</div>
</van-list>
<!-- 底部输入框 -->
<div class="reply-container van-hairline--top">
<van-field v-model="value" placeholder="写评论...">
<span class="submit" slot="button">提交</span>
</van-field>
</div>
</div>
- 布局样式
.comment {
margin-top: 10px;
/deep/ .item {
display: flex;
padding: 10px 0;
.info {
flex: 1;
padding-left: 10px;
.name{
color:#069;
}
.zan{
vertical-align:middle;
padding-right:2px;
}
.count{
vertical-align:middle;
font-size:10px;
color: #666;
}
.time{
color: #666;
}
p {
padding: 5px 0;
margin: 0;
}
}
}
/deep/ .van-button:active::before {
background: transparent;
}
}
.reply-container {
position: fixed;
left: 0;
bottom: 0;
height: 44px;
width: 100%;
background: #f5f5f5;
z-index: 9999;
.submit {
font-size: 12px;
color: #3296fa;
}
}
- 评论相关数据
data () {
return {
value: '',
loading: false,
finished: false
}
}
总结:单文件子组件用法:导入;配置;使用
# 评论列表渲染
目标:实现评论列表数据动态渲染
- 调用接口获取文章对应的评论数据
- 动态填充评论列表的模板
关于分页:
- page(页码)和per_page(每页条数)
- offset(偏移量:从第一条数据开始查询)和limit(每页条数):offset = (page - 1) * limit + 1
- 根据上一页返回的标记(时间戳;文章的id),查询下一页数据
- 封装列表数据接口
// 获取文章的评论
export const getComments = (articleId, offset) => {
return request({
method: 'get',
url: 'app/v1_0/comments',
params: {
// a表示文章的评论;c表示回复评论的数据
type: 'a',
// 评论的文章的id或者,回复的评论的id
source: articleId,
// 分页参数(评论的id)
offset: offset,
// 每页的条数
limit: 10
}
})
}
- 动态加载评论列表数据
methods: {
async getComments () {
try {
const ret = await getComments(this.articleId, this.offset)
this.list = ret.data.results
} catch {
this.$toast.fail('获取评论数据失败')
}
}
},
created () {
this.getComments()
}
- 列表数据动态渲染
<van-cell v-for='item in list' :key='item.com_id'>
<div class="item van-hairline--bottom van-hairline--top">
<van-image round width="1rem" height="1rem" fit="fill" :src="item.aut_photo" />
<div class="info">
<p>
<span class="name">{{item.aut_name}}</span>
<span style="float:right">
<span class="van-icon van-icon-good-job-o zan"></span>
<span class="count">{{item.like_count}}</span>
</span>
</p>
<p>{{item.content}}</p>
<p>
<span class="time">{{item.pubdate|timeFormat}}</span>
<van-tag plain @click="showReply=true">{{item.reply_count}} 回复</van-tag>
</p>
</div>
</div>
</van-cell>
总结:调用接口;获取数据;渲染页面
- 基于van-list实现评论的分页加载
async onLoad () {
// 分页加载评论数据
const ret = await getComments(this.articleId, this.offset)
this.list.push(...ret.data.results)
this.loading = false
// 判断结束的条件
if (ret.data.last_id === ret.data.end_id) {
// 没有更多数据了
this.finished = true
} else {
// 把下次查询的起始id记录一下
this.offset = ret.data.last_id
}
}
总结:van-list组件的分页流程(每次加载完成数据后需要手动设置loading状态位为false;onLoad如果加载的数据没有沾满一页,那么自动触发下一次加载的过程)
# 文章评论功能
目标:实现评论功能
- 获取表单数据并提交
- 调用接口实现评论功能
- 封装接口
// 发表评论接口
export const comment = (options) => {
return request({
method: 'post',
url: 'app/v1_0/comments',
data: {
target: options.target,
content: options.content,
art_id: options.articleId
}
})
}
- 绑定事件
<span class="submit" @click="handleSubmit()" slot="button">提交</span>
- 实现评论回复功能
async handleSubmit () {
// 发表评论
try {
// 发表评论
await comment({
target: this.articleId,
content: this.value
})
// 需要手工修改状态位为true(防止一页数据不够)
this.loading = true
// 刷新列表
this.list = []
this.onLoad()
// 清空表单
this.value = ''
} catch {
this.$toast.fail('发表评论失败')
}
},
总结:获取表单数据,调用接口,刷新列表
注意:需要手工修改状态位为true(防止一页数据不够)