vite+vant+vue3新闻客户端app(三)

一、文章搜索

1.1创建组件并配置路由

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

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

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

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

vite+vant+vue3新闻客户端app(三)_第1张图片

1.2页面布局

Search 组件提供了 searchcancel 事件,search 事件在点击键盘上的搜索/回车按钮后触发,cancel 事件在点击搜索框右侧取消按钮时触发。

Tips: 在 van-search 外层增加 form 标签,且 action 不为空,即可在 iOS 输入法中显示搜索按钮。

1、创建 src/views/search/components/search-suggestion.vue






2、创建 src/views/search/components/search-history.vue






3、创建 src/views/search/components/search-result.vue






4、搜索组件内容如下:






1.3处理页面显示状态

1、添加数据用来控制搜索结果的显示状态

const isResultShow = ref(false); //控制搜索结果的显示状态

2、在模板中绑定条件渲染

 
    
    
    
    
    

1.4搜索联想建议

基本思路:

  • 当搜索框输入内容的时候,请求加载联想建议的数据

  • 将请求得到的结果绑定到模板中

基本功能

一、将父组件中搜索输入框的内容传给联想建议子组件

vite+vant+vue3新闻客户端app(三)_第2张图片

二、在子组件中监视搜索框输入内容的变化,如果变化则请求获取联想建议数据

https://cn.vuejs.org/guide/essentials/watchers.html#basic-example

vite+vant+vue3新闻客户端app(三)_第3张图片

三、将获取到的联想建议数据展示到列表中

vite+vant+vue3新闻客户端app(三)_第4张图片

搜索关键字高亮

1、加一个方法处理高亮

function highlight(str) {
  // console.log(str);
  // RegExp是正则表达式的构造函数
  // 参数1:字符串
  // 参数2:匹配模式
  // 参数3:正则对象
  return str.replace(
    new RegExp(props.searchText, "gi"),
    `${props.searchText}`
  );
}

2、然后在联想建议列表项中绑定调用

 
      
    

1.5搜索结果

思路:

  • 找到数据接口

  • 请求获取数据

  • 将数据展示到模板中

一、获取搜索关键字

1、声明接收父组件中的搜索框输入的内容

 
     

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、请求获取

三、最后,模板绑定

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、声明接收父组件中的历史记录的内容

     

2、父组件给子组件传递数据

const props = defineProps({
  searchHistories: {
    type: Array,
    required: true,
  },
});

3、模板绑定

删除历史记录

基本思路:

  • 给历史记录中的每一项注册点击事件

  • 在处理函数中判断

  • 如果是删除状态,则执行删除操作

  • 如果是非删除状态,则执行搜索操作

一、处理删除相关元素的展示状态

1、添加一个数据用来控制删除相关元素的显示状态

const isDeleteShow = ref(false); //删除的显示状态

2、绑定使用


      
全部删除 完成

二、处理删除操作

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 监视统一存储数据

vite+vant+vue3新闻客户端app(三)_第5张图片

2、初始化的时候从本地存储获取数据

const searchHistories = ref(getItem("search-histories") || []); //搜索历史数据

二、文章详情

2.1创建组件并配置路由

1、创建 views/article/index.vue 组件





2、然后将该页面配置到根级路由

路由 props 传参

 {
        path: '/article/:articleId',
        name: 'article',
        component: Article,
        // 将动态路由的参数映射到组件的props中,无论是访问还是维护性都很方便
        props:true
    },

2.2页面布局

使用到的 Vant 中的组件:

  • NavBar 导航栏

  • Loading 加载

  • Cell 单元格

  • Button 按钮

  • Image 图片

  • Divider 分割线

  • Icon 图标

markdown-css:

https://gitee.com/ylx252/github-markdown-css/blob/gh-pages/github-markdown.css







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 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

参考链接:

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt

  • http://es6.ruanyifeng.com/#docs/number#BigInt-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B

2.4展示文章详情

思路:

  • 找到数据接口

  • 封装请求方法

  • 请求获取数据

  • 模板绑定

一、请求并展示文章详情

1、在 api/article.js 中新增封装接口方法

// 获取新闻详情
export const getArticleById= article_id =>{
    return request({
        method:'GET',
        url:`/v1_0/articles/${article_id}`,       
    })
}

2、在组件中调用获取文章详情

3、模板绑定

2.5处理内容加载状态

需求:

  • 加载中,显示loading

  • 加载成功,显示文章详情

  • 加载失败,显示错误提示

  • 如果404,提示资源不存在

  • 其他的,提示加载失败,用户可以点击重试重新加载

2.6关于文章正文的样式

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

  • 将 github-markdown-css 样式文件下载到项目中

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

2.7图片点击预览

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

一、ImagePreview 图片预览的使用

二、处理图片点击预览

思路:

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

vite+vant+vue3新闻客户端app(三)_第6张图片
const articleContent = ref(null); // 获取文章内容DOM容器
vite+vant+vue3新闻客户端app(三)_第7张图片
  1. 获取文章内容中所有图片地址

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

  1. 在img点击事件处理函数中,调用ImagePreview 预览

