十七、网上商城项目(3)

文章详细介绍了在Vue.js项目中创建商品列表组件,包括商品列表项组件和父组件的商品列表组件,以及使用Vuex处理购物车操作。同时,讨论了分类商品和搜索结果页面的实现,它们共用同一个组件并根据路由动态调整。此外,还展示了如何创建一个Loading组件来处理数据加载时的用户体验。
摘要由CSDN通过智能技术生成

本章概要

  • 商品列表
    • 商品列表项组件
    • 商品列表组件
  • 分类商品和搜索结果页面
    • Loading 组件
    • Books 组件

17.4 商品列表

商品列表页面以列表形式显示所有商品,将商品列表和商品列表项分别定义为单独的组件,商品列表组件作为父组件在其内部循环渲染商品列表项子组件。

17.4.1 商品列表项组件

在 components 目录下新建 BookLisItem.vue。如下:

BookLisItem.vue

<template>
    <div class="bookListItem">
        <div>
            <img :src="item.bigImgUrl" />
        </div>
        <p class="title">
            <router-link :to="{ name: 'book', params: { id: item.id } }" target="_blank">
                {{ item.title }}
            </router-link>
        </p>
        <p>
            <span class="factPrice">
                {{ currency(factPrice(item.price, item.discount)) }}
            </span>
            <span>
                定价:<i class="price">{{ currency(item.price) }}</i>
            </span>
        </p>
        <p>
            <span>{{ item.author }}</span>
            <span>{{ item.publishDate }}</span>
            <span>{{ item.bookConcern }}</span>
        </p>
        <p>
            {{ item.brief }}
        </p>
        <p>
            <button class="addCartButton" @click="addCartItem(item)">
                加入购物车
            </button>
        </p>
    </div>
</template>
<script>
import { mapActions } from 'vuex';

export default {
    name: 'BookListItem',
    props: {
        item: {
            type: Object,
            default: () => { }
        }
    },
    methods: {
        ...mapActions('cart', {
            // 将 this.addCart() 映射为 this.$store.commit('cart/addProductToCart')
            addCart: 'addProductToCart'
        }),
        factPrice(price, discount) {
            return price * discount;
        },
        addCartItem(item) {
            let quantity = 1;
            let newItem = {
                ...item,
                price: this.factPrice(item.price, item.discount),
                quantity
            };
            this.addCart(newItem);
            this.$router.push("/cart")
        }
    }
}
</script>
<style scoped>
.bookListItem {
    border-bottom: solid 1px #ccc;
    margin-top: 10px;
    margin-left: 30px;
    margin-right: 30px;
}

.bookListItem p {
    text-align: left;
}

.bookListItem p span {
    padding-left: 10px;
}

.bookListItem img {
    width: 180px;
    height: 180px;
    float: left;
}

.addCartButton {
    padding: 5px 10px 6px;
    color: #fff;
    border: none;
    border-bottom: solid 1px #222;
    border-radius: 5px;
    box-shadow: 0 1px 3px #999;
    text-shadow: 0 -1px 3px #444;
    cursor: pointer;
    background-color: #e33100;
}

.addCartButton:hover {
    text-shadow: 0 1px 1px #444;
}

.bookListItem .price {
    color: #cdcdcd;
    text-decoration: line-through;
}

.bookListItem .factPrice {
    color: red;
    font-weight: bold;
}

.bookListItem .title {
    color: #e00;
}
</style>

说明:
在这里插入图片描述

to 属性使用了表达式,因此要用 v-bind 指令(这里使用的是简写语法)进行绑定。params 和 path 字段不能同时存在,如果使用了 path 字段,那么 params 将被忽略,所以这里使用命名路由。当然,也可以采用前面例子中拼接路径字符串的方式。
在这里插入图片描述

router-link 默认渲染为 a 标签,所有路由的跳转都是在当前浏览器窗口中完成的,但有时希望在新的浏览器窗口中打开目标页面,那么可以使用 target=“_blank”。但要注意,如果使用 v-slot API 定制 router-link ,将其渲染为其它标签,那么就不能使用 a 标签的target 属性,只能编写单击事件响应代码,然后通过 window.open() 方法打开一个新的浏览器窗口。
在这里插入图片描述

BookListItem 组件需要的商品数据是由父组件通过 prop 传进来的,所以这里定义了一个 item prop。
在这里插入图片描述

单击“加入购物车”按钮时,会调用 addCartItem() 方法将该商品加入购物车中,由于购物车中的商品不需要商品的定价,所以这里先计算出商品的实际价格。
在这里插入图片描述

