本章概要
- 菜单组件
- 图书分类组件
- 广告图片轮播组件
- 热门推荐组件
- 新书上市组件
- 首页组件
17.3.2 菜单组件
菜单是单独定义的一个组件,本项目的菜单只有一级,如果需要定义多级菜单,可参照 《5.3.1》小节的实现。在 components 目录下新建 Menus.vue 。如下:
Menus.vue
<template>
<div class="menus">
<ul>
<li>
<router-link to="/home">首页</router-link>
</li>
<li>
<router-link to="/newBooks">新书</router-link>
</li>
<li>
<a href="javascript:;">特价书</a>
</li>
<li>
<a href="javascript:;">教材</a>
</li>
<li>
<a href="javascript:;">视听教程</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Menus'
}
</script>
<style scoped>
.menus {
position: relative;
width: 100%;
}
a {
text-decoration: none;
display: block;
color: #fff;
height: 40px;
line-height: 40px;
border: solid 1px #fff;
border-width: 1px 1px 0 0;
background: #255f9e;
}
li {
width: 20%;
list-style-type: none;
float: left;
text-align: center;
position: relative;
}
li a:hover {
color: #fff;
background: #ffb100;
}
</style>
这个组件比较简单,都是静态代码。由于本项目只是用于演示基于 Vue 前端开发涉及的各个功能的实现,所以暂时只提供了首页和新书菜单的实现,其它 3 个(特价书、教材、视听教程)功能的实现是类似的,只需要服务端提供相应的接口即可。
首页和新书菜单组件渲染的位置(即 router-view)在 App.vue 中指定。App.vue 的代码如下:
App.vue
<template>
<div id="app">
<Header />
<Menus />
<router-view />
</div>
</template>
<script>
import Header from '@/components/Header.vue'
import Menus from '@/components/Menus.vue';
export default {
components: {
Header,
Menus
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
width: 1200px;
}
</style>
本项目没有用到嵌套路由,所有页面级路由组件的渲染都是在这里。换句话说,即所有渲染的页面都有头部和菜单。
17.3.3 图书分类组件
图书分类组件用于显示商品的分类,每个分类都是一个链接,单击链接将跳转到展示该分类下所有商品的页面。
在 components 目录下新建 HomeCategory.vue。如下:
HomeCategory.vue
<template>
<div class="category">
<h3>图书分类</h3>
<div v-for="category in categorys" :key="category.id">
<h5>{{ category.name }}</h5>
<router-link v-for="child in category.children" :key="child.id" :to="'/category' + child.id">{{ child.name
}}</router-link>
</div>
</div>
</template>
<script>
export default {
name: 'HomeCategory',
data() {
return {
categories: []
}
},
created() {
this.axios.get('/category').then(res => {
if(res.status == 200){
this.categories = res.data;
}
}).catch(err => {
console.log(err)
})
}
}
</script>
<style scoped>
.scrollPic {
width: 610px;
height: 220px;
float: left;
margin: 10px 50px auto 50px;
}
.scrollPic img {
width: 610px;
height: 220px;
}
</style>
在 created 生命周期钩子中向服务端请求所有分类数据。服务端提供的该数据的内容如下:
http://111.229.37.167/api/category
[{
"id": 1,
"name": "Java EE",
"root": true,
"parentId": null,
"children": [{
"id": 3,
"name": "Servlet/JSP",
"root": false,
"parentId": 1,
"children": []
}, {
"id": 4,
"name": "应用服务器",
"root": false,
"parentId": 1,
"children": []
}, {
"id": 5,
"name": "MVC框架",
"root": false,
"parentId": 1,
"children": []
}]
}, {
"id": 2,
"name": "程序设计",
"root": true,
"parentId": null,
"children": [{
"id": 6,
"name": "C/C++",
"root": false,
"parentId": 2,
"children": [{
"id": 9,
"name": "C11",
"root": false,
"parentId": 6,
"children": []
}]
}, {
"id": 7,
"name": "Java",
"root": false,
"parentId": 2,
"children": []
}, {
"id": 8,
"name": "C#",
"root": false,
"parentId": 2,
"children": []
}]
}]
子分类是放到 children 数组属性中的,本项目中未用到 root 和 parentId 属性,前者可用于列出某个根分类下的所有商品,后者可以用于查找某个分类的父分类,甚至反向查找所有上级分类。
清楚了数据接口返回的数据结构,那么 HomeCategory 组件的代码也就清楚了。
17.3.4 广告图片轮播组件
广告图片轮播功能在电商网站属于标配的功能,其实是通过 JavaScript 代码控制图片的轮播,并处理一些控制图片显示的单击事件。
由于 Vue 3.0 推出的时间还不是特别长,之前 Vue 2.x 下的很多好用的图片轮播插件还没有移植到 Vue 3.0 下,如果自己编写一个成熟的图片轮播组件,又会增加本项目的复杂度,因此这里暂时先用一张静止的图片代替图片轮播。如果对图片轮播功能的实现有兴趣,可以在网上找到很多案例,将其封装为组件使用即可。(不过,此时网上应该已经有支持 Vue 3.0 的图片轮播插件了)。
在 components 目录下新建 HomeScrollPic.vue 。如下:
HomeScrollPic.vue
<template>
<div class="scrollpic">
<img src="/p1.jpg" style="width: 610px; height: 220px;" />
</div>
</template>
<script>
export default {};
</script>
<style scoped>
.scrollPic {
width: 610px;
height: 220px;
float: left;
margin: 10px 50px auto 50px;
}
.scrollPic img {
width: 610px;
height: 220px;
}
</style>
图片是保存在 public 目录下的,该目录下的资源直接通过根路径“/”引用即可。
17.3.5 热门推荐组件
热门推荐组件用于显示热门商品,用户如果对某一热门商品感兴趣,可以单击该商品链接,进入商品详情页面。
在 components 目录下新建 HomeBooksHot.vue 。如下:
HomeBooksHot.vue
<template>
<div class="bookRecommend">
<h3>热门推荐</h3>
<ul>
<li v-for="book in books" :key="book.id">
<router-link :to="'/book/' + book.id">
{{ book.title }}
<span>{{ currency(factPrice(book.price, book.discount))}}</span>
</router-link>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'HomeBooksHot',
data() {
return {
books: []
}
},
inject: ['factPrice', 'currency'],
created() {
this.axios.get('/book/hot').then(res => {
if (res.status == 200) {
this.books = res.data
}
}).catch(err => {
console.log(err)
})
}
}
</script>
<style scoped>
.bookRecommend {
width: 18%;
height: 220px;
border: solid 1px #ccc;
float: left;
margin-top: 10px;
}
.bookRecommend li {
list-style: none;
text-align: left;
padding-left: 0;
margin-left: -20px;
}
</style>
在 created 声明周期钩子中向服务端请求热门商品数据。服务端提供的该数据接口如下:
http://111.229.37.167/api/book/hot
返回的数据结构如下:
[
{
"id":1,
"title":" VC++深入详解(第3版)",
"author":"孙鑫",
"price":168,
"discount":0.95,
"bookConcern":null,
"imgUrl":"/api/img/vc++.jpg",
"bigImgUrl":"/api/img/vc++big.jpg",
"publishDate":null,
"brief":null,
"inventory":1000
},
{
"id":2,
"title":"Java编程思想",
"author":"Bruce Eckel",
"price":108,
"discount":0.5,
"bookConcern":null,
"imgUrl":"/api/img/javathink.jpg",
"bigImgUrl":"/api/img/javathinkbig.jpg",
"publishDate":null,
"brief":null,
"inventory":500
},
{
"id":3,
"title":"C Primer Plus 第6版",
"author":"Stephen Prata",
"price":89,
"discount":0.5,
"bookConcern":null,
"imgUrl":"/api/img/c++primer.jpg",
"bigImgUrl":"/api/img/c++primerbig.jpg",
"publishDate":null,
"brief":null,
"inventory":400
},
{
"id":4,
"title":"Servlet/JSP深入详解",
"author":"孙鑫",
"price":139,
"discount":0.9,
"bookConcern":null,
"imgUrl":"/api/img/jsp.jpg",
"bigImgUrl":"/api/img/jspbig.jpg",
"publishDate":null,
"brief":null,
"inventory":1000
}
]
实际上,热门推荐组件用不到全部信息,只是服务端的数据接口返回的数据就是如此,那么从这些数据中选择游泳的数据使用即可。
一般电商网站的商品有定价和实际销售价格,在前端展示商品的时候需要同时显示这两种价格。从这里返回的数据来看,服务端只提供了商品的定价和折扣,并没有实际销售价格,那么实际销售价格就需要我们自己来处理。这在实际开发中也很常见,不能期望服务端的开发人员专门为你(当然老板除外)的需求提供一个接口,也许还有其他前端也要用到该接口。
实际价格是定价与折扣相乘得到的,由于实际价格在多处要用到,因此编写一个单独的函数来计算价格。此外,还要考虑价格显示问题,价格只是显示到分就可以了,而在计算过程中,由于是浮点数,可能会出现小数点后两位之后的数据,所以要进行处理。除此之外,价格一般还会加上货币符号,如国内会加上人民币符号¥。为此,再编写一个函数,专门负责价格的格式化问题。
将这两个函数放到单独的 JS 文件中,在 src 目录下新建 utils 文件夹,在该文件夹下新建 util.js。如下:
util.js
const digitsRE = /(\d{3})(?=\d)/g
export function factPrice(value, discount) {
value = parseFloat(discount);
if (!discount) {
return value
}
return value * discount;
}
export function currency(value, currency, decimals) {
value = parseFloat(value);
if (!isFinite(value) || (!value && value !== 0)) {
return ''
}
currency = currency != null ? currency : '¥';
decimals = decimals != null ? decimals : 2;
var stringified = Math.abs(value).toFixed(decimals);
var _int = decimals ? stringified.slice(0, -1 - decimals) : stringified;
var i = _int.length % 3;
var head = i > 0 ? (_int.slice(0, i) + (_int.length > 3 ? ',' : '')) : '';
var _float = decimals ? stringified.slice(-1 - decimals) : '';
var sign = value < 0 ? '-' : '';
return sign + currency + head + _int.slice(i).replace(digitsRE, '$1,') + _float
}
为了方便在各个组件内使用这两个函数,在 App 组件内通过 provide 选项向所有后代组件提供这两个函数。编辑 App.vue ,修改后的代码如下:
App.vue
<template>
<div id="app">
<Header />
<Menus />
<router-view />
</div>
</template>
<script>
import Header from '@/components/Header.vue'
import Menus from '@/components/Menus.vue';
import { factPrice,currency } from './utils/util';
export default {
components: {
Header,
Menus
},
provide(){
return {
factPrice,
currency
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
width: 1200px;
}
</style>
之后记得在组件内使用 inject 选项注入这两个函数,如下:
inject:['factPrice','currency']
17.3.6 新书上市组件
新书上市组件用于显示刚上市的商品,用户如果对某一商品感兴趣,可以单击该商品链接,进入商品详情页面。
在 components 目录下新建 BooksNew.vue,由于该组件会被复用,所以这里没有使用主页的前缀 Home。
BooksNew.vue
<template>
<div class="booksNew">
<h3>新书上市</h3>
<div class="book" v-for="book in books" :key="book.id">
<figure>
<router-link :to="'/book/' + book.id">
<img :src="book.imgUrl">
<figcaption>{{ book.title }}</figcaption>
</router-link>
</figure>
<p>
{{ currency(factPrice(book.price, book.discount)) }}
<span>{{ currency(book.price) }}</span>
</p>
</div>
</div>
</template>
<script>
export default {
name: '',
props: [''],
data() {
return {
books: []
}
},
inject: ['factPrice', 'currency'],
created() {
this.axios.get("/book/new").then(res => {
if (res.status == 200) {
this.loading = false;
this.books = res.data;
}
}).catch(err => {
console.log(err)
})
}
}
</script>
<style scoped>
.booksNew {
float: left;
}
.booksNew .book {
display: inline-block;
width: 19%;
border-right: solid 1px #ccc;
margin-left: 10px;
}
.booksNew a:hover {
color: red;
}
.booksNew img {
width: 120px;
height: 100px
}
.booksNew span {
color: #cdcdcd;
text-decoration: line-through;
}
</style>
在 created 生命周期钩子中向服务端请求新书的数据。服务端提供的该数据接口如下:
http://111.229.37.167/api/book/new
返回的数据形式同 /book/hot。
17.3.7 首页组件
首页的各个组成部分编写完成后,就可以开始集成这几个部分了。首页作为页面级组件,放到 views 目录下。在 views 目录下新建 Home.vue 。如下:
src/views/Home.vue
<template>
<div class="home">
<HomeCategory />
<HomeScrollPic />
<HomeBooksHot />
<BooksNew />
</div>
</template>
<script>
import HomeCategory from '@/components/HomeCategory.vue'
import HomeScrollPic from '@/components/HomeScrollPic.vue';
import HomeBooksHot from '@/components/HomeBooksHot.vue';
import BooksNew from '@/components/BooksNew.vue';
export default {
name: 'home',
components: {
HomeCategory,
HomeScrollPic,
HomeBooksHot,
BooksNew
}
}
</script>
<style scoped>
.home {
float: left;
text-align: center;
width: 100%;
}
[v-cloak] {
display: none;
}
</style>
Home 组件比较简单,只是用于拼接各个子组件
其它,后面启动的时候可能会出现以下命名规则错误
修改 .eslintrc.js,如下:
//在rules中添加自定义规则
//关闭组件命名规则
"vue/multi-word-component-names": "off",