Vue综合练习

通过前面学习了Vue等知识、我们使用Vue写一个简单的小项目

一位大佬写的很详细: https://blog.csdn.net/wuyxinu/article/details/103684950

基本构建

使用Vue-Cli4.x创建项目

1
vue create sueprmall

记得安装 VuexVue-Router之后安装 axios: npm install -s axios

项目基本目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── public    					用于存放静态资源
│ ├── favicon.ico 图标资源
│ └── index.html 是一个模板文件,作用是生成项目的入口文件
├── src 项目源码目录
│ ├── main.js 入口js文件
│ ├── App.vue 根组件
│ ├── components 公共组件目录
│ │ └── common 在其他项目下也可以使用的
│ │ └── contents 业务相关组件
│ ├── common 工具
│ ├── assets 资源目录,这里的资源会被wabpack构建
│ │ └── img
│ │ └── css
│ ├── network 封装网络请求
│ │ └── request.js
│ ├── routes 前端路由
│ │ └── index.js
│ ├── store Vuex应用级数据(state)
│ │ └── index.js
│ └── views 页面目录
└── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息

配置别名

在根目录下创建vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
configureWebpack: {
resolve:{
extensions:[],
alias:{
'assets':'@/assets',
'components':'@/components',
'network':'@/network',
'views':'@/views',
}
}
}
}

在根目录下创建.editorconfig文件用来规范缩进等…

1
2
3
4
5
6
7
8
9
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

CSS导入

src/assets/css下创建normalize.css 内容为: https://github.com/necolas/normalize.css/blob/master/normalize.css

创建base.css 内容为: https://github.com/maclxf/supermall/blob/master/src/assets/css/base.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@import "./normalize.css";

/*:root -> 获取根元素html*/
:root {
--color-text: #666;
--color-high-text: #ff5777;
--color-tint: #ff8198;
--color-background: #fff;
--font-size: 14px;
--line-height: 1.5;
}

*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
user-select: none; /* 禁止用户鼠标在页面上选中文字/图片等 */
-webkit-tap-highlight-color: transpanett; /* webkit是苹果浏览器引擎,tap点击,highlight背景高亮,color颜色,颜色用数值调节 */
background: var(--color-background);
color: var(--color-text);
/* rem vw/vh */
width: 100vw;
}

a {
color: var(--color-text);
text-decoration: none;
}


.clear-fix::after {
clear: both;
content: '';
display: block;
width: 0;
height: 0;
visibility: hidden;
}

.clear-fix {
zoom: 1;
}

.left {
float: left;
}

.right {
float: right;
}

App.vue中导入

1
2
3
4
<style>
@import './assets/css/base.css';

</style>

基本页面

复制2020\07\Vue\supermall\public\favicon.ico下的所有文件到当前项目public\favicon.ico

复制2020\07\Vue\supermall\src\assets\img下的所有文件到当前项目src\assets\img

src\views下创建

在src\views\cart下创建

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h2>购物车</h2>
</template>

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

<style scoped>

</style>

在src\views\category下创建

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h2>品类</h2>
</template>

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

<style scoped>

</style>

在src\views\home下创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<h2>我是主页</h2>
</template>

<script>

export default {
name: "Home",
components: {},
data() {
return {}
},
computed: {},
created() {

},
methods: {}
}
</script>

<style scoped>

</style>

在src\views\me下创建

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h2>关于我</h2>
</template>

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

<style scoped>

</style>

之后配置路由router\index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import Vue from 'vue'
import VueRouter from 'vue-router'

// 懒加载
const Home = () => import('views/home/Home')
const Category = () => import('views/category/Category')
const Cart = () => import('views/cart/Cart')
const Me = () => import('views/me/Me')

const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}

Vue.use(VueRouter)

const routes = [
{
path: '',
redirect: '/home'
},
{
path: '/home',
// 指定的组件
component: Home
},
{
path: '/category',
component: Category
},
{
path: '/cart',
component: Cart
},
{
path: '/me',
component: Me
}
]

const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})

export default router

复制: https://github.com/maclxf/supermall/tree/master/src/components/common/tabbar 里的内容到 \src\components\common\tabbar

最后在src\App.vue编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div id="app">
<router-view/>
<main-tar-bar/>
</div>
</template>

<script>

import MainTarBar from "./components/contents/maintarbar/MainTarBar";

export default {
name: "App",
data(){
return{

}
},
components:{
MainTarBar
}
}
</script>

<style>
@import 'assets/css/base.css';

</style>

通过npm run serve测试效果

20200719225324.gif

首页开发

导航组件

顶部文字显示区域


\src\components\common\navbar下创建NavBar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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.vue"
}
</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>

编辑src\views\home\Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div id="home">
<NavBar class="home-nav-bar">
<div slot="center">购物街</div>
</NavBar>
</div>
</template>

<script>

import NavBar from "components/common/navbar/NavBar";

export default {
name: "Home",
components: {
NavBar
},
//...
}
</script>

<style scoped>
#home{
padding-top: 44px;
height: 100vh;
position: relative;
}

.home-nav-bar{
background-color: var(--color-tint);
color: #fff;
box-shadow: 0 1px 1px 1px rgba(100,100,100,.1);
position: fixed;
left: 0;
top: 0;
z-index: 9;
}
</style>

数据请求

src\network下创建request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import axios from 'axios';

export function request(config) {
const instance = axios.create({
baseUrl: "http://123.207.32.32:8000",
timeout: 5000
})

instance.interceptors.request.use(config => {
// console.log("拦截的请求");
// console.log(config)
// 拦截了请求要返回
return config;
}, err => {
console.log(err)
})

instance.interceptors.response.use(response => {
// console.log("拦截的响应");
// console.log(response);
return response.data
}, err => {
console.log(err)
})

return instance(config);
}

之后创建home.js

1
2
3
4
5
6
7
import { request } from './request'

export function getHomeMultiData() {
return request({
url: "/home/multidata"
})
}

Home.vue编辑、主要是注意在created()函数中发起请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>
...
</div>
</template>

<script>
//...
import {getHomeMultiData} from "../../network/home";

export default {
name: "Home",
//...
data() {
return {
banners: [],
recommends: []
}
},
created() {
//请求
this.getHomeMultiData();
},
methods:{
getHomeMultiData(){
getHomeMultiData().then(res=>{
this.banners = res.data.banner.list;
this.recommends = res.data.recommend.list;
})
}
}
}
</script>

轮播图组件

使用的是作者封装好的组件: https://github.com/maclxf/supermall/tree/master/src/components/common/swiper 放在src\components\common

为了使Home里的组件不那么复杂、我们需要提取一下、在src\views\home下创建childComps文件夹、里面创建HomeSwiper.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<swiper class="home-swiper">
<swiper-item v-for="item in banners" :key="item.link">
<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,
default() {
return []
}
}
},
components: {
Swiper, SwiperItem
}
}
</script>

<style scoped>

</style>

一个轮播图就导入成功了。

Home.vue添加

1
HomeSwiper

每日推荐组件

同样为了使Home里的组件不那么复杂、我们需要提取一下、在src\views\home\childComps里面创建HomeRecommend.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div class="home-recommend">
<div class="home-recommend-item" v-for="item in recommends" :key="item.title">
<a :href="item.link">
<img :src="item.image" alt="">
<span>{{ item.title }}</span>
</a>
</div>
</div>
</template>

<script>
export default {
name: "HomeRecommend",
props: {
recommends: {
type: Array,
default() {
return [];
}
}
}
}
</script>

<style scoped>
.home-recommend{
display: flex;
text-align: center;
font-size: 12px;
width: 100%;
padding: 5px;
border-bottom: 10px solid #eee;
}

.home-recommend .home-recommend-item{
flex: 1;
}

.home-recommend-item img{
width: 70px;
height: 70px;
border-radius: 100%;
}

.home-recommend-item span{
display: block;
}
</style>

之后统一写到Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div id="home">
...
<HomeSwiper :banners="banners"/>
...
</div>
</template>

<script>
//...
import HomeSwiper from "./childComps/HomeSwiper";
//...
import {getHomeMultiData} from "network/home";

export default {
name: "Home",
components: {
HomeSwiper //...
},
data() {
return {
banners: [],
//...
}
},
created() {
//请求
this.getHomeMultiData();
},
methods:{
getHomeMultiData(){
getHomeMultiData().then(res=>{
this.banners = res.data.banner.list;
this.recommends = res.data.recommend.list;
})
}
}
}
</script>

本周流行组件

