本文已参与「新人创作礼」活动,一起开启掘金创作之路。
之前章节传送:
【Vue项目实战】vue.js2.5 饿了么APP(1)概述+项目准备 juejin.cn/post/708900…
【Vue项目实战】vue.js2.5饿了么APP(2)主要组件实现 - 头部相关组件 - 【速看】面试教你有话可说 juejin.cn/post/708932…
【Vue项目实战】vue.js2.5饿了么APP(3)主要组件实现 - 购物车相关组件(上)juejin.cn/post/708900…
【Vue项目实战】vue.js2.5饿了么APP(4)主要组件实现 - 购物车相关组件(下)juejin.cn/post/709075… 【Vue项目实战】vue.js2.5饿了么APP(5)主要组件实现 - 商品详情页部分 juejin.cn/post/709627…
速看
1. 商家评价页面 商家评价页面主要是显示商家评分以及评论,复用了rating-select组件。其中需要从seller接口获取评价数据。
2. 商家详情页面 商家详情页主要包括:商家具体信息(评分 销量)+ 公告活动 + 商家实景 + 商家额外信息 + 页面收藏。其中收藏页面本地缓存的设计思想:收藏的效果就是,点击按钮切换图标的显示状态,但是问题是,当再次刷新页面,状态会被重新初始化。由于项目只提供了一个商家的信息,并且不包含id。因此,希望通过url解析出包含id值的一个对象,在每次请求时加上id进行模拟收藏效果。为了实现本地存储,使用了封装了sessionstorage的storage库,提供了set get接口,和直接调用原生api 需要对对象和字符串进行转换相比,使用起来更简单,可以直接对对现象操作。导出两个接口saveLocal和loadLocal,来实现本地缓存。saveload中构造一个复杂对象,第一层是id,它还是一个对象,包含收藏和其他需要存储的项,然后返回该对象。最后的效果就是:点击收藏之后会在localstorage中添加一个__seller__,会显示id,以及favorite的true false。
小掘友的话:本文充斥着很口语化的字样,在于友友希望做完一个项目之后可以很好的表达出来,面试被提问到的时候也有话可说、能够表达清除。
一、商家评价页面(ratings)
1. 概述
商家评价页面主要是显示商家评分以及评论,复用了rating-select组件。其中需要从seller接口获取评价数据。
2. 布局
评价页主要分两个部分:整体介绍(评分)+ 评价部分(rating-select组件 + ratings列表)
<div class="ratings-content">
<div class="overview">
<div class="overview-left">
<h1 class="score">{{seller.score}}</h1>
<div class="title">综合评分</div>
<div class="rank">高于周边商家{{seller.rankRate}}%</div>
</div>
<div class="overview-right">
<div class="score-wrapper">
<span class="title">服务态度</span>
<star :size="36" :score="seller.serviceScore"></star>
<span class="score">{{seller.serviceScore}}</span>
</div>
<div class="score-wrapper">
<span class="title">商品评分</span>
<star :size="36" :score="seller.foodScore"></star>
<span class="score">{{seller.foodScore}}</span>
</div>
<div class="delivery-wrapper">
<span class="title">送达时间</span>
<span class="delivery">{{seller.deliveryTime}}</span>
</div>
</div>
</div>
<split></split>
<rating-select
:ratings="ratings"
:onlyContent="onlyContent"
:selectType="selectType"
@select="onSelect"
@toggle="onToggle"
v-if="ratings.length">
</rating-select>
<div class="rating-wrapper">
<ul>
<li
v-for="(rating, index) in computedRatings"
:key="index"
class="rating-item border-bottom-1px">
<div class="avatar">
<img width="28" height="28" :src="rating.avatar">
</div>
<div class="content">
<h1 class="name">{{rating.username}}</h1>
<div class="star-wrapper">
<star :size="24" :score="rating.score"></star>
<span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span>
</div>
<p class="text">{{rating.text}}</p>
<div class="recommend" v-show="rating.recommend && rating.recommend.length">
<span class="icon-thumb_up"></span>
<span
class="item"
v-for="(item, index) in rating.recommend"
:key="index">{{item}}
</span>
</div>
<div class="time">{{format(rating.rateTime)}}</div>
</div>
</li>
</ul>
</div>
</div>
</cube-scroll>
3. 实现
(1)数据获取
对于整体介绍部分overview部分的数据都是通过seller取的。 为了拿到评价的数据,注意需要导出接口,而ratings有单独接口,在index.js中写入,并在ratings组件中导入import { getRatings } from 'api'
const getSeller = get('/api/seller')
const getGoods = get('/api/goods')
const getRatings = get('/api/ratings')
export {
getSeller,
getGoods,
getRatings
}
getRatings何时使用:会在每次tab切换的时候执行fetch钩子方法(获取数据)
因此在ratings中添加方法fetch,避免多次获取数据(只获取一次就可以了)
if (!this.fetched) {
this.fetched = true
getRatings().then((ratings) => {
this.ratings = ratings
})
}
},
(2)滑动的实现
为了实现滑动,使用了cube-scroll组件,并作默认设置(防止横向纵向滚动有冲突)
data: {
type: Object
}
},
data() {
return {
ratings: [],
scrollOptions: {
click: false,
directionLockThreshold: 0
}
}
},
(3)评价部分
商家评论列表部分和商品评价部分设计思路相同,但是没有做成大的评论组件,在于DOM结构还是有区别的。
(4)使用rating-select组件
新建ratings的mixin抽离出评价的公共部分,组件的使用如布局所示
export default {
data() {
return {
onlyContent: true,
selectType: ALL
}
},
computed: {
computedRatings() {
const ret = []
this.ratings.forEach((rating) => {
if (this.onlyContent && !rating.text) {
return
}
if (this.selectType === ALL || this.selectType === rating.rateType) {
ret.push(rating)
}
})
return ret
}
},
methods: {
onSelect(type) {
this.selectType = type
},
onToggle() {
this.onlyContent = !this.onlyContent
}
}
}
(5)优化
当点击评价时先展开rating-select然后渲染出评价部分,但是这样时不友好的,当没有ratings时是没有rating-select的,因此在组件上添加v-if="ratings.length",这时,只有评论加载完成之后才会渲染rating-select组件。
二、商家详情页面(seller)
1.概述
商家详情页主要包括:商家具体信息(评分 销量)+ 公告活动 + 商家实景 + 商家额外信息 + 页面收藏。
收藏页面本地缓存的设计思想:收藏的效果就是,点击按钮切换图标的显示状态,但是问题是,当再次刷新页面,状态会被重新初始化。由于项目只提供了一个商家的信息,并且不包含id。因此,希望通过url解析出包含id值的一个对象,在每次请求时加上id进行模拟收藏效果。为了实现本地存储,使用了封装了sessionstorage的storage库,提供了set get接口,和直接调用原生api 需要对对象和字符串进行转换相比,使用起来更简单,可以直接对对现象操作。导出两个接口saveLocal和loadLocal,来实现本地缓存。saveload中构造一个复杂对象,第一层是id,它还是一个对象,包含收藏和其他需要存储的项,然后返回该对象。最后的效果就是:点击收藏之后会在localstorage中添加一个__seller__,会显示id,以及favorite的true false。
2.布局
商家具体信息(评分 销量)+ 公告活动 + 商家实景 + 商家额外信息
其中使用了star组件 split组件等,主要是静态页面,没有着重要说的部分
<div class="seller-content">
<div class="overview">
<h1 class="title">{{seller.name}}</h1>
<div class="desc border-bottom-1px">
<star :size="36" :score="seller.score"></star>
<span class="text">({{seller.ratingCount}})</span>
<span class="text">月售{{seller.sellCount}}单</span>
</div>
<ul class="remark">
<li class="block">
<h2>起送价</h2>
<div class="content">
<span class="stress">{{seller.minPrice}}</span>元
</div>
</li>
<li class="block">
<h2>商家配送</h2>
<div class="content">
<span class="stress">{{seller.deliveryPrice}}</span>元
</div>
</li>
<li class="block">
<h2>平均配送时间</h2>
<div class="content">
<span class="stress">{{seller.deliveryPrice}}</span>元
</div>
</li>
</ul>
<div class="favorite">
<span class="icon-favorite" :class="{'active':favorite}"></span>
<span class="text">{{favoriteText}}</span>
</div>
</div>
<split></split>
<div class="bulletin">
<h1 class="title">公告与活动</h1>
<div class="content-wrapper border-bottom-1px">
<p class="content">{{seller.bulletin}}</p>
</div>
<ul v-if="seller.supports" class="supports">
<li
class="support-item border-bottom-1px"
v-for="(item, index) in seller.supports"
:key="index">
<support-ico :size=4 :type="seller.supports[index].type"></support-ico>
<span class="text">{{seller.supports[index].description}}</span>
</li>
</ul>
</div>
<split></split>
<div class="pics">
<h1 class="title">商家实景</h1>
<cube-scroll class="pic-wrapper" :options="picScrollOptions">
<ul class="pic-list">
<li class="pic-item"
v-for="(pic, index) in seller.pics"
:key="index">
<img :src="pic" width="120" height="90">
</li>
</ul>
</cube-scroll>
</div>
<split></split>
<div class="info">
<h1 class="title border-bottom-1px">商家信息</h1>
<ul>
<li
class="info-item border-bottom-1px"
v-for="(info, index) in seller.infos"
:key="index">
{{info}}
</li>
</ul>
</div>
</div>
</cube-scroll>
3. 实现
(1)滚动的实现
使用cube-scroll组件实现滚动,其中商家实景部分也需要使用cube-scroll,使用纵向滚动,其中横向滚动时每一个li的布局是inline-block,并且配置scrollX: true, 并且阻止滚动的冒泡。横向滚动和纵向滚动不影响。其中默认配置:
return {
favorite: false,
sellerScrollOptions: {
directionLockThreshold: 0,
click: false
},
picScrollOptions: {
scrollX: true,
stopPropagation: true,
directionLockThreshold: 0
}
}
},
(2)收藏页面设计思路
思路:在收藏商家的时候,不同的商家回有不同的标识如id,在url中有一个字段来区分(如?id=1或?id=2)对于请求不同的数据,商家信息也是不同的,因此需要解析出url中的id参数作为异步接口的参数告诉服务端服务端通过接口拿到id再根据不同的id取不同数据返回给前端。(但是由于本项目中模拟数据是相同的,因此返回的数据也是一样的)
(3)区分id
1)可以手写解析或者使用query-string库,npm安装之后在app.vue写入import qs from 'query-string'
return {
seller: {
id: qs.parse(location.search).id
}
}
},
2)这时就可以拿到id,为了将id作为接口的请求参数传递给服务端,因此修改方法 ,此时可以看到id作为参数已经传入
_getSeller() {
getSeller({
id: this.seller.id
}).then((seller) => {
this.seller = seller
})
}
},
3)同理,修改其他组件goods组件 ratings组件。之后可以看到效果seller和goods都可以传入参数,但是ratings没有,
if (!this.fetched) {
this.fetched = true
getGoods({
id: this.seller.id
}).then((goods) => {
this.goods = goods
})
}
},
if (!this.fetched) {
this.fetched = true
getRatings({
id: this.seller.id
}).then((ratings) => {
this.ratings = ratings
})
}
},
4)修改vue.config.js文件中的devServer部分代码,从而实现了前后端联动
app.get('/api/seller', function (req, res) {
res.json({
errno: 0,
data: seller
})
})
// 修改后
app.get('/api/seller', function (req, res) {
const id = req.query.id
res.json({
errno: 0,
data: Object.assign({}, seller, { id })
})
})
(4)添加收藏方法
文案“收藏”“已收藏”以及效果切换,但是效果是刷新页面就会丢失
this.favorite = !this.favorite
}
(5)本地缓存实现思路
在common中添加js文件,并创建storage.js
对于项目扩展可以使用本地缓存去存储其他东西,都可以复用通用存储的功能。 使用一个good-storage库,提供了set get接口,并且对sessionstorage做了封装,好处在于相对于直接使用原生API,存在两个问题:(1)首先需要测试storage可不可以用,在隐身模式操作loaclstorage和sessionstorageAPI会报错。(2)当存储对象的时候,需要先把对象转化为字符串存储,并且获取数据时需要利用json.pase转换成对象。
(6)导出存储通用接口
在商家页面定义一个SELLER_KEY
导出两个接口:saveToLocal(id, key, value)。逻辑:构造一个复杂对象,首先第一层是id,用于区分不同的商家,他是一个对象(因为对于一个商家可以存储不同的key,比如收藏favorite、评价等),因此先取到它,然后对其修改,然后把对象作为整体存储。
loadFromLocal(id, key, def)。逻辑:读取时如果读不到,直接取空对象。如果没有定义id就返回默认值,如果定义了id,如果没有key就取默认值,有key就返回key
const SELLER_KEY = '__seller__'
export function saveToLocal(id, key, val) {
const seller = storage.get(SELLER_KEY, {})
if (!seller[id]) {
seller[id] = {}
}
seller[id][key] = val
storage.set(SELLER_KEY, seller)
}
export function loadFromLocal(id, key, def) {
const seller = storage.get(SELLER_KEY, {})
if (!seller[id]) {
return def
}
return seller[id][key] || def
}
(7)存储方法使用
在seller中可以使用两个方法,添加created
this.favorite = loadFromLocal(this.seller.id, KEY, false)
},
methods: {
toggleFavorate() {
this.favorite = !this.favorite
saveToLocal(this.seller.id, KEY, this.favorite)
}
},
效果:点击“收藏”后会显示存储,切换id=1在收藏local storage中会添加,并且保存状态。