赞
踩
小贴士:
1.若想查询vue/cli的版本号,在终端输入:
vue -V
2.在cmd命令行重新安装了3.2.1的Vue:
npm install @vue/cli@3.2.1 -g
3.安装了3.0.2版本的vue-router
npm install vue-router@3.0.2
4.使用 li{分类列表$}*100 ,可以得到分类列表1~分类列表100
5.用户片段
"Print to console": { "prefix": "vue", "body": [ // "<script src=\"../js/vue.js\"></script>", "<template>", "", "</template>", "<script>", "\texport default {", "\t\tname: '$TM_FILENAME_BASE'", "\t}", "</script>", "<style scoped>", "", "</style>" ], "description": "Log output to console" }
"Print to console": { "prefix": "vue", "body": [ "<div id='app'>", "</div>", "<script src=\"../js/vue.js\"></script>", "<script>", "\tconst app = new Vue({", "\t\tel: '#app',", "\t\tdata: {", "\t\t\tmessage: '你好呀'", "\t\t}", "\t})", "</script>", ], "description": "Log output to console" }
6.关于VScode标签页的一些问题
这是因为你单击文件名的缘故,这个是“预览模式”,所以再单击其他文件时,会覆盖当前打开的文件。
如果你要每次都打开新tab,那就双击文件名好了。这个逻辑和sublime是一样的。不知道你是不是问的这个事情
预览模式是现在各类编辑器的默认功能,如果你实在不喜欢,可以关掉的。给你配置settings.json
里加一条:
"workbench.editor.enablePreview": false,
7.判断一个对象是否为空的方法
利用Object.keys(obj)可以获得对象的关键字构成的数组,利用if语句判断。若为length为0,则该对象为空。
Object.key(obj).length === 0
git config --global user.email lz4135@qq.com
git config --global user.name This-is-Leon
git remote add origin 远程仓库地址
修改远程仓库地址
git remote set-url origin [url]
查看当前远程仓库地址
git remote -v
1.安装normalize.css文件:
方法一:直接通过命令行下载
npm install normalize.css
方法二:
2.创建自定义的base.css文件
3.在App.vue文件中导入base.css
<style>
@import "./assets/css/base.css";
</style>
4.分析base.css代码
/* :root -> 获取根元素html */
:root {
--color-text: #666;
--color-high-text: #ff5777;
--color-tint: #ff8198;
--color-background: #fff;
--font-size: 14px;
--line-height: 1.5;
/* 这里定义了一些可以使用的变量 */
--large-seize: 50px;
}
body {
background: var(--color-background);
color: var(--color-text);
}
5.对于CLI3和CLI4,若想修改配置,则需要先创建一个vue.config.js文件
module.exports = {
configureWebpack: {
resolve: {
extensions: [],
alias: {
'assets': '@/assets',
'common': '@/common',
'compoents': '@/components',
'network': '@/network',
'views': '@/views'
}
}
}
}
6.创建CLI2的时候会自动生成一个.editorconfig文件,但CLI3之后便不会生成了。该文件里是关于缩进、空格等的一些配置文件。往往同一个项目组内会保持一个相同的编程格式。
即便是不同的项目也会选择使用相同的配置文件,所以这里将前一个axios的.editorconfig文件复制到supermall文件中。
1.在项目文件夹下安装vue-router:
npm install vue-router --save
2.创建router文件夹
创建index.js文件->导入Vue和Vue-router->使用use方法进行调用
如下是index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1.安装插件
Vue.useContext(VueRouter)
// 2.创建router
const router = new VueRouter({
})
export default router
3.在main.js文件中进行调用:
若是cli2,则代码如下:
import router from './router'
new Vue({
el: '#app',
router,
render: h => h(App)
})
若是cli3,则代码如下:
import router from './router'
createApp(App).use(router).mount('#app')
1.在components文件夹的common文件夹中创建导航栏的组件
2.设置具名插槽,用于之后的替换(注意每个插槽都要单独用div封装起来,否则容易出现bug)
<template> <div class="nav-bar"> <div class="left"><slot name="left"></slot></div> <div class="center"><slot name="center"></slot></div> <div class="right"><slot name="right"></slot></div> </div> </template> <script> export default { name: 'NavBar' } </script> <style scoped> .nav-bar { display: flex; height: 44px; line-height: 44px; text-align: center; box-shadow: 0 1px 1px rgba(100, 100, 100, 0.1); /* background-color: pink; */ } .left, .right { width: 60px; /* background-color: skyblue; */ } .center { flex: 1; /* 因为left和right设置了width,那么剩余部分均会被flex占据 */ } </style>
3.在views的Home.vue文件中进行引用
<template> <div id="home"> <nav-bar class="home-nav"> <span slot="center">购物街</span> </nav-bar> </div> </template> <script> import NavBar from '@/components/common/navbar/NavBar' export default { name: 'Home', components: { NavBar } } </script> <style scoped> .home-nav { background-color: var(--color-tint); color: white; } </style>
免费接口:
淘宝商品接口:http://suggest.taobao.com/sug?code=utf-8&q=商品关键字&callback=cb
复制轮播图组件swiper到components文件夹的common文件夹中
在Home.vue中导入
import {Swiper, SwiperItem} from '@/components/common/swiper'
在components中进行注册
export default {
name: 'Home',
components: {
NavBar,
Swiper,
SwiperItem
},
}
在template中进行调用(注意:因为swiper-item标签是自定义的,所以在使用v-for循环进行遍历时,需要添加:key=“index”,否则会不停报错。)
<template>
<div id="home">
<nav-bar class="home-nav">
<span slot="center">购物街</span>
</nav-bar>
<swiper>
<!-- 自定义属性要绑定key -->
<swiper-item v-for="(item, index) in banners" :key="index">
<a :href="item.link">
<img :src="item.image" alt="">
</a>
</swiper-item>
</swiper>
</div>
</template>
1.因为是公共组件,所以将组件放在components/common/content目录下:
2.注意要点如下:
/*TabControl.vue*/ <template> <div class="tab-control"> <div v-for="(item, index) in titles" :key="index" class="tab-control-item" :class="{active:index === currentIndex}" @click="itemClick(index)"> <span>{{item}}</span> </div> </div> </template> <script> export default { name: 'TabControl', props: { titles: { type: Array, default() { return [] } } }, data() { return { currentIndex: 0 } }, methods: { itemClick(index) { this.currentIndex = index; } } } </script> <style scoped> .tab-control { display: flex; text-align: center; font-size: 15px; height: 40px; line-height: 40px; } .tab-control-item { flex: 1; } .active { color: var(--color-high-text); } .active span { border-bottom: 3px solid var(--color-tint); } </style>
导入组件后,使用动态属性输入titles
为了达到向下滑动时,可以起到固定效果。使用position:sticky属性,还可以设置top值,表示在top的距离上固定住
为了防止底部文字穿过组件,设置背景颜色。
/*Home.vue的template*/
<tab-control class="tab-control" :titles="['流行', '新款', '精选']"></tab-control>
/*Home.vue的style*/
.tab-control {
position: sticky;
top: 44px;
/* 防止透明 */
background-color: #fff;
}
将home.vue中的created方法代码封装在methods中,这样created属性的代码就变得简洁明了了。
//全生命周期函数,在组件创建后获取接口数据 created() { // 1.请求多个数据 this.getHomeMultidata() // 2.请求商品数据 this.getHomeGoods() }, methods: { getHomeMultidata() { getHomeMultidata().then(res => { // this.banners = res.jokes; // this.recommends = res.jokes; this.banners = res.data.banner.list; this.recommends = res.data.recommend.list; // console.log(res); }) }, getHomeGoods(type) { const page = this.goods[type].page + 1; //当进行复次调用的时候page就会不断增加 getHomeGoods(type, page).then(res => { //运用扩展运算符把list数据一个一个push进去,相当于for循环的效果 this.goods[type].list.push(...res.data.list); this.goods[type].page += 1; }) } }
getHomeGoods()方法的关键点:
运用扩展运算符将从接口拿到的数据一个一个放到data的goods中:
getHomeGoods(type, page).then(res => {
//运用扩展运算符把list数据一个一个push进去,相当于for循环的效果
this.goods[type].list.push(...res.data.list);
this.goods[type].page += 1;
})
11.CSS布局设置
GoodsList.vue中的goods属性设置:flex-wrap:wrap表示根据宽度多行显示。 justify-content: space-around表示均等分布。
.goods {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
GoodsListItem.vue中的goods-item属性宽度设置为50%,则一行显示两个商品。
.goods-item {
padding-bottom: 40px;
position: relative;
width: 50%;
}
1.安装betterscroll
npm install better-scroll@1.32.2 --save
2.可以使用css自带的滚动,设置一个高度height,再设置overflow:auto或者overflow:scroll。但这种方案的缺点是在移动端是非常卡顿的。
.content {
height: 150px;
background-color: pink;
/* 设置溢出隐藏,即可实现在height=150px范围内的列表滚动 */
overflow: scroll;
}
3.betterscroll的使用,在script标签中,记住是在mounted这个生命周期函数中进行调用,因为mounted之后才将挂载的模板渲染到页面上。
import BScroll from 'better-scroll' export default { name: 'Category', data() { return{ scroll: null } }, mounted() { // console.log(document.querySelector('.wrapper')); this.scroll = new BScroll(document.querySelector('.wrapper'), { }) } }
注意:要刷新的元素的格式如下,wrapper的元素只能包含一个元素content(类名可以随便取)。但content元素内部可以包含很多元素。设置style的class是最外层包裹的类。
<style> .wrapper { height: 200px; background-color: pink; overflow: hidden; } </style> <body> <div class="wrapper"> <ul class="content"> <button class="btn">按钮</button> <li>列表1</li> <li>列表2</li> <li>列表3</li> </ul> </div> </body>
4.bscroll的监听事件
bscroll.on表示监听事件,当需要上拉加载更多时。参数pullUpLoad参数设为true,并进行事件监听。click属性设置为true表示滚动部分的按钮标签是可以监听到的。
<script src="./bscroll.js"></script> <script> // 默认情况下BScroll是不可以实时的监听滚动位置 // probe表示侦测,其参数0、1都是不侦测实时位置。2则是在手指滚动的过程中侦测,手指离开后的惯性滚动过程不侦测。3表示只要是滚动,都侦测。 const bscroll = new BScroll(document.querySelector('.wrapper'), { probeType: 2, click: true, pullUpLoad: true }); bscroll.on('scroll', (position) => { // console.log(position); }) bscroll.on('pullingUp', () => { console.log('上拉加载更多'); // 发送网络请求,请求更多页的数据 // 等数据请求完成,并且将新的数据展示出来后 setTimeout(() => { bscroll.finishPullUp() }, 2000) }) document.querySelector('.btn').addEventListener('click', function() { console.log('-------'); }) </script>
为了防止betterscroll有一天不再进行维护,需要对其进行一个封装处理。当之后需要更换其他的库时,便会方便很多。
在components的common文件夹下创建Scroll.vue:
<template> <div class="wrapper" ref="wrapper"> <div class="content"> <slot></slot> </div> </div> </template> <script> import BScroll from 'better-scroll' export default { name: 'Scroll', data() { return { scroll: null } }, mounted() { this.scroll = new BScroll(this.$refs.wrapper, { }) } } </script> <style scoped> </style>
问题1:当new一个BScroll时,不建议使用document.querySelector去获取类对象,因为一个项目里类会出现多次。
解答: 在Vue中建议使用ref属性对标签进行绑定。
ref如果是绑定在组件中的,那么通过this.$refs.refname获取到的是一个组件对象。
ref如果是绑定在普通元素中,那么如果this.$refs.refname获取到的是一个元素对象。
问题2:Vue中style标签中的scoped是什么意思?
解答:当一个style标签拥有scoped属性时,它的CSS样式就只能作用于当前的组件,也就是说,该样式只能适用于当前组件元素。 通过该属性,可以使得组件之间的样式不互相污染。 如果一个项目中的所有style标签全部加上了scoped,相当于实现了样式的模块化。
问题3:vh单位表示的含义?
解答:vh表示viewport height,即视口高度。
**问题4:**出现上拉加载的bug的解决方案?
**解答:**如果是2.0以上版本,在better-scroll的配置对象里面加入observeDom:true,observeImage:true就可以了
在home.vue中导入该组件,并进行使用。使用标签把需要滚动的标签包裹起来。
给content设置高度,例如这里设置height: calc(100vh - 93px)
给scroll组件的父组件设置高度,例如这里设置height: 100vh;
//script import Scroll from '@/components/common/scroll/Scroll' export default { components: { Scroll } } //template <scroll class="content"> <home-swiper :banners="banners" /> <recommend-view :recommends="recommends" /> <feature-view/> <tab-control class="tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick" /> <goods-list :goods="showGoods" /> </scroll>
1.在components的content文件夹下创建backTop文件夹,并在文件夹内创建BackTop.vue文件
<template> <div class="back-top"> <img src="@/assets/img/common/top.png" alt=""> </div> </template> <script> export default { name: 'BackTop' } </script> <style scoped> .back-top { position: fixed; right: 8px; bottom: 55px; } .back-top img { width: 43px; } </style>
2.在home.vue文件夹中进行导入,添加到components中,再进行使用。
关键点:对于自定义组件标签设置点击事件需要添加修饰符,例如:
.stop - 调用 event.stopPropagation()。
.prevent - 调用 event.preventDefault()。
.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
.native - 监听组件根元素的原生事件。
.once - 只触发一次回调。
<back-top @click.native="backClick"></back-top>
3.定义btnClick()方法
backClick() {
this.$refs.scroll.scroll.scrollTo(0, 0, 500)
}
4.设置对返回顶部按钮的显示隐藏
在mounted()中对滚动事件进行监听,当进行滚动时,通过emit将scroll事件和参数position传递给父组件。
this.scroll.on('scroll', (position) => {
// console.log(position);
this.$emit('scroll', position)
})
父组件中可以得到传出的事件scroll,通过@scroll进行事件监听并调用方法contentScroll。父组件中contentScroll不需要写参数,参数可以在methods方法中写。
<scroll
class="content"
ref="scroll"
:probe-type="3"
@scroll="contentScroll">
书写contentScroll()方法,判断条件很重要。这里的position.y是一个负数,所以取其绝对值,赋值时等式右边是一个表达式
contentScroll(position) {
//根据等号右侧条件判断true or false
this.isShowBackTop = Math.abs(position.y) > 1000
}
Better-Scroll在决定有多少区域可以滚动时,是根据scrollHeight属性决定
如何解决该问题呢?
事件总线的概念:
EventBus
又称为事件总线。在Vue中可以使用 EventBus
来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。
如何将GoodListItem.vue中的事件传入到home.vue中
事件总线使用方式:
首先打开main.js文件,在Vue的原型链上创建一个Vue实例,用来作为事件总线
// 需要创建事件总线的实例
Vue.prototype.$bus = new Vue()
- 在GoodListItem.vue中使用对图片的加载进行监听,并通过$bus.$emit进行发送。
<img :src=“goodsItem.show.img” alt="" @load=“imageLoad”>
imageLoad() {
this.$bus.$emit('itemImageLoad')
}
在home.vue的created()中对发送的事件进行监听并调用对scroll的refresh()进行刷新
this.$bus.$on('itemImageLoad', () => {
this.$refs.scroll.scroll.refresh()
})
若出现切换首先、分类出现函数报错问题时
在页面销毁的destroy(){}生命周期事件中移除事件线就好
destroyed() {
this.$bus.$off()
},
对于refresh非常频繁的问题,进行防抖操作
防抖debounce/节流throttle
防抖函数起作用的过程:
如果我们直接执行refresh,那么refresh函数会被执行30次
可以将refresh函数传入到debounce函数中,生成一个新的函数
debounce函数有两个参数,一个是函数,一个是延迟时间。记住传入函数是不要加(),因为我们需要传入的是函数本身而不是函数的返回值。
debounce函数的返回值是一个函数,声明一个变量令其等于返回值,再对该函数进行调用
if(timer) clearTimeout(timer) 这个判断语句表示当debounce函数在执行的过程中又被调用,则清除调之间的计时,重新再开始计时。
…args表示传入的函数的参数,…表示扩展运算符,表示你可以传入多个参数。
//methods中定义函数
debounce(func, delay) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
//mounted对函数进行调用
const refresh = this.debounce(this.$refs.scroll.refresh, 500)
this.$bus.$on('itemImageLoad', () => {
refresh()
})
对debounce函数进行封装
在common文件夹下的utils.js组件中导出debounce函数
export function debounce(func, delay) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
在Home.vue中导入debounce函数并进行使用
import {debounce} from '@/common/utils.js'
在props属性中设置pullUpLoad属性
pullUpLoad: {
type: Boolean,
default: false
}
在mounted函数中进行事件监听,监听pullingUp,并同$emit将事件传递出去,注意这里的事件名最好不要与pullingUp同名(这里取名pulling)
this.scroll.on('pullingUp', () => {
this.$emit('pulling')
})
在scroll标签中设置监听传入的pulling事件,并调用loadMore方法
<scroll @pulling="loadMore"></scroll>
在methods中定义loadMore方法,loadMore函数内部调用之间定义好的getHomeGoods方法
loadMore() {
this.getHomeGoods(this.currentType)
}
为了能够进行多次上拉加载更多,在Scroll.vue中定义finishPullUp方法
finishPullUp() {
this.scroll && this.scroll.finishPullUp()
}
在Home.vue中对getHomeGoods方法增添一行代码,这样每次加载图片后均为设定完成一次上拉加载更多
this.$refs.scroll.finishPullUp()
方案总结:
获取tabControl的offsetTop,知识点:所有的组件都有一个属性$el:用于获取组件中的元素
在mounted()中直接打印offsetTop,得到的值偏小。主要原因是轮播图的加载需要时间
所以,我们可以在HomeSwiper.vue组件中对图片的加载进行监听
<img :src="item.image" alt="" @load="imageLoad">
定义imageLoad方法,为了防止重复加载,在data中设置isLoad的bool值用作节流阀。isLoad默认为false,加载完成后设置为true
imageLoad() {
// 添加节流阀
if(!this.isLoad) {
this.$emit('swiperImageLoad')
this.isLoad = true
}
}
在Home.vue的home-swiper标签中对轮播图的图片加载进行监听,并赋值给tabOffsetTop
<home-swiper :banners="banners" @swiperImageLoad="swiperImageLoad" />
swiperImageLoad() {
this.taboffsetTop = this.$refs.tabControl2.$el.offsetTop
}
小技巧:偷天换日。在nav-bar组件下再设置一个tab-control组件。这样等于在scroll组件内外各有一个tab-control组件,两者均设置v-show属性,当滚动到一定距离时,前者显示后者隐藏。
//前者
<tab-control :titles="['流行', '新款', '精选']" @tabClick="tabClick"
ref="tabControl1"
class="tab-control" v-show="isTabFixed" />
//后者
<tab-control :titles="['流行', '新款', '精选']" @tabClick="tabClick"
ref="tabControl2"
v-show="!isTabFixed" />
在data中定义isTabFixed变量,默认为false。因为contentScroll方法对页面滚动进行监听,所以在方法内部添加一行代码,用于改变isTabFixed。当position>offsetTop时为true,否则为false
contentScroll(position) {
this.isTabFixed = Math.abs(position.y) > this.taboffsetTop
},
此时还会出现一个bug,即两个tab-control组件的标签不同步。解决方案:首先两个组件分别取名ref=“tab-Control1"和"tab-Control2”,然后给tabClick()添加两行代码,修改两者的currentIndex
this.$refs.tabControl1.currentIndex = index;
this.$refs.tabControl2.currentIndex = index;
1.首先让Home不要随意销毁掉:
**解决方案:**打开App.vue,使用keep-alive标签包裹住router-view标签
<keep-alive>
<router-view></router-view>
</keep-alive>
2.让Home中的内容保持原来的位置:
**解决方案:**因为不进行销毁了,所以多了两个生命周期函数,即activated和deactived。步骤:
1.首先在data中创建saveY变量用来存储位置信息,默认为0。
2.在activated函数中设置scroll的跳转位置,保险起见可以添加一个scroll的刷新。
activated() {
this.$refs.scroll.refresh()
this.$refs.scroll.scrollTo(0, this.saveY, 0)
},
3.在deactived函数中存储当前的位置,scroll对象中有一个y属性可以用来的得到位置信息
deactivated() {
this.saveY = this.$refs.scroll.scroll.y
},
1.在views文件夹中创建detail文件夹,并在文件夹下创建Detail.vue组件
<template> <div>{{iid}}</div> </template> <script> export default { name: 'Detail', data() { return { iid: null } }, created() { this.iid = this.$route.params.iid } } </script> <style scoped> </style>
2.打开router文件夹的index.js文件,对detail组件进行懒加载,并在路由中进行配置。因为每件商品对应的详情页不同,故配置的是一个动态路由。
const Detail = () => import('@/views/detail/Detail')
const routes = [
{
path: '/detail/:iid',
component: Detail
}
]
3.在GoodsListItem.vue文件中利用图片对点击进行监听,并定义itemClick()方法,让其通过路由进行跳转
itemClick() {
this.$router.push('/detail/' + this.goodsItem.iid)
}
4.开始编写Detail.vue组件,总体流程跟写Home.vue差不多,将页面的各个部分封装到外面。
5.从接口获取商品详情的数据
在network文件夹创建detail.js文件,创建getDetail()方法并进行导出
import {request} from './request';
export function getDetail(iid) {
return request({
url: '/detail',
params: {
iid
}
})
}
Detail.vue组件:导入getDetail方法,在created()生命周期函数中进行调用
import {getDetail} from '@/network/detail'
getDetail(this.iid).then(res => {
console.log(res);
})
同理,商品轮播图也是采取同样的做法。
在childComps文件夹中创建DetailSwiper.vue组件,导入Swiper、SwiperItem组件。
在props对象中,定义topImages属性,用来接收父组件Detail.vue的图片数据,最后定义css属性
在Detail.vue中导入DetailSwiper组件,并利用动态属性传入图片数据
<template> <swiper> <swiper-item class="swiper-item" v-for="(item, index) in topImages" :key="index"> <img :src="item" alt=""> </swiper-item> </swiper> </template> <script> import {Swiper, SwiperItem} from '@/components/common/swiper' export default { name: 'DetailSwiper', components: { Swiper, SwiperItem }, props: { topImages: { type: Array, default() { return [] } } } } </script> <style scoped> .swiper-item { height: 300px; overflow: hidden; } </style>
<detail-swiper :topImages="topImages"></detail-swiper>
6.针对接口数据分布在不同位置,我们这里提出一个方法将需调用的数据进行整合
打开network文件夹下的detail.js文件,定义一个商品类,在构造函数中传入相应数据
export class Goods {
constructor(itemInfo, columns, services) {
this.title = itemInfo.title
this.desc = itemInfo.desc
this.newPrice = itemInfo.price
this.oldPrice = itemInfo.oldPrice
this.discount = itemInfo.discountDesc
this.columns = columns
this.services = services
this.realPrice = itemInfo.lowNowPrice
}
}
在Detail.vue中:在data中定义goods属性。导入定义的类,并在使用getDetail()方法时通过new Goods传入数据
//data中定义goods属性 data() { return { iid: null, res: null, topImages: [], goods: {}, } }, getDetail(this.iid).then(res => { // 1.获取顶部的图片轮播数据 console.log(res); const data = res.result; this.topImages = data.itemInfo.topImages; // 2.获取商品信息 this.goods = new Goods(data.itemInfo, data.columns, data.shopInfo.services) })
数据整合完成之后,便可以开始定义DetailBaseInfo组件,在props对象中定义goods属性。goods属性用来接收从父组件Detail.vue传递过来的数据。
<template> <div v-if="Object.keys(goods).length !== 0" class="base-info"> <div class="info-title">{{goods.title}}</div> <div class="info-price"> <span class="n-price">{{goods.newPrice}}</span> <span class="o-price">{{goods.oldPrice}}</span> <span v-if="goods.discount" class="discount">{{goods.discount}}</span> </div> <div class="info-other"> <span>{{goods.columns[0]}}</span> <span>{{goods.columns[1]}}</span> <span>{{goods.services[goods.services.length-1].name}}</span> </div> <div class="info-service"> <span class="info-service-item" v-for="index in goods.services.length-1" :key="index"> <img :src="goods.services[index-1].icon"> <span>{{goods.services[index-1].name}}</span> </span> </div> </div> </template> <script> export default { name: 'DetailBaseInfo', props: { goods: { type: Object, default() { return {} } } } } </script> <style scoped> .base-info { margin-top: 15px; padding: 0 8px; color: #999; border-bottom: 5px solid #f2f5f8; } .info-title { color: #222 } .info-price { margin-top: 10px; } .info-price .n-price { font-size: 24px; color: var(--color-high-text); } .info-price .o-price { font-size: 13px; margin-left: 5px; text-decoration: line-through; } .info-price .discount { font-size: 12px; padding: 2px 5px; color: #fff; background-color: var(--color-high-text); border-radius: 8px; margin-left: 5px; /*让元素上浮一些: 使用相对定位即可*/ position: relative; top: -8px; } .info-other { margin-top: 15px; line-height: 30px; display: flex; font-size: 13px; border-bottom: 1px solid rgba(100,100,100,.1); justify-content: space-between; } .info-service { display: flex; justify-content: space-between; line-height: 60px; } .info-service-item img { width: 14px; height: 14px; position: relative; top: 2px; } .info-service-item span { font-size: 13px; color: #333; } </style>
7.给Detail组件,即商品详情页添加better-scroll
首先导入封装好的Scroll组件
import Scroll from '@/components/common/scroll/Scroll'
使用scroll标签包住要滑动的部分,接下来是关键步骤:给scroll标签定义一个类,类样式中利用计算属性添加高度。最后整个大标签也要添加一个高度(大标签的高度设置为100vh,即100%的视图高度)。
<div id="detail"> <detail-nav-bar class="detail-nav" /> <scroll class="content"> <detail-swiper :topImages="topImages" /> <detail-base-info :goods="goods" /> <detail-shop-info :shop="shop" /> </scroll> </div> <style scoped> #detail { position: relative; z-index: 9; background-color: #fff; height: 100vh; } .content { height: calc(100% - 44px); /* 减号两边记得留空格!否则无效 */ } </style>
固定导航标签,给导航标签定义detail-nav的类,设置相对定位,背景色定位白色,z-index设置为9
.detail-nav {
position: relative;
z-index: 9;
background-color: #fff;
}
8.导入其他组件,包括店铺信息、商品的详细展示信息、商品的尺码参数
9.给详情页添加推荐的子组件
首先,因为推荐的数据接口不同,所以先打开detail.js文件。写一个getCommend()函数
export function getRecommend() {
return request({
url: '/recommend'
})
}
第二,在Detail组件中导入getCommend()方法获取recommends数据
import {getDetail, getRecommend, Goods, Shop, GoodsParam} from '@/network/detail'
//在created周期函数中获取数据
getRecommend().then(res => {
console.log(res);
this.recommends = res.data.list;
})
第三,推荐商品的展示可以直接复用之前写过的GoodsList组件,但存在一个问题。即两者对于图片的数据结构不一致,所以会出现报错。解决方案是:在GoodsListItem组件中添加一个计算属性,用来获取图片数据。
<img :src="showImage" alt="" @load="imageLoad" @click="itemClick">
computed: {
showImage() {
return this.goodsItem.image || this.goodsItem.show.img
}
},
时间戳:1535694719(秒)
1.将时间转成Date对象,因为时间戳是秒,乘以1000将其转化成毫秒
const date = new Date(1535694719*1000)
2.将date进行格式化,转成对应的字符串
*date.getYear() + date.getMonth()+1
//date -> FormatString(太常用)
fmt.format(date, 'yyyy-MM-dd hh:mm:ss')
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
问题1:我自己测试的时候发现一个bug,当你点开商品详情页之后。再回到主页,向上滑动时会发现无法进行上拉加载更多了。
解决方案:我认为问题产生的原因时,home.vue停止监听的缘故。所以我把mixin.js中的mounted改成activated,问题就解决了。
问题2:
因为详情页的推荐部分复用了GoodsListItem组件,而该组件在图片进行加载时会通过事件总线向home.vue发送itemImgLoad事件。而详情页的加载又不需要home.vue进行监听,这就导致了代码的性能下降。
解决方案:
在Detail和Home组件的mounted函数中,分别使用debounce防抖函数进行刷新。然后在destroyed函数中将监听关闭。
因为两者在mounted中的代码相同,故可以利用mixin对象将相同的代码部分混合进mixin.js中。
首先,在mixin.js中定义itemListenerMixin对象。
import {debounce} from './utils';
export const itemListenerMixin = {
mounted() {
let newrefresh = debounce(this.$refs.scroll.refresh, 100)
this.itemImgListener = () => {
newrefresh()
}
this.$bus.$on('itemImgLoad', this.itemImgListener)
console.log('我是混入中的内容');
}
}
第二,在Home/Detail中进行导入,并添加进mixins属性数列的数组中。
import {itemListenerMixin} from '@/common/mixin.js'
mixins: [itemListenerMixin],
22.1 点击标题,滚动到对应的主题
在detail组件中监听主题的点击,获取index
itemClick(index) {
this.currentIndex = index;
this.$emit('titleClick', index);
},
<detail-nav-bar class="detail-nav" @titleClick="titleClick" />
滚动到对应的主题
titleClick(index) {
// 第一个参数是x值,第二个参数是y值,第三个参数是时间200ms
this.$refs.scroll.scrollTo(0, -this.themeTopYs[index], 200)
}
获取所有主题的offsetTop
关键点:在哪里才能获取到正确的offsetTop
1.created肯定不行,因为在created中压根不能获取元素
2.mounted也不行,因为数据还没有获取到
3.利用获取完数据的回调函数也不行,因为DOM还没有渲染完
4.$nextTick也不行,因为图片的高度没有被计算在内
/*
//当数据加载完成时会触发一个nextTick函数
this.$nextTick(() => {
//根据最新的数据,对应的DOM是已经被渲染出来
// 但是图片依然是没有加载完(目前获取到offsetTop不包含其中的图片)
this.themeTopYs = [];
this.themeTopYs.push(0);
this.themeTopYs.push(this.$refs.params.$el.offsetTop);
this.themeTopYs.push(this.$refs.comm ent.$el.offsetTop);
this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
console.log(this.themeTopYs);
})
*/
5.在图片加载后,获取的图片高度才是正确。因为会获取多次,所以可以利用防抖函数进行一个简单处理。
imageLoad() {
this.$refs.scroll.refresh();
this.themeTopYs = [];
this.themeTopYs.push(0);
this.themeTopYs.push(this.$refs.params.$el.offsetTop);
this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
console.log(this.themeTopYs);
},
防抖函数处理流程:
1.在data中创建getThemeTopY:null
getThemeTopY: null
2.在created中声明debounce函数
this.getThemeTopY = debounce(() => {
this.themeTopYs = [];
this.themeTopYs.push(0);
this.themeTopYs.push(this.$refs.params.$el.offsetTop);
this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
console.log(this.themeTopYs);
})
3.在methods的imageLoad()方法中进行函数调用,这样就可以只输出一次themeTopYs
imageLoad() {
this.$refs.scroll.refresh();
this.getThemeTopY();
},
22.2 内容滚动,显示正确的标题
1.普通做法
this.currentIndex !== i && ((i < length - 1 && positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i+1]) || (i === length - 1 && positionY >= this.themeTopYs[i]))
条件成立:this.currentIndex = i
条件一:防止赋值的过程过于频繁
条件二:((i < length - 1 && positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i+1]) || (i === length - 1 && positionY >= this.themeTopYs[i]))
条件1:(i < length - 1 && positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i+1])
*判断区间:在 0 和 某个数字之间(i < length - 1)
条件2:(i === length -1 && positionY >= this.themeTopY[i])
*判断大于等于:i === length - 1
1.创建DetailBottomBar的子组件,对子组件进行封装
<template> <div id="detail_bottom_bar"> <div class="bottom_left"> <div class="service"> <i class="icon"></i> <span>客服</span> </div> <div class="shop"> <i class="icon"></i> <span>店铺</span> </div> <div class="collect"> <i class="icon"></i> <span>收藏</span> </div> </div> <div class="bottom_right"> <div class="cart" @click="addToCart"> 加入购物车 </div> <div class="buy"> 购买 </div> </div> </div> </template> <script> export default { name: 'DetailBottomBar', methods: { addToCart() { this.$emit("addEvent"); } } } </script> <style scoped> #detail_bottom_bar { font-size: 0.65rem; display: flex; position: fixed; background-color: #fff; bottom: 0px; left: 0; right: 0; height: 2.09rem; text-align: center; box-shadow: 0 -0.04rem 0.4rem gray; } .bottom_left { display: flex; flex: 1; } .bottom_left > div { flex: 1; border-right: 0.04rem solid rgba(128, 128, 128, 0.2); } .bottom_left .icon { display: block; background: url("./detail_bottom.png") 0 0/100%; /* background: url("@/assets/img/detail/detail_bottom.png") 0 0/100%; */ width: 1rem; height: 1rem; margin: 0.12rem auto; } .service .icon { background-position: 0 -2.4rem; } .shop .icon { background-position: 0 -4.5rem; } .bottom_right { display: flex; flex: 1; } .bottom_right > div { flex: 1; line-height: 2.09rem; } .cart { background-color: rgb(255, 174, 0); } .buy { background-color: var(--color-tint); color: white; } </style>
2.在Detail中导入子组件,并将元素标签放在与scroll同级的位置
备注:rem定义
rem(font size of the root element)
是指相对于根元素的字体大小的单位。 1rem
等于根元素 htm
的 font-size
,即只需要设置根元素的 font-size
,其它元素使用 rem
单位时,设置成相应的百分比即可。
24.1常规方法:
1.导入BackTop组件
import BackTop from '@/components/content/backTop/BackTop'
2.在scroll组件同级位置添加back-top标签
<back-top @click.native="backClick" v-show="isShowBackTop"></back-top>
3.定义backClick方法和contentScroll方法
24.2混合Mixins:
1.因为Home和Detail都有BackTop组件,代码几乎相同,所以可以使用mixins功能。编写mixin.js文件
import BackTop from '@/components/content/backTop/BackTop' export const backTopMixin = { components: { BackTop }, data() { return { isShowBackTop: false, } }, methods: { backClick() { this.$refs.scroll.scrollTo(0, 0, 500) }, } }
2.在两个组件中导入backTopMixin
import {itemListenerMixin, backTopMixin} from '@/common/mixin.js'
mixins: [itemListenerMixin, backTopMixin],
25.1Vuex的准备
1.首先安装vuex
npm installl vuex@3.1.0 --dev
2.在main.js文件中导入store并添加到Vue对象中
import store from './store'
new Vue({
render: h => h(App),
router,
store
}).$mount('#app')
3.在store文件夹下创建index.js文件,并将mutations,actions,getters进行一个封装
import Vue from 'vue' import Vuex from 'vuex' import mutations from './mutations' import actions from './actions' import getters from './getters' // 1.安装插件 Vue.use(Vuex) // 2.创建Store对象 const state= { cartList: [] } const store = new Vuex.Store({ state, mutations, actions, getters }) // 3.挂载Vue实例上 export default store
25.2加入购物车
通过下述步骤,即可实现监听添加购物车的商品数量
1.在DetailBottomBar组件的加入购物车的标签中添加点击事件addToCart
<div class="cart" @click="addToCart">
加入购物车
</div>
2.在addToCart方法中利用子传父的$emit将addCart事件导出
addToCart() {
this.$emit("addCart");
}
3.Detail组件的detail-bottom-bar组件对addCart进行事件监听
<detail-bottom-bar @addCart="addCart" />
4.在Detail组件中定义同名的addCart方法,获取需要展示的商品信息并使用commit调用store中的addToCart方法。
addCart() {
// 1.获取购物车需要展示的基本信息
const product = {}
product.image = this.topImages[0]
product.title = this.goods.title;
product.desc = this.goods.desc;
product.price = this.goods.realPrice;
// 2.获取iid,商品的唯一标识
product.iid = this.iid;
// 3.将商品添加到购物车
this.$store.commit('addToCart', product);
}
5.store文件夹下的mutations文件定义addCounter和addToCart方法用来记录加入购物车的各类商品数量
export default {
//mutations唯一的目的就是修改state中的状态
//mutations中的每个方法尽可能完成的事件比较单一一点
addCounter(state, payload) {
payload.count++
},
addToCart(state, payload) {
state.cartList.push(payload)
}
}
6.store文件夹下的getters文件定义获取数据的计算属性,例如购物的商品种类
export default {
cartLength(state) {
return state.cartList.length
}
}
25.3购物车界面
1.进入Cart.vue组件
2.介绍vuex的mapGetters功能,可以将getters的计算属性直接导入到Cart.vue中
常规写法
computed: {
cartLength() {
return this.$store.getters.cartLength
}
}
mapgetters用法
import {mapGetters} from 'vuex'
computed: {
//用法1
...mapGetters(['cartLength', 'cartList'])
//用法2
...mapGetters({
length: 'cartLength',
list: 'cartList'
})
}
ions from ‘./actions’
import getters from ‘./getters’
// 1.安装插件
Vue.use(Vuex)
// 2.创建Store对象
const state= {
cartList: []
}
const store = new Vuex.Store({
state,
mutations,
actions,
getters
})
// 3.挂载Vue实例上
export default store
**25.2加入购物车**
通过下述步骤,即可实现监听添加购物车的商品数量
1.在DetailBottomBar组件的加入购物车的标签中添加点击事件addToCart
2.在addToCart方法中利用子传父的$emit将addCart事件导出
addToCart() {
this.$emit("addCart");
}
3.Detail组件的detail-bottom-bar组件对addCart进行事件监听
<detail-bottom-bar @addCart="addCart" />
4.在Detail组件中定义同名的addCart方法,获取需要展示的商品信息并使用commit调用store中的addToCart方法。
addCart() {
// 1.获取购物车需要展示的基本信息
const product = {}
product.image = this.topImages[0]
product.title = this.goods.title;
product.desc = this.goods.desc;
product.price = this.goods.realPrice;
// 2.获取iid,商品的唯一标识
product.iid = this.iid;
// 3.将商品添加到购物车
this.$store.commit('addToCart', product);
}
5.store文件夹下的mutations文件定义addCounter和addToCart方法用来记录加入购物车的各类商品数量
export default {
//mutations唯一的目的就是修改state中的状态
//mutations中的每个方法尽可能完成的事件比较单一一点
addCounter(state, payload) {
payload.count++
},
addToCart(state, payload) {
state.cartList.push(payload)
}
}
6.store文件夹下的getters文件定义获取数据的计算属性,例如购物的商品种类
export default {
cartLength(state) {
return state.cartList.length
}
}
25.3购物车界面
1.进入Cart.vue组件
2.介绍vuex的mapGetters功能,可以将getters的计算属性直接导入到Cart.vue中
常规写法
computed: {
cartLength() {
return this.$store.getters.cartLength
}
}
mapgetters用法
import {mapGetters} from 'vuex'
computed: {
//用法1
...mapGetters(['cartLength', 'cartList'])
//用法2
...mapGetters({
length: 'cartLength',
list: 'cartList'
})
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。