同样为了使Home里的组件不那么复杂、我们需要提取一下、在src\views\home\childComps里面创建HomeFeatureView.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="home-feature">
<a href="https://act.mogujie.com/zzlx67">
<img src="~assets/img/home/recommend_bg.jpg" alt="">
</a>
</div>
</template>

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

<style scoped>
.home-feature img{
width: 100%;
}
</style>

之后写到Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div id="home">
...
<HomeRecommend :recommends="recommends"/>
...
</div>
</template>

<script>
//...
import HomeRecommend from "./childComps/HomeRecommend";
//...

import {getHomeMultiData} from "network/home";

export default {
name: "Home",
components: {
HomeRecommend //...
},
data() {
return {
//...
recommends: []
}
},
created() {
//请求
this.getHomeMultiData();
},
methods:{
getHomeMultiData(){
getHomeMultiData().then(res=>{
this.banners = res.data.banner.list;
this.recommends = res.data.recommend.list;
})
}
}
}
</script>

TabControl组件

src\component\tabcontrol\里面创建TabControl.vue

通过索引indexcurnettIndex来判断是否选中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template>
<div class="tab-control">
<div v-for="(item,index) in titles" :key="item" class="tab-control-item" @click="tabClick(index)">
<span :class="{ active: index == curnettIndex }" >{{ item }}</span>
</div>
</div>
</template>

<script>
export default {
name: "TabControl",
data() {
return {
curnettIndex : 0
}
},
props: {
titles: {
type: Array,
data() {
return [];
}
}
},
methods:{
tabClick(index){
this.curnettIndex = index;
//将 index索引传给父组件
this.$emit("tabClick",index);
}
}
}
</script>

<style scoped>
.tab-control{
display: flex;
text-align: center;
line-height: 40px;
height: 40px;
font-size: 15px;
background-color: #fff;
}

.tab-control-item{
flex: 1;
}

.tab-control-item span{
padding: 5px;
}

.active{
border-bottom: 2px solid var(--color-tint);
color: var(--color-high-text);
}

</style>

编辑Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div id="home">
...
<tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" />

</div>
</template>

<script>

// ...
import TabControl from "components/contents/tabcontrol/TabControl";
// ...

export default {
name: "Home",
components: {
TabControl //...
},
//...
}
</script>

<style>
/* ... */
.home-tab-control{
/* sticky 粘性布局 部分浏览器不支持 */
position: sticky;
top: 44px;
z-index: 9;
}
</style>

后面发现position: sticky;没有作用了。。。不知道什么原因

商品数据封装

数据

我们需要这样的一个数据结构来存储商品数据

pop、new、sell代表商品、page代表页数、list代表商品、curnettType当前类型

1
2
3
4
5
6
goods: {
'pop': {page: 0, list: []},
'new': {page: 0, list: []},
'sell': {page: 0, list: []},
},
curnettType: "pop"
network

编辑src\network\home.js

1
2
3
4
5
6
7
8
9
export function getHomeGoods(type,page) {
return request({
url: "/data",
params:{
type,
page
}
})
}
views

最后编辑Home.vue

注意:

  • import {getHomeGoods} from "network/home";导入、不要忘记

  • 往数组里添加数据使用push方法、然而我们需要添加另一个数组的数据我们需要使用扩展运算符...

    如下面this.goods[type].list.push(...res.data.list)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<template>
<div id="home">
...
<tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick"/>
<good-list :goods="showGoods"/>

</div>
</template>
<script>
//...
import {getHomeMultiData ,getHomeGoods} from "network/home";

export default {
name: "Home",
//...
data() {
return {
//...
goods: {
'pop': {page: 0, list: []},
'new': {page: 0, list: []},
'sell': {page: 0, list: []}
},
curnettType: "pop"
}
},
computed: {
showGoods(){
return this.goods[this.curnettType].list
}
},
created() {
this.getHomeMultiData();
//需要请求多个类型的数据
this.getHomeGoods("pop");
this.getHomeGoods("new");
this.getHomeGoods("sell");
},
methods: {
/**
* 事件相关
*/
tabClick(index) {
switch (index) {
case 0:
this.curnettType = "pop"
break
case 1:
this.curnettType = "new"
break
case 2:
this.curnettType = "sell"
break
}
},

/**
* 网络请求相关
*/
getHomeMultiData() {
getHomeMultiData().then(res => {
// console.log(res.data)
this.banners = res.data.banner.list;
this.recommends = res.data.recommend.list;
})
},
getHomeGoods(type) {
let page = this.goods[type].page + 1;
getHomeGoods(type, page).then(res => {
// console.log(res.data.list)
this.goods[type].list.push(...res.data.list)
this.goods[type].page += 1;
})
}
}
}
</script>

Batter-Srcoll

一个更好的解决移动端滚动问题的框架、官网: https://ustbhuangyi.github.io/better-scroll/#/zh

基本使用

安装

1
npm install better-scroll --save

在Vue中使用

  • 我们需要在标签里包裹另一个标签 如:div标签包裹ul标签

  • 我们需要在mounted使用BScroll

    第一个参数: 包裹ul标签的div

    第二个参数: 绑定参数、如:probeType 、值为0时不派发 scroll 事件、值为1时屏幕滑动超过一定时间后派发scroll 事件;值为2时在屏幕滑动的过程中实时的派发 scroll 事件(如滑到底部时的动画然不会有事件)、值为3时屏幕滑动的过程中,滚动动画运行过程中实时派发 scroll 事件(如滑到底部时的动画然会有事件)

  • div需要高度、否则不起作用

  • 通过on方法绑定参数

  • 绑定pullingUp参数时需要添加pullUpLoad: true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li>1</li>
...
<li>8</li>
<li>9</li>
<li>10</li>
...
<li>25</li>
</ul>
</div>
</template>

<script>

import BScroll from 'better-scroll'

export default {
data() {
return {
scroll: null
}
},
mounted() {
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: 2,
pullUpLoad: true
});

this.scroll.on('scroll',(position)=>{
console.log(position)
})

this.scroll.on('pullingUp',()=>{
console.log("上拉刷新")
})
}
}
</script>

<style scoped>
.wrapper {
height: 200px;
background-color: red;
overflow: hidden;
}

</style>

简单封装

功能:

  • backtop在滑动一定距离时显示
  • 上拉加载更多
scroll

src\components\common\scroll下创建Scroll

  • probeType值不为0时、才会监听scroll事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>
</div>
</div>
</template>

<script>

import BScroll from "better-scroll"

export default {
name: "Scroll",
props:{
probeType: {
type: Number,
default: 0
},
pullUpLoad: {
type: Boolean,
default: false
}
},
data() {
return {
scroll: null
}
},
mounted() {
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType,
pullUpLoad: this.pullUpLoad,
//better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件
click: true
})

// 当有 probeType 属性时 才会监听滚动
this.scroll.on("scroll",( position =>{
//自定义事件 子传父
this.$emit('scroll',position)
}))

// 当有 pullUpLoad 属性为 true 才会监听上拉
this.scroll.on('pullingUp',()=>{
this.$emit("pullingUp")
})
},
methods: {
//对BScroll的scrollTo封装
scrollTo(x, y, time = 500) {
this.scroll.scrollTo(x, y, time)
}

// 当上拉加载数据加载完毕后,需要调用此方法告诉 better-scroll 数据已加载。
finishPullUp(){
this.scroll.finishPullUp()
}

//重新计算 better-scroll,当 DOM 结构发生变化的时候务必要调用确保滚动的效果正常。
refresh() {
this.scroll.refresh()
}
}
}
</script>

<style scoped>

</style>
backtop

src\components\contents\backtop下创建BackTop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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;
bottom: 53px;
right: 3px;
}
.back-top img{
width: 50px;
height: 50px;
}

</style>

使用
  • 通过绝对定位scroll组件就可以不用指定高度来显示滚动区域了、或者使用height:calc(100%-93px); 但是又bug
  • ref="refname"可以绑定组件、this.$refs.refname使用
  • 自定义组件本身不能添加自定义事件、需要添加事件修饰符.native
  • this.isShowBackTop = -position.y > 1000 判断 滑动距离大于1000时才会显示BackTop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<template>
<div id="home">
...
<scroll class="wrapper" ref="scroll" @scroll="contentScroll" :probeType="3" :pullUpLoad="true" @pullingUp="loadMore">
<HomeSwiper :banners="banners"/>
<HomeRecommend :recommends="recommends"/>
<HomeFeatureView/>
<tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick"/>
<good-list :goods="showGoods"/>
</scroll>
<BackTop @click.native="topClick" v-show="isShowBackTop"/>
</div>
</template>

