vite+vant+vue3新闻客户端app(三)
通过 Axios 请求得到的数据都是 Axios 处理(JSON.parse)之后的,我们应该在 Axios 执行处理之前手动使用 json-bigint 来解析处理。JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。弹层渲染出来以后就只是简单的切换显示和隐藏,
一、文章搜索
1.1创建组件并配置路由
1、创建 src/views/search/index.vue
2、然后把搜索页面的路由配置到根组件路由(一级路由)
{
path: '/search',
name: 'search',
component: Search
},
3、在home.vue配置路由跳转
1.2页面布局
Search 组件提供了 search 和 cancel 事件,search 事件在点击键盘上的搜索/回车按钮后触发,cancel 事件在点击搜索框右侧取消按钮时触发。
Tips: 在 van-search 外层增加 form 标签,且 action 不为空,即可在 iOS 输入法中显示搜索按钮。
1、创建 src/views/search/components/search-suggestion.vue
<!-- 视图层: html -->
<template>
<div class="search-suggestion">
<van-cell title="hello" icon="search"> </van-cell>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
</script>
<style lang="less" scoped>
</style>
2、创建 src/views/search/components/search-history.vue
<!-- 视图层: html -->
<template>
<div class="search-history">
<van-cell title="搜索历史">
<div>
<span>全部删除</span>
<span>完成</span>
</div>
<!-- <van-icon name="delete"></van-icon> -->
</van-cell>
<van-cell title="hello">
<van-icon name="close"></van-icon>
</van-cell>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
</script>
<style lang="less" scoped>
</style>
3、创建 src/views/search/components/search-result.vue
<!-- 视图层: html -->
<template>
<div class="search-result">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item" :title="item" />
</van-list>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import { ref } from "vue";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const onLoad = () => {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push(list.value.length + 1);
}
// 加载状态结束
loading.value = false;
// 数据全部加载完成
if (list.value.length >= 40) {
finished.value = true;
}
}, 1000);
};
</script>
<style lang="less" scoped>
</style>
4、搜索组件内容如下:
<!-- 视图层: html -->
<template>
<div class="search-container">
<!-- 搜索栏 -->
<!-- Tips: 在 van-search 外层增加 form 标签,且 action 不为空,即可在 iOS 输入法中显示搜索按钮。 -->
<form action="/">
<van-search
v-model="searchText"
show-action
placeholder="请输入搜索关键词"
@search="onSearch"
@cancel="$router.back"
/>
</form>
<!-- 联想建议 -->
<SearchSuggestion></SearchSuggestion>
<!-- 历史记录 -->
<SearchHistory></SearchHistory>
<!-- 搜索结果 -->
<SearchResult></SearchResult>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import SearchSuggestion from "./components/search-suggestion.vue";
import SearchHistory from "./components/search-history.vue";
import SearchResult from "./components/search-result.vue";
import { ref } from "vue";
const searchText = ref("");
function onSearch() {
console.log("onSearch");
}
function onCancel() {
console.log("onCancel");
}
</script>
<style lang="less" scoped>
</style>
1.3处理页面显示状态
1、添加数据用来控制搜索结果的显示状态
const isResultShow = ref(false); //控制搜索结果的显示状态
2、在模板中绑定条件渲染
<!-- 搜索结果 -->
<SearchResult v-if="isResultShow"></SearchResult>
<!-- 联想建议 -->
<SearchSuggestion v-else-if="searchText"></SearchSuggestion>
<!-- 历史记录 -->
<SearchHistory v-else></SearchHistory>
1.4搜索联想建议
基本思路:
当搜索框输入内容的时候,请求加载联想建议的数据
将请求得到的结果绑定到模板中
基本功能
一、将父组件中搜索输入框的内容传给联想建议子组件
二、在子组件中监视搜索框输入内容的变化,如果变化则请求获取联想建议数据
https://cn.vuejs.org/guide/essentials/watchers.html#basic-example
三、将获取到的联想建议数据展示到列表中
搜索关键字高亮
1、加一个方法处理高亮
function highlight(str) {
// console.log(str);
// RegExp是正则表达式的构造函数
// 参数1:字符串
// 参数2:匹配模式
// 参数3:正则对象
return str.replace(
new RegExp(props.searchText, "gi"),
`<span style="color:red">${props.searchText}</span>`
);
}
2、然后在联想建议列表项中绑定调用
<van-cell v-for="(str, index) in suggestions" :key="index" icon="search">
<template #title>
<div v-html="highlight(str)"></div>
</template>
</van-cell>
1.5搜索结果
思路:
找到数据接口
请求获取数据
将数据展示到模板中
一、获取搜索关键字
1、声明接收父组件中的搜索框输入的内容
<!-- 搜索结果 -->
<SearchResult :search-text="searchText" v-if="isResultShow"> </SearchResult>
2、父组件给子组件传递数据
const props = defineProps({
searchText: {
type: String,
required: true,
},
});
二、请求获取数据
1、在 api/serach.js 添加封装获取搜索结果的请求方法
//获取搜索结果
export const getSearchResult= (params) =>{
return request({
method:'GET',
url:'/v1_0/search',
params
})
}
2、请求获取
<script setup>
import { ref } from "vue";
import { getSearchResult } from "~/api/search.js";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1); //页数,不传默认为1
const per_page = ref(10); //每页数量,不传每页数量由后端决定
const props = defineProps({
searchText: {
type: String,
required: true,
},
});
async function onLoad() {
// 1.请求获取数据
const { data } = await getSearchResult({
page: page.value, //页数,不传默认为1
per_page: per_page.value, //每页数量,不传每页数量由后端决定
q: props.searchText, //搜索关键词
});
// 2.将数据放到数据列表中
const { results } = data.data;
list.value.push(...results);
// 3.关闭本次loading
loading.value = false;
// 4.判断是否还有数据
if (results.length) {
// 如果有,则更新获取下一页数据页码
page.value++;
} else {
// 如果没有,则把finished设置为true,关闭加载更多
finished.value = true;
}
console.log(data);
}
</script>
三、最后,模板绑定
<van-cell
v-for="(article, index) in list"
:key="index"
:title="article.title"
/>
1.6搜索历史记录
添加历史记录
当发生搜索的时候我们才需要记录历史记录。
1、添加一个数据用来存储历史记录
const searchHistories = ref([]); //搜索历史数据
2、在触发搜索的时候,记录历史记录
function onSearch(searchTexts) {
// 把输入框设置为你要搜索的文本
searchText.value = searchTexts;
const index = searchHistories.value.indexOf(searchTexts);
if (index !== -1) {
// 把重复项删除
searchHistories.value.splice(index, 1);
}
// 记录搜索历史记录
searchHistories.value.unshift(searchTexts);
//展示搜索结果
isResultShow.value = true;
}
展示历史记录
1、声明接收父组件中的历史记录的内容
<SearchHistory v-else :search-histories="searchHistories"> </SearchHistory>
2、父组件给子组件传递数据
const props = defineProps({
searchHistories: {
type: Array,
required: true,
},
});
3、模板绑定
<van-cell
:title="history"
v-for="(history, index) in props.searchHistories"
:key="index"
>
删除历史记录
基本思路:
给历史记录中的每一项注册点击事件
在处理函数中判断
如果是删除状态,则执行删除操作
如果是非删除状态,则执行搜索操作
一、处理删除相关元素的展示状态
1、添加一个数据用来控制删除相关元素的显示状态
const isDeleteShow = ref(false); //删除的显示状态
2、绑定使用
<van-cell title="搜索历史">
<div v-if="isDeleteShow">
<span @click="$emit('update-histories', [])">全部删除</span>
<span @click="isDeleteShow = false" class="finish">完成</span>
</div>
<van-icon @click="isDeleteShow = true" v-else name="delete"></van-icon>
</van-cell>
<van-cell
:title="history"
v-for="(history, index) in props.searchHistories"
:key="index"
@click="onDelete(history, index)"
>
<van-icon v-show="isDeleteShow" name="close"></van-icon>
</van-cell>
二、处理删除操作
function onDelete(history, index) {
// console.log(index);
if (isDeleteShow.value) {
props.searchHistories.splice(index, 1);
// 持久化处理
// 1.修改本地存储的数据
// 2.请求接口删除线上的数据
setItem("search-histories", props.searchHistories);
return;
}
// 非删除状态,展示搜索结果
emit("search", history);
}
数据持久化
1、利用 watch 监视统一存储数据
2、初始化的时候从本地存储获取数据
const searchHistories = ref(getItem("search-histories") || []); //搜索历史数据
二、文章详情
2.1创建组件并配置路由
1、创建 views/article/index.vue 组件
<!-- 视图层: html -->
<template>
<div class="article-container">
文章详情
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import "~/views/article/github-markdown.css";
const props = defineProps({
articleId: {
type: String,
required: true,
},
});
</script>
<style lang="less" scoped>
</style>
2、然后将该页面配置到根级路由
{
path: '/article/:articleId',
name: 'article',
component: Article,
// 将动态路由的参数映射到组件的props中,无论是访问还是维护性都很方便
props:true
},
2.2页面布局
使用到的 Vant 中的组件:
markdown-css:
https://gitee.com/ylx252/github-markdown-css/blob/gh-pages/github-markdown.css
<!-- 视图层: html -->
<template>
<div class="article-container">
<van-nav-bar
class="app-nav-bar"
title="文章详情"
left-arrow
@click-left="$router.back()"
/>
<h1 class="title">牛逼程序员</h1>
<van-cell center class="user-info">
<template #title>
<div class="name">天涯小型客</div>
</template>
<template #icon>
<van-image
fit="cover"
round
class="avatar"
src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
/>
</template>
<template #label>
<div class="pubdate">14小时前</div>
</template>
<van-button icon="plus" class="follow-btn" round size="small"
>关注</van-button
>
</van-cell>
<div class="markdown-body content">
<p>hello</p>
<p>hello</p>
<a href="#">1111</a>
</div>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import "~/views/article/github-markdown.css";
import { getArticleById } from "~/api/article.js";
// 在组件中获取动态路由参数
// 方式一:this.$route.params.articleId
// 方式二:props传参,this.articleId
const props = defineProps({
articleId: {
type: [String, Number, Object],
required: true,
},
});
</script>
<style lang="less" scoped>
.title {
font-size: 40px;
color: #3a3a3a;
padding: 34px;
background-color: #fff;
margin: 0;
}
.user-info {
.avatar {
width: 75px;
height: 75px;
margin-right: 15px;
}
.name {
font-size: 25px;
color: #333;
}
.pubdate {
font-size: 20px;
color: #b4b4b4;
}
.follow-btn {
width: 150px;
height: 70px;
background-color: rgb(197, 66, 34);
color: #fff;
border: none;
}
}
.markdown-body {
padding: 34px;
}
</style>
2.3关于后端返回数据中的大数字问题
之所以请求文章详情返回 404 是因为我们请求发送的文章 ID (article.art_id)不正确。
JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
Math.pow(2, 53) // 9007199254740992
9007199254740992 // 9007199254740992
9007199254740993 // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1
// true
上面代码中,超出 2 的 53 次方之后,一个数就不精确了。ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true
上面代码中,可以看到 JavaScript 能够精确表示的极限。
后端返回的数据一般都是 JSON 格式的字符串。
'{ "id": 9007199254740995, "name": "Jack", "age": 18 }'
如果这个字符不做任何处理,你能方便的获取到字符串中的指定数据吗?非常麻烦。所以我们要把它转换为 JavaScript 对象来使用就很方便了。
幸运的是 axios 为了方便我们使用数据,它会在内部使用 JSON.parse() 把后端返回的数据转为 JavaScript 对象。
// { id: 9007199254740996, name: 'Jack', age: 18 }
JSON.parse('{
"id": 9007199254740995, "name": "Jack", "age": 18 }')
可以看到,超出安全整数范围的 id 无法精确表示,这个问题并不是 axios 的错。
了解了什么是大整数的概念,接下来的问题是如何解决?
json-bigint 是一个第三方包,它可以帮我们很好的处理这个问题。
使用它的第一步就是把它安装到你的项目中。
npm i json-bigint
通过 Axios 请求得到的数据都是 Axios 处理(JSON.parse)之后的,我们应该在 Axios 执行处理之前手动使用 json-bigint 来解析处理。Axios 提供了自定义处理原始后端返回数据的 API:transformResponse 。
request.js
import JSONbig from 'json-bigint'
const request= axios.create({
baseURL:'http://toutiao.itheima.net',
transformResponse:[function(data){
try {
return JSONbig.parse(data)
} catch (err) {
console.log('转换失败',err);
return data;
}
}]
})
扩展:ES2020 BigInt
ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
参考链接:
2.4展示文章详情
思路:
找到数据接口
封装请求方法
请求获取数据
模板绑定
一、请求并展示文章详情
1、在 api/article.js 中新增封装接口方法
// 获取新闻详情
export const getArticleById= article_id =>{
return request({
method:'GET',
url:`/v1_0/articles/${article_id}`,
})
}
2、在组件中调用获取文章详情
<script setup>
import "~/views/article/github-markdown.css";
import { getArticleById } from "~/api/article.js";
// 在组件中获取动态路由参数
// 方式一:this.$route.params.articleId
// 方式二:props传参,this.articleId
import { useStore } from "vuex";
const store = useStore();
const props = defineProps({
articleId: {
type: [String, Number, Object],
required: true,
},
});
getArticleById(props.articleId).then((res) => {
console.log(res.data.data);
store.commit("setArticle", res.data.data);
});
</script>
3、模板绑定
<template>
<div class="article-container">
<van-nav-bar
class="app-nav-bar"
title="文章详情"
left-arrow
@click-left="$router.back()"
/>
<h1 class="title">{{ $store.state.article.title }}</h1>
<van-cell center class="user-info">
<template #title>
<div class="name">{{ $store.state.article.aut_name }}</div>
</template>
<template #icon>
<van-image
fit="cover"
round
class="avatar"
:src="$store.state.article.aut_photo"
/>
</template>
<template #label>
<div class="pubdate">{{ $store.state.article.pubdate }}</div>
</template>
<van-button
:icon="$store.state.article.is_followed ? '' : 'plus'"
:class="$store.state.article.is_followed ? '' : 'follow-btn'"
round
size="small"
>{{ $store.state.article.is_followed ? "已关注" : "关注" }}</van-button
>
</van-cell>
<div
v-html="$store.state.article.content"
class="markdown-body content"
></div>
</div>
</template>
2.5处理内容加载状态
需求:
加载中,显示loading
加载成功,显示文章详情
加载失败,显示错误提示
如果404,提示资源不存在
其他的,提示加载失败,用户可以点击重试重新加载
2.6关于文章正文的样式
文章正文包括各种数据:段落、标题、列表、链接、图片、视频等资源。
将 github-markdown-css 样式文件下载到项目中
配置不要转换样式文件中的字号
2.7图片点击预览
https://vant-ui.github.io/vant/#/zh-CN/image-preview
一、ImagePreview 图片预览的使用
二、处理图片点击预览
思路:
从文章内容中获取所有的img DOM节点
const articleContent = ref(null); // 获取文章内容DOM容器
获取文章内容中所有图片地址
遍历所有img节点,给每个节点注册点击事件
在img点击事件处理函数中,调用ImagePreview 预览
2.8关注用户
注意:自己不能关注自己,登录账号和发布文章账号不能是同一个。
思路:
给按钮注册点击事件
在事件处理函数中
如果已关注,则取消关注
如果没有关注,则添加关注
功能处理
找到数据接口
封装请求方法
请求调用
视图更新
1、在 api/user.js 中添加封装请求方法
// 关注用户
export const addFollow =target => {
return request({
method: 'POST',
url: '/v1_0/user/followings',
data:{
target
}
})
}
// 取消关注用户
export const deleteFollow = target => {
return request({
method: 'DELETE',
url: `/v1_0/user/followings/${target}`//userId目标用户(被取消关注的用户id)
})
}
2、给关注/取消关注按钮注册点击事件
3、在事件处理函数中
import { addFollow, deleteFollow } from "~/api/user.js";
async function onFollow() {
isFollowLoading.value = true;
const userId = store.state.article.aut_id;
if (store.state.article.is_followed) {
// 已关注,取消关注
console.log("取消关注");
await deleteFollow(userId);
// store.state.article.is_followed = false;
} else {
// 没有关注,添加关注
console.log("添加关注");
await addFollow(userId);
// store.state.article.is_followed = true;
}
store.state.article.is_followed = !store.state.article.is_followed;
isFollowLoading.value = false;
}
loading效果
两个作用:
交互反馈
防止网络慢用户多次点击按钮导致重复触发点击事件
2.9文章收藏
icon图标:设置 badge 属性后,会在图标右上角展示相应的徽标。
功能处理
思路:
给收藏按钮注册点击事件
如果已经收藏了,则取消收藏
如果没有收藏,则添加收藏
1、在 api/article.js 添加封装数据接口
// 收藏文章
export const addCollect= articleId =>{
return request({
method:'POST',
url:`/v1_0/article/collections`,
data:{
target: articleId
}
})
}
// 取消收藏文章
export const deleteCollect= articleId =>{
return request({
method:'DELETE',
url:`/v1_0/article/collections/${articleId}`,
})
}
2、给收藏按钮注册点击事件
3、处理函数
async function onCollect() {
isCollectLoading.value = true;
if (store.state.article.is_collected) {
// 已收藏,取消收藏
console.log("取消收藏");
await deleteCollect(props.articleId);
// store.state.article.is_followed = false;
} else {
// 没有收藏,添加收藏
console.log("添加收藏");
await addCollect(props.articleId);
// store.state.article.is_followed = true;
}
store.state.article.is_collected = !store.state.article.is_collected;
isCollectLoading.value = false;
showSuccessToast(`${store.state.article.is_collected ? "" : "取消"}收藏成功`);
}
2.10文章点赞
article中的attitude表示用户对文章的态度
-1无态度
0不喜欢
1已点赞
思路:
给点赞按钮注册点击事件
如果已经点赞,则请求取消点赞
如果没有点赞,则请求点赞
1、添加封装数据接口
// 对文章点赞
export const addLike= articleId =>{
return request({
method:'POST',
url:`/v1_0/article/likings`,
data:{
target: articleId
}
})
}
// 取消对文章点赞
export const deleteLike= articleId =>{
return request({
method:'DELETE',
url:`/v1_0/article/likings/${articleId}`,
})
}
2、给点赞按钮注册点击事件
3、处理函数
async function onLike() {
showLoadingToast({
message: "加载中...",
forbidClick: true, //禁止背景点击
});
if (store.state.article.attitude === 1) {
// 已点赞,取消点赞
console.log("取消收藏");
await deleteLike(props.articleId);
store.state.article.attitude = -1;
} else {
// 没有点赞,添加点赞
console.log("添加收藏");
await addLike(props.articleId);
store.state.article.attitude = 1;
}
showSuccessToast(
`${store.state.article.attitude === 1 ? "" : "取消"}点赞成功`
);
}
三、文章评论
3.1展示文章评论列表
为了更好的开发和维护,这里我们把文章评论单独封装到一个组件中来处理。
1、创建 src/views/article/components/article-comment.vue
<!-- 视图层: html -->
<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" :title="item">
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import { ref } from "vue";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
async function onLoad() {
// 异步更新数据
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;
}
}, 500);
}
}
</script>
<style lang="less" scoped>
</style>
2、在文章详情页面中加载注册文章评论子组件
import ArticleComment from "~/views/article/components/article-comment.vue";
3、在文章详情页面的加载失败提示消息后面使用文章评论子组件
<!-- 文章评论 -->
<ArticleComment :source="articleId"></ArticleComment>
3.2获取数据并展示
步骤:
封装接口
请求获取数据
处理模板
实现:
1、在 api/comment.js 中添加封装请求方法
import request from '~/utils/request'
//获取评论或评论回复
export const getComments= (params) =>{
return request({
method:'GET',
url:'/v1_0/comments',
params
})
}
2、请求获取数据
import { getComments } from "~/api/comment.js";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const offset = ref(null); //获取下一页数据的页码
const limit = ref(10); //每页大小
const props = defineProps({
source: {
type: [Number, String, Object],
required: true,
},
});
async function onLoad() {
// 1.请求获取数据
const { data } = await getComments({
type: "a", //评论类型,a-对文章(article)的评论,c-对评论(comment)的回复
source: props.source, //源id,文章id或评论id
offset: offset.value, //获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据
limit: limit.value, //获取的评论数据个数,不传表示采用后端服务设定的默认每页数据量
});
console.log(data);
// 2.把数据放到列表中
const { results } = data.data;
list.value.push(...results);
// 3.将本次的loading关闭
loading.value = false;
// 4.判断是否还有数据
if (results.length) {
// 如果有,更新获取下一页数据的页面
offset.value = data.data.last_id;
// 如果没有,则将finished设置为true,不再触发加载更多
} else {
finished.value = true;
}
}
3、模板绑定,创建src/views/article/components/comment-item.vue组件
article-comment.vue
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<CommentItem
v-for="(comment, index) in list"
:key="index"
:comment="comment"
></CommentItem>
import CommentItem from "./comment-item.vue";
comment-item.vue
<!-- 视图层: html -->
<template>
<van-cell class="comment-item">
<template #icon>
<van-image
round
width="36"
height="36"
fit="cover"
:src="comment.aut_photo"
/>
</template>
<template #title>
<div>{{ comment.aut_name }}</div>
<div>{{ comment.content }}</div>
<div>
<span>{{ comment.pubdate }}</span>
<van-button round size="mini">回复</van-button>
</div>
</template>
<div>
<van-icon name="good-job-o"></van-icon>
<span>12</span>
</div>
</van-cell>
</template>
<!-- 逻辑层:js -->
<script setup>
const props = defineProps({
comment: {
type: Object,
required: true,
},
});
</script>
3.3评论点赞
1、在 api/comment.js 中添加封装两个数据接口
//对评论或评论回复点赞
export const addCommentLike= (target) =>{
return request({
method:'POST',
url:'/v1_0/comment/likings',
data:{
target//点赞的评论id
}
})
}
//取消对评论或评论回复点赞
export const deleteCommentLike= (commentId) =>{
return request({
method:'DELETE',
url:`/v1_0/comment/likings/${commentId}`,
})
}
2、然后给评论项注册点击事件
<div
:loading="isCommentLikeLoading"
@click="onCommentLike"
class="like-wrap"
>
<van-icon
class="like-icon"
:class="{
liked: comment.is_liking,
}"
:name="comment.is_liking ? 'good-job' : 'good-job-o'"
></van-icon>
<span class="like-count">{{ comment.like_count }}</span>
</div>
3、在事件处理函数中
<script setup>
import { ref } from "vue";
import { addCommentLike, deleteCommentLike } from "~/api/comment.js";
const isCommentLikeLoading = ref(false); //收藏按钮的loading状态
const props = defineProps({
comment: {
type: Object,
required: true,
},
});
async function onCommentLike() {
isCommentLikeLoading.value = true;
const commentId = props.comment.com_id;
if (props.comment.is_liking) {
// 已点赞,取消点赞
await deleteCommentLike(commentId);
props.comment.like_count--;
} else {
// 没有点赞,添加点赞
await addCommentLike(commentId);
props.comment.like_count++;
}
// 更新视图
props.comment.is_liking = !props.comment.is_liking;
isCommentLikeLoading.value = false;
}
</script>
3.4发布文章评论
准备弹出层,封装组件
设置 maxlength 和 show-word-limit 属性后会在底部显示字数统计。
autosize:是否自适应内容高度,只对 textarea 有效,可传入对象,如 { maxHeight: 100, minHeight: 50 },单位为px
post-comment.vue
<!-- 视图层: html -->
<template>
<div class="post-comment">
<van-field
border
v-model="message"
rows="2"
autosize
type="textarea"
maxlength="500"
placeholder="请输入评论"
show-word-limit
/>
<van-button class="post-btn" type="danger" size="small">发布</van-button>
</div>
</template>
<!-- 逻辑层:js -->
<script setup>
import { ref } from "vue";
const message = ref("");
</script>
<style lang="less" scoped>
.post-btn {
position: absolute;
right: 30px;
margin-top: 30px;
}
</style>
步骤:
注册发布点击事件
请求提交表单
根据响应结果进行后续处理
一、使用弹层展示发布评论
1、添加弹层组件
<!-- 底部弹出 -->
<van-popup
:style="{ height: '30%' }"
v-model:show="isPostShow"
position="bottom"
>
<PostComment></PostComment>
</van-popup>
import PostComment from "./components/post-comment.vue";
const isPostShow = ref(false); //控制发布评论的显示状态
2、点击发评论按钮的时候显示弹层
<van-button
@click="isPostShow = true"
class="comment-btn"
type="default"
round
size="small"
>
写评论
</van-button>
二、发布评论
1、在 api/comment.js 中添加封装数据接口
//对文章或者评论进行评论
export const addComment= (data) =>{
return request({
method:'POST',
url:`/v1_0/comments`,
data
})
}
2、绑定获取添加评论的输入框数据并且注册发布按钮的点击事件
post-comment.vue中
<van-field
border
v-model="message"
rows="2"
autosize
type="textarea"
maxlength="500"
placeholder="请输入评论"
show-word-limit
/>
const message = ref("");
3、在事件处理函数中
<script setup>
import { ref } from "vue";
import { addComment } from "~/api/comment.js";
import { showLoadingToast, showSuccessToast } from "vant";
import "vant/es/toast/style";
const message = ref("");
const emit = defineEmits(["post-success"]);
const props = defineProps({
target: {
type: [String, Number, Object],
required: true,
},
articleId: {
type: [String, Number, Object],
default: null,
},
});
async function onPost() {
showLoadingToast({
message: "加载中...",
forbidClick: true, //禁止背景点击
});
// 找到数据接口
// 封装请求方法
// 请求提交数据
const { data } = await addComment({
target: props.target.toString(), //评论的目标id(评论文章即为文章id,对评论进行回复则为评论id)
content: message.value, //评论内容
art_id: props.articleId ? props.articleId.toString() : null, //文章id,对评论内容发表回复时,需要传递此参数,表明所属文章id。对文章进行评论,不要传此参数。
});
console.log(data);
emit("post-success", data.data.new_obj);
// 处理响应结果
showSuccessToast("发布成功");
//发布成功,清空文本框内容
message.value = "";
}
</script>
3.5展示文章评论总数量
在子组件article-comment.vue中向父组件src/views/article/index.vue发送评论总数量数据
在父组件中添加评论总数量,自定义事件监听评论总数量
发布成功更新评论的总数量
3.6评论回复
准备回复弹出层
1、添加数据用来控制展示回复弹层的显示状态
const isReplyShow = ref(false); //控制回复的显示状态
2、在详情页中添加使用弹层组件
<!-- 评论回复 -->
<van-popup
:style="{ height: '95%' }"
v-model:show="isReplyShow"
position="bottom"
>
</van-popup>
二、当点击评论项组件中的回复按钮的时候展示弹层
1、在 comment-item.vue 组件中点击回复按钮的时候,对外发布自定义事件
<van-button
@click="$emit('reply-click', comment)"
class="reply-btn"
round
size="mini"
>{{ comment.reply_count }} 回复</van-button
>
2、在article-comment.vue中使用对外发布自定义事件
<CommentItem
v-for="(comment, index) in list"
:key="index"
:comment="comment"
@reply-click="$emit('reply-click', $event)"
></CommentItem>
3.在详情页组件index.vue中使用的位置监听处理
<!-- 文章评论 -->
<ArticleComment
@update-total-count="totalCommentCount = $event"
:list="commentList"
:source="articleId"
@reply-click="onReplyClick"
></ArticleComment>
function onReplyClick(comment) {
console.log("onReplyClick", comment);
ReplyComment.value = comment;
// 展示回复内容
isReplyShow.value = true;
}
4.封装comment-reply.vue组件
<template>
<div class="comment-reply">
<van-nav-bar :title="`${comment.reply_count}条回复`">
<template #left>
<van-icon name="cross" @click="$emit('close')"></van-icon>
</template>
</van-nav-bar>
</div>
</template>
处理当前评论项
一、让 comment-reply.vue 组件拿到点击回复的评论对象
1、在 comment-item.vue 组件中点击回复按钮的时候把评论对象给传出来
<van-button
@click="$emit('reply-click', comment)"
class="reply-btn"
round
size="mini"
>{{ comment.reply_count }} 回复</van-button
>
2、在文章详情组件中接收处理
const ReplyComment = ref({}); //当前回复评论对象
function onReplyClick(comment) {
console.log("onReplyClick", comment);
ReplyComment.value = comment;
// 展示回复内容
isReplyShow.value = true;
}
3、在详情组件中将 ReplyComment传递给 comment-reply.vue 组件
<!-- 评论回复 -->
<van-popup
:style="{ height: '95%' }"
v-model:show="isReplyShow"
position="bottom"
>
<CommentReply
@close="isReplyShow = false"
:comment="ReplyComment"
></CommentReply>
</van-popup>
4、在 comment-reply.vue 组件中声明接收
const props = defineProps({
comment: {
type: Object,
required: true,
},
});
二、在 comment-reply.vue 组件中展示当前评论
1、加载注册 comment-item.vue 组件
2、使用展示
<template>
<div class="comment-reply">
<van-nav-bar :title="`${comment.reply_count}条回复`">
<template #left>
<van-icon name="cross" @click="$emit('close')"></van-icon>
</template>
</van-nav-bar>
<!-- 当前评论项 -->
<CommentItem :comment="comment"></CommentItem>
</div>
</template>
测试:点击不同的评论回复按钮,查看子组件中的 props 数据 comment 是否是当前点击回复所在的评论对象。
三、数据绑定:在评论回复组件中展示当前评论
<ArticleComment :source="comment.com_id" type="c"></ArticleComment>
import CommentItem from "./comment-item.vue";
展示评论回复列表
基本思路:
回复列表和文章的评论列表几乎是一样的
重用把之前封装的评论列表
<!-- 当前评论回复 -->
<van-cell title="所有回复"></van-cell>
<ArticleComment :source="comment.com_id" type="c"></ArticleComment>
import ArticleComment from "./article-comment.vue";
article-comment.vue
const props = defineProps({
// 如果获取文章评论,则传文章id
// 如果获取评论回复,则传评论id
source: {
type: [Number, String, Object],
required: true,
},
type: {
type: String,
default: "a",
},
list: {
type: Array,
// 数组或对象的默认值必须通过函数返回
default: function () {
return [];
},
},
});
// 1.请求获取数据
const { data } = await getComments({
type: props.type, //评论类型,a-对文章(article)的评论,c-对评论(comment)的回复
source: props.source, //源id,文章id或评论id
offset: offset.value, //获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据
limit: limit.value, //获取的评论数据个数,不传表示采用后端服务设定的默认每页数据量
});
3.7解决弹层中组件内容不更新问题
弹层组件:
如果初始的条件是 false,则弹层的内容不会渲染
程序运行期间,当条件变为 true 的时候,弹层才渲染了内容
之后切换弹层的展示,弹层只是通过 CSS 控制隐藏和显示
弹层渲染出来以后就只是简单的切换显示和隐藏,里面的内容也不再重新渲染了,所以会导致我们的评论的回复列表不会动态更新了。解决办法就是在每次弹层显示的时候重新渲染组件。
<CommentReply
v-if="isReplyShow"
@close="isReplyShow = false"
:comment="ReplyComment"
></CommentReply>
更多推荐
所有评论(0)