今天说到了文章评论列表,文章评论列表的话还是用到了我们熟悉的vant里的list组件
但是用到了新的技术,比如说,使用了v-model进行了父子组件的数据通信
首先是更新后了的article/index.vue代码
<template>
<div class="article-container">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
left-arrow
title="黑马头条"
@click-left="$router.back()"
></van-nav-bar>
<!-- /导航栏 -->
<div class="main-wrap">
<!-- 加载中 -->
<div class="loading-wrap" v-if="loading">
<van-loading color="#3296fa" vertical>加载中</van-loading>
</div>
<!-- /加载中 -->
<!-- 加载完成-文章详情 -->
<div class="article-detail" v-else-if="article.title">
<!-- 文章标题 -->
<h1 class="article-title">{{ article.title }}</h1>
<!-- /文章标题 -->
<!-- 用户信息 -->
<van-cell class="user-info" center :border="false">
<van-image
class="avatar"
slot="icon"
round
fit="cover"
:src="article.aut_photo"
/>
<div slot="title" class="user-name">{{ article.aut_name }}</div>
<div slot="label" class="publish-date">
{{ article.pubdate | relativeTime }}
</div>
<!-- <van-button
@click="onFollow"
v-if="article.is_followed"
class="follow-btn"
round
size="small"
:loading="followLoading"
>已关注</van-button
>
<van-button
@click="onFollow"
v-else
class="follow-btn"
type="info"
color="#3296fa"
round
size="small"
icon="plus"
:loading="followLoading"
>关注</van-button
> -->
<!-- $event是用来接收子组件传过来的值 -->
<!-- 在使用组件的同一个变量既要传递数据又要修改数据
传递props:
:is-followed="article.is_followed"
修改:自定义事件
@updataFollow="article.is_followed = $event"
-->
<!-- 可以使用简化形式 v-model -->
<!-- 传递props:
:value="article.is_followed"
修改:内置封装 input
@input="article.is_followed = $event"
-->
<!-- <FollowUser
:isFollowed="article.is_followed"
:followUserId="article.aut_id"
@updataFollow="article.is_followed = $event"
></FollowUser> -->
<FollowUser
:followUserId="article.aut_id"
v-model="article.is_followed"
></FollowUser>
</van-cell>
<!-- /用户信息 -->
<!-- 文章内容 -->
<div
class="article-content markdown-body"
v-html="article.content"
ref="article-content"
></div>
<van-divider>正文结束</van-divider>
<!-- 评论列表开始 -->
<CommentList
:articleId="article.art_id"
@commentCounts="totalCommentCount = $event"
></CommentList>
<!-- 评论列表结束 -->
<!-- 底部区域 -->
<div class="article-bottom">
<van-button class="comment-btn" type="default" round size="small"
>写评论</van-button
>
<van-icon name="comment-o" :info="totalCommentCount" color="#777" />
<!-- <van-icon color="#777" name="star-o" /> -->
<collectArticle
v-model="article.is_collected"
:articleId="article.art_id"
></collectArticle>
<!-- 点赞区域开始 -->
<!-- <van-icon color="#777" name="good-job-o" /> -->
<LikeArticle
class="btn-item"
v-model="article.attitude"
:article-id="article.art_id"
></LikeArticle>
<!-- 点赞区域结束 -->
<van-icon name="share" color="#777777"></van-icon>
</div>
<!-- /底部区域 -->
</div>
<!-- /加载完成-文章详情 -->
<!-- 加载失败:404 -->
<div class="error-wrap" v-else-if="errStatus === 404">
<van-icon name="failure" />
<p class="text">该资源不存在或已删除!</p>
</div>
<!-- /加载失败:404 -->
<!-- 加载失败:其它未知错误(例如网络原因或服务端异常) -->
<div class="error-wrap" v-else>
<van-icon name="failure" />
<p class="text">内容加载失败!</p>
<van-button class="retry-btn" @click="loadArticleDetail"
>点击重试</van-button
>
</div>
<!-- /加载失败:其它未知错误(例如网络原因或服务端异常) -->
</div>
</div>
</template>
<script>
import LikeArticle from "@/components/like-article";
import CommentList from "./components/comment-list";
import collectArticle from "@/components/collect-article";
import FollowUser from "@/components/follow-user";
// import { addFollowAPI, deleteFollowAPI } from "@/api";
import { getArticleByIdAPI } from "@/api";
import { ImagePreview } from "vant";
// vant的ImagePreview配置代码
// ImagePreview({
// images: [
// "https://img01.yzcdn.cn/vant/apple-1.jpg",
// "https://img01.yzcdn.cn/vant/apple-2.jpg",
// ],
// startPosition: 1,
// closeable: true,
// onClose() {
// this.$toast("关闭");
// },
// });
export default {
name: "ArticleIndex",
components: {
FollowUser,
collectArticle,
CommentList,
LikeArticle,
},
props: {
// 使用props获取动态路由的数据
articleId: {
type: [Number, String],
required: true,
},
},
data() {
return {
article: {},
loading: true, // 加载中的状态
errStatus: 0, // 存储错误的状态码
followLoading: false, // 控制加载中的状态
totalCommentCount: 0, // 存储评论总数
};
},
computed: {},
watch: {},
created() {
this.loadArticle();
},
mounted() {},
methods: {
// 3. 定义获取数据请求方法
async loadArticle() {
// 开启加载中的状态
this.loading = true;
try {
// 手动抛出错误,测试功能
// if (Math.random() > 0.6) {
// throw new Error("错误");
// }
// 3.1 发送请求
const { data } = await getArticleByIdAPI(this.articleId);
// 3.3 成功赋值
this.article = data.data; // vue的dom更新是异步的
setTimeout(() => {
// 在数据更新之后,调用图片预览功能
this.previewImage();
}, 0);
console.log(this.article); // 控制台查看数据输出
} catch (err) {
// 3.2 失败处理
if (err.response && err.response.status === 404) {
this.errStatus = 404;
this.$toast("访问的文章资源不存在");
} else {
this.$toast("获取文章详情失败");
console.log(err);
}
}
// 关闭加载中的状态
this.loading = false;
},
loadArticleDetail() {
this.loadArticle();
},
previewImage() {
// 得到所有的 img 节点
const articleContent = this.$refs["article-content"]; // 获取到了容器节点
const imgs = articleContent.querySelectorAll("img");
// 存储文章内容中的所有图片地址
const images = [];
// 遍历所有的图片,拿到每个图片的地址
imgs.forEach((img, index) => {
// 把图片地址push进images数组里
images.push(img.src);
// 给每个图片添加一个点击事件,调用ImagePreview图片预览功能
img.onclick = () => {
ImagePreview({
images: images,
// 指定预览图片的起始位置
startPosition: index,
});
};
});
},
// 关注用户,和取消关注
// async onFollow() {
// this.followLoading = true;
// // 判断用户是否登录,如果没有请先登录才能操作
// if (!this.$store.state.user) return this.$toast("请先登录");
// // 发送请求实现登录(添加关注,和取消关注)
// try {
// const AutId = this.article.aut_id;
// // 判断是否已经关注了
// if (this.article.is_followed) {
// await deleteFollowAPI(AutId);
// } else {
// await addFollowAPI(AutId);
// }
// // 更新视图
// this.article.is_followed = !this.article.is_followed;
// } catch (err) {
// if (err && err.response.status === 400) {
// this.$toast("用户不能关注自己");
// } else {
// this.$toast("请求失败");
// }
// }
// this.followLoading = false;
// },
},
};
</script>
<style scoped lang="less">
@import "./github-markdown.css";
.article-container {
.main-wrap {
position: fixed;
left: 0;
right: 0;
top: 92px;
bottom: 88px;
overflow-y: scroll;
background-color: #fff;
}
.article-detail {
.article-title {
font-size: 40px;
padding: 50px 32px;
margin: 0;
color: #3a3a3a;
}
.user-info {
padding: 0 32px;
.avatar {
width: 70px;
height: 70px;
margin-right: 17px;
}
.van-cell__label {
margin-top: 0;
}
.user-name {
font-size: 24px;
color: #3a3a3a;
}
.publish-date {
font-size: 23px;
color: #b7b7b7;
}
.follow-btn {
width: 170px;
height: 58px;
}
}
.article-content {
padding: 55px 32px;
/deep/ p {
text-align: justify;
}
}
}
.loading-wrap {
padding: 200px 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
.error-wrap {
padding: 200px 32px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
.van-icon {
font-size: 122px;
color: #b4b4b4;
}
.text {
font-size: 30px;
color: #666666;
margin: 33px 0 46px;
}
.retry-btn {
width: 280px;
height: 70px;
line-height: 70px;
border: 1px solid #c3c3c3;
font-size: 30px;
color: #666666;
}
}
.article-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-around;
align-items: center;
box-sizing: border-box;
height: 88px;
border-top: 1px solid #d8d8d8;
background-color: #fff;
.comment-btn {
width: 282px;
height: 46px;
border: 2px solid #eeeeee;
font-size: 30px;
line-height: 46px;
color: #a7a7a7;
}
.van-icon {
font-size: 40px;
.van-info {
font-size: 16px;
background-color: #e22829;
}
}
}
}
</style>
article/commponents/comment-item.vue
<template>
<van-cell class="comment-item">
<van-image
slot="icon"
class="avatar"
round
fit="cover"
:src="comment.aut_photo"
/>
<div slot="title" class="title-wrap">
<div class="user-name">{{ comment.aut_name }}</div>
<van-button
class="like-btn"
:icon="comment.is_liking ? 'good-job' : 'good-job-o'"
>{{ comment.like_count > 0 ? comment.like_count : "赞" }}</van-button
>
</div>
<div slot="label">
<p class="comment-content">{{ comment.content }}</p>
<div class="bottom-info">
<span class="comment-pubdate">{{
comment.pubdate | relativeTime
}}</span>
<van-button class="reply-btn" round
>回复 {{ comment.reply_count }}</van-button
>
</div>
</div>
</van-cell>
</template>
<script>
export default {
name: "CommentItem",
props: {
//每行的评论信息
comment: {
type: Object,
required: true,
},
},
methods: {},
};
</script>
<style scoped lang="less">
.comment-item {
.avatar {
width: 72px;
height: 72px;
margin-right: 25px;
}
.title-wrap {
display: flex;
justify-content: space-between;
align-items: center;
.user-name {
color: #406599;
font-size: 26px;
}
}
.comment-content {
font-size: 32px;
color: #222222;
word-break: break-all;
text-align: justify;
}
.comment-pubdate {
font-size: 19px;
color: #222;
margin-right: 25px;
}
.bottom-info {
display: flex;
align-items: center;
}
.reply-btn {
width: 135px;
height: 48px;
line-height: 48px;
font-size: 21px;
color: #222;
}
.like-btn {
height: 30px;
padding: 0;
border: none;
font-size: 19px;
line-height: 30px;
margin-right: 7px;
.van-icon {
font-size: 30px;
}
}
.liked {
background-color: orange;
}
}
</style>
配置好收藏功能代码api/article.js补充代码
/**
* 收藏文章
*/
export const addCollect = (target) => {
return request({
method: "POST",
url: "/v1_0/article/collections",
data: {
target,
},
});
};
/**
* 取消收藏文章
*/
export const deleteCollect = (target) => {
return request({
method: "DELETE",
url: `/v1_0/article/collections/${target}`,
});
};
article/commponents/comment-list.vue
<template>
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<!-- <van-cell
v-for="comment in list"
:key="comment.com_id"
:title="comment.content"
></van-cell> -->
<CommentItem
v-for="comment in list"
:key="comment.com_id"
:title="comment.content"
:comment="comment"
></CommentItem>
</van-list>
</template>
<script>
import CommentItem from "./comment-item";
import { getCommentsAPI } from "@/api";
export default {
name: "CommentList",
components: {
CommentItem,
},
props: {
articleId: {
type: [String, Number],
required: true,
},
},
created() {
// 页面刚开始触发函数,加载评论数
this.onLoad();
},
data() {
return {
list: [], // 评论列表
loading: false, // 上拉加载更多的 loading
finished: false, // 是否加载结束
offset: null,
limit: 20, // 每一页的个数
type: "a", // 'a | c'
};
},
methods: {
async onLoad() {
// 1. 获取数据
try {
const { data } = await getCommentsAPI({
type: this.type, // 评论类型,a-对文章(article)的评论,c-对评论(comment)的回复
source: this.articleId, // 源id,文章id或评论id
offset: this.offset, // 获取评论的偏移量
limit: this.limit, // 每一页的个数
});
console.log(data);
const { results } = data.data;
// 2. 更新数据,将获取到的数据追加到数组中
this.list = [...this.list, ...results];
this.$emit("commentCounts", data.data.total_count);
// 3. 将loading设置为false
this.loading = false;
// 4. 判断数据是否加载完毕
if (results.length) {
// 4.1 如果还有数据,进行翻页
this.offset = data.data.last_id;
} else {
// 4.2 如果没有数据将finished设置为false
this.finished = true;
}
} catch (error) {
this.loading = false;
this.$toast("请求评论失败");
}
},
},
};
</script>
然后就是src/components/collect-article/index.vue
<template>
<van-button
:loading="CollectLoading"
@click="onCollect"
:icon="value ? 'star' : 'star-o'"
:class="{ collected: value }"
></van-button>
</template>
<script>
import { addCollectAPI, deleteCollectAPI } from "@/api";
export default {
name: "CollectArticle",
props: {
value: {
type: Boolean,
required: true,
},
articleId: {
type: [String, Number],
required: true,
},
},
data() {
return {
CollectLoading: false,
};
},
methods: {
async onCollect() {
this.CollectLoading = true;
try {
// 判断用户是否登录,如果没有,让用户先登录在操作
if (!this.$store.state.user) return this.$toast("请先登录");
// 判断是否已经收藏
if (this.value) {
// 已经收藏,取消收藏
await deleteCollectAPI(this.articleId);
this.$toast("取消收藏成功");
} else {
// 收藏文章
await addCollectAPI(this.articleId);
this.$toast("收藏成功");
}
// 同步视图
this.$emit("input", !this.value);
} catch (error) {
this.$toast("收藏失败");
}
this.CollectLoading = false;
},
},
created() {},
};
</script>
<style scoped lang="less">
.collected {
.van-icon {
color: #ffa500;
}
}
</style>
over!