七、文章详情
创建组件并配置路由
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
}
页面布局
使用到的 Vant 中的组件:
<template>
<div class="article-container">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
left-arrow
title="黑马头条"
></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>
<!-- /加载完成-文章详情 -->
<!-- 加载失败: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-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>
</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^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
Math.pow(2, 53) // 9007199254740992
9007199254740992 // 9007199254740992
9007199254740993 // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1
// true
上面代码中,超出 2 的 53 次方之后,一个数就不精确了。
ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限。
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true
上面代码中,可以看到 JavaScript 能够精确表示的极限。
后端返回的数据一般都是 JSON 格式的字符串。
'{ "id": 9007199254740995, "name": "Jack", "age": 18 }'
如果这个字符不做任何处理,你能方便的获取到字符串中的指定数据吗?非常麻烦。所以我们要把它转换为 JavaScript 对象来使用就很方便了。
幸运的是 axios 为了方便我们使用数据,它会在内部使用 JSON.parse()
把后端返回的数据转为 JavaScript 对象。
// { id: 9007199254740996, name: 'Jack', age: 18 }
JSON.parse('{ "id": 9007199254740995, "name": "Jack", "age": 18 }')
可以看到,超出安全整数范围的 id 无法精确表示,这个问题并不是 axios 的错。
了解了什么是大整数的概念,接下来的问题是如何解决?
json-bigint 是一个第三方包,它可以帮我们很好的处理这个问题。
使用它的第一步就是把它安装到你的项目中。
npm i json-bigint
下面是使用它的一个简单示例。
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 格式的字符串转
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 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
参考链接:
展示文章详情
思路:
- 找到数据接口
- 封装请求方法
- 请求获取数据
- 模板绑定
一、请求并展示文章详情
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 = 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) {
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, // 持续展示 toast
message: '操作中...',
forbidClick: true // 是否禁止背景点击
})
try {
// 如果已收藏,则取消收藏
if (this.article.is_collected) {
await deleteCollect(this.articleId)
// this.article.is_collected = false
this.$toast.success('取消收藏')
} else {
// 添加收藏
await addCollect(this.articleId)
// this.article.is_collected = true
this.$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, // 持续展示 toast
message: '操作中...',
forbidClick: true // 是否禁止背景点击
})
try {
// 如果已经点赞,则取消点赞
if (this.article.attitude === 1) {
await deleteLike(this.articleId)
this.article.attitude = -1
this.$toast.success('取消点赞')
} else {
// 否则添加点赞
await addLike(this.articleId)
this.article.attitude = 1
this.$toast.success('点赞成功')
}
} catch (err) {
console.log(err)
this.$toast.fail('操作失败')
}
}
八、文章评论
展示文章评论列表
准备组件
为了更好的开发和维护,这里我们把文章评论单独封装到一个组件中来处理。
1、创建 src/views/article/components/article-comment.vue
<template>
<div class="article-comments">
<!-- 评论列表 -->
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item" :title="item">
<van-image
slot="icon"
round
width="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, // 上拉加载更多的 loading
finished: 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-H7tPhVnR-1650948799039)(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或评论id
offset: articleComment.offset, // 获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据
limit: 10 // 每页大小
})
// 2. 将数据添加到列表中
const {
results } = data.data
articleComment.list.push(...results)
// 更新总数据条数
articleComment.totalCount = data.data.total_count
// 3. 将加载更多的 loading 设置为 false
articleComment.loading = false
// 4. 判断是否还有数据
if (results.length) {
articleComment.offset = data.data.last_id // 更新获取下一页数据的页码
} else {
articleComment.finished = true // 没有数据了,关闭加载更多
}
}
3、模板绑定
<!-- 评论列表 -->
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" + :key="item.com_id.toString()">
<van-image
slot="icon"
round
width="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-image
slot="icon"
class="avatar"
round
fit="cover"
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
<div slot="title" class="title-wrap">
<div class="user-name">用户名称</div>
<van-button
class="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-button
class="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-image
slot="icon"
class="avatar"
round
fit="cover"
:src="comment.aut_photo"
/>
<div slot="title" class="title-wrap">
<div class="user-name">{
{ comment.aut_name }}</div>
<van-button
class="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-button
class="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-icon
slot="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_liking
this.$toast('操作成功')
}
发布文章评论
准备弹出层
封装组件
<template>
<div class="comment-post">
<van-field
class="post-field"
v-model="message"
rows="2"
autosize
type="textarea"
maxlength="50"
placeholder="请输入留言"
show-word-limit
/>
<van-button
class="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-popup
v-model="isPostShow"
position="bottom"
/>
<!-- /发布文章评论 -->
提示:不设置高度的时候,内容会自动撑开弹层高度
2、点击发评论按钮的时候显示弹层
<van-button
class="write-btn"
type="default"
round
size="small"
@click="isPostShow = true"
>写评论</van-button>
二、封装发布评论组件
1、创建 post-comment.vue
<template>
<div class="post-comment">
<van-field
class="post-field"
v-model="message"
rows="2"
autosize
type="textarea"
maxlength="50"
placeholder="优质评论将会被优先展示"
show-word-limit
/>
<van-button
type="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-popup
v-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, // 持续展示 toast
message: '发布中...',
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-popup
v-model="isReplyShow"
position="bottom"
style="height: 95%"
>
评论回复
</van-popup>
<!-- /评论回复 -->
二、当点击评论项组件中的回复按钮的时候展示弹层
1、在 comment-item.vue
组件中点击回复按钮的时候,对外发布自定义事件
<van-button
size="mini"
type="default"
@click="$emit('click-reply')"
>回复 {
{ comment.reply_count }}</van-button>
2、在详情页组件中使用的位置监听处理
<comment-item
v-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-icon
slot="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-button
size="mini"
type="default"
@click="$emit('click-reply', comment)"
>回复 {
{ comment.reply_count }}</van-button>
2、在文章详情组件中接收处理
data () {
return {
...
currentComment: {
} // 点击回复的那个评论对象
}
}
<comment-item
v-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-popup
v-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-icon
slot="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-popup
v-model="isReplyShow"
get-container="body"
round
position="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-image
slot="icon"
round
width="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-icon
slot="left"
name="cross"
@click="$emit('click-close')"
/>
</van-nav-bar>
<!-- /导航栏 -->
<!-- 当前评论项 -->
<comment-item :comment="comment" />
<!-- /当前评论项 -->
<van-cell title="所有回复" />
<!-- 评论的回复列表 -->
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<comment-item
v-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或评论id
offset: this.offset, // 获取评论数据的偏移量,值为评论id,表示从此id的数据向后取,不传表示从第一页开始读取数据
limit: this.limit // 获取的评论数据个数,不传表示采用后端服务设定的默认每页数据量
})
// 2. 将数据添加到列表中
const {
results } = data.data
this.list.push(...results)
// 3. 关闭 loading
this.loading = false
// 4. 判断数据是否加载完毕
if (results.length) {
this.offset = data.data.last_id
} else {
this.finished = true
}
}
}
}
</script>
<style scoped></style>
解决弹层中组件内容不更新问题
弹层组件:
- 如果初始的条件是 false,则弹层的内容不会渲染
- 程序运行期间,当条件变为 true 的时候,弹层才渲染了内容
- 之后切换弹层的展示,弹层只是通过 CSS 控制隐藏和显示
弹层渲染出来以后就只是简单的切换显示和隐藏,里面的内容也不再重新渲染了,所以会导致我们的评论的回复列表不会动态更新了。解决办法就是在每次弹层显示的时候重新渲染组件。
<!-- 评论回复 -->
<van-popup
v-model="isReplyShow"
get-container="body"
round
position="bottom"
:style="{ height: '90%' }"
>
<!-- 回复列表 -->
<comment-reply :comment="currentComment" + v-if="isReplyShow" />
<!-- /回复列表 -->
</van-popup>
<!-- 评论回复 -->
发布回复
九、用户页面
该模块作为学生作业,不讲解,仅供源码参考。
创建组件并配置路由
1、创建 views/user/index.vue
<template>
<div class="user-container">用户页面</div>
</template>
<script>
export default {
name: 'UserPage',
components: {
},
props: {
},
data () {
return {
}
},
computed: {
},
watch: {
},
created () {
},
mounted () {
},
methods: {
}
}
</script>
<style scoped></style>
2、配置到根路由
{
path: '/user/:userId',
name: 'user',
component: () => import('@/views/user')
}
最后访问 /user/用户ID
测试。
页面布局
<template>
<div class="user-container">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
left-arrow
title="用户名"
@click-left="$router.back()"
></van-nav-bar>
<!-- /导航栏 -->
<div class="user-info">
<div class="base-info">
<van-image
class="avatar"
round
fit="cover"
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
<div class="right-area">
<div class="stats-wrap">
<div class="stats-item">
<span class="count">66</span>
<span class="text">发布</span>
</div>
<div class="stats-item">
<span class="count">88</span>
<span class="text">关注</span>
</div>
<div class="stats-item">
<span class="count">28</span>
<span class="text">粉丝</span>
</div>
<div class="stats-item">
<span class="count">160</span>
<span class="text">获赞</span>
</div>
</div>
<van-button class="follow-btn">关注</van-button>
</div>
</div>
<div class="label-info">
<div class="bio-wrap">
<span class="label">简介:</span>
<span class="text">这是用户简介</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'UserIndex',
components: {
},
props: {
userId: {
type: [Number, String, Object],
required: true
}
},
data () {
return {
}
},
computed: {
},
watch: {
},
created () {
},
mounted () {
},
methods: {
}
}
</script>
<style scoped lang="less">
.user-container {
.user-info {
background-color: #fff;
padding: 25px 32px;
.base-info {
display: flex;
margin-bottom: 25px;
.avatar {
width: 155px;
height: 155px;
margin-right: 62px;
}
}
.label-info {
font-size: 25px;
.bio-wrap {
.label {
color: #646263;
}
.text {
color: #212121;
}
}
}
}
.right-area {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-evenly;
.stats-wrap {
display: flex;
justify-content: space-between;
.stats-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.count {
font-size: 26px;
color: #0d0a10;
}
.text {
font-size: 21px;
color: #9c9b9d;
}
}
}
.follow-btn {
width: 289px;
height: 55px;
line-height: 55px;
background-color: #6bb5ff;
color: #fff;
border: none;
}
}
}
</style>
<template>
<div class="user-container">
<!-- 导航栏 -->
<van-nav-bar title="黑马头条号" left-arrow />
<!-- /导航栏 -->
<!-- 用户信息 -->
<div class="user-info-container">
<div class="row1">
<van-image
class="col1"
fit="cover"
round
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
<div class="col2">
<div class="row1">
<div class="item">
<div class="count">123</div>
<div class="text">发布</div>
</div>
<div class="item">
<div class="count">123</div>
<div class="text">关注</div>
</div>
<div class="item">
<div class="count">123</div>
<div class="text">粉丝</div>
</div>
<div class="item">
<div class="count">123</div>
<div class="text">获赞</div>
</div>
</div>
<div class="action">
<van-button
type="primary"
size="small"
>私信</van-button>
<van-button
type="default"
size="small"
>编辑资料</van-button>
</div>
</div>
</div>
<div class="intro-wrap">
<div>
<span>认证:</span>
<span>用户的认证信息</span>
</div>
<div>
<span>简介:</span>
<span>用户的简介信息</span>
</div>
</div>
</div>
<!-- /用户信息 -->
<!-- 文章列表 -->
<!-- /文章列表 -->
</div>
</template>
<script>
export default {
name: 'UserPage',
components: {
},
props: {
},
data () {
return {
}
},
computed: {
},
watch: {
},
created () {
},
mounted () {
},
methods: {
}
}
</script>
<style scoped lang="less">
.user-container {
font-size: 14px;
.user-info-container {
padding: 12px;
background-color: #fff;
margin-bottom: 10px;
>.row1 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.item {
text-align: center;
.text {
font-size: 12px;
}
}
>.col1 {
width: 80px;
height: 80px;
}
>.col2 {
display: flex;
flex-direction: column;
justify-content: space-evenly;
width: 70%;
height: 80px;
padding: 0 12px;
>.row1 {
display: flex;
justify-content: space-between;
}
.action {
display: flex;
justify-content: space-between;
.van-button {
width: 45%;
}
}
}
}
}
}
</style>
展示用户信息
步骤:
- 封装数据接口
- 请求获取数据
- 模板绑定
1、在 api/user.js
中添加获取指定用户信息的数据接口
// 获取指定用户信息
export const getUserById = userId => {
return request({
method: 'GET',
url: `/app/v1_0/users/${
userId}`
})
}
2、在用户页面中请求获取数据
+ import {
getUserById } from '@/api/user'
export default {
name: 'UserPage',
components: {
},
props: {
},
data () {
return {
+ user: {
} // 用户信息
}
},
computed: {
},
watch: {
},
created () {
+ this.loadUser()
},
mounted () {
},
methods: {
+++ async loadUser () {
try {
const {
data } = await getUserById(this.$route.params.userId)
this.user = data.data
} catch (err) {
console.log(err)
this.$toast('获取用户数据失败')
}
}
}
}
3、模板绑定
用户关注
展示用户文章列表
列表组件
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
v-for="item in list"
:key="item"
:title="item"
/>
</van-list>
export default {
data() {
return {
list: [],
loading: false,
finished: 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);
}
}
}
分析列表组件使用
List 的运行机制是什么?
List 会监听浏览器的滚动事件并计算列表的位置,当列表底部与可视区域的距离小于offset时,List 会触发一次 load 事件。
为什么 List 初始化后会立即触发 load 事件?
List 初始化后会触发一次 load 事件,用于加载第一屏的数据,这个特性可以通过`immediate-check`属性关闭。
为什么会连续触发 load 事件?
如果一次请求加载的数据条数较少,导致列表内容无法铺满当前屏幕,List 会继续触发 load 事件,直到内容铺满屏幕或数据全部加载完成。因此你需要调整每次获取的数据条数,理想情况下每次请求获取的数据条数应能够填满一屏高度。
loading 和 finished 分别是什么含义?
List
有以下三种状态,理解这些状态有助于你正确地使用List
组件:
- 非加载中,
loading
为false
,此时会根据列表滚动位置判断是否触发load
事件(列表内容不足一屏幕时,会直接触发)- 加载中,
loading
为true
,表示正在发送异步请求,此时不会触发load
事件- 加载完成,
finished
为true
,此时不会触发load
事件在每次请求完毕后,需要手动将
loading
设置为false
,表示本次 load 加载结束
使用 float 布局后一直触发加载?
若 List 的内容使用了 float 布局,可以在容器上添加van-clearfix类名来清除浮动,使得 List 能正确判断元素位置
展示文章列表
1、封装获取用户文章列表的数据接口
/**
* 获取指定用户的文章列表
*/
export const getArticlesByUser = (userId, params) => {
return request({
method: 'GET',
url: `/app/v1_0/users/${
userId}/articles`,
params
})
}
2、在用户页面中请求获取数据
import {
getUserById } from '@/api/user'
+ import {
getArticlesByUser } from '@/api/article'
export default {
name: 'UserPage',
components: {
},
props: {
},
data () {
return {
user: {
}, // 用户信息
list: [], // 列表数据
loading: false, // 控制上拉加载更多的 loading
finished: false, // 控制是否加载结束了
+ page: 1 // 获取下一页数据的页码
}
},
computed: {
},
watch: {
},
created () {
this.loadUser()
},
mounted () {
},
methods: {
async loadUser () {
try {
const {
data } = await getUserById(this.$route.params.userId)
this.user = data.data
} catch (err) {
console.log(err)
this.$toast('获取用户数据失败')
}
},
+++ async onLoad () {
// 1. 请求获取数据
const {
data } = await getArticlesByUser(this.$route.params.userId, {
page: this.page, // 可选的,默认是第 1 页
per_page: 20 // 可选的,默认每页 10 条
})
// 2. 把数据添加到列表中
// list []
// data.data.results []
// ...[1, 2, 3] 会把数组给展开,所谓的展开就是一个一个的拿出来
const {
results } = data.data
this.list.push(...results)
// 3. 加载状态结束
this.loading = false
// 4. 判断数据是否全部加载完毕
if (results.length) {
this.page++ // 更新获取下一页数据的页码
} else {
this.finished = true // 没有数据了,不需要加载更多了
}
}
}
}
十、用户关注 & 粉丝
该模块作为学生作业,不讲解,仅供源码参考。
十一、我的收藏/历史
该模块作为学生作业,不讲解,仅供源码参考。
十二、编辑用户资料
创建组件并配置路由
1、创建 views/user/index.vue
<template>
<div>
<van-nav-bar title="个人信息" left-arrow right-text="保存" />
<van-cell-group>
<van-cell title="头像" is-link>
<van-image
round
width="30"
height="30"
fit="cover"
src="http://toutiao.meiduo.site/FgSTA3msGyxp5-Oufnm5c0kjVgW7"
/>
</van-cell>
<van-cell title="昵称" value="abc" is-link />
<van-cell title="性别" value="男" is-link />
<van-cell title="生日" value="2019-9-27" is-link />
</van-cell-group>
</div>
</template>
<script>
export default {
name: "UserIndex"
};
</script>
2、将该页面配置到根路由
{
name: 'user-profile',
path: '/user/profile',
component: () => import('@/views/user-profile')
}
页面布局
<template>
<div class="user-profile">
<!-- 导航栏 -->
<van-nav-bar
class="page-nav-bar"
title="个人信息"
left-arrow
@click-left="$router.back()"
/>
<!-- /导航栏 -->
<!-- 个人信息 -->
<van-cell class="avatar-cell" title="头像" is-link center>
<van-image
class="avatar"
round
fit="cover"
src="https://img.yzcdn.cn/vant/cat.jpeg"
/>
</van-cell>
<van-cell title="昵称" value="内容" is-link />
<van-cell title="性别" value="内容" is-link />
<van-cell title="生日" value="内容" is-link />
<!-- /个人信息 -->
</div>
</template>
<script>
export default {
name: 'UserProfile',
components: {
},
props: {
},
data () {
return {
}
},
computed: {
},
watch: {
},
created () {
},
mounted () {
},
methods: {
}
}
</script>
<style scoped lang="less">
.user-profile {
.avatar-cell {
.van-cell__value {
display: flex;
flex-direction: row-reverse;
}
.avatar {
width: 60px;
height: 60px;
}
}
}
</style>
展示用户信息
思路:
- 找到数据接口
- 封装请求方法
- 请求获取数据
- 模板绑定
1、在 api/user.js
中添加封装数据接口
2、在 views/user/index.vue
组件中请求获取数据
3、模板绑定
修改昵称
一、准备弹出层
二、封装组件
三、页面布局
四、基本功能处理
五、更新完成
修改性别
修改生日
修改头像
图片上传预览
方式一:结合服务器的图片上传预览
方式二:纯客户端实现上传图片预览
// 获取文文件对象
const file = fileInput.files[0]
// 设置图片的 src
img.src = window.URL.createObjectURL(file)
客户端上传预览示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>客户端图片上传预览示例</title>
<style>
.img-wrap {
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
img {
max-width: 100%;
}
</style>
</head>
<body>
<h1>客户端图片上传预览示例</h1>
<div class="img-wrap">
<img src="" alt="" id="img">
</div>
<br>
<input type="file" id="file" onchange="onFileChange()">
<script>
const img = document.querySelector('#img')
const file = document.querySelector('#file')
function onFileChange() {
// 得到 file-input 的文件对象
const fileObj = file.files[0]
const data = window.URL.createObjectURL(fileObj)
img.src = data
}
</script>
</body>
</html>
接下来就是在项目中使用纯客户端的方式处理用户头像上传预览。
在 views/user/index.vue
组件中:
<template>
<div>
<van-nav-bar title="个人信息" left-arrow right-text="保存" />
<van-cell-group>
+
<van-cell title="头像" is-link @click="onChangePhoto">
<van-image round width="30" height="30" :src="user.photo" />
</van-cell>
<van-cell title="昵称" :value="user.name" is-link />
<van-cell title="性别" :value="user.gender === 0 ? '男' : '女'" is-link />
<van-cell title="生日" :value="user.birthday" is-link />
</van-cell-group>
<!--
表单元素的 hidden 表示隐藏这个表单元素
-->
+ <input type="file" hidden ref="file" @change="onFileChange" />
</div>
</template>
<script>
import {
getProfile } from '@/api/user'
export default {
name: 'UserIndex',
data () {
return {
user: {
}
}
},
created () {
this.loadUserProfile()
},
methods: {
async loadUserProfile () {
const {
data } = await getProfile()
this.user = data.data
},
+ onChangePhoto () {
this.$refs.file.click()
},
+ onFileChange () {
this.user.photo = window.URL.createObjectURL(this.$refs.file.files[0])
}
}
}
</script>
头像裁切
方案一:结合服务端的图片裁切上传流程
方案二:纯客户端的图片裁切上传流程
viewMode: 1,
dragMode: 'move',
aspectRatio: 1,
autoCropArea: 1,
cropBoxMovable: false,
cropBoxResizable: false,
background: false,
movable: true
保存更新
如果是基于服务端的裁切,则使用:getData 方法,该方法得到裁切的区域参数。
如果是纯客户端的图片裁切,则使用:getCroppedCanvas 方法,该方法得到裁切之后的图片对象(类似于URL.createObjectURL 方法得到的文件对象)。
步骤:
- 封装接口
- 请求提交
- 更新视图
1、在 api/user.js
中添加封装数据接口
/**
* 1、接口重用
* 2、接口维护
* 实际开发过程中,接口经常容易变化,
* 你能记住你在哪里请求了这个接口吗?很难记住
* 怎么办呢?
* 我们单独的把接口请求的代码封装起来,统一的放到一起。
* 放到哪里呢?
* 我们放到项目的 api 目录中,
* 根据接口的种类进行归类管理
* 例如用户相关的接口放到 user.js 中
* 文章相关的接口放到 article.js 中
* 如何封装呢?
* 一个函数,接收参数,返回请求结果就可以了。
* 用户接口相关请求模块
*/
import request from '@/utils/request'
/**
* 登录
*/
export function login (user) {
return request({
// method 用来指定请求方法,这是 axios 的固定 API,不能瞎写
method: 'POST',
// url 用来指定请求路径
url: '/app/v1_0/authorizations',
// data 是 axios 请求要求的字段名,用来传递请求体数据的
data: user
})
}
/**
* 关注用户
*/
export function followUser (userId) {
return request({
method: 'POST',
url: '/app/v1_0/user/followings',
data: {
target: userId // 用户id
}
})
}
/**
* 取消关注用户
*/
export function unFollowUser (userId) {
return request({
method: 'DELETE',
url: '/app/v1_0/user/followings/' + userId
})
}
/**
* 获取用户自己信息
*/
export function getSelf (userId) {
return request({
method: 'GET',
url: '/app/v1_0/user'
})
}
/**
* 获取用户个人资料
*/
export function getProfile (userId) {
return request({
method: 'GET',
url: '/app/v1_0/user/profile'
})
}
/**
* 更新用户头像
*/
+ export function updateUserPhoto (data) {
return request({
method: 'PATCH',
url: '/app/v1_0/user/photo',
data
})
}
2、在 views/user/index.vue
组件中保存提交
<template>
<div>
<van-nav-bar
title="个人信息"
left-arrow
right-text="保存"
+
@click-right="onSave"
/>
<van-cell-group>
<van-cell title="头像" is-link @click="onChangePhoto">
<van-image round width="30" height="30" :src="user.photo" />
</van-cell>
<van-cell title="昵称" :value="user.name" is-link />
<van-cell title="性别" :value="user.gender === 0 ? '男' : '女'" is-link />
<van-cell title="生日" :value="user.birthday" is-link />
</van-cell-group>
<!--
表单元素的 hidden 表示隐藏这个表单元素
-->
<input type="file" hidden ref="file" @change="onFileChange" />
</div>
</template>
<script>
+ import {
getProfile, updateUserPhoto } from '@/api/user'
export default {
name: 'UserIndex',
data () {
return {
user: {
}
}
},
created () {
this.loadUserProfile()
},
methods: {
async loadUserProfile () {
const {
data } = await getProfile()
this.user = data.data
},
onChangePhoto () {
this.$refs.file.click()
},
onFileChange () {
const fileObj = this.$refs.file.files[0]
if (fileObj) {
this.user.photo = window.URL.createObjectURL(fileObj)
}
},
+ async onSave () {
// 如果 Content-Type 要求是 application/json ,则 data 传普通对象 {}
// 如果 Content-Type 要求是 multipart/form-data ,则 data 传 FormData 对象
// 纵观所有数据接口,你会发现大多数的接口都要求 Content-Type 要求是 application/json
// 一般只有涉及到文件上传的数据接口才要求Content-Type 要求是 multipart/form-data
// 这个时候传递一个 FormData 对象
this.$toast.loading({
duration: 0, // 持续展示 toast
forbidClick: true, // 禁用背景点击
loadingType: 'spinner',
message: '保存中'
})
try {
const formData = new FormData()
// formData.append('名字', 数据)
formData.append('photo', this.$refs.file.files[0])
await updateUserPhoto(formData)
this.$toast.success('保存成功')
} catch (err) {
console.log(err)
this.$toast.fail('保存失败')
}
}
}
}
</script>
ta 是 axios 请求要求的字段名,用来传递请求体数据的
data: user
})
}
/**
- 关注用户
*/
export function followUser (userId) {
return request({
method: ‘POST’,
url: ‘/app/v1_0/user/followings’,
data: {
target: userId // 用户id
}
})
}
/**
- 取消关注用户
*/
export function unFollowUser (userId) {
return request({
method: ‘DELETE’,
url: ‘/app/v1_0/user/followings/’ + userId
})
}
/**
- 获取用户自己信息
*/
export function getSelf (userId) {
return request({
method: ‘GET’,
url: ‘/app/v1_0/user’
})
}
/**
- 获取用户个人资料
*/
export function getProfile (userId) {
return request({
method: ‘GET’,
url: ‘/app/v1_0/user/profile’
})
}
/**
- 更新用户头像
*/
- export function updateUserPhoto (data) {
return request({
method: ‘PATCH’,
url: ‘/app/v1_0/user/photo’,
data
})
}
2、在 `views/user/index.vue` 组件中保存提交
```html
<template>
<div>
<van-nav-bar
title="个人信息"
left-arrow
right-text="保存"
+
@click-right="onSave"
/>
<van-cell-group>
<van-cell title="头像" is-link @click="onChangePhoto">
<van-image round width="30" height="30" :src="user.photo" />
</van-cell>
<van-cell title="昵称" :value="user.name" is-link />
<van-cell title="性别" :value="user.gender === 0 ? '男' : '女'" is-link />
<van-cell title="生日" :value="user.birthday" is-link />
</van-cell-group>
<!--
表单元素的 hidden 表示隐藏这个表单元素
-->
<input type="file" hidden ref="file" @change="onFileChange" />
</div>
</template>
<script>
+ import { getProfile, updateUserPhoto } from '@/api/user'
export default {
name: 'UserIndex',
data () {
return {
user: {}
}
},
created () {
this.loadUserProfile()
},
methods: {
async loadUserProfile () {
const { data } = await getProfile()
this.user = data.data
},
onChangePhoto () {
this.$refs.file.click()
},
onFileChange () {
const fileObj = this.$refs.file.files[0]
if (fileObj) {
this.user.photo = window.URL.createObjectURL(fileObj)
}
},
+ async onSave () {
// 如果 Content-Type 要求是 application/json ,则 data 传普通对象 {}
// 如果 Content-Type 要求是 multipart/form-data ,则 data 传 FormData 对象
// 纵观所有数据接口,你会发现大多数的接口都要求 Content-Type 要求是 application/json
// 一般只有涉及到文件上传的数据接口才要求Content-Type 要求是 multipart/form-data
// 这个时候传递一个 FormData 对象
this.$toast.loading({
duration: 0, // 持续展示 toast
forbidClick: true, // 禁用背景点击
loadingType: 'spinner',
message: '保存中'
})
try {
const formData = new FormData()
// formData.append('名字', 数据)
formData.append('photo', this.$refs.file.files[0])
await updateUserPhoto(formData)
this.$toast.success('保存成功')
} catch (err) {
console.log(err)
this.$toast.fail('保存失败')
}
}
}
}
</script>