1、创建 src/views/search/index.vue
2、然后把搜索页面的路由配置到根组件路由(一级路由)
{
path: '/search',
name: 'search',
component: Search
},
3、在home.vue配置路由跳转
Search 组件提供了 search 和 cancel 事件,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、添加数据用来控制搜索结果的显示状态
const isResultShow = ref(false); //控制搜索结果的显示状态
2、在模板中绑定条件渲染
基本思路:
当搜索框输入内容的时候,请求加载联想建议的数据
将请求得到的结果绑定到模板中
一、将父组件中搜索输入框的内容传给联想建议子组件
二、在子组件中监视搜索框输入内容的变化,如果变化则请求获取联想建议数据
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"),
`${props.searchText}`
);
}
2、然后在联想建议列表项中绑定调用
思路:
找到数据接口
请求获取数据
将数据展示到模板中
一、获取搜索关键字
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、添加一个数据用来存储历史记录
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 监视统一存储数据
2、初始化的时候从本地存储获取数据
const searchHistories = ref(getItem("search-histories") || []); //搜索历史数据
1、创建 views/article/index.vue 组件
文章详情
2、然后将该页面配置到根级路由
路由 props 传参
{
path: '/article/:articleId',
name: 'article',
component: Article,
// 将动态路由的参数映射到组件的props中,无论是访问还是维护性都很方便
props:true
},
使用到的 Vant 中的组件:
NavBar 导航栏
Loading 加载
Cell 单元格
Button 按钮
Image 图片
Divider 分割线
Icon 图标
markdown-css:
https://gitee.com/ylx252/github-markdown-css/blob/gh-pages/github-markdown.css
之所以请求文章详情返回 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
思路:
找到数据接口
封装请求方法
请求获取数据
模板绑定
一、请求并展示文章详情
1、在 api/article.js 中新增封装接口方法
// 获取新闻详情
export const getArticleById= article_id =>{
return request({
method:'GET',
url:`/v1_0/articles/${article_id}`,
})
}
2、在组件中调用获取文章详情
3、模板绑定
{{ $store.state.article.title }}
{{ $store.state.article.aut_name }}
{{ $store.state.article.pubdate }}
{{ $store.state.article.is_followed ? "已关注" : "关注" }}
需求:
加载中,显示loading
加载成功,显示文章详情
加载失败,显示错误提示
如果404,提示资源不存在
其他的,提示加载失败,用户可以点击重试重新加载
文章正文包括各种数据:段落、标题、列表、链接、图片、视频等资源。
将 github-markdown-css 样式文件下载到项目中
配置不要转换样式文件中的字号
https://vant-ui.github.io/vant/#/zh-CN/image-preview
一、ImagePreview 图片预览的使用
二、处理图片点击预览
思路:
从文章内容中获取所有的img DOM节点
const articleContent = ref(null); // 获取文章内容DOM容器
获取文章内容中所有图片地址
遍历所有img节点,给每个节点注册点击事件
在img点击事件处理函数中,调用ImagePreview 预览
注意:自己不能关注自己,登录账号和发布文章账号不能是同一个。
思路:
给按钮注册点击事件
在事件处理函数中
如果已关注,则取消关注
如果没有关注,则添加关注
找到数据接口
封装请求方法
请求调用
视图更新
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;
}
两个作用:
交互反馈
防止网络慢用户多次点击按钮导致重复触发点击事件
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 ? "" : "取消"}收藏成功`);
}
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 ? "" : "取消"}点赞成功`
);
}
为了更好的开发和维护,这里我们把文章评论单独封装到一个组件中来处理。
1、创建 src/views/article/components/article-comment.vue
2、在文章详情页面中加载注册文章评论子组件
import ArticleComment from "~/views/article/components/article-comment.vue";
3、在文章详情页面的加载失败提示消息后面使用文章评论子组件
步骤:
封装接口
请求获取数据
处理模板
实现:
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
{{ comment.aut_name }}
{{ comment.content }}
{{ comment.pubdate }}
回复
12
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、然后给评论项注册点击事件
{{ comment.like_count }}
3、在事件处理函数中
设置 maxlength 和 show-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、在事件处理函数中
在子组件article-comment.vue中向父组件src/views/article/index.vue发送评论总数量数据
在父组件中添加评论总数量,自定义事件监听评论总数量
发布成功更新评论的总数量
1、添加数据用来控制展示回复弹层的显示状态
const isReplyShow = ref(false); //控制回复的显示状态
2、在详情页中添加使用弹层组件
二、当点击评论项组件中的回复按钮的时候展示弹层
1、在 comment-item.vue 组件中点击回复按钮的时候,对外发布自定义事件
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 组件中点击回复按钮的时候把评论对象给传出来
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, //获取的评论数据个数,不传表示采用后端服务设定的默认每页数据量
});
弹层组件:
如果初始的条件是 false,则弹层的内容不会渲染
程序运行期间,当条件变为 true 的时候,弹层才渲染了内容
之后切换弹层的展示,弹层只是通过 CSS 控制隐藏和显示
弹层渲染出来以后就只是简单的切换显示和隐藏,里面的内容也不再重新渲染了,所以会导致我们的评论的回复列表不会动态更新了。解决办法就是在每次弹层显示的时候重新渲染组件。