vue项目总结

本文是关于Vue.js项目实战的总结,详细介绍了项目创建、目录结构划分、CSS文件引入、别名配置、组件封装等步骤。重点讲述了首页的开发,包括导航栏、轮播图、数据请求、滚动封装等实现,以及详情页的导航栏、数据请求、商品展示等。同时,文章提到了购物车页面的商品添加、Vuex状态管理和底部工具栏的实现。项目收获包括组件化思想的应用和问题解决能力的提升。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考博文https://blog.csdn.net/shixue7758/article/details/117200504?utm_source=app&app_version=4.12.0

一、项目介绍

这个项目是自学vue后的练习项目,使用vue-cli搭建vue.js环境和webpack配置环境,采用组件化思想划分目录结构,创建 assets用来存放 img 和 css ;components中划分成通用的common组件和本项目中业务相关的context组件;common中用来存放公共的 js文件;network用来网络封装模块;router用来实现组件和路径之间的映射关系;store用来存放状态管理;view用来存放 home category等大的视图;主要实现了首页商品页,商品详情页,购物车页的基本功能

1、项目创建和GitHub托管

1、先查看vscode有没有node环境 node -v,如果没有的话从官网下载http://nodejs.cn/download/与Windows对应的node版本即可,下载之后进行安装。安装成功的标志是 windows+R打开命令行之后输入 cmd 打开命令行,输入 node -v查看node版本。
2、安装 npm install vue
3、创建项目 vue create supermall
4、这时候查看 package.json文件中的script行,进行npm run build打包,npm run serve运行项目,即可在页面中查看项目了。
5、在github上创建自己的项目,readme和gitingore都没有必要选,因为本地的项目中有。
6、建立远程和本地的联系。
注意>、如果本地项目中没有 .git文件夹的话,就 git init 一下就有了
1>、在本地项目中的同级的文件夹下git clone 远程仓库的位置,这样就把远程仓库下载到本地了;之后在把interviewer中的内容复制到interviewer-network中;之后项目的编写在interviewer-network文件夹中进行,首先git status查看文件状态,之后git add . 把所有的文件都加入到项目中,在git commit -m ‘项目介绍’,在git push 推送到远程仓库。
2>、不用clone就可以建立连接。在远程建立一个空白仓库,在本地项目中首先 git remote add origin https://github.com/wanglu458512/supermall.git之后 git push -u origin maste

2、划分目录结构

assets/common/components(common,context)/network/router/store/views/App.vue/main.js
1、删除src中原有的内容
2、创建 assets用来存放 img 和 css ;components中划分成所有项目都可能用到的common组件和本项目中用到的context组件;common中用来存放公共的 js文件;network用来网络封装模块;router用来实现组件和路径之间的映射关系;store用来存放状态管理;view用来存放 home category等大的视图

3、引入基本的CSS文件

normalize.css文件作用:让不同的浏览器在渲染网页元素的时候样式更统一,比如有的浏览器规定超链接下边没有下划线有的有,在比如有的超链接是蓝色的有的是黑色的,这样可以对几乎所有的默认设置进行重置。
normalize.css的引入:在github上找到源文件进行引入。
base.css文件“就是引入了一些基本的样式。在base.css中导入@import “./normalize.css”;并且在App.vue中导入@import “./assets/css/base.css”;

4、配置别名和.editorconfig文件

使用vue-cli3.0搭建项目比之前更简洁,没有了build和config文件夹。vue-cli3的一些服务配置都迁移到CLI Service里面了,对于一些基础配置和一些扩展配置需要在根目录新建一个vue.config.js文件进行配置

`module.exports = {
    //配置别名
    configureWebpack: {
        resolve: {
            alias: {
                '@': 'src',
                'assets': '@/assets',
                'common': '@/common',
                'components': '@/components',
                'network': '@/network',
                'router': '@/router',
                'store': '@/store',
                'view': '@/view',
            }
        }
    }
}`

在一个就是.editorconfig文件配置整个项目的代码规范,这个下载EditorConfig for VS Code插件才管用

5、项目的模块划分通过底部tabbar组件

在components中的common中放进tabbar文件夹,用来引入一个通用的组件。然后在context中放入mainTabBar文件夹,然后在App.vue中导入注册使用MainTabBar组件。

6、TabBar的封装过程(第五步前)

1.如果在下方有一个单独的TabBar组件,你如何封装

自定义TabBar组件,在APP中使用

让TabBar出于底部,并且设置相关的样式

2.TabBar中显示的内容由外界决定

定义插槽

flex布局平分TabBar

3.自定义TabBarItem,可以传入 图片和文字

定义TabBarItem,并且定义两个插槽:图片、文字。

给两个插槽外层包装div,用于设置样式。

填充插槽,实现底部TabBar的效果

4.传入高亮图片

定义另外一个插槽,插入active-icon的数据

(暂时)定义一个变量isActive,通过v-show来决定是否显示对应的icon

5.TabBarItem绑定路由数据

安装路由:npm install vue-router —save

完成router/index.js的内容,以及创建对应的组件

main.js中注册router

APP中加入组件

6.点击item跳转到对应路由,并且动态决定isActive

监听item的点击,通过this.$router.replace()替换路由路径

通过this.$route.path.indexOf(this.link) !== -1来判断是否是active

7.动态计算active样式

封装新的计算属性:this.isActive ? {'color': 'red'} : {}

二、首页的开发

2.1 首页导航栏

首先封装一个nav-bar的组件:

<template>
  <div class="nav-bar ignore">
    <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;
  }

  .left {
    width: 60px;
  }

  .right {
    width: 60px;
  }

  .center {
    flex: 1;
  }
</style>

之后在views中的home文件夹下的Home.vue中导入 注册 使用该组件。使用的时候只用center这个中间的插槽,然后给该组件设置自己的样式。

    <nav-bar class="home-nav">
      <div slot="center">购物街</div>
    </nav-bar>
2.2. 请求首页数据

先安装网络封装模块 axios npm install axios,之后在network文件夹中的request.js文件中导入axios模块,之后创建网络请求,请求所有前端页面需要展示的数据
封装一个axios请求

创建axios实例

拦截响应,返回.data数据

import axios from 'axios'
export function request(config){
  const instance=axios.create({
    baseURL:'http://152.136.185.210:7878/api/m5',
    timeout:5000
  })
  // 请求拦截
  instance.interceptors.request.use(config=>{
    return config
  },err=>{
    console.log(err);
  })
  // 响应拦截:一般会过滤数据
  instance.interceptors.response.use(res => {
    return res.data
  }, error => {
    console.log(error);
  })
  return instance(config)
}

为了把不同页面展示的数据分开从request.js中请求,现在把首页性需要的数据放在network文件夹中的home.js文件中,暴露方法

import {request} from "./request"
// 轮播图数据
export function getHomeMultidata(){
  return request({
    url:"/home/multidata"
  })
}
// 请求首页商品列表数据
export function getHomeGoods(type,page){
return request({
  url:"/home/data",
  params:{
    type,
    page
  }
})
}

从服务器请求过来的首页数据怎么才能在home.vue中展示出来呢,就是在home.vue中导入一下这两个数据模块 这样的话 就是 home.vue面向home.js开发

import {getMultiData, getProductData} from "network/home"; //从服务器中请求过来的首页需要展示的数据

请求过来的轮播图数据home.vue中的created中先调用一下请求函数,然后在methods做一下数据处理,请求过来的数据在data中做保存。

  // 网络请求数据
created() {
    // 1.请求多个数据
    this.getHomeMultidata();
    //2 调用请求首页商品数据
    this.getHomeGoods("pop");
    this.getHomeGoods("new");
    this.getHomeGoods("sell");
},
 
methods: {
...
    // 网络请求相关
    // 1.请求首页轮播图、recmmoned的数据
	getHomeMultidata() {
      getHomeMultidata().then((res) => {
        this.banners = res.data.banner.list;
        this.recommends = res.data.recommend.list;
      });
    },
    // 2.请求首页商品数据,动态获取page
    getHomeGoods(type) {
      const page = this.goods[type].page + 1;
      getHomeGoods(type, page).then((res) => {
        // console.log(res.data);
        this.goods[type].list.push(...res.data.list);
        this.goods[type].page += 1;
        // 完成上拉加载更多
        this.$refs.scroll.finishPullUp();
      });
    },
...
}
2.3. 把轮播图数据在首页进行展示

在components中的common文件夹下引入封装好的组件swipper, 不抽的话接下来是这样的。在home.vue中导入封装好的组件import {Swiper, SwiperItem} from 'components/common/swiper’然后注册组件,最后在template中使用组件。

但是如果把整个轮播图放在Home.vue这里的话,后边的代码会越来越复杂,把这个轮播图抽出来,进行封装,放在home文件夹中的childcomps文件夹中,在该文件夹中建立HomeSwipper.vue文件,把刚才放在home.vue中的代码剪过来。

<template>
  <swiper>
    <swiper-item v-for="(item, index) in banners" :key="index">
      <!-- 动态的获取图片的链接 -->
      <a :href="item.link">
        <!-- 动态的获取轮播图中的图片 -->
        <img :src="item.image" alt="">
      </a>
    </swiper-item>
  </swiper>
</template>
<script>
  import {Swiper, SwiperItem} from 'components/common/swiper'

  export default {
    name: "HomeSwiper",
    //得从父亲那里 拿数据 啊
    props: {
      banners: {
        type: Array,
        required: true
      }
    },
    components: {
      Swiper,
      SwiperItem
    },
  }
</script>
<style scoped>
</style>

然后在home.vue中导入该组件import HomeSwiper from './childComps/HomeSwiper',然后注册一下,然后在template中使用<home-swiper :banners="banners" ></home-swiper>

2.4. 首页推荐数据的展示

在home文件夹的childcomps中封装一个组件RecommendView.vue

