菜单是单独定义的一个组件,本项目的菜单只有一级,如果需要定义多级菜单,可参照 《5.3.1》小节的实现。在 components 目录下新建 Menus.vue 。如下:
Menus.vue
这个组件比较简单,都是静态代码。由于本项目只是用于演示基于 Vue 前端开发涉及的各个功能的实现,所以暂时只提供了首页和新书菜单的实现,其它 3 个(特价书、教材、视听教程)功能的实现是类似的,只需要服务端提供相应的接口即可。
首页和新书菜单组件渲染的位置(即 router-view)在 App.vue 中指定。App.vue 的代码如下:
App.vue
本项目没有用到嵌套路由,所有页面级路由组件的渲染都是在这里。换句话说,即所有渲染的页面都有头部和菜单。
图书分类组件用于显示商品的分类,每个分类都是一个链接,单击链接将跳转到展示该分类下所有商品的页面。
在 components 目录下新建 HomeCategory.vue。如下:
HomeCategory.vue
图书分类
{{ category.name }}
{{ child.name
}}
在 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 组件的代码也就清楚了。
广告图片轮播功能在电商网站属于标配的功能,其实是通过 JavaScript 代码控制图片的轮播,并处理一些控制图片显示的单击事件。
由于 Vue 3.0 推出的时间还不是特别长,之前 Vue 2.x 下的很多好用的图片轮播插件还没有移植到 Vue 3.0 下,如果自己编写一个成熟的图片轮播组件,又会增加本项目的复杂度,因此这里暂时先用一张静止的图片代替图片轮播。如果对图片轮播功能的实现有兴趣,可以在网上找到很多案例,将其封装为组件使用即可。(不过,此时网上应该已经有支持 Vue 3.0 的图片轮播插件了)。
在 components 目录下新建 HomeScrollPic.vue 。如下:
HomeScrollPic.vue
图片是保存在 public 目录下的,该目录下的资源直接通过根路径“/”引用即可。
热门推荐组件用于显示热门商品,用户如果对某一热门商品感兴趣,可以单击该商品链接,进入商品详情页面。
在 components 目录下新建 HomeBooksHot.vue 。如下:
HomeBooksHot.vue
热门推荐
-
{{ book.title }}
{{ currency(factPrice(book.price, book.discount))}}
在 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']
新书上市组件用于显示刚上市的商品,用户如果对某一商品感兴趣,可以单击该商品链接,进入商品详情页面。
在 components 目录下新建 BooksNew.vue,由于该组件会被复用,所以这里没有使用主页的前缀 Home。
BooksNew.vue
新书上市
{{ currency(factPrice(book.price, book.discount)) }}
{{ currency(book.price) }}
在 created 生命周期钩子中向服务端请求新书的数据。服务端提供的该数据接口如下:
http://111.229.37.167/api/book/new
返回的数据形式同 /book/hot。
首页的各个组成部分编写完成后,就可以开始集成这几个部分了。首页作为页面级组件,放到 views 目录下。在 views 目录下新建 Home.vue 。如下:
src/views/Home.vue
Home 组件比较简单,只是用于拼接各个子组件
其它,后面启动的时候可能会出现以下命名规则错误
修改 .eslintrc.js,如下:
//在rules中添加自定义规则
//关闭组件命名规则
"vue/multi-word-component-names": "off",