七、文章详情
#1、业务功能分析
#2、创建组件并配置路由
1、创建 views/article/index.vue
组件
<template>
<div class="article-container">文章详情</div>
</template>
<script>
export default {
name: 'ArticleIndex',
components: {},
props: {
// 使用props解耦获得了的动态路由数据,这样我们就可以使用this.articleId 获取动态路由数据 而不需要使用 this.$route.params.articleId
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
}
3、找到首页文章列表项组件src/components/article-item/index.vue
配置路由跳转
<!--
Cell 单元格的 to 属性和 VueRouter 中的 RouterLink 导航组件的 to 属性用法是一样的
用法参考链接:https://router.vuejs.org/zh/api/#to
:to="'/article/' + article.art_id"
:to="`/article/${article.art_id}`"
:to="{ name:'路径名称', params:{ 标识符:数据 } }"
-->
<van-cell
class="article-item"
:to="{ name: 'article', params: { articleId: article.art_id} }"
>
#3、页面布局
使用到的 Vant 中的组件:
<template>
<div class="article-container">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
left-arrow
title="黑马头条"
@click-left="$router.back()"
></van-nav-bar>
<!-- /导航栏 -->
<div class="main-wrap">
<!-- 加载中 -->
<div class="loading-wrap">
<van-loading
color="#3296fa"
vertical
>加载中</van-loading>
</div>
<!-- /加载中 -->
<!-- 加载完成-文章详情 -->
<div class="article-detail">
<!-- 文章标题 -->
<h1 class="article-title">这是文章标题</h1>
<!-- /文章标题 -->
<!-- 用户信息 -->
<van-cell class="user-info" center :border="false">
<van-image
class="avatar"
slot="icon"
round
fit="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-button
class="follow-btn"
type="info"
color="#3296fa"
round
size="small"
icon="plus"
>关注</van-button>
<!-- <van-button
class="follow-btn"
round
size="small"
>已关注</van-button> -->
</van-cell>
<!-- /用户信息 -->
<!-- 文章内容 -->
<div class="article-content">这是文章内容</div>
<van-divider>正文结束</van-divider>
<!-- 底部区域 -->
<div class="article-bottom">
<van-button
class="comment-btn"
type="default"
round
size="small"
>写评论</van-button>
<van-icon
name="comment-o"
info="123"
color="#777"
/>
<van-icon
color="#777"
name="star-o"
/>
<van-icon
color="#777"
name="good-job-o"
/>
<van-icon name="share" color="#777777"></van-icon>
</div>
<!-- /底部区域 -->
</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>
</template>
<script>
export default {
name: 'ArticleIndex',
components: {},
props: {
// 使用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>
#4、实现功能
#4.1、获取文章数据
思路:
- 找到数据接口
- 封装请求方法
- 请求获取数据
- 模板绑定
1、在 api/article.js
中新增封装接口方法
/**
* 根据 id 获取指定文章
*/
export const getArticleById = articleId => {
return request({
method: 'GET',
url: `/v1_0/articles/${articleId}`
})
}
2、在组件article/index.vue
中调用获取文章详情
// 1.导入请求方法
import { getArticleById } from '@/api/article'
export default {
name: 'ArticlePage',
components: {},
props: {
articleId: {
type: String,
required: true
}
},
data () {
return {
article: {} // 2.定义变量存储文章详情
}
},
computed: {},
watch: {},
created () {
// 4. 调用方法
this.loadArticle()
},
mounted () {},
methods: {
// 3. 定义获取数据请求方法
async loadArticle () {
try {
// 3.1 发送请求
const { data } = await getArticleById(this.articleId)
// 3.3 成功赋值
this.article = data.data
console.log(this.article) // 控制台查看数据输出
} catch (err) {
// 3.2 失败处理
console.log(err)
}
}
}
}
3、模板绑定,稍后处理
我们发现有很多文章的数据是获取后抛出了404的状态,我们的代码没问题,主要是后台返回数据里面存在大数字的问题,我们接下来解决这个问题
#4.2、大数字问题
关于后端返回数据中的大数字问题,之所以请求文章详情返回 404 是因为我们请求发送的文章 ID (article.art_id)不正确。
1、问题原因
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 }'
1
如果这个字符不做任何处理,你能方便的获取到字符串中的指定数据吗?非常麻烦。所以我们要把它转换为 JavaScript 对象来使用就很方便了。
幸运的是 axios 为了方便我们使用数据,它会在内部使用 JSON.parse()
把后端返回的数据转为 JavaScript 对象。
// { id: 9007199254740996, name: 'Jack', age: 18 }
JSON.parse('{ "id": 9007199254740995, "name": "Jack", "age": 18 }')
1
2
可以看到,超出安全整数范围的 id 无法精确表示,这个问题并不是 axios 的错。
2、解决方案
了解了什么是大整数的概念,接下来的问题是如何解决?
json-bigint (opens new window)是一个第三方包,它可以帮我们很好的处理这个问题。
使用它的第一步就是把它安装到你的项目中。
npm i json-bigint
1
下面是使用它的一个简单示例。
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()) // 1245953273786007552
console.log(JSON.stringify(JSONBig.parse(jsonStr)))
console.log(JSONBig.stringify(JSONBig.parse(jsonStr))) // 把 JavaScript 对象 转为 JSON 格式的字符串转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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://toutiao-app.itheima.net", // 基础路径
// transformResponse 允许自定义原始的响应数据(字符串)
transformResponse: [function (data) {
try {
// 如果转换成功则返回转换的数据结果
return jsonBig.parse(data)
} catch (err) {
// 如果转换失败,则包装为统一数据格式并返回
return data
}
}]
})
export default request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
扩展:ES2020 BigInt
ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
参考链接:
注意调整文章详情组件article/index.vue
中props
里面的articleId
的类型
articleId: {
type: [Number, String, Object], // 增加Object类型,因为大数字已经被处理成一个大数字对象
required: true
}
1
2
3
4
#4.3、展示文章详情
先处理标题、作者信息、文章正文显示这几个部分,其他留到稍后处理
<!-- 文章标题 -->
<h1 class="article-title">{{ article.title }}</h1>
<!-- /文章标题 -->
<!-- 用户信息 -->
<van-cell class="user-info" center :border="false">
<van-image
class="avatar"
slot="icon"
round
fit="cover"
:src="article.aut_photo"
/>
<div slot="title" class="user-name">{{ article.aut_name }}</div>
<div slot="label" class="publish-date">{{ article.pubdate | relativeTime }}</div>
<!-- 文章内容 -->
<div class="article-content"
v-html="article.content"></div>
<van-divider>正文结束</van-divider>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#4.4、处理内容加载状态
需求:
- 加载中,显示 loading
- 加载成功,显示文章详情
- 加载失败,显示错误提示
- 如果 404,提示资源不存在
- 其它的,提示加载失败,用户可以点击重试重新加载
// data定义变量
loading: true, // 加载中的 loading 状态
errStatus: 0 // 失败的状态码
1
2
3
async loadArticle () {
// 展示 loading 加载中
this.loading = true
try {
const { data } = await getArticleById(this.articleId)
// if (Math.random() > 0.5) {
// JSON.parse('dsankljdnskaljndlkjsa')
// }
// 赋值
this.article = data.data
} catch (err) {
if (err.response && err.response.status === 404) {
this.errStatus = 404
}
}
// 无论成功还是失败,都需要关闭 loading
this.loading = false
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 加载中 -->
<div v-if="loading" class="loading-wrap"> ... </div>
<!-- /加载中 -->
<!-- 加载完成-文章详情 -->
<div v-else-if="article.title" class="article-detail"> ... </div>
<!-- /加载完成-文章详情 -->
<!-- 加载失败:404 -->
<div v-else-if="errStatus === 404" class="error-wrap"> ... </div>
<!-- /加载失败:404 -->
<!-- 加载失败:其它未知错误(例如网络原因或服务端异常) -->
<div v-else class="error-wrap">...</div>
<!-- /加载失败:其它未知错误(例如网络原因或服务端异常) -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#4.5、处理正文样式
文章正文包括各种数据:段落、标题、列表、链接、图片、视频等资源。
将 github-markdown-css (opens new window)样式文件下载到项目中
把 样式文件下载放在
views/article/github-markdown.css
在
article/index.vue
里面的style
标签里面导入<style scoped lang="less"> @import "./github-markdown.css";
1
2给内容正文标签增加
markdown-body
样式类<!-- 文章内容 --> <div class="article-content markdown-body" v-html="article.content" ></div> <van-divider>正文结束</van-divider>
1
2
3
.postcssrc.js
中配置不要转换样式文件中的字号
/** * PostCSS 配置文件 */ module.exports = { // 配置要使用的 PostCSS 插件 plugins: { 'postcss-pxtorem': { rootValue ({ file }) { return file.indexOf('vant') !== -1 ? 37.5 : 75 }, // * 表示所有 propList: ['*'], // 配置不要转换的样式资源 exclude: 'github-markdown' // 增加这一句! } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19需要重新启动项目,配置才会生效!
#4.6、图片点击预览
一、ImagePreview 图片预览 (opens new window)的使用
二、处理图片点击预览 (演示数据文章id: 135982 )
思路:
1、从文章内容中获取到所有的 img DOM 节点
2、获取文章内容中所有的图片地址
3、遍历所有 img 节点,给每个节点注册点击事件
4、在 img 点击事件处理函数中,调用 ImagePreview 预览
<!-- 增加ref属性 -->
<div class="article-content markdown-body" ref="article-content" v-html="article.content" ></div>
1
2
import { ImagePreview } from 'vant' // 导入预览插件
1
async loadArticle () {
// 前面省略很多代码...
// 数据驱动视图这件事儿不是立即的
this.article = data.data
// 初始化图片点击预览
console.log(this.$refs['article-content']) // 这里没有内容
// 这个时候其实找不到 这个refs引用的,原因是因为v-if的渲染其实需要时间,我们视图还没有立即更新完成。
// 使用定时器,延迟更新( setTimeout 0 会把要做的事情放在异步队列的最后面执行! )
setTimeout(() => {
console.log(this.$refs['article-content']) // 这里有内容
this.previewImage()
}, 0)
// 不使用定时器可以使用nextTick这个api方法
/*
this.loading = false
this.$nextTick(()=>{
this.previewImage()
})
*/
// 后面省略很多代码...
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 预览图片处理事件函数
previewImage () {
// 得到所有的 img 节点
const articleContent = this.$refs['article-content'] // 获取到了容器节点
const imgs = articleContent.querySelectorAll('img')
// 获取所有 img 地址
const images = []
imgs.forEach((img, index) => {
images.push(img.src)
// 给每个 img 注册点击事件,在处理函数中调用预览
img.onclick = () => {
ImagePreview({
// 预览的图片地址数组
images,
// 起始位置,从 0 开始
startPosition: index
})
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#4.7、关注用户
思路:
- 给按钮注册点击事件
- 在事件处理函数中
- 如果已关注,则取消关注
- 如果没有关注,则添加关注
下面是具体实现。
1、视图处理
<van-button
v-if="article.is_followed"
class="follow-btn"
round
size="small"
>已关注</van-button>
<van-button
v-else
class="follow-btn"
type="info"
color="#3296fa"
round
size="small"
icon="plus"
>关注</van-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2、功能处理
思路
- 找到数据接口
- 封装请求方法
- 请求调用
- 视图更新
在
api/user.js
中添加封装请求方法
/**
* 添加关注
*/
export const addFollow = userId => {
return request({
method: 'POST',
url: '/v1_0/user/followings',
data: {
target: userId
}
})
}
/**
* 取消关注
*/
export const deleteFollow = userId => {
return request({
method: 'DELETE',
url: `/v1_0/user/followings/${userId}`
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 给关注/取消关注按钮注册点击事件
<van-button
...
:loading="followLoading"
@click="onFollow"
>已关注</van-button>
<van-button
...
:loading="followLoading"
@click="onFollow"
>关注</van-button>
1
2
3
4
5
6
7
8
9
10
data
里面定义加载中变量
data(){
return{
// 其他变量 ...
followLoading:false // 控制是否处于载中
}
}
1
2
3
4
5
6
- 在事件处理函数中
import { addFollow, deleteFollow } from '@/api/user'
1
async onFollow () {
// 如果没有登录,就不允许操作
if(!this.$store.state.user) return this.$toast('请登录!')
// 开启按钮的 loading 状态
this.isFollowLoading = true
try {
// 如果已关注,则取消关注
const authorId = this.article.aut_id
if (this.article.is_followed) {
await deleteFollow(authorId)
} else {
// 否则添加关注
await addFollow(authorId)
}
// 更新视图
this.article.is_followed = !this.article.is_followed
} catch (err) {
if(err && err.response.status === 400){
this.$toast('你不能关注自己')
}else{
this.$toast.fail('操作失败')
}
}
// 关闭按钮的 loading 状态
this.isFollowLoading = false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
最后测试。
loading 效果的两个作用:
1.交互反馈
2.防止网络慢用户多次点击按钮导致重复触发点击事件
3、组件封装
- 删除
article/index.vue
里面部分内容:- 删除
followLoading
变量 - 删除
onFollow
事件 - 删除导入
addFollow, deleteFollow
方法
- 删除
- 创建
src/components/follow-user/index.vue
<template>
<van-button
v-if="isFollowed"
round
size="small"
:loading="loading"
@click="onFollow"
>已关注</van-button>
<van-button
v-else
type="info"
color="#3296fa"
round
size="small"
icon="plus"
:loading="loading"
@click="onFollow"
>关注</van-button>
</template>
<script>
import { addFollow, deleteFollow } from '@/api/user'
export default {
name: 'FollowUser',
props: {
// 是否关注了
isFollowed: {
type: Boolean,
required: true
},
// 用户ID
userId: {
type: [Number, String, Object],
required: true
}
},
data () {
return {
loading: false // 加载中控制变量
}
},
methods: {
async onFollow () {
// 如果没有登录,就不允许操作
if(!this.$store.state.user) return this.$toast('请登录!')
this.loading = true // 展示关注按钮的 loading 状态
try {
if (this.isFollowed) {
// 已关注,要取消关注
await deleteFollow(this.userId)
} else {
// 没有关注,要添加关注
await addFollow(this.userId)
}
// 更新视图状态
this.$emit('update-is_followed', !this.isFollowed)
} catch (err) {
console.log(err)
if(err && err.response.status === 400){
this.$toast('你不能关注自己')
}else{
this.$toast.fail('操作失败')
}
}
this.loading = false // 关闭关注按钮的 loading 状态
}
}
}
</script>
<style scoped lang="less"></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
article/index.vue
中导入关注组件
// 导入
import FollowUser from '@/components/follow-user'
// 注册
components: {
FollowUser
}
1
2
3
4
5
6
7
<follow-user
class="follow-btn"
:is-followed="article.is_followed"
:user-id="article.aut_id"
@update-is_followed="article.is_followed=$event"
/>
1
2
3
4
5
6
一个组件上面的class
类,会被组件的根标签继承过去使用,所以我们只需要调整组件上面的class,就可以去修改组件模板里面的样式
4、v-model
的使用
回顾v-model
, 双向绑定数据的!
<tempatel>
<div>
<input v-model="msg"> {{msg}}
<hr/>
<input :value="msg" @input="msg=$event.target.value"> {{msg}}
<hr>
<!--
总结: v-model 是 value属性和input事件的语法糖!
-->
</div>
</tempatel>
<script>
export default {
data(){
return {
msg:"呵呵!"
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
运用场景: 当我们的父组件通过props
传递某个数据给子组件,且子组件需要通过自定义事件$emit
去修改该数据,也就是父子之间共同操作同一个变量数据,我们可以使用v-model
指令
上述案例中,
props
我们定义的是isFollowed
,触发的自定义事件是update-is_followed
。这只是2个名字而已,我们可以把props
属性定义value
; 自定义事件 定义为input
<template> <!--【2.修改为value,进行判断】--> <van-button v-if="value" class="follow-btn" round size="small" :loading="loading" @click="onFollow" >已关注</van-button> <van-button v-else class="follow-btn" type="info" color="#3296fa" round size="small" icon="plus" :loading="loading" @click="onFollow" >关注</van-button> </template> <script> import { addFollow, deleteFollow } from '@/api/user' export default { name: 'FollowUser', props: { // 是否关注了 //isFollowed: { value:{ // <==== 【1.修改名称为value】 type: Boolean, required: true }, // 用户ID userId: { type: [Number, String, Object], required: true } }, data () { return { loading: false // 加载中控制变量 } }, methods: { async onFollow () { // 如果没有登录,就不允许操作 if(!this.$store.state.user) return this.$toast('请登录!') this.loading = true // 展示关注按钮的 loading 状态 try { if (this.value) { //<==== 【3.修改为value】 // 已关注,取消关注 await deleteFollow(this.userId) } else { // 没有关注,添加关注 await addFollow(this.userId) } // 更新视图状态 // this.$emit('update-is_followed', !this.isFollowed) this.$emit('input', !this.value) // <====【4.修改为 input事件和对this.value取反】 } catch (err) { console.log(err) let message = '操作失败,请重试!' if (err.response && err.response.status === 400) { message = '你不能关注你自己!' } this.$toast(message) } this.loading = false // 关闭关注按钮的 loading 状态 } } } </script> <style scoped lang="less"></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79自然父组件中就需要改成对应的
value
属性和监听input
事件
<follow-user class="follow-btn" :user-id="article.aut_id" :value="article.is_followed" @input="article.is_followed=$event" />
1
2
3
4
5
6那当一个组件上面同时有
value
属性和input
自定义事件的时候,且操作的是同一个变量,即可使用v-model
来简化
<follow-user class="follow-btn" :user-id="article.aut_id" v-model="article.is_followed" />
1
2
3
4
5一个组件标签只能有一个
v-model
指令
#4.8、文章收藏
该功能和关注用户的处理思路几乎一样,建议由学员自己编写。
封装组件
处理视图
功能处理
思路:
- 给收藏按钮注册点击事件
- 如果已经收藏了,则取消收藏
- 如果没有收藏,则添加收藏
下面是具体实现。
1、封装组件
创建src/components/collect-article/index.vue
<template>
<van-button icon="star-o"></van-button>
</template>
<script>
export default {
name:'CollectArticle',
props:{},
data(){
return{
}
},
methods:{},
created(){}
}
</script>
<style scoped lang="less">
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在src/views/article/index.vue
中导入注册,使用组件
import CollectArticle from '@/components/collect-article'
1
components:{
// ...其他注册
CollectArticle
}
1
2
3
4
<collect-article class="btn-item"/>
1
2、处理视图
在父组件article/index.vue
中通过v-model
绑定文章的收藏状态
<collect-article
class="btn-item"
v-model="article.is_collected"
/>
1
2
3
4
子组件中collect-article/index.vue
中定义props接收,且去渲染视图
props:{
// 接收文章收藏状态
value:{
type:Boolean,
required:true
}
}
1
2
3
4
5
6
7
<van-button
icon="value ? 'star' : 'star-o'"
:class="{ collected: value }"
></van-button>
1
2
3
4
<style scoped lang="less">
.collected {
.van-icon {
color: #ffa500;
}
}
</style>
1
2
3
4
5
6
7
3、功能处理
在 api/article.js
添加封装数据接口
/**
* 收藏文章
*/
export const addCollect = target => {
return request({
method: 'POST',
url: '/v1_0/article/collections',
data: {
target
}
})
}
/**
* 取消收藏文章
*/
export const deleteCollect = target => {
return request({
method: 'DELETE',
url: `/v1_0/article/collections/${target}`
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2、给收藏按钮注册点击事件
<template>
<van-button
:icon="value ? 'star' : 'star-o'"
:class="{ collected: value }"
:loading="loading"
@click="onCollect"
/>
1
2
3
4
5
6
7
3、父组件传递文章id给子组件,书写处理函数
article/index.vue
中传递文章id
<collect-article
class="btn-item"
v-model="article.is_collected"
:article-id="article.art_id"
/>
1
2
3
4
5
collect-article/index.js
定义props
接收; 定义loading
变量, 导入请求方法,书写事件函数,完成功能
<script>
import { addCollect, deleteCollect } from '@/api/article'
export default {
name: 'CollectArticle',
components: {},
props: {
// 接收文章收藏状态
value: {
type: Boolean,
required: true
},
// 接收文章id
articleId: {
type: [Number, String, Object],
required: true
}
},
data () {
return {
// 加载中效果
loading: false
}
},
methods: {
async onCollect () {
this.loading = true
try {
if (this.value) {
// 已收藏,要取消收藏
await deleteCollect(this.articleId)
} else {
// 没有收藏,要添加收藏
await addCollect(this.articleId)
}
// 更新视图
// 自定义事件修改数据并不是立即的
this.$emit('input', !this.value)
this.$toast.success(!this.value ? '收藏成功' : '取消收藏')
} catch (err) {
this.$toast.fail('操作失败,请重试!')
}
this.loading = false
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
拓展: 升级讨论一下v-model
的名称问题,默认是自定义属性value
和 自定义事件input
,但是这个名称不太语义化!希望用更加语义化的名称来实现v-model
同样的效果
export default {
name: 'CollectArticle',
// 配置 v-model 对应的自定义属性名和自定义事件名称
model:{
prop:'isCollected',
event:'updateCollect'
}
}
1
2
3
4
5
6
7
8
// 接收文章收藏状态
//value: {
isCollected:{
type: Boolean,
required: true
},
1
2
3
4
5
6
<template>
<van-button
:icon="isCollected ? 'star' : 'star-o'"
:class="{ collected: isCollected }"
:loading="loading"
@click="onCollect"
/>
1
2
3
4
5
6
7
async onCollect () {
this.loading = true
try {
if (this.isCollected) {
// 已收藏,要取消收藏
await deleteCollect(this.articleId)
} else {
// 没有收藏,要添加收藏
await addCollect(this.articleId)
}
// 更新视图
// 自定义事件修改数据并不是立即的
this.$emit('updateCollect', !this.isCollected)
this.$toast.success(!this.isCollected ? '收藏成功' : '取消收藏')
} catch (err) {
this.$toast.fail('操作失败,请重试!')
}
this.loading = false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#4.9、文章点赞
该功能和关注用户的处理思路几乎一样,建议由学员自己编写。
article 中的 attitude
表示用户对文章的态度
-1
无态度0
不喜欢1
已点赞
思路:
- 给点赞按钮注册点击事件
- 如果已经点赞,则请求取消点赞
- 如果没有点赞,则请求点赞
1、封装组件
创建src/components/like-article/index.vue
<template>
<van-button
:icon="value === 1 ? 'good-job' : 'good-job-o'"
:class="{ liked: value === 1 }"
:loading="loading"
/>
</template>
<script>
export default {
name:'LikeArticle',
props:{
// 接收对文字的态度
value:{
type:Number,
required:true
}
},
data(){
return{
// 加载中变量控制
loading:false
}
},
methods:{},
created(){}
}
</script>
<style scoped lang="less">
.liked {
.van-icon {
color: #e5645f;
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在src/views/article/index.vue
中导入注册,使用组件
import LikeArticle from '@/components/like-article'
1
components:{
// ...其他注册
LikeArticle
}
1
2
3
4
<like-article class="btn-item" v-model="article.attitude" />
1
2、处理功能
添加封装数据接口
/**
* 点赞
*/
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}`
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
给点赞按钮注册点击事件
<van-button
:icon="value === 1 ? 'good-job' : 'good-job-o'"
:class="{
liked: value === 1
}"
:loading="loading"
@click="onLike"
/>
1
2
3
4
5
6
7
8
article/index.vue
中传递文章id
<like-article
class="btn-item"
v-model="article.attitude"
:article-id="article.art_id"
/>
1
2
3
4
5
like-article/index.vue
定义props
接收; 定 导入请求方法,书写事件函数,完成功能
import { addLike, deleteLike } from '@/api/article'
export default {
name: 'LikeArticle',
components: {},
props: {
// 文章态度
value: {
type: Number,
required: true
},
// 文章id
articleId: {
type: [Number, String, Object],
required: true
}
},
data () {
return {
loading: false
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
async onLike () {
this.loading = true
try {
let status = -1
if (this.value === 1) {
// 已点赞,要取消点赞
await deleteLike(this.articleId)
} else {
// 没有点赞,要添加点赞
await addLike(this.articleId)
status = 1
}
// 更新视图
this.$emit('input', status)
this.$toast.success(status === 1 ? '点赞成功' : '取消点赞')
} catch (err) {
console.log(err)
this.$toast.fail('操作失败,请重试!')
}
this.loading = false
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
拓展: 我们组件只能一个v-model
,如果想要达到多个类似v-model
的效果,就需要使用属性的.sync
修饰符
子组件进like-article/index.vue
行修改
<template>
<van-button
:icon="attitudeNum === 1 ? 'good-job' : 'good-job-o'"
:class="{ liked: attitudeNum === 1 }"
:loading="loading"
/>
</template>
<script>
import { addLike, deleteLike } from '@/api/article'
export default {
name: 'LikeArticle',
components: {},
props: {
// 接收文章态度
//value: {
attitudeNum:{
type: Number,
required: true
},
// 文章id
articleId: {
type: [Number, String, Object],
required: true
}
},
data () {
return {
loading: false
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
async onLike () {
this.loading = true
try {
let status = -1
if (this.attitudeNum === 1) {
// 已点赞,要取消点赞
await deleteLike(this.articleId)
} else {
// 没有点赞,要添加点赞
await addLike(this.articleId)
status = 1
}
// 更新视图
// .sync 要使用,自定义事件名称必须符合规则==> this.$emit('update:自定义属性名', 实参数据)
this.$emit('update:attitudeNum', status)
this.$toast.success(status === 1 ? '点赞成功' : '取消点赞')
} catch (err) {
console.log(err)
this.$toast.fail('操作失败,请重试!')
}
this.loading = false
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
父组件article/index.vue
要修改
<like-article
class="btn-item"
:article-id="article.art_id"
:attitudeNum="article.attitude"
@update:attitudeNum="article.attitude=$event"
/>
1
2
3
4
5
6
简写为
<like-article
class="btn-item"
:article-id="article.art_id"
:attitudeNum.sync="article.attitude"
/>
1
2
3
4
5
<组件 :自定义属性名.sync="变量">
<!--本质是-->
<组件 :自定义属性名="变量" @update:自定义属性名="变量=$event" >