<template>
  <div class="recommend">
    <div class="recommend-item" v-for="(item, index) in recommends" :key="index">
      <a :href="item.link">
        //因为图片和标题都是可以被点击链接的,所以要包含在a标签中
        <img :src="item.image" alt="">
        <span>{{item.title}}</span>
      </a>
    </div>
  </div>
</template>

<script>
  export default {
    name: "RecommendView",
    //获取从 上一级请求过来的推荐数据
    props: {
      recommends: {
        type: Array,
        required: true
      }
    }
  }
</script>

<style scoped>
	//布局样式
  .recommend {
    display: flex;
    margin-top: 10px;
    font-size: 14px;
    padding-bottom: 30px;
    border-bottom: 10px solid #eee;
  }

  .recommend-item {
    flex: 1;
    text-align: center;
  }

  .recommend img {
    width: 80px;
    height: 80px;
    margin-bottom: 10px;
  }
</style>

然后在home.vue中导入该组件import RecommendView from './childComps/RecommendView'然后注册,最后使用<recommend-view :recommends="recommends"></recommend-view> 组件接收数据的时候通过:recommends="recommends"相当于用on来接收。

2.5. 封装FeatureView

本质上就是一张图片
首先在home文件夹下的childcomps中封装一个组件FeatureView.vue,

然后在home.vue中导入import FeatureView from './childComps/FeatureView',注册使用<feature-view></feature-view>

2.6. 流行新款精选:封装tabControl

逻辑:
接收从父级传来的titles,遍历div盒子,v-for="(item,index) in titles得到item,index;
实现选中哪一个tab, 哪一个tab的文字颜色变色, 利用 currentIndex,添加一个样式 :class="{active:index==currentIndex}",监听点击事件,设置this.currentIndex = index;
实现点击tab后显示对应的商品列表,在tab-control.vue组件的methods中将点击事件tabClick发出去
在Home.vue中使用tab-control组件中接受该事件,在methods中建立对应的tabClick方法,监听流行、新款、精选的点击,默认显示流行列表页的数据,data中设置参数currentType:pop

TabControl组件:

@<template>
  <div class="tab-control">
    <div :class="{active:index==currentIndex}" v-for="(item,index) in titles" class="tab-control-item" @click="itemClick(index)" :key="item.index">
      <span>{{ item }}</span>
    </div>
  </div>
</template>

<script>
export default {
  name: "TabControl",
  data() {
    return {
      currentIndex: 0,
    };
  },
  methods: {
    itemClick(index) {
      this.currentIndex = index;
      this.$emit('tabClick',index)
    },
  },
  props: {
    titles: {
      type: Array,
      default() {
        return [];
      },
    },
  },
};
</script>

<style>
.tab-control {
  display: flex;
  font-size: 15px;
  height: 40px;
  line-height: 40px;
  text-align: center;
}
.tab-control-item {
  flex: 1;
}
.tab-control-item span {
  padding: 5px;
}
.active span {
  color: var(--color-high-text);
  border-bottom: 3px solid var(--color-tint);
}