<script>

//...
import Scroll from "components/common/scroll/Scroll";
import BackTop from "components/contents/backtop/BackTop";
//...

export default {
name: "Home",
components: {
GoodList,
Scroll,BackTop //...
},
data(){
return{
//...
isShowBackTop:false
}
}
methods: {
/**
* 事件相关
*/
//...
topClick(){
console.log(this.$refs.scroll.scroll)
this.$refs.scroll.scrollTo(0,0)
},
contentScroll(position){
this.isShowBackTop = -position.y > 1000
},
//加载更多
loadMore(){
this.getHomeGoods(this.curnettType)
},
//...
getHomeGoods(type) {
let page = this.goods[type].page + 1;
getHomeGoods(type, page).then(res => {
// console.log(res.data.list)
this.goods[type].list.push(...res.data.list)
this.goods[type].page += 1;

this.$refs.scroll.finishPullUp();
})
}
}
}
</script>

<style scoped>
#home {
height: 100vh;
position: relative;
}

.home-nav-bar {
background-color: var(--color-tint);
color: #fff;

position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 9;

}

.home-tab-control {
/*两个要混合使用*/
position: sticky;
top: 44px; /*顶部navbar的高度*/
z-index: 8;
}

.wrapper{
position: absolute;
top: 44px;
bottom: 49px;
right: 0;
left: 0;
}

/*.wrapper {
height: calc(100% - 93px)
overflow: hidden;
margin-top: 44px;
} */
</style>

解决首页中Better-Scroll可滚动区域的问题

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

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

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

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

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

    • 所以滚动出现了问题

  • 如何解决这个问题了?

    • 监听每一张图片是否加载完成,只要有一张图片加载完成了,执行一 次refresh()
    • 如何监听图片加载完成了?
      • 元素的js监听图片img.onload = function(){}
      • Vue中监听: @load='方法'
    • 调用scroll的refresh
  • 如何将GoodsListltem.vue中 的事件传入到Home.vue

    • 因为涉及到非父子组件的通信,所以这里我们选择了事件总线
      • $bus -> 总线
      • Vue.prototype.$bus = new Vue()
      • this.$bus.emit('事件名称',参数)
      • this.$bus.on('事件名称',回调函数(参数))

对于非父子组件通信来说我们需要使用集中式的事件中间件:Bus

在组件中,可以使用$emit, $on, $off 分别来分发、监听、取消监听事件

需要在main.js中注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

// 全局实例化$bus事件总线
Vue.prototype.$bus = new Vue()

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

编辑GoodListItem.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div class="goods-item">
<!-- @load 事件是图片加载完成之后会调用的 -->
<img :src="goodItem.showLarge.img" alt="" @load="imageLoad">
<div class="goods-info">
<p>{{ goodItem.title }}</p>
<span class="price">{{goodItem.price}}</span>
<span class="collect">{{ goodItem.cfav }}</span>
</div>
</div>
</template>

<script>
export default {
name: "GoodListItem",
props: {
goodItem: {
type: Object,
default() {
return {}
}
}
},
methods:{
imageLoad(){
// $emit 分发事件
this.$bus.$emit('imageLoad');
}
}
}
</script>

Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div id="home">
...
</div>
</template>

<script>

//...

export default {
name: "Home",
components: {
//...
},
//...
mounted() {
//监听
this.$bus.$on('imageLoad',()=>{
this.$refs.scroll.refresh()
})
},
//...
}
</script>

防抖动

浅谈js防抖和节流

1
2
3
4
5
6
7
8
9
10
11
// 例子
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(fn,delay) // 简化写法
}
}
window.onscroll = debounce(showTop,1000)

刷新频繁找不到refresh的解决办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div id="home">
...
</div>
</template>

<script>
//...
export default {
name: "Home",
//...
mounted() {
const refresh = this.debounce(this.$refs.scroll.refresh,50)
//创建时监听
this.$bus.$on('imageLoad', () => {
refresh()
})
},
//...
methods: {
/**
* 事件相关
*/
//...
//防抖动
debounce(func, delay) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
},
//...
}
}
</script>

我们可以把函数封装到src\common\utils.js

tabControl的吸顶效果

获取到tabControl的offsetTop
  • 必须知道滚动到多少时,开始有吸顶效果,这个时候就需要获取tabControl的offsetTop

  • 但是,如果直接在mounted中获取tabControl的offsetTop,那么值是不正确.

  • 如何获取正确的值了?

    • 监听HomeSwiper中img的加载完成.
    • 加载完成后,发出事件,在Home.vue中,获取正确的值.
  • 补充:

    • 为了不让HomeSwiper多次发出事件,
    • 可以使用isLoad的变量进行状态的记录.
    • 注意:这里不进行多次调用和debounce的区别
  • 监听滚动,动态的改变tabControl的样式

    • 问题动态的改变tabControl的样式时,会出现两个问题:
    • 问题一:下面的商品内容,会突然上移
    • 问题二: tabControl虽然设置了fixed,但是也随着Better-Scroll-起滚出去了.
  • 其他方案来解决停留问题.

    • 在最上面,多复制了一份PlaceHolderTabControl组件对象,利用它来实现停留效果.
    • 当用户滚动到一定位置时, PlaceHolderTabControl显示出来.
    • 当用户滚动没有达到- -定位置时, PlaceHolderTabControl隐藏起来.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<div id="home">
<NavBar class="home-nav-bar">
<div slot="center">购物街</div>
</NavBar>
<tab-control class="home-tab-control" :titles="['流行', '新款', '精选']" @tabClick="tabClick" ref="tabControl1"
v-show="isFixed"/>
<scroll ref="scroll" @scroll="contentScroll" :probeType="3" :pullUpLoad="true" @pullingUp="loadMore">
<HomeSwiper :banners="banners" @swiperImageLoad="swiperLoad" ref="swiper"/>
<HomeRecommend :recommends="recommends"/>
<HomeFeatureView/>
<tab-control :titles="['流行', '新款', '精选']" @tabClick="tabClick" ref="tabControl2"/>
<good-list :goods="showGoods"/>
</scroll>
<BackTop @click.native="topClick" v-show="isShowBackTop"/>
</div>
</template>

<script>

import NavBar from "components/common/navbar/NavBar";
import TabControl from "components/contents/tabcontrol/TabControl";
import Scroll from "components/common/scroll/Scroll";

import BackTop from "components/contents/backtop/BackTop";
import GoodList from "../../components/contents/good/GoodList";
import HomeSwiper from "./childComps/HomeSwiper";
import HomeRecommend from "./childComps/HomeRecommend";
import HomeFeatureView from "./childComps/HomeFeatureView";

import {getHomeMultiData, getHomeGoods} from "network/home";
import {debounce} from "common/utils";

export default {
name: "Home",
components: {
GoodList,
NavBar, TabControl, Scroll, HomeSwiper, HomeRecommend, HomeFeatureView, BackTop
},
data() {
return {
//...
// tabControl2 距离顶部的高度
tabOffsetTop: 0,
//判断是否显示 tabControl1
isFixed: false
}
},
//...
methods: {
/**
* 事件相关
*/
tabClick(index) {
switch (index) {
case 0:
this.curnettType = "pop"
break
case 1:
this.curnettType = "new"
break
case 2:
this.curnettType = "sell"
break
}
this.$refs.tabControl1.curnettIndex = index;
this.$refs.tabControl2.curnettIndex = index;
},
//...
contentScroll(position) {
this.isShowBackTop = (-position.y) > 1000
this.isFixed = (-position.y) > this.tabOffsetTop
},
//...
swiperLoad() {
this.tabOffsetTop = this.$refs.tabControl2.$el.offsetTop
},
//...
}
}
</script>

<style scoped>
#home {
height: 100vh;
position: relative;
}

.home-nav-bar {
background-color: var(--color-tint);
color: #fff;
/*position: fixed;*/
/*left: 0;*/
/*right: 0;*/
/*top: 0;*/
/*z-index: 9;*/
}

.home-tab-control {
position: relative;
background-color: #fff;
/*调整一下层级 并且有 position定位的*/
z-index: 9;
margin-top: -1px;
}

.wrapper{
overflow: hidden;
position: absolute;
top: 44px;
bottom: 49px;
right: 0;
left: 0;
}

</style>

让Home保持原来的状态

  • 让Home不要随意销毁掉
    • keep-alive(可能使用还会有问题、但是我测试时候没问题)
  • 让Home中的内容保持原来的位置
    • 离开时,保存一一个位置信息TopY,
    • 进来时,将位置设置为原来保存的位置TopY信息即可.
      • 注意:最好回来时,进行一次refresh()