vite+vant+vue3新闻客户端app(三)_第8张图片

2.8关注用户

注意:自己不能关注自己,登录账号和发布文章账号不能是同一个。

vite+vant+vue3新闻客户端app(三)_第9张图片

思路:

  • 给按钮注册点击事件

  • 在事件处理函数中

  • 如果已关注,则取消关注

  • 如果没有关注,则添加关注

功能处理

  • 找到数据接口

  • 封装请求方法

  • 请求调用

  • 视图更新

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、给关注/取消关注按钮注册点击事件

vite+vant+vue3新闻客户端app(三)_第10张图片

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、给收藏按钮注册点击事件

vite+vant+vue3新闻客户端app(三)_第11张图片

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、给点赞按钮注册点击事件

vite+vant+vue3新闻客户端app(三)_第12张图片

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







2、在文章详情页面中加载注册文章评论子组件

import ArticleComment from "~/views/article/components/article-comment.vue";

3、在文章详情页面的加载失败提示消息后面使用文章评论子组件


      

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


      

import CommentItem from "./comment-item.vue";

comment-item.vue



  




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、然后给评论项注册点击事件

 

3、在事件处理函数中

3.4发布文章评论

准备弹出层,封装组件

设置 maxlengthshow-word-limit 属性后会在底部显示字数统计。

autosize:是否自适应内容高度,只对 textarea 有效,可传入对象,如 { maxHeight: 100, minHeight: 50 },单位为px

post-comment.vue






步骤:

  • 注册发布点击事件

  • 请求提交表单

  • 根据响应结果进行后续处理

一、使用弹层展示发布评论

1、添加弹层组件

 
    
      
    

import PostComment from "./components/post-comment.vue";
const isPostShow = ref(false); //控制发布评论的显示状态

2、点击发评论按钮的时候显示弹层

 
        写评论
      

二、发布评论

1、在 api/comment.js 中添加封装数据接口

//对文章或者评论进行评论
export const addComment= (data) =>{
    return request({
        method:'POST',
        url:`/v1_0/comments`,
        data
    })
}

2、绑定获取添加评论的输入框数据并且注册发布按钮的点击事件

post-comment.vue中

 

const message = ref("");

3、在事件处理函数中

3.5展示文章评论总数量

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

vite+vant+vue3新闻客户端app(三)_第13张图片
vite+vant+vue3新闻客户端app(三)_第14张图片
  1. 在父组件中添加评论总数量,自定义事件监听评论总数量

vite+vant+vue3新闻客户端app(三)_第15张图片
vite+vant+vue3新闻客户端app(三)_第16张图片
  1. 发布成功更新评论的总数量

vite+vant+vue3新闻客户端app(三)_第17张图片
vite+vant+vue3新闻客户端app(三)_第18张图片

3.6评论回复

准备回复弹出层

1、添加数据用来控制展示回复弹层的显示状态

const isReplyShow = ref(false); //控制回复的显示状态

2、在详情页中添加使用弹层组件


    
    

二、当点击评论项组件中的回复按钮的时候展示弹层

1、在 comment-item.vue 组件中点击回复按钮的时候,对外发布自定义事件

{{ comment.reply_count }} 回复

2、在article-comment.vue中使用对外发布自定义事件

 

3.在详情页组件index.vue中使用的位置监听处理


      

function onReplyClick(comment) {
  console.log("onReplyClick", comment);
  ReplyComment.value = comment;
  // 展示回复内容
  isReplyShow.value = true;
}

4.封装comment-reply.vue组件


处理当前评论项

一、让 comment-reply.vue 组件拿到点击回复的评论对象

1、在 comment-item.vue 组件中点击回复按钮的时候把评论对象给传出来

{{ comment.reply_count }} 回复

2、在文章详情组件中接收处理

const ReplyComment = ref({}); //当前回复评论对象

function onReplyClick(comment) {
  console.log("onReplyClick", comment);
  ReplyComment.value = comment;
  // 展示回复内容
  isReplyShow.value = true;
}

3、在详情组件中将 ReplyComment传递给 comment-reply.vue 组件

 
    
      
    

4、在 comment-reply.vue 组件中声明接收

const props = defineProps({
  comment: {
    type: Object,
    required: true,
  },
});

二、在 comment-reply.vue 组件中展示当前评论

1、加载注册 comment-item.vue 组件

2、使用展示

测试:点击不同的评论回复按钮,查看子组件中的 props 数据 comment 是否是当前点击回复所在的评论对象。

三、数据绑定:在评论回复组件中展示当前评论

    

import CommentItem from "./comment-item.vue";

展示评论回复列表

基本思路:

  • 回复列表和文章的评论列表几乎是一样的

  • 重用把之前封装的评论列表

 
    
    

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 控制隐藏和显示

弹层渲染出来以后就只是简单的切换显示和隐藏,里面的内容也不再重新渲染了,所以会导致我们的评论的回复列表不会动态更新了。解决办法就是在每次弹层显示的时候重新渲染组件。

你可能感兴趣的:(前端)