购物车中保存的每种商品都有一个数量,通过 quantity 字段表示,在商品列表项页面中的“加入购物车”功能是一种便捷方式,商品的数量默认为 1,后面会看到商品详情页面中加入任意数量商品功能的实现。
在这里插入图片描述

在添加商品到购物车中后,路由跳转到购物车页面,这也是电商网站通常采用的方式,可以刺激用户的冲动消费。

17.4.2 商品列表组件

商品列表组件作为商品列表项组件的父组件,负责为商品列表项组件提供商品数据,并通过 v-for 指令循环渲染商品列表项组件。
在 components 目录下新建 BookList.vue 。如下:

BookList.vue

<template>
    <div>
        <div v-for="book in list" :key="book.id">
            <BookListItem :item="book" />
        </div>
    </div>
</template>

<script>
import BookListItem from './BookLisItem.vue';
export default {
    name: 'BookList',
    props: {
        list: {
            type: Array,
            default: () => []
        }
    },
    components: {
        BookListItem
    }
}

</script>

BookList 组件的代码比较简单,主要就是通过 v-for 命令循环渲染 BookListItem 子组件。某些项目的实现是在列表组件中向服务端请求数据渲染列表项,但在本项目中,BookList 组件会被多个页面复用,并且请求的数据接口是不同的,因此 BookList 组件仅仅是定义了一个 list prop 用来接收父组件传递进来的商品列表数据。

17.5 分类商品和搜索结果页面

单击某个分类链接,将跳转到分类商品页面,在该页面下,将以列表形式列出该分类下的所有商品信息;当搜索框中输入某个关键字,单击“搜索”按钮后,将跳转到搜索结果页面,在该页面下,也是以列表形式列出匹配关键字的所有商品信息。
既然这两个页面都是以列表形式显示商品信息,那么可以将他们合并为一个页面组件来实现,在该页面中无非就是根据路由的路径来动态切换页面标题,以及向服务端请求不同的数据接口。
先给出这两个页面的路由配置,编辑 router 目录下的 index.js 文件。如下:

router/index.js

...
const routes = [
  {
    path: '/',
    redirect: {
      name: 'home'
    }
  },
  {
    path: '/home',
    name: 'home',
    meta: {
      title: '首页'
    },
    component: Home
  },
  {
    path: '/category/:id',
    name: 'category',
    meta: {
      title: '图书分类'
    },
    component: () => import('../views/Books.vue')
  },
  {
    path: '/search',
    name: 'search',
    meta: {
      title: '搜索结果'
    },
    component: () => import('../views/Books.vue')
  }
]

routes.afterEach((to) => {
  document.title = to.meta.title;
})
...

在路由配置中,采用的是延迟加载路由的方式,只有在路由到该组件时才加载。关于延时加载路由,可以参看 14.14 节。
将分类图书(/category/:id)和搜索结果(/search)的导航链接对应到同一个目标路由组件 Books 上,同时根据 14.10.1 小节介绍的知识,利用全局后置钩子来为路由跳转后的页面设置标题。

17.5.1 Loading 组件

考虑到图书列表的数据是从服务端去请求数据及网络状况的原因,图书列表的显示可能会有延迟,为此,决定编写一个 Loading 组件,在图书列表数据还没有渲染时,给用户一个提示,让用户稍安勿躁。
在 10.9 节中,已经给出了一个使用 loading 图片实现加载提示的示例,也可以沿用该示例实现加载提示。在这里换一种实现方式,考虑到图片本身加载也需要时间(虽然 loading 图片一般都很小),采用 CSS 实现 loading 加载的动画效果,这种实现在网上有很多,本项目从中找了一个实现,并将其封装为组件。
在 components 目录下新建 Loading.vue 。如下:

Loading.vue

<template>
    <div class="loading">
        <div class="shadow">
            <div class="loader">
                <div class="mask"></div>
            </div>
        </div>
    </div>
</template>
  
<script>
export default {
    name: "Loading",
};
</script>
<style scoped>
.shadow {
    position: absolute;
    top: 50%;
    left: 50%;
    border-radius: 50%;
    margin-top: -50px;
    margin-left: -50px;
    box-shadow: -2px 2px 10px 0 rgba(0, 0, 0, 0.5),
        2px -2px 10px 0 rgba(255, 255, 255, 0.5);
}