编辑App.app

1
2
3
4
5
6
7
8
<template>
<div id="app">
<keep-alive exclude="Detail">
<router-view/>
</keep-alive>
<main-tar-bar/>
</div>
</template>

编辑Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
//...
export default {
//...
data() {
return {
//...
TopY: 0
}
},
//...
activated() {
this.$refs.scroll.scrollTo(0,this.TopY,0);
this.$refs.scroll.refresh()
},
deactivated() {
this.TopY = this.$refs.scroll.getTopY();
}
}
</script>

详情页开发

src/views/detail下创建Detail.vue

编辑src/router/index.js

1
2
3
4
5
6
7
8
9
10
11
//...
const Detail = ()=>import('views/detail/Detail')
//...
const routes = [
//...
{
path: '/detail/:iid',
component: Detail
}
]
//...

再编辑src/components/contents/good/GoodListItem.vuegoods-items添加一个点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="goods-item" @click="clickDetail">
...
</div>
</template>

<script>
export default {
//...
methods:{
//...
clickDetail(){
this.$router.push("/detail/"+ this.goodItem.iid)
}
}
}
</script>

封装顶部导航栏

src/views/detail/childComps下创建DetailNav.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<div>
<NavBar>
<div slot="left">
<div class="back" @click="back">
<img src="~assets/img/common/back.svg" alt="">
</div>
</div>
<div slot="center" class="detail">
<div v-for="(item,index) in titles" class="detail-item" :class="{active: curnettIndex == index}" @click="itemClick(index)">
{{ item }}
</div>
</div>
</NavBar>
</div>
</template>

<script>

import NavBar from "components/common/navbar/NavBar";

export default {
name: "DetailNavBar",
components:{
NavBar
},
data(){
return{
titles: ['商品','参数','评论','推荐'],
curnettIndex: 0
}
},
methods:{
itemClick(index){
this.curnettIndex = index
},
back(){
this.$router.go(-1);
}
}
}
</script>

<style scoped>
.detail{
display: flex;
font-size: 15px;
}

.detail-item{
flex:1;
}

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

.back img{
vertical-align: middle;
}
</style>

轮播图展示

src/network下创建detail.js

1
2
3
4
5
6
7
8
9
10
import { request } from "./request";

export function getDetail(iid) {
return request({
url: '/detail',
params:{
iid
}
})
}

src/views/detail/childComps下创建DetailSwiper.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div class="detail-swiper">
<swiper>
<swiper-item v-for="(item,index) in banners" :key="index" class="detail-swiper">
<img :src="item" alt="">
</swiper-item>
</swiper>
</div>
</template>

<script>
import {Swiper, SwiperItem} from '@/components/common/swiper/index'

export default {
name: "DetailSwiper",
components: {
Swiper,SwiperItem
},
props:{
banners: {
type:Array,
default(){
return []
}
}
}
}
</script>

<style scoped>
.detail-swiper{
height:300px;
overflow: hidden;
}
</style>

基本信息

编辑src/network/detail.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6的类,详情数据
export class Goods {
constructor(itemInfo, columns, services) {
this.title = itemInfo.title;
this.desc = itemInfo.desc;
this.newPrice = itemInfo.price;
this.lowNowPrice = itemInfo.lowNowPrice;
this.oldPrice = itemInfo.oldPrice;
this.discount = itemInfo.discountDesc;
this.discountBgColor = itemInfo.discountBgColor;
this.columns = columns;
this.services = services;
this.realPrice = itemInfo.lowNowPrice;
}
}

src/views/detail/childComps下创建DetailBaseInfo.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<!-- 判断对象是否为空 -->
<div class="base-info" v-if="Object.keys(goods).length !== 0">
<div class="info-title">{{ goods.title }}</div>
<div class="info-price">
<span class="n-price">{{ goods.newPrice }}</span>
<span v-if="goods.oldPrice" class="o-price">{{ goods.oldPrice }}</span>
<span
:style="{ backgroundColor: goods.discountBgColor }"
class="discount"
v-if="goods.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
:key="index"
class="info-service-item"
v-for="index in goods.services.length - 1"
v-if="goods.services[index - 1].icon"
>
<img :src="goods.services[index - 1].icon" alt="" />
<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 {
width: 100%;
margin-top: 15px;
padding: 0 10px;
color: #999999;
border-bottom: 5px solid #f2f5f8;
}

.info-title {
text-align: justify;
color: #222222;
}

.info-price {
margin-top: 10px;
}

.info-price .n-price {
font-size: 24px;
color: #ff5777;
}

.info-price .o-price {
font-size: 13px;
margin-left: 5px;
text-decoration: line-through;
}

.info-price .discount {
font-size: 12px;
position: relative;
top: -4px;
margin-left: 5px;
padding: 3px 6px;
color: #ffffff;
border-radius: 8px;
background-color: #ff5777;
}

.info-other {
font-size: 13px;
line-height: 30px;
display: flex;
justify-content: space-between;
margin-top: 15px;
border-bottom: 1px solid rgba(100, 100, 100, 0.1);
}

.info-service {
line-height: 60px;
display: flex;
justify-content: space-between;
}

.info-service-item img {
position: relative;
top: 2px;
width: 14px;
height: 14px;
}

.info-service-item span {
font-size: 13px;
margin-left: 5px;
color: #333333;
}
</style>

店铺信息

编辑src/network/detail.js

1
2
3
4
5
6
7
8
9
10
11
// 店铺数据
export class Shop {
constructor(shopInfo) {
this.logo = shopInfo.shopLogo;
this.name = shopInfo.name;
this.fans = shopInfo.cFans;
this.sells = shopInfo.cSells;
this.score = shopInfo.score;
this.goodsCount = shopInfo.cGoods;
}
}

src/views/detail/childComps下创建DetailShopInfo.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<template>
<div class="shop-info" v-if="Object.keys(shop).length !== 0">
<div class="shop-top">
<img :src="shop.logo" alt="" v-if="shop.logo" />
<span class="title">{{ shop.name }}</span>
</div>
<div class="shop-middle">
<div class="shop-middle-item shop-middle-left">
<div class="info-sells">
<div class="sells-count">
{{ shop.sells | sellCountFilter }}
</div>
<div class="sells-text">总销量</div>
</div>
<div class="info-goods">
<div class="goods-count">
{{ shop.goodsCount }}
</div>
<div class="goods-text">全部宝贝</div>
</div>
</div>
<div class="shop-middle-item shop-middle-right">
<table>
<tr :key="index" v-for="(item, index) in shop.score">
<td>{{ item.name }}</td>
<td :class="{ 'score-better': item.isBetter }" class="score">
{{ item.score }}
</td>
<td :class="{ 'better-more': item.isBetter }" class="better">
<span>{{ item.isBetter ? "高" : "低" }}</span>
</td>
</tr>
</table>
</div>
</div>
<div class="shop-bottom">
<div class="enter-shop">进店逛逛</div>
</div>
</div>
</template>

<script>
export default {
name: "DetailShopInfo",
props: {
shop: {
type: Object,
default() {
return {};
}
}
},
filters: {
sellCountFilter(value) {
if (value < 10000) return value;
return (value / 10000).toFixed(1) + "万";
}
}
};
</script>

<style scoped>
.shop-info {
padding: 25px 8px;
border-bottom: 5px solid #f2f5f8;
}

.shop-top {
line-height: 45px;
display: flex;
align-items: center;
}

.shop-top img {
width: 45px;
height: 45px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
}

.shop-top .title {
margin-left: 10px;
vertical-align: center;
}

.shop-middle {
display: flex;
align-items: center;
margin-top: 15px;
}

.shop-middle-item {
flex: 1;
}

