一、文章搜索

1.1创建组件并配置路由

1、创建 src/views/search/index.vue

2、然后把搜索页面的路由配置到根组件路由(一级路由)

 {
        path: '/search',
        name: 'search',
        component: Search
    },

3、在home.vue配置路由跳转

1.2页面布局

Search 组件提供了 searchcancel 事件,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、然后将该页面配置到根级路由

路由 props 传参

 {
        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关于文章正文的样式

文章正文包括各种数据:段落、标题、列表、链接、图片、视频等资源。

  • 配置不要转换样式文件中的字号

2.7图片点击预览

https://vant-ui.github.io/vant/#/zh-CN/image-preview

一、ImagePreview 图片预览的使用

二、处理图片点击预览

思路:

  1. 从文章内容中获取所有的img DOM节点

const articleContent = ref(null); // 获取文章内容DOM容器
  1. 获取文章内容中所有图片地址

  1. 遍历所有img节点,给每个节点注册点击事件

  1. 在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发布文章评论

准备弹出层,封装组件

设置 maxlengthshow-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展示文章评论总数量

  1. 在子组件article-comment.vue中向父组件src/views/article/index.vue发送评论总数量数据

  1. 在父组件中添加评论总数量,自定义事件监听评论总数量

  1. 发布成功更新评论的总数量

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>

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