.loader {
    background: -webkit-linear-gradient(left,
            skyblue 50%,
            #fafafa 50%);
    /* Foreground color, Background color */
    border-radius: 100%;
    height: 100px;
    /* Height and width */
    width: 100px;
    /* Height and width */
    animation: time 8s steps(500, start) infinite;
}

.mask {
    border-radius: 100% 0 0 100% / 50% 0 0 50%;
    height: 100%;
    left: 0;
    position: absolute;
    top: 0;
    width: 50%;
    animation: mask 8s steps(250, start) infinite;
    transform-origin: 100% 50%;
}

@keyframes time {
    100% {
        transform: rotate(360deg);
    }
}

@keyframes mask {
    0% {
        background: #fafafa;
        /* Background color */
        transform: rotate(0deg);
    }

    50% {
        background: #fafafa;
        /* Background color */
        transform: rotate(-180deg);
    }

    50.01% {
        background: skyBlue;
        /* Foreground color */
        transform: rotate(0deg);
    }

    100% {
        background: skyBlue;
        /* Foreground color */
        transform: rotate(-180deg);
    }
}
</style>

主要代码就是 CSS 的样式规则,没必要去深究具体的实现细节,当然想研究 CSS 如何实现该种动画效果另当别论。

17.5.2 Books 组件

有了 Loading 组件,接下来就可以开始编写 Books 组件了。在 views 目录下新建 Books.vue 。如下:

views/Books.vue

<template>
    <div>
        <Loading v-if="loading" />
        <h3 v-else>{{ title }}</h3>
        <BookList :list="books" v-if="books.length" />
        <h1>{{ message }}</h1>
    </div>
</template>

<script>
import BookList from "@/components/BookList.vue";
import Loading from "@/components/Loading.vue";
export default {
    name: 'Books',
    data() {
        return {
            title: '',
            books: [],
            message: '',
            loading: true
        }
    },
    beforeRouteEnter(to, from, next) {
        next(vm => {
            vm.title = to.meta.title;
            let url = vm.setRequestUrl(to.fullPath);
            vm.getBooks(url);
        })
    },
    beforeRouteUpdate(to) {
        let url = this.setRequestUrl(to.fullPath);
        this.getBooks(url);
        return true;
    },
    components: {
        BookList,
        Loading
    },
    methods: {
        getBooks(url) {
            this.message = '';
            this.axios.get(url).then(res => {
                if (res.status == 200) {
                    this.loading = false;
                    this.books = res.data;
                    if (this.books.length === 0) {
                        if (this.$route.name === "category") {
                            this.message = "当前分类下没有图书!"
                        } else {
                            this.message = "没有搜索到匹配的图书";
                        }
                    }
                }
            }).catch(err => {
                alert(err)
            })
        },
        // 动态设置服务端数据接口的请求 URL
        setRequestUrl(path) {
            let url = path;
            if (path.indexOf("/category") != -1) {
                url = "/book" + url;
            }
        }
    }
}

</script>

说明:
在这里插入图片描述

为了控制 Loading 组件的显示与删除,定义一个数据属性 loading ,其默认值为 true,然后使用 v-if 指令进行条件判断。当成功接收到服务端发回的数据时,将数据属性 loading 设置为 false,这样 v-if 指令就会删除 Loading 组件。
在这里插入图片描述

因为分类商品和搜索结果使用的是同一个组件,但是向服务端请求的数据接口是不同的,分类商品请求的接口是 /book/category/6,而搜索请求的接口是 /search?wd=keyword,为此定义了 setRequestUrl 方法动态设置请求的接口 URL。
在这里插入图片描述

判断目标路由有多种方式,可以在导航守卫中通过 to.path 或 to.fullPath 判断,也可以使用 this.route.path 和 this.$route.fullPath 判断,如果在路由配置中使用了命名路由,还可以使用 this.route.name 判断,如本例所示。
在这里插入图片描述

在组件内导航守卫 beforeRouteEnter() 中请求初次渲染的数据,当然也可以利用 created 生命周期钩子完成相同的功能。
在这里插入图片描述

由于搜索框是独立的,用户可能会多次进行搜索行为,所以使用组件内守卫 beforeRouteUpdate() ,在组件被复用的时候再次请求数据。
在这里插入图片描述

BookList 组件所需要的数据是通过 list prop 传进去的,由于父子组件生命周期的调用时机问题,可能会出现子组件已经 mounted ,而父组件的数据才传过去,导致子组件不能正常渲染,为此可以添加一个 v-if 指令,使用列表数据的长度作为条件判断,确保子组件能正常接收到数据并渲染。在本项目使用的 Vue.js 版本和采用的实现方式下,不添加 v-if 指令也能正常工作,如果以后遇到子组件的列表数据不能正常渲染,可以试试这种解决方案。

Books 组件的渲染效果与 BookList 组件渲染的效果是类似的,只是多了一个标题,以及在没有请求到数据时给出的一个提示信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一只小熊猫呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值