.shop-middle-left {
display: flex;
justify-content: space-evenly;
text-align: center;
color: #333333;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}

.sells-count,
.goods-count {
font-size: 18px;
}

.sells-text,
.goods-text {
font-size: 12px;
margin-top: 10px;
}

.shop-middle-right {
font-size: 13px;
color: #333333;
}

.shop-middle-right table {
width: 120px;
margin-left: 30px;
}

.shop-middle-right table td {
padding: 5px 0;
}

.shop-middle-right .score {
color: #5ea732;
}

.shop-middle-right .score-better {
color: #f13e3a;
}

.shop-middle-right .better span {
padding: 3px;
text-align: center;
color: #ffffff;
background-color: #5ea732;
}

.shop-middle-right .better-more span {
background-color: #f13e3a;
}

.shop-bottom {
margin-top: 10px;
text-align: center;
}

.enter-shop {
font-size: 14px;
line-height: 30px;
display: inline-block;
width: 150px;
height: 30px;
text-align: center;
border-radius: 10px;
background-color: #f2f5f8;
}
</style>

商品信息

编辑src/network/detail.js

1
2
3
4
5
6
7
8
9
10
11
// 店铺数据
export class Shop {
constructor(shopInfo) {
this.logo = shopInfo.shopLogo;
this.name = shopInfo.name;
this.fans = shopInfo.cFans;
this.sells = shopInfo.cSells;
this.score = shopInfo.score;
this.goodsCount = shopInfo.cGoods;
}
}

src/views/detail/childComps下创建DetailGoodsInfo.vue

  • data中counter计算图片加载个数
  • data中detailLength为获取的数据detailInfodetailImage数组的长度
  • 通过侦听属性watch监听detailImage数组的长度、提高性能
  • 当counter等于detailImage时才向父组件发送imgLoad事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<template>
<div v-if="Object.keys(detailInfo).length !== 0">
<div class="info-text-wrap">
<div class="text-top-style"></div>
<div class="desc info-text-desc">{{detailInfo.desc}}</div>
<div class="text-bot-style"></div>
</div>
<div class="img-list-wrap" v-for="item in detailInfo.detailImage" :key="item.id">
<div class="desc">{{item.key}}</div>
<div v-for="(item, index) in item.list" :key="index">
<img :src="item" alt="" class="img" @load="imgLoad">
</div>
</div>
</div>
</template>

<script>
export default {
name: 'DetailGoodsInfo',
props: {
detailInfo: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
counter: 0,
detailLength: 0
}
},
methods: {
imgLoad() {
if (++this.counter == this.detailLength)
this.$emit('imgLoad')
}
},
watch: {
detailInfo() {
this.detailInfo.detailImage.forEach((item,index)=>{
this.detailLength += this.detailInfo.detailImage[index].list.length
})
}
}
}
</script>

<style scoped>

.info-text-wrap {
position: relative;
}

.info-text-wrap .text-top-style {
width: 60px;
height: 1px;
background-color: #333;
margin-left: 4px;
}

.info-text-wrap .text-top-style::before {
position: absolute;
left: 4px;
top: -2.5px;
display: block;
content: '';
width: 5px;
height: 5px;
background-color: #333333;
}

.info-text-wrap .text-top-style .text-bot-style {
width: 60px;
height: 1px;
background-color: #333;
position: absolute;
right: 4px;
bottom: 0;
}

.info-text-wrap .text-top-style .text-bot-style::before {
position: absolute;
left: 4px;
top: -2.5px;
display: block;
content: '';
width: 5px;
height: 5px;
background-color: #333333;
}

.info-text-wrap .text-bot-style {
width: 60px;
height: 1px;
background-color: #333;
position: absolute;
right: 4px;
bottom: 0;
}

.info-text-wrap .text-bot-style::after {
position: absolute;
right: 0;
top: -2.5px;
display: block;
content: '';
width: 5px;
height: 5px;
background-color: #333;
}

.info-text-wrap .info-text-desc {
padding: 10px 4px;
}

.desc {
font-size: 14px;
padding-bottom: 6px;
line-height: 20px;
margin: 4px 0;
text-indent: 10px;
}

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

参数信息

编辑src/network/detail.js

1
2
3
4
5
6
7
8
9
// 尺寸数据
export class GoodsParams {
constructor(info, rule) {
// 注: images可能没有值(某些商品有值, 某些没有值)
this.image = info.images ? info.images[0] : "";
this.infos = info.set;
this.sizes = rule.tables;
}
}

src/views/detail/childComps下创建DetailParamInfo.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div v-if="Object.keys(paramInfo).length !== 0" class="params-wrap">
<div v-for="item in paramInfo.rule" :key="item.id">
<div v-for="list in item" class="flex" :key="list.id">
<div v-for="listitem in list" class="rule-list-item" :key="listitem.id">
{{listitem}}
</div>
</div>
</div>
<div v-for="(info, index) in paramInfo.info" :key="index" class="flex info-list-wrap">
<div class="info-list-tit">{{info.key}}</div>
<div>{{info.value}}</div>
</div>
</div>
</template>

<script>
export default {
name: 'DetailParamInfo',
props: {
paramInfo: {
type: Object,
default() {
return {}
}
}
}
}
</script>

<style scoped>
.params-wrap {
border-top: 4px solid #ececec;
border-bottom: 4px solid #ececec;
}

.rule-list-item {
font-size: 12px;
width: 20%;
border-bottom: 1px solid #ececec;
padding: 10px 4px;
}

.info-list-wrap {
font-size: 14px;;
border-bottom: 1px solid #ececec;
padding: 10px 4px;
line-height: 20px;

}

.info-list-wrap .info-list-tit {
width: 18%;
}
</style>

评论信息

编辑src/common/utils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 时间格式化
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;
}

function padLeftZero(str) {
return ("00" + str).substr(str.length);
}

src/views/detail/childComps下创建DetailCommentInfo.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<div>
<div class="comment-info" v-if="Object.keys(commentInfo).length !== 0">
<div class="info-header">
<div class="header-title">用户评价</div>
<div class="header-more">
更多
<i class="arrow-right" />
</div>
</div>
<div class="info-user">
<img :src="commentInfo.user.avatar" alt="" />
<span>{{ commentInfo.user.uname }}</span>
</div>
<div class="info-detail">
<p>{{ commentInfo.content }}</p>
<div class="info-other">
<span class="date">{{ commentInfo.created | showDate }}</span>
<span>{{ commentInfo.style }}</span>
</div>
<div class="info-imgs">
<img :key="index" :src="item" alt="" v-for="(item, index) in commentInfo.images" />
</div>
</div>
</div>
</div>
</template>

<script>
import { formatDate } from "common/utils";

export default {
name: "DetailCommentInfo",
props: {
commentInfo: {
type: Object
}
},
filters: {
showDate: function (value) {
let date = new Date(value * 1000);
return formatDate(date, "yyyy-MM-dd hh:mm");
}
}
};
</script>

<style scoped>
.comment-info {
padding: 5px 12px;
color: #333333;
border-bottom: 5px solid #f2f5f8;
}

.info-header {
line-height: 50px;
height: 50px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

.header-title {
font-size: 15px;
float: left;
}

.header-more {
font-size: 13px;
float: right;
margin-right: 10px;
}

.info-user {
padding: 10px 0 5px;
}

.info-user img {
width: 42px;
height: 42px;
border-radius: 50%;
}

.info-user span {
font-size: 15px;
position: relative;
top: -15px;
margin-left: 10px;
}

.info-detail {
padding: 0 5px 15px;
}

.info-detail p {
font-size: 14px;
line-height: 1.5;
color: #777777;
}

.info-detail .info-other {
font-size: 12px;
margin-top: 10px;
color: #999999;
}

.info-other .date {
margin-right: 8px;
}

.info-imgs {
margin-top: 10px;
}

.info-imgs img {
width: 70px;
height: 70px;
margin-right: 5px;
}
</style>

推荐信息

编辑src/network/detail.js

1
2
3
4
5
export function getRecommend() {
return request({
url: "/recommend"
})
}

由于推荐信息中图片显示的GoodsList与首页显示的不同所以需要修改GoodsListItem.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<template>
<div class="goods-item" @click="clickDetail">
<img :src="showImage" alt="" @load="imageLoad">
<div class="goods-info">
<p>{{ goodItem.title }}</p>
<span class="price">{{goodItem.price}}</span>
<span class="collect">{{ goodItem.cfav }}</span>
</div>
</div>
</template>

<script>
export default {
name: "GoodListItem",
props: {
goodItem: {
type: Object,
default() {
return {}
}
}
},
computed:{
showImage(){
return this.goodItem.img || this.goodItem.image
}
},
methods:{
imageLoad(){
this.$bus.$emit('imageLoad');
},
clickDetail(){
this.$router.push("/detail/"+ this.goodItem.iid)
}
}
}
</script>

<style scoped>
.goods-item {
padding-bottom: 40px;
position: relative;

width: 48%;
}

.goods-item img {
width: 100%;
border-radius: 5px;
}

.goods-info {
font-size: 12px;
position: absolute;
bottom: 5px;
left: 0;
right: 0;
overflow: hidden;
text-align: center;
}

.goods-info p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 3px;
}

.goods-info .price {
color: var(--color-high-text);
margin-right: 20px;
}

