五、首页—频道编辑

处理页面弹出层

Vant 中内置了 Popup 弹出层 组件。

1、在 data中添加一个数据用来控制弹层的显示和隐藏

data () {return {...isChannelEditShow: true // 这里我们先设置为 true 就能看到弹窗的页面了}
}

2、然后在首页模板中的频道列表后面添加弹出层组件

<!-- 频道编辑 -->
<van-popupclass="edit-channel-popup"v-model="isEditChannelShow"position="bottom":style="{ height: '100%' }"closeableclose-icon-position="top-left"
>内容</van-popup>
<!-- /频道编辑 -->
.edit-channel-popup {padding-top: 100px;box-sizing: border-box;
}

测试查看结果。

创建频道编辑组件

1、创建 views/home/components/channel-edit.vue

<template><div class="channel-edit">频道编辑</div>
</template><script>
export default {name: 'ChannelEdit',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less"></style>

2、在首页中加载注册

import ChannelEdit from './components/channel-edit'
export default {...components: {...ChannelEdit}
}

3、在弹出层中使用频道编辑组件

<!-- 频道编辑 -->
<van-popupv-model="isChannelEditShow"position="bottom"closeableclose-icon-position="top-left":style="{ height: '100%' }"
>
+  <channel-edit />
</van-popup>
<!-- /频道编辑 -->

页面布局

<template><div class="channel-edit"><van-cell title="我的频道" :border="false"><van-buttonsize="mini"roundtype="danger"plain>编辑</van-button></van-cell><van-grid :gutter="10"><van-grid-itemclass="channel-item"v-for="value in 8":key="value"text="文字"/></van-grid><van-cell title="频道推荐" :border="false"></van-cell><van-grid :gutter="10"><van-grid-itemclass="channel-item"v-for="value in 8":key="value"text="文字"/></van-grid></div>
</template><script>
export default {name: 'ChannelEdit',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less">
.channel-edit {.channel-item {height: 86px;/deep/ .van-grid-item__content {background-color: #f5f5f6;.van-grid-item__text {color: #222;font-size: 28px;}}}
}
</style>

展示我的频道

1、在父组件中把 channels 传递给频道编辑组件

2、在频道编辑组件中声明接收父组件的 userChannels 频道列表数据并遍历展示

展示推荐频道列表

没有用来获取推荐频道的数据接口,但是我们有获取所有频道列表的数据接口。

所以:所有频道列表 - 我的频道 = 剩余推荐的频道

实现过程所以一共分为两大步:

  • 获取所有频道
  • 基于所有频道和我的频道计算获取剩余的推荐频道

获取所有频道

1、封装数据接口

/*** 获取所有频道*/
export const getAllChannels = () => {return request({method: 'GET',url: '/app/v1_0/channels'})
}

2、在编辑频道组件中请求获取所有频道数据

3、在调试工具中测试是否有拿到数据

处理展示推荐频道

思路:所有频道 - 用户频道 = 推荐频道

1、封装计算属性筛选数据

  • 遍历所有频道
  • 对每一个频道都判断:该频道是否属于我的频道
  • 如果不属于我的频道,则收集起来
  • 直到遍历结束,剩下来就是那些剩余的推荐频道

2、模板绑定

添加频道

思路:

  • 给推荐频道列表中每一项注册点击事件
  • 获取点击的频道项
  • 将频道项添加到我的频道中
  • 将当前点击的频道项从推荐频道中移除
    • 不需要删除,因为我们获取数据使用的是计算属性,当我频道发生改变,计算属性重新求值了

1、给推荐频道中的频道注册点击事件

2、在添加频道事件处理函数中

然后你会神奇的发现点击的那个推荐频道跑到我的频道中了,我们并没有去手动的删除点击的这个推荐频道,但是它没了!主要是因为推荐频道是通过一个计算属性获取的,计算属性中使用了 channels(我的频道)数据,所以只要我的频道中的数据发生变化,那么计算属性就会重新运算获取最新的数据。

编辑频道

思路:

  • 给我的频道中的频道项注册点击事件
  • 在事件处理函数中
    • 如果是编辑状态,则执行删除频道操作
    • 如果是非编辑状态,则执行切换频道操作

处理编辑状态

1、在 data 中添加数据用来控制编辑状态的显示

2、在我的频道项中添加删除图标

3、处理点击编辑按钮

切换频道

功能需求:在非编辑器状态下切换频道。

1、给我的频道项注册点击事件

2、处理函数

3、在父组件中监听处理自定义事件

让激活频道高亮

思路:

  • 将首页中的激活的标签索引传递给频道编辑组件
  • 在频道编辑组件中遍历我的频道列表的时候判断遍历项的索引是否等于激活的频道标签索引,如果一样则作用一个高亮的 CSS 类名

1、将首页组件中的 active 传递到频道编辑组件中

2、在频道编辑组件中声明 props 接收

3、判断遍历项,如果 遍历项索引 === active,则给这个频道项设置高亮样式

删除频道

功能需求:在编辑状态下删除频道。

频道数据持久化

业务分析

频道编辑这个功能,无论用户是否登录用户都可以使用。

不登录也能使用

  • 数据存储在本地
  • 不支持同步功能

登录也能使用

  • 数据存储在线上后台服务器
  • 更换不同的设备可以同步数据

添加频道

思路:

  • 如果未登录,则存储到本地
  • 如果已登录,则存储到线上
    • 找到数据接口
    • 封装请求方法
    • 请求调用

1、封装添加频道的请求方法

/*** 添加用户频道*/
export const addUserChannel = channels => {return request({method: 'PATCH',url: '/app/v1_0/user/channels',data: {channels}})
}

2、修改添加频道的处理逻辑

async onAddChannel (channel) {try {this.userChannels.push(channel)if (this.user) {// 已登录,数据存储到线上await addUserChannel([{id: channel.id, // 频道 idseq: this.userChannels.length // 频道的 序号}])} else {// 未登录,数据存储到本地setItem('channels', this.userChannels)}} catch (err) {console.log(err)this.$toast('添加频道失败')}
},

删除频道

思路:

  • 如果未登录,则存储到本地
  • 如果已登录,则存储到线上
    • 找到数据接口
    • 封装请求方法
    • 请求调用

1、封装删除用户频道请求方法

/*** 删除用户频道*/
export const deleteUserChannel = channelId => {return request({method: 'DELETE',url: `/app/v1_0/user/channels/${channelId}`})
}

2、修改删除频道的处理逻辑

async deleteChannel (channel) {try {if (this.user) {// 已登录,将数据存储到线上await deleteUserChannel(channel.id)} else {// 未登录,将数据存储到本地setItem('channles', this.userChannels)}} catch (err) {console.log(err)this.$toast('删除频道失败,请稍后重试')}
}

正确的获取首页频道列表数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-At2CxmGV-1659971681941)(assets/首页-获取频道列表.png)]

提示:获取登录用户的频道列表和获取默认推荐的频道列表是同一个数据接口。后端会根据接口中的 token 来判定返回数据。

async loadChannels () {try {let channels = []if (this.user) {// 已登录,请求获取线上的频道数据const { data } = await getChannels()channels = data.data.channels} else {// 未登录const localChannels = getItem('channels')if (localChannels) {// 有本地频道数据,则使用channels = localChannels} else {// 没有本地频道数据,则请求获取默认推荐的频道列表const { data } = await getChannels()channels = data.data.channels}}// 将数据更新到组件中this.channels = channels} catch (err) {console.log(err)this.$toast('数据获取失败')}
},

六、文章搜索

创建组件并配置路由

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

<template><div class="search-container">搜索页面</div>
</template><script>export default {name: "SearchPage",components: {},props: {},data() {return {};},computed: {},watch: {},created() {},methods: {}};
</script><style scoped></style>

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

{path: '/search',omponent: Search
}

最后访问 /search 测试。

页面布局

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

<template><div class="search-history"><van-cell title="搜索历史"><span>全部删除</span><span>完成</span><van-icon name="delete" /></van-cell><van-cell title="hello"><van-icon name="close" /></van-cell><van-cell title="hello"><van-icon name="close" /></van-cell><van-cell title="hello"><van-icon name="close" /></van-cell><van-cell title="hello"><van-icon name="close" /></van-cell></div>
</template><script>
export default {name: 'SearchHistory',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less"></style>

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

<template><div class="search-suggestion"><van-cell title="黑马程序员..." icon="search"></van-cell><van-cell title="黑马程序员..." icon="search"></van-cell><van-cell title="黑马程序员..." icon="search"></van-cell><van-cell title="黑马程序员..." icon="search"></van-cell><van-cell title="黑马程序员..." icon="search"></van-cell></div>
</template><script>
export default {name: 'SearchSuggestion',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less"></style>

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

<template><div class="search-result"><van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"><van-cell v-for="item in list" :key="item" :title="item" /></van-list></div>
</template><script>
export default {name: 'SearchResult',components: {},props: {},data () {return {list: [],loading: false,finished: false}},computed: {},watch: {},created () {},mounted () {},methods: {onLoad () {// 异步更新数据// setTimeout 仅做示例,真实场景中一般为 ajax 请求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}}, 1000)}}
}
</script><style scoped lang="less"></style>

4、搜索组件内容如下:

<template><div class="search-container"><!-- 搜索栏 --><!--Tips: 在 van-search 外层增加 form 标签,且 action 不为空,即可在 iOS 输入法中显示搜索按钮--><form action="/"><van-searchv-model="searchText"show-actionplaceholder="请输入搜索关键词"background="#3296fa"@search="onSearch"@cancel="onCancel"/></form><!-- /搜索栏 --><!-- 搜索历史记录 --><search-history /><!-- /搜索历史记录 --><!-- 联想建议 --><search-suggestion /><!-- /联想建议 --><!-- 历史记录 --><search-result /><!-- /历史记录 --></div>
</template><script>
import SearchHistory from './components/search-history'
import SearchSuggestion from './components/search-suggestion'
import SearchResult from './components/search-result'export default {name: 'SearchIndex',components: {SearchHistory,SearchSuggestion,SearchResult},props: {},data () {return {searchText: ''}},computed: {},watch: {},created () {},mounted () {},methods: {onSearch (val) {console.log(val)},onCancel () {this.$router.back()}}
}
</script><style scoped lang="less">
.search-container {.van-search__action {color: #fff;}
}
</style>

处理页面显示状态

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

data () {...isResultShow: false
}

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

<!-- 搜索结果 -->
<search-result v-if="isResultShow" />
<!-- /搜索结果 --><!-- 联想建议 -->
<search-suggestion v-else-if="searchText" />
<!-- /联想建议 --><!-- 搜索历史记录 -->
<search-history v-else />
<!-- /搜索历史记录 -->

搜索联想建议

基本思路:

  • 当搜索框输入内容的时候,请求加载联想建议的数据
  • 将请求得到的结果绑定到模板中

基本功能

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

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

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

防抖优化

1、安装 lodash

# yarn add lodash
npm i lodash

2、防抖处理

// lodash 支持按需加载,有利于打包结果优化
import { debounce } from "lodash"

不建议下面这样使用,因为这样会加载整个模块。

import _ from 'lodash'
_.debounce()
// debounce 函数
// 参数1:函数
// 参数2:防抖时间
// 返回值:防抖之后的函数,和参数1功能是一样的
onSearchInput: debounce(async function () {const searchContent = this.searchContentif (!searchContent) {return}// 1. 请求获取数据const { data } = await getSuggestions(searchContent)// 2. 将数据添加到组件实例中this.suggestions = data.data.options// 3. 模板绑定
}, 200),

搜索关键字高亮

如何将字符串中的指定字符在网页中高亮展示?

"Hello World";

将需要高亮的字符包裹 HTML 标签,为其单独设置颜色。

"Hello <span style="color: red">World</span>"

在 Vue 中如何渲染带有 HTML 标签的字符串?

data () {return {htmlStr: 'Hello <span style="color: red">World</span>'}
}
<div>{{ htmlStr }}</div>
<div v-html="htmlStr"></div>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3GZ2Dk8J-1659971703945)(assets/image-20200112154732044.png)]

如何把字符串中指定字符统一替换为高亮(包裹了 HTML)的字符?

const str = "Hello World"// 结果:<span style="color: red">Hello</span> World
"Hello World".replace('Hello', '<span style="color: red">Hello</span>')// 需要注意的是,replace 方法的字符串匹配只能替换第1个满足的字符
// <span style="color: red">Hello</span> World Hello abc
"Hello World Hello abc".replace('Hello', '<span style="color: red">Hello</span>')// 如果想要全文替换,使用正则表达式
// g 全局
// i 忽略大小写
// <span style="color: red">Hello</span> World <span style="color: red">Hello</span> abc
"Hello World Hello abc".replace(/Hello/gi, '<span style="color: red">Hello</span>')

一个小扩展:使用字符串的 split 结合数组的 join 方法实现高亮

var str = "hello world 你好 hello";// ["", " world 你好 ", ""]
const arr = str.split("hello");// "<span>hello</span> world 你好 <span>hello</span>"
arr.join("<span>hello</span>");

下面是具体的处理。

1、在 methods 中添加一个方法处理高亮

// 参数 source: 原始字符串
// 参数 keyword: 需要高亮的关键词
// 返回值:替换之后的高亮字符串
highlight (source, keyword) {// /searchContent/ 正则表达式中的一切内容都会当做字符串使用// 这里可以 new RegExp 方式根据字符串创建一个正则表达式// RegExp 是原生 JavaScript 的内置构造函数// 参数1:字符串,注意,这里不要加 //// 参数2:匹配模式,g 全局,i 忽略大小写const reg = new RegExp(keyword, 'gi')return source.replace(reg, `<span style="color: #3296fa">${keyword}</span>`)
},

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

<!-- 联想建议 -->
<van-cell-group v-else-if="searchContent"><van-cellicon="search"v-for="(item, index) in suggestions":key="index"@click="onSearch(item)"><div slot="title" v-html="highlight(item, searchContent)"></div></van-cell>
</van-cell-group>
<!-- /联想建议 -->

搜索结果

思路:

  • 找到数据接口
  • 请求获取数据
  • 将数据展示到模板中

一、获取搜索关键字

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

props: {q: {type: String,require: true}
},

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

<!-- 搜索结果 -->
<search-result v-if="isResultShow" :q="searchText" />
<!-- /搜索结果 -->

最后在调试工具中查看确认是否接收到 props 数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AK7yijVm-1659971703946)(assets/image-20200112162223915.png)]

二、请求获取数据

1、在 api/serach.js 添加封装获取搜索结果的请求方法

/*** 获取搜索结果*/
export function getSearch(params) {return request({method: "GET",url: "/app/v1_0/search",params})
}

2、请求获取

+ import { getSearch } from '@/api/search'export default {name: 'SearchResult',components: {},props: {q: {type: String,require: true}},data () {return {list: [],loading: false,finished: false,
+      page: 1,
+      perPage: 20}},computed: {},watch: {},created () {},mounted () {},methods: {+++    async onLoad () {// 1. 请求获取数据const { data } = await getSearch({page: this.page, // 页码per_page: this.perPage, // 每页大小q: this.q // 搜索关键字})// 2. 将数据添加到列表中const { results } = data.datathis.list.push(...results)// 3. 设置加载状态结束this.loading = false// 4. 判断数据是否加载完毕if (results.length) {this.page++ // 更新获取下一页数据的页码} else {this.finished = true // 没有数据了,将加载状态设置结束,不再 onLoad}}}
}

三、最后,模板绑定

<van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"
><van-cell
+    v-for="(article, index) in list"
+    :key="index"
+    :title="article.title"/>
</van-list>

搜索历史记录

添加历史记录

当发生搜索的时候我们才需要记录历史记录。

1、在 data 中添加一个数据用来存储历史记录

data () {return {...searchHistories: []}
}

2、在触发搜索的时候,记录历史记录

onSearch (val) {// 更新文本框内容this.searchText = val// 存储搜索历史记录// 要求:不要有重复历史记录、最新的排在最前面const index = this.searchHistories.indexOf(val)if (index !== -1) {this.searchHistories.splice(index, 1)}this.searchHistories.unshift(val)// 渲染搜索结果this.isResultShow = true
},

展示历史记录

<!-- 历史记录 -->
<van-cell-group v-else><van-cell title="历史记录"><van-icon name="delete" /><span>全部删除</span>&nbsp;&nbsp;<span>完成</span></van-cell><van-cell:title="item"v-for="(item, index) in searchHistories":key="index"><van-icon name="close"></van-icon></van-cell>
</van-cell-group>
<!-- /历史记录 -->

删除历史记录

基本思路:

  • 给历史记录中的每一项注册点击事件
  • 在处理函数中判断
    • 如果是删除状态,则执行删除操作
    • 如果是非删除状态,则执行搜索操作

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

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

data () {return {...isDeleteShow: false}
}

2、绑定使用

<!-- 历史记录 -->
<van-cell-group v-else><van-cell title="历史记录"><template v-if="isDeleteShow"><span @click="searchHistories = []">全部删除</span>&nbsp;&nbsp;<span @click="isDeleteShow = false">完成</span></template><van-icon v-else name="delete" @click="isDeleteShow = true"></van-icon></van-cell><van-cell:title="item"v-for="(item, index) in searchHistories":key="index"@click="onSearch(item)"><van-iconv-show="isDeleteShow"name="close"@click="searchHistories.splice(index, 1)"></van-icon></van-cell>
</van-cell-group>
<!-- /历史记录 -->

二、处理删除操作

<!-- 历史记录 -->
<van-cell-group v-else><van-cell title="历史记录"><template v-if="isDeleteShow">
+      <span @click="searchHistories = []">全部删除</span>&nbsp;&nbsp;<span @click="isDeleteShow = false">完成</span></template><van-icon v-else name="delete" @click="isDeleteShow = true" /></van-cell><van-cell:title="item"v-for="(item, index) in searchHistories":key="index"
+    @click="onHistoryClick(item, index)"><van-icon v-show="isDeleteShow" name="close"></van-icon></van-cell>
</van-cell-group>
<!-- /历史记录 -->
onHistoryClick (item, index) {// 如果是删除状态,则执行删除操作if (this.isDeleteShow) {this.searchHistories.splice(index, 1)} else {// 否则执行搜索操作this.onSearch(item)}
}

数据持久化

1、利用 watch 监视统一存储数据

watch: {searchHistories (val) {// 同步到本地存储setItem('serach-histories', val)}
},

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

data () {return {...searchHistories: getItem('serach-histories') || [],}
}

七、文章详情

创建组件并配置路由

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

<template><div class="article-container">文章详情</div>
</template><script>
export default {name: 'ArticleIndex',components: {},props: {articleId: {type: [Number, String],required: true}},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less"></style>

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

{path: '/article/:articleId',name: 'article',component: () => import('@/views/article'),// 将路由动态参数映射到组件的 props 中,更推荐这种做法props: true
}

官方文档:路由 props 传参

页面布局

使用到的 Vant 中的组件:

  • NavBar 导航栏

  • Loading 加载

  • Cell 单元格

  • Button 按钮

  • Image 图片

  • Divider 分割线

  • Icon 图标

<template><div class="article-container"><!-- 导航栏 --><van-nav-barclass="page-nav-bar"left-arrowtitle="黑马头条"></van-nav-bar><!-- /导航栏 --><div class="main-wrap"><!-- 加载中 --><div class="loading-wrap"><van-loadingcolor="#3296fa"vertical>加载中</van-loading></div><!-- /加载中 --><!-- 加载完成-文章详情 --><div class="article-detail"><!-- 文章标题 --><h1 class="article-title">这是文章标题</h1><!-- /文章标题 --><!-- 用户信息 --><van-cell class="user-info" center :border="false"><van-imageclass="avatar"slot="icon"roundfit="cover"src="https://img.yzcdn.cn/vant/cat.jpeg"/><div slot="title" class="user-name">黑马头条号</div><div slot="label" class="publish-date">14小时前</div><van-buttonclass="follow-btn"type="info"color="#3296fa"roundsize="small"icon="plus">关注</van-button><!-- <van-buttonclass="follow-btn"roundsize="small">已关注</van-button> --></van-cell><!-- /用户信息 --><!-- 文章内容 --><div class="article-content">这是文章内容</div><van-divider>正文结束</van-divider></div><!-- /加载完成-文章详情 --><!-- 加载失败:404 --><div class="error-wrap"><van-icon name="failure" /><p class="text">该资源不存在或已删除!</p></div><!-- /加载失败:404 --><!-- 加载失败:其它未知错误(例如网络原因或服务端异常) --><div class="error-wrap"><van-icon name="failure" /><p class="text">内容加载失败!</p><van-button class="retry-btn">点击重试</van-button></div><!-- /加载失败:其它未知错误(例如网络原因或服务端异常) --></div><!-- 底部区域 --><div class="article-bottom"><van-buttonclass="comment-btn"type="default"roundsize="small">写评论</van-button><van-iconname="comment-o"info="123"color="#777"/><van-iconcolor="#777"name="star-o"/><van-iconcolor="#777"name="good-job-o"/><van-icon name="share" color="#777777"></van-icon></div><!-- /底部区域 --></div>
</template><script>
export default {name: 'ArticleIndex',components: {},props: {articleId: {type: [Number, String],required: true}},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less">
.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>

关于后端返回数据中的大数字问题

之所以请求文章详情返回 404 是因为我们请求发送的文章 ID (article.art_id)不正确。

JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。

Math.pow(2, 53) // 90071992547409929007199254740992  // 9007199254740992
9007199254740993  // 9007199254740992Math.pow(2, 53) === Math.pow(2, 53) + 1
// true

上面代码中,超出 2 的 53 次方之后,一个数就不精确了。
ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// trueNumber.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

下面是使用它的一个简单示例。

const jsonStr = '{ "art_id": 1245953273786007552 }'console.log(JSON.parse(jsonStr)) // 1245953273786007600
// JSON.stringify()// JSONBig 可以处理数据中超出 JavaScript 安全整数范围的问题
console.log(JSONBig.parse(jsonStr)) // 把 JSON 格式的字符串转为 JavaScript 对象// 使用的时候需要把 BigNumber 类型的数据转为字符串来使用
console.log(JSONBig.parse(jsonStr).art_id.toString()) // 1245953273786007552console.log(JSON.stringify(JSONBig.parse(jsonStr)))console.log(JSONBig.stringify(JSONBig.parse(jsonStr))) // 把 JavaScript 对象 转为 JSON 格式的字符串转

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lrba5ik0-1659971714986)(assets/1582099315865-5e805425-7abf-4cf2-9df3-acc2ef8f9bb9.png)]

json-bigint 会把超出 JS 安全整数范围的数字转为一个 BigNumber 类型的对象,对象数据是它内部的一个算法处理之后的,我们要做的就是在使用的时候转为字符串来使用。

通过 Axios 请求得到的数据都是 Axios 处理(JSON.parse)之后的,我们应该在 Axios 执行处理之前手动使用 json-bigint 来解析处理。Axios 提供了自定义处理原始后端返回数据的 API:transformResponse

import axios from 'axios'import jsonBig from 'json-bigint'var json = '{ "value" : 9223372036854775807, "v2": 123 }'console.log(jsonBig.parse(json))const request = axios.create({baseURL: 'http://ttapi.research.itcast.cn/', // 接口基础路径// transformResponse 允许自定义原始的响应数据(字符串)transformResponse: [function (data) {try {// 如果转换成功则返回转换的数据结果return jsonBig.parse(data)} catch (err) {// 如果转换失败,则包装为统一数据格式并返回return {data}}}]
})export default request

扩展: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 中新增封装接口方法

/*** 根据 id 获取指定文章*/
export const getArticleById = articleId => {return request({method: 'GET',url: `/app/v1_0/articles/${articleId}`})
}

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

+ import { getArticleById } from '@/api/article'export default {name: 'ArticlePage',components: {},props: {articleId: {type: String,required: true}},data () {return {+      article: {} // 文章详情}},computed: {},watch: {},created () {+    this.loadArticle()},mounted () {},methods: {+++    async loadArticle () {try {const { data } = await getArticleById(this.articleId)this.article = data.data} catch (err) {console.log(err)}}}
}

3、模板绑定

处理内容加载状态

需求:

  • 加载中,显示 loading
  • 加载成功,显示文章详情
  • 加载失败,显示错误提示
    • 如果 404,提示资源不存在
    • 其它的,提示加载失败,用户可以点击重试重新加载

关于文章正文的样式

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

  • 将 github-markdown-css 样式文件下载到项目中
  • 配置不要转换样式文件中的字号

图片点击预览

一、ImagePreview 图片预览 的使用

二、处理图片点击预览

思路:

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

2、获取文章内容中所有的图片地址

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

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

关注用户

思路:

  • 给按钮注册点击事件
  • 在事件处理函数中
    • 如果已关注,则取消关注
    • 如果没有关注,则添加关注

下面是具体实现。

视图处理

功能处理

  • 找到数据接口
  • 封装请求方法
  • 请求调用
  • 视图更新

1、在 api/user.js 中添加封装请求方法

/*** 添加关注*/
export const addFollow = userId => {return request({method: 'POST',url: '/app/v1_0/user/followings',data: {target: userId}})
}/*** 取消关注*/
export const deleteFollow = userId => {return request({method: 'DELETE',url: `/app/v1_0/user/followings/${userId}`})
}

2、给关注/取消关注按钮注册点击事件

3、在事件处理函数中

import { addFollow, deleteFollow } from '@/api/user'
async onFollow () {// 开启按钮的 loading 状态this.isFollowLoading = truetry {// 如果已关注,则取消关注const authorId = this.article.aut_idif (this.article.is_followed) {await deleteFollow(authorId)} else {// 否则添加关注await addFollow(authorId)}// 更新视图this.article.is_followed = !this.article.is_followed} catch (err) {console.log(err)this.$toast.fail('操作失败')}// 关闭按钮的 loading 状态this.isFollowLoading = false
}

最后测试。

loading 效果

两个作用:

  • 交互反馈
  • 防止网络慢用户多次点击按钮导致重复触发点击事件

组件封装

文章收藏

该功能和关注用户的处理思路几乎一样,建议由学员自己编写。

封装组件

处理视图

功能处理

思路:

  • 给收藏按钮注册点击事件
  • 如果已经收藏了,则取消收藏
  • 如果没有收藏,则添加收藏

下面是具体实现。

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

/*** 收藏文章*/
export const addCollect = target => {return request({method: 'POST',url: '/app/v1_0/article/collections',data: {target}})
}/*** 取消收藏文章*/
export const deleteCollect = target => {return request({method: 'DELETE',url: `/app/v1_0/article/collections/${target}`})
}

2、给收藏按钮注册点击事件

3、处理函数

async onCollect () {// 这里 loading 不仅仅是为了交互提示,更重要的是请求期间禁用背景点击功能,防止用户不断的操作界面发出请求this.$toast.loading({duration: 0, // 持续展示 toastmessage: '操作中...',forbidClick: true // 是否禁止背景点击})try {// 如果已收藏,则取消收藏if (this.article.is_collected) {await deleteCollect(this.articleId)// this.article.is_collected = falsethis.$toast.success('取消收藏')} else {// 添加收藏await addCollect(this.articleId)// this.article.is_collected = truethis.$toast.success('收藏成功')}this.article.is_collected = !this.article.is_collected} catch (err) {console.log(err)this.$toast.fail('操作失败')}
}

文章点赞

该功能和关注用户的处理思路几乎一样,建议由学员自己编写。

article 中的 attitude 表示用户对文章的态度

  • -1 无态度
  • 0 不喜欢
  • 1 已点赞

思路:

  • 给点赞按钮注册点击事件
  • 如果已经点赞,则请求取消点赞
  • 如果没有点赞,则请求点赞

1、添加封装数据接口

/*** 点赞*/
export const addLike = articleId => {return request({method: 'POST',url: '/app/v1_0/article/likings',data: {target: articleId}})
}/*** 取消点赞*/
export const deleteLike = articleId => {return request({method: 'DELETE',url: `/app/v1_0/article/likings/${articleId}`})
}

2、给点赞按钮注册点击事件

3、处理函数

async onLike () {// 两个作用:1、交互提示 2、防止网络慢用户连续不断的点击按钮请求this.$toast.loading({duration: 0, // 持续展示 toastmessage: '操作中...',forbidClick: true // 是否禁止背景点击})try {// 如果已经点赞,则取消点赞if (this.article.attitude === 1) {await deleteLike(this.articleId)this.article.attitude = -1this.$toast.success('取消点赞')} else {// 否则添加点赞await addLike(this.articleId)this.article.attitude = 1this.$toast.success('点赞成功')}} catch (err) {console.log(err)this.$toast.fail('操作失败')}
}

八、文章评论

展示文章评论列表

准备组件

为了更好的开发和维护,这里我们把文章评论单独封装到一个组件中来处理。

1、创建 src/views/article/components/article-comment.vue

<template><div class="article-comments"><!-- 评论列表 --><van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"><van-cell v-for="item in list" :key="item" :title="item"><van-imageslot="icon"roundwidth="30"height="30"style="margin-right: 10px;"src="https://img.yzcdn.cn/vant/cat.jpeg"/><span style="color: #466b9d;" slot="title">hello</span><div slot="label"><p style="color: #363636;">我出去跟别人说我的是。。。</p><p><span style="margin-right: 10px;">3天前</span><van-button size="mini" type="default">回复</van-button></p></div><van-icon slot="right-icon" name="like-o" /></van-cell></van-list><!-- 评论列表 --><!-- 发布评论 --><van-cell-group class="publish-wrap"><van-field clearable placeholder="请输入评论内容"><van-button slot="button" size="mini" type="info">发布</van-button></van-field></van-cell-group><!-- /发布评论 --></div>
</template><script>export default {name: "ArticleComment",props: {},data() {return {list: [], // 评论列表loading: false, // 上拉加载更多的 loadingfinished: false // 是否加载结束};},methods: {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 scoped lang="less">.publish-wrap {position: fixed;left: 0;bottom: 0;width: 100%;}.van-list {margin-bottom: 45px;}
</style>

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

import ArticleComment from './components/article-comment'export default {...components: {ArticleComment}
}

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

<!-- 文章评论 -->
<article-comment />
<!-- /文章评论 -->

最终页面效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0o4QSFvj-1659971724465)(assets/image-20191206152846065.png)]

获取数据并展示

提示:有评论数据的文章 id:139987

步骤:

  • 封装接口
  • 请求获取数据
  • 处理模板

实现:

1、在 api/comment.js 中添加封装请求方法

/*** 评论接口模块*/
import request from "@/utils/request";/*** 获取文章列表*/
export function getComments(params) {return request({method: "GET",url: "/app/v1_0/comments",params});
}

2、请求获取数据

data () {return {...articleComment: { // 文章评论相关数据list: [],loading: false,finished: false,offset: null, // 请求下一页数据的页码totalCount: 0 // 总数据条数}}
}
async onLoad () {const articleComment = this.articleComment// 1. 请求获取数据const { data } = await getComments({type: 'a', // 评论类型,a-对文章(article)的评论,c-对评论(comment)的回复source: this.articleId, // 源id,文章id或评论idoffset: articleComment.offset, // 获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据limit: 10 // 每页大小})// 2. 将数据添加到列表中const { results } = data.dataarticleComment.list.push(...results)// 更新总数据条数articleComment.totalCount = data.data.total_count// 3. 将加载更多的 loading 设置为 falsearticleComment.loading = false// 4. 判断是否还有数据if (results.length) {articleComment.offset = data.data.last_id // 更新获取下一页数据的页码} else {articleComment.finished = true // 没有数据了,关闭加载更多}
}

3、模板绑定

<!-- 评论列表 -->
<van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"
><van-cell v-for="item in list" + :key="item.com_id.toString()"><van-imageslot="icon"roundwidth="30"height="30"style="margin-right: 10px;"+:src="item.aut_photo"/>+ <span style="color: #466b9d;" slot="title">{{ item.aut_name }}</span><div slot="label">+<p style="color: #363636;">{{ item.content }}</p><p>+<span style="margin-right: 10px;">{{ item.pubdate | relativeTime }}</span><van-button size="mini" type="default">回复</van-button></p></div><van-icon slot="right-icon" name="like-o" /></van-cell>
</van-list>
<!-- 评论列表 -->

展示文章评论总数量

文章评论项

<template><van-cell class="comment-item"><van-imageslot="icon"class="avatar"roundfit="cover"src="https://img.yzcdn.cn/vant/cat.jpeg"/><div slot="title" class="title-wrap"><div class="user-name">用户名称</div><van-buttonclass="like-btn"icon="good-job-o">赞</van-button></div><div slot="label"><p class="comment-content">这是评论内容</p><div class="bottom-info"><span class="comment-pubdate">4天前</span><van-buttonclass="reply-btn"round>回复 0</van-button></div></div></van-cell>
</template><script>
export default {name: 'CommentItem',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},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;}}
}
</style>

绑定之后:

<template><van-cell class="comment-item"><van-imageslot="icon"class="avatar"roundfit="cover":src="comment.aut_photo"/><div slot="title" class="title-wrap"><div class="user-name">{{ comment.aut_name }}</div><van-buttonclass="like-btn"icon="good-job-o">{{ 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-buttonclass="reply-btn"round>回复 {{ comment.reply_count }}</van-button></div></div></van-cell>
</template><script>
export default {name: 'CommentItem',components: {},props: {comment: {type: Object,required: true}},data () {return {}},computed: {},watch: {},created () {},mounted () {},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;}}
}
</style>

评论点赞

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

/*** 对评论或评论回复点赞*/
export function addCommentLike(commentId) {return request({method: "POST",url: "/app/v1_0/comment/likings",data: {target: commentId}});
}/*** 取消对评论或评论回复点赞*/
export function deleteCommentLike(commentId) {return request({method: "DELETE",url: `/app/v1_0/comment/likings/${commentId}`});
}

2、然后给评论项中的 like 图标注册点击事件

<van-iconslot="right-icon"color="red"+:name="item.is_liking ? 'like' : 'like-o'"+@click="onCommentLike(item)"
/>

3、在事件处理函数中

import {getComments,addComment,
+  addCommentLike,
+  deleteCommentLike
} from '@/api/comment'
async onCommentLike (comment) {// 如果已经赞了则取消点赞if (comment.is_liking) {await deleteCommentLike(comment.com_id)} else {// 如果没有赞,则点赞await addCommentLike(comment.com_id)}// 更新视图状态comment.is_liking = !comment.is_likingthis.$toast('操作成功')
}

发布文章评论

准备弹出层

封装组件

<template><div class="comment-post"><van-fieldclass="post-field"v-model="message"rows="2"autosizetype="textarea"maxlength="50"placeholder="请输入留言"show-word-limit/><van-buttonclass="post-btn">发布</van-button></div>
</template><script>
export default {name: 'CommentPost',components: {},props: {},data () {return {message: ''}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less">
.comment-post {display: flex;align-items: center;padding: 32px 0 32px 32px;.post-field {background-color: #f5f7f9;}.post-btn {width: 150px;border: none;padding: 0;color: #6ba3d8;&::before {display: none;}}
}
</style>

步骤:

  • 注册发布点击事件
  • 请求提交表单
  • 根据响应结果进行后续处理

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

1、添加弹层组件

data () {return {...isPostShow: false}
}
<!-- 发布文章评论 -->
<van-popupv-model="isPostShow"position="bottom"
/>
<!-- /发布文章评论 -->

提示:不设置高度的时候,内容会自动撑开弹层高度

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

<van-buttonclass="write-btn"type="default"roundsize="small"@click="isPostShow = true"
>写评论</van-button>

二、封装发布评论组件

1、创建 post-comment.vue

<template><div class="post-comment"><van-fieldclass="post-field"v-model="message"rows="2"autosizetype="textarea"maxlength="50"placeholder="优质评论将会被优先展示"show-word-limit/><van-buttontype="primary"size="small">发布</van-button></div>
</template><script>
export default {name: 'PostComment',components: {},props: {},data () {return {message: ''}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped lang="less">
.post-comment {display: flex;padding: 15px;align-items: flex-end;.post-field {background: #f5f7f9;margin-right: 15px;}
}
</style>

2、在详情页加载注册

3、在发布评论的弹层中使用

<!-- 发布文章评论 -->
<van-popupv-model="isPostShow"position="bottom"
><post-comment />
</van-popup>
<!-- /发布文章评论 -->

三、发布评论

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

/*** 添加评论或评论回复*/
export function addComment(data) {return request({method: "POST",url: "/app/v1_0/comments",data});
}

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

data () {return {...inputComment: ''}
}
<!-- 发布评论 -->
<van-cell-group class="publish-wrap"><van-field + v-model="inputComment" clearable placeholder="请输入评论内容"><van-button slot="button" size="mini" type="info" + @click="onAddComment">发布</van-button></van-field>
</van-cell-group>
<!-- /发布评论 -->

3、在事件处理函数中

import {getComments,
+  addComment
} from '@/api/comment'
async onAddComment () {const inputComment = this.inputComment.trim()// 非空校验if (!inputComment.length) {return}// 请求添加const res = await addComment({target: this.$route.params.articleId, // 评论的目标id(评论文章即为文章id,对评论进行回复则为评论id)content: inputComment // 评论内容// art_id // 文章id,对评论内容发表回复时,需要传递此参数,表明所属文章id。对文章进行评论,不要传此参数。})// 将发布的最新评论展示到列表顶部this.list.unshift(res.data.data.new_obj)// 清空文本框this.inputComment = ''
}

请求发布

基本思路:

  • 找到数据接口
  • 封装请求方法
  • 注册发布点击事件
    • 请求发布
    • 成功:将发布的内容展示到列表中
    • 失败:提示失败

1、封装数据接口

/*** 发布评论*/
export const addComment = data => {return request({method: 'POST',url: '/app/v1_0/comments',data})
}

2、给发布按钮点击事件

3、事件处理函数

async onAddComment () {// 1. 拿到数据const postMessage = this.postMessage// 非空校验if (!postMessage) {return}this.$toast.loading({duration: 0, // 持续展示 toastmessage: '发布中...',forbidClick: true // 是否禁止背景点击})try {// 2. 请求提交const { data } = await addComment({target: this.articleId, // 评论的目标id(评论文章即为文章id,对评论进行回复则为评论id)content: postMessage// art_id: // 文章id,对评论内容发表回复时,需要传递此参数,表明所属文章id。对文章进行评论,不要传此参数。})// 关闭发布评论的弹层this.isPostShow = false// 将最新发布的评论展示到列表的顶部this.articleComment.list.unshift(data.data.new_obj)// 更新文章评论的总数量this.articleComment.totalCount++// 清空文本框this.postMessage = ''this.$toast.success('发布成功')} catch (err) {console.log(err)this.$toast.fail('发布失败')}
}

发布成功处理

评论回复

准备回复弹层

一、在详情页中使用弹层用来展示文章的回复

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

data () {return {...isReplyShow: false}
}

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

<!-- 评论回复 -->
<van-popupv-model="isReplyShow"position="bottom"style="height: 95%"
>评论回复
</van-popup>
<!-- /评论回复 -->

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

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

<van-buttonsize="mini"type="default"@click="$emit('click-reply')"
>回复 {{ comment.reply_count }}</van-button>

2、在详情页组件中使用的位置监听处理

<comment-itemv-for="(comment, index) in articleComment.list":key="index":comment="comment"@click-reply="isReplyShow = true"
/>

点击回复显示弹出层

封装内容组件

<template><div class="comment-reply"><!-- 导航栏 --><van-nav-bar :title="`${comment.reply_count}条回复`"><van-iconslot="left"name="cross"@click="$emit('click-close')"/></van-nav-bar><!-- /导航栏 --><!-- 当前评论项 --><!-- /当前评论项 --><van-cell title="所有回复" /><!-- 评论的回复列表 --><!-- /评论的回复列表 --><!-- 底部 --><!-- /底部 --></div>
</template><script>
export default {name: 'CommentReply',components: {},props: {},data () {return {}},computed: {},watch: {},created () {},mounted () {},methods: {}
}
</script><style scoped></style>

传递当前点击回复的评论项

处理头部

处理当前评论项

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

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

<van-buttonsize="mini"type="default"@click="$emit('click-reply', comment)"
>回复 {{ comment.reply_count }}</van-button>

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

data () {return {...currentComment: {} // 点击回复的那个评论对象}
}
<comment-itemv-for="(comment, index) in articleComment.list":key="index":comment="comment"@click-reply="onReplyShow"
/>
async onReplyShow (comment) {// 将子组件中传给我评论对象存储到当前组件this.currentComment = comment// 展示评论回复弹层this.isReplyShow = true
}

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

<!-- 评论回复 -->
<van-popupv-model="isReplyShow"position="bottom"style="height: 95%"
><comment-reply@click-close="isReplyShow = false":comment="currentComment"/>
</van-popup>
<!-- /评论回复 -->

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

props: {comment: {type: Object,required: true}
},

最后使用调试工具查看 props 数据是否接收正确。

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

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

2、使用展示

<template><div class="comment-reply"><!-- 导航栏 --><van-nav-bar :title="`${comment.reply_count}条回复`"><van-iconslot="left"name="cross"@click="$emit('click-close')"/></van-nav-bar><!-- /导航栏 --><!-- 当前评论项 --><comment-item :comment="comment" /><!-- /当前评论项 --><!-- 评论的回复列表 --><!-- /评论的回复列表 --><!-- 底部 --><!-- /底部 --></div>
</template>

一:把点击回复的评论对象传递给评论回复组件

1、在 data 中添加一个数据用来存储点击回复的评论对象

data () {return {...currentComment: {} // 存储当前点击回复的评论对象}
}

2、在点击回复的处理函数中评论对象存储到数据中

async onReplyShow (comment) {+  this.currentComment = comment// 显示回复的弹层this.isReplyShow = true
}

3、把当前组件的 currentComment 传递给评论回复组件

<!-- 评论回复 -->
<van-popupv-model="isReplyShow"get-container="body"roundposition="bottom":style="{ height: '90%' }"
><!-- 回复列表 -->+ <comment-reply :comment="currentComment" /><!-- /回复列表 -->
</van-popup>
<!-- 评论回复 -->

4、在评论回复组件中声明 props 接收数据

props: {comment: {type: Object,required: true}
},

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

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

<!-- 导航栏 -->
+
<van-nav-bar :title="comment.reply_count + '条回复'"><van-icon slot="left" name="cross" />
</van-nav-bar>
<!-- /导航栏 --><!-- 当前评论 -->
<van-cell title="当前评论"><van-imageslot="icon"roundwidth="30"height="30"style="margin-right: 10px;":src="comment.aut_photo"/>+ <span style="color: #466b9d;" slot="title">{{ comment.aut_name }}</span><div slot="label">+<p style="color: #363636;">{{ comment.content }}</p><p>+<span style="margin-right: 10px;">{{ comment.pubdate | relativeTime }}</span><van-button size="mini" type="default" +>回复 {{ comment.reply_count }}</van-button></p></div><van-icon slot="right-icon" />
</van-cell>
<!-- /当前评论 -->

展示评论回复列表

基本思路:

  • 回复列表和文章的评论列表几乎是一样的
  • 重用把之前封装的评论列表
<template><div class="comment-reply"><!-- 导航栏 --><van-nav-bar :title="`${comment.reply_count}条回复`"><van-iconslot="left"name="cross"@click="$emit('click-close')"/></van-nav-bar><!-- /导航栏 --><!-- 当前评论项 --><comment-item :comment="comment" /><!-- /当前评论项 --><van-cell title="所有回复" /><!-- 评论的回复列表 --><van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"><comment-itemv-for="(comment, index) in list":key="index":comment="comment"/></van-list><!-- /评论的回复列表 --><!-- 底部 --><!-- /底部 --></div>
</template><script>
import CommentItem from './comment-item'
import { getComments } from '@/api/comment'export default {name: 'CommentReply',components: {CommentItem},props: {comment: {type: Object,required: true}},data () {return {list: [],loading: false,finished: false,offset: null,limit: 20}},computed: {},watch: {},created () {},mounted () {},methods: {async onLoad () {// 1. 请求获取数据const { data } = await getComments({type: 'c', // 评论类型,a-对文章(article)的评论,c-对评论(comment)的回复source: this.comment.com_id.toString(), // 源id,文章id或评论idoffset: this.offset, // 获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据limit: this.limit // 获取的评论数据个数,不传表示采用后端服务设定的默认每页数据量})// 2. 将数据添加到列表中const { results } = data.datathis.list.push(...results)// 3. 关闭 loadingthis.loading = false// 4. 判断数据是否加载完毕if (results.length) {this.offset = data.data.last_id} else {this.finished = true}}}
}
</script><style scoped></style>

解决弹层中组件内容不更新问题

弹层组件:

  • 如果初始的条件是 false,则弹层的内容不会渲染
  • 程序运行期间,当条件变为 true 的时候,弹层才渲染了内容
  • 之后切换弹层的展示,弹层只是通过 CSS 控制隐藏和显示

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

<!-- 评论回复 -->
<van-popupv-model="isReplyShow"get-container="body"roundposition="bottom":style="{ height: '90%' }"
><!-- 回复列表 --><comment-reply :comment="currentComment" + v-if="isReplyShow" /><!-- /回复列表 -->
</van-popup>
<!-- 评论回复 -->

发布回复

Vue移动端项目(二)相关推荐

  1. 17.vue移动端项目二

    FilmList.vue 电影列表 <template><div class="mz-film-list"><!-- 正在热映 https://m.m ...

  2. vue移动端项目缓存问题实践

    最近在做一个vue移动端项目,被缓存问题搞得头都大了,积累了一些经验,特此记录总结下,权当是最近项目问题的一个回顾吧! 先描述下问题场景:A页面->B页面->C页面.假设A页面是列表页面, ...

  3. 2021-03-24 从零开始搭建vue移动端项目

    从零开始搭建vue移动端项目 一.Vue项目搭建 二.使用步骤 1.初始化 2.路由 3.Vuex(状态管理) 4.Axios(数据请求模块) 5.使用Less 6.移动端适配 7.注意事项 8.移动 ...

  4. Vue购物商城项目(二) 数据请求使用

    Vue购物商城项目(二) 文章目录 Vue购物商城项目(二) 前言 一.请求数据 request.js home.js Home.vue 二.使用数据 总结 前言 1.这里面包含了大量的.我的个人理解 ...

  5. Vue 移动端项目创建

    前言 移动端我们一般通过Vue脚手架手动自定义创建项目, 只需要Node环境我们就可以通过npm下载Vue脚手架,通过命令创建项目. npm下载脚手架 npm install -g @vue/cli ...

  6. vue移动端项目日历组件,月周切换,点击进入上/下一个月

    项目场景: Vue移动端项目的日历组件,移动端如果没有别的特别要求,一般用vant中的日历组件就OK,这里用的另一个.组件是网上找的,原网址:vue-hash-calendar,需要的请自行去看. 我 ...

  7. Vue移动端项目---尚硅谷外卖

    文章目录 Vue移动端项目--尚硅谷外卖 项目目录结构介绍 移动端适配 头部和底部导航 登录注册页面 密码登录 短信登录 Profile页面 未登录 已登录 Msite页面 首页导航 ShopList ...

  8. vue移动端项目实现真机调试

    在vue移动端项目中,我们可以通过真机调试来更好的看到项目实际运行在移动端的效果. 下面就来介绍一下实现的方法: 1.使手机与电脑连接在同一个wifi下 2.修改vue.config.js文件配置 将 ...

  9. vue移动端项目实现定位

    vue移动端项目实现定位 腾讯地图官方地址 <template><div><!-- 定义地图显示容器 --><div id="container&q ...

  10. vue移动端项目基础框架搭建

    本文章,主要提供vue移动端项目基础框架搭建思路,每个独立的模块网上有很多相关的文档. 移动端vue项目基础框架搭建,主要包括6个步骤 项目使用的脚手架vue-cli搭建模板,2.使用淘宝lib-fl ...

最新文章

  1. 下载合适的python-python下载文件的三种方法
  2. Eclipse 高亮显示选中的相同变量
  3. md文件编辑器_可能是颜值最高的微信Markdown编辑器,用Markdown的你一定会爱上
  4. Java/Android引用类型及其使用分析
  5. 一元二次方程python脚本_Python实现求解一元二次方程的方法示例
  6. 如何加快 Node.js 应用的启动速度
  7. mysql limitorderby
  8. Eclipse怎样连接并打开oracle等数据库?
  9. Hadoop2 自己动手编译Hadoop的eclipse插件
  10. 游戏开发学什么?四步修炼骨灰级高手
  11. GAMES101 Rasteriztion
  12. java科学计算器代码_用JAVA编写的科学计算器源代码是什么?
  13. 电脑键盘打字跳转计算机,电脑键盘打字方法
  14. 企业微信三方开发(五):扫码登录
  15. base64编码,原理是什么,有什么作用?
  16. 一次Linux系统被攻击的分析过程
  17. 机器学习基础(五)最大似然估计
  18. gsl for windows
  19. Clickhouse 在云原生场景下的部署和使用
  20. Spring JdbcTemplate2-update和query

热门文章

  1. 2021 PyTorch官方实战教程(一)Tensor 详解
  2. Java面试题之ServletJSP篇
  3. 高级计划和排程(APS)软件的功能与用途
  4. 《程序设计基础》 第五章 函数 6-6 字符金字塔 (15 分)
  5. MIPS指令集:运算指令、访存指令、分支和跳转、协处理器0指令
  6. 破解明星网红带货易翻车的方法——企业直播
  7. MongoDb查询语句($ne $in )
  8. 区块链搭建eos开发环境
  9. Intel Distiller工具包-量化实现1
  10. 【XSY3549】Tree(线段树,换根)