然后把这个组件在home.vue中导入import TabControl from ‘components/content/tabControl/TabControl’,注册使用
Home.vue中:

      <tab-control
        :titles="['流行', '新款', '精选']"
        @tabClick="tabClick"
        ref="tabControl2"
      ></tab-control>


  methods: {
    // 事件监听
    // 1.监听tabclick 流行 精选 热卖的点击,以及吸顶功能
    tabClick(index) {
      switch (index) {
        case 0:
          this.currentType = "pop";
          break;
        case 1:
          this.currentType = "new";
          break;
        case 2:
          this.currentType = "sell";
          break;
      }
      this.$refs.tabControl1.currentIndex = index;
      this.$refs.tabControl2.currentIndex = index;
    },
2.7. 商品列表数据的保存

因为要一次性从服务器请求流行 新款 精品的三类数据,并且实现点击哪一个就跳转到哪一个的功能,所以把这三类数据保存在一个对象goods中

  • 定义goods数据结构,用于存储请求到的商品数据,包含三个对象,pop news sell,每个对象中包含用于记录当前页码的page以及请求的商品数据列表list

    goods: {

    ​ pop: { page: 0, list: [] },

    ​ new: { page: 0, list: [] },

    ​ sell: { page: 0, list: [] },

    },

  • 在network文件夹下的home.js中封装getHomeGoods(type, page)函数

  • 在Home.vue中导入该函数,import { getHomeMultidata, getHomeGoods } from “network/home”;

  • 在created中调用该函数,加了this才能是指代该组件的函数

  • 在Home.vue 的 methods中处理该函数 getHomeGoods(type)

home.js

import {request} from "./request"

export function getHomeMultidata(){
  return request({
    url:"/home/multidata"
  })
}
// 请求首页商品数据
export function getHomeGoods(type,page){
return request({
  url:"/home/data",
  params:{
    type,
    page
  }
})
}

Home.vue中:

  // 网络请求数据
  created() {
    // 1.请求多个数据
    this.getHomeMultidata();
    //2 调用请求首页商品数据
    this.getHomeGoods("pop");
    this.getHomeGoods("new");
    this.getHomeGoods("sell");
  },
  methods:{
	...
         getHomeGoods(type) {
           const page = this.goods[type].page + 1;
           getHomeGoods(type, page).then((res) => {
             // console.log(res.data);
             this.goods[type].list.push(...res.data.list);
             this.goods[type].page += 1;
             // 完成上拉加载更多
             this.$refs.scroll.finishPullUp();
           });
         },
  

  
2.8. 商品数据的展示:封装GoodsList和GoodsListItem
  • 展示商品列表,封装GoodsList,然后将该组件在Home.vue中导入注册使用
<template>
  <div class="goods">
    <goods-list-item v-for="item in goods" :goodsItem="item" :key="item.index"></goods-list-item>
  </div>
</template>

<script>
  import GoodsListItem from "./GoodsListItem"
  export default {
    name: "GoodsList",
    components:{
      GoodsListItem
    },
    props:{
      goods:{
        type:Array,
        default(){
          return []
        }
      }
    }

  }
</script>
<style scoped>
.goods{
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  padding: 2px;
}

</style>

  • 列表中每一个商品,封装GoodsListItem
  • 在Home.vue中使用GoodList的时候,动态绑定goods,动态获取pop\news\sell数据,利用currentType
<goods-list :goods="goods[this.currentType].list" />

为了代码好看,可以进一步封装为方法

  computed: {
    showGoods() {
      return this.goods[this.currentType].list;
    },
  },

然后就可以这样使用

<goods-list :goods="showGoods" />
  • 注意CSS属性的设置即可
  • 点击切换商品的流行、新款、精选
2.9. 滚动的封装Scroll
1.学习BetterScroll的使用
  • 安装better-scroll

作用:如果按照之前的滚动的话就会出现手指停留在哪里就会滚到哪里的效果而不会出现滚动到之后仍然下滑(弹簧效果)的效果,并且部署在移动端的话会出现非常卡顿的现象,所以才引入这个框架。

BetterScorll使用步骤:
1、首先安装 npm install better-scroll --save
2、在组件中导入 import bscroll from ‘better-scroll’
3、使用,在最外边包一层 wrapper 在里边的滚动内容也得放在一个标签中,然后给wrapper一个高度,然后在 mounted中新建一个 new BScroll(‘要滚动的元素标签’,{一些参数的设置}),这里不能在created中new 因为这个时候 template还没有被挂载,即dom元素还没有形成。

BetterScorll第二个参数:
1、better-scroll默认情况下是不实时监测滑动位置的,
对better-scroll的位置的实时监测有一个属性 probeType
就是在new一个BScroll的时候传入对象形式的第二个参数,probeType: 2,( 0 1都是默认不监测,2是手指滑动过程中监测,在手指离开的惯性过程中不监测 ,3是只要是滚动都监测)。
2、better-scroll默认情况下是不会有DOM元素事件操作,设置click:true就可以了。
3、上拉加载更多的实现 better-scroll默认情况下是不会有上拉加载更多,设置pullupLoad: true就可以了,在添加事件的时候用 BSccroll.on(pullingUp,function) 但是这个只能执行一次,如果想要接着实现上拉加载更多的话,就得在BSccroll.on(pullingUp,function) 这里边在加入BScrroll.finishPullUp(),表示这次上拉加载更多的操作已经结束

  • 封装一个独立的组件,用于作为滚动组件:Scroll

Scroll组件(片段):

<template>
  <div ref="wrapper" class="wrapper">
    <div>
      <slot></slot>
    </div>
  </div>
</template>

<script>
  import BScroll from 'better-scroll'

  export default {
    name: "Scroll",
    //防止better-scroll创建完之后在执行完mounted之后会被销毁 所以提前保存下来
    data() {
      return {
        scroll: null
      }
    },
    mounted() {
      this.scroll = new BScroll(this.$refs.wrapper
    }
</script>

<style scoped>
  .wrapper {
    overflow: hidden;
  }
</style>

然后在Home.vue组件中导入注册,使用

给scroll设置content高度,上下高度确定,如何自适应中间的高度,用定位

.content {
  overflow: hidden;
  position: absolute;
  top: 44px;
  bottom: 49px;
  left: 0;
  right: 0;
  /* height: calc(100vh-93px); 该方法2报错*/
}
2.解决问题,利用事件总线,防抖优化

可能会出现加载到一半拉不动的问题,在创建的BScroll对象的时候,滚动高度是由scrollerHeight(根据放在scroll中的子组件的高度决定的)决定的,但因为图片是异步加载的,刚开始的时候子组件中的图片可能并没有请求过来,所以scrollerHeight的高度就是错误的,但是当新的图片已经被请求过来的时候,scrollerHeight不会进行更新,所以,会出现加载到一半加载不动的问题

解决方法:
监听GoodsListItem组件的每一张图片是否加载完成,只要有一张图片加载完成,就让Scroll组件刷新一次
1、那么如何监听到图片加载完成了呢?
vue中@load事件可以监听图片加载完成,在GoodsListItem组件中,添加加载完成事件<img @load=“imgLoad”> 然后在下边的methods中,创建imgLoad()的方法

methods:{
  // vue中监听图片记载完成用@load
  // 非父子组件之间通信,这里用事件总线的方式
  // Vue.prototype.$bus = new Vue() 在main.js中利用原型创建一个bus事件
  // this.bus.emit('事件名称', 参数)
  // this.bus.on('事件名称', 回调函数(参数))
  imageLoad(){
    this.$bus.$emit('itemImageLoad')
  },

2、但是怎么把GoodsListItem组件中的事件发送出去,Home.vue怎么拿到这个事件呢?
他们是非父子组件,涉及到非父子组件通信,采用事件总线的方法

Home.vue组件:

  mounted() {
    // bug解决首页中可滚动区域的问题:
    // 监听每一张图片是否加载完成,每一次执行依次refresh
    // 1.图片加载完成后的事件监听,非父子组件的通信,itemImageLoad->GoodsListItem
    const refresh = debounce(this.$refs.scroll.refresh, 50);
    this.$bus.$on("itemImageLoad", () => {
      refresh();
    });
  },

防抖的实现:
作用:如果mounted()中每加载完一张照片就刷新一次的话,会刷新的非常频繁,浪费性能,所以使用防抖函数,即事件触发n秒之内再去执行,如果在这n秒之内事件被再次触发,则重新计时。
在home.vue的methods中创建一个防抖函数,可以独立封装出来,放在common文件夹下的untils ,然后以后再有组件使用就调用该函数,import { debounce } from “common/utils”;

export function debounce(func, delay) {
  let timer = null
  return function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, delay)
  }
}
3.封装scroll
  • Home.vue和Scroll.vue之间进行通信,父子组件,监听页面滚动的位置position以及实现上拉加载更多功能
    Home.vue传给scroll.vue,probeType,pullUpLoad
    Scroll.vue需要通过$emit, 实时将事件发送到Home.vue

    Home.vue:

    <scroll
      class="content"
      ref="scroll"
      :probe-type="3"
      :pull-up-load="true"
      @scroll="contentScroll"
      @pullingUp="loadMore"
    >
      <home-swiper
        :banners="banners"
        @swiperImageLoad="swiperImageLoad"
      ></home-swiper>
      <home-recommend-view :recommends="recommends"></home-recommend-view>
      <feature-view></feature-view>
      <tab-control
        :titles="['流行', '新款', '精选']"
        @tabClick="tabClick"
        ref="tabControl2"
      ></tab-control>
      <goods-list :goods="showGoods" />
    </scroll>
  • Scorll组件内代码的封装:
    • 1.创建BetterScroll对象,并且传入DOM和选项(probeType、click、pullUpLoad)

    • 2.监听scroll事件,该事件会返回一个position

    • 3.监听pullingUp事件,监听到该事件进行上拉加载更多

    • 4.封装刷新的方法:this.scroll.refresh()

    • 5.封装滚动的方法:this.scroll.scrollTo(x, y, time)

    • 6.封装完成刷新的方法:this.scroll.finishedPullUp

    • 7.封装getScrollY():this.scroll.y 让home中的内容保持原来的位置

Scroll组件:

@<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() {
    //防止better-scroll创建完之后在执行完mounted之后会被销毁 所以提前保存下来
    return {
      scroll: null,
      message: "haha",
    };
  },
  // 在这里需要知道scroll的滚动的位置,但是scroll作为一个封装好的组件,又不能把是不是可以监听滚动写死,所以有一个props属性,用来保存父级的probeType属性,父级中要想设置的话就在相应地组件标签中 :probeType=“ ”,如果不是动态绑定的话就是传过去的字符串,加了冒号的话就是一个整数类型。
 props: {
    probeType: {
      type: Number,
      default: 0,
    },
    pullUpLoad: {
      type: Boolean,
      default: false,
    },
  },
  mounted() {
    // 1.创建BScroll对象,在这里元素的选取要采用this.$refs.ref名字,这样的话选取的就是一个唯一确定的元素了,如果用document.querySelector选取的话如果类名冲突,就会导致选取元素错误还不好排错,所以要用refs的形式。
    this.scroll = new BScroll(this.$refs.wrapper, {
      probeType: this.probeType,
      pullUpLoad: this.pullUpLoad,
      click: true,
      observeDOM: true,
      observeImage: true,
      mouseWheel:true
    });
    // 2.监听滚动的位置
    if (this.probeType === 2 || this.probeType === 3) {
      this.scroll.on("scroll", (position) => {
        // console.log(position);
        this.$emit("scroll", position);
      });
    }
    // 3.监听scroll滚到底部,pullingUp上拉刷新
    if (this.pullUpLoad) {
      this.scroll.on('pullingUp',()=>{
        // console.log('监听滚动到底部');
        this.$emit('pullingUp')
      })
    }

  },
  methods: {
    scrollTo(x, y, time = 300) {
      this.scroll && this.scrollTo && this.scroll.scrollTo(x, y, time);
    },
    finishPullUp() {
      this.scroll && this.scroll.finishPullUp();
    },
    refresh() {
      this.scroll && this.scroll.refresh();
    },
    getScrollY(){
      return this.scroll ?this.scroll.y :0
    }
  },
};
</script>

<style scoped>
</style>
2.12 解决refresh找不到的问题,可滚动区域的问题
  • 问题一: refresh找不到的问题
    • 第一: 在Scroll.vue中, 调用this.scroll的方法之前, 判断this.scroll对象是否有值,this.scroll && this.scroll.refresh()
    • 第二: 在mounted生命周期函数中使用 this.$refs.scroll而不是created中
  • 问题二: 对于refresh非常频繁的问题, 进行防抖操作
    • 防抖debounce/节流throttle(课下研究一下)
    • 防抖函数起作用的过程:
      • 如果我们直接执行refresh, 那么refresh函数会被执行30次.
      • 可以将refresh函数传入到debounce函数中, 生成一个新的函数.
      • 之后在调用非常频繁的时候, 就使用新生成的函数.
      • 而新生成的函数, 并不会非常频繁的调用, 如果下一次执行来的非常快, 那么会将上一次取消掉
function debounce(func,delay){
	let time=null
	return function(...args){
		if(time) clearTimeout();
		time=setTimeout(()=>{
			func.aplly(this,args)
		},delay)
	}
}	

解决首页中可滚动区域的问题:

  • Better-Scroll在决定有多少区域可以滚动时, 是根据scrollerHeight属性决定

  • scrollerHeight属性是根据放Better-Scroll的content中的子组件的高度

  • 但是我们的首页中, 刚开始在计算scrollerHeight属性时, 是没有将图片计算在内的

  • 所以, 计算出来的告诉是错误的(1300+)

  • 后来图片加载进来之后有了新的高度, 但是scrollerHeight属性并没有进行更新.

  • 所以滚动出现了问题

  • 如何解决这个问题了?
    方法1:
    [*observe-dom 搭配observe-image *]

方法2:

  • 监听每一张图片是否加载完成, 只要有一张图片加载完成了, 执行一次refresh()

  • 如何监听图片加载完成了?

    • 原生的js监听图片: img.onload = function() {}
    • Vue中监听: @load=“imageLoad”
  • 调用scroll的refresh()

  • 如何将GoodsListItem.vue中的事件传入到Home.vue中?

    • 因为涉及到非父子组件的通信, 所以这里我们选择了事件总线
      • bus ->总线
      • Vue.prototype.$bus = new Vue()
      • this. b u s . bus. bus.emit(‘事件名称’, 参数)
      • this. b u s . bus. bus.on(‘事件名称’, 回调函数(参数))
2.10. 上拉加载更多
  • 通过Scroll监听上拉加载更多
  • 在Scroll.vue中:首先监听什么时候滚动到底部,在scroll的props中接收pullUpLoad,初始设置false
  • 然后在下边的mouteds中创建BScroll,设置参数
  • 当监听到scroll滚动到底部,发出这个事件
  props: {
    probeType: {
      type: Number,
      default: 0,
    },
    pullUpLoad: {
      type: Boolean,
      default: false,
    },
  },
  mounted() {
    // 1.创建BScroll对象,在这里元素的选取要采用this.$refs.ref名字,这样的话选取的就是一个唯一确定的元素了,如果用document.querySelector选取的话如果类名冲突,就会导致选取元素错误还不好排错,所以要用refs的形式。
    this.scroll = new BScroll(this.$refs.wrapper, {
      probeType: this.probeType,
      pullUpLoad: this.pullUpLoad,
      click: true,
      observeDOM: true,
      observeImage: true,
      mouseWheel:true
    });
    // 2.监听滚动的位置

    // 3.监听scroll滚到底部,pullingUp上拉刷新
    if (this.pullUpLoad) {
      this.scroll.on('pullingUp',()=>{
        // console.log('监听滚动到底部');
        this.$emit('pullingUp')
      })
    }

  },
  
  
  • 在Home中加载更多的数据

  • 请求数据完成后,调动finishedPullUp

在Home.vue中使用

    <scroll
      class="content"
      ref="scroll"
      :probe-type="3"
      :pull-up-load="true"
      @scroll="contentScroll"
      @pullingUp="loadMore"
    >

在methods中定义loadMore函数

    // @pullingUp="loadMore" 滚动到底部,上拉加载更多
    loadMore() {
      // 传入tabClick方法的currentType,获取商品列表数据
      this.getHomeGoods(this.currentType);
      this.$refs.scroll.refresh();
    },

因为默认情况下是只上拉加载一次的,所以得有这个 this.$refs.scroll.finishedPullUp(),才能实现一次上拉事件完毕之后在进行下一次的上拉

    // 2.请求首页商品数据,动态获取page
    getHomeGoods(type) {
      const page = this.goods[type].page + 1;
      getHomeGoods(type, page).then((res) => {
        // console.log(res.data);
        this.goods[type].list.push(...res.data.list);
        this.goods[type].page += 1;
        // 完成上拉加载更多
        this.$refs.scroll.finishPullUp();
      });
    },
2.11. 返回顶部BackTop组件

两个问题:
什么时候显示backtop组件<back-top v-show="isShowBackTop"></back-top>

  • 封装BackTop组件,在components中的content文件夹下
  • 定义一个常量,决定是否显示BackTop组件, Home.vue的data中 定义 isShowBackTop: false初始值给false
  • 监听滚动,决定BackTop的显示和隐藏 ,即在scroll中添加滚动的位置的事件监听,this.scroll.on(“scroll”, (position) => { }然后将这个事件传出this.$emit(“scroll”, position)
    -在Home.vue中的contentScroll(position) 方法中,判断是否显示,利用 isShowBackTop = -position.y > 1000时显示
    实现点击返回顶部
  • 实现点击时,调用scroll对象的scroll.scrollTo(x, y, time)返回顶部 , 监听BackTop的点击
  • 直接监听back-top的点击 ?
    • 不能直接监听组件的点击, 必须添加修饰.native,<back-top @click.native="backClick" v-show="isShowBackTop"></back-top>
    • 在methods方法中,设置方法
    • backClick() {
      this.$refs.scroll.scrollTo(0, 0);
      },

部分代码:

Home.vue中:

<back-top @click.native="backClick" v-show="isShowBackTop"></back-top>
methods:{
...
    // 利用scrollTo方法实现返回顶端
    backClick() {
      // console.log('backClick');
      this.$refs.scroll.scrollTo(0, 0);
    },
   // 利用position属性,判断BackTop显示和隐藏、TabContro是否吸顶
    contentScroll(position) {
      // console.log(position+'返回顶端的显示和隐藏');
      // 1.判断backTop是否显示
      this.isShowBackTop = -position.y > 1000;
      // 2.判断tabControl是否吸顶
      this.isTabFixed = -position.y > this.tabOffsetTop;
    },  
}

Scroll组件:

    // 2.监听滚动的位置
    if (this.probeType === 2 || this.probeType === 3) {
      this.scroll.on("scroll", (position) => {
        // console.log(position);
        this.$emit("scroll", position);
      });
    }
2.13. tabControl的停留

tabControl2滚动到什么位置才吸顶?(也就是2在什么位置的时候1出现)

  • 重新添加一个tabControl1组件(需要设置定位,否则会被盖住)
  • 需要知道滚动到多少的时候有吸顶,定义变量taboffsetTop,获取tabControl的offsetTop,但是组件是没有offsetTop的,所以就得用el拿组件的元素
this.tabOffsetTop =this.$refs.tabControl2.$el.offsetTop;
  • 但是这样拿的是不准确的,因为这个时候可能轮播图的图片还没有加载完成,所以要先监听轮播图的图片是否已经加载完成。在HomeSwipper.vue中 <img :src=“item.image” @load=“imageLoad”>,然后在下边的methods中的imageLoaded中把这个事件发送出去
  methods:{
    imageLoad(){
      // 为了不让HomeSwiper多次发出事件,可以使用isLoad的变量进行状态的记录
      if(!this.isLoad){
        this.$emit("swiperImageLoad")
        this.isLoad=true
      } 
    }
  }

在Home.vue中监听:

    //  获取到tabControl的offsetTop,监听HomeSwiper中img的加载完,可以使用isLoad的变量进行状态的记录不让HomeSwiper多次发出事件
    swiperImageLoad() {
      this.tabOffsetTop = this.$refs.tabControl2.$el.offsetTop;
    },

这样打印的offsetTop值就是正确的了,但是这样事件发送了四次,在这里只是需要发送一次,在homeSwipper.vue的data中设置一个isLoad:false的data属性,可以使用isLoad的变量进行状态的记录不让HomeSwiper多次发出事件

什么时候显示新添加的tabControl1?
这个时候拿到了tabControl2的offsetTop

  • 判断是否滚动超过了offsetTop来决定是否显示新添加的tabControl,显示隐藏定义一个变量isTabFixed,初始为false
    <tab-control
      :titles="['流行', '新款', '精选']"
      @tabClick="tabClick"
      ref="tabControl1"
      class="tab-control"
      v-show="isTabFixed"
    ></tab-control>

在methods中判断是否吸顶:

    contentScroll(position) {
      // console.log(position+'返回顶端的显示和隐藏');
      // 1.判断backTop是否显示
      this.isShowBackTop = -position.y > 1000;
      // 2.判断tabControl是否吸顶
      this.isTabFixed = -position.y > this.tabOffsetTop;
    },

这个时候存在 流行 新款 精选与下边的显示不对应的问题,所以在实现tabclick点击的时候把currentIndex改成当前的index ,并且让两个tabcontrol的currentindex保持一致

  methods: {
    // 事件监听
    // 1.监听tabclick 流行 精选 热卖的点击,实现吸顶
    tabClick(index) {
      switch (index) {
        case 0:
          this.currentType = "pop";
          break;
        case 1:
          this.currentType = "new";
          break;
        case 2:
          this.currentType = "sell";
          break;
      }
      this.$refs.tabControl1.currentIndex = index;
      this.$refs.tabControl2.currentIndex = index;
    },
2.14 让Home保持原来的状态

当从home.vue切换到其他页面的时候,home.vue会被销毁,为了不让home.vue被销毁,要在app.vue中加一个keep-alive属性把路由包起来

App.vue中:

<keep-alive exclude="Detail">
      <router-view></router-view>
</keep-alive>

这样的话是不会被销毁了,但是存在当再次切换到home.vue的时候回不到原来的浏览位置,解决方法:在home.vue中给一个saveY: 0的data,然后在组件被切换之前记录scrollY的值,当组件被切换回来的时候,直接scrollTo到原来的saveY的位置。

让Home中的内容保持原来的位置

  • 离开时, 保存一个位置信息saveY.
  • 进来时, 将位置设置为原来保存的位置saveY信息即可.
  • 注意: 最好回来时, 进行一次refresh()
  activated() {
    this.$refs.scroll.refresh()
    this.$refs.scroll.scrollTo(0, this.saveY,0);
  },
  deactivated() {
    this.saveY = this.$refs.scroll.getScrollY() // .getScrollY() 等同于 .scroll.y 不过是进行了方法的封装
    // console.log(this.saveY);
  },

三 详情页开发

1.点击商品进入详情页

作用:点击某一个商品信息,就跳转到对应的详情页面,每一个商品就是一个单独的goodslistitem
给详情页配置一个router,在views文件夹下,创建一个detail的文件夹,然后在下边创建一个Detail.vue的组件,然后在router下的index.js中配置对应的path。
这里传参使用的是params格式
router/index.js
配置路由格式:

  {
    path: '/detail/:iid',
    component: Detail
  },

在goodslistitem.vue组件中添加click事件,

 <div class="goods-item" @click="itemClick">

然后在下边的methods中,动态的获取iid

itemclick() {
	this.$router.push('/detail/'+this.goodsItem.iid) //实现路由的跳转,因为detail界面有前进和后退按钮,所以用push来切换路由。
}

详情页如何拿到iid?

在detail.vue中的data中实现 iid的保存 iid: null
在created中

  // 请求数据
  created() {
    // 1.保存传入的iid
    this.iid = this.$route.params.iid;
2.详情页的导航栏

之前已经封装好了一个navbar的组件,所以直接引用即可,但是如何直接在detail.vue中引入的话会出现一个功能一个功能的一坨坨的代码,所以把每一个单独的功能封装到childComponents文件夹中,
在这里,先创建一个DetailNavBar.vue的组件,然后在这里边写详细的导航栏的封装,再把这个组件在detail.vue中导入 注册 使用即可。
DetailNavBar.vue组件的详细内容如下:

<template>
  <nav-bar class="detail-nav">
    <img
      slot="left"
      class="back"
      @click="backClick"
      src="~assets/img/common/back.svg"
    />
    <div class="title" slot="center">
      <span
        class="title-item"
        v-for="(item, index) in titles"
        :key="index"
        :class="{ active: index === currentIndex }"
        @click="titleClick(index)"
      >
        {{ item }}
      </span>
    </div>
  </nav-bar>
</template>

<script>
import NavBar from "components/common/navbar/NavBar";

export default {
  name: "DetailNavBar",
  components: {
    NavBar,
  },
  data() {
    return {
      titles: ["商品", "参数", "评论", "推荐"],
      currentIndex: 0,
    };
  },
  methods: {
    titleClick: function (index) {
      this.currentIndex = index;// 实现点击哪一个哪一个变为红
      this.$emit("titleClick", index);
    },
    backClick() {
      this.$router.back();// 返回上一个页面
    },
  },
};
</script>

<style scoped>
.detail-nav {
  background-color: #fff;
  font-weight: normal;
}

.title {
  display: flex;
  padding: 0 20px;
  font-size: 14px;
}

.title-item {
  flex: 1;
}

.title-item.active {
  color: var(--color-high-text);
}

.back {
  margin-top: 12px;
}
</style>

3.请求详情页的数据

detail的网络请求放在network下的detail.js中

然后在detail.vue中导入该模块

import {
  getDetailId,
  getRecommend,
  Goods,
  Shop,
  GoodsParam,
} from "network/detail";

然后在detail.vue 的created发起请求,获取数据

  // 请求数据
  created() {
    // 1.保存传入的iid
    this.iid = this.$route.params.iid;
    // 2.请求数据
    getDetailId(this.iid).then((res) => {
      // console.log(res);
      // 2.1.获取结果
      const data = res.result;
      // 2.2.获取顶部信息
      this.topImages = data.itemInfo.topImages;
      // 2.3.获取商品信息
      this.goods = new Goods(
        data.itemInfo,
        data.columns,
        data.shopInfo.services
      );
      // 2.4.获取店铺信息
      this.shop = new Shop(data.shopInfo);
      // 2.5.获取商品x详细信息
      this.detailInfo = data.detailInfo;
      // 2.6.保存参数信息
      this.paramInfo = new GoodsParam(
        data.itemParams.info,
        data.itemParams.rule
      );
      // 2.7.保存评论信息
      if (data.rate.list) {
        this.commentInfo = data.rate.list[0];
      }
    });
4.轮播图的实现

首先在detail.vue中拿到轮播图的数据,在data中创建topimages: []然后在methods中获取顶部图片信息 this.topImages = data.itemInfo.topImages因为detail-swipper 要单独抽成一个组件所以把请求到的图片信息传出去,使用 <detail-swiper :images=“topImages”/ 然后在DetailSwiper.vue组件中接收来自父组件的参数。

<template>
  <swiper class="detail-swiper">
    <swiper-item v-for="item in topImages" :key="item">
      <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>
  .detail-swiper {
    height: 300px;
    overflow: hidden;
  }
</style>

最后在detail.vue中导入 注册 使用DetailSwiper.vue组件

5.商品基本数据的展示

数据的请求放在network的detail.js中,为了充分体现面向对象的封装思想,把请求的店铺信息封装成一个函数导出
然后在detail.vue的组件中导入该函数,在data中设置参数goods:null,在created中使用该函数

      // 2.3.获取商品信息
      this.goods = new Goods(
        data.itemInfo,
        data.columns,
        data.shopInfo.services
      );

展示的话是放在一个子组件DetailBaseInfo.vue中的
然后在detail.vue组件中导入注册 使用子组件DetailBaseInfo.vue

<detail-base-info :goods="goods"/>
6.店铺信息的展示

数据的请求放在network的detail.js中,为了充分体现面向对象的封装思想,把请求的店铺信息封装成一个函数导出
然后在detail.vue的组件中导入该函数,然后在data的shop: {},保存店铺信息,在methods中创建请求函数,this.shop = new Shop(data.shopInfo);
然后在子组件DetailShopInfo.vue中展示数据
最后在detail.vue中导入 注册 使用该子组件

<detail-shop-info :shop="shop"/>
7.商品图片的展示

在detail的methods中创建请求函数,在created中发送数据请求
然后在DetailGoodsInfo.vue组件中进行数据的展示
然后在detail.vue中导入 注册 使用该组件

<detail-goods-info :detail-info="detailInfo"/>

但是这里也是存在better-scroll刚开始的时候的height是有内容撑开的,但是这个时候图片还没有请求过来,所以当图片再次请求过来的时候没有及时的refresh(),所以滚动有时候不好用

解决方法 :在DetialGoodsInfo.vue中监听每一张图片的滚动事件,然后把这个事件发送出去到detail.vue中进行refresh()

<template>
  <div v-if="Object.keys(detailInfo).length !== 0" class="goods-info">
    <div class="info-desc">
      <div class="start"></div>
      <div class="desc">{{ detailInfo.desc }}</div>
      <div class="end"></div>
    </div>
    <div class="info-key">{{ detailInfo.detailImage[0].key }}</div>
    <div class="info-list">
      <img
        v-for="(item, index) in detailInfo.detailImage[0].list" v-lazy="item" alt="" :key="index" @load="imgLoad"/>
    </div>
  </div>
</template>

<script>
//  clear-fix
	export default {
		name: "DetailGoodsInfo",
    data(){
      return {
        counter:0,
        imagesLength:0
      }
    },
    props: {
      detailInfo: {
        type: Object
      }
    },
    methods:{
      imgLoad(){
        if(++this.counter===this.imagesLength){
          this.$emit('imageLoad')
        }
      }
    },
    watch:{
      detailInfo(){
        this.imagesLength=this.detailInfo.detailImage[0].list.length
      }
    }
	}
</script>

<style scoped>
  .goods-info {
    padding: 20px 0;
    border-bottom: 5px solid #f2f5f8;
  }

  .info-desc {
    padding: 0 15px;
  }

  .info-desc .start, .info-desc .end {
    width: 90px;
    height: 1px;
    background-color: #a3a3a5;
    position: relative;
  }

  .info-desc .start {
    float: left;
  }

  .info-desc .end {
    float: right;
  }

  .info-desc .start::before, .info-desc .end::after {
    content: '';
    position: absolute;
    width: 5px;
    height: 5px;
    background-color: #333;
    bottom: 0;
  }

  .info-desc .end::after {
    right: 0;
  }

  .info-desc .desc {
    padding: 15px 0;
    font-size: 14px;
  }

  .info-key {
    margin: 10px 0 10px 15px;
    color: #333;
    font-size: 15px;
  }

  .info-list img {
    width: 100%;
  }
</style>

8.参数信息的展示

首先在detail.js中创建请求对象

export class GoodsParam {
  constructor(info, rule) {
    // 注: images可能没有值(某些商品有值, 某些没有值)
    this.image = info.images ? info.images[0] : '';
    this.infos = info.set;
    this.sizes = rule.tables;
  }
}

然后在home.vue中导入该函数 ,在created中请求数据,data中初始设置paramInfo: {}
在detail.vue中导入 注册该组件 ,传递paramInfo信息

<detail-param-info ref="param" :param-info="paramInfo"/>

在DetailParamInfo.vue中props中接收paramInfo信息,进行展示

9.评论信息的展示

在detail.vue的data中保存评论信息,然后在methods中创建请求函数,之后在created发送网络请求。
数据的展示放在DetailCommentInfo.vue组件中
在这里存在一个年月日的转化问题,因为这是一个常用的方法,所以在until.js中封装一个时间的格式化函数

export function formatDate(date, fmt) {
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  };
  for (let k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      let str = o[k] + '';
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
    }
  }
  return fmt;
};

最后在detail.vue中导入 注册 使用该组件DetailCommentInfo

<detail-comment-info ref="comment" :comment-info="commentInfo"/>
10.推荐数据的展示

在detail.js中封装请求推荐数据的函数

export function getRecommend() {
	return request({
		url: '/recommend'
	})
}

在detail.vue中导入该函数,然后在data中用一个属性recommendList保存数据,在created中发送网络请求,获得推荐数据信息

    getRecommend().then((res, error) => {
      // console.log(res);
      if (error) return;
      this.recommendList = res.data.list;
    });

数据的展示就用goodslist组件来展示,在detail.vue中导入 注册 使用该组件

 <goods-list ref="recommend" :goods="goodsList"/>
详情页数据滚动出现问题

在这里出现滚动bug ,现象就是推荐数据的图片无法显示,所以需要监听goodslistitem的imgload,当imgload之后就进行页面的刷新

  mounted() {
    // bug解决首页中可滚动区域的问题:监听每一张图片是否加载完成,每一次执行依次refresh
    // 1.图片加载完成后的事件监听,非父子组件的通信,itemImageLoad->GoodsListItem
    const refresh = debounce(this.$refs.scroll.refresh, 50);
    this.$bus.$on("itemImageLoad", () => {
      refresh();
    });

但是这样的话home.vue和detail.vue都对事件同时做了监听,所以在home.vue这个组件被切换掉的时候,要把事件监听取消,这样的话在detail.vue中才可以实现同一事件的监听。

  deactivated() {
    // 1.保存Y值
    this.saveY = this.$refs.scroll.getScrollY() 
    // 2.取消监听
    this.$bus.$off('itemImageLoad', () => {
      this.debounce(this.$refs.scroll.refresh, 50)
	})
  },

同理,在detail.vue组件中当组件被切换掉的时候也需要做同样的处理,但是因为detail.vue没有在keep-alive中所以没有deactivited函数,组件的销毁是在destroyed中的,所以也有对事件监听的取消函数

  destroyed(){
    // 取消监听
    this.$bus.$off('itemImageLoad', () => {
      this.debounce(this.$refs.scroll.refresh, 50)
	})
  },

因为两个组件中的事件监听和取消代码是完全可以复用的,所以可以抽出来,使用混入实现 mixin,在common文件夹中的mixin.js中对可以复用的代码做一个封装

11.mixin混入

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

import { debounce } from "common/utils";

export const itemLisenterMixin = {
  mounted() {
    // 3.监听一些事件
    const refresh = debounce(this.$refs.scroll.refresh, 500)
    this.$bus.$on('imgLoad', () => {
      refresh() 
      console.log("我是混入的内容");
    })
  },
}

在detail.vue中使用混入的话,就先导入 itemLisenterIn 然后设置mixins: [itemLisenterIn]

import {itemLisenterMixin} from "common/mixin"

mixins:[itemLisenterMixin] // 和data同级
12.标题和内容的联动

首先监听这四个参数的点击,在detailnavbar.vue组件中,然后把这个点击事件传出去,

  methods: {
    titleClick: function (index) {
      this.currentIndex = index;// 实现点击哪一个哪一个变为红
      this.$emit("titleClick", index);
    },
    backClick() {
      this.$router.back();// 返回上一个页面
    },
  },

在detail.vue对事件进行监听

<detail-nav-bar class="detail-nav" @titleClick="selectClick" ref="nav" />

然后在下边的methods中进行方法的设置,data中设置themeTopYs: [],来保存四个参数对应的offsettop

  methods: {
    // 点击标题立即滚动到对应的位置
    selectClick(index) {
      // console.log(index);
      this.$refs.scroll.scrollTo(0, -this.themeTopYs[index], 100);
    },

每一个themeTopYs值在哪里可以取到呢?
created中开始请求数据,但是拿不到dom,不可以;mounted中拿到dom,但是数据还没有拿到,所以不可以;在methods中的图片加载完之后再去调用;
在data中给一个getThemeTopy:[],在created中设置一个函数;methods调用

为了避免获取高度太频繁,可以用一个debounce函数来包装

created(){
    // 获取主题的高度  
    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);
    }, 100);

然后在methods中的imageLoad()中调用一下 函数。

methods:{
    // 图片加载完获取主题的高度
    detailImageLoad() {
      // this.$refs.scroll.refresh();
      const refresh=debounce(this.$refs.scroll.refresh,50)
      refresh()
      this.getThemeTopy();
    },

联动效果之混动到商品 参数 评论 推荐上边显示对应的标题

首先获取scroll滚动的实时位置

    <scroll
      class="content"
      ref="scroll"
      @scroll="contentScroll"
      :probe-type="3"
    >

然后在下边的methods设置相应的滚动

    // 内容滚动 联动显示正确的标题???
    contentScroll(position) {
      // 获取y值
      const positionY = -position.y;
      // 在positionY值和主题值之间对比
      for (let i = 0; i < this.themeTopYs.length - 1; i++) {
        if (this.currentIndex !== i && 
          (positionY >= this.themeTopYs[i] &&
          positionY < this.themeTopYs[i + 1] )) {
          this.currentIndex=i
          this.$refs.nav.currentIndex = this.currentIndex;
        }
      }
    },
13.底部工具栏

封装一个detailBottomBar.vue,然后在detail.vue中导入 注册 使用该组件

<template>
  <div class="bottom-bar">
    <div class="bar-item bar-left">
      <div>
        <i class="icon service"></i>
        <span class="text">客服</span>
      </div>
      <div>
        <i class="icon shop"></i>
        <span class="text">店铺</span>
      </div>
      <div>
        <i class="icon select"></i>
        <span class="text">收藏</span>
      </div>
    </div>
    <div class="bar-item bar-right">
      <div class="cart" @click="addToCart">加入购物车</div>
      <div class="buy">购买</div>
    </div>
  </div>
</template>

<script>
	export default {
		name: "DetailBottomBar",
    methods: {
      addToCart(){
        // console.log('加入到购物车');
        this.$emit("addToCart")
      }
    }
	}
</script>

<style scoped>
  .bottom-bar {
    height: 58px;
    position: fixed;
    background-color: #fff;
    left: 0;
    right: 0;
    bottom: 0;

    display: flex;
    text-align: center;
  }

  .bar-item {
    flex: 1;
    display: flex;
  }

  .bar-item>div {
    flex: 1;
  }

  .bar-left .text {
    font-size: 13px;
    text-align: center;
  }

  .bar-left .icon {
    display: block;
    width: 22px;
    height: 22px;
    margin: 10px auto 3px;
    background: url("~assets/img/detail/detail_bottom.png") 0 0/100%;
  }

  .bar-left .service {
    background-position:0 -54px;
  }

  .bar-left .shop {
    background-position:0 -98px;
  }

  .bar-right {
    font-size: 15px;
    color: #fff;
    line-height: 58px;
  }

  .bar-right .cart {
    background-color: #ffe817;
    color: #333;
  }

  .bar-right .buy {
    background-color: #f69;
  }
</style>

14.返回顶部

按照正常的流程的话就是,导入 backTop组件 注册 使用,复制一些home.vue中的代码,但是这里我们可以把该组件包装成一个混入

import BackTop from "components/content/backTop/BackTop"
export const backTopMixin = {
  components: {
    BackTop
  },
  data() {
    return {
      isShowBackTop: false
    }
  },
  methods: {
    backClick() {
      this.$refs.scroll.scrollTo(0, 0, 1000)
    }
  }
}

然后在detail.vue中

import {backTopMixin} from "common/mixin"; //导入

mixins: [backTopMixin],// 使用一下

四、购物车页面

15.将商品添加入到购物车

在DetailBottomBar.vue中监听 加入购物车 按钮的点击,设置一个addToCart事件并且发送出去,在Detail组件中进行监听

 methods: {
      addToCart(event) {
      	// this.$refs.ball.run(event.target)
        this.$emit('addToCart')
      }
    }

在detail.vue中监听,获取购物车需要的信息,添加到store,这里需要使用vuex管理商品信息,然后在购物车Cart组件中就可以使用获得的数据了

首先安装 npm install vuex --save
然后在store文件夹下的index.js中创建vuex对象并且导出
然后在main.js中导入注册 store

index.js

import Vuex from "vuex"
import Vue from "vue"
import getters from "./getters"

Vue.use(Vuex)

const store=new Vuex.Store({
  state:{
    cartList:[]
  },
  mutations:{
    // 提交 mutation 是更改状态state的唯一方法
  },
  actions:{
  // 逻辑操作,异步操作都可以放在这里,这里实现了
  // 查找之前的数组是否有该数据,如果有,当前商品数量加1,addCounter;如果没有,添加了新的商品,addToCart;虽然也可以放在mutations中,不过涉及逻辑,actions最好,Action 提交的是 mutation, 使用的时候通过dispatch提交到mutations
  }
  getters:{
  // 从state中派生出来一些状态
}

export default store

在detail.vue中监听,获取购物车需要的信息,添加到store

<detail-bottom-bar @addToCart="addToCart"></detail-bottom-bar>

    addToCart() {
      // 获取购物车信息
      // 1.创建对象
      const product = {};
      // 获取购物车需要展示的信息
      product.iid = this.iid;
      product.imgURL = this.topImages[0];
      product.title = this.goods.title;
      product.desc = this.goods.desc;
      product.realPrice = this.goods.realPrice;
      // 2.将商品添加到购物车中
      this.$store.commit("addCart", product);// 没用actions
      this.$store.dispatch("addCart", product);// actions
16.将商品添加到store,重构vuex的代码

index.js

const store=new Vuex.Store({
  state:{
    cartList:[]
  },
  mutations:{
    addCounter(state,payload){
      payload.count++
    },
    addToCart(state,payload){
      payload.checked=true
      state.cartList.push(payload);
    }
  },
  actions:{
  // 为什么不放到mutations?官方建议将异步和逻辑相关的操作放在actions中
    addCart(context,payload){
        // 1.查找之前数组中是否有该数据
        const oldProduct = context.state.cartList.find(item => item.iid === payload.iid)
        // 2.判断oldProduct
        if (oldProduct) {
          // oldProduct.count+=1
          context.commit('addCounter', oldProduct)
          
        } else {
          payload.count = 1
          context.commit('addToCart', payload)
          
        }
      })
    }
  },
  getters
})

getters.js

export default {
  cartLength(state) {
    return state.cartList.length
  },
  cartList(state) {
    return state.cartList
  }
}
17.购物车导航栏以及商品的展示

前提是已经把商品的iid即需要在购物车页面展示的信息放在vuex中了。

1、购物车导航栏的制作(样式居中显示 购物车(数量)
首先在cart.vue中导入 注册 使用navbar组件,购物车商品数量的展示就是store中存放商品信息的cartList的长度。购物车({{ cartLength }})

@<template>
  <div class="cart">
    <nav-bar class="nav-bar">
      <div slot="center">购物车({{ cartLength }})</div>
    </nav-bar>
    <cart-list></cart-list>
    <cart-bottom-bar></cart-bottom-bar>
  </div>
</template>

<script>
import NavBar from "components/common/navbar/NavBar";
import { mapGetters } from "vuex";
import CartList from "./childCart/CartList"
import CartBottomBar from "./childCart/CartBottomBar"

export default {
  name: "Cart",
  components: {
    NavBar,
    CartList,
    CartBottomBar
  },
  computed: {
    ...mapGetters(["cartLength"]),
  },
};
</script>

<style scoped>
.nav-bar {
  background-color: var(--color-high-text);
  color: #fff;
}
.cart {
  height: 100vh;
}
</style>

在getter.js中将商品数量,商品导出,在cart.vue组件中用mapGetters工具函数会将store中的getter映射到局部计算属性中,而不必以下面的形式使用

computed: {
      cartLength() {
      	return this.$store.state.cartList.length
      }
      ])
    }

getter.js

export default {
  cartLength(state) {
    return state.cartList.length
  },
  cartList(state) {
    return state.cartList
  }
}

2.购物车页面的展示
在cart文件夹下的子组件文件中封装一个cartList.vue的组件,然后在cart.vue中导入 注册 使用该组件

<template>
  <div class="cart-list">
    <scroll class="content" ref="scroll">
      <cart-list-item v-for="(item,index) in cartList" :key="index" :item-info="item"/>
    </scroll>
  </div>
</template>

<script>
  import Scroll from "components/common/scroll/Scroll";
  import CartListItem from "./CartListItem";
  import {mapGetters} from "vuex";
  export default {
    name: "CartList",
    data(){
      return{

      }
    },
    components:{
      CartListItem,Scroll
    },
    computed:{
      ...mapGetters(['cartList'])
    },
    activated() {
      this.$refs.scroll.refresh()
    },
  }
</script>

<style scoped>
  .content{
    height:100%;
  }
  .cart-list{
    height:calc(100% - 44px - 49px - 40px);
    overflow: hidden;
  }
</style>

因为每一个商品也相当于一个单独的组件,所以要封装CartListItem这个组件。每一行

<template>
  <div id="shop-item">
    <div class="item-selector">
      <check-button
        :isChecked="itemInfo.checked"
        @click.native="checkButton"
      ></check-button>
    </div>
    <div class="item-img">
      <img :src="itemInfo.imgURL" alt="商品图片" />
    </div>
    <div class="item-info">
      <div class="item-title">{{ itemInfo.title }}</div>
      <div class="item-desc">商品描述: {{ itemInfo.desc }}</div>
      <div class="info-bottom">
        <div class="item-price left">¥{{ itemInfo.realPrice }}</div>
        <div class="item-count right">x{{ itemInfo.count }}</div>
      </div>
    </div>
  </div>
</template>

<script>
import CheckButton from "components/common/checkButton/CheckButton";
export default {
  name: "CartListItem",
  components: {
    CheckButton,
  },
  props: {
    itemInfo: Object,//接收传递过来的itemInfo
    default() {
      return {};
    },
  },
  methods: {
    checkButton() {
      this.itemInfo.checked = !this.itemInfo.checked;
    },
  },
};
</script>

<style scoped>
#shop-item {
  width: 100%;
  display: flex;
  font-size: 0;
  padding: 5px;
  border-bottom: 1px solid #ccc;
}

.item-selector {
  width: 14%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.item-title,
.item-desc {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.item-img {
  padding: 5px;
  /*border: 1px solid #ccc;*/
}

.item-img img {
  width: 80px;
  height: 100px;
  display: block;
  border-radius: 5px;
}

.item-info {
  font-size: 17px;
  color: #333;
  padding: 5px 10px;
  position: relative;
  overflow: hidden;
}

.item-info .item-desc {
  font-size: 14px;
  color: #666;
  margin-top: 15px;
}

.info-bottom {
  margin-top: 10px;
  position: absolute;
  bottom: 10px;
  left: 10px;
  right: 10px;
}

.info-bottom .item-price {
  color: orangered;
}
</style>

18.商品选中不选中

这样的话就可以正常显示了,但是前边还缺一个小按钮的图标,进行CheckButton.vue封装。接受cartListItem组件传递过来的isChecked

<template>
  <div class="check-button" :class="{check: isChecked}">
    <img src="~assets/img/cart/tick.svg" alt="">
  </div>
</template>

<script>
  export default {
    name: "CheckButton",
    props:{
      isChecked:{
        type:Boolean,
        default:false
      }
    }
  }
</script>

<style scoped>
.check-button{
  border-radius: 50%;
  background-color: #a3a3a5;
}
.check{
  background-color: red;
}
</style>

然后把这个组件在cartListItem使用

    <div class="item-selector">
      <check-button
        :isChecked="itemInfo.checked"
        @click.native="checkButton"
      ></check-button>
    </div>
...
  methods: {
    checkButton() {
      this.itemInfo.checked = !this.itemInfo.checked;
    },
  },

19.底部工具栏的封装 CartBottomBar

封装一个单独的组件,然后在cart.vue中导入 注册 使用该组件。

20 全选全不选逻辑

逻辑:当所有按钮均被选中的时候该位置就全选了。
在CartBottomBar.vue中进行

<check-button class="check-button" @click.native="checkClick" :isChecked="isCheckAll"></check-button>

    // 全选的状态显示:只要有一个不选中,全选按钮就是不选中
    isCheckAll(){
      if(this.cartList.length===0) return false
		  // return !(this.$store.state.cartList.filter(item => !item.checked).length)
		  // return !this.$store.state.cartList.find(item => !item.checked)   //  return false
	  return this.cartList.every(item => item.checked)// retunr false 一假为假
    }

点击全选按钮就把所有商品选中的实现逻辑:首先得监听全选按钮的点击事件

  methods:{
    // 点击全选
    checkClick(){
      // console.log('---');
      if(this.isCheckAll){
        // 原来都是全选,点击一次,全部不选中
        this.cartList.forEach(item=>item.checked=false)
      }else {
        // 原来是全不选或者有一个不选,点击一次,全选中
        this.cartList.forEach(item => item.checked=true)
      }
    },
21.合计去结算逻辑

在CartBottomBar.vue中,去结算有一个购物车为空的时候的提示,弹窗实现请添加商品到购物车

@<template>
  <div class="bottom-bar">
    <div class="check-content">
      <check-button class="check-button" @click.native="checkClick" :isChecked="isCheckAll"></check-button>
      <span>全选</span>
    </div>
    <div class="totalPrice">合计:{{totalPrice}}</div>
    <div class="calculate" @click="calcClick">去结算({{checkLength}})</div>
  </div>
</template>

<script>
import CheckButton from "components/common/checkButton/CheckButton";
import { mapGetters } from 'vuex';

export default {
  name: "CartBottomBar",
  data(){
    return{
      isChecked:false
    }
  },
  components: {
    CheckButton,
  },
  computed: {
    ...mapGetters(['cartList']),
    totalPrice() {
      return ("¥" + this.$store.state.cartList.filter((item) => {
            return item.checked;
          }).reduce((preValue, item) => {
            return preValue + item.realPrice * item.count;
          },0).toFixed(2)
      );
    },
    checkLength(){
      return this.$store.state.cartList.filter((item)=>{
        return item.checked
      }).reduce((preValue,item)=>{
        return preValue + item.count
      },0)
    },
    // 全选的状态显示:判断是否有一个不选中,全选就是不选中
    isCheckAll(){
      if(this.cartList.length===0) return false
		  // return !(this.$store.state.cartList.filter(item => !item.checked).length)
		  // return !this.$store.state.cartList.find(item => !item.checked)   //  return false
		  return this.cartList.every(item => item.checked)// retunr false 一假为假
    }

  },
  methods:{
    // 点击全选
    checkClick(){
      // console.log('---');
      if(this.isCheckAll){
        // 原来都是全选,点击一次,全部不选中
        this.cartList.forEach(item=>item.checked=false)
      }else {
        // 原来是全不选或者有一个不选,点击一次,全选中
        this.cartList.forEach(item => item.checked=true)
      }
    },
    calcClick(){
      if(!this.isCheckAll){
        if(this.cartList.length!=0){
            this.$toast.show('请选择要购买的商品')
          }else {
              this.$toast.show('请先加购商品到购物车')
          }
      }
    }
  }
};
</script>
22.详情页添加一个点击加入购物车弹出一个toast弹窗

目前的detail.vue

    addToCart() {
      // 获取购物车信息
      // 1.创建对象
      const product = {};
      // 1.获取购物车需要展示的信息
      product.iid = this.iid;
      product.imgURL = this.topImages[0];
      product.title = this.goods.title;
      product.desc = this.goods.desc;
      product.realPrice = this.goods.realPrice;
      // 2.将商品添加到购物车中
      // this.$store.commit("addCart", product);
      this.$store.dispatch("addCart", product)
      // 3.添加到购物车成功
      

如何知道添加到购物车成功,我们知道dispatch会返回一个promise对象,可以用promise函数把resolve的结果传出去,然后在对用的位置then方法调用一下。

store文件夹下的index.js,return Promise,可以用resolve做回调

  actions:{
    addCart(context,payload){
      return new Promise((resolve,reject)=>{
        // 1.查找之前数组中是否有该数据
        const oldProduct = context.state.cartList.find(item => item.iid === payload.iid)
        // 2.判断oldProduct
        if (oldProduct) {
          // oldProduct.count+=1
          context.commit('addCounter', oldProduct)
          resolve('当前商品数量+1')
        } else {
          payload.count = 1
          context.commit('addToCart', payload)
          resolve('添加了新的商品')
        }
      })
    }
  },

在detail.vue中

    addToCart() {
      // 获取购物车信息
      // 1.创建对象
      const product = {};
      // 2.获取购物车需要展示的信息
      product.iid = this.iid;
      product.imgURL = this.topImages[0];
      product.title = this.goods.title;
      product.desc = this.goods.desc;
      product.realPrice = this.goods.realPrice;
      // 3.将商品添加到购物车中(1.promise 2.mapActions映射)
      // this.$store.commit("addCart", product);
      this.$store.dispatch("addCart", product).then((res) => {
         console.log(res);
        });
	// this.addCart(product).then(res =>{console.log(res);}) 
	// mapActions映射,在methods中...mapActions(['addCart']),本质上还是调用dispatch
23.封装toast

之前那样做,只是将res打印出来,因此需要一个组件将其以弹窗的形式显示出来toast

封装一个toast组件,普通方法封装,在main.js中导入注册使用,包含一个show方法,控制显示和隐藏

<template>
  <div class="toast" v-show="isShow">
    <div >{{message}}</div>
  </div>
</template>

<script>
  export default {
    name: "Toast",
    data(){
      return {
        message:'',
        isShow:false
      }
    },
    methods:{
      show(message,duration=2000){
        this.isShow=true;
        this.message=message
        setTimeout(()=>{
          this.isShow=false;
          this.message=''
        },duration)
      }
    }
  }
</script>

在detail中导入toast组件,注册使用,data中增加message,show属性,addCart方法中使用定时器

<toast :message="message" :isShow="show"/>

isShow
在这里插入图片描述
插件的方式封装
推荐
将toast作为插件使用,最后能用this.$toast.show(res, )
在toast文件夹中 新建一个index.js,在main.js中将其作为插件安装进来
main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from "./store"
import toast from "components/common/toast"
import VueLazyload from 'vue-lazyload'

Vue.config.productionTip = false

Vue.prototype.$bus = new Vue()
// 安装插件
Vue.use(toast)
Vue.use(VueLazyload)

new Vue({
  render: h => h(App),
  router,
  store,
  toast
}).$mount('#app')

toast/index.js

import Toast from "./Toast"

const obj={}
obj.install=function(Vue){
  // Vue.extend(Toast)
  // 1 创建组件构造器
  const toastContrustor = Vue.extend(Toast)
  // 2.根据组件构造器可以new出来一个组件对象
  const toast = new toastContrustor()
  // 3.将组建对象 手动挂载待一个元素上
  toast.$mount(document.createElement('div'))
  //4. toast.$el对应的就是div
  document.body.appendChild(toast.$el)
  Vue.prototype.$toast = toast
}
export default obj

项目收获与优化

收获

首先,通过做这个项目,体验到了在实现一个页面功能设计的时候,要先进行功能分析,划分目录结构和页面结构,体会到了采用组件化的思想封装基础公共组件、独立业务组件的好处,对项目开发的基本流程有了一定的认识

其次,在项目的推进过程中,自己独立解决了一些遇到的问题,提高了自己的纠错能力,同时还是对自学期间理论知识的回顾,查缺补漏,夯实了基础

优化
1、图片懒加载
图片的懒加载:用到时在加载。
首先安装,npm install vue-lazyload --save,
然后在main.js中 导入 使用Vue.use(VueLazyLoad),
最后把需要用到图片的地方修改img:src改成v-lazy

<img v-lazy="showImage" alt="" @load="imageLoad">

设置占位图的方法:就是在main.js中给一张默认图片,在图片加载过程中显示这张图片。

Vue.use(LazyLoad, {
  loading: require('assets/img/common/placeholder.png')
})

2、过程中优化,防抖
作用:如果mounted()中每加载完一张照片就刷新一次的话,会刷新的非常频繁,浪费性能,所以使用防抖函数,即事件触发n秒之内再去执行,如果在这n秒之内事件被再次触发,则重新计时。
在home.vue的methods中创建一个防抖函数,可以独立封装出来,放在common文件夹下的untils ,然后以后再有组件使用就调用该函数,import { debounce } from “common/utils”;

export function debounce(func, delay) {
  let timer = null
  return function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, delay)
  }
}

项目中遇到的问题

1、Scroll滚动的问题
问题:
使用scroll组件的时候,可能会出现加载到一半拉不动的问题,在创建的BScroll对象的时候,滚动高度是由scrollerHeight(根据放在scroll中的子组件的高度决定的)决定的,但因为图片是异步加载的,刚开始的时候子组件中的图片可能并没有请求过来,所以scrollerHeight的高度就是错误的,但是当新的图片已经被请求过来的时候,scrollerHeight不会进行更新,所以,会出现加载到一半加载不动的问题

解决方法:
监听GoodsListItem组件的每一张图片是否加载完成,只要有一张图片加载完成,就让Scroll组件刷新一次
1、那么如何监听到图片加载完成了呢?
vue中@load事件可以监听图片加载完成,在GoodsListItem组件中,添加加载完成事件<img @load=“imgLoad”> 然后在下边的methods中,创建imgLoad()的方法,再发送

methods:{
  // vue中监听图片记载完成用@load
  // 非父子组件之间通信,这里用事件总线的方式
  imageLoad(){
    this.$bus.$emit('itemImageLoad')
  },

2、但是怎么把GoodsListItem组件中的事件发送出去,Home.vue怎么拿到这个事件呢?
涉及到非父子组件通信,采用事件总线的方法
Home.vue组件:

  mounted() {
    // bug解决首页中可滚动区域的问题:
    // 监听每一张图片是否加载完成,每一次执行依次refresh
    // 1.图片加载完成后的事件监听,非父子组件的通信,itemImageLoad->GoodsListItem
    const refresh = debounce(this.$refs.scroll.refresh, 50);
    this.$bus.$on("itemImageLoad", () => {
      refresh();
    });
  },

2、解决refresh找不到的问题,可滚动区域的问题
问题一: refresh找不到的问题

  • 在Scroll.vue中, 调用this.scroll的方法之前, 判断this.scroll对象是否有值
  • 在mounted生命周期函数中使用 this.$refs.scroll而不是created中

问题二: 对于refresh非常频繁的问题, 进行防抖操作
防抖函数起作用的过程:

  • 如果我们直接执行refresh, 那么refresh函数会被执行30次.
  • 可以将refresh函数传入到debounce函数中,生成一个新的函数
  • 之后在调用非常频繁的时候, 就使用新生成的函数.
  • 而新生成的函数, 并不会非常频繁的调用, 如果下一次执行来的非常快,那么会将上一次取消掉

3、详情页商品推荐数据滚动出现问题
在这里出现滚动bug ,现象就是推荐数据的图片无法显示,所以需要监听goodslistitem的imgload,当imgload之后就进行页面的刷新

  mounted() {
    // bug解决首页中可滚动区域的问题:监听每一张图片是否加载完成,每一次执行依次refresh
    // 1.图片加载完成后的事件监听,非父子组件的通信,itemImageLoad->GoodsListItem
    const refresh = debounce(this.$refs.scroll.refresh, 50);
    this.$bus.$on("itemImageLoad", () => {
      refresh();
    });

但是这样的话home.vue和detail.vue都对事件同时做了监听,所以在home.vue这个组件被切换掉的时候,要把事件监听取消,这样的话在detail.vue中才可以实现同一事件的监听。

  deactivated() {
    // 1.保存Y值
    this.saveY = this.$refs.scroll.getScrollY() 
    // 2.取消监听
    this.$bus.$off('itemImageLoad', () => {
      this.debounce(this.$refs.scroll.refresh, 50)
	})
  },

同理,在detail.vue组件中当组件被切换掉的时候也需要做同样的处理,但是因为detail.vue没有在keep-alive中所以没有deactivited函数,组件的销毁是在destroyed中的,所以也有对事件监听的取消函数

 destroyed(){
    // 取消监听
    this.$bus.$off('itemImageLoad', () => {
      this.debounce(this.$refs.scroll.refresh, 50)
	})
  },

因为两个组件中的事件监听和取消代码是完全可以复用的,所以可以抽出来,使用混入实现 mixin,在common文件夹中的mixin.js中对可以复用的代码做一个封装

封装组件中印象最深的是,怎样封装的

项目的模块是通过 划分的,在components中的common中放进tabbar文件夹,用来引入一个通用的组件。然后在context中放入mainTabBar文件夹,然后在App.vue中导入注册使用MainTabBar组件。

1.如果在下方有一个单独的TabBar组件,你如何封装

自定义TabBar组件,在APP中使用

让TabBar出于底部,并且设置相关的样式

2.TabBar中显示的内容由外界决定

定义插槽

flex布局平分TabBar

3.自定义TabBarItem,可以传入 图片和文字

定义TabBarItem,并且定义两个插槽:图片、文字。

给两个插槽外层包装div,用于设置样式。

填充插槽,实现底部TabBar的效果

4.传入高亮图片

定义另外一个插槽,插入active-icon的数据

(暂时)定义一个变量isActive,通过v-show来决定是否显示对应的icon

5.TabBarItem绑定路由数据

安装路由:npm install vue-router —save

完成router/index.js的内容,以及创建对应的组件

main.js中注册router

APP中加入组件

6.点击item跳转到对应路由,并且动态决定isActive

监听item的点击,通过this.$router.replace()替换路由路径

通过this.$route.path.indexOf(this.link) !== -1来判断isActiv的TRUE,false,判断显示高亮图片

7.动态计算文字active样式

封装新的计算属性:this.isActive ? {'color': 'red'} : {}

tabbar

<template>
    <div class="tab-bar">
      <slot></slot>
    </div>
</template>

<script>
  export default {
    name: "TabBar"
  }
</script>

<style scoped>
  .tab-bar{
    display: flex;
    background-color: #fdfdfd;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    box-shadow:0 -2px 1px rgba(100,100,100,.2);
    z-index: 9;
  }
</style>

TabBarItem

<template>
    <div class="tab-bar-item" @click="itemClick">
      <div v-if="!isActive">
        <slot  name="item-icon"></slot>
      </div>
      <div v-else>
        <slot  name="item-icon-active"></slot>
      </div>
      <div :style="activeStyle">
        <slot  name="item-text"></slot>
      </div>
    </div>
</template>

<script>
  export default {
    name: "TaBarItem",
    data(){
      return{
        // isActive:false
        // 不能写死,需要想办法知道当前的item是不是活跃的那个
      }
    },
    props:{
      path:String,
      activeColor:{
        type:String,
        default:'red'
      }
    },
    computed:{
      isActive(){
        // /home => item1(/home) =true
        return this.$route.path.indexOf(this.path)!==-1
      },
      activeStyle(){
        return this.isActive ?{color:this.activeColor}:{}
      }
    },   
    methods:{
      itemClick(){
        this.$router.replace(this.path)
      }
    }
  }
</script>

<style scoped>
  .tab-bar-item {
    flex: 1;
    text-align: center;
    height: 49px;
    font-size: 14px;
  }
  .tab-bar-item img{
    width: 24px;
    height: 24px;
    vertical-align: middle;
    margin-bottom: 2px;
  }
</style>

MainTabBar

<template>
  <tab-bar>
    <ta-bar-item   path="/home">
      <img slot="item-icon" src="~assets/img/tabbar/home.svg" alt="">
      <img slot="item-icon-active" src="~assets/img/tabbar/home_active.svg" alt="">
      <div slot="item-text">首页</div>
    </ta-bar-item>
    <ta-bar-item   path="/category">
      <img slot="item-icon" src="~assets/img/tabbar/category.svg" alt="">
      <img slot="item-icon-active" src="~assets/img/tabbar/category_active.svg" alt="">
      <div slot="item-text">分类</div>
    </ta-bar-item>
    <ta-bar-item   path="/cart">
      <img slot="item-icon" src="~assets/img/tabbar/cart.svg" alt="">
      <img slot="item-icon-active" src="~assets/img/tabbar/cart_active.svg" alt="">
      <div slot="item-text">购物车</div>
    </ta-bar-item>
    <ta-bar-item   path="/profile">
      <img slot="item-icon" src="~assets/img/tabbar/profile.svg" alt="">
      <img slot="item-icon-active" src="~assets/img/tabbar/profile_active.svg" alt="">
      <div slot="item-text">我的</div>
    </ta-bar-item>
  </tab-bar>
</template>

<script>
  import TaBarItem from "components/common/tabbar/TaBarItem";
  import TabBar from "components/common/tabbar/TabBar";
  export default {
    name: "MainTabBar",
    components: {
      TabBar,TaBarItem
    }

  }
</script>

<style scoped>

</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wanglu的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值