.goods-info .collect {
position: relative;
}

.goods-info .collect::before {
content: '';
position: absolute;
left: -15px;
top: -1px;
width: 14px;
height: 14px;
background: url("~assets/img/common/collect.svg") 0 0/14px 14px;
}

</style>

详情页

Detail.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<template>
<div class="detail">
<detail-nav-bar class="detail-nav"/>
<Scroll class="content">
<DetailSwiper :banners="banners"/>
<DetailBaseInfo :goods="goods"/>
<DetailShopInfo :shop="shop"/>
<DetailGoodsInfo :detailInfo="detailInfo" @imageLoad="imageLoad"/>
<DetailParamInfo :paramInfo="paramInfo"/>
<DetailCommentInfo :commentInfo="commentInfo"/>
<GoodList :goods="recommendList"/>
</Scroll>
</div>
</template>

<script>
import DetailNavBar from "./childComps/DetailNav";
import DetailSwiper from "./childComps/DetailSwiper";
import DetailBaseInfo from "./childComps/DetailBaseInfo";
import DetailShopInfo from "./childComps/DetailShopInfo";
import DetailGoodsInfo from "./childComps/DetailGoodsInfo";
import DetailParamInfo from "./childComps/DetailParamInfo";
import DetailCommentInfo from "./childComps/DetailCommentInfo";
import GoodList from "components/contents/good/GoodList";


import {getDetail, getRecommend, Goods, Shop, GoodsParams} from "network/detail";
import Scroll from "components/common/scroll/Scroll";

export default {
name: "Detail",
components: {
DetailCommentInfo,
DetailNavBar,
DetailSwiper,
DetailBaseInfo,
DetailShopInfo,
DetailGoodsInfo,
DetailParamInfo,
GoodList,
Scroll
},
data() {
return {
iid: null,
banners: [],
goods: {},
shop: {},
detailInfo: {},
paramInfo: {},
commentInfo: {},
recommendList: []
}
},
created() {
this.iid = this.$route.params.iid

// 发送网络请求
this.getProductDetail();
this.getRecommend();
},
methods: {
imageLoad() {
this.$refs.scroll.refresh()
},
getProductDetail() {
getDetail(this.iid).then((res) => {
const data = res.result;
console.log(res)
//获取顶部图片信息
this.banners = data.itemInfo.topImages

// 获取商品数据,调用封装的ES6的class
this.goods = new Goods(data.itemInfo, data.columns, data.shopInfo.services);

// 获取店铺数据
this.shop = new Shop(data.shopInfo);

// 获取商品数据
this.detailInfo = data.detailInfo

//获取商品参数
this.paramInfo = new GoodsParams(data.itemParams.info, data.itemParams.rule || {});

// 获取评论数据
if (data.rate.cRate !== 0) {
this.commentInfo = data.rate.list[0] || {};
}
})
},
getRecommend() {
getRecommend().then((res) => {
console.log(res.data)
this.recommendList = res.data.list;
})
}
}
}
</script>

<style scoped>

.detail {
position: relative;
z-index: 9;
background-color: #fff;
height: 100vh;
}

.content {
position: relative;
height: calc(100% - 44px);
overflow: hidden;
/*position: absolute;*/
/*top: 44px;*/
/*left: 0;*/
/*right: 0;*/
/*bottom: 49px;*/
}
</style>

mixin的使用

解决全局监听产生的bug https://cn.vuejs.org/v2/api/#mixins mixins也就是把其他地方的东西整合在一个地方

创建src/common/mixins.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {debounce} from "./utils";

export const imgListenerMixin = {
data() {
return {
imageListener: null,
newRefresh:null
}
},
mounted() {
this.newRefresh = debounce(this.$refs.scroll.refresh, 50)

this.imageListener = () =>{
this.newRefresh()
}

//创建时监听
this.$bus.$on('imageLoad', this.imageListener)
}
}

编辑src/views/home/Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
// ...
// 导入
import { imgListenerMixin } from "common/mixins";

export default {
name: "Home",
//...
//使用
mixins: [imgListenerMixin],
computed: {
showGoods() {
return this.goods[this.curnettType].list
}
},
mounted() {

},
//...
destroyed() {
//离开Home时取消事件监听
this.$bus.$off('imageLoad',this.imageListener);
}
}
</script>

编辑src/views/detail/Detail.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// ...
import {imgListenerMixin} from "common/mixins";
// ...

export default {
name: "Detail",
//...
mixins: [imgListenerMixin],
//...
destroyed() {
this.$bus.$off("imageLoad",this.imageListener)
}
}
</script>

再次解决详情页滑动的小bug

Detail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
//...

export default {
//...
methods: {
// 判断图片加载完成,刷新可滚动区域
imageLoad() {
this.newRefresh()
//this.$refs.scroll.refresh()
},
}
//...
}
</script>

标题内容联动

点击标题,滚动到对应的主题
在detail中监听标题的点击,获取index
滚动到对应的主题:

  • 获取所有主题的offsetTop

  • 问题:在哪里才能获取到正确的offsetTop

    1. created肯定不行, 压根不能获取元素
    2. mounted也不行,数据还没有获取到
    3. 获取到数据的回调中也不行,DOM还没有渲染完
    4. $nextTick也不行,因为图片的高度没有被计算在类
    5. 在图片加载完成后,获取的高度才是正确

编辑src/views/detail/childComps/DetailNav.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>

import NavBar from "components/common/navbar/NavBar";

export default {
//...
methods:{
itemClick(index){
this.curnettIndex = index
this.$emit("titleClick",index)
},
back(){
this.$router.go(-1);
}
}
}
</script>

编辑src/views/detail/Detail.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<template>
<div class="detail">
<detail-nav-bar class="detail-nav" @titleClick="titleClick" ref="nav"/>
<Scroll class="content" ref="scroll" :probeType="3" @scroll="contentScroll">
<DetailSwiper :banners="banners"/>
<DetailBaseInfo :goods="goods"/>
<DetailShopInfo :shop="shop"/>
<DetailGoodsInfo :detailInfo="detailInfo" @imageLoad="imageLoad"/>
<DetailParamInfo :paramInfo="paramInfo" ref="paramInfo"/>
<DetailCommentInfo :commentInfo="commentInfo" ref="commentInfo"/>
<GoodList :goods="recommendList" ref="recommendList"/>
</Scroll>
</div>
</template>

<script>
//...

export default {
//...
data() {
return {
//...
navBarTops: [],
getTops: null,
curnettIndex: 0
}
},
mixins: [imgListenerMixin],
created() {
//...
this.getTops = debounce(() => {
this.navBarTops = []
this.navBarTops.push(0)
this.navBarTops.push(this.$refs.paramInfo.$el.offsetTop)
this.navBarTops.push(this.$refs.commentInfo.$el.offsetTop)
this.navBarTops.push(this.$refs.recommendList.$el.offsetTop)
this.navBarTops.push(Number.MAX_VALUE)
console.log(this.navBarTops)
}, 100)
},
mounted() {

},
methods: {
// 判断图片加载完成,刷新可滚动区域
imageLoad() {
this.newRefresh()
//this.$refs.scroll.refresh()
this.getTops()
},
//...
titleClick(index) {
this.$refs.scroll.scrollTo(0, -this.navBarTops[index], 200)
},
contentScroll(position) {
let positionY = -position.y
let length = this.navBarTops.length
for (let i = 0; i < length - 1; i++) {
if (this.curnettIndex !== i && (positionY >= this.navBarTops[i] && positionY < this.navBarTops[i + 1])) {
this.curnettIndex = i
this.$refs.nav.curnettIndex = this.curnettIndex
}
}

}
},
//...
}
</script>

底部栏

src/views/detail/childComps下创建DetailBottomBar.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<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() {
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;
}
.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>

BackTop的封装

由于我们需要在Home.vueDetail.vue都使用BackTop、此时我们就需要使用mixins

编辑src/common/mixins.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//...
import BackTop from "components/contents/backtop/BackTop";
//...
export const backTopMixin = {
data(){
return{
isShowBackTop: false,
}
},
components: {
BackTop
},
methods:{
topClick() {
this.$refs.scroll.scrollTo(0, 0, 1000)
},
}
}

然后在Home.vueDetail.vue导入

并且在监听Scroll滚动事件中添加来判断是否显示BackTop

1
this.isShowBackTop = (-position.y) > 1000

购物车相关

Vuex

确保安装了Vuex

src/store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from "./mutations";
import actions from "./actions";

Vue.use(Vuex)

const state = {
cartList: []
}

