# 七、文章详情

# 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>
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

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

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

官方文档:路由 props 传参 (opens new window)

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} }"
  >
1
2
3
4
5
6
7
8
9
10
11

# 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>

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241

# 4、实现功能

# 4.1、获取文章数据

思路:

  • 找到数据接口
  • 封装请求方法
  • 请求获取数据
  • 模板绑定

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

/**
 * 根据 id 获取指定文章
 */
export const getArticleById = articleId => {
  return request({
    method: 'GET',
    url: `/v1_0/articles/${articleId}`
  })
}
1
2
3
4
5
6
7
8
9

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)
      }
    }
  }
}

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

3、模板绑定,稍后处理

我们发现有很多文章的数据是获取后抛出了404的状态,我们的代码没问题,主要是后台返回数据里面存在大数字的问题,我们接下来解决这个问题

# 4.2、大数字问题

关于后端返回数据中的大数字问题,之所以请求文章详情返回 404 是因为我们请求发送的文章 ID (article.art_id)不正确。

1、问题原因

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

Math.pow(2, 53) // 9007199254740992

9007199254740992  // 9007199254740992
9007199254740993  // 9007199254740992

Math.pow(2, 53) === Math.pow(2, 53) + 1
// true
1
2
3
4
5
6
7

上面代码中,超出 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
// true

Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true
1
2
3
4
5
6
7
8
9

上面代码中,可以看到 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

image.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://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.vueprops里面的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" >    
1
2
3
4