export default new Vuex.Store({
state,
mutations,
actions
})
1
2
export const ADD_COUNTER = 'add_counter'
export const ADD_TO_CART = 'add_to_cart'
1
2
3
4
5
6
7
8
9
10
11
12
13
import {
ADD_TO_CART,
ADD_COUNTER
} from "./mutations-types";

export default {
[ADD_COUNTER](state, payload) {
payload.count+=1
},
[ADD_TO_CART](state, payload) {
state.cartList.push(payload)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {
ADD_TO_CART,
ADD_COUNTER
} from "./mutations-types";

export default {
//注意: 更新操作需要在mutations中进行
addCart(context, payload) {
let oldProduct = context.state.cartList.find(item => item.iid == payload.iid);

if (oldProduct) {
context.commit(ADD_COUNTER, oldProduct)
} else {
payload.count = 1
context.commit(ADD_TO_CART, payload)
}
}
}

编辑Detail.vue

1
2
3
4
5
6
7
8
9
10
11
12
<DetailBottomBar @addToCart="addToCart"/>

addToCart(){
const product = {}
product.image = this.banners[0];
product.title = this.goods.title;
product.desc = this.goods.desc;
product.price = this.goods.realPrice;
product.iid = this.iid;

this.$store.dispatch("addCart",product)
}

导航栏

创建src/store/getters.js

1
2
3
4
5
6
7
8
export default {
cartLength(state){
return state.cartList.length
},
cartList(state) {
return state.cartList
}
}

编辑src/store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from "./mutations";
import actions from "./actions";
import getters from "./getters";

Vue.use(Vuex)

const state = {
cartList: []
}

export default new Vuex.Store({
state,
mutations,
actions,
getters
})

编辑src/views/Cart.vue 这里使用了mapGetters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div id="cart">
<NavBar class="cart-nav-bar">
<div slot="center">购物街({{ length }})</div>
</NavBar>
</div>
</template>

<script>

import NavBar from "components/common/navbar/NavBar";

import {mapGetters} from 'vuex'

export default {
name: "Cart",
components: {
NavBar,
},
computed:{
...mapGetters({
length: 'cartLength'
})
}
}
</script>

<style scoped>

#cart {
height: 100vh;
position: relative;
}

.cart-nav-bar {
background-color: var(--color-tint);
color: #fff;
}

</style>

封装CheckButton

src/components/content/checkbutton下创建CheckButton.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div>
<div class="icon-selector" :class="{'active': value}" >
<img src="~assets/img/cart/tick.svg" alt="">
</div>
</div>
</template>

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

<style scoped>
.icon-selector {
position: relative;
margin: 0;
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #ccc;
cursor: pointer;
}
.active {
background-color: #ff8198;
border-color: #ff8198;
}
</style>

封装商品信息

src/views/cart/childComps下创建

CartListItem.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<template>
<div id="shop-item">
<div class="item-selector">
<CheckButton @checkBtnClick="checkedChange" v-model="itemInfo.checked"></CheckButton>
</div>
<div class="item-img">
<img :src="itemInfo.image" 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.price}}</div>
<div class="item-count right">x{{itemInfo.count}}</div>
</div>
</div>
</div>
</template>

<script>
import CheckButton from "components/contents/checkbutton/CheckButton";
export default {
name: "ShopCartItem",
props: {
itemInfo: Object
},
components: {
CheckButton
},
methods: {
checkedChange: function () {
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>

CartList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<Scroll ref="content">
<div>
<cart-list-item v-for="(item,index) in cartList" :key="index" :item-info="item" />
</div>
</Scroll>
</template>

<script>

import Scroll from "components/common/scroll/Scroll";
import CartListItem from "./CartListItem";

export default {
name: "CartList",
components: {
Scroll,
CartListItem
},
props: {
cartList:{
type:Array,
default(){
return []
}
}
},
activated() {
this.$refs.content.refresh()
}
}
</script>

<style scoped>

</style>

编辑Cart.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div id="cart">
<NavBar class="cart-nav-bar">
<div slot="center">购物街({{ cartLength }})</div>
</NavBar>
<CartList class="cart-list" :cart-list="cartList" />
</div>
</template>

<script>

import NavBar from "components/common/navbar/NavBar";
import CartList from "./childComps/CartList";

import {mapGetters} from 'vuex'

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

<style scoped>

#cart {
height: 100vh;
position: relative;
}

.cart-nav-bar {
background-color: var(--color-tint);
color: #fff;
}

.cart-list {
overflow: hidden;
position: absolute;
top: 44px;
bottom: 49px;
width: 100%;
}

</style>

底部信息栏

src/views/cart/childComps下创建CartBottomBar

几个注意点:

  • 计算总价格时我们通过filter筛选出选中状态的价格、再使用reduce对价格进行累加
  • every()是对数组中每一项运行给定函数,如果该函数所有一项返回true,则返回true。一旦有一项不满足则返回false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<template>
<div class="bottom-menu">
<div class="checkButton">
<CheckButton class="select-all" :value="isSelectAll" @click.native="checkClick"></CheckButton>
<span class="checkAll">全选</span>
</div>
<span class="total-price">合计: ¥{{totalPrice}}</span>
<span class="buy-product">去计算({{cartLength}})</span>
</div>
</template>

<script>
import CheckButton from "components/contents/checkbutton/CheckButton";

export default {
name: "BottomBar",
components: {
CheckButton
},
computed: {
totalPrice() {
const cartList = this.$store.getters.cartList;
return cartList.filter(item => {
return item.checked
}).reduce((preValue, item) => {
return preValue + item.count * item.price
}, 0).toFixed(2)
},
cartLength() {
return this.$store.state.cartList.filter((item) => item.checked).length
},
isSelectAll() {
if(this.$store.state.cartList.length == 0) return false
return this.$store.state.cartList.every((item)=> item.checked == true)
}
},
methods: {
checkClick(){
if (this.isSelectAll){
this.$store.state.cartList.forEach((item)=> item.checked = false)
}else {
this.$store.state.cartList.forEach((item)=> item.checked = true)
}
}
}
}
</script>

<style scoped>
.bottom-menu {
position: fixed;
left: 0;
right: 0;
bottom: 49px;
display: flex;
height: 44px;
text-align: center;
}

.checkButton {
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
}

.checkButton .checkAll {
line-height: 44px;
margin-left: 5px;
}

.bottom-menu .total-price {
flex: 1;
margin-left: 15px;
font-size: 16px;
color: #666;
line-height: 44px;
text-align: left;
}

.bottom-menu .buy-product {
line-height: 44px;
width: 90px;
float: right;
background-color: red;
color: #fff;
}
</style>

封装Toast

将actions返回Promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
addCart(context, payload) {
return new Promise(((resolve, reject) => {
let oldProduct = context.state.cartList.find(item => item.iid == payload.iid);
if (oldProduct) {
context.commit(ADD_COUNTER, oldProduct)
resolve("当前商品数量+1")
} else {
payload.count = 1
context.commit(ADD_TO_CART, payload)
resolve("添加了此商品")
}
}))
}

由于我们需要在Detail.vueCart.vue中都使用

src/components/common/toast下创建

Toast.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div class="toast" v-show="isShow">
{{ message }}
</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>

<style scoped>

.toast{
transform: translate(-50%, -50%);
position: fixed;
left: 50%;
top: 50%;
z-index: 999;
padding: 8px 15px;
color: #fff;
background-color: rgba(0,0,0,.7);
border-radius: 3px;

}

</style>

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Toast from "./Toast";
const obj = {}
obj.install = function (Vue) {
//创建组件构造器
const toastContrustor = Vue.extend(Toast)
//new的方式,根据组件构造器,可以创建出来一个组件对象
const toast = new toastContrustor()
//将组件对象,手动挂载到某个元素上
toast.$mount(document.createElement('div'))
//toast.$el对应的就是创建的div
document.body.appendChild(toast.$el)

Vue.prototype.$toast = toast
}
export default obj

src/main.js上使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import toast from 'components/common/toast'

Vue.config.productionTip = false

// 全局实例化$bus事件总线
Vue.prototype.$bus = new Vue()

// 注册全局Toast
Vue.use(toast)

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

最后在Detail.vue使用

1
2
3
4
5
6
7
8
9
10
11
12
addToCart(){
const product = {}
product.image = this.banners[0];
product.title = this.goods.title;
product.desc = this.goods.desc;
product.price = this.goods.realPrice;
product.iid = this.iid;

this.$store.dispatch("addCart",product).then((res)=>{
this.$toast.show(res)
})
}

或者修改CartBottomBar.vue添加一个点击事件

1
2
3
4
5
cartClick(){
if (!this.isSelectAll){
this.$toast.show("请选择商品")
}
}

分类页

数据请求

src/network下创建category.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {request} from './request'


export function getCategory(){
return request({
url: '/category'
})
}

export function getSubcategory(maitKey) {
return request({
url: '/subcategory',
params: {
maitKey
}
})
}

export function getCategoryDetail(miniWallkey, type) {
return request({
url: '/subcategory/detail',
params: {
miniWallkey,
type
}
})
}

TabMenu

src/views/category/childComps下创建TabMenu.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<template>
<scroll id="tab-menu">
<div class="menu-list">
<div
class="menu-list-item"
:class="{ active: index === curnettIndex }"
v-for="(item, index) in categories"
:key="index"
@click="itemClick(index)"
>
{{ item.title }}
</div>
</div>
</scroll>
</template>

<script>
import Scroll from "components/common/scroll/Scroll";

export default {
name: "TabMenu",
components: {
Scroll
},
props: {
categories: Array
},
data() {
return {
curnettIndex: 0
};
},
methods: {
itemClick(index) {
this.curnettIndex = index;
this.$emit("selectItem", index);
}
}
};
</script>

<style scoped>
#tab-menu {
background-color: #f6f6f6;
height: 100%;
width: 100px;
box-sizing: border-box;
}

.menu-list-item {
height: 45px;
line-height: 45px;
text-align: center;
font-size: 14px;
}

.menu-list-item.active {
font-weight: 700;
color: var(--color-high-text);
background-color: #fff;
border-left: 3px solid var(--color-high-text);
}
</style>

GridView

src/compoments/common/gridView下创建GridView.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<div class="grid-view" ref="gridView">
<slot></slot>
</div>
</template>

<script>
export default {
name: "GridView",
props: {
cols: {
type: Number,
default: 2
},
hMargin: {
type: Number,
default: 8
},
vMargin: {
type: Number,
default: 8
},
itemSpace: {
type: Number,
default: 8
},
lineSpace: {
type: Number,
default: 8
}
},
mounted: function() {
setTimeout(this._autoLayout, 20);
},

updated: function() {
this._autoLayout();
},

methods: {
_autoLayout: function() {
// 1.获取gridEl和childnet
let gridEl = this.$refs.gridView;
let childnet = gridEl.childnet;
// 2.设置gridEl的内边距
gridEl.style.padding = `${this.vMargin}px ${this.hMargin}px`;
// 3.计算item的宽度
let itemWidth =
(gridEl.clientWidth -
2 * this.hMargin -
(this.cols - 1) * this.itemSpace) /
this.cols;
for (let i = 0; i < childnet.length; i++) {
let item = childnet[i];
item.style.width = itemWidth + "px";
if ((i + 1) % this.cols !== 0) {
item.style.marginRight = this.itemSpace + "px";
}
if (i >= this.cols) {
item.style.marginTop = this.lineSpace + "px";
}
}
}
}
};
</script>

<style scoped>
.grid-view {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
</style>

src/views/category/childComps下创建TabContentCategory.vue用来封装GridView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<div>
<grid-view :cols="3" :lineSpace="15" :v-margin="20" v-if="subcategories.list">
<div class="item" v-for="(item, index) in subcategories.list" :key="index">
<a :href="item.link">
<img class="item-img" :src="item.image" alt="">
<div class="item-text">{{item.title}}</div>
</a>
</div>
</grid-view>
</div>
</template>

<script>
import GridView from 'components/common/gridView/GridView'

export default {
name: "TabContentCategory",
components: {
GridView
},
props: {
subcategories: {
type: Object,
default() {
return []
}
}
}
}
</script>

<style scoped>
.panel img {
width: 100%;
}

.item {
text-align: center;
font-size: 12px;
}

.item-img {
width: 80%;
}

.item-text {
margin-top: 15px;
}
</style>

其他整合

Category.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<template>
<div id="category">
<nav-bar class="nav-bar"><div slot="center">商品分类</div></nav-bar>
<div class="content">
<tab-menu :categories="categories"
@selectItem="selectItem"/>

<scroll id="tab-content"
:data="[categoryData]"
ref="scroll">
<div>
<tab-content-category :subcategories="showSubcategory"/>
<tab-control :titles="['综合', '新品', '销量']"
@itemClick="tabClick"/>
<goods-list :goods="showCategoryDetail"/>
</div>
</scroll>
</div>
</div>
</template>

<script>
import NavBar from 'components/common/navbar/NavBar'

import TabMenu from './childComps/TabMenu'
import TabContentCategory from './childComps/TabContentCategory'


import TabControl from 'components/contents/tabcontrol/TabControl'
import Scroll from 'components/common/scroll/Scroll'
import GoodsList from 'components/contents/good/GoodList'

import {getCategory, getSubcategory, getCategoryDetail} from "network/category";

import {tabControlMixin} from "@/common/mixins";

export default {
name: "Category",
components: {
NavBar,
TabMenu,
TabControl,
Scroll,
TabContentCategory,
GoodsList
},
mixins: [tabControlMixin],
data() {
return {
categories: [],
categoryData: {
},
curnettIndex: -1
}
},
created() {
// 1.请求分类数据
this._getCategory()

// 2.监听图片加载完成
this.$bus.$on('imgLoad', () => {
this.$refs.scroll.refresh()
})
},
computed: {
showSubcategory() {
if (this.curnettIndex === -1) return {}
return this.categoryData[this.curnettIndex].subcategories
},
showCategoryDetail() {
if (this.curnettIndex === -1) return []
return this.categoryData[this.curnettIndex].categoryDetail[this.curnettType]
}
},
methods: {
_getCategory() {
getCategory().then(res => {
// 1.获取分类数据
this.categories = res.data.category.list
// 2.初始化每个类别的子数据
for (let i = 0; i < this.categories.length; i++) {
this.categoryData[i] = {
subcategories: {},
categoryDetail: {
'pop': [],
'new': [],
'sell': []
}
}
}
// 3.请求第一个分类的数据
this._getSubcategories(0)
})
},
_getSubcategories(index) {
this.curnettIndex = index;
const mailKey = this.categories[index].maitKey;
getSubcategory(mailKey).then(res => {
this.categoryData[index].subcategories = res.data
this.categoryData = {...this.categoryData}
this._getCategoryDetail('pop')
this._getCategoryDetail('sell')
this._getCategoryDetail('new')
})
},
_getCategoryDetail(type) {
// 1.获取请求的miniWallkey
const miniWallkey = this.categories[this.curnettIndex].miniWallkey;
// 2.发送请求,传入miniWallkey和type
getCategoryDetail(miniWallkey, type).then(res => {
// 3.将获取的数据保存下来
this.categoryData[this.curnettIndex].categoryDetail[type] = res
this.categoryData = {...this.categoryData}
})
},
/**
* 事件响应相关的方法
*/
selectItem(index) {
this._getSubcategories(index)
}
}
}
</script>

<style scoped>
#category {
height: 100vh;
}

.nav-bar {
background-color: var(--color-tint);
font-weight: 700;
color: #fff;
z-index: 99;
}

.content {
position: absolute;
left: 0;
right: 0;
top: 44px;
bottom: 49px;
display: flex;
overflow: hidden;
}

#tab-content {
height: 100%;
flex: 1;
}
</style>

移动端300ms延迟

安装fastclick插件

1
npm install fastclick --save

使用: index.js

1
2
3
4
5
6
7
//...
import fastclick from 'fastclick'

//...
//移动端 300ms 延迟
fastclick.attach(document.body)
//...

图片懒加载

https://github.com/hilongjw/vue-lazyload

安装

1
npm install --save vue-lazyload

使用:

1
2
3
4
5
import VueLazyload from 'vue-lazyload'

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

在需要懒加载的图片添加v-lazy

px转vw

安装

1
npm install postcss-px-to-viewport -dev--save

根目录新建postcss.config.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
plugins: {
'postcss-px-to-viewport': {
unitToConvert: 'px', //需要转换的单位,默认为"px"
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
viewportHeight: 667,//视窗的高度,根据375设备的宽度来指定,一般指定667,也可以不配置
unitPrecision: 5, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
selectorBlackList: ['ignore', 'tarbar','tab-bar-item'], //指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换`px`
exclude: [/^TabBar/], //忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
}
}
}

总结

虽然跟着视频敲了一边、但是还是有很多不是特别理解的、样式方面还是不是特别熟练